From 6aae1d787f1ac7c8fefe344a2f72d1914bdc819a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Feb 2026 14:30:16 -0400 Subject: [PATCH 001/193] Fix unpleasant string chopping --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 5846e1951..407695b06 100644 --- a/.clang-format +++ b/.clang-format @@ -22,7 +22,7 @@ Cpp11BracedListStyle: 'true' KeepEmptyLinesAtTheStartOfBlocks: 'true' NamespaceIndentation: Inner CompactNamespaces: 'true' -PenaltyBreakString: '3' +PenaltyBreakString: '1000' SpaceBeforeParens: ControlStatements SpacesInAngles: 'false' SpacesInContainerLiterals: 'false' From 3831d05433309fc72d7f99545beade1b64f4641c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 18:08:56 -0300 Subject: [PATCH 002/193] Refactor internal dependencies to use session-deps session-deps is a new repo that consolidates how we handle loading and doing static bundle builds of various common external dependencies across Session projects. --- .gitmodules | 9 +- CMakeLists.txt | 19 +--- cmake/StaticBuild.cmake | 224 ------------------------------------- cmake/session-deps | 1 + external/CMakeLists.txt | 70 ++++++------ external/simdutf | 1 - external/zstd | 1 - src/CMakeLists.txt | 7 +- tests/test_compression.cpp | 10 +- 9 files changed, 50 insertions(+), 292 deletions(-) delete mode 100644 cmake/StaticBuild.cmake create mode 160000 cmake/session-deps delete mode 160000 external/simdutf delete mode 160000 external/zstd diff --git a/.gitmodules b/.gitmodules index a029c2087..79febb6fa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,18 +7,15 @@ [submodule "external/ios-cmake"] path = external/ios-cmake url = https://github.com/leetal/ios-cmake -[submodule "external/zstd"] - path = external/zstd - url = https://github.com/facebook/zstd.git [submodule "external/protobuf"] path = external/protobuf url = https://github.com/protocolbuffers/protobuf.git [submodule "external/session-router"] path = external/session-router url = https://github.com/session-foundation/session-router.git -[submodule "external/simdutf"] - path = external/simdutf - url = https://github.com/simdutf/simdutf.git [submodule "external/date"] path = external/date url = https://github.com/HowardHinnant/date.git +[submodule "cmake/session-deps"] + path = cmake/session-deps + url = https://github.com/session-foundation/session-deps.git diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6190fff..b95d0fd9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,8 @@ else() set(static_default ON) endif() -option(BUILD_STATIC_DEPS "Build all dependencies statically rather than trying to link to them on the system" ${static_default}) +include(cmake/session-deps/Deps.cmake) + option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${static_default}) if(BUILD_SHARED_LIBS OR libsession_IS_TOPLEVEL_PROJECT) @@ -121,26 +122,12 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(external) if(ENABLE_NETWORKING) - if(NOT TARGET nettle::nettle) - if(BUILD_STATIC_DEPS) - message(FATAL_ERROR "Internal error: nettle::nettle target (expected via libquic BUILD_STATIC_DEPS) not found") - else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(NETTLE REQUIRED IMPORTED_TARGET nettle) - add_library(nettle INTERFACE) - target_link_libraries(nettle INTERFACE PkgConfig::NETTLE) - add_library(nettle::nettle ALIAS nettle) - endif() - endif() + session_dep(nettle 3) endif() add_subdirectory(src) add_subdirectory(proto) -if (BUILD_STATIC_DEPS) - include(StaticBuild) -endif() - if(STATIC_BUNDLE) include(combine_archives) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake deleted file mode 100644 index 49863f8c8..000000000 --- a/cmake/StaticBuild.cmake +++ /dev/null @@ -1,224 +0,0 @@ -# cmake bits to do a full static build, downloading and building all dependencies. - -# Most of these are CACHE STRINGs so that you can override them using -DWHATEVER during cmake -# invocation to override. - -set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") - -include(ExternalProject) - -set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) -set(DEPS_SOURCEDIR ${CMAKE_BINARY_DIR}/static-deps-sources) - -file(MAKE_DIRECTORY ${DEPS_DESTDIR}/include) - -add_library(libsession-external-libs INTERFACE IMPORTED GLOBAL) -target_include_directories(libsession-external-libs SYSTEM BEFORE INTERFACE ${DEPS_DESTDIR}/include) - -set(deps_cc "${CMAKE_C_COMPILER}") -set(deps_cxx "${CMAKE_CXX_COMPILER}") - - -function(expand_urls output source_file) - set(expanded) - foreach(mirror ${ARGN}) - list(APPEND expanded "${mirror}/${source_file}") - endforeach() - set(${output} "${expanded}" PARENT_SCOPE) -endfunction() - -function(add_static_target target ext_target libname) - add_library(${target} STATIC IMPORTED GLOBAL) - add_dependencies(${target} ${ext_target}) - target_link_libraries(${target} INTERFACE libsession-external-libs) - set_target_properties(${target} PROPERTIES - IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname} - ) - if(ARGN) - target_link_libraries(${target} INTERFACE ${ARGN}) - endif() - libsession_static_bundle(${target}) -endfunction() - - - -set(cross_host "") -set(cross_rc "") -if(CMAKE_CROSSCOMPILING) - if(APPLE AND NOT ARCH_TRIPLET AND APPLE_TARGET_TRIPLE) - set(ARCH_TRIPLET "${APPLE_TARGET_TRIPLE}") - endif() - set(cross_host "--host=${ARCH_TRIPLET}") - if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) - set(cross_rc "WINDRES=${CMAKE_RC_COMPILER}") - endif() -endif() -if(ANDROID) - set(android_toolchain_suffix linux-android) - set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) - if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) - set(cross_host "--host=x86_64-linux-android") - set(android_compiler_prefix x86_64) - set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) - set(android_toolchain_prefix x86_64) - set(android_toolchain_suffix linux-android) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86) - set(cross_host "--host=i686-linux-android") - set(android_compiler_prefix i686) - set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) - set(android_toolchain_prefix i686) - set(android_toolchain_suffix linux-android) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a) - set(cross_host "--host=armv7a-linux-androideabi") - set(android_compiler_prefix armv7a) - set(android_compiler_suffix linux-androideabi${ANDROID_PLATFORM_LEVEL}) - set(android_toolchain_prefix arm) - set(android_toolchain_suffix linux-androideabi) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a) - set(cross_host "--host=aarch64-linux-android") - set(android_compiler_prefix aarch64) - set(android_compiler_suffix linux-android${ANDROID_PLATFORM_LEVEL}) - set(android_toolchain_prefix aarch64) - set(android_toolchain_suffix linux-android) - else() - message(FATAL_ERROR "unknown android arch: ${CMAKE_ANDROID_ARCH_ABI}") - endif() - set(deps_cc "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang") - set(deps_cxx "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang++") - set(deps_ld "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_toolchain_suffix}-ld") - set(deps_ranlib "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ranlib") - set(deps_ar "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ar") -endif() - -set(deps_CFLAGS "-O2") -set(deps_CXXFLAGS "-O2") - -if(CMAKE_C_COMPILER_LAUNCHER) - set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") -endif() -if(CMAKE_CXX_COMPILER_LAUNCHER) - set(deps_cxx "${CMAKE_CXX_COMPILER_LAUNCHER} ${deps_cxx}") -endif() - -if(WITH_LTO) - set(deps_CFLAGS "${deps_CFLAGS} -flto") -endif() - -if(APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET) - if(SDK_NAME) - set(deps_CFLAGS "${deps_CFLAGS} -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - set(deps_CXXFLAGS "${deps_CXXFLAGS} -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - else() - set(deps_CFLAGS "${deps_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - set(deps_CXXFLAGS "${deps_CXXFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - endif() -endif() - -if(_winver) - set(deps_CFLAGS "${deps_CFLAGS} -D_WIN32_WINNT=${_winver}") - set(deps_CXXFLAGS "${deps_CXXFLAGS} -D_WIN32_WINNT=${_winver}") -endif() - - -if("${CMAKE_GENERATOR}" STREQUAL "Unix Makefiles") - set(_make $(MAKE)) -else() - set(_make make) -endif() - - -# Builds a target; takes the target name (e.g. "readline") and builds it in an external project with -# target name suffixed with `_external`. Its upper-case value is used to get the download details -# (from the variables set above). The following options are supported and passed through to -# ExternalProject_Add if specified. If omitted, these defaults are used: -set(build_def_DEPENDS "") -set(build_def_PATCH_COMMAND "") -set(build_def_CONFIGURE_COMMAND ./configure ${cross_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic - "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}" "CXXFLAGS=${deps_CXXFLAGS}" ${cross_rc}) -set(build_def_CONFIGURE_EXTRA "") -set(build_def_BUILD_COMMAND ${_make}) -set(build_def_INSTALL_COMMAND ${_make} install) -set(build_def_BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/lib___TARGET___.a ${DEPS_DESTDIR}/include/___TARGET___.h) - -function(build_external target) - set(options DEPENDS PATCH_COMMAND CONFIGURE_COMMAND CONFIGURE_EXTRA BUILD_COMMAND INSTALL_COMMAND BUILD_BYPRODUCTS) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "${options}") - foreach(o ${options}) - if(NOT DEFINED arg_${o}) - set(arg_${o} ${build_def_${o}}) - endif() - endforeach() - string(REPLACE ___TARGET___ ${target} arg_BUILD_BYPRODUCTS "${arg_BUILD_BYPRODUCTS}") - - string(TOUPPER "${target}" prefix) - expand_urls(urls ${${prefix}_SOURCE} ${${prefix}_MIRROR}) - set(extract_ts) - if(NOT CMAKE_VERSION VERSION_LESS 3.24) - set(extract_ts DOWNLOAD_EXTRACT_TIMESTAMP ON) - endif() - ExternalProject_Add("${target}_external" - DEPENDS ${arg_DEPENDS} - BUILD_IN_SOURCE ON - PREFIX ${DEPS_SOURCEDIR} - URL ${urls} - URL_HASH ${${prefix}_HASH} - DOWNLOAD_NO_PROGRESS ON - PATCH_COMMAND ${arg_PATCH_COMMAND} - CONFIGURE_COMMAND ${arg_CONFIGURE_COMMAND} ${arg_CONFIGURE_EXTRA} - BUILD_COMMAND ${arg_BUILD_COMMAND} - INSTALL_COMMAND ${arg_INSTALL_COMMAND} - BUILD_BYPRODUCTS ${arg_BUILD_BYPRODUCTS} - EXCLUDE_FROM_ALL ON - ${extract_ts} - ) -endfunction() - - -set(apple_cflags_arch) -set(apple_cxxflags_arch) -set(apple_ldflags_arch) -set(gmp_build_host "${cross_host}") -if(APPLE AND CMAKE_CROSSCOMPILING) - if(gmp_build_host MATCHES "^(.*-.*-)ios([0-9.]+)(-.*)?$") - set(gmp_build_host "${CMAKE_MATCH_1}darwin${CMAKE_MATCH_2}${CMAKE_MATCH_3}") - endif() - if(gmp_build_host MATCHES "^(.*-.*-.*)-simulator$") - set(gmp_build_host "${CMAKE_MATCH_1}") - endif() - - set(apple_arch) - if(ARCH_TRIPLET MATCHES "^(arm|aarch)64.*") - set(apple_arch "arm64") - elseif(ARCH_TRIPLET MATCHES "^x86_64.*") - set(apple_arch "x86_64") - else() - message(FATAL_ERROR "Don't know how to specify -arch for GMP for ${ARCH_TRIPLET} (${APPLE_TARGET_TRIPLE})") - endif() - - set(apple_cflags_arch " -arch ${apple_arch}") - set(apple_cxxflags_arch " -arch ${apple_arch}") - if(CMAKE_OSX_DEPLOYMENT_TARGET) - if (SDK_NAME) - set(apple_ldflags_arch " -m${SDK_NAME}-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - elseif(CMAKE_OSX_DEPLOYMENT_TARGET) - set(apple_ldflags_arch " -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") - endif() - endif() - set(apple_ldflags_arch "${apple_ldflags_arch} -arch ${apple_arch}") - - if(CMAKE_OSX_SYSROOT) - foreach(f c cxx ld) - set(apple_${f}flags_arch "${apple_${f}flags_arch} -isysroot ${CMAKE_OSX_SYSROOT}") - endforeach() - endif() -elseif(gmp_build_host STREQUAL "") - set(gmp_build_host "--build=${CMAKE_LIBRARY_ARCHITECTURE}") -endif() - -link_libraries(-static-libstdc++) -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - link_libraries(-static-libgcc) -endif() -if(MINGW) - link_libraries(-Wl,-Bstatic -lpthread) -endif() diff --git a/cmake/session-deps b/cmake/session-deps new file mode 160000 index 000000000..58839b50a --- /dev/null +++ b/cmake/session-deps @@ -0,0 +1 @@ +Subproject commit 58839b50a0a2ad21b074c42e949613aad69ad396 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index b79fbf875..aceb83301 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,3 +1,4 @@ +<<<<<<< HEAD option(SUBMODULE_CHECK "Enables checking that vendored library submodules are up to date" ON) if(SUBMODULE_CHECK) @@ -33,11 +34,22 @@ if(SUBMODULE_CHECK) check_submodule(date) endif() endif() +======= +include(../cmake/session-deps/Deps.cmake) + +message(STATUS "Checking submodules") +check_submodule(ios-cmake) +check_submodule(oxen-libquic external/oxen-logging external/oxen-encoding) +check_submodule(nlohmann-json) +check_submodule(protobuf) +check_submodule(session-sqlite SQLiteCpp cmake/session-deps) +>>>>>>> 46a23593 (Refactor internal dependencies to use session-deps) if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() +<<<<<<< HEAD macro(libsession_system_or_submodule BIGNAME smallname target pkgconf subdir) if(NOT TARGET ${target}) option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) @@ -121,6 +133,18 @@ if(OXENLOGGING_FOUND) target_link_libraries(oxen-logging-fmt-spdlog INTERFACE oxenlogging::oxenlogging ${OXEN_LOGGING_FMT_TARGET} ${OXEN_LOGGING_SPDLOG_TARGET}) add_library(oxen::logging ALIAS oxen-logging-fmt-spdlog) endif() +======= +set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") +if(ENABLE_ONIONREQ) + sessiondep_or_submodule(liboxenquic 1.3.0 oxen-libquic quic) +endif() + +if(NOT TARGET oxenc::oxenc) + # The oxenc target will already exist if we load libquic above via submodule + set(OXENC_BUILD_TESTS OFF CACHE BOOL "") + set(OXENC_BUILD_DOCS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenc 1.3.0 oxen-libquic/external/oxen-encoding oxenc::oxenc) +>>>>>>> 46a23593 (Refactor internal dependencies to use session-deps) endif() @@ -162,36 +186,19 @@ set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(protobuf_ABSL_PROVIDER "module" CACHE STRING "" FORCE) set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) -libsession_system_or_submodule(PROTOBUF_LITE protobuf_lite protobuf::libprotobuf-lite protobuf-lite>=3.21 protobuf) -if(TARGET PkgConfig::PROTOBUF_LITE AND NOT TARGET protobuf::libprotobuf-lite) - add_library(protobuf::libprotobuf-lite ALIAS PkgConfig::PROTOBUF_LITE) -endif() - +sessiondep_or_submodule(protobuf-lite 3.21 protobuf protobuf::libprotobuf-lite) -set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "") -set(ZSTD_BUILD_TESTS OFF CACHE BOOL "") -set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "") -set(ZSTD_BUILD_SHARED OFF CACHE BOOL "") -set(ZSTD_BUILD_STATIC ON CACHE BOOL "") -set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "") -add_subdirectory(zstd/build/cmake EXCLUDE_FROM_ALL) -# zstd's cmake doesn't properly set up include paths on its targets, so we have to wrap it in an -# interface target that does: -add_library(libzstd_static_fixed_includes INTERFACE) -target_include_directories(libzstd_static_fixed_includes INTERFACE zstd/lib zstd/lib/common) -target_link_libraries(libzstd_static_fixed_includes INTERFACE libzstd_static) -add_library(libzstd::static ALIAS libzstd_static_fixed_includes) -export( - TARGETS libzstd_static_fixed_includes - NAMESPACE libsession:: - FILE libsessionZstd.cmake -) -libsession_static_bundle(libzstd_static) +# Force a static libzstd: we want semi-stable compressed output, which means we want all session +# clients to use the same version (where that is guaranteed) as much as possible, so that duplicate +# configs get deduplicated at the swarm. +set(BUILD_STATIC_libzstd ON CACHE BOOL "" FORCE) +session_dep(libzstd 1.5) +libsession_static_bundle(sessiondep::libzstd) set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use -libsession_system_or_submodule(NLOHMANN nlohmann_json nlohmann_json::nlohmann_json nlohmann_json>=3.7.0 session-router/external/nlohmann) +sessiondep_or_submodule(nlohmann_json 3.7.0 nlohmann-json nlohmann_json::nlohmann_json) if(ENABLE_NETWORKING AND ENABLE_NETWORKING_SROUTER) set(SROUTER_FULL OFF CACHE BOOL "") @@ -205,17 +212,8 @@ if(ENABLE_NETWORKING AND ENABLE_NETWORKING_SROUTER) libsession_static_bundle(session-router::libsessionrouter) endif() -set(JSON_BuildTests OFF CACHE INTERNAL "") -set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use - -function(simdutf_subdir) - set(SIMDUTF_TESTS OFF CACHE BOOL "") - set(SIMDUTF_TOOLS OFF CACHE BOOL "") - set(BUILD_SHARED_LIBS OFF) - add_subdirectory(simdutf) -endfunction() -simdutf_subdir() -libsession_static_bundle(simdutf) +session_dep(simdutf 7) +libsession_static_bundle(sessiondep::simdutf) # We need Howard Hinnant's header-only date library for now because the STL implementation of # std::chrono::parse() is spotty or broken on: diff --git a/external/simdutf b/external/simdutf deleted file mode 160000 index 7b3f5afca..000000000 --- a/external/simdutf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7b3f5afcae322391a03736809ef6eea0c2934388 diff --git a/external/zstd b/external/zstd deleted file mode 160000 index e47e674cd..000000000 --- a/external/zstd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e47e674cd09583ff0503f0f6defd6d23d8b718d3 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7530af298..bec697e96 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,8 +85,9 @@ target_link_libraries(util PUBLIC common oxen::logging - libzstd::static - simdutf + PRIVATE + sessiondep::libzstd + sessiondep::simdutf ) target_link_libraries(crypto @@ -143,7 +144,7 @@ if(ENABLE_NETWORKING) PRIVATE nlohmann_json::nlohmann_json libsodium::sodium-internal - nettle::nettle + sessiondep::nettle date::date libevent::core ) diff --git a/tests/test_compression.cpp b/tests/test_compression.cpp index 2667d3423..fa361e334 100644 --- a/tests/test_compression.cpp +++ b/tests/test_compression.cpp @@ -123,11 +123,11 @@ TEST_CASE("compression", "[config][compression]") { d = data2; session::config::compress_message(d, 19); CHECK(d[0] == 'z'); - CHECK(d.size() == 157); // Yeah, it actually gets *bigger* with supposedly "higher" compression + CHECK(d.size() == 156); CHECK(d.size() < data2.size()); CHECK(to_hex(d) == - "7a28b52ffd20aa9d0400e40764313a23693165313a2664313a6e31323a4b616c6c6965313a7032393a68" - "7474703a2f2f6b2e6578616d706c652e6f72672f4b626d70313a71323473656372657465313a3c6c6c69" - "306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564653d6431" - "3a6e303a313a7071303a6565070028812c55282f03fceac460149b57cd509a"); + "7a28b52ffd20aa95040022881f1f907d9c93291a7627219a79d06bb82c3c69341b104115dbf3c0860176" + "f63013ff7ba4247de211d1275be493fffff6eb7892db81b9dc9da26f40955e5d868586cd577bb69e00f7" + "caf2110f04219f7cf49bda3f19a5f4091966d5c199a3f14132c4d26f7cc7e14914edbca3903ef91e0862" + "955712d1275be1939f78844fb606008c12e0cb50be3a1c18c5e655339426"); } From 9474dadcabc211132a1f6fd5c6e8357b0859956f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 18:12:31 -0300 Subject: [PATCH 003/193] Drop libsodium-internal; add tweetnacl implementation for X->Ed Carrying a libsodium fork is too much of a nuissance as updating it to the latest version is non-trivial. This drops the libsodium-internal fork in favour of using tweenacl's implementation *just* for the X25519 -> Ed25519 pubkey conversion, and using a stock libsodium for everything else. This also bumps the libsodium requirement up to 1.0.21: that version will be required for SHAKE support in future commits on this branch. --- .gitmodules | 3 - external/CMakeLists.txt | 187 +++++++++--------------------------- external/libsodium-internal | 1 - src/CMakeLists.txt | 7 +- src/blinding.cpp | 8 +- src/config/groups/keys.cpp | 4 +- src/xed25519-tweetnacl.cpp | 148 ++++++++++++++++++++++++++++ src/xed25519.cpp | 39 ++------ tests/CMakeLists.txt | 2 +- tests/test_xed25519.cpp | 8 +- 10 files changed, 216 insertions(+), 191 deletions(-) delete mode 160000 external/libsodium-internal create mode 100644 src/xed25519-tweetnacl.cpp diff --git a/.gitmodules b/.gitmodules index 79febb6fa..8d6f7cfea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "external/libsodium-internal"] - path = external/libsodium-internal - url = https://github.com/session-foundation/libsodium-internal.git [submodule "tests/Catch2"] path = tests/Catch2 url = https://github.com/catchorg/Catch2 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index aceb83301..99edc1f40 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,150 +1,64 @@ -<<<<<<< HEAD -option(SUBMODULE_CHECK "Enables checking that vendored library submodules are up to date" ON) - -if(SUBMODULE_CHECK) - find_package(Git) - if(GIT_FOUND) - function(check_submodule relative_path) - execute_process(COMMAND git rev-parse "HEAD" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE localHead) - execute_process(COMMAND git rev-parse "HEAD:external/${relative_path}" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE checkedHead) - string(COMPARE EQUAL "${localHead}" "${checkedHead}" upToDate) - if (upToDate) - message(STATUS "Submodule 'external/${relative_path}' is up-to-date") - else() - message(FATAL_ERROR "Submodule 'external/${relative_path}' is not up-to-date. Please update with\ngit submodule update --init --recursive\nor run cmake with -DSUBMODULE_CHECK=OFF") - endif() - - # Extra arguments check nested submodules - foreach(submod ${ARGN}) - execute_process(COMMAND git rev-parse "HEAD" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path}/${submod} OUTPUT_VARIABLE localHead) - execute_process(COMMAND git rev-parse "HEAD:${submod}" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE checkedHead) - string(COMPARE EQUAL "${localHead}" "${checkedHead}" upToDate) - if (NOT upToDate) - message(FATAL_ERROR "Nested submodule '${relative_path}/${submod}' is not up-to-date. Please update with\ngit submodule update --init --recursive\nor run cmake with -DSUBMODULE_CHECK=OFF") - endif() - endforeach() - endfunction () - - message(STATUS "Checking submodules") - check_submodule(ios-cmake) - check_submodule(libsodium-internal) - check_submodule(zstd) - check_submodule(protobuf) - check_submodule(session-router) - check_submodule(date) - endif() -endif() -======= include(../cmake/session-deps/Deps.cmake) message(STATUS "Checking submodules") check_submodule(ios-cmake) -check_submodule(oxen-libquic external/oxen-logging external/oxen-encoding) -check_submodule(nlohmann-json) +check_submodule(session-router + external/oxen-libquic + external/oxen-libquic/external/oxen-logging + external/oxen-libquic/external/oxen-logging/fmt + external/oxen-libquic/external/oxen-logging/spdlog + external/nlohmann) check_submodule(protobuf) +check_submodule(date) check_submodule(session-sqlite SQLiteCpp cmake/session-deps) ->>>>>>> 46a23593 (Refactor internal dependencies to use session-deps) if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() -<<<<<<< HEAD -macro(libsession_system_or_submodule BIGNAME smallname target pkgconf subdir) - if(NOT TARGET ${target}) - option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) - if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES) - pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET GLOBAL) - endif() - if(${BIGNAME}_FOUND) - add_library(${smallname} INTERFACE IMPORTED GLOBAL) - if(NOT TARGET PkgConfig::${BIGNAME} AND CMAKE_VERSION VERSION_LESS "3.21") - # Work around cmake bug 22180 (PkgConfig::THING not set if no flags needed) - else() - target_link_libraries(${smallname} INTERFACE PkgConfig::${BIGNAME}) - endif() - message(STATUS "Found system ${smallname} ${${BIGNAME}_VERSION}") - else() - message(STATUS "using ${smallname} submodule ${subdir}") - add_subdirectory(${subdir}) - endif() - if(NOT TARGET ${target}) - add_library(${target} ALIAS ${smallname}) - endif() - if(BUILD_STATIC_DEPS AND STATIC_BUNDLE) - libsession_static_bundle(${smallname}::${smallname}) - endif() - endif() -endmacro() - - -set(deps_cc "${CMAKE_C_COMPILER}") -set(cross_host "") -set(cross_rc "") -if(CMAKE_CROSSCOMPILING) - if(APPLE_TARGET_TRIPLE) - set(cross_host "--host=${APPLE_TARGET_TRIPLE}") - elseif(ANDROID) - if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) - set(cross_host "--host=x86_64-linux-android") - set(android_compiler_prefix x86_64) - set(android_compiler_suffix linux-android) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86) - set(cross_host "--host=i686-linux-android") - set(android_compiler_prefix i686) - set(android_compiler_suffix linux-android) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a) - set(cross_host "--host=armv7a-linux-androideabi") - set(android_compiler_prefix armv7a) - set(android_compiler_suffix linux-androideabi) - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a) - set(cross_host "--host=aarch64-linux-android") - set(android_compiler_prefix aarch64) - set(android_compiler_suffix linux-android) - else() - message(FATAL_ERROR "unknown android arch: ${CMAKE_ANDROID_ARCH_ABI}") - endif() +function(add_static_subdirectory dir) + set(BUILD_SHARED_LIBS OFF) + add_subdirectory(${dir} ${ARGN}) +endfunction() - string(REPLACE "android-" "" android_platform_num "${ANDROID_PLATFORM}") - set(deps_cc "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_compiler_prefix}-${android_compiler_suffix}${android_platform_num}-clang") +if(ENABLE_NETWORKING) + if(ENABLE_NETWORKING_SROUTER) + set(SROUTER_FULL OFF CACHE BOOL "") + set(SROUTER_DAEMON OFF CACHE BOOL "") + set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") + set(SROUTER_JEMALLOC OFF CACHE BOOL "") + + add_library(sodium INTERFACE) + target_link_libraries(sodium INTERFACE libsodium::sodium-internal) + add_static_subdirectory(session-router EXCLUDE_FROM_ALL) + libsession_static_bundle(session-router::libsessionrouter) else() - set(cross_host "--host=${ARCH_TRIPLET}") - if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) - set(cross_rc "WINDRES=${CMAKE_RC_COMPILER}") - endif() + set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic quic) endif() endif() -if(ENABLE_NETWORKING) - set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") - libsession_system_or_submodule(OXENQUIC quic oxen::quic liboxenquic>=1.8 session-router/external/oxen-libquic) +if(NOT TARGET oxenc::oxenc) + # The oxenc target will already exist if we load libquic above via submodule + set(OXENC_BUILD_TESTS OFF CACHE BOOL "") + set(OXENC_BUILD_DOCS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenc 1.5.0 oxen-libquic/external/oxen-encoding oxenc::oxenc) endif() -libsession_system_or_submodule(OXENC oxenc oxenc::oxenc liboxenc>=1.5.0 session-router/external/oxen-libquic/external/oxen-encoding) +if(NOT TARGET oxen::logging) + sessiondep_or_submodule(liboxen-logging 1.2.0 session-router/external/oxen-libquic/external/oxen-logging oxen::logging) -libsession_system_or_submodule(OXENLOGGING oxenlogging oxen::logging liboxen-logging>=1.2.0 session-router/external/oxen-libquic/external/oxen-logging) -if(OXENLOGGING_FOUND) - if(NOT TARGET oxen::logging) - # If we load oxen-logging via system lib then we won't necessarily have fmt/spdlog targets, - # but this script will give us them: - include(session-router/external/oxen-libquic/external/oxen-logging/cmake/load_fmt_spdlog.cmake) + # If we load oxen-logging via system lib then we won't necessarily have fmt/spdlog targets, + # but this script will give us them: + if(OXENLOGGING_FOUND) + if(NOT TARGET oxen::logging) + include(session-router/external/oxen-libquic/external/oxen-logging/cmake/load_fmt_spdlog.cmake) - add_library(oxen-logging-fmt-spdlog INTERFACE) - target_link_libraries(oxen-logging-fmt-spdlog INTERFACE oxenlogging::oxenlogging ${OXEN_LOGGING_FMT_TARGET} ${OXEN_LOGGING_SPDLOG_TARGET}) - add_library(oxen::logging ALIAS oxen-logging-fmt-spdlog) + add_library(oxen-logging-fmt-spdlog INTERFACE) + target_link_libraries(oxen-logging-fmt-spdlog INTERFACE oxenlogging::oxenlogging ${OXEN_LOGGING_FMT_TARGET} ${OXEN_LOGGING_SPDLOG_TARGET}) + add_library(oxen::logging ALIAS oxen-logging-fmt-spdlog) + endif() endif() -======= -set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") -if(ENABLE_ONIONREQ) - sessiondep_or_submodule(liboxenquic 1.3.0 oxen-libquic quic) -endif() - -if(NOT TARGET oxenc::oxenc) - # The oxenc target will already exist if we load libquic above via submodule - set(OXENC_BUILD_TESTS OFF CACHE BOOL "") - set(OXENC_BUILD_DOCS OFF CACHE BOOL "") - sessiondep_or_submodule(liboxenc 1.3.0 oxen-libquic/external/oxen-encoding oxenc::oxenc) ->>>>>>> 46a23593 (Refactor internal dependencies to use session-deps) endif() @@ -167,13 +81,8 @@ if(APPLE) endforeach() endif() -function(add_static_subdirectory dir) - set(BUILD_SHARED_LIBS OFF) - add_subdirectory(${dir} ${ARGN}) -endfunction() - -add_static_subdirectory(libsodium-internal) -libsession_static_bundle(libsodium::sodium-internal) +session_dep(libsodium 1.0.21) +libsession_static_bundle(sessiondep::libsodium) set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) @@ -188,6 +97,7 @@ set(protobuf_BUILD_PROTOC_BINARIES OFF CACHE BOOL "") set(protobuf_BUILD_PROTOBUF_BINARIES ON CACHE BOOL "" FORCE) sessiondep_or_submodule(protobuf-lite 3.21 protobuf protobuf::libprotobuf-lite) + # Force a static libzstd: we want semi-stable compressed output, which means we want all session # clients to use the same version (where that is guaranteed) as much as possible, so that duplicate # configs get deduplicated at the swarm. @@ -200,17 +110,6 @@ set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use sessiondep_or_submodule(nlohmann_json 3.7.0 nlohmann-json nlohmann_json::nlohmann_json) -if(ENABLE_NETWORKING AND ENABLE_NETWORKING_SROUTER) - set(SROUTER_FULL OFF CACHE BOOL "") - set(SROUTER_DAEMON OFF CACHE BOOL "") - set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") - set(SROUTER_JEMALLOC OFF CACHE BOOL "") - - add_library(sodium INTERFACE) - target_link_libraries(sodium INTERFACE libsodium::sodium-internal) - add_static_subdirectory(session-router EXCLUDE_FROM_ALL) - libsession_static_bundle(session-router::libsessionrouter) -endif() session_dep(simdutf 7) libsession_static_bundle(sessiondep::simdutf) diff --git a/external/libsodium-internal b/external/libsodium-internal deleted file mode 160000 index e12e612dc..000000000 --- a/external/libsodium-internal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e12e612dc735909a59be552f9bf03fe98e320703 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bec697e96..0409512fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,7 @@ add_libsession_util_library(crypto session_protocol.cpp sodium_array.cpp xed25519.cpp + xed25519-tweetnacl.cpp pro_backend.cpp types.cpp ) @@ -94,7 +95,7 @@ target_link_libraries(crypto PUBLIC util PRIVATE - libsodium::sodium-internal + sessiondep::libsodium nlohmann_json::nlohmann_json libsession::protos ) @@ -104,7 +105,7 @@ target_link_libraries(config crypto libsession::protos PRIVATE - libsodium::sodium-internal + sessiondep::libsodium ) if(ENABLE_NETWORKING) @@ -143,7 +144,7 @@ if(ENABLE_NETWORKING) quic PRIVATE nlohmann_json::nlohmann_json - libsodium::sodium-internal + sessiondep::libsodium sessiondep::nettle date::date libevent::core diff --git a/src/blinding.cpp b/src/blinding.cpp index 927aa5ea8..659bf0113 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -67,7 +67,9 @@ namespace { auto k = blind15_factor(server_pk); if (session_id.size() == 33) session_id = session_id.subspan(1); - auto ed_pk = xed25519::pubkey(session_id); + if (session_id.size() != 32) + throw std::invalid_argument{"Invalid session id"}; + auto ed_pk = xed25519::pubkey(session_id.first<32>()); if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; out[0] = 0x15; @@ -80,7 +82,9 @@ namespace { auto k = blind25_factor(session_id, server_pk); if (session_id.size() == 33) session_id = session_id.subspan(1); - auto ed_pk = xed25519::pubkey(session_id); + if (session_id.size() != 32) + throw std::invalid_argument{"Invalid session id"}; + auto ed_pk = xed25519::pubkey(session_id.first<32>()); if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; out[0] = 0x25; diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index e35b4d04d..85e242872 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -596,7 +596,7 @@ std::vector Keys::swarm_make_subaccount( auto k = subaccount_blind_factor(X); // T = |S| - auto T = xed25519::pubkey(std::span{X.data(), X.size()}); + auto T = xed25519::pubkey(X); // kT is the user's Ed25519 blinded pubkey: std::array kT; @@ -633,7 +633,7 @@ std::vector Keys::swarm_subaccount_token( auto k = subaccount_blind_factor(X); // T = |S| - auto T = xed25519::pubkey(std::span{X.data(), X.size()}); + auto T = xed25519::pubkey(X); std::vector out; out.resize(4 + 32); diff --git a/src/xed25519-tweetnacl.cpp b/src/xed25519-tweetnacl.cpp new file mode 100644 index 000000000..2891dcb00 --- /dev/null +++ b/src/xed25519-tweetnacl.cpp @@ -0,0 +1,148 @@ +// This file contains a subset of TweetNaCl (https://tweetnacl.cr.yp.to/software.html) public domain +// code to perform the X25519 -> Ed25519 conversion; libsodium doesn't provide enough access to +// internals to compute this without hacking up libsodium's build, which is fragile. Hence we use +// this subset of the portable TweetNaCl for that single function, and libsodium for everything +// else. + +#include +#include +#include + +namespace session::xed25519 { + +namespace { + + // clang-format off + +#define FOR(i,n) for (i = 0;i < n;++i) + +using gf = int64_t[16]; + +const gf gf1 = {1}; + +void car25519(gf o) +{ + int i; + int64_t c; + FOR(i,16) { + o[i]+=(1LL<<16); + c=o[i]>>16; + o[(i+1)*(i<15)]+=c-1+37*(c-1)*(i==15); + o[i]-=c<<16; + } +} + +void sel25519(gf p,gf q,int b) +{ + int64_t t,i,c=~(b-1); + FOR(i,16) { + t= c&(p[i]^q[i]); + p[i]^=t; + q[i]^=t; + } +} + +void pack25519(uint8_t *o,const gf n) +{ + int i,j,b; + gf m,t; + FOR(i,16) t[i]=n[i]; + car25519(t); + car25519(t); + car25519(t); + FOR(j,2) { + m[0]=t[0]-0xffed; + for(i=1;i<15;i++) { + m[i]=t[i]-0xffff-((m[i-1]>>16)&1); + m[i-1]&=0xffff; + } + m[15]=t[15]-0x7fff-((m[14]>>16)&1); + b=(m[15]>>16)&1; + m[14]&=0xffff; + sel25519(t,m,1-b); + } + FOR(i,16) { + o[2*i]=t[i]&0xff; + o[2*i+1]=t[i]>>8; + } +} + +void unpack25519(gf o, const uint8_t *n) +{ + int i; + FOR(i,16) o[i]=n[2*i]+((int64_t)n[2*i+1]<<8); + o[15]&=0x7fff; +} + +void A(gf o,const gf a,const gf b) +{ + int i; + FOR(i,16) o[i]=a[i]+b[i]; +} + +void Z(gf o,const gf a,const gf b) +{ + int i; + FOR(i,16) o[i]=a[i]-b[i]; +} + +void M(gf o,const gf a,const gf b) +{ + int64_t i,j,t[31]; + FOR(i,31) t[i]=0; + FOR(i,16) FOR(j,16) t[i+j]+=a[i]*b[j]; + FOR(i,15) t[i]+=38*t[i+16]; + FOR(i,16) o[i]=t[i]; + car25519(o); + car25519(o); +} + +void S(gf o,const gf a) +{ + M(o,a,a); +} + +// Y +void inv25519(gf o,const gf i) +{ + gf c; + int a; + FOR(a,16) c[a]=i[a]; + for(a=253;a>=0;a--) { + S(c,c); + if(a!=2&&a!=4) M(c,c,i); + } + FOR(a,16) o[a]=c[a]; +} + + // clang-format on + +} // namespace + +std::array pubkey(std::span x_pk) noexcept { + gf u; + unpack25519(u, x_pk.data()); + + // u - 1 + gf u_minus_one; + Z(u_minus_one, u, gf1); + + // Compute: u + 1 + gf u_plus_one; + A(u_plus_one, u, gf1); + + // Compute: (u + 1)^-1 + gf u_plus_one_inv; + inv25519(u_plus_one_inv, u_plus_one); + + // Compute: y = (u - 1) * (u + 1)^-1 + gf y; + M(y, u_minus_one, u_plus_one_inv); + + // Encode to 32 bytes (sign bit is naturally 0) + std::array ed_pk; + pack25519(ed_pk.data(), y); + return ed_pk; +} + +} // namespace session::xed25519 diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 9d23390a3..3110eea54 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -21,16 +20,6 @@ using bytes = std::array; namespace { - void fe25519_montx_to_edy(fe25519 y, const fe25519 u) { - fe25519 one; - crypto_internal_fe25519_1(one); - fe25519 um1, up1; - crypto_internal_fe25519_sub(um1, u, one); - crypto_internal_fe25519_add(up1, u, one); - crypto_internal_fe25519_invert(up1, up1); - crypto_internal_fe25519_mul(y, um1, up1); - } - // We construct an Ed25519-like signature with one important difference: where Ed25519 // calculates `r = H(S || M) mod L` (where S is the second half of the SHA-512 hash of the // secret key) we instead calculate `r = H(a || M || Z) mod L`. @@ -127,7 +116,7 @@ bool verify( std::span msg) { assert(signature.size() == crypto_sign_ed25519_BYTES); assert(curve25519_pubkey.size() == 32); - auto ed_pubkey = pubkey(curve25519_pubkey); + auto ed_pubkey = pubkey(curve25519_pubkey.first<32>()); return 0 == crypto_sign_ed25519_verify_detached( signature.data(), msg.data(), msg.size(), ed_pubkey.data()); } @@ -136,19 +125,12 @@ bool verify(std::string_view signature, std::string_view curve25519_pubkey, std: return verify(to_span(signature), to_span(curve25519_pubkey), to_span(msg)); } -std::array pubkey(std::span curve25519_pubkey) { - fe25519 u, y; - crypto_internal_fe25519_frombytes(u, curve25519_pubkey.data()); - fe25519_montx_to_edy(y, u); - - std::array ed_pubkey; - crypto_internal_fe25519_tobytes(ed_pubkey.data(), y); - - return ed_pubkey; -} +// pubkey(...) is in xed25519-tweetnacl.cpp std::string pubkey(std::string_view curve25519_pubkey) { - auto ed_pk = pubkey(to_span(curve25519_pubkey)); + if (curve25519_pubkey.size() != 32) + throw std::invalid_argument{"Invalid X25519 pubkey"}; + auto ed_pk = pubkey(to_span(curve25519_pubkey).first<32>()); return std::string{reinterpret_cast(ed_pk.data()), ed_pk.size()}; } @@ -179,16 +161,11 @@ LIBSESSION_C_API bool session_xed25519_verify( return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}); } -LIBSESSION_C_API bool session_xed25519_pubkey( +LIBSESSION_C_API void session_xed25519_pubkey( unsigned char* ed25519_pubkey, const unsigned char* curve25519_pubkey) { assert(ed25519_pubkey != NULL); - try { - auto edpk = session::xed25519::pubkey({curve25519_pubkey, 32}); - std::memcpy(ed25519_pubkey, edpk.data(), edpk.size()); - return true; - } catch (...) { - return false; - } + std::span xpk{curve25519_pubkey, 32}; + std::memcpy(ed25519_pubkey, session::xed25519::pubkey(xpk).data(), 32); } } // extern "C" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2ed0d1af5..002dd8765 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -51,7 +51,7 @@ add_library(test_libs INTERFACE) target_link_libraries(test_libs INTERFACE libsession::config - libsodium::sodium-internal + sessiondep::libsodium nlohmann_json::nlohmann_json oxen::logging) diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 143ba82bb..74a9ae351 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -57,11 +57,11 @@ TEST_CASE("XEd25519 pubkey conversion", "[xed25519][pubkey]") { REQUIRE(rc == 0); REQUIRE(view_hex(xpk2) == view_hex(xpub2)); - auto xed1 = session::xed25519::pubkey(session::to_span(xpub1)); + auto xed1 = session::xed25519::pubkey(xpub1); REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(session::to_span(xpub2)); + auto xed2 = session::xed25519::pubkey(xpub2); REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: xed2[31] |= 0x80; @@ -125,11 +125,11 @@ TEST_CASE("XEd25519 verification", "[xed25519][verify]") { } TEST_CASE("XEd25519 pubkey conversion (C wrapper)", "[xed25519][pubkey][c]") { - auto xed1 = session::xed25519::pubkey(session::to_span(xpub1)); + auto xed1 = session::xed25519::pubkey(xpub1); REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(session::to_span(xpub2)); + auto xed2 = session::xed25519::pubkey(xpub2); REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: xed2[31] |= 0x80; From 49608f5678c31e3d9ad5f1115b940f6917a2cca1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 19:04:40 -0300 Subject: [PATCH 004/193] API improvements for hashing, arrays, clearing, spans - Blake2b hashing: - Add nicer hash::blake2b functions for simpler has computations where you can just pass a bunch of spannables and get the hash over them, rather than needing to do a bunch of manual C API update calls. - Add a ""_b2b_pers for a compile-time validated blake2b personalisation string - Drop make_blake2b32_hasher(). - TODO: convert these to make full use of hash::blake2b(...), as above. - Change cleared_array to take a Char type instead of forcing unsigned char. - Add cleared_uchars (and cleared_bytes) for the old force-unsigned char typedef. - Make `to_span` work for any input convertible to a span - Tighten up various functions taking fixed length values (like session ids, pubkeys) to take compile-time-fixed spans instead of dynamic extent spans. This allows these generic functions to not have to worry about length checking. - Add a generic random::fill(s) function that fills some spannable type s with random bytes. --- include/session/hash.hpp | 150 +++++++++++++++++++++++++++ include/session/random.hpp | 12 +++ include/session/session_protocol.h | 11 -- include/session/session_protocol.hpp | 13 ++- include/session/sodium_array.hpp | 23 +++- include/session/util.hpp | 11 +- include/session/xed25519.h | 2 +- include/session/xed25519.hpp | 5 +- src/pro_backend.cpp | 41 +++----- src/random.cpp | 16 ++- src/session_encrypt.cpp | 10 +- src/session_protocol.cpp | 41 +------- 12 files changed, 240 insertions(+), 95 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 24e1a2382..458923069 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,10 +1,13 @@ #pragma once #include +#include #include #include +#include #include +#include #include #include "types.hpp" @@ -45,6 +48,119 @@ std::vector hash( std::span msg, std::optional> key = std::nullopt); +template +concept ByteContainer = + std::ranges::contiguous_range && oxenc::basic_char>; + +/// API: hash/blake2b_update +/// +/// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte +/// containers and updates the hash state with them, in argument order. +template + requires(sizeof...(T) > 0) +void update_all(crypto_generichash_blake2b_state& st, const T&... args) { + auto update_one = [&st](const auto& arg) { + crypto_generichash_blake2b_update( + &st, + reinterpret_cast(std::ranges::data(arg)), + std::ranges::size(arg)); + }; + (update_one(args), ...); +} + +namespace detail { + + template + std::integral_constant extract_extent(const std::array&); + template + std::integral_constant extract_extent(std::span); + template + std::integral_constant extract_extent(const T (&)[N]); + std::integral_constant extract_extent(...); + + template + constexpr size_t container_extent_v = decltype(extract_extent(std::declval()))::value; +} // namespace detail + +template +concept Blake2BOutputContainer = + std::ranges::contiguous_range && !std::is_const_v> && + oxenc::basic_char> && + detail::container_extent_v != std::dynamic_extent && + detail::container_extent_v >= 1 && detail::container_extent_v <= 64; + +template +concept Blake2BKey = + std::ranges::contiguous_range && oxenc::basic_char> && + detail::container_extent_v != std::dynamic_extent && detail::container_extent_v <= 64; + +/// Helper value to pass a null `key` to blake2b or blake2b_pers. +inline constexpr std::span nullkey{}; + +/// API: hash/blake2b_key +/// +/// This version of blake2b() takes a key as the second argument and computes a keyed hash. The key +/// must be between 0 and 64 characters long. (A 0-length key is equivalent to no key). +template + requires(sizeof...(T) > 0) +void blake2b_key(Out& out, const Key& key, const T&... args) { + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init( + &st, std::ranges::data(key), std::ranges::size(key), std::ranges::size(out)); + update_all(st, args...); + crypto_generichash_blake2b_final(&st, std::ranges::data(out), std::ranges::size(out)); +} + +/// API: hash/blake2b +/// +/// One-shot hasher that takes an output container and and any number of contiguous byte containers, +/// computes the blake2b hash of the concatentation of the containers (in argument order) and then +/// writes the hash into the output container. +/// +/// This version uses neither key nor personalisation strings; see blake2b_key, blake2b_pers, and +/// blake2b_key_pers if you want one or both of those. +/// +/// Output must be a fixed extent span or containers (e.g. std::array), and must satisfy the blake2b +/// requirements (output size in [1,64]). +template + requires(sizeof...(T) > 0) +void blake2b(Out& out, const T&... args) { + return blake2b_key(out, nullkey, args...); +} + +/// API: hash/blake2b_key_pers +/// +/// This version of blake2b() takes a both a key and a 16-byte personalisation string as the second +/// and third arguments and computes a keyed hash with a personalisation string. The +/// personalisation string must be exact 16 bytes, and is typically constructed with "..."_b2b_pers +/// for compile-time validation. The key must be between 0 and 64 bytes long. +template + requires(sizeof...(T) > 0) +void blake2b_key_pers( + Out& out, const Key& key, std::span pers, const T&... args) { + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init_salt_personal( + &st, + std::ranges::data(key), + std::ranges::size(key), + std::ranges::size(out), + /*salt=*/nullptr, + pers.data()); + update_all(st, args...); + crypto_generichash_blake2b_final(&st, std::ranges::data(out), std::ranges::size(out)); +} + +/// API: hash/blake2b_pers +/// +/// This version of blake2b() takes a 16-byte personality string as the second argument and computes +/// a unkeyed hash with a personalisation string. The personalization string must be exact 16 +/// bytes, and is typically constructed with "..."_b2b_pers for compile-time validation. +template + requires(sizeof...(T) > 0) +void blake2b_pers(Out& out, std::span pers, const T&... args) { + return blake2b_key_pers(out, nullkey, pers, args...); +} + // Helper callable usable with unordered_map and similar to hash an array of chars by simply copying // the first sizeof(size_t) bytes, suitable for use with pre-hashed values. struct identity_hasher { @@ -58,3 +174,37 @@ struct identity_hasher { }; } // namespace session::hash + +namespace session { + +template +struct StringLiteral { + std::array chars; + constexpr StringLiteral(const char (&s)[N]) { + for (size_t i = 0; i < N - 1; ++i) + chars[i] = s[i]; + } +}; + +inline namespace literals { + + /// User-defined literal for a 16-byte, unsigned char array intended for use as a BLAKE2b + /// personality value. Example: + /// + /// using namespace session::hash::literals; + /// constexpr auto PERS_XYZ = "XYZ-XYZ-XYZ-WXYZ"_b2b_pers; + /// + template + constexpr auto operator""_b2b_pers() { + static_assert( + Str.chars.size() == 16, + "BLAKE2b personalization strings must be exactly 16 bytes long"); + std::array pers; + for (size_t i = 0; i < pers.size(); i++) + pers[i] = static_cast(Str.chars[i]); + return pers; + } + +} // namespace literals + +} // namespace session diff --git a/include/session/random.hpp b/include/session/random.hpp index 9968b1e90..553ae7f56 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -36,6 +36,18 @@ inline constexpr CSRNG csrng{}; namespace session::random { +/// API: random/random_fill +/// +/// Wrapper around the randombytes_buf function. +/// +/// Inputs: +/// - `buf` -- span to fill with random bytes +/// +/// Outputs: None. +void fill(std::span buf); +void fill(std::span buf); +void fill(std::span buf); + /// API: random/random /// /// Wrapper around the randombytes_buf function. diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index f2cd6dc44..3fd062e6f 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -31,17 +31,6 @@ enum { SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING = 160, }; -// clang-format off -/// Session Pro personalisation bytes for hashing. Must match -/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/backend.py#L21 -/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/server.py#L571 -static const char SESSION_PROTOCOL_GENERATE_PROOF_HASH_PERSONALISATION[] = "ProGenerateProof"; -static const char SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION[] = "ProProof________"; -static const char SESSION_PROTOCOL_ADD_PRO_PAYMENT_HASH_PERSONALISATION[] = "ProAddPayment___"; -static const char SESSION_PROTOCOL_SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION[] = "ProSetRefundReq_"; -static const char SESSION_PROTOCOL_GET_PRO_DETAILS_HASH_PERSONALISATION[] = "ProGetProDetReq_"; -// clang-format on - /// Bundle of hard-coded strings that an implementing application may use for various scenarios. typedef struct session_protocol_strings session_protocol_strings; struct session_protocol_strings { diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 1ff3c21ea..f66a1c3cd 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,15 @@ namespace session { enum ProProofVersion { ProProofVersion_v0 }; +/// Session Pro personalisation bytes for hashing. Must match +/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/backend.py#L21 +/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/server.py#L571 +inline constexpr auto GENERATE_PROOF_PERS = "ProGenerateProof"_b2b_pers; +inline constexpr auto BUILD_PROOF_PERS = "ProProof________"_b2b_pers; +inline constexpr auto ADD_PRO_PAYMENT_PERS = "ProAddPayment___"_b2b_pers; +inline constexpr auto SET_PAYMENT_REFUND_REQUESTED_PERS = "ProSetRefundReq_"_b2b_pers; +inline constexpr auto GET_PRO_DETAILS_PERS = "ProGetProDetReq_"_b2b_pers; + enum class ProStatus { // Pro proof sig was not signed by the Pro backend key InvalidProBackendSig = SESSION_PROTOCOL_PRO_STATUS_INVALID_PRO_BACKEND_SIG, @@ -640,7 +650,4 @@ DecodedCommunityMessage decode_for_community( sys_ms unix_ts, const array_uc32& pro_backend_pubkey); -/// Initialiser the blake2b hashing context to generate 32 byte hashes for Session Pro features. -void make_blake2b32_hasher( - struct crypto_generichash_blake2b_state* hasher, std::string_view personalization); } // namespace session diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp index a0a52de0a..c14ed7f5b 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -62,11 +62,26 @@ struct sodium_cleared : T { ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } }; -template -using cleared_array = sodium_cleared>; +template +struct cleared_array : sodium_cleared> { + using sodium_cleared>::sodium_cleared; + + // Provide implicit conversion to fixed extent span because otherwise span's built-in is dynamic + // extent (because span uses CTAD which detects std::array but not our subclass). + operator std::span() { return std::span{static_cast&>(*this)}; } + operator std::span() const { + return std::span{static_cast&>(*this)}; + } +}; -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; +template +using cleared_uchars = cleared_array; +using cleared_uc32 = cleared_uchars<32>; +using cleared_uc64 = cleared_uchars<64>; +template +using cleared_bytes = cleared_array; +using cleared_b32 = cleared_bytes<32>; +using cleared_b64 = cleared_bytes<64>; // This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation // and freeing via libsodium. It is slower and heavier than a regular allocation type but takes diff --git a/include/session/util.hpp b/include/session/util.hpp index 75c9b4753..1baead82d 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -49,9 +49,14 @@ inline std::span to_span(const char (&literal)[N]) { } template - requires(!oxenc::bt_input_string) -inline std::span to_span(const Container& c) { - return {reinterpret_cast(c.data()), c.size()}; + requires( + std::convertible_to< + const Container&, + std::span> && + !oxenc::bt_input_string && oxenc::basic_char) +inline auto to_span(const Container& c) { + constexpr size_t Extent{decltype(std::span{c})::extent}; + return std::span{reinterpret_cast(c.data()), c.size()}; } // Helper functions to convert container types diff --git a/include/session/xed25519.h b/include/session/xed25519.h index 4963ce89c..d65307287 100644 --- a/include/session/xed25519.h +++ b/include/session/xed25519.h @@ -29,7 +29,7 @@ LIBSESSION_EXPORT bool session_xed25519_verify( /// in a given curve25519 pubkey: this always returns the positive value. You can get the other /// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`. /// Returns 0 on success, non-0 on failure. -LIBSESSION_EXPORT bool session_xed25519_pubkey( +LIBSESSION_EXPORT void session_xed25519_pubkey( unsigned char* ed25519_pubkey /* 32-byte output buffer */, const unsigned char* curve25519_pubkey /* 32 bytes */); diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index 1cedd9ca8..ef62d9881 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -32,9 +32,10 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string /// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519 /// pubkey: this always returns the positive value. You can get the other possibility (the /// negative) by setting the sign bit, i.e. `returned_pubkey[31] |= 0x80`. -std::array pubkey(std::span curve25519_pubkey); +std::array pubkey(std::span curve25519_pubkey) noexcept; -/// "Softer" version that takes/returns strings of regular chars +/// "Softer" version that takes/returns strings of regular chars. Throws invalid_argument if the +/// input is not 32 bytes. std::string pubkey(std::string_view curve25519_pubkey); /// Utility function that provides a constant-time `if (b) f = g;` implementation for byte arrays. diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index f8d9ad8f7..b995e1491 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -59,6 +59,7 @@ const session_pro_backend_payment_provider_metadata SESSION_PRO_BACKEND_PAYMENT_ .cancel_subscription_url = string8_literal(""), } }; +// clang-format on namespace { const nlohmann::json json_parse(std::string_view json, std::vector& errors) { @@ -223,12 +224,10 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L171 - array_uc32 hash_to_sign = {}; - crypto_generichash_blake2b_state state = {}; - make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_ADD_PRO_PAYMENT_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_ADD_PRO_PAYMENT_HASH_PERSONALISATION) - 1}); + array_uc32 hash_to_sign; + crypto_generichash_blake2b_state state; + crypto_generichash_blake2b_init_salt_personal( + &state, nullptr, 0, hash_to_sign.size(), nullptr, ADD_PRO_PAYMENT_PERS.data()); crypto_generichash_blake2b_update(&state, &version, sizeof(version)); crypto_generichash_blake2b_update( &state, @@ -404,12 +403,10 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 uint8_t version = 0; uint64_t unix_ts_ms = epoch_ms(unix_ts); - array_uc32 hash_to_sign = {}; - crypto_generichash_blake2b_state state = {}; - make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_GENERATE_PROOF_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_GENERATE_PROOF_HASH_PERSONALISATION) - 1}); + array_uc32 hash_to_sign; + crypto_generichash_blake2b_state state; + crypto_generichash_blake2b_init_salt_personal( + &state, nullptr, 0, 32, nullptr, GENERATE_PROOF_PERS.data()); crypto_generichash_blake2b_update(&state, &version, sizeof(version)); crypto_generichash_blake2b_update( &state, master_privkey.data() + 32, crypto_sign_ed25519_PUBLICKEYBYTES); @@ -571,13 +568,11 @@ array_uc64 GetProDetailsRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 - array_uc32 hash_to_sign = {}; - crypto_generichash_blake2b_state state = {}; + array_uc32 hash_to_sign; + crypto_generichash_blake2b_state state; uint64_t unix_ts_ms = epoch_ms(unix_ts); - make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_GET_PRO_DETAILS_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_GET_PRO_DETAILS_HASH_PERSONALISATION) - 1}); + crypto_generichash_blake2b_init_salt_personal( + &state, nullptr, 0, 32, nullptr, GET_PRO_DETAILS_PERS.data()); crypto_generichash_blake2b_update(&state, &version, sizeof(version)); crypto_generichash_blake2b_update( &state, @@ -813,12 +808,10 @@ array_uc64 SetPaymentRefundRequestedRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 - array_uc32 hash_to_sign = {}; - crypto_generichash_blake2b_state state = {}; - make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION) - 1}); + array_uc32 hash_to_sign; + crypto_generichash_blake2b_state state; + crypto_generichash_blake2b_init_salt_personal( + &state, nullptr, 0, 32, nullptr, SET_PAYMENT_REFUND_REQUESTED_PERS.data()); crypto_generichash_blake2b_update(&state, &version, sizeof(version)); crypto_generichash_blake2b_update( &state, diff --git a/src/random.cpp b/src/random.cpp index 434b859ae..7e325d589 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -11,11 +11,20 @@ namespace session::random { +void fill(std::span buf) { + randombytes_buf(buf.data(), buf.size()); +} +void fill(std::span buf) { + fill(std::span{reinterpret_cast(buf.data()), buf.size()}); +} +void fill(std::span buf) { + fill(std::span{reinterpret_cast(buf.data()), buf.size()}); +} + std::vector random(size_t size) { std::vector result; result.resize(size); - randombytes_buf(result.data(), size); - + fill(result); return result; } @@ -50,9 +59,8 @@ std::string unique_id(std::string_view prefix) { extern "C" { LIBSESSION_C_API unsigned char* session_random(size_t size) { - auto result = session::random::random(size); auto* ret = static_cast(malloc(size)); - std::memcpy(ret, result.data(), result.size()); + session::random::fill(std::span{ret, size}); return ret; } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 56eba9de7..8b232b5c1 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -144,7 +144,7 @@ std::vector encrypt_for_recipient_deterministic( // To make our ephemeral seed we're going to hash: SENDER_SEED || RECIPIENT_PK || MESSAGE with a // keyed blake2b hash. - cleared_array seed; + cleared_uchars seed; crypto_generichash_blake2b_state st; crypto_generichash_blake2b_init(&st, BOX_HASHKEY.data(), BOX_HASHKEY.size(), seed.size()); crypto_generichash_blake2b_update(&st, ed25519_privkey.data(), 32); @@ -152,15 +152,15 @@ std::vector encrypt_for_recipient_deterministic( crypto_generichash_blake2b_update(&st, message.data(), message.size()); crypto_generichash_blake2b_final(&st, seed.data(), seed.size()); - cleared_array eph_sk; - cleared_array eph_pk; + cleared_uchars eph_sk; + cleared_uchars eph_pk; crypto_box_seed_keypair(eph_pk.data(), eph_sk.data(), seed.data()); // The nonce for a sealed box is not passed but is implicitly defined as the (unkeyed) blake2b // hash of: // EPH_PUBKEY || RECIPIENT_PUBKEY - cleared_array nonce; + cleared_uchars nonce; crypto_generichash_blake2b_init(&st, nullptr, 0, nonce.size()); crypto_generichash_blake2b_update(&st, eph_pk.data(), eph_pk.size()); crypto_generichash_blake2b_update(&st, recipient_pubkey.data(), recipient_pubkey.size()); @@ -327,7 +327,7 @@ std::vector encrypt_for_blinded_recipient( } // Encrypt using xchacha20-poly1305 - cleared_array nonce; + cleared_uchars nonce; randombytes_buf(nonce.data(), nonce.size()); std::vector ciphertext; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 232d678b8..fa49be92d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -17,26 +17,6 @@ #include "WebSocketResources.pb.h" #include "session/export.h" -static_assert( - sizeof(SESSION_PROTOCOL_GENERATE_PROOF_HASH_PERSONALISATION) - 1 == - crypto_generichash_blake2b_PERSONALBYTES); - -static_assert( - sizeof(SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION) - 1 == - crypto_generichash_blake2b_PERSONALBYTES); - -static_assert( - sizeof(SESSION_PROTOCOL_ADD_PRO_PAYMENT_HASH_PERSONALISATION) - 1 == - crypto_generichash_blake2b_PERSONALBYTES); - -static_assert( - sizeof(SESSION_PROTOCOL_SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION) - 1 == - crypto_generichash_blake2b_PERSONALBYTES); - -static_assert( - sizeof(SESSION_PROTOCOL_GET_PRO_DETAILS_HASH_PERSONALISATION) - 1 == - crypto_generichash_blake2b_PERSONALBYTES); - // clang-format off const session_protocol_strings SESSION_PROTOCOL_STRINGS = { .build_variant_apk = string8_literal("APK"), @@ -78,11 +58,9 @@ session::array_uc32 proof_hash_internal( // This must match the hashing routine at // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 session::array_uc32 result = {}; - crypto_generichash_blake2b_state state = {}; - session::make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION) - 1}); + crypto_generichash_blake2b_state state; + crypto_generichash_blake2b_init_salt_personal( + &state, nullptr, 0, 32, nullptr, session::BUILD_PROOF_PERS.data()); crypto_generichash_blake2b_update(&state, &version, sizeof(version)); crypto_generichash_blake2b_update(&state, gen_index_hash.data(), gen_index_hash.size()); crypto_generichash_blake2b_update(&state, rotating_pubkey.data(), rotating_pubkey.size()); @@ -1122,19 +1100,6 @@ DecodedCommunityMessage decode_for_community( return result; } -void make_blake2b32_hasher( - crypto_generichash_blake2b_state* hasher, std::string_view personalization) { - assert(personalization.data() == nullptr || - (personalization.data() && - personalization.size() == crypto_generichash_blake2b_PERSONALBYTES)); - crypto_generichash_blake2b_init_salt_personal( - hasher, - /*key*/ nullptr, - 0, - 32, - /*salt*/ nullptr, - reinterpret_cast(personalization.data())); -} } // namespace session using namespace session; From 3f6244cbdfc4c9218992d79d4738067a2ca3303e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 19:11:42 -0300 Subject: [PATCH 005/193] WIP - session db, PFS, device linking, etc. --- .gitmodules | 3 + external/CMakeLists.txt | 26 + external/session-sqlite | 1 + include/session/config/namespaces.h | 5 + include/session/core.hpp | 161 ++++ include/session/core/callbacks.hpp | 75 ++ include/session/core/component.hpp | 34 + include/session/core/devices.hpp | 195 +++++ include/session/core/globals.hpp | 80 ++ include/session/core/pro.hpp | 52 ++ .../session/core/schema/schema_registry.hpp | 22 + src/CMakeLists.txt | 19 + src/core.cpp | 70 ++ src/core/component.cpp | 15 + src/core/devices.cpp | 749 ++++++++++++++++++ src/core/globals.cpp | 151 ++++ src/core/pro.cpp | 76 ++ src/core/schema/000_devices.sql | 62 ++ src/core/schema/000_globals.sql | 6 + src/core/schema/000_pro_revocations.sql | 4 + src/core/schema/CMakeLists.txt | 45 ++ src/core/schema/README | 31 + src/core/schema/apply_schema.cpp.in | 14 + src/core/schema/schema_registry.cpp.in | 15 + src/core/schema/sql_migration.cpp.in | 10 + 25 files changed, 1921 insertions(+) create mode 160000 external/session-sqlite create mode 100644 include/session/core.hpp create mode 100644 include/session/core/callbacks.hpp create mode 100644 include/session/core/component.hpp create mode 100644 include/session/core/devices.hpp create mode 100644 include/session/core/globals.hpp create mode 100644 include/session/core/pro.hpp create mode 100644 include/session/core/schema/schema_registry.hpp create mode 100644 src/core.cpp create mode 100644 src/core/component.cpp create mode 100644 src/core/devices.cpp create mode 100644 src/core/globals.cpp create mode 100644 src/core/pro.cpp create mode 100644 src/core/schema/000_devices.sql create mode 100644 src/core/schema/000_globals.sql create mode 100644 src/core/schema/000_pro_revocations.sql create mode 100644 src/core/schema/CMakeLists.txt create mode 100644 src/core/schema/README create mode 100644 src/core/schema/apply_schema.cpp.in create mode 100644 src/core/schema/schema_registry.cpp.in create mode 100644 src/core/schema/sql_migration.cpp.in diff --git a/.gitmodules b/.gitmodules index 8d6f7cfea..5a2ee4c52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,6 +10,9 @@ [submodule "external/session-router"] path = external/session-router url = https://github.com/session-foundation/session-router.git +[submodule "external/session-sqlite"] + path = external/session-sqlite + url = https://github.com/session-foundation/session-sqlite.git [submodule "external/date"] path = external/date url = https://github.com/HowardHinnant/date.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 99edc1f40..62ea1bd1c 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -105,6 +105,28 @@ set(BUILD_STATIC_libzstd ON CACHE BOOL "" FORCE) session_dep(libzstd 1.5) libsession_static_bundle(sessiondep::libzstd) +# TODO FIXME: integrate this with the updated networking PR, which will end up right about here when +# merging. This can basically just get deleted once we are always building session-router. +if(NOT TARGET mlkem_native::mlkem768) + file(GLOB_RECURSE mlkem_sources + session-router/external/mlkem-native/mlkem/src/*.c + session-router/external/mlkem-native/mlkem/src/*.S) + + add_library(mlkem_native768 STATIC ${mlkem_sources}) + + target_compile_definitions(mlkem_native768 PUBLIC + MLK_CONFIG_NO_RANDOMIZED_API + MLK_CONFIG_PARAMETER_SET=768 + MLK_CONFIG_NAMESPACE_PREFIX=sr_mlkem768 + MLK_CONFIG_NO_SUPERCOP + MLK_CONFIG_USE_NATIVE_BACKEND_ARITH + MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202 + ) + + target_include_directories(mlkem_native768 PUBLIC session-router/external/mlkem-native/mlkem) + + add_library(mlkem_native::mlkem768 ALIAS mlkem_native768) +endif() set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use @@ -114,6 +136,10 @@ sessiondep_or_submodule(nlohmann_json 3.7.0 nlohmann-json nlohmann_json::nlohman session_dep(simdutf 7) libsession_static_bundle(sessiondep::simdutf) + +add_subdirectory(session-sqlite) + + # We need Howard Hinnant's header-only date library for now because the STL implementation of # std::chrono::parse() is spotty or broken on: # - Apple. libc++ date parsing is only available when on XCode 16.3+ *and* targetting macOS 15+. diff --git a/external/session-sqlite b/external/session-sqlite new file mode 160000 index 000000000..75746b6ff --- /dev/null +++ b/external/session-sqlite @@ -0,0 +1 @@ +Subproject commit 75746b6ffa1ca0b6f8365e7cf799616e5d2edf2b diff --git a/include/session/config/namespaces.h b/include/session/config/namespaces.h index b96703233..47220525f 100644 --- a/include/session/config/namespaces.h +++ b/include/session/config/namespaces.h @@ -24,6 +24,11 @@ typedef enum NAMESPACE { NAMESPACE_GROUP_INFO = 13, NAMESPACE_GROUP_MEMBERS = 14, + // Device group namespaces: + NAMESPACE_DEVICE_GROUP = 21, + NAMESPACE_DEVICE_LINK = 22, + NAMESPACE_DEVICE_PUBKEYS = -21, + // The local config should never be pushed but this gives us a nice identifier for each config // type NAMESPACE_LOCAL = 9999, diff --git a/include/session/core.hpp b/include/session/core.hpp new file mode 100644 index 000000000..fbc9b1b20 --- /dev/null +++ b/include/session/core.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include + +#include "core/callbacks.hpp" +#include "core/devices.hpp" +#include "core/globals.hpp" +#include "core/pro.hpp" + +/// The "Core" class is intended to be used by a Session account instance to hold account state. It +/// manages an encrypted sqlite database for storing account keys, messages, and other account +/// state. +/// +/// This class is currently in an early state with only partial database, but this class is intended +/// to eventually grow to the point where it contains the entire data model of a Session account +/// (i.e. conversations, contacts, messages, etc.) and manages that data (e.g. by fetching data +/// from swarms). +/// +/// Currently Core does not yet have its own network stack and so the integrating application must +/// update the Core instance when it receives the appropriate data (such as pro revocations) from +/// the network. +/// +/// The typical intended flow for using the Core is to construct it early in the application and +/// store it for the application duration: +/// +/// session::core::Core core{ +/// std::filesystem::path{"/path/to/libsession.db"}, +/// session::sqlite::argon2id_password{"user-supplied password"} +/// }; +/// +/// (Or keep it in a unique or shared ptr if you have more complex ownership needs). +/// +/// The above examples shows a usage where the database is: +/// - encrypted using AEGIS-256 +/// - encrypted using a password resulting from argon2id password hashing +/// - is fully encrypted, with the first 16 bytes of the file containing the password salt. +/// +/// The above can be modified to pass any of the options supported by session::sqlite::Database, but +/// some of the most common ones are depicted here. +/// +/// If you have secure storage of a 32 byte secure random value (for example, 32 bytes generated by +/// libsodium's randombytes_buf) then instead of the argon2id_password argument you can pass a +/// raw_key: +/// +/// session::core::Core core{ +/// std::filesystem::path{"/path/to/libsession.db"}, +/// session::sqlite::raw_key{key} +/// }; +/// +/// If you have such a raw key that you can keep secure, this will open the database substantially +/// faster than needing to perform an argon2id hash. DO NOT use this approach with a user-supplied +/// password. +/// +/// (Note that the above makes a secure copy when opening the database. If you cannot avoid making +/// a copy yourself, remember to copy into at least a `session::cleared_uc32` or use similar secure +/// clearing after use to ensure the key bytes does not remain in unused process memory). +/// +/// If you want password protection instead of secure raw key protection then replace +/// +/// session::sqlite::raw_key{key} +/// +/// with +/// +/// session::sqlite::argon2id_password{user_pass} +/// +/// You can optionally crank up the argon2 settings for a more secure (but slower to open) database +/// by adding additional arguments to the argon2id_password constructor; see the session-sqlite +/// documentation for details. +/// +/// For iOS, where full encryption causes the OS to kill the process but opens up a magic special +/// snowflake exception if it thinks your file is SQLite, you can pass an extra argument value: +/// +/// session::sqlite::plaintext_header +/// +/// which will cause the initial 24 bytes of the file to be unencrypted, allowing the file to pass +/// iOS's sniff test to get magic permissions. Note that if you combine this with argon2id_password +/// you must also create and store a session::sqlite::salt value and pass that every time the +/// database is opened. The salt should be random bytes generated by a cryptographically secure +/// RNG, but can be stored without encryption once generated (i.e. it is not a sensitive value). +/// +/// Example construction: +/// +/// ```C++ +/// #include +/// #include +/// +/// int main() { +/// +/// session::core::Core core{ +/// std::filesystem::path{"/path/to/libsession.db"}, +/// session::sqlite::argon2id_password{"correct horse battery staple"} +/// }; +/// +/// run_my_app(core); +/// } +/// +/// +/// // Later on somewhere deep inside `run_my_app` when an updated revocation list is received: +/// try { +/// core.pro.update_revocations(...); +/// } catch (const std::exception& e) { +/// log::warning(cat, "Failed to update Pro revocation list: {}", e.what()); +/// } +/// +/// // When checking to verify a Pro account proof: +/// if (!core.pro.proof_is_revoked(...)) { +/// // bro is Pro! +/// } +/// +/// ``` + +namespace session::pro_backend { +struct ProRevocationItem; +}; // namespace session::pro_backend + +namespace session::core { + +namespace detail { + class CoreComponent; +} + +class Core { + sqlite::Database db; + friend class detail::CoreComponent; + + core::callbacks callbacks; + + // Called during the constructor: the database is opened and all members are constructed, but + // `init()` hasn't called called yet. + void apply_migrations(); + + std::list _comp_init; + void register_comp_init(detail::CoreComponent* c); + + // Performs the non-templated part of Core construction: this executes any needed database + // migrations, and then calls init() on each sub-component. + void init(); + + public: + // Constructor taking a specific encryption type, any number of session::SQLite database + // options; anything else gets passed through to the individual component constructors. + template + Core(core::callbacks callbacks, std::filesystem::path db_path, const DBOpts&... opts) : + callbacks{std::move(callbacks)}, db{std::move(db_path), opts...} { + init(); + } + + // Global value storage. This are used by some components, but can also be used by the + // application to persist settings. + Globals globals{*this}; + + // Session Pro-related capabilities + Pro pro{*this}; + + // Device groups for handling shared encryption key among account devices + Devices devices{*this}; +}; + +} // namespace session::core diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp new file mode 100644 index 000000000..ebd4af709 --- /dev/null +++ b/include/session/core/callbacks.hpp @@ -0,0 +1,75 @@ +#pragma once +#include +#include + +namespace session::core { + +class Core; + +/// Struct holding application callbacks to fire when libsession Core events happen to allow the +/// Core object to fire into the application front-end. +struct callbacks { + + /// Callback that is invoked when a device linking request is received for entry into the device + /// group. This is expected to notify the user of the linking request, and ask them to confirm + /// it. Generally this should be followed (after user interaction) by a call to one of the + /// core.devices methods: ignore_request(), accept_request(), delete_request() with the reqid + /// value. + /// + /// This may fire multiple times: it generally fires when the request first comes in, but + /// will also fire during startup if there is a still-active request that has not been + /// accepted, ignored, or deleted. (This is so that Session a shutdown or crash does not + /// lose a device request). + /// + /// It may also not fire at all if the request has been superceded (such as being accepted + /// by a third device). + /// + /// This request is not fired for the devices own linking request, i.e. when this device is the + /// one requesting entry into a device group. + /// + /// If this callback is not set then new device link requests are ignored by this device. + /// + /// Parameters: + /// - reqid -- a unique identifier for this request; this is only used within the current + /// Core object, and is used to uniquely identify the request. It will *not* work across + /// Core teardown and restart. + /// - new_device -- the new device metadata included in the link request. Crucial in this info + /// is the link_emoji sequence: a vector of 8 emoji (each in a separate utf8 string, leaving + /// separation, display, etc. up to the front-end) that provides a visual identifier of the + /// account device identifier, and is used to visually verify that the device making the + /// request is in fact the same device that is being confirmed. + std::function device_link_request; + + /// Callback that is invoked when a new device has been linked to the account. If a batch + /// of messages being processed includes both a device link request *and* an acceptance + /// (such as could happen if third device accepts the request) then only this, not the + /// request, will be fired. + /// + /// This callback is not fired if *this* is the device that has been added: see + /// device_self_added instead for that case. + /// + /// Note that this is fired once the new device is confirmed via stored swarm message, i.e. + /// it does not fire instantly upon calling `accept_request()`. + /// + /// Paramters: + /// - reqid -- if `on_device_link_request` had previously been called for this device, this + /// value will be the same value, allowing the application to correlate linking requests and + /// acceptance. If there was no previous link request (such as when catching up on device + /// updates performed by other account devices) then the value will be 0. + /// - new_device -- the metadata about the new device. + std::function device_added; + + /// Callback invoked when *this* device has been confirmed linked to the account by another + /// device. + std::function device_self_added; + + /// Callback that is invoked if we determine that a device has been kicked out of the device + /// group, either initiated by this device or another device. This does not, however, fire if + /// the *current* device gets kicked out; see device_self_removed for that. + /// + /// Parameters: + /// - removed_device -- the most recent info we have (locally) for the removed device. + std::function device_removed; +}; + +} // namespace session::core diff --git a/include/session/core/component.hpp b/include/session/core/component.hpp new file mode 100644 index 000000000..82c7823ee --- /dev/null +++ b/include/session/core/component.hpp @@ -0,0 +1,34 @@ +#pragma once + +namespace session::sqlite { +class Connection; +} +namespace session::core { + +class Core; + +namespace detail { + // Internal base class bridge between Core and the various components of core. This bridge + // can be used to allow components to access selected private parts of core, such as the + // database, without needing components to be direct friends of Core. + class CoreComponent { + protected: + friend class core::Core; + Core& core; + + // Gets a thread-unique database connection from the Core's Database's connection pool. This + // is unique to the calling thread and must not be used across threads. + sqlite::Connection conn(); + + explicit CoreComponent(Core& core); + + // Default component `init()` does nothing; classes can override this if they want to be + // called after database migrations are complete, but still during the parent Core + // construction. This will be called on each CoreComponent-derived member of Core, in the + // same order that those members were constructed (i.e. class declaration order). + virtual void init() {} + }; + +} // namespace detail + +} // namespace session::core diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp new file mode 100644 index 000000000..aee56d9c8 --- /dev/null +++ b/include/session/core/devices.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "component.hpp" + +namespace session::core { + +class Core; + +namespace device { + + enum class Type { + Unknown, + Session_iOS, + Session_Desktop, + Session_Android, + }; + + enum class State { + Registered = 0, ///< Device is in the account's registered device set + Pending = 1, ///< Local device info that has a pending request to join the account's device + ///< set + Unregistered = 2, ///< Local device info that cannot be pushed because the device is not + /// currently in the device group. + }; + + // Value returned to indicate the push status of a device info or account keys update. + enum class PushStatus { + Synced = 0, // We have pushed and confirmed (i.e. fetched the update) + Pushed = 1, // We have pushed, but not yet confirmed + Pending = 2, // We need to push, but haven't yet done so + NotInGroup = 3, // We are not in the device group and so can't push + }; + + struct Info { + // Unique device id, in raw bytes. Typically randomized during device initial setup. + std::array id; + + // Device seqno. Incremented on device key rotation and/or info updates. + int64_t seqno; + + // Timestamp of the most recent update. + std::chrono::sys_seconds timestamp; + + // The device type; one of the above enum values, or DevType::Unknown if the device type is + // not one of the standard Session clients. + Type type; + + // When device type is not one of the standard session clients, this will be set to a + // free-form string indicating the device type. Will be empty if no device type is provided + // in the device info at all. When DevType has a non-Unknown value, this will be + // empty/ignored. + std::string other_device; + + // Device-provided description of itself. This could contain the OS type or version, + // possible a device nickname, but is generally free-form data. + std::string description; + + // This computes the current "device emoji" sequence, which depends on the device id and + // pubkeys. This allows a user to visually identify (and verify) a device within the device + // list at any time, but its primary purpose is for identifying the device during linking. + + // Indicates whether the device is registered or not. + State state; + + // Application version triplet as reported by the device. The 2nd and 3rd values will + // always be in [0, 999]. (If setting device info, they will be clamped if outside this + // range). + std::array version; + + // The current device-specific X25519 pubkey + std::array pk_x25519; + + // The current device-specific MLKEM-768 pubkey + std::array pk_mlkem768; + + // Unknown extra keys. Single-character keys in here are reserved for future + // libsession-util use; longer strings can be used for custom client data, if needed. + oxenc::bt_dict extra; + }; + + using map = std::map, Info>; + + struct decryption_failed : std::runtime_error { + using std::runtime_error::runtime_error; + }; +}; // namespace device + +class Devices final : detail::CoreComponent { + public: + private: + friend class Core; + explicit Devices(Core& c) : detail::CoreComponent{c} {} + + void init() override; + + std::array self_id; + + // Encrypts the inner device data for all the members of the device group. + std::vector encrypt_device_data(const device::map& devices); + + // Inverse of encrypt_device_data. Throws if invalid. Throws `Devices::decryption_failed` if + // the message was parsed successfully, but we failed to decrypt any of its encrypted values. + device::map decrypt_device_data(std::span data); + + public: + // Returns the current device's random identifier, in hex. + std::string device_id() const; + + // Returns info for all registered and/or pending devices and/or unregistered devices for this + // account. + device::map devices( + bool include_registered = true, + bool include_pending = false, + bool include_unregistered = false); + + // Returns *this* device's info for this account, and whether the device is registered in the + // device list. + std::pair device_info(); + + // Updates this device's info locally to match the given info; if the current device is + // registered then this dirties the device config data, requiring a push. + // + // The state and pk_* fields of the input value are ignored. + void update_info(const device::Info& info); + + // Stores the X25519 + MLKEM768 keys that make up an "X-Wing" key + struct XWingKeys { + cleared_uc32 x25519_sec; + std::array x25519_pub; + cleared_array mlkem768_sec; + std::array mlkem768_pub; + }; + + struct DeviceKeys : XWingKeys { + std::chrono::sys_seconds created; + std::optional rotated; + }; + + // Rotates the device keys used for encrypting device group data. This also implicitly updates + // the current device's public keys. If the current device is registered, calling this will + // dirty the config data and require another push. + // + // This returns the newly created keys. (It can be safely discarded as it will already be + // stored in the database). + DeviceKeys rotate_device_keys(); + + // Returns current and recent local device private keys. This will be sorted with most recent + // key first. If there is no current key at all, this generates one. + std::vector active_device_keys(); + + // Returns true if the device key updates needs to be pushed for other account devices to + // receive. Returns an enum value: + // - + + struct AccountKeys : XWingKeys { + std::chrono::sys_seconds created; + std::optional rotated; + }; + + // Returns the current active account keys after pruning obsolete ones: that is, the current key + // plus all keys that were rotated away fewer than 16 days ago. (16 days = 14 day max incoming + // message TTL + 24h sender key update lag + 24h safety margin). Keys are returned sorted from + // newest to oldest. + std::vector active_account_keys(); + + // Returns the time when this device's unique device key is due to be rotated. Returns nullopt + // if this device is not currently part of the device group. + std::optional next_device_rotation(); + + bool device_rotation_due() { + auto t = next_device_rotation(); + return t && *t >= std::chrono::system_clock::now(); + } + + // Return true if the account key is due to be rotated by this device. Returns nullopt if this + // device is not currently part of the device group. + std::optional next_account_rotation(); + + bool account_rotation_due() { + auto t = next_account_rotation(); + return t && *t >= std::chrono::system_clock::now(); + } +}; + +} // namespace session::core diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp new file mode 100644 index 000000000..61a3d125e --- /dev/null +++ b/include/session/core/globals.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "component.hpp" + +namespace session::core { + +class Core; + +// Core component contains one-off global values that don't make sense storing in a table, typically +// because the value is highly special purpose or is only used in one single place. If you ever +// find yourself wanting to put multiple values in here under the same key, that is a sign that you +// should not be using this class and should instead refactor to use proper table relations. +// +// A note on keys: to avoid conflicts, external users of these globals should use prefix names that +// are unlikely to conflict with other uses. For example, "session_ios_dark_mode" is a decent name, +// but "pubkey" is a terrible one. All internal libsession keys in this table begin with an +// underscore, and should never be accesses outside libsession itself. +// +class Globals final : detail::CoreComponent { + + private: + friend class Core; + explicit Globals(Core& c) : detail::CoreComponent{c} {} + + // Holds the account seed; loaded during initialization (created if it doesn't exist). A new + // account seed is generated during initialization if the database doesn't contain one (e.g. if + // brand new). + // + // Read-only access is available via the account_seed() method, or via the `seed()` + // CoreComponent base class method from other components. + session::secure_buffer _account_seed; + std::array _pubkey_ed25519; + std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix + + void init() override; + + public: + // Retrieval methods. These query for the given key and, if the type matches, return the given + // value. You get back nullopt if the database key does not exist, or if it contains + std::optional get_integer(std::string_view key); + std::optional get_real(std::string_view key); + std::optional get_text(std::string_view key); + std::optional> get_blob(std::string_view key); + // Same as get_blob, but allocates a libsodium secure buffer to old the value. + // + // Do not use this to access the "seed" value: that value is cached in the Core object and + // accessible via CoreComponent::access_seed(). + std::optional get_blob_secure(std::string_view key); + // Reads a fixed size blob into `to`. If the database does not contain a BLOB value of byte + // length `to.size()`, returns false; other writes the blob value to `to` and returns true. + bool get_blob_to(std::string_view key, std::span to); + + // Retrieves the value of whatever type it currently contains. Returns a std::monostate if the + // database key is not set at all, otherwise of of the other variant values. + std::variant> get( + std::string_view key); + std::variant get_secure( + std::string_view key); + + // Assignment. If the database key already exists, this overwrites it. + void set(std::string_view key, int64_t integer); + void set(std::string_view key, double real); + void set(std::string_view key, std::string_view text); + void set(std::string_view key, std::span blob); + + session::secure_buffer::r_accessor account_seed() { return _account_seed.access(); } + // These are computed from the account_seed during construction: + std::span session_id() { return _session_id; } + std::span pubkey_ed25519() { return _pubkey_ed25519; } +}; + +} // namespace session::core diff --git a/include/session/core/pro.hpp b/include/session/core/pro.hpp new file mode 100644 index 000000000..be8f862dc --- /dev/null +++ b/include/session/core/pro.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +#include "component.hpp" + +namespace session::pro_backend { +struct ProRevocationItem; +} +namespace session::core { + +class Core; + +class Pro final : detail::CoreComponent { + public: + /// API: core/Pro::pro_proof_is_revoked + /// + /// Check if the proof identified by its `gen_index_hash` is revoked as of the given + /// timestamp from the list of proofs stored in the database. + /// + /// Outputs: + /// - `bool` -- True if the proof was revoked, false otherwise. + bool proof_is_revoked( + std::span gen_index_hash, + std::chrono::sys_time unix_ts); + + /// API: core/Pro::pro_update_revocations + /// + /// Update the list of pro revocations. If the `revocations_ticket` matches the current ticket, + /// this is a no-op. + /// + /// Inputs: + /// - `revocations_ticket` -- Ticket that describes the version of the revocations. This value + /// comes alongside the revocation list when queried. This ticket changes whenever the + /// revocation list is updated and is used to identify when an actual update is needed. + /// - `revocations` -- New list of Session Pro revocations. + void update_revocations( + uint32_t ticket, std::span revocations); + + private: + friend class Core; + + explicit Pro(Core& core) : detail::CoreComponent{core} {} + + // Stores the version of the revocation list that we last updated. Used as an optimization to + // short-circuit updates that are the same as the previous update. + std::optional revocations_ticket_; +}; + +} // namespace session::core diff --git a/include/session/core/schema/schema_registry.hpp b/include/session/core/schema/schema_registry.hpp new file mode 100644 index 000000000..913a5af67 --- /dev/null +++ b/include/session/core/schema/schema_registry.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace session::sqlite { +class Connection; +} +namespace session::core { +class Core; +} + +namespace session::core::schema { + +struct Migration { + std::string name; + void (*apply)(session::sqlite::Connection&, Core& core); +}; + +extern const std::span MIGRATIONS; + +} // namespace session::core::schema diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0409512fb..b52b88a68 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,25 @@ target_link_libraries(crypto libsession::protos ) +# libsession "Core" for maintaining persistent Session client state +add_libsession_util_library(core + core.cpp + core/component.cpp + core/devices.cpp + core/pro.cpp +) +add_subdirectory(core/schema) + +target_link_libraries( + core + PUBLIC + crypto + PRIVATE + sessiondep::libsodium + session::SQLite + mlkem_native::mlkem768 +) + target_link_libraries(config PUBLIC crypto diff --git a/src/core.cpp b/src/core.cpp new file mode 100644 index 000000000..503ca7eb4 --- /dev/null +++ b/src/core.cpp @@ -0,0 +1,70 @@ +#include + +#include +#include +#include +#include +#include + +#include "session/core/component.hpp" + +namespace session::core { + +namespace log = oxen::log; +using namespace session::sqlite; + +void Core::init() { + apply_migrations(); + + for (auto* component : _comp_init) + component->init(); + + _comp_init.clear(); +} + +void Core::register_comp_init(detail::CoreComponent* c) { + _comp_init.push_back(c); +} + +void Core::apply_migrations() { + auto cat = log::Cat("schema"); + + auto conn = db.conn(); + exec_query(conn.sql, R"( +CREATE TABLE IF NOT EXISTS migrations_applied ( + name TEXT PRIMARY KEY NOT NULL +) STRICT +)"); + + std::unordered_set applied; + { + SQLite::Statement st{conn.sql, "SELECT name FROM migrations_applied"}; + while (st.executeStep()) + applied.insert(get(st)); + } + + log::debug(cat, "Checking schema migrations"); + for (const auto& [name, apply] : schema::MIGRATIONS) { + if (applied.count(name)) { + log::debug(cat, "Schema migration {} already applied", name); + continue; + } + + try { + log::info(cat, "Applying database schema migration {}", name); + + SQLite::Transaction tx{conn.sql}; + + apply(conn, *this); + conn.prepared_exec("INSERT INTO migrations_applied (name) VALUES (?)", name); + + tx.commit(); + } catch (const std::exception& e) { + log::critical(cat, "Database schema migration '{}' failed: {}", name, e.what()); + throw; + } + } + log::debug(cat, "All schema migrations are applied"); +} + +} // namespace session::core diff --git a/src/core/component.cpp b/src/core/component.cpp new file mode 100644 index 000000000..0854bd85f --- /dev/null +++ b/src/core/component.cpp @@ -0,0 +1,15 @@ +#include +#include +#include + +namespace session::core::detail { + +sqlite::Connection CoreComponent::conn() { + return core.db.conn(); +} + +CoreComponent::CoreComponent(Core& core) : core{core} { + core.register_comp_init(this); +} + +} // namespace session::core::detail diff --git a/src/core/devices.cpp b/src/core/devices.cpp new file mode 100644 index 000000000..74c8bddaf --- /dev/null +++ b/src/core/devices.cpp @@ -0,0 +1,749 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace session::core { + +using namespace oxen::log::literals; + +namespace log = oxen::log; +static auto cat = log::Cat("core.dev"); + +static constexpr auto dev_key = "device_unique_id"sv; + +void Devices::init() { + if (core.globals.get_blob_to(dev_key, self_id)) + log::info(cat, "Loaded existing unique device id: {}", oxenc::to_hex(self_id)); + else { + random::fill(self_id); + core.globals.set(dev_key, self_id); + log::info(cat, "Generated new unique device id: {}", oxenc::to_hex(self_id)); + } +} + +std::string Devices::device_id() const { + return oxenc::to_hex(self_id.begin(), self_id.end()); +} + +template +consteval unsigned char KEY_DOMAIN() = delete; +template <> +consteval unsigned char KEY_DOMAIN() { + return static_cast('D'); +} +template <> +consteval unsigned char KEY_DOMAIN() { + return static_cast('A'); +} + +template Keys> +static Keys keys_from_seed(std::span seed) { + Keys keys; + auto& [x_sec, x_pub, ml_sec, ml_pub] = static_cast(keys); + + crypto_xof_turboshake256_state st; + crypto_xof_turboshake256_init_with_domain(&st, KEY_DOMAIN()); + crypto_xof_turboshake256_update( + &st, reinterpret_cast(seed.data()), seed.size()); + crypto_xof_turboshake256_squeeze(&st, x_sec.data(), x_sec.size()); + crypto_scalarmult_curve25519_base(x_pub.data(), x_pub.data()); + + static_assert(MLKEM768_PUBLICKEYBYTES == sizeof(ml_pub)); + static_assert(MLKEM768_SECRETKEYBYTES == sizeof(ml_sec)); + + cleared_array ml_seed; + crypto_xof_turboshake256_squeeze(&st, ml_seed.data(), ml_seed.size()); + + if (0 != sr_mlkem768_keypair_derand(ml_pub.data(), ml_sec.data(), ml_seed.data())) + throw std::runtime_error{"ML-KEM-768 keygen failed!"}; + + return keys; +} + +Devices::DeviceKeys Devices::rotate_device_keys() { + // We store just one single seed value, then use TurboSHAKE256 to expand it into separate X25519 + // (32B) and MLKEM-768 (64B) seeds. + cleared_b32 seed; + random::fill(seed); + + // Call this mainly to ensure that we can successfully produce keys from this seed. + auto keys = keys_from_seed(seed); + + auto c = conn(); + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()); + c.prepared_exec("INSERT INTO device_privkeys (created, seed) VALUES (?, ?)", now.count(), seed); + + log::info( + cat, + "New rotating device keys generated: X25519[{}…{}], MLKEM768[{}…{}]", + oxenc::to_hex(keys.x25519_pub.begin(), keys.x25519_pub.begin() + 2), + oxenc::to_hex(keys.x25519_pub.end() - 2, keys.x25519_pub.end()), + oxenc::to_hex(keys.mlkem768_pub.begin(), keys.mlkem768_pub.begin() + 2), + oxenc::to_hex(keys.mlkem768_pub.end() - 2, keys.mlkem768_pub.end())); + + return keys; +} + +std::vector Devices::active_device_keys() { + std::vector keys; + auto c = conn(); + SQLite::Transaction tx{c.sql}; + bool have_active = false; + for (auto [seed, active] : c.prepared_results, int>( + "SELECT seed, rotated IS NULL FROM device_privkeys" + " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + keys.push_back(keys_from_seed(seed)); + if (active) + have_active = true; + } + + if (!have_active) { + log::info(cat, "No currently active device keys; generating a new one"); + keys.insert(keys.begin(), rotate_device_keys()); + } + + return keys; +} + +std::vector Devices::active_account_keys() { + std::vector keys; + + auto rotation_cutoff = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) - + 16 * 24h; + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + c.prepared_exec("DELETE FROM device_group_keys WHERE rotated < ?", rotation_cutoff.count()); + for (auto [id, created, rotated, seed, pk_ml, pk_x] : + c.prepared_results< + int64_t, + int64_t, + std::optional, + sqlite::blobn<32>, + sqlite::blobn, + sqlite::blobn<32>>("SELECT created, rotated, seed, pubkey_mlkem768, pubkey_x25519" + " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + keys.push_back(keys_from_seed(seed)); + if (0 != std::memcmp(keys.back().mlkem768_pub.data(), pk_ml.data(), pk_ml.size()) || + 0 != std::memcmp(keys.back().x25519_pub.data(), pk_x.data(), pk_x.size())) { + log::warning( + cat, + "device_group_keys row with id={} ignored: row contains invalid precomputed " + "pubkeys", + id); + keys.pop_back(); + } + } + + return keys; +} + +device::map Devices::devices( + bool include_registered, bool include_pending, bool include_unregistered) { + + std::string where_clause; + if (include_registered && include_pending && include_unregistered) + ; // leave empty to select all + else { + std::vector where_states; + if (include_registered) + where_states.push_back(static_cast(device::State::Registered)); + if (include_pending) + where_states.push_back(static_cast(device::State::Pending)); + if (include_unregistered) + where_states.push_back(static_cast(device::State::Unregistered)); + if (where_states.empty()) + return {}; + where_clause = "WHERE state IN ({})"_format(fmt::join(where_states, ",")); + } + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + device::map devs; + + for (auto [id, devid, state, changes, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : + c.prepared_results< + int64_t, + sqlite::blob_guts>, + int, + int, + int, + int64_t, + std::string, + std::string, + int64_t, + sqlite::blobn, + sqlite::blobn<32>>(R"( +SELECT id, unique_id, state, changes, seqno, timestamp, device_type, description, version, + pubkey_mlkem768, pubkey_x25519 +FROM devices +{} +ORDER BY unique_id +)"_format(where_clause))) { + auto& info = devs[devid]; + + info.id = devid; + info.seqno = seqno; + info.timestamp = std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; + info.type = device::Type::Unknown; + if (type.size() == 1) + switch (type[0]) { + case 'a': info.type = device::Type::Session_Android; break; + case 'd': info.type = device::Type::Session_Desktop; break; + case 'i': info.type = device::Type::Session_iOS; break; + } + if (info.type == device::Type::Unknown) + info.other_device = std::move(type); + info.description = std::move(desc); + info.state = static_cast(state); + info.version[2] = ver % 1000; + info.version[1] = ver / 1000 % 1000; + info.version[0] = ver / 1000000; + std::memcpy(info.pk_x25519.data(), pk_x.data(), info.pk_x25519.size()); + std::memcpy(info.pk_mlkem768.data(), pk_ml.data(), info.pk_mlkem768.size()); + + for (auto [key, value] : c.prepared_results( + "SELECT key, bt_value FROM device_unknown WHERE device = ? ORDER BY key", + id)) { + try { + info.extra[key] = oxenc::bt_deserialize(value); + } catch (const std::exception& e) { + log::warning(cat, "Failed to deserialize extra device data: {}", e.what()); + } + } + } + + return devs; +} + +namespace { + + // Called while building a bt dict to pull out any unknown intermediate keys immediately before + // appending a new one. E.g. call `write_extra(out, "a", it, end)` to write out any keys from + // `it` that precede "a". `it` is mutated, and left at the first value > "a", ready for the + // next call. The iterator range must be sorted (such as a bt_dict, or a std::map, but not an unordered_map). + template End> + void write_extras(bt_dict_producer& out, std::string_view until, It& it, End end) { + for (; it != end; ++it) { + auto& [k, v] = *it; + if (auto comp = k <=> until; comp >= 0) { + if (comp == 0) + // We found an exact match, which probably means we upgraded and learned what + // the key meant. We probably shouldn't get here at all, but just in case skip + // it so we don't break the bt_dict. + ++it; + return; + } + out.append_bt(k, v); + } + } + + // Combines a call to write_extras + out.append for appending simple bt dict keys with scalar + // values. + template End> + void write_next( + oxenc::bt_dict_producer& out, std::string_view key, const T& value, It& it, End end) { + write_extras(out, key, it, end); + out.append(key, value); + } + + std::string encode_device_data(const device::map& devices) { + oxenc::bt_dict_producer out; + for (const auto& [id, info] : devices) { + auto devout = out.append_dict( + std::string_view{reinterpret_cast(id.data()), id.size()}); + + auto xit = info.extra.cbegin(); + auto xend = info.extra.cend(); + write_next(devout, "#", info.seqno, xit, xend); + write_next(devout, "@", info.timestamp.time_since_epoch().count(), xit, xend); + write_next(devout, "M", info.pk_mlkem768, xit, xend); + write_next(devout, "X", info.pk_x25519, xit, xend); + write_next(devout, "d", info.description, xit, xend); + write_extras(devout, "t", xit, xend); + switch (info.type) { + case device::Type::Session_iOS: devout.append("t", "i"); break; + case device::Type::Session_Android: devout.append("t", "a"); break; + case device::Type::Session_Desktop: devout.append("t", "d"); break; + default: + if (!info.other_device.empty()) + devout.append("t", info.other_device); + } + auto ver = info.version[0] * 1000000 + std::clamp(info.version[1], 0, 999) * 1000 + + std::clamp(info.version[2], 0, 999); + write_extras(devout, "v", xit, xend); + if (ver != 0) + devout.append("v", ver); + + for (; xit != xend; ++xit) + out.append_bt(xit->first, xit->second); + } + + return std::move(out).str(); + } + + // Stores the current btdc key/value in `extra`; the value is consumed (i.e. the consumer + // advances to the next key). + void consume_extra(oxenc::bt_dict_consumer& btdc, oxenc::bt_dict& extra) { + auto& x = extra[std::string{btdc.key()}]; + if (btdc.is_string()) + x = btdc.consume_string(); + else if (btdc.is_unsigned_integer()) + x = btdc.consume_integer(); + else if (btdc.is_integer()) + x = btdc.consume_integer(); + else if (btdc.is_dict()) + x = btdc.consume_dict(); + else + x = btdc.consume_list(); + } + + // Consumes and stores any unknown extra fields from `btdc` up to (but not including) `key` into + // `extras` + void read_extras(oxenc::bt_dict_consumer& btdc, std::string_view key, oxenc::bt_dict& extra) { + while (btdc.key() < key) + consume_extra(btdc, extra); + } + + void decode_one(device::Info& info, oxenc::bt_dict_consumer dev, device::State state) { + read_extras(dev, "#", info.extra); + info.seqno = dev.require("#"); + + read_extras(dev, "@", info.extra); + info.timestamp = std::chrono::sys_seconds{std::chrono::seconds{dev.require("@")}}; + + read_extras(dev, "M", info.extra); + auto M = dev.require_span("M"); + std::memcpy(info.pk_mlkem768.data(), M.data(), M.size()); + + read_extras(dev, "X", info.extra); + auto X = dev.require_span("X"); + std::memcpy(info.pk_x25519.data(), X.data(), X.size()); + + read_extras(dev, "d", info.extra); + info.description = dev.maybe("d").value_or(""sv); + + read_extras(dev, "t", info.extra); + auto type = dev.require("t"); + info.other_device.clear(); + if (type == "i") + info.type = device::Type::Session_iOS; + else if (type == "a") + info.type = device::Type::Session_Android; + else if (type == "d") + info.type = device::Type::Session_Desktop; + else { + info.type = device::Type::Unknown; + info.other_device = type; + } + + read_extras(dev, "v", info.extra); + auto ver = dev.maybe("v").value_or(0); + info.version[0] = ver / 1000000; + info.version[1] = ver / 1000 % 1000; + info.version[2] = ver % 1000; + + while (!dev.is_finished()) + consume_extra(dev, info.extra); + } + + device::map decode_device_data(std::span data, device::State state) { + device::map devices; + + oxenc::bt_dict_consumer in{data}; + while (!in.is_finished()) { + auto in_id = in.key(); + // A 32-byte keys are device IDs, but we allow (and ignore) keys with other sizes to + // allow for future expansion + if (in_id.size() != 32) { + log::debug( + cat, + "Skipping unknown {}-length key: not a 32-byte device id", + in_id.size()); + continue; + } + + if (in.is_integer()) { + // An integer indicates a "device removed" timestamp, used to distinguish between + // "device removed" and "I don't know about the device yet". It gets pruned when + // updating once it hits a certain age threshold. + log::debug(cat, "Skipping recently removed device id={}", oxenc::to_hex(in_id)); + continue; + } + + std::array id; + std::memcpy(id.data(), in_id.data(), 32); + auto [it, ins] = devices.try_emplace(id); + if (!ins) + throw std::runtime_error{"Invalid encoded device data: duplicate devices ids"}; + + decode_one(it->second, in.consume_dict_consumer(), state); + } + + return devices; + } + + constexpr auto PERS_DEV_NONCE = "SessionDevDNonce"_b2b_pers; + constexpr auto PERS_KEY_NONCE = "SessionDevKNonce"_b2b_pers; + constexpr auto PERS_KEY_KEY = "SessionDevKeyKey"_b2b_pers; + constexpr auto PERS_KEY_KEY_IND = "SessionDevKeyInd"_b2b_pers; + + constexpr int bt_bytes_encoded(int x) { + int sz = 1 + x; + + do { + ++sz; + } while (x /= 10); + + return sz; + } + + static_assert(bt_bytes_encoded(0) == 2); // "0:" + static_assert(bt_bytes_encoded(9) == 11); // "9:…" + static_assert(bt_bytes_encoded(10) == 13); // "10:…" + static_assert(bt_bytes_encoded(99) == 102); // "99:…" + static_assert(bt_bytes_encoded(100) == 104); // "100:…" + +} // namespace + +std::vector Devices::encrypt_device_data(const device::map& devices) { + cleared_uc32 a; + random::fill(a); + + std::array A; + crypto_scalarmult_curve25519_base(A.data(), a.data()); + + int padded_count = devices.size(); + padded_count = (padded_count + 3) / 4 * 4; + + auto indices = std::views::iota(0, padded_count); + + // We randomize the positions of devices (and padding) in the list of keys, so build a random + // mapping first so that we place everything directly into its final position through it: + std::vector pos_map{indices.begin(), indices.end()}; + std::ranges::shuffle(pos_map, csrng); + + // Holds MLKEM ciphertexts: + std::vector ciphertext_raw; + ciphertext_raw.resize(MLKEM768_CIPHERTEXTBYTES * padded_count); + // Holds per-device-encrypted copies of the base key, each prefixed with a 2-byte key indicator + // hash: + std::vector enc_key_raw; + enc_key_raw.resize((2 + 32) * padded_count); + + // Accessor for the relevant, position-mapped subspan of ciphertext_raw/enc_key_raw containing + // the location of index i as a subspan of the raw vector: + auto ciphertext = indices | std::views::transform([&](int i) { + return std::span{ + ciphertext_raw.data() + pos_map[i] * MLKEM768_CIPHERTEXTBYTES, + MLKEM768_CIPHERTEXTBYTES}; + }); + + auto enc_indicator = + indices | std::views::transform([&](int i) { + return std::span{enc_key_raw.data() + pos_map[i] * (2 + 32), 2}; + }); + + auto enc_key = indices | std::views::transform([&](int i) { + return std::span{ + enc_key_raw.data() + pos_map[i] * (2 + 32) + 2, 32}; + }); + + sodium_array ml_ss_raw{MLKEM768_BYTES * devices.size()}; + + // Dynamic ss subspan accessor of ml_ss_raw, but *doesn't* go through the pos_map (unlike the + // above constructs), and only goes up to the actual number of devices, not the padded number + // (because this is never transmitted, and so not shuffled or padded). + auto ml_ss = std::views::iota(size_t{0}, devices.size()) | std::views::transform([&](size_t i) { + return std::span{ + ml_ss_raw.data() + i * MLKEM768_BYTES, MLKEM768_BYTES}; + }); + + cleared_uc32 rnd; + int i = -1; + for (auto& [devid, info] : devices) { + ++i; + random::fill(rnd); + if (0 != sr_mlkem768_enc_derand( + ciphertext[i].data(), + ml_ss[i].data(), + reinterpret_cast(info.pk_mlkem768.data()), + rnd.data())) + throw std::runtime_error{"ML-KEM-768 encapsulation failed!"}; + } + // Fill padding entries with randomness: + for (; i < padded_count; i++) + random::fill(ciphertext[i]); + + std::array nonce; + hash::blake2b_key_pers(nonce, A, PERS_DEV_NONCE, ciphertext_raw); + + cleared_uc32 key_base; + random::fill(key_base); + + auto plaintext_devices = encode_device_data(devices); + std::vector enc_devices; + enc_devices.resize(plaintext_devices.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + crypto_aead_xchacha20poly1305_ietf_encrypt( + enc_devices.data(), + nullptr, + reinterpret_cast(plaintext_devices.data()), + plaintext_devices.size(), + nullptr, + 0, + nullptr, + nonce.data(), + key_base.data()); + + cleared_uc32 ki; + cleared_uc32 aB; + i = -1; + for (auto& [devid, info] : devices) { + ++i; + auto eind = enc_indicator[i]; + auto ekey = enc_key[i]; + auto ct = ciphertext[i]; + + auto B = to_span(info.pk_x25519); + if (0 != crypto_scalarmult_curve25519(aB.data(), a.data(), B.data())) { + // This really shouldn't happen: we shouldn't have accepted an invalid pubkey in the + // first place. + log::error( + cat, + "X25519 scalarmult failed: device '{}' ({}) published an invalid X25519 " + "pubkey!", + oxenc::to_hex(devid), + info.description); + // Without a proper key, we can't properly encrypt for the device so we'll just have to + // fill the entry with random and move on. + random::fill(eind); + random::fill(ekey); + continue; + } + + hash::blake2b_key_pers(nonce, A, PERS_KEY_NONCE, ct, enc_devices); + hash::blake2b_pers(ki, PERS_KEY_KEY, aB, A, B, ml_ss[i], info.pk_mlkem768); + + static_assert(decltype(ekey)::extent == key_base.size()); + crypto_stream_xchacha20_xor( + ekey.data(), key_base.data(), key_base.size(), nonce.data(), ki.data()); + + // Hash a bunch of stuff together as a checksum to let decryption skip most not-for-me + // values. + hash::blake2b_pers(eind, PERS_KEY_KEY_IND, A, B, info.pk_mlkem768, ct, ekey); + } + // Fill padding entries with randomness: + for (; i < padded_count; i++) { + random::fill(enc_indicator[i]); + random::fill(enc_key[i]); + } + + // We're done: now we just need to encode everything together: + std::vector out; + out.resize( + 2 // Outer "d" ... "e" delimiters + + 3 + bt_bytes_encoded(A.size()) // "1:A" + "32:...(A eph pk)..." + + 3 + bt_bytes_encoded(ciphertext_raw.size()) // "1:C" + "NNNN:...(mlkem cts)..." + + 3 + bt_bytes_encoded(enc_key_raw.size()) // "1:K" + "NNN:...(encrypted keys)..." + + 3 + bt_bytes_encoded(enc_devices.size()) // "1:d" + "MMMM:...(enc device info)..." + + 3 + bt_bytes_encoded(64) // "1:~" + "64:...(Ed25519 signature)..." + ); + + oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; + + o.append("A", A); + o.append("C", ciphertext_raw); + o.append("K", enc_key_raw); + o.append("d", enc_devices); + o.append_signature( + "~", [seed = core.globals.account_seed()](std::span body) { + std::array sig; + crypto_sign_ed25519_detached( + sig.data(), + nullptr, + body.data(), + body.size(), + reinterpret_cast(seed.buf.data())); + return sig; + }); + + assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above + + return out; +} + +device::map Devices::decrypt_device_data(std::span enc_data) { + + oxenc::bt_dict_consumer in{enc_data}; + auto A = in.require_span("A"); + auto ciphertext_raw = in.require_span("C"); + auto enc_key_raw = in.require_span("K"); + auto enc_devices = in.require_span("d"); + + in.require_signature( + "~", [this](std::span body, std::span sig) { + if (0 != crypto_sign_ed25519_verify_detached( + sig.data(), + body.data(), + body.size(), + core.globals.pubkey_ed25519().data())) + throw std::runtime_error{ + "Invalid encrypted device message: signature verification failed"}; + }); + + in.finish(); + + if (ciphertext_raw.size() % MLKEM768_CIPHERTEXTBYTES != 0) + throw std::runtime_error{ + "Invalid encrypted device group data: invalid ciphertext size ({} is not N*{})"_format( + ciphertext_raw.size(), MLKEM768_CIPHERTEXTBYTES)}; + const int count = ciphertext_raw.size() / MLKEM768_CIPHERTEXTBYTES; + if (enc_key_raw.size() % (32 + 2) != 0) + throw std::runtime_error{ + "Invalid encrypted device group data: invalid encrypted keys size ({} is not N*34)"_format( + enc_key_raw.size())}; + if (const int k_count = enc_key_raw.size() / (32 + 2); count != k_count) + throw std::runtime_error{ + "Invalid encrypted device data: ciphertext ({}) vs enc key ({}) size mismatch"_format( + count, k_count)}; + if (enc_devices.size() <= crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::runtime_error{ + "Invalid encrypted device data: encrypted data is too short ({}B)"_format( + enc_devices.size())}; + + auto indices = std::views::iota(0, count); + + // Accessors for chunk-by-chunk access to the ciphertext_raw/enc_key_raw spans: + auto ciphertext = indices | std::views::transform([&](int i) { + return std::span{ + ciphertext_raw.data() + i * MLKEM768_CIPHERTEXTBYTES, + MLKEM768_CIPHERTEXTBYTES}; + }); + auto enc_indicator = + indices | std::views::transform([&](int i) { + return std::span{enc_key_raw.data() + i * (2 + 32), 2}; + }); + auto enc_key = indices | std::views::transform([&](int i) { + return std::span{ + enc_key_raw.data() + i * (2 + 32) + 2, 32}; + }); + + auto active_keys = active_device_keys(); + + std::array devices_nonce; + hash::blake2b_pers(devices_nonce, PERS_DEV_NONCE, ciphertext_raw); + + cleared_uc32 ml_ss, aB, ki, key_base; + + std::vector plaintext_devices; + plaintext_devices.resize(enc_devices.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + // Trial decrypt until we find one that works, except that we can skip most of the heavy + // operations for most keys not intended for us. Note that we have to attempt each received key + // by all of our recent device keys because it might be a pre-rotation message encrypted using + // an older key, so even if we have only 4 incoming values, we might have 20 recent device keys + // meaning 80 potential decryptions. + bool found = false; + for (int i = 0; !found && i < count; i++) { + auto ct = ciphertext[i]; + auto ekey = enc_key[i]; + auto eind = enc_indicator[i]; + + std::array knonce; + hash::blake2b_key_pers(knonce, A, PERS_KEY_NONCE, ct, enc_devices); + + for (int active_i = 0; active_i < active_keys.size(); active_i++) { + const auto& k = active_keys[active_i]; + const auto& B = k.x25519_pub; + const auto& M = k.mlkem768_pub; + + // First work out the checksum hash; the vast majority of the time this won't match for + // a key other than our own (only 1/65535 chance of collision), and so we can short + // circuit and save a bunch of calculations. + std::array our_ind; + hash::blake2b_pers(our_ind, PERS_KEY_KEY_IND, A, B, M, ct, ekey); + if (!std::ranges::equal(our_ind, eind)) + continue; + + if (0 != crypto_scalarmult_curve25519(aB.data(), k.x25519_sec.data(), A.data())) { + log::warning(cat, "X25519 multiplication failed; ignoring encrypted entry"); + continue; + } + + if (0 != sr_mlkem768_dec(ml_ss.data(), ct.data(), k.mlkem768_sec.data())) { + log::warning(cat, "MLKEM768 decapsulation failed; skipping device entry"); + continue; + } + + // Now we have various shared secret data: hash it into the k[i] value that should have + // been used to encrypt the key_base value for us: + hash::blake2b_pers(ki, PERS_KEY_KEY, aB, A, B, ml_ss, M); + + // and then use it to recover the key_base: + static_assert(decltype(ekey)::extent == key_base.size()); + crypto_stream_xchacha20_xor( + key_base.data(), ekey.data(), ekey.size(), knonce.data(), ki.data()); + + // Now we can decrypt the encrypted payload: + if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + plaintext_devices.data(), + nullptr, + nullptr, + reinterpret_cast(enc_devices.data()), + enc_devices.size(), + nullptr, + 0, + devices_nonce.data(), + key_base.data())) { + found = true; + break; + } + + log::debug( + cat, + "Decryption of record {} against recent key {} failed; probably a checksum " + "false positive", + i, + active_i); + } + } + + if (!found) { + // There are a bunch of reasons for this: maybe we aren't in the device group, maybe it was + // corrupted, or many it is an old message and we don't have the keys for it anymore. + log::warning(cat, "Failed to decrypt incoming device data"); + throw device::decryption_failed{"Failed to decrypt incoming device data"}; + } + + return decode_device_data(plaintext_devices, device::State::Registered); +} + +} // namespace session::core diff --git a/src/core/globals.cpp b/src/core/globals.cpp new file mode 100644 index 000000000..df0fdf5b6 --- /dev/null +++ b/src/core/globals.cpp @@ -0,0 +1,151 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace session::core { + +namespace log = oxen::log; +auto cat = log::Cat("core.gbl"); + +using namespace std::literals; + +static const std::string GET_ONE = "SELECT value FROM globals WHERE key = ? AND typeof(value) = ?"s; + +std::optional Globals::get_integer(std::string_view key) { + return conn().prepared_maybe_get(GET_ONE, key, "integer"); +} +std::optional Globals::get_real(std::string_view key) { + return conn().prepared_maybe_get(GET_ONE, key, "real"); +} +std::optional Globals::get_text(std::string_view key) { + return conn().prepared_maybe_get(GET_ONE, key, "text"); +} +std::optional> Globals::get_blob(std::string_view key) { + std::optional> result; + auto c = conn(); + auto st = c.prepared_bind(GET_ONE, key, "blob"); + if (st->executeStep()) { + auto data = get(st); + result.emplace().reserve(data.size()); + result->assign(data.begin(), data.end()); + } + return result; +} +std::optional Globals::get_blob_secure(std::string_view key) { + std::optional result; + auto c = conn(); + auto st = c.prepared_bind(GET_ONE, key, "blob"); + if (st->executeStep()) + result.emplace(get(st)); + return result; +} +bool Globals::get_blob_to(std::string_view key, std::span to) { + auto c = conn(); + auto st = c.prepared_bind(GET_ONE, key, "blob"); + if (st->executeStep()) { + if (auto data = get(st); data.size() == to.size()) { + std::memcpy(to.data(), data.data(), to.size()); + return true; + } + } + return false; +} + +static const std::string GET_ANY = "SELECT value, typeof(value) FROM globals WHERE key = ?"s; + +template BlobLoader> +static auto get_variant_impl(sqlite::Connection&& c, std::string_view key, BlobLoader&& b) { + + using blob_t = decltype(b(std::declval())); + + std::variant result; + + auto st = c.prepared_bind(GET_ANY, key); + if (st->executeStep()) { + auto val = st->getColumn(0); + auto type = static_cast(st->getColumn(1)); + if (type == "int") + result.template emplace(std::move(val)); + else if (type == "text") + result.template emplace(std::move(val)); + else if (type == "blob") + result = b(sqlite::blob{std::move(val)}); + else if (type == "real") + result.template emplace(std::move(val)); + } + + return result; +} + +std::variant> Globals::get( + std::string_view key) { + return get_variant_impl(conn(), key, [](sqlite::blob data) { + std::vector v; + v.reserve(data.size()); + v.assign(data.begin(), data.end()); + return v; + }); +} + +std::variant +Globals::get_secure(std::string_view key) { + return get_variant_impl(conn(), key, [](sqlite::blob data) { return secure_buffer{data}; }); +} + +static const std::string SET_VAL = + "INSERT INTO globals (key, value) VALUES (?, ?)" + " ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value"s; + +void Globals::set(std::string_view key, int64_t integer) { + conn().prepared_exec(SET_VAL, key, integer); +} +void Globals::set(std::string_view key, double real) { + conn().prepared_exec(SET_VAL, key, real); +} +void Globals::set(std::string_view key, std::string_view text) { + conn().prepared_exec(SET_VAL, key, text); +} +void Globals::set(std::string_view key, std::span blob) { + conn().prepared_exec(SET_VAL, key, blob); +} + +void Globals::init() { + auto c = conn(); + SQLite::Transaction tx{c.sql}; + cleared_b32 seed; + bool have_seed = get_blob_to("_seed", seed); + if (!have_seed) { + // FIXME: we should allow full 32-byte seeds here, but for now this is compatible with the + // 16-byte/128-bit seed that Session accounts use which is 16 random bytes followed by 16 + // 0s: + randombytes_buf(seed.data(), 16); + std::memset(seed.data() + 16, 0, 16); + } + + auto rw = _account_seed.resize(64); + + crypto_sign_ed25519_seed_keypair( + _pubkey_ed25519.data(), + reinterpret_cast(rw.buf.data()), + reinterpret_cast(seed.data())); + _session_id[0] = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(_session_id.data() + 1, _pubkey_ed25519.data())) + // This *should* be impossible when starting from a seed because that would mean the seed + // generation produced an invalid Ed pubkey! + log::critical(cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); + + if (!have_seed) { + log::info(cat, "Generated new Session account seed"); + set("_seed", rw.buf); + } + + log::info(cat, "Initialized with Session ID: {}", oxenc::to_hex(_session_id)); +} + +} // namespace session::core diff --git a/src/core/pro.cpp b/src/core/pro.cpp new file mode 100644 index 000000000..ebbebcd69 --- /dev/null +++ b/src/core/pro.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include + +#include "SQLiteCpp/Transaction.h" +#include "session/sqlite.hpp" + +namespace session::core { + +bool Pro::proof_is_revoked( + std::span gen_index_hash, + std::chrono::sys_time unix_ts) { + return conn().prepared_get( + "SELECT EXISTS (SELECT 1 FROM pro_revocations" + " WHERE gen_index_hash = ? AND expiry_unix_ts_ms <= ?)", + gen_index_hash, + unix_ts.time_since_epoch().count()); +} + +/// API: core/Pro::pro_update_revocations +/// +/// Update the list of pro revocations. If the `revocations_ticket` matches the current ticket, +/// this is a no-op. +/// +/// Inputs: +/// - `ticket` -- Ticket that describes the version of the revocations. This value comes +/// alongside the revocation list when queried. This ticket changes whenever the revocation +/// list is updated and is used to identify when an actual update is needed. +/// - `revocations` -- New list of Session Pro revocations. +void Pro::update_revocations( + uint32_t ticket, std::span revocations) { + + if (revocations_ticket_ && ticket == *revocations_ticket_) + return; + + auto already_hashed = [](const array_uc32& a) { + size_t h; + std::memcpy(&h, a.data(), sizeof(h)); + return h; + }; + + auto c = conn(); + + SQLite::Transaction tx{c.sql}; + + std::unordered_set to_remove; + for (auto id : c.prepared_results>( + "SELECT gen_index_hash FROM pro_revocations")) + to_remove.insert(id); + + for (auto st = c.prepared_st( + "INSERT INTO pro_revocations (gen_index_hash, expiry_unix_ts_ms) VALUES (?, ?)" + " ON CONFLICT (gen_index_hash) " + " DO UPDATE SET expiry_unix_ts_ms = excluded.expiry_unix_ts_ms" + " WHERE excluded.expiry_unix_ts_ms != expiry_unix_ts_ms"); + const auto& revoke : revocations) { + + exec_query(st, revoke.gen_index_hash, revoke.expiry_unix_ts.time_since_epoch().count()); + to_remove.erase(revoke.gen_index_hash); + st->reset(); + } + + if (!to_remove.empty()) { + auto st = c.prepared_st("DELETE FROM pro_revocations WHERE gen_index_hash = ?"); + for (const auto& id : to_remove) { + exec_query(st, id); + st->reset(); + } + } + + tx.commit(); +} + +} // namespace session::core diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql new file mode 100644 index 000000000..e73775d17 --- /dev/null +++ b/src/core/schema/000_devices.sql @@ -0,0 +1,62 @@ + +-- Table storing all the device group info +CREATE TABLE devices ( + id INTEGER PRIMARY KEY NOT NULL, + unique_id device_id BLOB UNIQUE NOT NULL CHECK(length(id) == 32), + + state INTEGER NOT NULL CHECK(state == 0 OR state == 1 OR state == 2), -- registered, pending, unregistered + changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unpushed local device info changes + seqno INTEGER NOT NULL DEFAULT 1, + timestamp INTEGER NOT NULL, + device_type TEXT NOT NULL, -- typically a/i/d (Android/iOS/Desktop), but can be anything + description TEXT NOT NULL, -- freeform device description + version INTEGER NOT NULL, -- = 1000000*V + 1000*v + p for version "V.v.p" + pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) == 1184), + pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) == 32) +) STRICT; + +-- This table holds any extra info not captured by the above. The data is stored as key/value pairs +-- where the value is the bt-encoded data received in the last device info message. The purpose of +-- this is so that future versions that add new fields can have those unknown fields propagated by +-- older clients that do not yet understand them without the older clients silently dropping unknown +-- fields. +CREATE TABLE device_unknown ( + device INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE, + key TEXT NOT NULL, + bt_value BLOB NOT NULL, + PRIMARY KEY(device, key) +) STRICT; + +-- This table holds current and recent device private keys for *this* device, including the +-- timestamp then the device keypairs were created, and when they were rotated away from. +CREATE TABLE device_privkeys ( + id INTEGER PRIMARY KEY NOT NULL, + created INTEGER NOT NULL, -- unix timestamp + rotated INTEGER, -- timestamp when a newer key was added, superceding this key + pushed INTEGER, -- timestamp when this key was verified as pushed to the device group + seed BLOB NOT NULL CHECK(length(seed) == 32), +) STRICT; + +-- This trigger handles key rotation: whenever we insert a new key, any existing keys are +-- automatically rotated with the `creation` timestamp of the new row as the rotation timestamp. +CREATE TRIGGER device_privkey_rotation AFTER INSERT ON device_privkeys +FOR EACH ROW WHEN NEW.rotated IS NULL +BEGIN + UPDATE device_privkeys SET rotated = NEW.created WHERE rotated IS NULL; +END; + +-- This table holds current and recent *account* keys, which are shared within the device +-- group and have their public keys published for remote users to use to encrypt messages. +-- Unlike device_privkeys, these keys are shared among all devices in the device group. +CREATE TABLE device_group_keys ( + id INTEGER PRIMARY KEY NOT NULL, + created INTEGER NOT NULL, + rotated INTEGER, -- timestamp when a new key superceded this key + seed BLOB NOT NULL CHECK(length(seed) == 32), + pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) == 1184), + pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) == 32), + -- Virtual column containing the first two mlkem pubkey values to assist with lookups based on + -- incoming message key indicator: + key_indicator BLOB GENERATED ALWAYS AS (substr(pubkey_mlkem768, 1, 2)) VIRTUAL +) STRICT; +CREATE INDEX device_group_keys_ki_index ON device_group_keys(key_indicator); diff --git a/src/core/schema/000_globals.sql b/src/core/schema/000_globals.sql new file mode 100644 index 000000000..908ba2bd7 --- /dev/null +++ b/src/core/schema/000_globals.sql @@ -0,0 +1,6 @@ + +CREATE TABLE globals ( + key TEXT PRIMARY KEY NOT NULL, + value ANY NOT NULL +) STRICT; + diff --git a/src/core/schema/000_pro_revocations.sql b/src/core/schema/000_pro_revocations.sql new file mode 100644 index 000000000..0db407d32 --- /dev/null +++ b/src/core/schema/000_pro_revocations.sql @@ -0,0 +1,4 @@ +CREATE TABLE pro_revocations ( + gen_index_hash BLOB PRIMARY KEY NOT NULL, + expiry_unix_ts_ms INTEGER NOT NULL +) STRICT diff --git a/src/core/schema/CMakeLists.txt b/src/core/schema/CMakeLists.txt new file mode 100644 index 000000000..45539511a --- /dev/null +++ b/src/core/schema/CMakeLists.txt @@ -0,0 +1,45 @@ +# Watch the current directory so CMake re-runs if files are added/removed: +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ".") + +file(GLOB SCHEMA_FILES "[0-9]*.sql" "[0-9]*.cpp") +list(SORT SCHEMA_FILES) + +set(DECLARATIONS "") +set(SCHEMA_ENTRIES "") +set(SCHEMA_SOURCES "") + +foreach(f IN LISTS SCHEMA_FILES) + get_filename_component(filename "${f}" NAME) + string(REGEX REPLACE "\\.(sql|cpp)$" "" basename "${filename}") + if(CMAKE_MATCH_1 STREQUAL "sql") + set(is_sql TRUE) + list(APPEND SQL_BASENAMES "${basename}") + else() + set(is_sql FALSE) + list(APPEND CPP_BASENAMES "${basename}") + endif() + list(APPEND SCHEMA_BASENAMES "${basename}") + + # Watch individual files so edits trigger a re-configure: + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${f}") + + string(MAKE_C_IDENTIFIER "apply_${basename}" FUNC_NAME) + if(is_sql) + file(RELATIVE_PATH SCHEMA_FULL_FILENAME "${PROJECT_SOURCE_DIR}" "${f}") + file(READ "${f}" SCHEMA_SQL) + set(wrapper_cpp "${CMAKE_CURRENT_BINARY_DIR}/apply_schema__${basename}__sql.cpp") + configure_file("apply_schema.cpp.in" "${wrapper_cpp}" @ONLY) + list(APPEND SCHEMA_SOURCES "${wrapper_cpp}") + else() + list(APPEND SCHEMA_SOURCES "${f}") + endif() + + string(APPEND DECLARATIONS "extern void ${FUNC_NAME}(session::sqlite::Connection&, session::core::Core&);\n") + string(APPEND SCHEMA_ENTRIES " Migration{\"${basename}\", &${FUNC_NAME}},\n") +endforeach() + +# Write all of the migration names, declarations, and function pointers: +configure_file("schema_registry.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/schema_registry.cpp" @ONLY) +list(APPEND SCHEMA_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/schema_registry.cpp") + +target_sources(core PRIVATE ${SCHEMA_SOURCES}) diff --git a/src/core/schema/README b/src/core/schema/README new file mode 100644 index 000000000..37fb9fc15 --- /dev/null +++ b/src/core/schema/README @@ -0,0 +1,31 @@ +Any files in this directory beginning with a number and ending with .cpp or .sql are one-time +migrations: they are processed in ascii-sorted order against the `migrations_applied` table: if the +migration doesn't exist there then the migration is executed when the and the entry is inserted into +`migrations_applied`. + +It is recommended that migrations defined here use the format `NNN_description_of_migration.sql`, +such as `000_device_tables.sql`. Updates that depend on earlier migrations having happend must then +ensure that they use a larger `NNN` value than the update that they depend on. Migrations that +depend on no other database components at all should start at 000. + +There are two sorts of migrations permitted here: SQL and C++. + +SQL migrations are simply a file of queries to execute. If the queries execute successfully, the +migration is considered applied. Each migration runs inside a transaction (and so does not need to +worry about starting a transaction itself); if migration fails, the transaction is rolled back, +otherwise it (and the insertion into `migrations_applied`) is committed. + +C++ migrations are for cases where more complex logic is needed to perform a migration: any such +migration should define a function `apply_FILENAME` in the session::core::schema namespace that +takes arguments `(session::sqlite::Connection&, session::core::Core&)` and performs any needed +migrations. FILENAME here will be the filename on disk without the .cpp extension, with any +non-alphanumeric characters replaced with `_`. As with the SQL migration, there will be an active +transaction around the call; the C++ code should throw if the migration cannot be performed (which +will roll back the transaction). Migrations are performed as the final step of Core instance +construction (and so Core initialization is finished, but the Core object has not yet been returned +to the creator). + +It is not permitted to use the same base filename for both SQL and C++ migrations (e.g. having both +007_bond_james_bond.sql and 007_bond_james_bond.cpp): if a migration wants to use both, use a +different numeric prefix or some sort of suffix (either of which will also defines the order the two +migrations would be applied). diff --git a/src/core/schema/apply_schema.cpp.in b/src/core/schema/apply_schema.cpp.in new file mode 100644 index 000000000..38c9f65fe --- /dev/null +++ b/src/core/schema/apply_schema.cpp.in @@ -0,0 +1,14 @@ +// Auto-generated by CMake from @SCHEMA_FULL_FILENAME@. Do not edit. +#include + +namespace session::core { class Core; } + +namespace session::core::schema { + +void @FUNC_NAME@(session::sqlite::Connection& conn, Core&) { + session::sqlite::exec_query(conn.sql, R"_SQL_DELIM_( +@SCHEMA_SQL@ +)_SQL_DELIM_"); +} + +} // namespace session::core::schema diff --git a/src/core/schema/schema_registry.cpp.in b/src/core/schema/schema_registry.cpp.in new file mode 100644 index 000000000..d7ef7a0e6 --- /dev/null +++ b/src/core/schema/schema_registry.cpp.in @@ -0,0 +1,15 @@ +// Auto-generated by CMake from src/core/schema/schema_registry.cpp.in. Do not edit. + +#include + +namespace session::core::schema { + +@DECLARATIONS@ + +static std::array migrations = { +@SCHEMA_ENTRIES@ +}; + +const std::span MIGRATIONS{migrations}; + +} // namespace session::core::schema diff --git a/src/core/schema/sql_migration.cpp.in b/src/core/schema/sql_migration.cpp.in new file mode 100644 index 000000000..0d64d3093 --- /dev/null +++ b/src/core/schema/sql_migration.cpp.in @@ -0,0 +1,10 @@ +#include + +namespace session::core::migration { + + void apply_@MIGRATION_NAME@(session::sqlite::Connection& conn) { + session::sqlite::exec_query(conn.sql, R"_SQL_DELIM_( +@MIGRATION_SQL@ +)_SQL_DELIM_"s); + } +} From 61d3a40f80f8e254a6e40942e37991f76562742a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 20:48:26 -0300 Subject: [PATCH 006/193] Add simdutf to session-deps --- cmake/session-deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/session-deps b/cmake/session-deps index 58839b50a..3384e6242 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit 58839b50a0a2ad21b074c42e949613aad69ad396 +Subproject commit 3384e624291b2e38c5840c6c962d564819d0af1c From 9b5418ad26392dace691e484297205a91d975e46 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 23:27:36 -0300 Subject: [PATCH 007/193] Various build fixes --- CMakeLists.txt | 8 ++++++++ external/CMakeLists.txt | 17 ++++++++--------- proto/CMakeLists.txt | 5 ----- src/CMakeLists.txt | 5 ----- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b95d0fd9d..a4aef1aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,10 +58,18 @@ else() set(static_default ON) endif() +# Override the default OFF value set in cmake/session-deps/Deps.cmake so that BUILD_STATIC_DEPS +# defaults to the same value as BUILD_SHARED_LIBS (i.e. static by default). +set(BUILD_STATIC_DEPS ${static_default} CACHE BOOL "Build all dependencies statically rather than trying to link to them on the system") + include(cmake/session-deps/Deps.cmake) option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${static_default}) +if(STATIC_BUNDLE AND NOT BUILD_STATIC_DEPS) + message(FATAL_ERROR "STATIC_BUNDLE requires BUILD_STATIC_DEPS to be enabled") +endif() + if(BUILD_SHARED_LIBS OR libsession_IS_TOPLEVEL_PROJECT) set(install_default ON) else() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 62ea1bd1c..72d7c1be3 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -21,6 +21,9 @@ function(add_static_subdirectory dir) add_subdirectory(${dir} ${ARGN}) endfunction() +session_dep(libsodium 1.0.21) +libsession_static_bundle(sessiondep::libsodium) + if(ENABLE_NETWORKING) if(ENABLE_NETWORKING_SROUTER) set(SROUTER_FULL OFF CACHE BOOL "") @@ -28,8 +31,6 @@ if(ENABLE_NETWORKING) set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") set(SROUTER_JEMALLOC OFF CACHE BOOL "") - add_library(sodium INTERFACE) - target_link_libraries(sodium INTERFACE libsodium::sodium-internal) add_static_subdirectory(session-router EXCLUDE_FROM_ALL) libsession_static_bundle(session-router::libsessionrouter) else() @@ -81,10 +82,6 @@ if(APPLE) endforeach() endif() -session_dep(libsodium 1.0.21) -libsession_static_bundle(sessiondep::libsodium) - - set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) set(protobuf_INSTALL ON CACHE BOOL "" FORCE) set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) @@ -128,9 +125,11 @@ if(NOT TARGET mlkem_native::mlkem768) add_library(mlkem_native::mlkem768 ALIAS mlkem_native768) endif() -set(JSON_BuildTests OFF CACHE INTERNAL "") -set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use -sessiondep_or_submodule(nlohmann_json 3.7.0 nlohmann-json nlohmann_json::nlohmann_json) +if(NOT TARGET nlohmann_json::nlohmann_json) + set(JSON_BuildTests OFF CACHE INTERNAL "") + set(JSON_Install ON CACHE INTERNAL "") # Required to export targets that we use + sessiondep_or_submodule(nlohmann_json 3.7.0 nlohmann-json nlohmann_json::nlohmann_json) +endif() session_dep(simdutf 7) diff --git a/proto/CMakeLists.txt b/proto/CMakeLists.txt index 324b116b8..a7ab778af 100644 --- a/proto/CMakeLists.txt +++ b/proto/CMakeLists.txt @@ -23,11 +23,6 @@ endif() libsession_static_bundle(protos) add_library(libsession::protos ALIAS protos) -export( - TARGETS protos - NAMESPACE libsession:: - FILE libsessionTargets.cmake -) list(APPEND libsession_export_targets protos) set(libsession_export_targets "${libsession_export_targets}" PARENT_SCOPE) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b52b88a68..8fb16b201 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -231,11 +231,6 @@ target_link_libraries(common INTERFACE version) foreach(tgt ${export_targets}) add_library("libsession::${tgt}" ALIAS "${tgt}") endforeach() -export( - TARGETS ${export_targets} common version - NAMESPACE libsession:: - APPEND FILE libsessionTargets.cmake -) list(APPEND libsession_export_targets ${export_targets}) set(libsession_export_targets "${libsession_export_targets}" PARENT_SCOPE) From 7d744e5d5f2111478291d5c6ef0522c11d960f9f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 00:31:07 -0300 Subject: [PATCH 008/193] session-deps: session-router, libquic, and fix This switches to the PR branches for session-router and libquic to use session-deps. (This was required under ninja builds, in particular, to get the deduplication handling for gmp and nettle via new session-deps code to deal with that). --- cmake/session-deps | 2 +- external/session-router | 2 +- src/CMakeLists.txt | 10 ++-------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cmake/session-deps b/cmake/session-deps index 3384e6242..e5677c8fa 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit 3384e624291b2e38c5840c6c962d564819d0af1c +Subproject commit e5677c8fa649043e7b985ba45459fa2e9b96bc09 diff --git a/external/session-router b/external/session-router index d2def4c91..e9cfe3bea 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit d2def4c91d024f9d896b43e817520f27a41a7995 +Subproject commit e9cfe3beaa2d9a9169f1ba99a7025aa534a6f2d0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8fb16b201..6cdfd57d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -128,13 +128,7 @@ target_link_libraries(config ) if(ENABLE_NETWORKING) - # libevent - if(NOT TARGET libevent::core) - add_library(libevent_core INTERFACE) - pkg_check_modules(LIBEVENT_core libevent_core>=2.1 IMPORTED_TARGET REQUIRED) - target_link_libraries(libevent_core INTERFACE PkgConfig::LIBEVENT_core) - add_library(libevent::core ALIAS libevent_core) - endif() + session_dep(libevent_core 2.1) add_libsession_util_library(network onionreq/builder.cpp @@ -166,7 +160,7 @@ if(ENABLE_NETWORKING) sessiondep::libsodium sessiondep::nettle date::date - libevent::core + sessiondep::libevent_core ) if(ENABLE_NETWORKING_SROUTER) From 4485a012c6ed29a34ef497d229216ebaa40518c9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 15:08:57 -0300 Subject: [PATCH 009/193] Add devices message parsing --- include/session/config/namespaces.hpp | 5 ++ include/session/core/devices.hpp | 17 +++++++ src/core/devices.cpp | 72 ++++++++++++++++++++++++--- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 6945f70ab..16f23b79c 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -24,6 +24,11 @@ enum class Namespace : std::int16_t { GroupInfo = NAMESPACE_GROUP_INFO, GroupMembers = NAMESPACE_GROUP_MEMBERS, + // Device group namespaces: + DeviceGroup = NAMESPACE_DEVICE_GROUP, + DeviceLink = NAMESPACE_DEVICE_LINK, + DevicePubkeys = NAMESPACE_DEVICE_PUBKEYS, + // The local config should never be pushed but this gives us a nice identifier for each config // type Local = NAMESPACE_LOCAL, diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index aee56d9c8..f9430471f 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -86,6 +86,17 @@ namespace device { // Unknown extra keys. Single-character keys in here are reserved for future // libsession-util use; longer strings can be used for custom client data, if needed. oxenc::bt_dict extra; + + // Returns the encoded device type string: "i", "a", or "d" for the standard Session + // client types, `other_device` for unknown types, or "" if unknown with no other_device. + std::string_view encoded_type() const { + switch (type) { + case Type::Session_iOS: return "i"; + case Type::Session_Android: return "a"; + case Type::Session_Desktop: return "d"; + default: return other_device; + } + } }; using map = std::map, Info>; @@ -116,6 +127,12 @@ class Devices final : detail::CoreComponent { // Returns the current device's random identifier, in hex. std::string device_id() const; + // Decrypts an incoming encrypted device data message (from the Namespace::DeviceGroup swarm + // message namespace) and stores the resulting device list in the database. Returns without + // doing anything if decryption fails (e.g. we are not in the device group, or we don't have + // the right keys for the message). + void receive_device_data(std::span data); + // Returns info for all registered and/or pending devices and/or unregistered devices for this // account. device::map devices( diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 74c8bddaf..927220eaf 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace session::core { @@ -289,14 +290,8 @@ namespace { write_next(devout, "X", info.pk_x25519, xit, xend); write_next(devout, "d", info.description, xit, xend); write_extras(devout, "t", xit, xend); - switch (info.type) { - case device::Type::Session_iOS: devout.append("t", "i"); break; - case device::Type::Session_Android: devout.append("t", "a"); break; - case device::Type::Session_Desktop: devout.append("t", "d"); break; - default: - if (!info.other_device.empty()) - devout.append("t", info.other_device); - } + if (auto t = info.encoded_type(); !t.empty()) + devout.append("t", t); auto ver = info.version[0] * 1000000 + std::clamp(info.version[1], 0, 999) * 1000 + std::clamp(info.version[2], 0, 999); write_extras(devout, "v", xit, xend); @@ -600,6 +595,67 @@ std::vector Devices::encrypt_device_data(const device::map& devices) return out; } +void Devices::receive_device_data(std::span data) { + device::map devs; + try { + devs = decrypt_device_data(std::as_bytes(data)); + } catch (const device::decryption_failed& e) { + log::warning(cat, "Ignoring incoming device group message: {}", e.what()); + return; + } + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + for (const auto& [id, info] : devs) { + auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; + + // Returns the row id if inserted or updated (i.e. seqno increased), nullopt if the seqno + // guard prevented an update. + auto dev_id = c.prepared_maybe_get( + R"(INSERT INTO devices + (unique_id, state, seqno, timestamp, device_type, description, version, + pubkey_mlkem768, pubkey_x25519) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(unique_id) DO UPDATE SET + state = excluded.state, + seqno = excluded.seqno, + timestamp = excluded.timestamp, + device_type = excluded.device_type, + description = excluded.description, + version = excluded.version, + pubkey_mlkem768 = excluded.pubkey_mlkem768, + pubkey_x25519 = excluded.pubkey_x25519 + WHERE excluded.seqno > seqno + RETURNING id)", + id, + static_cast(info.state), + info.seqno, + info.timestamp.time_since_epoch().count(), + info.encoded_type(), + info.description, + ver, + info.pk_mlkem768, + info.pk_x25519); + + if (!dev_id) + continue; + + c.prepared_exec("DELETE FROM device_unknown WHERE device = ?", *dev_id); + for (const auto& [key, val] : info.extra) { + auto encoded = std::visit( + [](const auto& v) { return oxenc::bt_serialize(v); }, val); + c.prepared_exec( + "INSERT INTO device_unknown (device, key, bt_value) VALUES (?, ?, ?)", + *dev_id, + key, + to_span(encoded)); + } + } + + tx.commit(); +} + device::map Devices::decrypt_device_data(std::span enc_data) { oxenc::bt_dict_consumer in{enc_data}; From eee237ceba355c35a1d3f4f19e1e2d9b46d8efc1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 15:11:59 -0300 Subject: [PATCH 010/193] Expose callbacks to components --- include/session/core/component.hpp | 4 ++++ src/core/component.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/include/session/core/component.hpp b/include/session/core/component.hpp index 82c7823ee..394280e7e 100644 --- a/include/session/core/component.hpp +++ b/include/session/core/component.hpp @@ -6,6 +6,7 @@ class Connection; namespace session::core { class Core; +struct callbacks; namespace detail { // Internal base class bridge between Core and the various components of core. This bridge @@ -20,6 +21,9 @@ namespace detail { // is unique to the calling thread and must not be used across threads. sqlite::Connection conn(); + // Returns the application callbacks registered with Core. + core::callbacks& cb(); + explicit CoreComponent(Core& core); // Default component `init()` does nothing; classes can override this if they want to be diff --git a/src/core/component.cpp b/src/core/component.cpp index 0854bd85f..49392c8b0 100644 --- a/src/core/component.cpp +++ b/src/core/component.cpp @@ -8,6 +8,10 @@ sqlite::Connection CoreComponent::conn() { return core.db.conn(); } +core::callbacks& CoreComponent::cb() { + return core.callbacks; +} + CoreComponent::CoreComponent(Core& core) : core{core} { core.register_comp_init(this); } From f6ea190374b9503a14517700287ac52b3eff6717 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:07:57 -0300 Subject: [PATCH 011/193] WIP: device linking --- include/session/config/encrypt.hpp | 16 ++ include/session/core.hpp | 15 +- include/session/core/callbacks.hpp | 4 + include/session/core/component.hpp | 8 + include/session/core/devices.hpp | 27 ++- src/CMakeLists.txt | 1 + src/config/encrypt.cpp | 45 ++-- src/core.cpp | 7 + src/core/component.cpp | 5 + src/core/devices.cpp | 373 ++++++++++++++++++++++------- src/core/schema/000_devices.sql | 3 +- 11 files changed, 394 insertions(+), 110 deletions(-) diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index b4b46ef44..406de6020 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -54,6 +54,22 @@ void encrypt_inplace( std::span key_base, std::string_view domain); +/// API: encrypt/encrypt_prealloced +/// +/// Encrypts a pre-allocated buffer in place. `message` must have exactly ENCRYPT_DATA_OVERHEAD +/// bytes of trailing space already allocated beyond the plaintext (i.e. message.size() must equal +/// plaintext_size + ENCRYPT_DATA_OVERHEAD). The plaintext in the leading bytes is encrypted in +/// place, and the auth tag and nonce are written into the trailing ENCRYPT_DATA_OVERHEAD bytes. +/// +/// Inputs: +/// - `message` -- buffer containing plaintext followed by ENCRYPT_DATA_OVERHEAD reserved bytes +/// - `key_base` -- Fixed key that all clients, must be 32 bytes. +/// - `domain` -- short string for the keyed hash +void encrypt_prealloced( + std::span message, + std::span key_base, + std::string_view domain); + /// API: encrypt/ENCRYPT_DATA_OVERHEAD /// /// Member variable diff --git a/include/session/core.hpp b/include/session/core.hpp index fbc9b1b20..9b0e9a1f0 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -111,17 +111,28 @@ /// /// ``` +namespace oxen::quic { +class Loop; +} // namespace oxen::quic + namespace session::pro_backend { struct ProRevocationItem; }; // namespace session::pro_backend namespace session::core { +namespace quic = oxen::quic; + namespace detail { class CoreComponent; } class Core { + // Constructed first (in init()), destroyed last: must outlive all components that use it. + // Custom deleter allows quic::Loop to remain an incomplete type in this header. + struct LoopDeleter { void operator()(quic::Loop*) const; }; + std::unique_ptr _loop; + sqlite::Database db; friend class detail::CoreComponent; @@ -139,8 +150,8 @@ class Core { void init(); public: - // Constructor taking a specific encryption type, any number of session::SQLite database - // options; anything else gets passed through to the individual component constructors. + // Constructor taking a struct of callbacks to invoke on various events, and any number of + // session::SQLite database options to open the core database. template Core(core::callbacks callbacks, std::filesystem::path db_path, const DBOpts&... opts) : callbacks{std::move(callbacks)}, db{std::move(db_path), opts...} { diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index ebd4af709..8fec37106 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -70,6 +70,10 @@ struct callbacks { /// Parameters: /// - removed_device -- the most recent info we have (locally) for the removed device. std::function device_removed; + + /// Callback invoked when *this* device has been confirmed removed from the account (typically + /// from another device) from an incoming device group update. + std::function device_self_removed; }; } // namespace session::core diff --git a/include/session/core/component.hpp b/include/session/core/component.hpp index 394280e7e..450c8a95a 100644 --- a/include/session/core/component.hpp +++ b/include/session/core/component.hpp @@ -3,8 +3,13 @@ namespace session::sqlite { class Connection; } +namespace oxen::quic { +class Loop; +} // namespace oxen::quic namespace session::core { +namespace quic = oxen::quic; + class Core; struct callbacks; @@ -24,6 +29,9 @@ namespace detail { // Returns the application callbacks registered with Core. core::callbacks& cb(); + // Returns the event loop for scheduling async work. + quic::Loop& loop(); + explicit CoreComponent(Core& core); // Default component `init()` does nothing; classes can override this if they want to be diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index f9430471f..b7ab454c7 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -6,9 +6,12 @@ #include #include #include +#include #include +#include #include #include +#include #include "component.hpp" @@ -53,7 +56,7 @@ namespace device { // The device type; one of the above enum values, or DevType::Unknown if the device type is // not one of the standard Session clients. - Type type; + Type type = device::Type::Unknown; // When device type is not one of the standard session clients, this will be set to a // free-form string indicating the device type. Will be empty if no device type is provided @@ -69,9 +72,14 @@ namespace device { // pubkeys. This allows a user to visually identify (and verify) a device within the device // list at any time, but its primary purpose is for identifying the device during linking. - // Indicates whether the device is registered or not. + // Indicates whether the device is registered, pending registration, or not registered. State state; + // For state == State::Unregistered, this timestamp (if set) indicates that the device was + // removed from the device group at that timestamp. It will be nullopt for a device that + // was never in the device group. + std::optional kicked; + // Application version triplet as reported by the device. The 2nd and 3rd values will // always be in [0, 999]. (If setting device info, they will be clamped if outside this // range). @@ -134,16 +142,23 @@ class Devices final : detail::CoreComponent { void receive_device_data(std::span data); // Returns info for all registered and/or pending devices and/or unregistered devices for this - // account. + // account. If `only_device` is non-empty it must be a 32-byte device id that is used to + // filter the results to just that one device. device::map devices( bool include_registered = true, bool include_pending = false, - bool include_unregistered = false); + bool include_unregistered = false, + std::span only_device = {}); - // Returns *this* device's info for this account, and whether the device is registered in the - // device list. + // Returns *this* device's info and whether it is registered in the device group. std::pair device_info(); + // Builds an outgoing link request message to upload to Namespace::DeviceLink. This should + // only be called when this device is not currently registered in the device group; throws + // std::logic_error if it is already registered. The returned bytes are to be pushed to + // Namespace::DeviceLink with a 10-minute TTL. + std::vector build_link_request(); + // Updates this device's info locally to match the given info; if the current device is // registered then this dirties the device config data, requiring a push. // diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6cdfd57d7..fbc936f3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,6 +117,7 @@ target_link_libraries( sessiondep::libsodium session::SQLite mlkem_native::mlkem768 + quic ) target_link_libraries(config diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 23a87cd9c..85c73bd7d 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -51,21 +51,15 @@ static std::array ma return key; } -std::vector encrypt( - std::span message, +void encrypt_prealloced( + std::span message, std::span key_base, std::string_view domain) { - std::vector msg; - msg.reserve(message.size() + ENCRYPT_DATA_OVERHEAD); - msg.assign(message.begin(), message.end()); - encrypt_inplace(msg, key_base, domain); - return msg; -} -void encrypt_inplace( - std::vector& message, - std::span key_base, - std::string_view domain) { - auto key = make_encrypt_key(key_base, message.size(), domain); + if (message.size() < ENCRYPT_DATA_OVERHEAD) + throw std::invalid_argument{ + "encrypt_prealloced: buffer is smaller than ENCRYPT_DATA_OVERHEAD"}; + size_t plaintext_len = message.size() - ENCRYPT_DATA_OVERHEAD; + auto key = make_encrypt_key(key_base, plaintext_len, domain); std::string nonce_key{NONCE_KEY_PREFIX}; nonce_key += domain; @@ -75,13 +69,10 @@ void encrypt_inplace( nonce.data(), nonce.size(), message.data(), - message.size(), + plaintext_len, to_unsigned(nonce_key.data()), nonce_key.size()); - size_t plaintext_len = message.size(); - message.resize(plaintext_len + ENCRYPT_DATA_OVERHEAD); - unsigned long long outlen = 0; crypto_aead_xchacha20poly1305_ietf_encrypt( message.data(), @@ -94,10 +85,28 @@ void encrypt_inplace( nonce.data(), key.data()); - assert(outlen == message.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + assert(outlen == plaintext_len + crypto_aead_xchacha20poly1305_ietf_ABYTES); std::memcpy(message.data() + outlen, nonce.data(), nonce.size()); } +std::vector encrypt( + std::span message, + std::span key_base, + std::string_view domain) { + std::vector out(message.size() + ENCRYPT_DATA_OVERHEAD); + std::memcpy(out.data(), message.data(), message.size()); + encrypt_prealloced(out, key_base, domain); + return out; +} + +void encrypt_inplace( + std::vector& message, + std::span key_base, + std::string_view domain) { + message.resize(message.size() + ENCRYPT_DATA_OVERHEAD); + encrypt_prealloced(message, key_base, domain); +} + static_assert( ENCRYPT_DATA_OVERHEAD == crypto_aead_xchacha20poly1305_IETF_ABYTES + crypto_aead_xchacha20poly1305_IETF_NPUBBYTES); diff --git a/src/core.cpp b/src/core.cpp index 503ca7eb4..b57555143 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -13,7 +14,13 @@ namespace session::core { namespace log = oxen::log; using namespace session::sqlite; +void Core::LoopDeleter::operator()(quic::Loop* p) const { + delete p; +} + void Core::init() { + _loop.reset(new quic::Loop()); + apply_migrations(); for (auto* component : _comp_init) diff --git a/src/core/component.cpp b/src/core/component.cpp index 49392c8b0..0138d8b15 100644 --- a/src/core/component.cpp +++ b/src/core/component.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,6 +13,10 @@ core::callbacks& CoreComponent::cb() { return core.callbacks; } +quic::Loop& CoreComponent::loop() { + return *core._loop; +} + CoreComponent::CoreComponent(Core& core) : core{core} { core.register_comp_init(this); } diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 927220eaf..f2cbc9752 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -165,52 +166,22 @@ std::vector Devices::active_account_keys() { return keys; } -device::map Devices::devices( - bool include_registered, bool include_pending, bool include_unregistered) { - - std::string where_clause; - if (include_registered && include_pending && include_unregistered) - ; // leave empty to select all - else { - std::vector where_states; - if (include_registered) - where_states.push_back(static_cast(device::State::Registered)); - if (include_pending) - where_states.push_back(static_cast(device::State::Pending)); - if (include_unregistered) - where_states.push_back(static_cast(device::State::Unregistered)); - if (where_states.empty()) - return {}; - where_clause = "WHERE state IN ({})"_format(fmt::join(where_states, ",")); - } - - auto c = conn(); - SQLite::Transaction tx{c.sql}; - - device::map devs; - - for (auto [id, devid, state, changes, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : - c.prepared_results< - int64_t, - sqlite::blob_guts>, - int, - int, - int, - int64_t, - std::string, - std::string, - int64_t, - sqlite::blobn, - sqlite::blobn<32>>(R"( -SELECT id, unique_id, state, changes, seqno, timestamp, device_type, description, version, - pubkey_mlkem768, pubkey_x25519 -FROM devices -{} -ORDER BY unique_id -)"_format(where_clause))) { - auto& info = devs[devid]; +namespace { - info.id = devid; + // Builds a device::Info from the fields of a devices table row (excluding the row id, changes, + // and kicked_timestamp columns, which are not part of device::Info). + device::Info fill_device_info( + std::span devid, + int state, + int seqno, + int64_t timestamp, + std::string type, + std::string desc, + int64_t ver, + const sqlite::blobn& pk_ml, + const sqlite::blobn<32>& pk_x) { + device::Info info; + std::memcpy(info.id.data(), devid.data(), info.id.size()); info.seqno = seqno; info.timestamp = std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; info.type = device::Type::Unknown; @@ -229,10 +200,13 @@ ORDER BY unique_id info.version[0] = ver / 1000000; std::memcpy(info.pk_x25519.data(), pk_x.data(), info.pk_x25519.size()); std::memcpy(info.pk_mlkem768.data(), pk_ml.data(), info.pk_mlkem768.size()); + return info; + } + void load_device_extras(sqlite::Connection& c, int64_t row_id, device::Info& info) { for (auto [key, value] : c.prepared_results( "SELECT key, bt_value FROM device_unknown WHERE device = ? ORDER BY key", - id)) { + row_id)) { try { info.extra[key] = oxenc::bt_deserialize(value); } catch (const std::exception& e) { @@ -241,9 +215,69 @@ ORDER BY unique_id } } +} // namespace + +device::map Devices::devices( + bool include_registered, + bool include_pending, + bool include_unregistered, + std::span only_device) { + + // Encode included states as a bitmask (bit 0 = Registered, 1 = Pending, 2 = Unregistered) so + // we use a stable query string regardless of which states are selected. + int state_mask = (include_registered ? 1 : 0) | (include_pending ? 2 : 0) | + (include_unregistered ? 4 : 0); + if (state_mask == 0) + return {}; + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + device::map devs; + + std::string query = + "SELECT id, unique_id, state, changes, seqno, timestamp, device_type, description," + " version, pubkey_mlkem768, pubkey_x25519" + " FROM devices WHERE ((1 << state) & ?) != 0"; + if (!only_device.empty()) + query += " AND unique_id = ?"; + query += " ORDER BY unique_id"; + + auto st = c.prepared_st(query); + if (only_device.empty()) + bind_oneshot(st, state_mask); + else + bind_oneshot(st, state_mask, only_device); + + for (auto [id, devid, state, changes, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : + sqlite::IterableStatementWrapper< + int64_t, + sqlite::blob_guts>, + int, + int, + int, + int64_t, + std::string, + std::string, + int64_t, + sqlite::blobn, + sqlite::blobn<32>>{std::move(st)}) { + auto& info = devs[devid]; + info = fill_device_info( + devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, + pk_x); + load_device_extras(c, id, info); + } + return devs; } +std::pair Devices::device_info() { + auto devs = devices(true, true, true, self_id); + if (auto it = devs.find(self_id); it != devs.end()) + return {std::move(it->second), it->second.state == device::State::Registered}; + return {device::Info{.id = self_id}, false}; +} + namespace { // Called while building a bt dict to pull out any unknown intermediate keys immediately before @@ -276,35 +310,69 @@ namespace { out.append(key, value); } + // Encodes the fields of a device::Info into an already-opened bt_dict_producer (passed as + // rvalue to allow callers to pass sub-producers directly from append_dict()). + void encode_device_info(oxenc::bt_dict_producer&& devout, const device::Info& info) { + auto xit = info.extra.cbegin(); + auto xend = info.extra.cend(); + write_next(devout, "#", info.seqno, xit, xend); + write_next(devout, "@", info.timestamp.time_since_epoch().count(), xit, xend); + write_next(devout, "M", info.pk_mlkem768, xit, xend); + write_next(devout, "X", info.pk_x25519, xit, xend); + write_next(devout, "d", info.description, xit, xend); + write_extras(devout, "t", xit, xend); + if (auto t = info.encoded_type(); !t.empty()) + devout.append("t", t); + auto ver = info.version[0] * 1000000 + std::clamp(info.version[1], 0, 999) * 1000 + + std::clamp(info.version[2], 0, 999); + write_extras(devout, "v", xit, xend); + if (ver != 0) + devout.append("v", ver); + for (; xit != xend; ++xit) + devout.append_bt(xit->first, xit->second); + } + std::string encode_device_data(const device::map& devices) { oxenc::bt_dict_producer out; for (const auto& [id, info] : devices) { - auto devout = out.append_dict( - std::string_view{reinterpret_cast(id.data()), id.size()}); - - auto xit = info.extra.cbegin(); - auto xend = info.extra.cend(); - write_next(devout, "#", info.seqno, xit, xend); - write_next(devout, "@", info.timestamp.time_since_epoch().count(), xit, xend); - write_next(devout, "M", info.pk_mlkem768, xit, xend); - write_next(devout, "X", info.pk_x25519, xit, xend); - write_next(devout, "d", info.description, xit, xend); - write_extras(devout, "t", xit, xend); - if (auto t = info.encoded_type(); !t.empty()) - devout.append("t", t); - auto ver = info.version[0] * 1000000 + std::clamp(info.version[1], 0, 999) * 1000 + - std::clamp(info.version[2], 0, 999); - write_extras(devout, "v", xit, xend); - if (ver != 0) - devout.append("v", ver); - - for (; xit != xend; ++xit) - out.append_bt(xit->first, xit->second); + + std::string_view id_sv{reinterpret_cast(id.data()), id.size()}; + + if (info.state == device::State::Pending) { + log::debug( + cat, "Skipping pending device {} in device group data", oxenc::to_hex(id)); + continue; + } else if (info.state == device::State::Unregistered) { + // We write a timestamp tombstone value for a kicked device, with the kick timestamp + // as the value. + // + // TODO: we should prune devices that were kicked a long time ago. + if (info.kicked) + out.append(id_sv, info.kicked->time_since_epoch().count()); + else + log::debug( + cat, + "Skipping unregistered (but not kicked) device {}", + oxenc::to_hex(id)); + continue; + } + + encode_device_info(out.append_dict(id_sv), info); } return std::move(out).str(); } + std::string encode_link_request_plaintext( + std::span device_id, const device::Info& info) { + oxenc::bt_dict_producer out; + // "I" (device id) sorts before "i" (info dict) + out.append("I", std::string_view{ + reinterpret_cast(device_id.data()), device_id.size()}); + encode_device_info(out.append_dict("i"), info); + return std::move(out).str(); + } + // Stores the current btdc key/value in `extra`; the value is consumed (i.e. the consumer // advances to the next key). void consume_extra(oxenc::bt_dict_consumer& btdc, oxenc::bt_dict& extra) { @@ -370,7 +438,10 @@ namespace { consume_extra(dev, info.extra); } - device::map decode_device_data(std::span data, device::State state) { + // Decodes an incoming devices data message. The return map will include both full device + // records, and deleted devices: deleted devices will have a mostly default-constructed Info + // object where only id, state (=State::Unregistered) and kicked (=removal timestamp) are set. + device::map decode_device_data(std::span data) { device::map devices; oxenc::bt_dict_consumer in{data}; @@ -386,21 +457,26 @@ namespace { continue; } - if (in.is_integer()) { - // An integer indicates a "device removed" timestamp, used to distinguish between - // "device removed" and "I don't know about the device yet". It gets pruned when - // updating once it hits a certain age threshold. - log::debug(cat, "Skipping recently removed device id={}", oxenc::to_hex(in_id)); - continue; - } - std::array id; std::memcpy(id.data(), in_id.data(), 32); auto [it, ins] = devices.try_emplace(id); if (!ins) throw std::runtime_error{"Invalid encoded device data: duplicate devices ids"}; - decode_one(it->second, in.consume_dict_consumer(), state); + auto& info = it->second; + + if (in.is_integer()) { + // An integer indicates a "device removed" timestamp, used to distinguish between + // "device removed" and "I don't know about the device yet". It gets pruned when + // updating once it hits a certain age threshold. + // + // If the device wants to get re-added to the group then it must generate a new + // device id. + info.state = device::State::Unregistered; + info.kicked.emplace(std::chrono::seconds{in.consume_integer()}); + } else { + decode_one(info, in.consume_dict_consumer(), device::State::Registered); + } } return devices; @@ -607,7 +683,61 @@ void Devices::receive_device_data(std::span data) { auto c = conn(); SQLite::Transaction tx{c.sql}; + // Snapshot the current state of all devices before processing updates so we can detect + // transitions and fire the appropriate callbacks afterward. + device::map old_devs; + for (auto [id, devid, state, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : + c.prepared_results< + int64_t, + sqlite::blob_guts>, + int, + int, + int64_t, + std::string, + std::string, + int64_t, + sqlite::blobn, + sqlite::blobn<32>>( + "SELECT id, unique_id, state, seqno, timestamp, device_type, description, version," + " pubkey_mlkem768, pubkey_x25519 FROM devices")) { + auto& info = old_devs[devid]; + info = fill_device_info( + devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, + pk_x); + load_device_extras(c, id, info); + } + + std::vector> post_commit; + for (const auto& [id, info] : devs) { + bool is_self = (id == self_id); + auto old_it = old_devs.find(id); + bool was_registered = old_it != old_devs.end() && + old_it->second.state == device::State::Registered; + + if (info.state == device::State::Unregistered) { + // Kicked device: preserve whatever existing row data we have, just update state and + // kicked_timestamp. If we have no row for this device we can't do anything useful + // (we'd have no data to fill the required columns with), so skip it. + assert(info.kicked); + c.prepared_exec( + R"(UPDATE devices SET state = ?, kicked_timestamp = ? WHERE unique_id = ?)", + static_cast(device::State::Unregistered), + info.kicked->time_since_epoch().count(), + id); + + if (was_registered) { + if (is_self) { + if (auto& f = cb().device_self_removed) + post_commit.push_back(f); + } else if (auto& f = cb().device_removed) { + auto& old = old_it->second; + post_commit.push_back([&f, &old] { f(old); }); + } + } + continue; + } + auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; // Returns the row id if inserted or updated (i.e. seqno increased), nullopt if the seqno @@ -615,8 +745,8 @@ void Devices::receive_device_data(std::span data) { auto dev_id = c.prepared_maybe_get( R"(INSERT INTO devices (unique_id, state, seqno, timestamp, device_type, description, version, - pubkey_mlkem768, pubkey_x25519) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + pubkey_mlkem768, pubkey_x25519, kicked_timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL) ON CONFLICT(unique_id) DO UPDATE SET state = excluded.state, seqno = excluded.seqno, @@ -625,7 +755,8 @@ void Devices::receive_device_data(std::span data) { description = excluded.description, version = excluded.version, pubkey_mlkem768 = excluded.pubkey_mlkem768, - pubkey_x25519 = excluded.pubkey_x25519 + pubkey_x25519 = excluded.pubkey_x25519, + kicked_timestamp = excluded.kicked_timestamp WHERE excluded.seqno > seqno RETURNING id)", id, @@ -643,17 +774,93 @@ void Devices::receive_device_data(std::span data) { c.prepared_exec("DELETE FROM device_unknown WHERE device = ?", *dev_id); for (const auto& [key, val] : info.extra) { - auto encoded = std::visit( - [](const auto& v) { return oxenc::bt_serialize(v); }, val); + auto encoded = std::visit([](const auto& v) { return oxenc::bt_serialize(v); }, val); c.prepared_exec( "INSERT INTO device_unknown (device, key, bt_value) VALUES (?, ?, ?)", *dev_id, key, to_span(encoded)); } + + if (!was_registered) { + if (is_self) { + if (auto& f = cb().device_self_added) + post_commit.push_back(f); + } else if (auto& f = cb().device_added) { + // reqid=0: correlating with pending link request records is not yet implemented + post_commit.push_back([&f, &inf = info] { f(0, inf); }); + } + } } tx.commit(); + for (auto& f : post_commit) + f(); +} + +std::vector Devices::build_link_request() { + auto [info, is_registered] = device_info(); + + if (is_registered) + throw std::logic_error{ + "build_link_request() called on a device that is already registered in the device " + "group"}; + + info.id = self_id; + info.seqno++; + info.timestamp = std::chrono::time_point_cast( + std::chrono::system_clock::now()); + + // Always use the current active device keys for the pubkeys in the link request, regardless + // of what is stored in the DB, as the DB may lag a key rotation. + auto keys = active_device_keys(); + std::memcpy( + info.pk_x25519.data(), + reinterpret_cast(keys.front().x25519_pub.data()), + info.pk_x25519.size()); + std::memcpy( + info.pk_mlkem768.data(), + reinterpret_cast(keys.front().mlkem768_pub.data()), + info.pk_mlkem768.size()); + + // Upsert our own device row with the updated seqno, timestamp, and pubkeys. Mark changes=TRUE + // so that other parts of the system know a pending link request is outstanding. + auto c = conn(); + auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; + c.prepared_exec( + R"(INSERT INTO devices + (unique_id, state, seqno, timestamp, device_type, description, version, + pubkey_mlkem768, pubkey_x25519, changes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE) + ON CONFLICT(unique_id) DO UPDATE SET + state = excluded.state, + seqno = excluded.seqno, + timestamp = excluded.timestamp, + device_type = excluded.device_type, + description = excluded.description, + version = excluded.version, + pubkey_mlkem768 = excluded.pubkey_mlkem768, + pubkey_x25519 = excluded.pubkey_x25519, + changes = excluded.changes)", + self_id, + static_cast(device::State::Pending), + info.seqno, + info.timestamp.time_since_epoch().count(), + info.encoded_type(), + info.description, + ver, + info.pk_mlkem768, + info.pk_x25519); + + auto plaintext = encode_link_request_plaintext(self_id, info); + std::vector out(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); + std::memcpy(out.data(), plaintext.data(), plaintext.size()); + auto seed = core.globals.account_seed(); + config::encrypt_prealloced( + as_span(std::span{out}), + as_span(seed.buf).first(32), + "link-request"); + return out; } device::map Devices::decrypt_device_data(std::span enc_data) { @@ -799,7 +1006,7 @@ device::map Devices::decrypt_device_data(std::span enc_data) { throw device::decryption_failed{"Failed to decrypt incoming device data"}; } - return decode_device_data(plaintext_devices, device::State::Registered); + return decode_device_data(plaintext_devices); } } // namespace session::core diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index e73775d17..1e5a30fcc 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -5,9 +5,10 @@ CREATE TABLE devices ( unique_id device_id BLOB UNIQUE NOT NULL CHECK(length(id) == 32), state INTEGER NOT NULL CHECK(state == 0 OR state == 1 OR state == 2), -- registered, pending, unregistered - changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unpushed local device info changes + changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unconfirmed local device info changes seqno INTEGER NOT NULL DEFAULT 1, timestamp INTEGER NOT NULL, + kicked_timestamp INTEGER, -- set when the device was kicked from the device group device_type TEXT NOT NULL, -- typically a/i/d (Android/iOS/Desktop), but can be anything description TEXT NOT NULL, -- freeform device description version INTEGER NOT NULL, -- = 1000000*V + 1000*v + p for version "V.v.p" From 49c9f0f78514f628f5a7a606b7faab79840da5ca Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:21:02 -0300 Subject: [PATCH 012/193] Add generic core message consumer --- include/session/core.hpp | 12 ++++++++++++ include/session/core/devices.hpp | 17 +++++++++++------ src/core.cpp | 24 ++++++++++++++++++++++++ src/core/devices.cpp | 18 +++++++++++++++++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index 9b0e9a1f0..4ee759745 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -167,6 +168,17 @@ class Core { // Device groups for handling shared encryption key among account devices Devices devices{*this}; + + // Passes a batch of messages retrieved from the swarm to the appropriate handler based on the + // namespace they were retrieved from. `is_final` should be true if this batch represents the + // complete current contents of the namespace (i.e. there are no more messages pending + // retrieval), and false if more messages may follow. Note that if there turn out to be no more + // messages after a non-final call, the caller should still call this with an empty span and + // is_final=true to flush any actions that are deferred until the end of a fetch. + void receive_messages( + std::span> messages, + config::Namespace ns, + bool is_final); }; } // namespace session::core diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index b7ab454c7..3aeb619a2 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -127,6 +127,17 @@ class Devices final : detail::CoreComponent { // Encrypts the inner device data for all the members of the device group. std::vector encrypt_device_data(const device::map& devices); + // Processes a single incoming encrypted device group message. + void receive_device_group_message(std::span data); + + // Handlers for incoming swarm messages by namespace, called from Core::receive_messages. + void parse_device_group( + std::span> messages, bool is_final); + void parse_link_request( + std::span> messages, bool is_final); + void parse_device_pubkeys( + std::span> messages, bool is_final); + // Inverse of encrypt_device_data. Throws if invalid. Throws `Devices::decryption_failed` if // the message was parsed successfully, but we failed to decrypt any of its encrypted values. device::map decrypt_device_data(std::span data); @@ -135,12 +146,6 @@ class Devices final : detail::CoreComponent { // Returns the current device's random identifier, in hex. std::string device_id() const; - // Decrypts an incoming encrypted device data message (from the Namespace::DeviceGroup swarm - // message namespace) and stores the resulting device list in the database. Returns without - // doing anything if decryption fails (e.g. we are not in the device group, or we don't have - // the right keys for the message). - void receive_device_data(std::span data); - // Returns info for all registered and/or pending devices and/or unregistered devices for this // account. If `only_device` is non-empty it must be a 32-byte device id that is used to // filter the results to just that one device. diff --git a/src/core.cpp b/src/core.cpp index b57555143..bc4283b55 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -13,6 +13,7 @@ namespace session::core { namespace log = oxen::log; using namespace session::sqlite; +static auto cat = log::Cat("core"); void Core::LoopDeleter::operator()(quic::Loop* p) const { delete p; @@ -33,6 +34,29 @@ void Core::register_comp_init(detail::CoreComponent* c) { _comp_init.push_back(c); } +void Core::receive_messages( + std::span> messages, + config::Namespace ns, + bool is_final) { + using config::Namespace; + switch (ns) { + case Namespace::DeviceGroup: + devices.parse_device_group(messages, is_final); + break; + case Namespace::DeviceLink: + devices.parse_link_request(messages, is_final); + break; + case Namespace::DevicePubkeys: + devices.parse_device_pubkeys(messages, is_final); + break; + default: + log::warning( + cat, + "receive_messages: ignoring unhandled namespace {}", + static_cast(ns)); + } +} + void Core::apply_migrations() { auto cat = log::Cat("schema"); diff --git a/src/core/devices.cpp b/src/core/devices.cpp index f2cbc9752..935b54ed1 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -671,7 +671,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) return out; } -void Devices::receive_device_data(std::span data) { +void Devices::receive_device_group_message(std::span data) { device::map devs; try { devs = decrypt_device_data(std::as_bytes(data)); @@ -1009,4 +1009,20 @@ device::map Devices::decrypt_device_data(std::span enc_data) { return decode_device_data(plaintext_devices); } +void Devices::parse_device_group( + std::span> messages, bool /*is_final*/) { + for (const auto& msg : messages) + receive_device_group_message(msg); +} + +void Devices::parse_link_request( + std::span> /*messages*/, bool /*is_final*/) { + log::debug(cat, "parse_link_request: not yet implemented"); +} + +void Devices::parse_device_pubkeys( + std::span> /*messages*/, bool /*is_final*/) { + log::debug(cat, "parse_device_pubkeys: not yet implemented"); +} + } // namespace session::core From aa6fff8d45f72d167c7352b1613e3fd8ce2af905 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:27:35 -0300 Subject: [PATCH 013/193] formatting --- include/session/core.hpp | 4 +++- include/session/core/globals.hpp | 2 +- src/core.cpp | 12 +++--------- src/core/devices.cpp | 20 ++++++++++---------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index 4ee759745..9562b3419 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -131,7 +131,9 @@ namespace detail { class Core { // Constructed first (in init()), destroyed last: must outlive all components that use it. // Custom deleter allows quic::Loop to remain an incomplete type in this header. - struct LoopDeleter { void operator()(quic::Loop*) const; }; + struct LoopDeleter { + void operator()(quic::Loop*) const; + }; std::unique_ptr _loop; sqlite::Database db; diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 61a3d125e..a52684411 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -38,7 +38,7 @@ class Globals final : detail::CoreComponent { // CoreComponent base class method from other components. session::secure_buffer _account_seed; std::array _pubkey_ed25519; - std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix + std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix void init() override; diff --git a/src/core.cpp b/src/core.cpp index bc4283b55..875872a12 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -40,15 +40,9 @@ void Core::receive_messages( bool is_final) { using config::Namespace; switch (ns) { - case Namespace::DeviceGroup: - devices.parse_device_group(messages, is_final); - break; - case Namespace::DeviceLink: - devices.parse_link_request(messages, is_final); - break; - case Namespace::DevicePubkeys: - devices.parse_device_pubkeys(messages, is_final); - break; + case Namespace::DeviceGroup: devices.parse_device_group(messages, is_final); break; + case Namespace::DeviceLink: devices.parse_link_request(messages, is_final); break; + case Namespace::DevicePubkeys: devices.parse_device_pubkeys(messages, is_final); break; default: log::warning( cat, diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 935b54ed1..bdec9fc45 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -263,8 +263,7 @@ device::map Devices::devices( sqlite::blobn<32>>{std::move(st)}) { auto& info = devs[devid]; info = fill_device_info( - devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, - pk_x); + devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, pk_x); load_device_extras(c, id, info); } @@ -367,8 +366,10 @@ namespace { std::span device_id, const device::Info& info) { oxenc::bt_dict_producer out; // "I" (device id) sorts before "i" (info dict) - out.append("I", std::string_view{ - reinterpret_cast(device_id.data()), device_id.size()}); + out.append( + "I", + std::string_view{ + reinterpret_cast(device_id.data()), device_id.size()}); encode_device_info(out.append_dict("i"), info); return std::move(out).str(); } @@ -702,8 +703,7 @@ void Devices::receive_device_group_message(std::span data) " pubkey_mlkem768, pubkey_x25519 FROM devices")) { auto& info = old_devs[devid]; info = fill_device_info( - devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, - pk_x); + devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, pk_x); load_device_extras(c, id, info); } @@ -712,8 +712,8 @@ void Devices::receive_device_group_message(std::span data) for (const auto& [id, info] : devs) { bool is_self = (id == self_id); auto old_it = old_devs.find(id); - bool was_registered = old_it != old_devs.end() && - old_it->second.state == device::State::Registered; + bool was_registered = + old_it != old_devs.end() && old_it->second.state == device::State::Registered; if (info.state == device::State::Unregistered) { // Kicked device: preserve whatever existing row data we have, just update state and @@ -808,8 +808,8 @@ std::vector Devices::build_link_request() { info.id = self_id; info.seqno++; - info.timestamp = std::chrono::time_point_cast( - std::chrono::system_clock::now()); + info.timestamp = + std::chrono::time_point_cast(std::chrono::system_clock::now()); // Always use the current active device keys for the pubkeys in the link request, regardless // of what is stored in the DB, as the DB may lag a key rotation. From d8db5b42b776f11a54b315a1fc8c9493bf1bb5e1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 23:06:07 -0300 Subject: [PATCH 014/193] Hash & type unification - Refactor manual libsodium blake2b hash calls to use simpler hash::blake2b functions instead. - Unify usage of array_uc32/33/64 and uc32/33/64: now we have just uc32/33/64 and cleared_uc32/33/64 (i.e. the "array_" prefix is gone). - Add a unsigned char array literal, which is quite useful for static hash keys. --- .../session/config/convo_info_volatile.hpp | 2 +- include/session/hash.hpp | 22 ++++- include/session/pro_backend.hpp | 46 +++++------ include/session/session_protocol.hpp | 36 ++++---- include/session/types.hpp | 6 +- include/session/util.hpp | 2 +- src/attachments.cpp | 9 +- src/blinding.cpp | 39 ++------- src/config.cpp | 5 +- src/config/encrypt.cpp | 13 ++- src/config/groups/keys.cpp | 46 ++++------- src/core/pro.cpp | 8 +- src/ed25519.cpp | 46 ++++------- src/pro_backend.cpp | 82 +++++++++---------- src/session_encrypt.cpp | 23 ++---- src/session_protocol.cpp | 51 ++++++------ src/xed25519.cpp | 16 ++-- 17 files changed, 193 insertions(+), 259 deletions(-) diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 9280110fb..5a2770ed5 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -87,7 +87,7 @@ namespace convo { struct pro_base : base { /// Hash of the generation index set by the Session Pro Backend - std::optional pro_gen_index_hash; + std::optional pro_gen_index_hash; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid /// to diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 458923069..b93eefa94 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -52,7 +52,7 @@ template concept ByteContainer = std::ranges::contiguous_range && oxenc::basic_char>; -/// API: hash/blake2b_update +/// API: hash/update_all /// /// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte /// containers and updates the hash state with them, in argument order. @@ -122,6 +122,9 @@ void blake2b_key(Out& out, const Key& key, const T&... args) { /// /// Output must be a fixed extent span or containers (e.g. std::array), and must satisfy the blake2b /// requirements (output size in [1,64]). +/// +/// It is permitted for overlap between the output and input containers; the output container is not +/// written until all input containers have been consumed. template requires(sizeof...(T) > 0) void blake2b(Out& out, const T&... args) { @@ -184,6 +187,7 @@ struct StringLiteral { for (size_t i = 0; i < N - 1; ++i) chars[i] = s[i]; } + static constexpr size_t size() { return N; } }; inline namespace literals { @@ -191,7 +195,7 @@ inline namespace literals { /// User-defined literal for a 16-byte, unsigned char array intended for use as a BLAKE2b /// personality value. Example: /// - /// using namespace session::hash::literals; + /// using namespace session::literals; /// constexpr auto PERS_XYZ = "XYZ-XYZ-XYZ-WXYZ"_b2b_pers; /// template @@ -205,6 +209,20 @@ inline namespace literals { return pers; } + /// User-defined literal for an arbitrary-length, unsigned char array; this is primarily + /// intended for fixed keys with BLAKE2b hash. Example: + /// + /// using namespace session::literals; + /// constexpr auto HASH_KEY_42 = "forty-two"_uc; + /// + template + constexpr auto operator""_uc() { + std::array pers; + for (size_t i = 0; i < pers.size(); i++) + pers[i] = static_cast(Str.chars[i]); + return pers; + } + } // namespace literals } // namespace session diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 4b671bbcd..15fec99ad 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -61,10 +61,10 @@ namespace session::pro_backend { /// TODO: Assign the Session Pro backend public key for verifying proofs to allow users of the /// library to have the pubkey available for verifying proofs. -constexpr array_uc32 PUBKEY = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -static_assert(sizeof(PUBKEY) == array_uc32{}.size()); +constexpr uc32 PUBKEY = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static_assert(sizeof(PUBKEY) == uc32{}.size()); enum struct AddProPaymentResponseStatus { /// Payment was claimed and the pro proof was successfully generated @@ -101,8 +101,8 @@ struct ResponseHeader { }; struct MasterRotatingSignatures { - array_uc64 master_sig; - array_uc64 rotating_sig; + uc64 master_sig; + uc64 rotating_sig; }; struct AddProPaymentUserTransaction { @@ -132,20 +132,20 @@ struct AddProPaymentRequest { /// 32-byte Ed25519 Session Pro master public key derived from the Session account seed to /// register a Session Pro payment under. - array_uc32 master_pkey; + uc32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key to authorise to use the generated Session /// Pro proof - array_uc32 rotating_pkey; + uc32 rotating_pkey; /// Transaction containing the payment details to register on the Session Pro backend AddProPaymentUserTransaction payment_tx; /// 64-byte signature proving knowledge of the master key's secret component - array_uc64 master_sig; + uc64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - array_uc64 rotating_sig; + uc64 rotating_sig; /// API: pro/AddProPaymentRequest::to_json /// @@ -239,19 +239,19 @@ struct GenerateProProofRequest { /// 32-byte Ed25519 Session Pro master public key to generate a Session Pro proof from. This key /// must have had a prior, and still active payment registered under it for a new proof to be /// generated successfully. - array_uc32 master_pkey; + uc32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key authorized to use the generated proof - array_uc32 rotating_pkey; + uc32 rotating_pkey; /// Unix timestamp of the request sys_ms unix_ts; /// 64-byte signature proving knowledge of the master key's secret component - array_uc64 master_sig; + uc64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - array_uc64 rotating_sig; + uc64 rotating_sig; /// API: pro/GenerateProProofRequest::build_sigs /// @@ -324,7 +324,7 @@ struct GetProRevocationsRequest { struct ProRevocationItem { /// 32-byte hash of the generation index, identifying a proof - array_uc32 gen_index_hash; + uc32 gen_index_hash; /// Unix timestamp when the proof expires sys_ms expiry_unix_ts; @@ -356,10 +356,10 @@ struct GetProDetailsRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - array_uc32 master_pkey; + uc32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - array_uc64 master_sig; + uc64 master_sig; /// Unix timestamp of the request sys_ms unix_ts; @@ -380,8 +380,8 @@ struct GetProDetailsRequest { /// - `count` -- Amount of historical payments to request /// /// Outputs: - /// - `array_uc64` - the 64-byte signature - static array_uc64 build_sig( + /// - `uc64` - the 64-byte signature + static uc64 build_sig( uint8_t version, std::span master_privkey, sys_ms unix_ts, @@ -567,10 +567,10 @@ struct SetPaymentRefundRequestedRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - array_uc32 master_pkey; + uc32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - array_uc64 master_sig; + uc64 master_sig; /// Unix timestamp of the current time sys_ms unix_ts; @@ -603,8 +603,8 @@ struct SetPaymentRefundRequestedRequest { /// `AddProPaymentUserTransaction` /// /// Outputs: - /// - `array_uc64` - the 64-byte signature - static array_uc64 build_sig( + /// - `uc64` - the 64-byte signature + static uc64 build_sig( uint8_t version, std::span master_privkey, sys_ms unix_ts, diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index f66a1c3cd..139fdff63 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -86,12 +86,12 @@ class ProProof { std::uint8_t version; /// Hash of the generation index set by the Session Pro Backend - array_uc32 gen_index_hash; + uc32 gen_index_hash; /// The public key that the Session client registers their Session Pro entitlement under. /// Session clients must sign messages with this key along side the sending of this proof for /// the network to authenticate their usage of the proof - array_uc32 rotating_pubkey; + uc32 rotating_pubkey; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid to sys_ms expiry_unix_ts; @@ -99,7 +99,7 @@ class ProProof { /// Signature over the contents of the proof. It is signed by the Session Pro Backend key which /// is the entity responsible for issueing tamper-proof Sesison Pro certificates for Session /// clients. - array_uc64 sig; + uc64 sig; /// API: pro/Proof::verify_signature /// @@ -175,7 +175,7 @@ class ProProof { /// API: pro/Proof::hash /// /// Create a 32-byte hash from the proof. This hash is the payload that is signed in the proof. - array_uc32 hash() const; + uc32 hash() const; bool operator==(const ProProof& other) const { return version == other.version && gen_index_hash == other.gen_index_hash && @@ -238,14 +238,14 @@ struct Destination { // When type => (CommunityInbox || SyncMessage || Contact): set to the recipient's Session // public key - array_uc33 recipient_pubkey; + uc33 recipient_pubkey; // When type => CommunityInbox: set this pubkey to the server's key - array_uc32 community_inbox_server_pubkey; + uc32 community_inbox_server_pubkey; // When type => Group: set to the group public keys for a 0x03 prefix (e.g. groups v2) // `group_pubkey` to encrypt the message for. - array_uc33 group_ed25519_pubkey; + uc33 group_ed25519_pubkey; // When type => Group: Set the encryption key of the group for groups v2 messages. Typically // the latest key for the group, e.g: `Keys::group_enc_key` or `groups_keys_group_enc_key` @@ -259,12 +259,12 @@ struct Envelope { // Optional fields. These fields are set if the appropriate flag has been set in `flags` // otherwise the corresponding values are to be ignored and those fields will be // zero-initialised. - array_uc33 source; + uc33 source; uint32_t source_device; uint64_t server_timestamp; // Signature by the sending client's rotating key - array_uc64 pro_sig; + uc64 pro_sig; }; struct DecodedPro { @@ -286,11 +286,11 @@ struct DecodedEnvelope { // Sender public key extracted from the encrypted content payload. This is not set if the // envelope was a groups v2 envelope where the envelope was encrypted and only the x25519 pubkey // was available. - array_uc32 sender_ed25519_pubkey; + uc32 sender_ed25519_pubkey; // The x25519 pubkey, always populated on successful parse. Either it's present from decrypting // a Groups v2 envelope or it's re-derived from the Ed25519 pubkey. - array_uc32 sender_x25519_pubkey; + uc32 sender_x25519_pubkey; // Set if the envelope included a pro payload. The caller must check the status to determine if // the embedded pro data/proof was valid, invalid or whether or not the proof has expired. @@ -312,7 +312,7 @@ struct DecodedCommunityMessage { // signature is sourced from the envelope, the envelope's `pro_sig` field is also set to the // same signature as this instance for consistency. Otherwise the signature, if set was // extracted from the community-exclusive pro signature field in the content message. - std::optional pro_sig; + std::optional pro_sig; // Set if the envelope included a pro payload. The caller must check the status to determine if // the embedded pro data/proof was valid, invalid or whether or not the proof has expired. @@ -422,7 +422,7 @@ std::vector encode_for_1o1( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, + const uc33& recipient_pubkey, std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_community_inbox @@ -456,8 +456,8 @@ std::vector encode_for_community_inbox( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, - const array_uc32& community_pubkey, + const uc33& recipient_pubkey, + const uc32& community_pubkey, std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_community @@ -519,7 +519,7 @@ std::vector encode_for_group( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& group_ed25519_pubkey, + const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, std::optional> pro_rotating_ed25519_privkey); @@ -614,7 +614,7 @@ std::vector encode_for_destination( DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, std::span envelope_payload, - const array_uc32& pro_backend_pubkey); + const uc32& pro_backend_pubkey); /// API: session_protocol/decode_for_community /// @@ -648,6 +648,6 @@ DecodedEnvelope decode_envelope( DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, sys_ms unix_ts, - const array_uc32& pro_backend_pubkey); + const uc32& pro_backend_pubkey); } // namespace session diff --git a/include/session/types.hpp b/include/session/types.hpp index d28dbc145..57e537d3b 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -1,8 +1,8 @@ #pragma once -#include #include #include +#include #include "types.h" @@ -11,10 +11,6 @@ namespace session { template static constexpr bool is_one_of = (std::is_same_v || ...); -using array_uc32 = std::array; -using array_uc33 = std::array; -using array_uc64 = std::array; - enum class SessionIDPrefix { standard = 0, group = 0x3, diff --git a/include/session/util.hpp b/include/session/util.hpp index 1baead82d..94a3288bb 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -394,4 +394,4 @@ Fn make_callback_atomic(Fn cb) { if (!called->exchange(true)) cb(std::forward(args)...); }; -} \ No newline at end of file +} diff --git a/src/attachments.cpp b/src/attachments.cpp index 9ad0b1337..7df924a54 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -17,6 +17,7 @@ #include #include "internal-util.hpp" +#include "session/hash.hpp" namespace session::attachment { @@ -236,13 +237,9 @@ encrypt_buffer_init( std::span udata{ reinterpret_cast(data.data()), data.size()}; - crypto_generichash_blake2b_state b_st; const auto domain_byte = static_cast(domain); - crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(seed.data()), 32); - crypto_generichash_blake2b_update(&b_st, udata.data(), udata.size()); - crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); + hash::blake2b_key( + nonce_key, std::span{&domain_byte, 1}, seed.first(32), udata); std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); inpos = udata.data(); diff --git a/src/blinding.cpp b/src/blinding.cpp index 659bf0113..81aa25c5e 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -11,6 +11,7 @@ #include "session/ed25519.hpp" #include "session/export.h" +#include "session/hash.hpp" #include "session/platform.h" #include "session/platform.hpp" #include "session/xed25519.hpp" @@ -19,18 +20,11 @@ namespace session { using namespace std::literals; -using uc32 = std::array; -using uc33 = std::array; -using uc64 = std::array; - std::array blind15_factor(std::span server_pk) { assert(server_pk.size() == 32); - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, nullptr, 0, 64); - crypto_generichash_blake2b_update(&st, server_pk.data(), server_pk.size()); uc64 blind_hash; - crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + hash::blake2b(blind_hash, server_pk); uc32 k; crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); @@ -324,7 +318,7 @@ std::pair blind25_key_pair( return result; } -static const auto version_blinding_hash_key_sig = to_span("VersionCheckKey_sig"); +static constexpr auto version_blinding_hash_key_sig = "VersionCheckKey_sig"_uc; std::pair blind_version_key_pair(std::span ed25519_sk) { if (ed25519_sk.size() != 32 && ed25519_sk.size() != 64) @@ -335,13 +329,7 @@ std::pair blind_version_key_pair(std::span result; cleared_uc32 blind_seed; auto& [pk, sk] = result; - crypto_generichash_blake2b( - blind_seed.data(), - 32, - ed25519_sk.data(), - 32, - version_blinding_hash_key_sig.data(), - version_blinding_hash_key_sig.size()); + hash::blake2b_key(blind_seed, version_blinding_hash_key_sig, ed25519_sk.first(32)); // Reuse `sk` to avoid needing extra secure erasing: if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), blind_seed.data())) @@ -350,8 +338,8 @@ std::pair blind_version_key_pair(std::span blind25_sign( std::span ed25519_sk, @@ -377,21 +365,10 @@ std::vector blind25_sign( auto [A, a] = blind25_key_pair(ed25519_sk, to_span(server_pk)); uc32 seedhash; - crypto_generichash_blake2b( - seedhash.data(), - seedhash.size(), - ed25519_sk.data(), - 32, - hash_key_seed.data(), - hash_key_seed.size()); + hash::blake2b_key(seedhash, hash_key_seed, ed25519_sk.first(32)); uc64 r_hash; - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, hash_key_sig.data(), hash_key_sig.size(), r_hash.size()); - crypto_generichash_blake2b_update(&st, seedhash.data(), seedhash.size()); - crypto_generichash_blake2b_update(&st, A.data(), A.size()); - crypto_generichash_blake2b_update(&st, message.data(), message.size()); - crypto_generichash_blake2b_final(&st, r_hash.data(), r_hash.size()); + hash::blake2b_key(r_hash, hash_key_sig, seedhash, A, message); uc32 r; crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); diff --git a/src/config.cpp b/src/config.cpp index a8e2cf1fc..c1df4f693 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -16,6 +15,7 @@ #include "config/internal.hpp" #include "session/bt_merge.hpp" +#include "session/hash.hpp" #include "session/util.hpp" using namespace std::literals; @@ -346,8 +346,7 @@ namespace { } hash_t& hash_msg(hash_t& into, std::span serialized) { - crypto_generichash_blake2b( - into.data(), into.size(), serialized.data(), serialized.size(), nullptr, 0); + hash::blake2b(into, serialized); return into; } diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 85c73bd7d..8a37e20b2 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -8,6 +8,7 @@ #include #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" using namespace std::literals; @@ -40,14 +41,12 @@ static std::array ma // nonce reuse concern so that you would not only have to hash collide but also have it happen // on messages of identical sizes and identical domain. std::array key{0}; - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init(&state, nullptr, 0, key.size()); - crypto_generichash_blake2b_update(&state, key_base.data(), key_base.size()); oxenc::host_to_big_inplace(message_size); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&message_size), sizeof(message_size)); - crypto_generichash_blake2b_update(&state, to_unsigned(domain.data()), domain.size()); - crypto_generichash_blake2b_final(&state, key.data(), key.size()); + hash::blake2b( + key, + key_base, + std::span{reinterpret_cast(&message_size), sizeof(message_size)}, + to_span(domain)); return key; } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 85e242872..833317197 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -21,6 +21,7 @@ #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" +#include "session/hash.hpp" #include "session/multi_encrypt.hpp" #include "session/session_encrypt.hpp" #include "session/xed25519.hpp" @@ -244,7 +245,7 @@ namespace { const std::span enc_key_hash_key = to_span("SessionGroupKeyGen"); constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; constexpr auto enc_key_member_hash_key = "SessionGroupKeyMemberKey"sv; - const std::span junk_seed_hash_key = to_span("SessionGroupJunkMembers"); + constexpr auto junk_seed_hash_key = "SessionGroupJunkMembers"_uc; } // namespace @@ -372,11 +373,7 @@ std::span Keys::rekey(Info& info, Members& members) { junk_data.resize(encrypted.size() * n_junk); std::array rng_seed; - crypto_generichash_blake2b_init( - &st, junk_seed_hash_key.data(), junk_seed_hash_key.size(), rng_seed.size()); - crypto_generichash_blake2b_update(&st, h1.data(), h1.size()); - crypto_generichash_blake2b_update(&st, _sign_sk.data(), _sign_sk.size()); - crypto_generichash_blake2b_final(&st, rng_seed.data(), rng_seed.size()); + hash::blake2b_key(rng_seed, junk_seed_hash_key, h1, _sign_sk); randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); std::string_view junk_view = to_string_view(junk_data); @@ -540,13 +537,13 @@ std::array Keys::subaccount_blind_factor( static_assert(mask.size() == crypto_generichash_blake2b_KEYBYTES); std::array h; - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, mask.data(), mask.size(), h.size()); - crypto_generichash_blake2b_update(&st, to_unsigned("\x05"), 1); - crypto_generichash_blake2b_update(&st, session_xpk.data(), session_xpk.size()); - crypto_generichash_blake2b_update(&st, to_unsigned("\x03"), 1); - crypto_generichash_blake2b_update(&st, _sign_pk->data(), _sign_pk->size()); - crypto_generichash_blake2b_final(&st, h.data(), h.size()); + hash::blake2b_key( + h, + mask, + std::array{'\x05'}, + session_xpk, + std::array{'\x03'}, + *_sign_pk); std::array out; crypto_core_ed25519_scalar_reduce(out.data(), h.data()); @@ -716,25 +713,16 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // // (using the standard Ed25519 SHA-512 here for H) - constexpr auto seed_hash_key = "SubaccountSeed"sv; - constexpr auto r_hash_key = "SubaccountSig"sv; + constexpr std::array seed_hash_key = { + 'S', 'u', 'b', 'a', 'c', 'c', 'o', 'u', 'n', 't', 'S', 'e', 'e', 'd'}; + constexpr std::array r_hash_key = { + 'S', 'u', 'b', 'a', 'c', 'c', 'o', 'u', 'n', 't', 'S', 'i', 'g'}; std::array hseed; - crypto_generichash_blake2b( - hseed.data(), - hseed.size(), - user_ed25519_sk.data(), - 32, - to_unsigned(seed_hash_key.data()), - seed_hash_key.size()); + hash::blake2b_key( + hseed, seed_hash_key, std::span{user_ed25519_sk.data(), 32}); std::array tmp; - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init( - &st, to_unsigned(r_hash_key.data()), r_hash_key.size(), tmp.size()); - crypto_generichash_blake2b_update(&st, hseed.data(), hseed.size()); - crypto_generichash_blake2b_update(&st, kT.data(), kT.size()); - crypto_generichash_blake2b_update(&st, msg.data(), msg.size()); - crypto_generichash_blake2b_final(&st, tmp.data(), tmp.size()); + hash::blake2b_key(tmp, r_hash_key, hseed, kT, msg); std::array r; crypto_core_ed25519_scalar_reduce(r.data(), tmp.data()); diff --git a/src/core/pro.cpp b/src/core/pro.cpp index ebbebcd69..ad7d1d7fa 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -35,7 +35,7 @@ void Pro::update_revocations( if (revocations_ticket_ && ticket == *revocations_ticket_) return; - auto already_hashed = [](const array_uc32& a) { + auto already_hashed = [](const uc32& a) { size_t h; std::memcpy(&h, a.data(), sizeof(h)); return h; @@ -45,9 +45,9 @@ void Pro::update_revocations( SQLite::Transaction tx{c.sql}; - std::unordered_set to_remove; - for (auto id : c.prepared_results>( - "SELECT gen_index_hash FROM pro_revocations")) + std::unordered_set to_remove; + for (auto id : + c.prepared_results>("SELECT gen_index_hash FROM pro_revocations")) to_remove.insert(id); for (auto st = c.prepared_st( diff --git a/src/ed25519.cpp b/src/ed25519.cpp index bce3d269a..943e2d27a 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -1,42 +1,14 @@ #include "session/ed25519.hpp" -#include #include #include #include #include "session/export.h" +#include "session/hash.hpp" #include "session/sodium_array.hpp" -template -using uc32 = std::array; -using uc64 = std::array; - -namespace { -uc64 derived_ed25519_privkey(std::span ed25519_seed, std::string_view key) { - if (ed25519_seed.size() != 32 && ed25519_seed.size() != 64) - throw std::invalid_argument{ - "Invalid ed25519_seed: expected 32 bytes or libsodium style 64 bytes seed"}; - - // Construct seed for derived key - // new_seed = Blake2b32(ed25519_seed, key=) - // b/B = Ed25519FromSeed(new_seed) - session::cleared_uc32 s2 = {}; - int hash_result = crypto_generichash_blake2b( - s2.data(), - s2.size(), - ed25519_seed.data(), - ed25519_seed.size(), - reinterpret_cast(key.data()), - key.size()); - assert(hash_result == 0); // This function can't return 0 unless misused - - auto [pubkey, privkey] = session::ed25519::ed25519_key_pair(s2); - return privkey; -} -} // namespace - namespace session::ed25519 { std::pair, std::array> ed25519_key_pair() { @@ -111,9 +83,21 @@ bool verify( std::array ed25519_pro_privkey_for_ed25519_seed( std::span ed25519_seed) { - auto result = derived_ed25519_privkey(ed25519_seed, "SessionProRandom"); - return result; + + if (ed25519_seed.size() != 32 && ed25519_seed.size() != 64) + throw std::invalid_argument{ + "Invalid ed25519_seed: expected 32 bytes or libsodium style 64 bytes seed"}; + + // Construct seed for derived key + // new_seed = Blake2b32(ed25519_seed, key=) + // b/B = Ed25519FromSeed(new_seed) + session::cleared_uc32 s2; + session::hash::blake2b_key(s2, "SessionProRandom"_uc, ed25519_seed.first<32>()); + + auto [pubkey, privkey] = session::ed25519::ed25519_key_pair(s2); + return privkey; } + } // namespace session::ed25519 using namespace session; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index b995e1491..87eb4e5b4 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -192,7 +193,7 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -202,7 +203,7 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( cleared_uc64 rotating_from_seed; if (rotating_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 rotating_pubkey; + uc32 rotating_pubkey; crypto_sign_ed25519_seed_keypair( rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); rotating_privkey = rotating_from_seed; @@ -224,7 +225,7 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L171 - array_uc32 hash_to_sign; + uc32 hash_to_sign; crypto_generichash_blake2b_state state; crypto_generichash_blake2b_init_salt_personal( &state, nullptr, 0, hash_to_sign.size(), nullptr, ADD_PRO_PAYMENT_PERS.data()); @@ -278,7 +279,7 @@ std::string AddProPaymentRequest::build_to_json( std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -288,7 +289,7 @@ std::string AddProPaymentRequest::build_to_json( cleared_uc64 rotating_from_seed; if (rotating_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 rotating_pubkey; + uc32 rotating_pubkey; crypto_sign_ed25519_seed_keypair( rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); rotating_privkey = rotating_from_seed; @@ -381,7 +382,7 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( cleared_uc64 master_from_seed; if (master_privkey.size() == 32) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -391,7 +392,7 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( cleared_uc64 rotating_from_seed; if (rotating_privkey.size() == 32) { - array_uc32 rotating_pubkey; + uc32 rotating_pubkey; crypto_sign_ed25519_seed_keypair( rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); rotating_privkey = rotating_from_seed; @@ -403,18 +404,15 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 uint8_t version = 0; uint64_t unix_ts_ms = epoch_ms(unix_ts); - array_uc32 hash_to_sign; - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init_salt_personal( - &state, nullptr, 0, 32, nullptr, GENERATE_PROOF_PERS.data()); - crypto_generichash_blake2b_update(&state, &version, sizeof(version)); - crypto_generichash_blake2b_update( - &state, master_privkey.data() + 32, crypto_sign_ed25519_PUBLICKEYBYTES); - crypto_generichash_blake2b_update( - &state, rotating_privkey.data() + 32, crypto_sign_ed25519_PUBLICKEYBYTES); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)); - crypto_generichash_blake2b_final(&state, hash_to_sign.data(), hash_to_sign.size()); + uc32 hash_to_sign; + hash::blake2b_pers( + hash_to_sign, + GENERATE_PROOF_PERS, + std::span{&version, 1}, + master_privkey.last<32>(), + rotating_privkey.last<32>(), + std::span{ + reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)}); // Sign the hash with both keys MasterRotatingSignatures result = {}; @@ -441,7 +439,7 @@ std::string GenerateProProofRequest::build_to_json( // Rederive keys from 32 byte seed if given cleared_uc64 master_from_seed; if (master_privkey.size() == 32) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -451,7 +449,7 @@ std::string GenerateProProofRequest::build_to_json( cleared_uc64 rotating_from_seed; if (rotating_privkey.size() == 32) { - array_uc32 rotating_pubkey; + uc32 rotating_pubkey; crypto_sign_ed25519_seed_keypair( rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); rotating_privkey = rotating_from_seed; @@ -551,14 +549,14 @@ std::string GetProDetailsRequest::to_json() const { return result; } -array_uc64 GetProDetailsRequest::build_sig( +uc64 GetProDetailsRequest::build_sig( uint8_t version, std::span master_privkey, std::chrono::sys_time unix_ts, uint32_t count) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -568,24 +566,20 @@ array_uc64 GetProDetailsRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 - array_uc32 hash_to_sign; - crypto_generichash_blake2b_state state; uint64_t unix_ts_ms = epoch_ms(unix_ts); - crypto_generichash_blake2b_init_salt_personal( - &state, nullptr, 0, 32, nullptr, GET_PRO_DETAILS_PERS.data()); - crypto_generichash_blake2b_update(&state, &version, sizeof(version)); - crypto_generichash_blake2b_update( - &state, - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&count), sizeof(count)); - crypto_generichash_blake2b_final(&state, hash_to_sign.data(), hash_to_sign.size()); + uc32 hash_to_sign; + hash::blake2b_pers( + hash_to_sign, + GET_PRO_DETAILS_PERS, + std::span{&version, 1}, + master_privkey.last<32>(), + std::span{ + reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)}, + std::span{ + reinterpret_cast(&count), sizeof(count)}); // Sign the hash - array_uc64 result = {}; + uc64 result = {}; crypto_sign_ed25519_detached( result.data(), nullptr, @@ -602,7 +596,7 @@ std::string GetProDetailsRequest::build_to_json( uint32_t count) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -788,7 +782,7 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { return result; } -array_uc64 SetPaymentRefundRequestedRequest::build_sig( +uc64 SetPaymentRefundRequestedRequest::build_sig( uint8_t version, std::span master_privkey, std::chrono::sys_time unix_ts, @@ -798,7 +792,7 @@ array_uc64 SetPaymentRefundRequestedRequest::build_sig( std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - array_uc32 master_pubkey; + uc32 master_pubkey; crypto_sign_ed25519_seed_keypair( master_pubkey.data(), master_from_seed.data(), master_privkey.data()); master_privkey = master_from_seed; @@ -808,7 +802,7 @@ array_uc64 SetPaymentRefundRequestedRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 - array_uc32 hash_to_sign; + uc32 hash_to_sign; crypto_generichash_blake2b_state state; crypto_generichash_blake2b_init_salt_personal( &state, nullptr, 0, 32, nullptr, SET_PAYMENT_REFUND_REQUESTED_PERS.data()); @@ -844,7 +838,7 @@ array_uc64 SetPaymentRefundRequestedRequest::build_sig( crypto_generichash_blake2b_final(&state, hash_to_sign.data(), hash_to_sign.size()); // Sign the hash - array_uc64 result = {}; + uc64 result = {}; crypto_sign_ed25519_detached( result.data(), nullptr, @@ -862,7 +856,7 @@ std::string SetPaymentRefundRequestedRequest::build_to_json( SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, std::span payment_tx_payment_id, std::span payment_tx_order_id) { - array_uc64 sig = SetPaymentRefundRequestedRequest::build_sig( + uc64 sig = SetPaymentRefundRequestedRequest::build_sig( version, master_privkey, unix_ts, diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 8b232b5c1..3f763b17a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -26,6 +26,7 @@ #include #include "session/blinding.hpp" +#include "session/hash.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" @@ -108,7 +109,7 @@ std::vector sign_for_recipient( return buf; } -static const std::span BOX_HASHKEY = to_span("SessionBoxEphemeralHashKey"); +static constexpr auto BOX_HASHKEY = "SessionBoxEphemeralHashKey"_uc; std::vector encrypt_for_recipient( std::span ed25519_privkey, @@ -145,12 +146,8 @@ std::vector encrypt_for_recipient_deterministic( // To make our ephemeral seed we're going to hash: SENDER_SEED || RECIPIENT_PK || MESSAGE with a // keyed blake2b hash. cleared_uchars seed; - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, BOX_HASHKEY.data(), BOX_HASHKEY.size(), seed.size()); - crypto_generichash_blake2b_update(&st, ed25519_privkey.data(), 32); - crypto_generichash_blake2b_update(&st, recipient_pubkey.data(), 32); - crypto_generichash_blake2b_update(&st, message.data(), message.size()); - crypto_generichash_blake2b_final(&st, seed.data(), seed.size()); + hash::blake2b_key( + seed, BOX_HASHKEY, ed25519_privkey.first(32), recipient_pubkey.first(32), message); cleared_uchars eph_sk; cleared_uchars eph_pk; @@ -161,10 +158,7 @@ std::vector encrypt_for_recipient_deterministic( // hash of: // EPH_PUBKEY || RECIPIENT_PUBKEY cleared_uchars nonce; - crypto_generichash_blake2b_init(&st, nullptr, 0, nonce.size()); - crypto_generichash_blake2b_update(&st, eph_pk.data(), eph_pk.size()); - crypto_generichash_blake2b_update(&st, recipient_pubkey.data(), recipient_pubkey.size()); - crypto_generichash_blake2b_final(&st, nonce.data(), nonce.size()); + hash::blake2b(nonce, eph_pk, recipient_pubkey); // A sealed box is a regular box (using the ephermal keys and nonce), but with the ephemeral // pubkey prepended: @@ -267,12 +261,7 @@ static cleared_uc32 blinded_shared_secret( auto& recipient = sending ? jB : kA; // H(kjsR || kS || jR): - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, nullptr, 0, 32); - crypto_generichash_blake2b_update(&st, shared_secret.data(), shared_secret.size()); - crypto_generichash_blake2b_update(&st, sender.data(), sender.size()); - crypto_generichash_blake2b_update(&st, recipient.data(), recipient.size()); - crypto_generichash_blake2b_final(&st, shared_secret.data(), shared_secret.size()); + hash::blake2b(shared_secret, shared_secret, sender, recipient); return shared_secret; } diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index fa49be92d..507b01cc9 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -2,11 +2,11 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -48,25 +48,24 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { // clang-format on namespace { -session::array_uc32 proof_hash_internal( +session::uc32 proof_hash_internal( std::uint8_t version, std::span gen_index_hash, std::span rotating_pubkey, std::uint64_t expiry_unix_ts_ms) { - constexpr std::string_view PRO_BACKEND_BLAKE2B_PERSONALISATION = "SeshProBackend__"; // This must match the hashing routine at // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 - session::array_uc32 result = {}; - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init_salt_personal( - &state, nullptr, 0, 32, nullptr, session::BUILD_PROOF_PERS.data()); - crypto_generichash_blake2b_update(&state, &version, sizeof(version)); - crypto_generichash_blake2b_update(&state, gen_index_hash.data(), gen_index_hash.size()); - crypto_generichash_blake2b_update(&state, rotating_pubkey.data(), rotating_pubkey.size()); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&expiry_unix_ts_ms), sizeof(expiry_unix_ts_ms)); - crypto_generichash_blake2b_final(&state, result.data(), result.size()); + session::uc32 result = {}; + session::hash::blake2b_pers( + result, + session::BUILD_PROOF_PERS, + std::span{&version, 1}, + gen_index_hash, + rotating_pubkey, + std::span{ + reinterpret_cast(&expiry_unix_ts_ms), + sizeof(expiry_unix_ts_ms)}); return result; } @@ -106,7 +105,7 @@ bool proof_verify_message_internal( struct array_uc32_from_ptr_result { bool success; - session::array_uc32 data; + session::uc32 data; }; static array_uc32_from_ptr_result array_uc32_from_ptr(const void* ptr, size_t len) { @@ -163,7 +162,7 @@ bool ProProof::verify_signature(const std::span& verify_pubkey) c "Invalid verify_pubkey: Must be 32 byte Ed25519 public key (was: {})", verify_pubkey.size())}; - array_uc32 hash_to_sign = hash(); + uc32 hash_to_sign = hash(); bool result = proof_verify_signature_internal(hash_to_sign, sig, verify_pubkey); return result; } @@ -202,8 +201,8 @@ ProStatus ProProof::status( return result; } -array_uc32 ProProof::hash() const { - array_uc32 result = proof_hash_internal( +uc32 ProProof::hash() const { + uc32 result = proof_hash_internal( version, gen_index_hash, rotating_pubkey, expiry_unix_ts.time_since_epoch().count()); return result; } @@ -279,7 +278,7 @@ std::vector encode_for_1o1( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, + const uc33& recipient_pubkey, std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::SyncOr1o1; @@ -295,8 +294,8 @@ std::vector encode_for_community_inbox( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, - const array_uc32& community_pubkey, + const uc33& recipient_pubkey, + const uc32& community_pubkey, std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::CommunityInbox; @@ -325,7 +324,7 @@ std::vector encode_for_group( std::span plaintext, std::span ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& group_ed25519_pubkey, + const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; @@ -579,7 +578,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( // We need to sign the padded content, so we pad the `Content` then sign it tmp_content_buffer = pad_message(content); - array_uc64 pro_sig; + uc64 pro_sig; bool was_signed = crypto_sign_ed25519_detached( pro_sig.data(), nullptr, @@ -657,7 +656,7 @@ std::vector encode_for_destination( DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, std::span envelope_payload, - const array_uc32& pro_backend_pubkey) { + const uc32& pro_backend_pubkey) { DecodedEnvelope result = {}; SessionProtos::Envelope envelope = {}; std::span envelope_plaintext = envelope_payload; @@ -916,7 +915,7 @@ DecodedEnvelope decode_envelope( DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, std::chrono::sys_time unix_ts, - const array_uc32& pro_backend_pubkey) { + const uc32& pro_backend_pubkey) { // TODO: Community message parsing requires a custom code path for now as we are planning to // migrate from sending plain `Content` to `Content` with a pro signature embedded in `Content` // (added exclusively for communities usecase), then, transitioning to sending an `Envelope` to @@ -1154,7 +1153,7 @@ LIBSESSION_C_API void session_protocol_pro_message_bitset_unset( LIBSESSION_C_API bytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { bytes32 result = {}; - session::array_uc32 hash = proof_hash_internal( + session::uc32 hash = proof_hash_internal( proof->version, proof->gen_index_hash.data, proof->rotating_pubkey.data, @@ -1170,7 +1169,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( if (verify_pubkey_len != crypto_sign_ed25519_PUBLICKEYBYTES) return false; auto verify_pubkey_span = std::span(verify_pubkey, verify_pubkey_len); - session::array_uc32 hash = proof_hash_internal( + session::uc32 hash = proof_hash_internal( proof->version, proof->gen_index_hash.data, proof->rotating_pubkey.data, diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 3110eea54..0d7411a9c 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -1,7 +1,6 @@ #include "session/xed25519.hpp" #include -#include #include #include #include @@ -11,10 +10,13 @@ #include #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" namespace session::xed25519 { +using namespace session::literals; + template using bytes = std::array; @@ -31,18 +33,10 @@ namespace { bytes<64> random; randombytes_buf(random.data(), random.size()); - constexpr static bytes<16> personality = { - 'x', 'e', 'd', '2', '5', '5', '1', '9', 's', 'i', 'g', 'n', 'a', 't', 'u', 'r'}; + constexpr static auto personality = "xed25519signatur"_b2b_pers; - crypto_generichash_blake2b_state st; - static_assert(personality.size() == crypto_generichash_blake2b_PERSONALBYTES); - crypto_generichash_blake2b_init_salt_personal( - &st, nullptr, 0, 64, nullptr, personality.data()); - crypto_generichash_blake2b_update(&st, a.data(), a.size()); - crypto_generichash_blake2b_update(&st, msg.data(), msg.size()); - crypto_generichash_blake2b_update(&st, random.data(), random.size()); bytes<64> h_aMZ; - crypto_generichash_blake2b_final(&st, h_aMZ.data(), h_aMZ.size()); + hash::blake2b_pers(h_aMZ, personality, a, msg, random); bytes<32> r; crypto_core_ed25519_scalar_reduce(r.data(), h_aMZ.data()); From a46b400f3b63f0494a62167097b5a59faf05b0bb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 23:27:49 -0300 Subject: [PATCH 015/193] Make hashing endian-safe; more blake2b_pers conversion - Including raw integer bytes would break the hashes on a non-little endian arches; this adds a helper than ensures we are always hashing the little-endian value. - Make the remaining manual hash use blake2b_pers. This didn't get autoconverted before because the `if` around part of the hash, but that if is actually completely unnecessary: if the value is empty, a blake2b hash update does nothing, and so it can just be always included (and when empty, it is still the right thing). --- include/session/util.hpp | 10 +++++ src/pro_backend.cpp | 94 ++++++++++++---------------------------- src/session_protocol.cpp | 6 +-- 3 files changed, 40 insertions(+), 70 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index 94a3288bb..aa5dd8a5a 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -343,6 +344,15 @@ inline int64_t to_epoch_seconds(int64_t timestamp) { : timestamp; } +// Converts an integer to a fixed-size array of its little-endian byte representation, suitable for +// passing to a hash function in an endian-safe way. +template +std::array int_for_hashing(T value) { + std::array result; + oxenc::write_host_as_little(value, result.data()); + return result; +} + // Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it. inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) { return std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 87eb4e5b4..2ebe49d63 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -12,6 +11,7 @@ #include #include #include +#include // clang-format off const session_pro_backend_payment_provider_metadata SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT] = { @@ -226,32 +226,17 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L171 uc32 hash_to_sign; - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init_salt_personal( - &state, nullptr, 0, hash_to_sign.size(), nullptr, ADD_PRO_PAYMENT_PERS.data()); - crypto_generichash_blake2b_update(&state, &version, sizeof(version)); - crypto_generichash_blake2b_update( - &state, - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - crypto_generichash_blake2b_update( - &state, - rotating_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - - uint8_t provider_u8 = payment_tx_provider; - crypto_generichash_blake2b_update(&state, &provider_u8, sizeof(provider_u8)); - crypto_generichash_blake2b_update( - &state, - reinterpret_cast(payment_tx_payment_id.data()), - payment_tx_payment_id.size()); - if (payment_tx_order_id.size()) { - crypto_generichash_blake2b_update( - &state, - reinterpret_cast(payment_tx_order_id.data()), - payment_tx_order_id.size()); - } - crypto_generichash_blake2b_final(&state, hash_to_sign.data(), hash_to_sign.size()); + hash::blake2b_pers( + hash_to_sign, + ADD_PRO_PAYMENT_PERS, + int_for_hashing(version), + master_privkey.subspan( + crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), + rotating_privkey.subspan( + crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), + int_for_hashing(static_cast(payment_tx_provider)), + payment_tx_payment_id, + payment_tx_order_id); // Sign the hash with both keys MasterRotatingSignatures result = {}; @@ -408,11 +393,10 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( hash::blake2b_pers( hash_to_sign, GENERATE_PROOF_PERS, - std::span{&version, 1}, + int_for_hashing(version), master_privkey.last<32>(), rotating_privkey.last<32>(), - std::span{ - reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)}); + int_for_hashing(unix_ts_ms)); // Sign the hash with both keys MasterRotatingSignatures result = {}; @@ -571,12 +555,10 @@ uc64 GetProDetailsRequest::build_sig( hash::blake2b_pers( hash_to_sign, GET_PRO_DETAILS_PERS, - std::span{&version, 1}, + int_for_hashing(version), master_privkey.last<32>(), - std::span{ - reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)}, - std::span{ - reinterpret_cast(&count), sizeof(count)}); + int_for_hashing(unix_ts_ms), + int_for_hashing(count)); // Sign the hash uc64 result = {}; @@ -803,39 +785,19 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 uc32 hash_to_sign; - crypto_generichash_blake2b_state state; - crypto_generichash_blake2b_init_salt_personal( - &state, nullptr, 0, 32, nullptr, SET_PAYMENT_REFUND_REQUESTED_PERS.data()); - crypto_generichash_blake2b_update(&state, &version, sizeof(version)); - crypto_generichash_blake2b_update( - &state, - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - - // Timestamps uint64_t unix_ts_ms = epoch_ms(unix_ts); uint64_t refund_requested_unix_ts_ms = epoch_ms(refund_requested_unix_ts); - crypto_generichash_blake2b_update( - &state, reinterpret_cast(&unix_ts_ms), sizeof(unix_ts_ms)); - crypto_generichash_blake2b_update( - &state, - reinterpret_cast(&refund_requested_unix_ts_ms), - sizeof(refund_requested_unix_ts_ms)); - - // Payment provider - uint8_t provider_u8 = payment_tx_provider; - crypto_generichash_blake2b_update(&state, &provider_u8, sizeof(provider_u8)); - crypto_generichash_blake2b_update( - &state, - reinterpret_cast(payment_tx_payment_id.data()), - payment_tx_payment_id.size()); - if (payment_tx_order_id.size()) { - crypto_generichash_blake2b_update( - &state, - reinterpret_cast(payment_tx_order_id.data()), - payment_tx_order_id.size()); - } - crypto_generichash_blake2b_final(&state, hash_to_sign.data(), hash_to_sign.size()); + hash::blake2b_pers( + hash_to_sign, + SET_PAYMENT_REFUND_REQUESTED_PERS, + int_for_hashing(version), + master_privkey.subspan( + crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), + int_for_hashing(unix_ts_ms), + int_for_hashing(refund_requested_unix_ts_ms), + int_for_hashing(static_cast(payment_tx_provider)), + payment_tx_payment_id, + payment_tx_order_id); // Sign the hash uc64 result = {}; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 507b01cc9..8407cd19b 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -60,12 +60,10 @@ session::uc32 proof_hash_internal( session::hash::blake2b_pers( result, session::BUILD_PROOF_PERS, - std::span{&version, 1}, + session::int_for_hashing(version), gen_index_hash, rotating_pubkey, - std::span{ - reinterpret_cast(&expiry_unix_ts_ms), - sizeof(expiry_unix_ts_ms)}); + session::int_for_hashing(expiry_unix_ts_ms)); return result; } From 7463d476197fdd3deec801e68554604ed640fff0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 14:02:44 -0300 Subject: [PATCH 016/193] Use std::byte to avoid oxenc buggy overload oxenc has buggy "constexpr" overloads that just break if invoked, and they get invoked here with a uint8_t value + unsigned char array. The issue is fixed in oxenc dev, but switching to a byte here works around it for current and older oxenc versions. --- include/session/util.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index aa5dd8a5a..2821e4af3 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -347,8 +347,8 @@ inline int64_t to_epoch_seconds(int64_t timestamp) { // Converts an integer to a fixed-size array of its little-endian byte representation, suitable for // passing to a hash function in an endian-safe way. template -std::array int_for_hashing(T value) { - std::array result; +std::array int_for_hashing(T value) { + std::array result; oxenc::write_host_as_little(value, result.data()); return result; } From ec673dd931ee4d1cf7d2c3b71fb6eae732d45ae2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 14:06:08 -0300 Subject: [PATCH 017/193] array_ucNN -> ucNN, for test suite The recent commit to unify the types wasn't applied to the test suite. --- tests/test_config_convo_info_volatile.cpp | 4 +-- tests/test_session_protocol.cpp | 30 +++++++++++------------ tests/utils.hpp | 22 ++++++++--------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index fe812f236..74b30dc5d 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -626,7 +626,7 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { c.pro_expiry_unix_ts = std::chrono::sys_time{ std::chrono::milliseconds{unix_timestamp(i)}}; - session::array_uc32 hash{}; + session::uc32 hash{}; std::fill(hash.begin(), hash.end(), static_cast(i % 256)); c.pro_gen_index_hash = hash; } @@ -836,7 +836,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { .count(); c.pro_expiry_unix_ts_ms = 10000; - session::array_uc32 hash{}; + session::uc32 hash{}; std::fill(hash.begin(), hash.end(), static_cast(3)); std::memcpy(c.pro_gen_index_hash.data, hash.data(), hash.size()); c.has_pro_gen_index_hash = true; diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 8b154c5f8..d1ed14be4 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -17,17 +17,17 @@ struct SerialisedProtobufContentWithProForTesting { ProProof proof; std::string plaintext; std::vector plaintext_padded; - array_uc64 sig_over_plaintext_with_user_pro_key; - array_uc64 sig_over_plaintext_padded_with_user_pro_key; - array_uc32 pro_proof_hash; + uc64 sig_over_plaintext_with_user_pro_key; + uc64 sig_over_plaintext_padded_with_user_pro_key; + uc32 pro_proof_hash; bytes64 sig_over_plaintext_with_user_pro_key_c; bytes32 pro_proof_hash_c; }; static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_session_pro( std::string_view data_body, - const array_uc64& user_rotating_privkey, - const array_uc64& pro_backend_privkey, + const uc64& user_rotating_privkey, + const uc64& pro_backend_privkey, std::chrono::sys_seconds content_unix_ts, std::chrono::sys_seconds pro_expiry_unix_ts, session_protocol_pro_message_bitset msg_bitset, @@ -178,8 +178,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Pro metadata const auto user_pro_seed = "0123456789abcdef0123456789abcdeff00baa00000000000000000000000000"_hexbytes; - array_uc32 user_pro_ed_pk; - array_uc64 user_pro_ed_sk; + uc32 user_pro_ed_pk; + uc64 user_pro_ed_sk; crypto_sign_ed25519_seed_keypair( user_pro_ed_pk.data(), user_pro_ed_sk.data(), user_pro_seed.data()); @@ -230,8 +230,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Setup a dummy "Session Pro Backend" key // We reuse test key 1 as the "Session Pro" backend key that signs the proofs as it // doesn't matter what key really, just that we have one available for signing. - const array_uc64& pro_backend_ed_sk = keys.ed_sk1; - const array_uc32& pro_backend_ed_pk = keys.ed_pk1; + const uc64& pro_backend_ed_sk = keys.ed_sk1; + const uc32& pro_backend_ed_pk = keys.ed_pk1; char error[256]; SECTION("Encrypt/decrypt for contact in default namespace w/o pro attached") { @@ -286,7 +286,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro ProProof nil_proof = {}; - array_uc32 nil_hash = nil_proof.hash(); + uc32 nil_hash = nil_proof.hash(); bytes32 decrypt_result_pro_hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(decrypt_result.pro.status == @@ -526,8 +526,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // TODO: Finish setting up a fake group const auto group_v2_seed = "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; - array_uc64 group_v2_sk = {}; - array_uc32 group_v2_pk = {}; + uc64 group_v2_sk = {}; + uc32 group_v2_pk = {}; crypto_sign_ed25519_seed_keypair( group_v2_pk.data(), group_v2_sk.data(), group_v2_seed.data()); @@ -693,7 +693,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Try decrypt with a bad backend key { - array_uc32 bad_pro_backend_ed_pk = pro_backend_ed_pk; + uc32 bad_pro_backend_ed_pk = pro_backend_ed_pk; bad_pro_backend_ed_pk[0] ^= 1; session_protocol_decoded_envelope decrypt_result = session_protocol_decode_envelope( &decrypt_keys, @@ -850,8 +850,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encode/decode for community inbox (content message)") { const auto community_seed = "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; - array_uc64 community_sk = {}; - array_uc32 community_pk = {}; + uc64 community_sk = {}; + uc32 community_pk = {}; crypto_sign_ed25519_seed_keypair( community_pk.data(), community_sk.data(), community_seed.data()); diff --git a/tests/utils.hpp b/tests/utils.hpp index 047a3ba77..b2e9fd6ab 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -205,17 +205,17 @@ std::set> make_set(T&&... args) { } struct TestKeys { - session::array_uc32 seed0; - session::array_uc64 ed_sk0; - session::array_uc32 ed_pk0; - session::array_uc32 curve_pk0; - session::array_uc33 session_pk0; - - session::array_uc32 seed1; - session::array_uc64 ed_sk1; - session::array_uc32 ed_pk1; - session::array_uc32 curve_pk1; - session::array_uc33 session_pk1; + session::uc32 seed0; + session::uc64 ed_sk0; + session::uc32 ed_pk0; + session::uc32 curve_pk0; + session::uc33 session_pk0; + + session::uc32 seed1; + session::uc64 ed_sk1; + session::uc32 ed_pk1; + session::uc32 curve_pk1; + session::uc33 session_pk1; }; static inline TestKeys get_deterministic_test_keys() { From fae949cad3ec162f9cf48b94f490827b962f217c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 15:31:20 -0300 Subject: [PATCH 018/193] Bake int_for_hashing into the hash helpers hash::blake2b (and related) now take integers directly, writing the integer bytes (with byte swapping applied, if necessary), which further simplies the hashing API. --- include/session/hash.hpp | 48 ++++++++++++++++++++++++++-------------- include/session/util.hpp | 10 --------- src/pro_backend.cpp | 22 +++++++++--------- src/session_protocol.cpp | 4 ++-- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index b93eefa94..c65b8d2e9 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -10,8 +11,6 @@ #include #include -#include "types.hpp" - namespace session::hash { /// API: hash/hash @@ -51,21 +50,35 @@ std::vector hash( template concept ByteContainer = std::ranges::contiguous_range && oxenc::basic_char>; +template +concept HashInput = ByteContainer || oxenc::endian_swappable_integer; /// API: hash/update_all /// /// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte -/// containers and updates the hash state with them, in argument order. -template +/// containers *or* integer values and updates the hash state with them, in argument order. Integer +/// values are always written as raw bytes in little-endian encoding (i.e. they will be byte-swapped +/// if necessary). +template requires(sizeof...(T) > 0) void update_all(crypto_generichash_blake2b_state& st, const T&... args) { - auto update_one = [&st](const auto& arg) { - crypto_generichash_blake2b_update( - &st, - reinterpret_cast(std::ranges::data(arg)), - std::ranges::size(arg)); + auto make_hashable = [](const U& val) { + if constexpr (ByteContainer) + return std::span{ + reinterpret_cast(std::ranges::data(val)), + std::ranges::size(val)}; + else if constexpr (oxenc::little_endian || sizeof(val) == 1) + return std::span{reinterpret_cast(&val), sizeof(val)}; + else { + std::array swapped; + oxenc::write_big_as_host(swapped.data(), val); + return swapped; + } + }; + auto update_hash = [&st](std::span arg) { + crypto_generichash_blake2b_update(&st, arg.data(), arg.size()); }; - (update_one(args), ...); + (update_hash(make_hashable(args)), ...); } namespace detail { @@ -101,7 +114,7 @@ inline constexpr std::span nullkey{}; /// /// This version of blake2b() takes a key as the second argument and computes a keyed hash. The key /// must be between 0 and 64 characters long. (A 0-length key is equivalent to no key). -template +template requires(sizeof...(T) > 0) void blake2b_key(Out& out, const Key& key, const T&... args) { crypto_generichash_blake2b_state st; @@ -113,9 +126,10 @@ void blake2b_key(Out& out, const Key& key, const T&... args) { /// API: hash/blake2b /// -/// One-shot hasher that takes an output container and and any number of contiguous byte containers, -/// computes the blake2b hash of the concatentation of the containers (in argument order) and then -/// writes the hash into the output container. +/// One-shot hasher that takes an output container and any number of contiguous byte containers or +/// integer values, computes the blake2b hash of the concatentation of the containers (in argument +/// order) and then writes the hash into the output container. Integer values are hashed as their +/// little-endian (fixed size) byte representation. /// /// This version uses neither key nor personalisation strings; see blake2b_key, blake2b_pers, and /// blake2b_key_pers if you want one or both of those. @@ -125,7 +139,7 @@ void blake2b_key(Out& out, const Key& key, const T&... args) { /// /// It is permitted for overlap between the output and input containers; the output container is not /// written until all input containers have been consumed. -template +template requires(sizeof...(T) > 0) void blake2b(Out& out, const T&... args) { return blake2b_key(out, nullkey, args...); @@ -137,7 +151,7 @@ void blake2b(Out& out, const T&... args) { /// and third arguments and computes a keyed hash with a personalisation string. The /// personalisation string must be exact 16 bytes, and is typically constructed with "..."_b2b_pers /// for compile-time validation. The key must be between 0 and 64 bytes long. -template +template requires(sizeof...(T) > 0) void blake2b_key_pers( Out& out, const Key& key, std::span pers, const T&... args) { @@ -158,7 +172,7 @@ void blake2b_key_pers( /// This version of blake2b() takes a 16-byte personality string as the second argument and computes /// a unkeyed hash with a personalisation string. The personalization string must be exact 16 /// bytes, and is typically constructed with "..."_b2b_pers for compile-time validation. -template +template requires(sizeof...(T) > 0) void blake2b_pers(Out& out, std::span pers, const T&... args) { return blake2b_key_pers(out, nullkey, pers, args...); diff --git a/include/session/util.hpp b/include/session/util.hpp index 2821e4af3..94a3288bb 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -344,15 +343,6 @@ inline int64_t to_epoch_seconds(int64_t timestamp) { : timestamp; } -// Converts an integer to a fixed-size array of its little-endian byte representation, suitable for -// passing to a hash function in an endian-safe way. -template -std::array int_for_hashing(T value) { - std::array result; - oxenc::write_host_as_little(value, result.data()); - return result; -} - // Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it. inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) { return std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 2ebe49d63..95278e076 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -229,12 +229,12 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( hash::blake2b_pers( hash_to_sign, ADD_PRO_PAYMENT_PERS, - int_for_hashing(version), + version, master_privkey.subspan( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), rotating_privkey.subspan( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), - int_for_hashing(static_cast(payment_tx_provider)), + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); @@ -393,10 +393,10 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( hash::blake2b_pers( hash_to_sign, GENERATE_PROOF_PERS, - int_for_hashing(version), + version, master_privkey.last<32>(), rotating_privkey.last<32>(), - int_for_hashing(unix_ts_ms)); + unix_ts_ms); // Sign the hash with both keys MasterRotatingSignatures result = {}; @@ -555,10 +555,10 @@ uc64 GetProDetailsRequest::build_sig( hash::blake2b_pers( hash_to_sign, GET_PRO_DETAILS_PERS, - int_for_hashing(version), + version, master_privkey.last<32>(), - int_for_hashing(unix_ts_ms), - int_for_hashing(count)); + unix_ts_ms, + count); // Sign the hash uc64 result = {}; @@ -790,12 +790,12 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( hash::blake2b_pers( hash_to_sign, SET_PAYMENT_REFUND_REQUESTED_PERS, - int_for_hashing(version), + version, master_privkey.subspan( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), - int_for_hashing(unix_ts_ms), - int_for_hashing(refund_requested_unix_ts_ms), - int_for_hashing(static_cast(payment_tx_provider)), + unix_ts_ms, + refund_requested_unix_ts_ms, + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 8407cd19b..061312344 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -60,10 +60,10 @@ session::uc32 proof_hash_internal( session::hash::blake2b_pers( result, session::BUILD_PROOF_PERS, - session::int_for_hashing(version), + version, gen_index_hash, rotating_pubkey, - session::int_for_hashing(expiry_unix_ts_ms)); + expiry_unix_ts_ms); return result; } From 04cbc9c030e9f96d3040bf2bd067e2e494a9fe01 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 16:58:22 -0300 Subject: [PATCH 019/193] Add emoji SAS support for link requests --- include/session/core/devices.hpp | 13 ++++++-- include/session/core/link_sas.hpp | 45 ++++++++++++++++++++++++++++ src/CMakeLists.txt | 1 + src/core.cpp | 4 +++ src/core/devices.cpp | 6 ++-- src/core/link_sas.cpp | 49 +++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 include/session/core/link_sas.hpp create mode 100644 src/core/link_sas.cpp diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 3aeb619a2..000f87c1e 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "component.hpp" @@ -158,11 +159,17 @@ class Devices final : detail::CoreComponent { // Returns *this* device's info and whether it is registered in the device group. std::pair device_info(); + struct LinkRequestResult { + std::vector message; // encrypted bytes to push to Namespace::DeviceLink + std::array sas; // emoji SAS sequence for user display + }; + // Builds an outgoing link request message to upload to Namespace::DeviceLink. This should // only be called when this device is not currently registered in the device group; throws - // std::logic_error if it is already registered. The returned bytes are to be pushed to - // Namespace::DeviceLink with a 10-minute TTL. - std::vector build_link_request(); + // std::logic_error if it is already registered. The returned message is to be pushed to + // Namespace::DeviceLink with a 10-minute TTL. The sas field contains the short authentication + // string that should be displayed to the user for verification against the accepting device. + LinkRequestResult build_link_request(); // Updates this device's info locally to match the given info; if the current device is // registered then this dirties the device config data, requiring a push. diff --git a/include/session/core/link_sas.hpp b/include/session/core/link_sas.hpp new file mode 100644 index 000000000..e3725c135 --- /dev/null +++ b/include/session/core/link_sas.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace session { + +using namespace std::literals; + +/// The 64 emoji used for short authentication strings, from the Matrix client-server API +/// specification v1.17, section 10.12.2.2.6. Selected for reasonable distinctiveness and +/// cross-platform compatibility. +inline constexpr std::array SAS_EMOJI = { + // clang-format off + "🐶"sv, "🐱"sv, "🦁"sv, "🐎"sv, "🦄"sv, "🐷"sv, "🐘"sv, "🐰"sv, + "🐼"sv, "🐓"sv, "🐧"sv, "🐢"sv, "🐟"sv, "🐙"sv, "🦋"sv, "🌷"sv, + "🌳"sv, "🌵"sv, "🍄"sv, "🌏"sv, "🌙"sv, "☁️"sv, "🔥"sv, "🍌"sv, + "🍎"sv, "🍓"sv, "🌽"sv, "🍕"sv, "🎂"sv, "❤️"sv, "😀"sv, "🤖"sv, + "🎩"sv, "👓"sv, "🔧"sv, "🎅"sv, "👍"sv, "☂️"sv, "⌛"sv, "⏰"sv, + "🎁"sv, "💡"sv, "📕"sv, "✏️"sv, "📎"sv, "✂️"sv, "🔒"sv, "🔑"sv, + "🔨"sv, "☎️"sv, "🏁"sv, "🚂"sv, "🚲"sv, "✈️"sv, "🚀"sv, "🏆"sv, + "⚽"sv, "🎸"sv, "🎺"sv, "🔔"sv, "⚓"sv, "🎧"sv, "📁"sv, "📌"sv, + // clang-format on +}; +static_assert(SAS_EMOJI.size() == 64); + +} // namespace session + +namespace session::core { + +/// Computes the short authentication string (SAS) for a device link request from its decrypted +/// plaintext bytes. The derivation is: +/// 1. salt = BLAKE2b-16(M, pers="SessionLinkEmoji") +/// 2. seed = Argon2id(M, salt, size=16, ops=2, mem=16MiB) +/// 3. Interpret seed as a 128-bit little-endian integer; extract 6-bit indices. +/// +/// Returns 21 string_view values (into the SAS_EMOJI table) for the full SAS sequence. The first +/// 7 are the standard short display; all 21 are available for the extended view. Formatting and +/// joining is left to the caller; the recommended layout is the first 7 joined with spaces for the +/// standard view, and 3 lines of 7 (joined with spaces within lines, newlines between) for the +/// extended view. +std::array link_request_sas(std::span plaintext); + +} // namespace session::core diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbc936f3b..68e3d8d1e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ add_libsession_util_library(core core.cpp core/component.cpp core/devices.cpp + core/link_sas.cpp core/pro.cpp ) add_subdirectory(core/schema) diff --git a/src/core.cpp b/src/core.cpp index 875872a12..140f38251 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -20,6 +21,9 @@ void Core::LoopDeleter::operator()(quic::Loop* p) const { } void Core::init() { + if (sodium_init() < 0) + throw std::runtime_error{"libsodium initialization failed!"}; + _loop.reset(new quic::Loop()); apply_migrations(); diff --git a/src/core/devices.cpp b/src/core/devices.cpp index bdec9fc45..10a7e3052 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -798,7 +799,7 @@ void Devices::receive_device_group_message(std::span data) f(); } -std::vector Devices::build_link_request() { +Devices::LinkRequestResult Devices::build_link_request() { auto [info, is_registered] = device_info(); if (is_registered) @@ -853,6 +854,7 @@ std::vector Devices::build_link_request() { info.pk_x25519); auto plaintext = encode_link_request_plaintext(self_id, info); + auto sas = link_request_sas(to_span(plaintext)); std::vector out(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); std::memcpy(out.data(), plaintext.data(), plaintext.size()); auto seed = core.globals.account_seed(); @@ -860,7 +862,7 @@ std::vector Devices::build_link_request() { as_span(std::span{out}), as_span(seed.buf).first(32), "link-request"); - return out; + return {std::move(out), sas}; } device::map Devices::decrypt_device_data(std::span enc_data) { diff --git a/src/core/link_sas.cpp b/src/core/link_sas.cpp new file mode 100644 index 000000000..52a396c4e --- /dev/null +++ b/src/core/link_sas.cpp @@ -0,0 +1,49 @@ +#include +#include + +#include +#include +#include + +using namespace session::literals; + +namespace session::core { + +std::array link_request_sas(std::span plaintext) { + std::array salt; + hash::blake2b_pers(salt, "SessionLinkEmoji"_b2b_pers, plaintext); + + std::array seed; + if (0 != crypto_pwhash( + seed.data(), + seed.size(), + reinterpret_cast(plaintext.data()), + plaintext.size(), + salt.data(), + /*opslimit=*/2, + /*memlimit=*/16ULL * 1024 * 1024, + crypto_pwhash_ALG_ARGON2ID13)) + throw std::runtime_error{"link_request_sas: Argon2id key derivation failed"}; + + // Interpret the 16-byte seed as a 128-bit little-endian integer split into two 64-bit words. + uint64_t lo = oxenc::load_little_to_host(seed.data()); + uint64_t hi = oxenc::load_little_to_host(seed.data() + 8); + + std::array result; + for (int k = 0; k < 21; k++) { + int bit = k * 6; + uint8_t index; + if (bit + 6 <= 64) + index = (lo >> bit) & 0x3F; + else if (bit >= 64) + index = (hi >> (bit - 64)) & 0x3F; + else + // Single crossing point: k=10, bit=60. + // Low 4 bits come from lo (bits 60-63), high 2 bits come from hi (bits 64-65). + index = ((lo >> 60) | (hi << 4)) & 0x3F; + result[k] = SAS_EMOJI[index]; + } + return result; +} + +} // namespace session::core From edf9cbba3eeb6bea6ab435e22694aaffb3fd7014 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 20:14:07 -0300 Subject: [PATCH 020/193] link request handling WIP --- include/session/core/callbacks.hpp | 18 ++--- include/session/core/devices.hpp | 11 ++- include/session/core/link_sas.hpp | 14 +++- src/core/devices.cpp | 109 +++++++++++++++++------------ src/core/link_sas.cpp | 16 +++-- src/core/schema/000_devices.sql | 13 ++++ 6 files changed, 116 insertions(+), 65 deletions(-) diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 8fec37106..0dbc8fc80 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -1,5 +1,7 @@ #pragma once #include +#include +#include #include namespace session::core { @@ -30,15 +32,13 @@ struct callbacks { /// If this callback is not set then new device link requests are ignored by this device. /// /// Parameters: - /// - reqid -- a unique identifier for this request; this is only used within the current - /// Core object, and is used to uniquely identify the request. It will *not* work across - /// Core teardown and restart. - /// - new_device -- the new device metadata included in the link request. Crucial in this info - /// is the link_emoji sequence: a vector of 8 emoji (each in a separate utf8 string, leaving - /// separation, display, etc. up to the front-end) that provides a visual identifier of the - /// account device identifier, and is used to visually verify that the device making the - /// request is in fact the same device that is being confirmed. - std::function device_link_request; + /// - reqid -- a unique identifier for this request that persists across Core restarts and can + /// be used to correlate this request with a subsequent device_added callback. + /// - new_device -- the new device metadata included in the link request. + /// - sas -- a span of 21 string_views representing the short authentication string for this + /// request. The first 7 are the standard display; all 21 are available for the extended + /// view. Formatting and joining is left to the caller. + std::function sas)> device_link_request; /// Callback that is invoked when a new device has been linked to the account. If a batch /// of messages being processed includes both a device link request *and* an acceptance diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 000f87c1e..b8c091dc6 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -31,8 +31,11 @@ namespace device { enum class State { Registered = 0, ///< Device is in the account's registered device set - Pending = 1, ///< Local device info that has a pending request to join the account's device - ///< set + Pending = 1, ///< A device with a pending link request. This is used for two cases: + ///< - This device has sent a request to join the account's device group and + ///< is awaiting acceptance by an existing device. + ///< - Another device has sent a link request that has been received but not + ///< yet accepted, ignored, or rejected by this device. Unregistered = 2, ///< Local device info that cannot be pushed because the device is not /// currently in the device group. }; @@ -69,10 +72,6 @@ namespace device { // possible a device nickname, but is generally free-form data. std::string description; - // This computes the current "device emoji" sequence, which depends on the device id and - // pubkeys. This allows a user to visually identify (and verify) a device within the device - // list at any time, but its primary purpose is for identifying the device during linking. - // Indicates whether the device is registered, pending registration, or not registered. State state; diff --git a/include/session/core/link_sas.hpp b/include/session/core/link_sas.hpp index e3725c135..e065ea774 100644 --- a/include/session/core/link_sas.hpp +++ b/include/session/core/link_sas.hpp @@ -29,11 +29,19 @@ static_assert(SAS_EMOJI.size() == 64); namespace session::core { -/// Computes the short authentication string (SAS) for a device link request from its decrypted -/// plaintext bytes. The derivation is: +/// Computes the Argon2id seed underlying the SAS for a device link request. The derivation is: /// 1. salt = BLAKE2b-16(M, pers="SessionLinkEmoji") /// 2. seed = Argon2id(M, salt, size=16, ops=2, mem=16MiB) -/// 3. Interpret seed as a 128-bit little-endian integer; extract 6-bit indices. +/// +/// The seed can be stored to avoid re-running the expensive Argon2id computation; pass it to +/// sas_from_seed() to recover the emoji sequence at any time. +std::array derive_sas_seed(std::span plaintext); + +/// Extracts the 21 SAS emoji from a pre-computed seed (as returned by derive_sas_seed). This is +/// a cheap bit-extraction operation with no cryptographic cost. +std::array sas_from_seed(std::span seed); + +/// Convenience wrapper: derives the seed and immediately returns the emoji sequence. /// /// Returns 21 string_view values (into the SAS_EMOJI table) for the full SAS sequence. The first /// 7 are the standard short display; all 21 are available for the extended view. Formatting and diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 10a7e3052..2d11595ef 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -216,6 +216,56 @@ namespace { } } + // Upserts a device into the devices table (guarded by seqno) and updates device_unknown extras. + // Returns the row id if inserted or updated (i.e. seqno guard allowed it), nullopt if the + // update was rejected by the seqno guard. info.id must be set to the 32-byte device id. + // Always sets changes=FALSE since this is for storing incoming device data, not local changes. + std::optional upsert_device_info(sqlite::Connection& c, const device::Info& info) { + auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; + auto dev_id = c.prepared_maybe_get( + R"(INSERT INTO devices + (unique_id, state, seqno, timestamp, device_type, description, version, + pubkey_mlkem768, pubkey_x25519, kicked_timestamp, changes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, FALSE) + ON CONFLICT(unique_id) DO UPDATE SET + state = excluded.state, + seqno = excluded.seqno, + timestamp = excluded.timestamp, + device_type = excluded.device_type, + description = excluded.description, + version = excluded.version, + pubkey_mlkem768 = excluded.pubkey_mlkem768, + pubkey_x25519 = excluded.pubkey_x25519, + kicked_timestamp = excluded.kicked_timestamp, + changes = excluded.changes + WHERE excluded.seqno > seqno + RETURNING id)", + info.id, + static_cast(info.state), + info.seqno, + info.timestamp.time_since_epoch().count(), + info.encoded_type(), + info.description, + ver, + info.pk_mlkem768, + info.pk_x25519); + + if (!dev_id) + return std::nullopt; + + c.prepared_exec("DELETE FROM device_unknown WHERE device = ?", *dev_id); + for (const auto& [key, val] : info.extra) { + auto encoded = std::visit([](const auto& v) { return oxenc::bt_serialize(v); }, val); + c.prepared_exec( + "INSERT INTO device_unknown (device, key, bt_value) VALUES (?, ?, ?)", + *dev_id, + key, + to_span(encoded)); + } + + return dev_id; + } + } // namespace device::map Devices::devices( @@ -399,6 +449,7 @@ namespace { } void decode_one(device::Info& info, oxenc::bt_dict_consumer dev, device::State state) { + info.state = state; read_extras(dev, "#", info.extra); info.seqno = dev.require("#"); @@ -466,6 +517,7 @@ namespace { throw std::runtime_error{"Invalid encoded device data: duplicate devices ids"}; auto& info = it->second; + info.id = id; if (in.is_integer()) { // An integer indicates a "device removed" timestamp, used to distinguish between @@ -739,57 +791,28 @@ void Devices::receive_device_group_message(std::span data) continue; } - auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; - - // Returns the row id if inserted or updated (i.e. seqno increased), nullopt if the seqno - // guard prevented an update. - auto dev_id = c.prepared_maybe_get( - R"(INSERT INTO devices - (unique_id, state, seqno, timestamp, device_type, description, version, - pubkey_mlkem768, pubkey_x25519, kicked_timestamp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL) - ON CONFLICT(unique_id) DO UPDATE SET - state = excluded.state, - seqno = excluded.seqno, - timestamp = excluded.timestamp, - device_type = excluded.device_type, - description = excluded.description, - version = excluded.version, - pubkey_mlkem768 = excluded.pubkey_mlkem768, - pubkey_x25519 = excluded.pubkey_x25519, - kicked_timestamp = excluded.kicked_timestamp - WHERE excluded.seqno > seqno - RETURNING id)", - id, - static_cast(info.state), - info.seqno, - info.timestamp.time_since_epoch().count(), - info.encoded_type(), - info.description, - ver, - info.pk_mlkem768, - info.pk_x25519); + auto dev_id = upsert_device_info(c, info); if (!dev_id) continue; - c.prepared_exec("DELETE FROM device_unknown WHERE device = ?", *dev_id); - for (const auto& [key, val] : info.extra) { - auto encoded = std::visit([](const auto& v) { return oxenc::bt_serialize(v); }, val); - c.prepared_exec( - "INSERT INTO device_unknown (device, key, bt_value) VALUES (?, ?, ?)", - *dev_id, - key, - to_span(encoded)); - } - if (!was_registered) { if (is_self) { if (auto& f = cb().device_self_added) post_commit.push_back(f); - } else if (auto& f = cb().device_added) { - // reqid=0: correlating with pending link request records is not yet implemented - post_commit.push_back([&f, &inf = info] { f(0, inf); }); + } else { + // If we had a pending link request for this device, consume it: delete the record + // and use its id as the reqid so the app can correlate the acceptance with the + // earlier device_link_request callback. + auto reqid = c.prepared_maybe_get( + "DELETE FROM device_link_requests WHERE device = ?" + " RETURNING id", + *dev_id) + .value_or(0); + if (auto& f = cb().device_added) + post_commit.push_back([&f, &inf = info, reqid] { + f(static_cast(reqid), inf); + }); } } } diff --git a/src/core/link_sas.cpp b/src/core/link_sas.cpp index 52a396c4e..c2a771979 100644 --- a/src/core/link_sas.cpp +++ b/src/core/link_sas.cpp @@ -9,13 +9,13 @@ using namespace session::literals; namespace session::core { -std::array link_request_sas(std::span plaintext) { +std::array derive_sas_seed(std::span plaintext) { std::array salt; hash::blake2b_pers(salt, "SessionLinkEmoji"_b2b_pers, plaintext); - std::array seed; + std::array seed; if (0 != crypto_pwhash( - seed.data(), + reinterpret_cast(seed.data()), seed.size(), reinterpret_cast(plaintext.data()), plaintext.size(), @@ -23,8 +23,12 @@ std::array link_request_sas(std::span pla /*opslimit=*/2, /*memlimit=*/16ULL * 1024 * 1024, crypto_pwhash_ALG_ARGON2ID13)) - throw std::runtime_error{"link_request_sas: Argon2id key derivation failed"}; + throw std::runtime_error{"derive_sas_seed: Argon2id key derivation failed"}; + + return seed; +} +std::array sas_from_seed(std::span seed) { // Interpret the 16-byte seed as a 128-bit little-endian integer split into two 64-bit words. uint64_t lo = oxenc::load_little_to_host(seed.data()); uint64_t hi = oxenc::load_little_to_host(seed.data() + 8); @@ -46,4 +50,8 @@ std::array link_request_sas(std::span pla return result; } +std::array link_request_sas(std::span plaintext) { + return sas_from_seed(derive_sas_seed(plaintext)); +} + } // namespace session::core diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index 1e5a30fcc..cbf4aa154 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -28,6 +28,19 @@ CREATE TABLE device_unknown ( PRIMARY KEY(device, key) ) STRICT; +-- This table tracks pending incoming device link requests from other devices that have been +-- received but not yet accepted, ignored, or denied. Device info for the requesting device is +-- stored in the devices table (with state=Pending); this table holds the link-request-specific +-- fields: when the request was received locally, and the precomputed Argon2id seed from which +-- the short authentication string emoji are derived (stored to avoid re-running the expensive +-- hash on every display). +CREATE TABLE device_link_requests ( + id INTEGER PRIMARY KEY NOT NULL, + device INTEGER UNIQUE NOT NULL REFERENCES devices(id) ON DELETE CASCADE, + received_at INTEGER NOT NULL, -- unix timestamp of when this request was stored locally + sas_seed BLOB NOT NULL CHECK(length(sas_seed) == 16) -- 16-byte Argon2id output for SAS display +) STRICT; + -- This table holds current and recent device private keys for *this* device, including the -- timestamp then the device keypairs were created, and when they were rotated away from. CREATE TABLE device_privkeys ( From b2d37e26dc5794b5c9cbd780ed5b9943ccc86a94 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Mar 2026 17:30:33 -0300 Subject: [PATCH 021/193] Merge link requests and devices into same namespace --- include/session/config/namespaces.h | 3 +- include/session/config/namespaces.hpp | 3 +- include/session/core/devices.hpp | 15 +- src/core.cpp | 3 +- src/core/devices.cpp | 332 ++++++++++++++++++++------ src/core/schema/000_devices.sql | 1 + 6 files changed, 270 insertions(+), 87 deletions(-) diff --git a/include/session/config/namespaces.h b/include/session/config/namespaces.h index 47220525f..f9a5ae1cb 100644 --- a/include/session/config/namespaces.h +++ b/include/session/config/namespaces.h @@ -25,8 +25,7 @@ typedef enum NAMESPACE { NAMESPACE_GROUP_MEMBERS = 14, // Device group namespaces: - NAMESPACE_DEVICE_GROUP = 21, - NAMESPACE_DEVICE_LINK = 22, + NAMESPACE_DEVICES = 21, NAMESPACE_DEVICE_PUBKEYS = -21, // The local config should never be pushed but this gives us a nice identifier for each config diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 16f23b79c..62e5cbe48 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -25,8 +25,7 @@ enum class Namespace : std::int16_t { GroupMembers = NAMESPACE_GROUP_MEMBERS, // Device group namespaces: - DeviceGroup = NAMESPACE_DEVICE_GROUP, - DeviceLink = NAMESPACE_DEVICE_LINK, + Devices = NAMESPACE_DEVICES, DevicePubkeys = NAMESPACE_DEVICE_PUBKEYS, // The local config should never be pushed but this gives us a nice identifier for each config diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index b8c091dc6..911343a58 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -127,19 +127,20 @@ class Devices final : detail::CoreComponent { // Encrypts the inner device data for all the members of the device group. std::vector encrypt_device_data(const device::map& devices); - // Processes a single incoming encrypted device group message. + // Processes a single incoming device group ("D") or link request ("L") message. `data` is the + // full raw message bytes including the outer bt-dict wrapper with the "" type key. void receive_device_group_message(std::span data); + void receive_link_request(std::span data); // Handlers for incoming swarm messages by namespace, called from Core::receive_messages. - void parse_device_group( - std::span> messages, bool is_final); - void parse_link_request( + void parse_device_messages( std::span> messages, bool is_final); void parse_device_pubkeys( std::span> messages, bool is_final); // Inverse of encrypt_device_data. Throws if invalid. Throws `Devices::decryption_failed` if // the message was parsed successfully, but we failed to decrypt any of its encrypted values. + // `data` should be positioned just past the "" type key (i.e. starting at "A"). device::map decrypt_device_data(std::span data); public: @@ -159,14 +160,14 @@ class Devices final : detail::CoreComponent { std::pair device_info(); struct LinkRequestResult { - std::vector message; // encrypted bytes to push to Namespace::DeviceLink + std::vector message; // encrypted bytes to push to Namespace::Devices std::array sas; // emoji SAS sequence for user display }; - // Builds an outgoing link request message to upload to Namespace::DeviceLink. This should + // Builds an outgoing link request message to upload to Namespace::Devices. This should // only be called when this device is not currently registered in the device group; throws // std::logic_error if it is already registered. The returned message is to be pushed to - // Namespace::DeviceLink with a 10-minute TTL. The sas field contains the short authentication + // Namespace::Devices with a 10-minute TTL. The sas field contains the short authentication // string that should be displayed to the user for verification against the accepting device. LinkRequestResult build_link_request(); diff --git a/src/core.cpp b/src/core.cpp index 140f38251..500822ff4 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -44,8 +44,7 @@ void Core::receive_messages( bool is_final) { using config::Namespace; switch (ns) { - case Namespace::DeviceGroup: devices.parse_device_group(messages, is_final); break; - case Namespace::DeviceLink: devices.parse_link_request(messages, is_final); break; + case Namespace::Devices: devices.parse_device_messages(messages, is_final); break; case Namespace::DevicePubkeys: devices.parse_device_pubkeys(messages, is_final); break; default: log::warning( diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 2d11595ef..cb8945170 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -536,6 +537,23 @@ namespace { return devices; } + // Values for the devices.processing column, set during batch message processing and cleared + // after callbacks are fired at is_final. + enum class Processing { + LinkRequest = 1, // new/updated link request received + Registered = 2, // device newly transitioned to Registered + Removed = 3, // device newly transitioned to Unregistered + }; + + constexpr std::string_view format_as(Processing p) { + switch (p) { + case Processing::LinkRequest: return "link-request"; + case Processing::Registered: return "registered"; + case Processing::Removed: return "removed"; + } + return "unknown"; + } + constexpr auto PERS_DEV_NONCE = "SessionDevDNonce"_b2b_pers; constexpr auto PERS_KEY_NONCE = "SessionDevKNonce"_b2b_pers; constexpr auto PERS_KEY_KEY = "SessionDevKeyKey"_b2b_pers; @@ -695,6 +713,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) std::vector out; out.resize( 2 // Outer "d" ... "e" delimiters + + 5 // "0:" + "1:D" (message type indicator) + 3 + bt_bytes_encoded(A.size()) // "1:A" + "32:...(A eph pk)..." + 3 + bt_bytes_encoded(ciphertext_raw.size()) // "1:C" + "NNNN:...(mlkem cts)..." + 3 + bt_bytes_encoded(enc_key_raw.size()) // "1:K" + "NNN:...(encrypted keys)..." @@ -704,6 +723,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; + o.append("", "D"); o.append("A", A); o.append("C", ciphertext_raw); o.append("K", enc_key_raw); @@ -737,89 +757,45 @@ void Devices::receive_device_group_message(std::span data) auto c = conn(); SQLite::Transaction tx{c.sql}; - // Snapshot the current state of all devices before processing updates so we can detect - // transitions and fire the appropriate callbacks afterward. - device::map old_devs; - for (auto [id, devid, state, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : - c.prepared_results< - int64_t, - sqlite::blob_guts>, - int, - int, - int64_t, - std::string, - std::string, - int64_t, - sqlite::blobn, - sqlite::blobn<32>>( - "SELECT id, unique_id, state, seqno, timestamp, device_type, description, version," - " pubkey_mlkem768, pubkey_x25519 FROM devices")) { - auto& info = old_devs[devid]; - info = fill_device_info( - devid, state, seqno, timestamp, std::move(type), std::move(desc), ver, pk_ml, pk_x); - load_device_extras(c, id, info); - } - - std::vector> post_commit; - for (const auto& [id, info] : devs) { - bool is_self = (id == self_id); - auto old_it = old_devs.find(id); - bool was_registered = - old_it != old_devs.end() && old_it->second.state == device::State::Registered; - if (info.state == device::State::Unregistered) { // Kicked device: preserve whatever existing row data we have, just update state and // kicked_timestamp. If we have no row for this device we can't do anything useful - // (we'd have no data to fill the required columns with), so skip it. + // (we'd have no data to fill the required columns with), so skip it. Set + // processing=Removed only if the device was previously Registered. assert(info.kicked); c.prepared_exec( - R"(UPDATE devices SET state = ?, kicked_timestamp = ? WHERE unique_id = ?)", + R"(UPDATE devices + SET state = ?, kicked_timestamp = ?, + processing = CASE WHEN state = ? THEN ? ELSE processing END + WHERE unique_id = ?)", static_cast(device::State::Unregistered), info.kicked->time_since_epoch().count(), + static_cast(device::State::Registered), + static_cast(Processing::Removed), id); - - if (was_registered) { - if (is_self) { - if (auto& f = cb().device_self_removed) - post_commit.push_back(f); - } else if (auto& f = cb().device_removed) { - auto& old = old_it->second; - post_commit.push_back([&f, &old] { f(old); }); - } - } continue; } - auto dev_id = upsert_device_info(c, info); + // Check state before upsert to detect a registration transition. + bool was_registered = c.prepared_maybe_get( + "SELECT state FROM devices WHERE unique_id = ?", id) + .value_or(-1) == + static_cast(device::State::Registered); + auto dev_id = upsert_device_info(c, info); if (!dev_id) continue; - if (!was_registered) { - if (is_self) { - if (auto& f = cb().device_self_added) - post_commit.push_back(f); - } else { - // If we had a pending link request for this device, consume it: delete the record - // and use its id as the reqid so the app can correlate the acceptance with the - // earlier device_link_request callback. - auto reqid = c.prepared_maybe_get( - "DELETE FROM device_link_requests WHERE device = ?" - " RETURNING id", - *dev_id) - .value_or(0); - if (auto& f = cb().device_added) - post_commit.push_back([&f, &inf = info, reqid] { - f(static_cast(reqid), inf); - }); - } - } + // Mark as newly registered only on a state transition (not for info-only updates). + if (!was_registered) + c.prepared_exec( + "UPDATE devices SET processing = ? WHERE id = ?", + static_cast(Processing::Registered), + *dev_id); } tx.commit(); - for (auto& f : post_commit) - f(); } Devices::LinkRequestResult Devices::build_link_request() { @@ -878,19 +854,34 @@ Devices::LinkRequestResult Devices::build_link_request() { auto plaintext = encode_link_request_plaintext(self_id, info); auto sas = link_request_sas(to_span(plaintext)); - std::vector out(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); - std::memcpy(out.data(), plaintext.data(), plaintext.size()); + + // Encrypt the plaintext + std::vector encrypted(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); + std::memcpy(encrypted.data(), plaintext.data(), plaintext.size()); auto seed = core.globals.account_seed(); config::encrypt_prealloced( - as_span(std::span{out}), + as_span(std::span{encrypted}), as_span(seed.buf).first(32), "link-request"); + + // Wrap in outer bt-dict: {"": "L", "L": } + std::vector out( + 2 // Outer "d" ... "e" delimiters + + 5 // "0:" + "1:L" (message type indicator) + + 3 + bt_bytes_encoded(encrypted.size()) // "1:L" + "NNN:...(encrypted blob)..." + ); + oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; + o.append("", "L"); + o.append("L", std::span{encrypted}); + assert(o.view().size() == out.size()); + return {std::move(out), sas}; } device::map Devices::decrypt_device_data(std::span enc_data) { oxenc::bt_dict_consumer in{enc_data}; + in.require(""); // skip the "" type key added by the outer wrapper auto A = in.require_span("A"); auto ciphertext_raw = in.require_span("C"); auto enc_key_raw = in.require_span("K"); @@ -1034,15 +1025,208 @@ device::map Devices::decrypt_device_data(std::span enc_data) { return decode_device_data(plaintext_devices); } -void Devices::parse_device_group( - std::span> messages, bool /*is_final*/) { - for (const auto& msg : messages) - receive_device_group_message(msg); +void Devices::receive_link_request(std::span data) { + // Parse outer bt-dict: {"": "L", "L": } + oxenc::bt_dict_consumer outer{data}; + outer.require(""); // skip type indicator + auto encrypted = outer.require_span("L"); + + // Decrypt using the account seed + std::vector plaintext; + try { + auto seed = core.globals.account_seed(); + plaintext = config::decrypt( + encrypted, as_span(seed.buf).first(32), "link-request"); + } catch (const config::decrypt_error& e) { + log::warning(cat, "Ignoring incoming link request: decryption failed: {}", e.what()); + return; + } + + // Parse plaintext: {"I": <32-byte device id>, "i": {device info dict}} + device::Info info; + try { + oxenc::bt_dict_consumer pt{std::span{plaintext}}; + auto in_id = pt.require_span("I"); + std::memcpy(info.id.data(), in_id.data(), info.id.size()); + + // Skip any unknown keys between "I" and "i" + oxenc::bt_dict extra_outer; + while (!pt.is_finished() && std::string_view{pt.key()} < "i") + consume_extra(pt, extra_outer); + if (pt.is_finished() || std::string_view{pt.key()} != "i") + throw std::runtime_error{"missing 'i' device info dict"}; + decode_one(info, pt.consume_dict_consumer(), device::State::Pending); + } catch (const std::exception& e) { + log::warning(cat, "Ignoring incoming link request: failed to parse: {}", e.what()); + return; + } + + auto c = conn(); + + // Reject if already registered or unregistered; only Pending (or absent) is valid + auto existing_state = + c.prepared_maybe_get("SELECT state FROM devices WHERE unique_id = ?", info.id) + .value_or(-1); + if (existing_state != -1 && existing_state != static_cast(device::State::Pending)) { + log::debug( + cat, + "Ignoring link request from {}: device already in state {}", + oxenc::to_hex(info.id), + existing_state); + return; + } + + SQLite::Transaction tx{c.sql}; + + auto dev_id = upsert_device_info(c, info); + if (!dev_id) { + log::debug( + cat, + "Ignoring link request from {}: rejected by seqno guard", + oxenc::to_hex(info.id)); + return; + } + + auto sas_seed = derive_sas_seed(as_span(std::span{plaintext})); + + c.prepared_exec( + R"(INSERT INTO device_link_requests (device, received_at, sas_seed) + VALUES (?, ?, ?) + ON CONFLICT(device) DO UPDATE SET + received_at = excluded.received_at, + sas_seed = excluded.sas_seed)", + *dev_id, + epoch_seconds(sysclock_now_s()), + sas_seed); + + // Set processing=LinkRequest only if not already set to a higher-priority value by a + // concurrent device group message in the same batch + c.prepared_exec( + "UPDATE devices SET processing = ? WHERE id = ? AND processing IS NULL", + static_cast(Processing::LinkRequest), + *dev_id); + + tx.commit(); } -void Devices::parse_link_request( - std::span> /*messages*/, bool /*is_final*/) { - log::debug(cat, "parse_link_request: not yet implemented"); +void Devices::parse_device_messages( + std::span> messages, bool is_final) { + for (const auto& msg : messages) { + try { + oxenc::bt_dict_consumer in{msg}; + auto type = in.require(""); + if (type == "D") + receive_device_group_message(msg); + else if (type == "L") + receive_link_request(msg); + else + log::warning(cat, "Ignoring device message with unknown type '{}'", type); + } catch (const std::exception& e) { + log::warning(cat, "Ignoring malformed device message: {}", e.what()); + } + } + + if (!is_final) + return; + + // Fire deferred callbacks for all devices with a pending processing state. We collect first + // to avoid nested statement conflicts during callback + processing-clear operations. + struct ProcessingItem { + int64_t row_id; + std::array id; + Processing processing; + device::Info info; + }; + + auto c = conn(); + std::vector items; + for (auto [row_id, raw_id, processing_int, state_int, seqno, timestamp, dtype, desc, ver, + pk_ml, pk_x, kicked_ts] : + c.prepared_results< + int64_t, + sqlite::blob_guts>, + int, + int, + int, + int64_t, + std::string, + std::string, + int64_t, + sqlite::blobn, + sqlite::blobn<32>, + std::optional>( + "SELECT id, unique_id, processing, state, seqno, timestamp, device_type," + " description, version, pubkey_mlkem768, pubkey_x25519, kicked_timestamp" + " FROM devices WHERE processing IS NOT NULL ORDER BY unique_id")) { + auto& item = items.emplace_back(); + item.row_id = row_id; + item.id = raw_id; + item.processing = static_cast(processing_int); + item.info = fill_device_info( + raw_id, state_int, seqno, timestamp, + std::move(dtype), std::move(desc), ver, pk_ml, pk_x); + if (kicked_ts) + item.info.kicked.emplace(std::chrono::seconds{*kicked_ts}); + load_device_extras(c, row_id, item.info); + } + + for (const auto& item : items) { + bool is_self = (item.id == self_id); + try { + switch (item.processing) { + case Processing::LinkRequest: + if (auto& f = cb().device_link_request) { + auto [lr_id, sas_seed] = c.prepared_get< + int64_t, sqlite::blob_guts>>( + "SELECT id, sas_seed FROM device_link_requests WHERE device = ?", + item.row_id); + f(static_cast(lr_id), item.info, sas_from_seed(sas_seed)); + } + break; + case Processing::Registered: + if (is_self) { + if (auto& f = cb().device_self_added) + f(); + } else { + if (auto& f = cb().device_added) { + auto reqid = + c.prepared_maybe_get( + "SELECT id FROM device_link_requests WHERE device = ?", + item.row_id) + .value_or(0LL); + f(static_cast(reqid), item.info); + } + // Clean up any link request row (whether callback was set or not) + c.prepared_exec( + "DELETE FROM device_link_requests WHERE device = ?", item.row_id); + } + break; + case Processing::Removed: + if (is_self) { + if (auto& f = cb().device_self_removed) + f(); + } else { + if (auto& f = cb().device_removed) + f(item.info); + } + break; + } + c.prepared_exec("UPDATE devices SET processing = NULL WHERE id = ?", item.row_id); + } catch (const std::exception& e) { + log::warning( + cat, + "Exception in {} device callback for device {}: {}", + item.processing, + oxenc::to_hex(item.id), + e.what()); + // Don't clear processing so the callback will be retried + } + } + + // Prune stale link requests (older than 10 minutes) + c.prepared_exec( + "DELETE FROM device_link_requests WHERE received_at < ?", + epoch_seconds(sysclock_now_s() - std::chrono::seconds{600})); } void Devices::parse_device_pubkeys( diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index cbf4aa154..4e6d44f41 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -6,6 +6,7 @@ CREATE TABLE devices ( state INTEGER NOT NULL CHECK(state == 0 OR state == 1 OR state == 2), -- registered, pending, unregistered changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unconfirmed local device info changes + processing INTEGER, -- non-null during batch processing: 1=new link request, 2=newly registered, 3=newly removed seqno INTEGER NOT NULL DEFAULT 1, timestamp INTEGER NOT NULL, kicked_timestamp INTEGER, -- set when the device was kicked from the device group From b96b20719120021ea025db0484c2a4b0eeae0149 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Mar 2026 18:10:59 -0300 Subject: [PATCH 022/193] Move device list to subkey of device group message Make way for account keys to be in here as well. --- include/session/config/namespaces.h | 2 +- include/session/config/namespaces.hpp | 2 +- include/session/core/devices.hpp | 2 +- src/core.cpp | 2 +- src/core/devices.cpp | 43 +++++++++++++-------------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/include/session/config/namespaces.h b/include/session/config/namespaces.h index f9a5ae1cb..f0e2b4903 100644 --- a/include/session/config/namespaces.h +++ b/include/session/config/namespaces.h @@ -26,7 +26,7 @@ typedef enum NAMESPACE { // Device group namespaces: NAMESPACE_DEVICES = 21, - NAMESPACE_DEVICE_PUBKEYS = -21, + NAMESPACE_ACCOUNT_PUBKEYS = -21, // The local config should never be pushed but this gives us a nice identifier for each config // type diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index 62e5cbe48..89de2eec2 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -26,7 +26,7 @@ enum class Namespace : std::int16_t { // Device group namespaces: Devices = NAMESPACE_DEVICES, - DevicePubkeys = NAMESPACE_DEVICE_PUBKEYS, + AccountPubkeys = NAMESPACE_ACCOUNT_PUBKEYS, // The local config should never be pushed but this gives us a nice identifier for each config // type diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 911343a58..327788d5a 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -135,7 +135,7 @@ class Devices final : detail::CoreComponent { // Handlers for incoming swarm messages by namespace, called from Core::receive_messages. void parse_device_messages( std::span> messages, bool is_final); - void parse_device_pubkeys( + void parse_account_pubkeys( std::span> messages, bool is_final); // Inverse of encrypt_device_data. Throws if invalid. Throws `Devices::decryption_failed` if diff --git a/src/core.cpp b/src/core.cpp index 500822ff4..66a21635c 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -45,7 +45,7 @@ void Core::receive_messages( using config::Namespace; switch (ns) { case Namespace::Devices: devices.parse_device_messages(messages, is_final); break; - case Namespace::DevicePubkeys: devices.parse_device_pubkeys(messages, is_final); break; + case Namespace::AccountPubkeys: devices.parse_account_pubkeys(messages, is_final); break; default: log::warning( cat, diff --git a/src/core/devices.cpp b/src/core/devices.cpp index cb8945170..0a3e971b6 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -385,6 +385,7 @@ namespace { std::string encode_device_data(const device::map& devices) { oxenc::bt_dict_producer out; + auto devs = out.append_dict("D"); for (const auto& [id, info] : devices) { std::string_view id_sv{reinterpret_cast(id.data()), id.size()}; @@ -399,7 +400,7 @@ namespace { // // TODO: we should prune devices that were kicked a long time ago. if (info.kicked) - out.append(id_sv, info.kicked->time_since_epoch().count()); + devs.append(id_sv, info.kicked->time_since_epoch().count()); else log::debug( cat, @@ -408,7 +409,7 @@ namespace { continue; } - encode_device_info(out.append_dict(id_sv), info); + encode_device_info(devs.append_dict(id_sv), info); } return std::move(out).str(); @@ -499,28 +500,26 @@ namespace { device::map devices; oxenc::bt_dict_consumer in{data}; - while (!in.is_finished()) { - auto in_id = in.key(); - // A 32-byte keys are device IDs, but we allow (and ignore) keys with other sizes to - // allow for future expansion - if (in_id.size() != 32) { - log::debug( - cat, - "Skipping unknown {}-length key: not a 32-byte device id", - in_id.size()); - continue; - } + auto devs = in.require("D"); + // Unknown top-level keys alongside "D" are ignored for forward compatibility. + + while (!devs.is_finished()) { + auto in_id = devs.key(); + if (in_id.size() != 32) + throw std::runtime_error{ + "Invalid encoded device data: unexpected {}-byte key in device dict (expected 32)"_format( + in_id.size())}; std::array id; std::memcpy(id.data(), in_id.data(), 32); auto [it, ins] = devices.try_emplace(id); if (!ins) - throw std::runtime_error{"Invalid encoded device data: duplicate devices ids"}; + throw std::runtime_error{"Invalid encoded device data: duplicate device ids"}; auto& info = it->second; info.id = id; - if (in.is_integer()) { + if (devs.is_integer()) { // An integer indicates a "device removed" timestamp, used to distinguish between // "device removed" and "I don't know about the device yet". It gets pruned when // updating once it hits a certain age threshold. @@ -528,9 +527,9 @@ namespace { // If the device wants to get re-added to the group then it must generate a new // device id. info.state = device::State::Unregistered; - info.kicked.emplace(std::chrono::seconds{in.consume_integer()}); + info.kicked.emplace(std::chrono::seconds{devs.consume_integer()}); } else { - decode_one(info, in.consume_dict_consumer(), device::State::Registered); + decode_one(info, devs.consume_dict_consumer(), device::State::Registered); } } @@ -713,7 +712,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) std::vector out; out.resize( 2 // Outer "d" ... "e" delimiters - + 5 // "0:" + "1:D" (message type indicator) + + 5 // "0:" + "1:G" (message type indicator) + 3 + bt_bytes_encoded(A.size()) // "1:A" + "32:...(A eph pk)..." + 3 + bt_bytes_encoded(ciphertext_raw.size()) // "1:C" + "NNNN:...(mlkem cts)..." + 3 + bt_bytes_encoded(enc_key_raw.size()) // "1:K" + "NNN:...(encrypted keys)..." @@ -723,7 +722,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; - o.append("", "D"); + o.append("", "G"); o.append("A", A); o.append("C", ciphertext_raw); o.append("K", enc_key_raw); @@ -1115,7 +1114,7 @@ void Devices::parse_device_messages( try { oxenc::bt_dict_consumer in{msg}; auto type = in.require(""); - if (type == "D") + if (type == "G") receive_device_group_message(msg); else if (type == "L") receive_link_request(msg); @@ -1229,9 +1228,9 @@ void Devices::parse_device_messages( epoch_seconds(sysclock_now_s() - std::chrono::seconds{600})); } -void Devices::parse_device_pubkeys( +void Devices::parse_account_pubkeys( std::span> /*messages*/, bool /*is_final*/) { - log::debug(cat, "parse_device_pubkeys: not yet implemented"); + log::debug(cat, "parse_account_pubkeys: not yet implemented"); } } // namespace session::core From b1e9babd67e870d1ae9666483543077aaf06f58b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 16 Mar 2026 19:40:11 -0300 Subject: [PATCH 023/193] WIP on device and account messages --- external/session-sqlite | 2 +- include/session/core/devices.hpp | 44 ++++- include/session/hash.hpp | 17 +- src/core/devices.cpp | 315 +++++++++++++++++++++++++------ src/core/schema/000_devices.sql | 8 +- 5 files changed, 312 insertions(+), 74 deletions(-) diff --git a/external/session-sqlite b/external/session-sqlite index 75746b6ff..e3105baec 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit 75746b6ffa1ca0b6f8365e7cf799616e5d2edf2b +Subproject commit e3105baec20ca7a2c8f5e9ad50817e6a24ee2bf4 diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 327788d5a..c48fa9139 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -18,6 +18,8 @@ namespace session::core { +using namespace std::literals; + class Core; namespace device { @@ -138,10 +140,12 @@ class Devices final : detail::CoreComponent { void parse_account_pubkeys( std::span> messages, bool is_final); - // Inverse of encrypt_device_data. Throws if invalid. Throws `Devices::decryption_failed` if - // the message was parsed successfully, but we failed to decrypt any of its encrypted values. - // `data` should be positioned just past the "" type key (i.e. starting at "A"). - device::map decrypt_device_data(std::span data); + // Decrypts an incoming encrypted device group ("G") message, returning the bt-encoded group + // payload plaintext (a bt-dict containing at minimum a "D" devices subdict and optionally a + // "K" account keys list). Throws if parsing or decryption fails. Throws + // `device::decryption_failed` if we could not find a key that successfully decrypts the data + // (i.e. we are not in the device group, or all our keys have rotated past this message). + std::vector decrypt_device_data(std::span data); public: // Returns the current device's random identifier, in hex. @@ -211,10 +215,25 @@ class Devices final : detail::CoreComponent { std::optional rotated; }; + // How long after rotation to keep an old account key. 14 days is the maximum 1-to-1 message + // TTL, plus 24h for sender key update lag, plus 24h safety margin. + static constexpr auto ACCOUNT_KEY_RETENTION = 16 * 24h; + + // Base rotation period and jitter window for account key rotation. The formula is designed so + // that the minimum rotation time across all N devices in the group is Unif[PERIOD-WINDOW/2, + // PERIOD+WINDOW/2], regardless of N, masking the number of devices in the group. + static constexpr auto ACCOUNT_KEY_ROTATION_PERIOD = 12h; + static constexpr auto ACCOUNT_KEY_ROTATION_WINDOW = 2h; + + // Rotates the shared account keys used for PFS+PQ message encryption. Generates a new random + // seed, stores it in the database, marks the previous active key as rotated, and prunes keys + // older than ACCOUNT_KEY_RETENTION. Should be called when account_rotation_due() is true and + // when a device first joins the device group with no existing account keys. + void rotate_account_keys(); + // Returns the current active account keys after pruning obsolete ones: that is, the current key - // plus all keys that were rotated away fewer than 16 days ago. (16 days = 14 day max incoming - // message TTL + 24h sender key update lag + 24h safety margin). Keys are returned sorted from - // newest to oldest. + // plus all keys that were rotated away fewer than ACCOUNT_KEY_RETENTION ago. Keys are returned + // sorted from newest to oldest. If there are no keys at all, generates an initial one. std::vector active_account_keys(); // Returns the time when this device's unique device key is due to be rotated. Returns nullopt @@ -223,7 +242,7 @@ class Devices final : detail::CoreComponent { bool device_rotation_due() { auto t = next_device_rotation(); - return t && *t >= std::chrono::system_clock::now(); + return t && *t <= std::chrono::system_clock::now(); } // Return true if the account key is due to be rotated by this device. Returns nullopt if this @@ -232,8 +251,15 @@ class Devices final : detail::CoreComponent { bool account_rotation_due() { auto t = next_account_rotation(); - return t && *t >= std::chrono::system_clock::now(); + return t && *t <= std::chrono::system_clock::now(); } + + // Builds the signed account public key message for upload to namespace -21. The message is a + // bt-encoded dict containing the current active ML-KEM-768 pubkey ("M"), X25519 pubkey ("X"), + // and a "positive alternative" Ed25519 signature ("~") over the preceding fields, allowing + // recipients who only know the account's Session ID (X25519) to verify the keys. + // Throws if there are no active account keys. + std::vector build_account_pubkey_message(); }; } // namespace session::core diff --git a/include/session/hash.hpp b/include/session/hash.hpp index c65b8d2e9..7215a2d88 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -119,9 +119,15 @@ template void blake2b_key(Out& out, const Key& key, const T&... args) { crypto_generichash_blake2b_state st; crypto_generichash_blake2b_init( - &st, std::ranges::data(key), std::ranges::size(key), std::ranges::size(out)); + &st, + reinterpret_cast(std::ranges::data(key)), + std::ranges::size(key), + std::ranges::size(out)); update_all(st, args...); - crypto_generichash_blake2b_final(&st, std::ranges::data(out), std::ranges::size(out)); + crypto_generichash_blake2b_final( + &st, + reinterpret_cast(std::ranges::data(out)), + std::ranges::size(out)); } /// API: hash/blake2b @@ -158,13 +164,16 @@ void blake2b_key_pers( crypto_generichash_blake2b_state st; crypto_generichash_blake2b_init_salt_personal( &st, - std::ranges::data(key), + reinterpret_cast(std::ranges::data(key)), std::ranges::size(key), std::ranges::size(out), /*salt=*/nullptr, pers.data()); update_all(st, args...); - crypto_generichash_blake2b_final(&st, std::ranges::data(out), std::ranges::size(out)); + crypto_generichash_blake2b_final( + &st, + reinterpret_cast(std::ranges::data(out)), + std::ranges::size(out)); } /// API: hash/blake2b_pers diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 0a3e971b6..4477f8bc8 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -24,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +90,30 @@ static Keys keys_from_seed(std::span seed) { return keys; } +// Lightweight formattable wrapper for logging a brief "aabb…xxyy" hex summary of a key. The +// hex computation is deferred to when the formatter is invoked, so it is skipped entirely if +// the log level is disabled. +struct key_summary { + std::span key; + + template + requires oxenc::basic_char> + key_summary(const T& k) : key{std::as_bytes(std::span{k})} {} +}; + +std::string format_as(const key_summary& ks) { + return "{}…{}"_format( + oxenc::to_hex(ks.key.begin(), ks.key.begin() + 2), + oxenc::to_hex(ks.key.end() - 2, ks.key.end())); +} + +// format_as for XWingKeys-derived types (DeviceKeys, AccountKeys), defined in session::core so +// that fmtlib's ADL-based lookup can find it when logging these types. +template Keys> +std::string format_as(const Keys& k) { + return "X25519[{}], MLKEM768[{}]"_format(key_summary{k.x25519_pub}, key_summary{k.mlkem768_pub}); +} + Devices::DeviceKeys Devices::rotate_device_keys() { // We store just one single seed value, then use TurboSHAKE256 to expand it into separate X25519 // (32B) and MLKEM-768 (64B) seeds. @@ -102,17 +128,28 @@ Devices::DeviceKeys Devices::rotate_device_keys() { std::chrono::system_clock::now().time_since_epoch()); c.prepared_exec("INSERT INTO device_privkeys (created, seed) VALUES (?, ?)", now.count(), seed); - log::info( - cat, - "New rotating device keys generated: X25519[{}…{}], MLKEM768[{}…{}]", - oxenc::to_hex(keys.x25519_pub.begin(), keys.x25519_pub.begin() + 2), - oxenc::to_hex(keys.x25519_pub.end() - 2, keys.x25519_pub.end()), - oxenc::to_hex(keys.mlkem768_pub.begin(), keys.mlkem768_pub.begin() + 2), - oxenc::to_hex(keys.mlkem768_pub.end() - 2, keys.mlkem768_pub.end())); + log::info(cat, "New rotating device keys generated: {}", keys); return keys; } +void Devices::rotate_account_keys() { + cleared_b32 seed; + random::fill(seed); + auto keys = keys_from_seed(seed); + + auto c = conn(); + c.prepared_exec( + "INSERT INTO device_account_keys (created, seed, pubkey_mlkem768, pubkey_x25519)" + " VALUES (?, ?, ?, ?)", + epoch_seconds(sysclock_now_s()), + seed, + keys.mlkem768_pub, + keys.x25519_pub); + + log::info(cat, "New account keys generated: {}", keys); +} + std::vector Devices::active_device_keys() { std::vector keys; auto c = conn(); @@ -135,15 +172,15 @@ std::vector Devices::active_device_keys() { } std::vector Devices::active_account_keys() { - std::vector keys; - - auto rotation_cutoff = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - - 16 * 24h; - auto c = conn(); SQLite::Transaction tx{c.sql}; - c.prepared_exec("DELETE FROM device_group_keys WHERE rotated < ?", rotation_cutoff.count()); + + c.prepared_exec( + "DELETE FROM device_account_keys WHERE rotated < ?", + epoch_seconds(sysclock_now_s() - ACCOUNT_KEY_RETENTION)); + + std::vector keys; + bool have_active = false; for (auto [id, created, rotated, seed, pk_ml, pk_x] : c.prepared_results< int64_t, @@ -151,20 +188,35 @@ std::vector Devices::active_account_keys() { std::optional, sqlite::blobn<32>, sqlite::blobn, - sqlite::blobn<32>>("SELECT created, rotated, seed, pubkey_mlkem768, pubkey_x25519" - " ORDER BY rotated DESC NULLS FIRST, created DESC")) { - keys.push_back(keys_from_seed(seed)); - if (0 != std::memcmp(keys.back().mlkem768_pub.data(), pk_ml.data(), pk_ml.size()) || - 0 != std::memcmp(keys.back().x25519_pub.data(), pk_x.data(), pk_x.size())) { + sqlite::blobn<32>>( + "SELECT id, created, rotated, seed, pubkey_mlkem768, pubkey_x25519" + " FROM device_account_keys" + " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + auto& k = keys.emplace_back(keys_from_seed(seed)); + k.created = std::chrono::sys_seconds{std::chrono::seconds{created}}; + if (rotated) + k.rotated.emplace(std::chrono::seconds{*rotated}); + if (!rotated) + have_active = true; + if (0 != std::memcmp(k.mlkem768_pub.data(), pk_ml.data(), pk_ml.size()) || + 0 != std::memcmp(k.x25519_pub.data(), pk_x.data(), pk_x.size())) { log::warning( cat, - "device_group_keys row with id={} ignored: row contains invalid precomputed " + "device_account_keys row with id={} ignored: row contains invalid precomputed " "pubkeys", id); keys.pop_back(); } } + tx.commit(); + + if (!have_active) { + log::info(cat, "No currently active account keys; generating a new one"); + rotate_account_keys(); + return active_account_keys(); + } + return keys; } @@ -331,6 +383,19 @@ std::pair Devices::device_info() { namespace { + // Plain-old-data representation of a single account key seed entry as read from or written to + // the "K" list in the device group plaintext payload. + struct AccountKeySeed { + cleared_b32 seed; + int64_t created; + std::optional rotated; + }; + + struct GroupPayload { + device::map devices; + std::vector account_keys; + }; + // Called while building a bt dict to pull out any unknown intermediate keys immediately before // appending a new one. E.g. call `write_extra(out, "a", it, end)` to write out any keys from // `it` that precede "a". `it` is mutated, and left at the first value > "a", ready for the @@ -383,33 +448,53 @@ namespace { devout.append_bt(xit->first, xit->second); } - std::string encode_device_data(const device::map& devices) { + std::string encode_group_payload( + const device::map& devices, + std::span acc_keys) { oxenc::bt_dict_producer out; - auto devs = out.append_dict("D"); - for (const auto& [id, info] : devices) { - std::string_view id_sv{reinterpret_cast(id.data()), id.size()}; + { + auto devs = out.append_dict("D"); + for (const auto& [id, info] : devices) { - if (info.state == device::State::Pending) { - log::debug( - cat, "Skipping pending device {} in device group data", oxenc::to_hex(id)); - continue; - } else if (info.state == device::State::Unregistered) { - // We write a timestamp tombstone value for a kicked device, with the kick timestamp - // as the value. - // - // TODO: we should prune devices that were kicked a long time ago. - if (info.kicked) - devs.append(id_sv, info.kicked->time_since_epoch().count()); - else + std::string_view id_sv{reinterpret_cast(id.data()), id.size()}; + + if (info.state == device::State::Pending) { log::debug( cat, - "Skipping unregistered (but not kicked) device {}", + "Skipping pending device {} in device group data", oxenc::to_hex(id)); - continue; + continue; + } else if (info.state == device::State::Unregistered) { + // We write a timestamp tombstone value for a kicked device, with the kick + // timestamp as the value. + // + // TODO: we should prune devices that were kicked a long time ago. + if (info.kicked) + devs.append(id_sv, info.kicked->time_since_epoch().count()); + else + log::debug( + cat, + "Skipping unregistered (but not kicked) device {}", + oxenc::to_hex(id)); + continue; + } + + encode_device_info(devs.append_dict(id_sv), info); + } + } // "D" dict closed here + + if (!acc_keys.empty()) { + auto kl = out.append_list("K"); + for (const auto& k : acc_keys) { + auto e = kl.append_dict(); + e.append("c", k.created); + if (k.rotated) + e.append("r", *k.rotated); + e.append("s", + std::string_view{ + reinterpret_cast(k.seed.data()), k.seed.size()}); } - - encode_device_info(devs.append_dict(id_sv), info); } return std::move(out).str(); @@ -493,15 +578,14 @@ namespace { consume_extra(dev, info.extra); } - // Decodes an incoming devices data message. The return map will include both full device - // records, and deleted devices: deleted devices will have a mostly default-constructed Info - // object where only id, state (=State::Unregistered) and kicked (=removal timestamp) are set. - device::map decode_device_data(std::span data) { - device::map devices; + // Decodes the plaintext bt-encoded device group payload. The returned device map will include + // both full device records and tombstoned devices: the latter have a mostly default-constructed + // Info where only id, state (=State::Unregistered), and kicked (=removal timestamp) are set. + GroupPayload decode_group_payload(std::span data) { + GroupPayload result; oxenc::bt_dict_consumer in{data}; auto devs = in.require("D"); - // Unknown top-level keys alongside "D" are ignored for forward compatibility. while (!devs.is_finished()) { auto in_id = devs.key(); @@ -512,7 +596,7 @@ namespace { std::array id; std::memcpy(id.data(), in_id.data(), 32); - auto [it, ins] = devices.try_emplace(id); + auto [it, ins] = result.devices.try_emplace(id); if (!ins) throw std::runtime_error{"Invalid encoded device data: duplicate device ids"}; @@ -533,7 +617,17 @@ namespace { } } - return devices; + auto kl = in.require("K"); + while (!kl.is_finished()) { + auto& k = result.account_keys.emplace_back(); + auto e = kl.consume_dict_consumer(); + k.created = e.require("c"); + k.rotated = e.maybe("r"); + auto s = e.require_span("s"); + std::memcpy(k.seed.data(), s.data(), 32); + } + + return result; } // Values for the devices.processing column, set during batch message processing and cleared @@ -557,6 +651,7 @@ namespace { constexpr auto PERS_KEY_NONCE = "SessionDevKNonce"_b2b_pers; constexpr auto PERS_KEY_KEY = "SessionDevKeyKey"_b2b_pers; constexpr auto PERS_KEY_KEY_IND = "SessionDevKeyInd"_b2b_pers; + constexpr auto PERS_ACC_KEY_ROT = "SessionAccKeyRot"_b2b_pers; constexpr int bt_bytes_encoded(int x) { int sz = 1 + x; @@ -651,7 +746,19 @@ std::vector Devices::encrypt_device_data(const device::map& devices) cleared_uc32 key_base; random::fill(key_base); - auto plaintext_devices = encode_device_data(devices); + // Fetch account key seeds for inclusion in the payload. + std::vector acc_keys; + for (auto [seed, created, rotated] : + conn().prepared_results, int64_t, std::optional>( + "SELECT seed, created, rotated FROM device_account_keys" + " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + auto& k = acc_keys.emplace_back(); + std::memcpy(k.seed.data(), seed.data(), 32); + k.created = created; + k.rotated = rotated; + } + + auto plaintext_devices = encode_group_payload(devices, acc_keys); std::vector enc_devices; enc_devices.resize(plaintext_devices.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); crypto_aead_xchacha20poly1305_ietf_encrypt( @@ -745,9 +852,10 @@ std::vector Devices::encrypt_device_data(const device::map& devices) } void Devices::receive_device_group_message(std::span data) { - device::map devs; + GroupPayload payload; try { - devs = decrypt_device_data(std::as_bytes(data)); + auto raw = decrypt_device_data(std::as_bytes(data)); + payload = decode_group_payload(raw); } catch (const device::decryption_failed& e) { log::warning(cat, "Ignoring incoming device group message: {}", e.what()); return; @@ -756,7 +864,26 @@ void Devices::receive_device_group_message(std::span data) auto c = conn(); SQLite::Transaction tx{c.sql}; - for (const auto& [id, info] : devs) { + // Merge incoming account keys. New seeds are inserted as-is (the rotation trigger handles + // marking any existing active key as rotated when a newer active key arrives). For seeds we + // already have, we reconcile the `rotated` column: if both rotated at different times, take the + // minimum; if only one side has rotated, adopt that rotation. + for (const auto& k : payload.account_keys) { + auto keys = keys_from_seed(k.seed); + c.prepared_exec( + "INSERT INTO device_account_keys" + " (created, rotated, seed, pubkey_mlkem768, pubkey_x25519)" + " VALUES (?, ?, ?, ?, ?)" + " ON CONFLICT (seed) DO UPDATE SET" + " rotated = COALESCE(MIN(excluded.rotated, rotated), excluded.rotated, rotated)", + k.created, + k.rotated, + k.seed, + keys.mlkem768_pub, + keys.x25519_pub); + } + + for (const auto& [id, info] : payload.devices) { if (info.state == device::State::Unregistered) { // Kicked device: preserve whatever existing row data we have, just update state and // kicked_timestamp. If we have no row for this device we can't do anything useful @@ -877,7 +1004,7 @@ Devices::LinkRequestResult Devices::build_link_request() { return {std::move(out), sas}; } -device::map Devices::decrypt_device_data(std::span enc_data) { +std::vector Devices::decrypt_device_data(std::span enc_data) { oxenc::bt_dict_consumer in{enc_data}; in.require(""); // skip the "" type key added by the outer wrapper @@ -941,7 +1068,7 @@ device::map Devices::decrypt_device_data(std::span enc_data) { cleared_uc32 ml_ss, aB, ki, key_base; - std::vector plaintext_devices; + std::vector plaintext_devices; plaintext_devices.resize(enc_devices.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); // Trial decrypt until we find one that works, except that we can skip most of the heavy @@ -992,7 +1119,7 @@ device::map Devices::decrypt_device_data(std::span enc_data) { // Now we can decrypt the encrypted payload: if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - plaintext_devices.data(), + reinterpret_cast(plaintext_devices.data()), nullptr, nullptr, reinterpret_cast(enc_devices.data()), @@ -1021,7 +1148,7 @@ device::map Devices::decrypt_device_data(std::span enc_data) { throw device::decryption_failed{"Failed to decrypt incoming device data"}; } - return decode_device_data(plaintext_devices); + return plaintext_devices; } void Devices::receive_link_request(std::span data) { @@ -1233,4 +1360,80 @@ void Devices::parse_account_pubkeys( log::debug(cat, "parse_account_pubkeys: not yet implemented"); } +std::optional Devices::next_account_rotation() { + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + if (!c.prepared_maybe_get( + "SELECT 1 FROM devices WHERE unique_id = ? AND state = ?", + self_id, static_cast(device::State::Registered))) + return std::nullopt; + + int64_t t_created = 0; + std::optional active_seed; + for (auto [created, seed] : c.prepared_results>( + "SELECT created, seed FROM device_account_keys" + " WHERE rotated IS NULL ORDER BY created DESC LIMIT 1")) { + t_created = created; + std::memcpy(active_seed.emplace().data(), seed.data(), seed.size()); + } + if (!active_seed) + return std::nullopt; + + auto N = c.prepared_get( + "SELECT count(*) FROM devices WHERE state = ?", + static_cast(device::State::Registered)); + + tx.commit(); + + // u is a per-device uniform random value in [0,1], derived deterministically from the device + // ID and current account key seed so that each device independently computes a consistent + // rotation schedule. + std::array hash_out; + hash::blake2b_key_pers(hash_out, *active_seed, PERS_ACC_KEY_ROT, self_id); + double u = oxenc::load_little_to_host(hash_out.data()) / 0x1p64; + + // With N registered devices, the minimum of their N individual offsets is uniformly + // distributed in [PERIOD - WINDOW/2, PERIOD + WINDOW/2]. + auto offset = std::chrono::duration_cast( + (ACCOUNT_KEY_ROTATION_PERIOD - ACCOUNT_KEY_ROTATION_WINDOW / 2) + + ACCOUNT_KEY_ROTATION_WINDOW * (1.0 - std::pow(u, static_cast(N)))); + + return std::chrono::sys_seconds{std::chrono::seconds{t_created}} + offset; +} + +std::optional Devices::next_device_rotation() { + // TODO: implement device key rotation scheduling + return std::nullopt; +} + +std::vector Devices::build_account_pubkey_message() { + auto keys = active_account_keys(); + if (keys.empty()) + throw std::runtime_error{"build_account_pubkey_message: no active account keys"}; + const auto& k = keys.front(); + + std::vector out( + 2 // outer dict d...e + + 3 + bt_bytes_encoded(1184) // "1:M" + mlkem768_pub + + 3 + bt_bytes_encoded(32) // "1:X" + x25519_pub + + 3 + bt_bytes_encoded(64) // "1:~" + XEd25519 signature + ); + + oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; + o.append("M", k.mlkem768_pub); + o.append("X", k.x25519_pub); + o.append_signature( + "~", [seed = core.globals.account_seed()](std::span body) { + cleared_uc32 x25519_priv; + crypto_sign_ed25519_sk_to_curve25519( + x25519_priv.data(), + reinterpret_cast(seed.buf.data())); + return xed25519::sign(x25519_priv, body); + }); + + assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above + return out; +} + } // namespace session::core diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index 4e6d44f41..abe36d762 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -57,21 +57,21 @@ CREATE TABLE device_privkeys ( CREATE TRIGGER device_privkey_rotation AFTER INSERT ON device_privkeys FOR EACH ROW WHEN NEW.rotated IS NULL BEGIN - UPDATE device_privkeys SET rotated = NEW.created WHERE rotated IS NULL; + UPDATE device_privkeys SET rotated = NEW.created WHERE rotated IS NULL AND id != NEW.id; END; -- This table holds current and recent *account* keys, which are shared within the device -- group and have their public keys published for remote users to use to encrypt messages. -- Unlike device_privkeys, these keys are shared among all devices in the device group. -CREATE TABLE device_group_keys ( +CREATE TABLE device_account_keys ( id INTEGER PRIMARY KEY NOT NULL, created INTEGER NOT NULL, rotated INTEGER, -- timestamp when a new key superceded this key - seed BLOB NOT NULL CHECK(length(seed) == 32), + seed BLOB UNIQUE NOT NULL CHECK(length(seed) == 32), pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) == 1184), pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) == 32), -- Virtual column containing the first two mlkem pubkey values to assist with lookups based on -- incoming message key indicator: key_indicator BLOB GENERATED ALWAYS AS (substr(pubkey_mlkem768, 1, 2)) VIRTUAL ) STRICT; -CREATE INDEX device_group_keys_ki_index ON device_group_keys(key_indicator); +CREATE INDEX device_account_keys_ki_index ON device_account_keys(key_indicator); From 4f4b59a3304aa5a75010dc6635dc8d8e34879cba Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 16 Mar 2026 19:59:04 -0300 Subject: [PATCH 024/193] Account key rotation tie-breaking If two devices rotate account keys at approximately the same time, both might end up with inconsistent "active" keys. This commit adds deterministic tie breaking (always prefering the later, with fallback to seed ordering). --- src/core/devices.cpp | 9 +++++---- src/core/schema/000_devices.sql | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 4477f8bc8..2f275f984 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -864,10 +864,11 @@ void Devices::receive_device_group_message(std::span data) auto c = conn(); SQLite::Transaction tx{c.sql}; - // Merge incoming account keys. New seeds are inserted as-is (the rotation trigger handles - // marking any existing active key as rotated when a newer active key arrives). For seeds we - // already have, we reconcile the `rotated` column: if both rotated at different times, take the - // minimum; if only one side has rotated, adopt that rotation. + // Merge incoming account keys. New seeds are inserted and the rotation trigger applies + // tie-breaking: latest created wins (smallest seed as tiebreaker), so concurrent rotations + // from multiple devices converge deterministically. For seeds we already have, we reconcile + // the `rotated` column: if both sides have rotated at different times, take the minimum; if + // only one side has rotated, adopt that rotation. for (const auto& k : payload.account_keys) { auto keys = keys_from_seed(k.seed); c.prepared_exec( diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index abe36d762..fb653ccb5 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -75,3 +75,19 @@ CREATE TABLE device_account_keys ( key_indicator BLOB GENERATED ALWAYS AS (substr(pubkey_mlkem768, 1, 2)) VIRTUAL ) STRICT; CREATE INDEX device_account_keys_ki_index ON device_account_keys(key_indicator); + +-- When a new account key is inserted as active (rotated IS NULL), apply deterministic +-- tie-breaking: the key with the latest created timestamp wins (ties broken by smallest seed), +-- and all unrotated losers are immediately marked as rotated at the winner's creation time. +-- This handles concurrent rotations from multiple devices: once all devices sync, the trigger +-- guarantees they all converge on the same active key regardless of insertion order. +CREATE TRIGGER device_account_key_rotation AFTER INSERT ON device_account_keys +FOR EACH ROW WHEN NEW.rotated IS NULL +BEGIN + UPDATE device_account_keys SET rotated = winner.created + FROM (SELECT id, created FROM device_account_keys + WHERE rotated IS NULL + ORDER BY created DESC, seed ASC + LIMIT 1) AS winner + WHERE device_account_keys.rotated IS NULL AND device_account_keys.id != winner.id; +END; From 8865051034180fafc6dc3986ca84c28ebce7231d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 16:55:30 -0300 Subject: [PATCH 025/193] bump session-deps for sqlite3mc namespacing patch --- cmake/session-deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/session-deps b/cmake/session-deps index e5677c8fa..d66944abf 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit e5677c8fa649043e7b985ba45459fa2e9b96bc09 +Subproject commit d66944abf027dacc07d7cb0e58aae53aaecd8b8a From 78210a498be95beb85e66e05133d97e200a9da1e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 22:38:32 -0300 Subject: [PATCH 026/193] Add network to core + fixes --- include/session/core.hpp | 13 +++++++++++++ src/CMakeLists.txt | 1 + src/core/globals.cpp | 6 +++--- src/core/schema/000_devices.sql | 2 +- tests/CMakeLists.txt | 4 ++++ tests/test_core_network.cpp | 29 +++++++++++++++++++++++++++++ 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 tests/test_core_network.cpp diff --git a/include/session/core.hpp b/include/session/core.hpp index 9562b3419..1fc583a19 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -120,6 +121,10 @@ namespace session::pro_backend { struct ProRevocationItem; }; // namespace session::pro_backend +namespace session::network { +class Network; +} + namespace session::core { namespace quic = oxen::quic; @@ -136,6 +141,8 @@ class Core { }; std::unique_ptr _loop; + std::shared_ptr _network; + sqlite::Database db; friend class detail::CoreComponent; @@ -161,6 +168,12 @@ class Core { init(); } + /// Set an optional network interface that can be used to make network requests to swarm members + void set_network(std::shared_ptr network) { _network = std::move(network); } + + /// Returns the optional network interface, if set. + const std::shared_ptr& network() const { return _network; } + // Global value storage. This are used by some components, but can also be used by the // application to persist settings. Globals globals{*this}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 68e3d8d1e..864665488 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ add_libsession_util_library(core core.cpp core/component.cpp core/devices.cpp + core/globals.cpp core/link_sas.cpp core/pro.cpp ) diff --git a/src/core/globals.cpp b/src/core/globals.cpp index df0fdf5b6..9b97e216e 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -31,7 +31,7 @@ std::optional> Globals::get_blob(std::string_view key) { auto c = conn(); auto st = c.prepared_bind(GET_ONE, key, "blob"); if (st->executeStep()) { - auto data = get(st); + auto data = sqlite::get(st); result.emplace().reserve(data.size()); result->assign(data.begin(), data.end()); } @@ -42,14 +42,14 @@ std::optional Globals::get_blob_secure(std::string_view auto c = conn(); auto st = c.prepared_bind(GET_ONE, key, "blob"); if (st->executeStep()) - result.emplace(get(st)); + result.emplace(sqlite::get(st)); return result; } bool Globals::get_blob_to(std::string_view key, std::span to) { auto c = conn(); auto st = c.prepared_bind(GET_ONE, key, "blob"); if (st->executeStep()) { - if (auto data = get(st); data.size() == to.size()) { + if (auto data = sqlite::get(st); data.size() == to.size()) { std::memcpy(to.data(), data.data(), to.size()); return true; } diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index fb653ccb5..c35f3fcb9 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -2,7 +2,7 @@ -- Table storing all the device group info CREATE TABLE devices ( id INTEGER PRIMARY KEY NOT NULL, - unique_id device_id BLOB UNIQUE NOT NULL CHECK(length(id) == 32), + unique_id BLOB UNIQUE NOT NULL CHECK(length(unique_id) == 32), state INTEGER NOT NULL CHECK(state == 0 OR state == 1 OR state == 2), -- registered, pending, unregistered changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unconfirmed local device info changes diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 002dd8765..47ba2c8da 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,12 +45,16 @@ if(ENABLE_NETWORKING) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onion_request_router.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_snode_pool.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_core_network.cpp) endif() add_library(test_libs INTERFACE) target_link_libraries(test_libs INTERFACE libsession::config + libsession::core + libsession::crypto + session::SQLite sessiondep::libsodium nlohmann_json::nlohmann_json oxen::logging) diff --git a/tests/test_core_network.cpp b/tests/test_core_network.cpp new file mode 100644 index 000000000..e4808f3aa --- /dev/null +++ b/tests/test_core_network.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include "utils.hpp" + +using namespace session; + +TEST_CASE("Core can hold an optional Network interface", "[core][network]") { + core::callbacks callbacks; + auto db_path = std::filesystem::temp_directory_path() / "test_core_network.db"; + if (std::filesystem::exists(db_path)) + std::filesystem::remove(db_path); + + core::Core core{callbacks, db_path, sqlite::argon2id_password{"test"}}; + + SECTION("Network is initially null") { + CHECK(core.network() == nullptr); + } + + SECTION("Network can be set and retrieved") { + auto network = std::make_shared(network::config::Config{}); + core.set_network(network); + CHECK(core.network() == network); + } + + if (std::filesystem::exists(db_path)) + std::filesystem::remove(db_path); +} From 91b448daae173373e7b05998d16ed88b3cc92edd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 19:08:13 -0300 Subject: [PATCH 027/193] devices: publish & distributed tracking Adds a "needs push" concept, along with more tracking fields to let us distinguish between various possible states. --- include/session/core/devices.hpp | 27 +++++- src/core/devices.cpp | 145 ++++++++++++++++++++++++------- src/core/schema/000_devices.sql | 6 +- 3 files changed, 139 insertions(+), 39 deletions(-) diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index c48fa9139..4b7da5487 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -206,10 +206,6 @@ class Devices final : detail::CoreComponent { // key first. If there is no current key at all, this generates one. std::vector active_device_keys(); - // Returns true if the device key updates needs to be pushed for other account devices to - // receive. Returns an enum value: - // - - struct AccountKeys : XWingKeys { std::chrono::sys_seconds created; std::optional rotated; @@ -260,6 +256,29 @@ class Devices final : detail::CoreComponent { // recipients who only know the account's Session ID (X25519) to verify the keys. // Throws if there are no active account keys. std::vector build_account_pubkey_message(); + + // Flags indicating which messages need to be pushed to the swarm. + struct NeedsPush { + bool device_group; ///< True if an updated device group message needs to be pushed + bool account_pubkey; ///< True if an updated account pubkey message needs to be pushed + }; + + // Returns whether a push is currently needed. Should be called after processing a final swarm + // message batch (or at startup) to determine whether outgoing pushes are required. + // + // device_group is true when this device is registered AND any of the following hold: + // - our own device info has changed since the last confirmed device group push + // - any device has a state transition (registered/removed) that needs broadcasting + // - any account key seed has not yet been distributed via a confirmed push + // + // account_pubkey is true when the current active account key has not yet been seen confirmed + // on the swarm (i.e. neither we nor another device has pushed it and we've received it back). + NeedsPush needs_push(); + + // Marks the device group message as successfully pushed with the given own-device seqno (which + // the caller reads from device_info() before building the push message). Updates pushed_seqno, + // clears broadcast_needed on all device rows, and marks all account key seeds as distributed. + void mark_device_group_pushed(int64_t seqno); }; } // namespace session::core diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 2f275f984..678dcffde 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -124,9 +124,24 @@ Devices::DeviceKeys Devices::rotate_device_keys() { auto keys = keys_from_seed(seed); auto c = conn(); - auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()); - c.prepared_exec("INSERT INTO device_privkeys (created, seed) VALUES (?, ?)", now.count(), seed); + SQLite::Transaction tx{c.sql}; + + auto now = epoch_seconds(sysclock_now_s()); + c.prepared_exec("INSERT INTO device_privkeys (created, seed) VALUES (?, ?)", now, seed); + + // Update our own device row with the new pubkeys and bump seqno so the change gets broadcast. + // If no row exists yet, this is a no-op; the new pubkeys will be read from the active device + // keys when the row is first created. + c.prepared_exec( + "UPDATE devices" + " SET pubkey_mlkem768 = ?, pubkey_x25519 = ?, seqno = seqno + 1, timestamp = ?" + " WHERE unique_id = ?", + keys.mlkem768_pub, + keys.x25519_pub, + now, + self_id); + + tx.commit(); log::info(cat, "New rotating device keys generated: {}", keys); @@ -272,14 +287,13 @@ namespace { // Upserts a device into the devices table (guarded by seqno) and updates device_unknown extras. // Returns the row id if inserted or updated (i.e. seqno guard allowed it), nullopt if the // update was rejected by the seqno guard. info.id must be set to the 32-byte device id. - // Always sets changes=FALSE since this is for storing incoming device data, not local changes. std::optional upsert_device_info(sqlite::Connection& c, const device::Info& info) { auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; auto dev_id = c.prepared_maybe_get( R"(INSERT INTO devices (unique_id, state, seqno, timestamp, device_type, description, version, - pubkey_mlkem768, pubkey_x25519, kicked_timestamp, changes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, FALSE) + pubkey_mlkem768, pubkey_x25519, kicked_timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL) ON CONFLICT(unique_id) DO UPDATE SET state = excluded.state, seqno = excluded.seqno, @@ -289,8 +303,7 @@ namespace { version = excluded.version, pubkey_mlkem768 = excluded.pubkey_mlkem768, pubkey_x25519 = excluded.pubkey_x25519, - kicked_timestamp = excluded.kicked_timestamp, - changes = excluded.changes + kicked_timestamp = excluded.kicked_timestamp WHERE excluded.seqno > seqno RETURNING id)", info.id, @@ -339,7 +352,7 @@ device::map Devices::devices( device::map devs; std::string query = - "SELECT id, unique_id, state, changes, seqno, timestamp, device_type, description," + "SELECT id, unique_id, state, seqno, timestamp, device_type, description," " version, pubkey_mlkem768, pubkey_x25519" " FROM devices WHERE ((1 << state) & ?) != 0"; if (!only_device.empty()) @@ -352,13 +365,12 @@ device::map Devices::devices( else bind_oneshot(st, state_mask, only_device); - for (auto [id, devid, state, changes, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : + for (auto [id, devid, state, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : sqlite::IterableStatementWrapper< int64_t, sqlite::blob_guts>, int, int, - int, int64_t, std::string, std::string, @@ -851,6 +863,21 @@ std::vector Devices::encrypt_device_data(const device::map& devices) return out; } +// Prebuilt SQL with Processing/State enum values embedded as literals rather than parameters. +static const std::string KICK_DEVICE_SQL = + "UPDATE devices" + " SET state = {0}, kicked_timestamp = ?," + " processing = CASE WHEN state = {1} THEN {2} ELSE processing END," + " broadcast_needed = CASE WHEN state = {1} THEN 1 ELSE broadcast_needed END" + " WHERE unique_id = ?"_format( + static_cast(device::State::Unregistered), + static_cast(device::State::Registered), + static_cast(Processing::Removed)); + +static const std::string REGISTER_DEVICE_SQL = + "UPDATE devices SET processing = {}, broadcast_needed = 1 WHERE id = ?"_format( + static_cast(Processing::Registered)); + void Devices::receive_device_group_message(std::span data) { GroupPayload payload; try { @@ -891,16 +918,7 @@ void Devices::receive_device_group_message(std::span data) // (we'd have no data to fill the required columns with), so skip it. Set // processing=Removed only if the device was previously Registered. assert(info.kicked); - c.prepared_exec( - R"(UPDATE devices - SET state = ?, kicked_timestamp = ?, - processing = CASE WHEN state = ? THEN ? ELSE processing END - WHERE unique_id = ?)", - static_cast(device::State::Unregistered), - info.kicked->time_since_epoch().count(), - static_cast(device::State::Registered), - static_cast(Processing::Removed), - id); + c.prepared_exec(KICK_DEVICE_SQL, info.kicked->time_since_epoch().count(), id); continue; } @@ -916,10 +934,7 @@ void Devices::receive_device_group_message(std::span data) // Mark as newly registered only on a state transition (not for info-only updates). if (!was_registered) - c.prepared_exec( - "UPDATE devices SET processing = ? WHERE id = ?", - static_cast(Processing::Registered), - *dev_id); + c.prepared_exec(REGISTER_DEVICE_SQL, *dev_id); } tx.commit(); @@ -950,15 +965,16 @@ Devices::LinkRequestResult Devices::build_link_request() { reinterpret_cast(keys.front().mlkem768_pub.data()), info.pk_mlkem768.size()); - // Upsert our own device row with the updated seqno, timestamp, and pubkeys. Mark changes=TRUE - // so that other parts of the system know a pending link request is outstanding. + // Upsert our own device row with the updated seqno, timestamp, and pubkeys. The pending link + // request is detectable via state=Pending on our own row; needs_push() detects dirty state via + // the seqno increment above exceeding pushed_seqno. auto c = conn(); auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; c.prepared_exec( R"(INSERT INTO devices (unique_id, state, seqno, timestamp, device_type, description, version, - pubkey_mlkem768, pubkey_x25519, changes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE) + pubkey_mlkem768, pubkey_x25519) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(unique_id) DO UPDATE SET state = excluded.state, seqno = excluded.seqno, @@ -967,8 +983,7 @@ Devices::LinkRequestResult Devices::build_link_request() { description = excluded.description, version = excluded.version, pubkey_mlkem768 = excluded.pubkey_mlkem768, - pubkey_x25519 = excluded.pubkey_x25519, - changes = excluded.changes)", + pubkey_x25519 = excluded.pubkey_x25519)", self_id, static_cast(device::State::Pending), info.seqno, @@ -1357,8 +1372,72 @@ void Devices::parse_device_messages( } void Devices::parse_account_pubkeys( - std::span> /*messages*/, bool /*is_final*/) { - log::debug(cat, "parse_account_pubkeys: not yet implemented"); + std::span> messages, bool /*is_final*/) { + if (messages.empty()) + return; + + // The x25519 pubkey for signature verification: session_id() is 0x05 || x25519_pub + auto x25519_pub = core.globals.session_id().subspan<1>(); + + auto c = conn(); + for (const auto& msg : messages) { + try { + oxenc::bt_dict_consumer in{msg}; + auto M = in.require_span("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub]( + std::span body, + std::span sig) { + if (!xed25519::verify(sig, x25519_pub, body)) + throw std::runtime_error{ + "Invalid account pubkey message: signature verification failed"}; + }); + + // Look up the key by indicator (indexed) then verify full pubkeys, and mark published. + c.prepared_exec( + "UPDATE device_account_keys SET published = 1" + " WHERE key_indicator = ? AND pubkey_mlkem768 = ? AND pubkey_x25519 = ?", + M.first<2>(), + M, + X); + } catch (const std::exception& e) { + log::warning(cat, "Ignoring malformed account pubkey message: {}", e.what()); + } + } +} + +static const std::string NEEDS_PUSH_SQL = + "SELECT" + // device_group: we are registered AND (own seqno dirty OR broadcast needed OR + // undistributed account key) + " CASE WHEN EXISTS(" + " SELECT 1 FROM devices WHERE unique_id = ? AND state = {0}" + " ) THEN (" + " (SELECT pushed_seqno IS NULL OR seqno > pushed_seqno" + " FROM devices WHERE unique_id = ?)" + " OR EXISTS(SELECT 1 FROM devices WHERE broadcast_needed)" + " OR EXISTS(SELECT 1 FROM device_account_keys WHERE NOT distributed)" + " ) ELSE 0 END," + // account_pubkey: the current active account key has not yet been confirmed on the swarm + " EXISTS(SELECT 1 FROM device_account_keys WHERE rotated IS NULL AND NOT published)"_format( + static_cast(device::State::Registered)); + +Devices::NeedsPush Devices::needs_push() { + auto c = conn(); + auto [dg, ap] = c.prepared_get(NEEDS_PUSH_SQL, self_id, self_id); + return {.device_group = bool(dg), .account_pubkey = bool(ap)}; +} + +void Devices::mark_device_group_pushed(int64_t seqno) { + auto c = conn(); + SQLite::Transaction tx{c.sql}; + c.prepared_exec( + "UPDATE devices SET pushed_seqno = ? WHERE unique_id = ?", seqno, self_id); + c.prepared_exec("UPDATE devices SET broadcast_needed = 0"); + c.prepared_exec("UPDATE device_account_keys SET distributed = 1"); + tx.commit(); } std::optional Devices::next_account_rotation() { diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index c35f3fcb9..c8b2772e0 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -5,9 +5,10 @@ CREATE TABLE devices ( unique_id BLOB UNIQUE NOT NULL CHECK(length(unique_id) == 32), state INTEGER NOT NULL CHECK(state == 0 OR state == 1 OR state == 2), -- registered, pending, unregistered - changes INTEGER NOT NULL DEFAULT 0, -- 1 means there are unconfirmed local device info changes processing INTEGER, -- non-null during batch processing: 1=new link request, 2=newly registered, 3=newly removed seqno INTEGER NOT NULL DEFAULT 1, + pushed_seqno INTEGER, -- seqno of the last confirmed device group push; NULL = never pushed + broadcast_needed INTEGER NOT NULL DEFAULT 0, -- 1 when a state transition (registered/removed) needs broadcasting timestamp INTEGER NOT NULL, kicked_timestamp INTEGER, -- set when the device was kicked from the device group device_type TEXT NOT NULL, -- typically a/i/d (Android/iOS/Desktop), but can be anything @@ -48,7 +49,6 @@ CREATE TABLE device_privkeys ( id INTEGER PRIMARY KEY NOT NULL, created INTEGER NOT NULL, -- unix timestamp rotated INTEGER, -- timestamp when a newer key was added, superceding this key - pushed INTEGER, -- timestamp when this key was verified as pushed to the device group seed BLOB NOT NULL CHECK(length(seed) == 32), ) STRICT; @@ -67,6 +67,8 @@ CREATE TABLE device_account_keys ( id INTEGER PRIMARY KEY NOT NULL, created INTEGER NOT NULL, rotated INTEGER, -- timestamp when a new key superceded this key + distributed INTEGER NOT NULL DEFAULT 0, -- 1 once this key's seed has been included in a confirmed device group push + published INTEGER NOT NULL DEFAULT 0, -- 1 once this key's pubkeys have been confirmed pushed as the account pubkey message seed BLOB UNIQUE NOT NULL CHECK(length(seed) == 32), pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) == 1184), pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) == 32), From 39f20b514c59b435e9e5af9541b9b477443746be Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 22:36:50 -0300 Subject: [PATCH 028/193] Implement update_info --- include/session/core/devices.hpp | 5 +++ src/core/devices.cpp | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 4b7da5487..e881adfbd 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -107,6 +107,11 @@ namespace device { default: return other_device; } } + + // Returns true if the user-settable fields (those controlled by update_info()) are equal to + // the corresponding fields in `other`. Does NOT compare id, seqno, timestamp, state, pk_*, + // or kicked. The unknown `extra` fields are included in the comparison. + bool same_user_fields(const Info& other) const; }; using map = std::map, Info>; diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 678dcffde..86336aa2d 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -393,6 +393,63 @@ std::pair Devices::device_info() { return {device::Info{.id = self_id}, false}; } +bool device::Info::same_user_fields(const Info& other) const { + auto fields = [](const Info& i) { + return std::tie(i.type, i.other_device, i.description, i.version, i.extra); + }; + return fields(*this) == fields(other); +} + +void Devices::update_info(const device::Info& info) { + auto [current, is_registered] = device_info(); + + // Early-exit if nothing changed: no seqno bump, no push triggered. + // current.seqno == 0 means no row exists yet (default-init sentinel; real rows have seqno >= 1). + if (current.seqno > 0 && current.same_user_fields(info)) + return; + + auto keys = active_device_keys(); + auto& front_key = keys.front(); + auto now = sysclock_now_s(); + auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + auto dev_id = c.prepared_get( + R"(INSERT INTO devices + (unique_id, state, seqno, timestamp, device_type, description, version, + pubkey_mlkem768, pubkey_x25519) + VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) + ON CONFLICT(unique_id) DO UPDATE SET + seqno = seqno + 1, + timestamp = excluded.timestamp, + device_type = excluded.device_type, + description = excluded.description, + version = excluded.version + RETURNING id)", + self_id, + static_cast(device::State::Unregistered), + now.time_since_epoch().count(), + info.encoded_type(), + info.description, + ver, + std::as_bytes(std::span{front_key.mlkem768_pub}), + std::as_bytes(std::span{front_key.x25519_pub})); + + c.prepared_exec("DELETE FROM device_unknown WHERE device = ?", dev_id); + for (const auto& [key, val] : info.extra) { + auto encoded = std::visit([](const auto& v) { return oxenc::bt_serialize(v); }, val); + c.prepared_exec( + "INSERT INTO device_unknown (device, key, bt_value) VALUES (?, ?, ?)", + dev_id, + key, + to_span(encoded)); + } + + tx.commit(); +} + namespace { // Plain-old-data representation of a single account key seed entry as read from or written to From 1afcc8957c295cf9e03ec3fa5b89a37126809f40 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Mar 2026 14:32:38 -0300 Subject: [PATCH 029/193] Add a global adjustable system clock `session::AdjustedClock` now carries an adjustable static offset and when `now()` is called it returns standard system clock timepoints with the adjustment applied. The networking code had a similar adjustment already, although it was per-network-object: this change replaces that, and makes it now global across all uses of the clock anywhere, instead of per-Network instance. --- include/session/config/base.hpp | 3 +- .../session/config/convo_info_volatile.hpp | 1 + include/session/config/user_profile.hpp | 1 + include/session/core/callbacks.hpp | 6 +- include/session/core/devices.hpp | 14 ++- include/session/hash.hpp | 8 +- include/session/network/session_network.hpp | 6 +- include/session/pro_backend.hpp | 1 + include/session/session_protocol.hpp | 1 + include/session/util.hpp | 64 ---------- src/config/base.cpp | 11 +- src/config/convo_info_volatile.cpp | 5 +- src/config/groups/keys.cpp | 1 + src/config/internal.hpp | 7 +- src/config/user_profile.cpp | 14 +-- src/core/devices.cpp | 111 ++++++++++-------- src/network/backends/session_file_server.cpp | 3 +- src/network/session_network.cpp | 9 +- src/network/snode_pool.cpp | 7 +- src/session_encrypt.cpp | 1 + tests/test_core_network.cpp | 1 + tests/utils.hpp | 16 +++ 22 files changed, 138 insertions(+), 153 deletions(-) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index fad7c7d76..ed6d57d89 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -202,7 +203,7 @@ class ConfigBase : public ConfigSig { done = true; size = 0; parts.clear(); - expiry = std::chrono::system_clock::now() + lifetime; + expiry = clock_now() + lifetime; } }; diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 5a2770ed5..8147ca829 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "base.hpp" diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index fd9f1821a..56242f985 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "base.hpp" diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 0dbc8fc80..66b9a2bc2 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -1,8 +1,8 @@ #pragma once #include +#include #include #include -#include namespace session::core { @@ -38,7 +38,9 @@ struct callbacks { /// - sas -- a span of 21 string_views representing the short authentication string for this /// request. The first 7 are the standard display; all 21 are available for the extended /// view. Formatting and joining is left to the caller. - std::function sas)> device_link_request; + std::function sas)> + device_link_request; /// Callback that is invoked when a new device has been linked to the account. If a batch /// of messages being processed includes both a device link request *and* an acceptance diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index e881adfbd..0f0848e78 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -33,7 +34,7 @@ namespace device { enum class State { Registered = 0, ///< Device is in the account's registered device set - Pending = 1, ///< A device with a pending link request. This is used for two cases: + Pending = 1, ///< A device with a pending link request. This is used for two cases: ///< - This device has sent a request to join the account's device group and ///< is awaiting acceptance by an existing device. ///< - Another device has sent a link request that has been received but not @@ -226,6 +227,9 @@ class Devices final : detail::CoreComponent { static constexpr auto ACCOUNT_KEY_ROTATION_PERIOD = 12h; static constexpr auto ACCOUNT_KEY_ROTATION_WINDOW = 2h; + // How long to keep a pending link request before pruning it as stale. + static constexpr auto LINK_REQUEST_MAX_AGE = 10min; + // Rotates the shared account keys used for PFS+PQ message encryption. Generates a new random // seed, stores it in the database, marks the previous active key as rotated, and prunes keys // older than ACCOUNT_KEY_RETENTION. Should be called when account_rotation_due() is true and @@ -243,7 +247,7 @@ class Devices final : detail::CoreComponent { bool device_rotation_due() { auto t = next_device_rotation(); - return t && *t <= std::chrono::system_clock::now(); + return t && *t <= clock_now(); } // Return true if the account key is due to be rotated by this device. Returns nullopt if this @@ -252,7 +256,7 @@ class Devices final : detail::CoreComponent { bool account_rotation_due() { auto t = next_account_rotation(); - return t && *t <= std::chrono::system_clock::now(); + return t && *t <= clock_now(); } // Builds the signed account public key message for upload to namespace -21. The message is a @@ -264,8 +268,8 @@ class Devices final : detail::CoreComponent { // Flags indicating which messages need to be pushed to the swarm. struct NeedsPush { - bool device_group; ///< True if an updated device group message needs to be pushed - bool account_pubkey; ///< True if an updated account pubkey message needs to be pushed + bool device_group; ///< True if an updated device group message needs to be pushed + bool account_pubkey; ///< True if an updated account pubkey message needs to be pushed }; // Returns whether a push is currently needed. Should be called after processing a final swarm diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 7215a2d88..6eab27300 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -125,9 +125,7 @@ void blake2b_key(Out& out, const Key& key, const T&... args) { std::ranges::size(out)); update_all(st, args...); crypto_generichash_blake2b_final( - &st, - reinterpret_cast(std::ranges::data(out)), - std::ranges::size(out)); + &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); } /// API: hash/blake2b @@ -171,9 +169,7 @@ void blake2b_key_pers( pers.data()); update_all(st, args...); crypto_generichash_blake2b_final( - &st, - reinterpret_cast(std::ranges::data(out)), - std::ranges::size(out)); + &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); } /// API: hash/blake2b_pers diff --git a/include/session/network/session_network.hpp b/include/session/network/session_network.hpp index 9c8f07eb9..dd1f370cd 100644 --- a/include/session/network/session_network.hpp +++ b/include/session/network/session_network.hpp @@ -5,6 +5,7 @@ #include #include +#include "session/clock.hpp" #include "session/network/backends/session_file_server.hpp" #include "session/network/network_config.hpp" #include "session/network/routing/network_router.hpp" @@ -59,7 +60,9 @@ class Network : public std::enable_shared_from_this { bool has_retrieved_time_offset() const { return (_last_successful_clock_resync == std::chrono::steady_clock::time_point{}); }; - std::chrono::milliseconds network_time_offset() const { return _network_time_offset; }; + std::chrono::milliseconds network_time_offset() const { + return std::chrono::duration_cast(AdjustedClock::get_offset()); + }; fork_versions fork() const { return _fork_versions.load(); }; uint16_t hardfork() const { return _fork_versions.load().hardfork; }; uint16_t softfork() const { return _fork_versions.load().softfork; }; @@ -107,7 +110,6 @@ class Network : public std::enable_shared_from_this { private: std::atomic _status{ConnectionStatus::unknown}; - std::atomic _network_time_offset{0ms}; std::atomic _fork_versions{{0, 0}}; void configure(); diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 15fec99ad..64d4b883a 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 139fdff63..1ff2e0939 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include diff --git a/include/session/util.hpp b/include/session/util.hpp index 94a3288bb..611eb2436 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -131,70 +131,6 @@ inline unsigned char* to_unsigned(unsigned char* x) { return x; } -// The same as std::chrono::system_clock::now(), except that it allows you to get it in a different -// precision. E.g. sysclock_now gives a timepoint with seconds precision (aka -// std::chrono::sys_seconds). -template -inline std::chrono::sys_time sysclock_now() { - return std::chrono::floor(std::chrono::system_clock::now()); -} -// Shortcut for sysclock_now(); -inline std::chrono::sys_seconds sysclock_now_s() { - return sysclock_now(); -} -using sys_ms = std::chrono::sys_time; -// Shortcut for sysclock_now>(); -inline sys_ms sysclock_now_ms() { - return sysclock_now(); -} - -// Returns the duration count of the given duration cast into ToDuration. Example: -// duration_count(30000ms) // returns 30 -// This function requires that the target type is no more precise than d, that is, it will not allow -// you to cast from seconds to milliseconds because such a cast indicates that the sub-second -// precision has already been lost. -template - requires std::is_convertible_v> -constexpr int64_t duration_count(const std::chrono::duration& d) { - return std::chrono::duration_cast(d).count(); -} -// Returns the seconds count of the given duration -template - requires std::is_convertible_v> -constexpr int64_t duration_seconds(const std::chrono::duration& d) { - return duration_count(d); -} -// Returns the milliseconds count of the given duration -template - requires std::is_convertible_v> -constexpr int64_t duration_ms(const std::chrono::duration& d) { - return duration_count(d); -} - -// Returns the time-since-epoch count of the given time point, cast into ToDuration. The given time -// point must be at least as precise as ToDuration, i.e. this will not allow you to cast to a more -// precise time point as that would mean the intended precision has already been lost by an earlier -// cast. -template - requires std::is_convertible_v -constexpr int64_t epoch_count(const std::chrono::time_point& t) { - return duration_count(t.time_since_epoch()); -} -// Returns the seconds-since-epoch count of the given time point. The given time point must be at -// least as precise as seconds. -template - requires std::is_convertible_v -constexpr int64_t epoch_seconds(const std::chrono::time_point& t) { - return duration_seconds(t.time_since_epoch()); -} -// Returns the milliseconds-since-epoch count of the given time point. The given time point must -// have at least milliseconds precision. -template - requires std::is_convertible_v -constexpr int64_t epoch_ms(const std::chrono::time_point& t) { - return duration_ms(t.time_since_epoch()); -} - /// Returns true if the first string is equal to the second string, compared case-insensitively. inline bool string_iequal(std::string_view s1, std::string_view s2) { return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) { diff --git a/src/config/base.cpp b/src/config/base.cpp index 440844d1f..b5f0cd4a5 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -20,6 +20,7 @@ #include "internal.hpp" #include "oxenc/bt_serialize.h" +#include "session/clock.hpp" #include "session/config/base.h" #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" @@ -270,7 +271,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span ConfigBase::active_hashes() const { // First copy any hashes that make up the currently active config: std::unordered_set hashes{_curr_hashes}; - auto now = std::chrono::system_clock::now(); + auto now = clock_now(); // Add include any pending partial configs that *might* be newer: for (const auto& [_, part] : _multiparts) if (!part.done && part.expiry > now) diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 7f62f9b29..4ec9ba58a 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -12,6 +12,7 @@ #include #include "internal.hpp" +#include "session/clock.hpp" #include "session/config/convo_info_volatile.h" #include "session/config/error.h" #include "session/export.h" @@ -353,7 +354,7 @@ void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { r = c.last_read; else { std::chrono::system_clock::time_point last_read{std::chrono::milliseconds{c.last_read}}; - if (last_read > std::chrono::system_clock::now() - PRUNE_LOW) + if (last_read > clock_now() - PRUNE_LOW) info["r"] = c.last_read; } @@ -371,7 +372,7 @@ static bool is_stale(const C& c, std::chrono::system_clock::time_point cutoff) { } void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { - const auto cutoff = std::chrono::system_clock::now() - prune; + const auto cutoff = clock_now() - prune; std::vector stale; for (auto it = begin_1to1(); it != end(); ++it) diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 833317197..cbbc3bc4b 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -18,6 +18,7 @@ #include #include "../internal.hpp" +#include "session/clock.hpp" #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" diff --git a/src/config/internal.hpp b/src/config/internal.hpp index e64e699f9..33c503a6e 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -8,6 +8,7 @@ #include #include +#include "session/clock.hpp" #include "session/config/base.h" #include "session/config/base.hpp" #include "session/config/error.h" @@ -172,12 +173,6 @@ std::optional maybe_int(const session::config::dict& d, const char* key // int. Equivalent to `maybe_int(d, key).value_or(0)`. int64_t int_or_0(const session::config::dict& d, const char* key); -// Returns std::chrono::system_clock::now(), with the given precision (seconds, if unspecified). -template -std::chrono::sys_time ts_now() { - return std::chrono::floor(std::chrono::system_clock::now()); -} - // Digs into a config `dict` to get out an int64_t containing unix timestamp seconds, returns it // wrapped in a std::chrono::sys_seconds. Returns nullopt if not there (or not int). std::optional maybe_ts(const session::config::dict& d, const char* key); diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 37c86cf0d..2edfb50f0 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -38,7 +38,7 @@ void UserProfile::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = ts_now(); + data[target_timestamp] = clock_now_s(); } void UserProfile::set_name_truncated(std::string new_name) { set_name(utf8_truncate(std::move(new_name), contact_info::MAX_NAME_LENGTH)); @@ -75,7 +75,7 @@ void UserProfile::set_profile_pic(std::string_view url, std::span value) { data["M"] = static_cast(*value); const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = ts_now(); + data[target_timestamp] = clock_now_s(); } std::optional UserProfile::get_blinded_msgreqs() const { @@ -175,7 +175,7 @@ void UserProfile::set_pro_config(const ProConfig& pro) { const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = ts_now(); + data[target_timestamp] = clock_now_s(); } } @@ -198,7 +198,7 @@ void UserProfile::set_pro_badge(bool enabled) { if (dirtied) { const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = ts_now(); + data[target_timestamp] = clock_now_s(); } } @@ -208,7 +208,7 @@ void UserProfile::set_animated_avatar(bool enabled) { if (dirtied) { const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = ts_now(); + data[target_timestamp] = clock_now_s(); } } diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 86336aa2d..72ccb0334 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -13,28 +12,30 @@ #include #include -#include #include +#include #include #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include #include +#include #include namespace session::core { using namespace oxen::log::literals; +using namespace std::literals; namespace log = oxen::log; static auto cat = log::Cat("core.dev"); @@ -111,7 +112,8 @@ std::string format_as(const key_summary& ks) { // that fmtlib's ADL-based lookup can find it when logging these types. template Keys> std::string format_as(const Keys& k) { - return "X25519[{}], MLKEM768[{}]"_format(key_summary{k.x25519_pub}, key_summary{k.mlkem768_pub}); + return "X25519[{}], MLKEM768[{}]"_format( + key_summary{k.x25519_pub}, key_summary{k.mlkem768_pub}); } Devices::DeviceKeys Devices::rotate_device_keys() { @@ -126,7 +128,7 @@ Devices::DeviceKeys Devices::rotate_device_keys() { auto c = conn(); SQLite::Transaction tx{c.sql}; - auto now = epoch_seconds(sysclock_now_s()); + auto now = epoch_seconds(clock_now_s()); c.prepared_exec("INSERT INTO device_privkeys (created, seed) VALUES (?, ?)", now, seed); // Update our own device row with the new pubkeys and bump seqno so the change gets broadcast. @@ -157,7 +159,7 @@ void Devices::rotate_account_keys() { c.prepared_exec( "INSERT INTO device_account_keys (created, seed, pubkey_mlkem768, pubkey_x25519)" " VALUES (?, ?, ?, ?)", - epoch_seconds(sysclock_now_s()), + epoch_seconds(clock_now_s()), seed, keys.mlkem768_pub, keys.x25519_pub); @@ -192,18 +194,17 @@ std::vector Devices::active_account_keys() { c.prepared_exec( "DELETE FROM device_account_keys WHERE rotated < ?", - epoch_seconds(sysclock_now_s() - ACCOUNT_KEY_RETENTION)); + epoch_seconds(clock_now_s() - ACCOUNT_KEY_RETENTION)); std::vector keys; bool have_active = false; - for (auto [id, created, rotated, seed, pk_ml, pk_x] : - c.prepared_results< - int64_t, - int64_t, - std::optional, - sqlite::blobn<32>, - sqlite::blobn, - sqlite::blobn<32>>( + for (auto [id, created, rotated, seed, pk_ml, pk_x] : c.prepared_results< + int64_t, + int64_t, + std::optional, + sqlite::blobn<32>, + sqlite::blobn, + sqlite::blobn<32>>( "SELECT id, created, rotated, seed, pubkey_mlkem768, pubkey_x25519" " FROM device_account_keys" " ORDER BY rotated DESC NULLS FIRST, created DESC")) { @@ -404,13 +405,14 @@ void Devices::update_info(const device::Info& info) { auto [current, is_registered] = device_info(); // Early-exit if nothing changed: no seqno bump, no push triggered. - // current.seqno == 0 means no row exists yet (default-init sentinel; real rows have seqno >= 1). + // current.seqno == 0 means no row exists yet (default-init sentinel; real rows have seqno >= + // 1). if (current.seqno > 0 && current.same_user_fields(info)) return; auto keys = active_device_keys(); auto& front_key = keys.front(); - auto now = sysclock_now_s(); + auto now = clock_now_s(); auto ver = info.version[0] * 1000000 + info.version[1] * 1000 + info.version[2]; auto c = conn(); @@ -518,8 +520,7 @@ namespace { } std::string encode_group_payload( - const device::map& devices, - std::span acc_keys) { + const device::map& devices, std::span acc_keys) { oxenc::bt_dict_producer out; { @@ -560,9 +561,10 @@ namespace { e.append("c", k.created); if (k.rotated) e.append("r", *k.rotated); - e.append("s", - std::string_view{ - reinterpret_cast(k.seed.data()), k.seed.size()}); + e.append( + "s", + std::string_view{ + reinterpret_cast(k.seed.data()), k.seed.size()}); } } @@ -980,10 +982,9 @@ void Devices::receive_device_group_message(std::span data) } // Check state before upsert to detect a registration transition. - bool was_registered = c.prepared_maybe_get( - "SELECT state FROM devices WHERE unique_id = ?", id) - .value_or(-1) == - static_cast(device::State::Registered); + bool was_registered = + c.prepared_maybe_get("SELECT state FROM devices WHERE unique_id = ?", id) + .value_or(-1) == static_cast(device::State::Registered); auto dev_id = upsert_device_info(c, info); if (!dev_id) @@ -1007,8 +1008,7 @@ Devices::LinkRequestResult Devices::build_link_request() { info.id = self_id; info.seqno++; - info.timestamp = - std::chrono::time_point_cast(std::chrono::system_clock::now()); + info.timestamp = clock_now_s(); // Always use the current active device keys for the pubkeys in the link request, regardless // of what is stored in the DB, as the DB may lag a key rotation. @@ -1065,9 +1065,9 @@ Devices::LinkRequestResult Devices::build_link_request() { // Wrap in outer bt-dict: {"": "L", "L": } std::vector out( - 2 // Outer "d" ... "e" delimiters - + 5 // "0:" + "1:L" (message type indicator) - + 3 + bt_bytes_encoded(encrypted.size()) // "1:L" + "NNN:...(encrypted blob)..." + 2 // Outer "d" ... "e" delimiters + + 5 // "0:" + "1:L" (message type indicator) + + 3 + bt_bytes_encoded(encrypted.size()) // "1:L" + "NNN:...(encrypted blob)..." ); oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; o.append("", "L"); @@ -1295,7 +1295,7 @@ void Devices::receive_link_request(std::span data) { received_at = excluded.received_at, sas_seed = excluded.sas_seed)", *dev_id, - epoch_seconds(sysclock_now_s()), + epoch_seconds(clock_now_s()), sas_seed); // Set processing=LinkRequest only if not already set to a higher-priority value by a @@ -1339,8 +1339,18 @@ void Devices::parse_device_messages( auto c = conn(); std::vector items; - for (auto [row_id, raw_id, processing_int, state_int, seqno, timestamp, dtype, desc, ver, - pk_ml, pk_x, kicked_ts] : + for (auto [row_id, + raw_id, + processing_int, + state_int, + seqno, + timestamp, + dtype, + desc, + ver, + pk_ml, + pk_x, + kicked_ts] : c.prepared_results< int64_t, sqlite::blob_guts>, @@ -1362,8 +1372,15 @@ void Devices::parse_device_messages( item.id = raw_id; item.processing = static_cast(processing_int); item.info = fill_device_info( - raw_id, state_int, seqno, timestamp, - std::move(dtype), std::move(desc), ver, pk_ml, pk_x); + raw_id, + state_int, + seqno, + timestamp, + std::move(dtype), + std::move(desc), + ver, + pk_ml, + pk_x); if (kicked_ts) item.info.kicked.emplace(std::chrono::seconds{*kicked_ts}); load_device_extras(c, row_id, item.info); @@ -1376,7 +1393,8 @@ void Devices::parse_device_messages( case Processing::LinkRequest: if (auto& f = cb().device_link_request) { auto [lr_id, sas_seed] = c.prepared_get< - int64_t, sqlite::blob_guts>>( + int64_t, + sqlite::blob_guts>>( "SELECT id, sas_seed FROM device_link_requests WHERE device = ?", item.row_id); f(static_cast(lr_id), item.info, sas_from_seed(sas_seed)); @@ -1425,7 +1443,7 @@ void Devices::parse_device_messages( // Prune stale link requests (older than 10 minutes) c.prepared_exec( "DELETE FROM device_link_requests WHERE received_at < ?", - epoch_seconds(sysclock_now_s() - std::chrono::seconds{600})); + epoch_seconds(clock_now_s() - LINK_REQUEST_MAX_AGE)); } void Devices::parse_account_pubkeys( @@ -1449,7 +1467,8 @@ void Devices::parse_account_pubkeys( std::span sig) { if (!xed25519::verify(sig, x25519_pub, body)) throw std::runtime_error{ - "Invalid account pubkey message: signature verification failed"}; + "Invalid account pubkey message: signature verification " + "failed"}; }); // Look up the key by indicator (indexed) then verify full pubkeys, and mark published. @@ -1490,8 +1509,7 @@ Devices::NeedsPush Devices::needs_push() { void Devices::mark_device_group_pushed(int64_t seqno) { auto c = conn(); SQLite::Transaction tx{c.sql}; - c.prepared_exec( - "UPDATE devices SET pushed_seqno = ? WHERE unique_id = ?", seqno, self_id); + c.prepared_exec("UPDATE devices SET pushed_seqno = ? WHERE unique_id = ?", seqno, self_id); c.prepared_exec("UPDATE devices SET broadcast_needed = 0"); c.prepared_exec("UPDATE device_account_keys SET distributed = 1"); tx.commit(); @@ -1503,7 +1521,8 @@ std::optional Devices::next_account_rotat if (!c.prepared_maybe_get( "SELECT 1 FROM devices WHERE unique_id = ? AND state = ?", - self_id, static_cast(device::State::Registered))) + self_id, + static_cast(device::State::Registered))) return std::nullopt; int64_t t_created = 0; @@ -1551,10 +1570,10 @@ std::vector Devices::build_account_pubkey_message() { const auto& k = keys.front(); std::vector out( - 2 // outer dict d...e - + 3 + bt_bytes_encoded(1184) // "1:M" + mlkem768_pub - + 3 + bt_bytes_encoded(32) // "1:X" + x25519_pub - + 3 + bt_bytes_encoded(64) // "1:~" + XEd25519 signature + 2 // outer dict d...e + + 3 + bt_bytes_encoded(1184) // "1:M" + mlkem768_pub + + 3 + bt_bytes_encoded(32) // "1:X" + x25519_pub + + 3 + bt_bytes_encoded(64) // "1:~" + XEd25519 signature ); oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index 1a59e16b9..d2af4f3a0 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -8,6 +8,7 @@ #include "../session_network_internal.hpp" #include "session/blinding.hpp" +#include "session/clock.hpp" #include "session/network/backends/backend_util.hpp" #include "session/network/backends/session_file_server.h" #include "session/random.hpp" @@ -316,7 +317,7 @@ Request get_client_version( // Generate the auth signature auto blinded_keys = blind_version_key_pair(to_span(seckey.view())); - auto timestamp = epoch_seconds(std::chrono::system_clock::now()); + auto timestamp = epoch_seconds(clock_now_s()); auto signature = blind_version_sign(to_span(seckey.view()), platform, timestamp); auto pubkey = x25519_pubkey::from_hex(DEFAULT_CONFIG.pubkey_hex); std::string blinded_pk_hex; diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 341d5e2de..088b69998 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -771,7 +771,10 @@ void Network::_update_network_state(const std::string& body) { if (new_versions != old_versions) on_network_info_changed( - _network_time_offset.load(), new_versions.hardfork, new_versions.softfork); + std::chrono::duration_cast( + AdjustedClock::get_offset()), + new_versions.hardfork, + new_versions.softfork); } } catch (const std::exception& e) { log::warning(cat, "Failed to parse network state from response: {}", e.what()); @@ -966,7 +969,7 @@ void Network::_launch_next_clock_out_of_sync_request( std::vector> headers, std::optional response) { auto end_steady = std::chrono::steady_clock::now(); - auto end_system = sysclock_now_ms(); + auto end_system = clock_now_ms(); // If the resync was cancelled or completed while we were in-flight, do nothing if (!_current_clock_resync_id || *_current_clock_resync_id != request_id) { @@ -1064,7 +1067,7 @@ void Network::_on_clock_resync_complete(const uint8_t total_requests) { median_offset = (middle_values_sum / 2); } - _network_time_offset = median_offset; + AdjustedClock::set_offset(median_offset); _last_successful_clock_resync = std::chrono::steady_clock::now(); log::info( cat, "[Request {}] Network offset set to: {}ms", refresh_id, median_offset.count()); diff --git a/src/network/snode_pool.cpp b/src/network/snode_pool.cpp index 41f2c6926..5c271a22f 100644 --- a/src/network/snode_pool.cpp +++ b/src/network/snode_pool.cpp @@ -14,6 +14,7 @@ #include #include +#include "session/clock.hpp" #include "session/file.hpp" #include "session/hash.hpp" #include "session/random.hpp" @@ -175,7 +176,7 @@ void SnodePool::_load_from_disk() { throw empty_file_exception{}; // We want to filter on load so we don't start the app with expired strikes - auto threshold = sysclock_now_s() - STRIKE_EXPIRY; + auto threshold = clock_now_s() - STRIKE_EXPIRY; std::string_view buf{ reinterpret_cast(loaded_strikes_data.data()), @@ -938,7 +939,7 @@ void SnodePool::record_node_failure(const service_node& node, bool permanent) { void SnodePool::record_node_failure(const ed25519_pubkey& key, bool permanent) { _loop->call([this, key, permanent] { - auto now = sysclock_now_s(); + auto now = clock_now_s(); if (permanent) for (int i = 0; i < _config.cache_node_strike_threshold; ++i) @@ -985,7 +986,7 @@ uint16_t SnodePool::node_strike_count(const ed25519_pubkey& key) { const auto& stamps = it->second; - const auto threshold = sysclock_now_s() - STRIKE_EXPIRY; + const auto threshold = clock_now_s() - STRIKE_EXPIRY; uint16_t count = 0; for (auto t : stamps) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 3f763b17a..e1ebb7645 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -26,6 +26,7 @@ #include #include "session/blinding.hpp" +#include "session/clock.hpp" #include "session/hash.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" diff --git a/tests/test_core_network.cpp b/tests/test_core_network.cpp index e4808f3aa..450f10a1d 100644 --- a/tests/test_core_network.cpp +++ b/tests/test_core_network.cpp @@ -2,6 +2,7 @@ #include #include #include + #include "utils.hpp" using namespace session; diff --git a/tests/utils.hpp b/tests/utils.hpp index b2e9fd6ab..a0795f32a 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -13,9 +13,25 @@ #include #include +#include "session/clock.hpp" #include "session/types.hpp" #include "session/util.hpp" +// RAII helper that saves the current AdjustedClock offset, installs a new one on construction, +// and restores the prior offset on destruction. +struct ScopedClockOffset { + explicit ScopedClockOffset(session::AdjustedClock::duration new_offset) : + _saved{session::AdjustedClock::get_offset()} { + session::AdjustedClock::set_offset(new_offset); + } + ~ScopedClockOffset() { session::AdjustedClock::set_offset(_saved); } + ScopedClockOffset(const ScopedClockOffset&) = delete; + ScopedClockOffset& operator=(const ScopedClockOffset&) = delete; + + private: + session::AdjustedClock::duration _saved; +}; + using namespace std::literals; using namespace oxenc::literals; using namespace oxen::log::literals; From 5aabcb587142b2d4f49ecbf50eed16fce6a8223e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:13:42 -0300 Subject: [PATCH 030/193] Add test helper friend class --- include/session/core.hpp | 6 ++++++ tests/test_helper.hpp | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/test_helper.hpp diff --git a/include/session/core.hpp b/include/session/core.hpp index 1fc583a19..b59073962 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -125,6 +125,10 @@ namespace session::network { class Network; } +namespace session { +class TestHelper; +} + namespace session::core { namespace quic = oxen::quic; @@ -134,6 +138,8 @@ namespace detail { } class Core { + friend class session::TestHelper; // for unit tests + // Constructed first (in init()), destroyed last: must outlive all components that use it. // Custom deleter allows quic::Loop to remain an incomplete type in this header. struct LoopDeleter { diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp new file mode 100644 index 000000000..97663291b --- /dev/null +++ b/tests/test_helper.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace session { + +class TestHelper { +public: +}; + +} // namespace session From d7a519dd3728a7b9f88b5f0a7c862d95d3eee2c2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:45:55 -0300 Subject: [PATCH 031/193] Add optional Core polling of device & account namespaces --- include/session/core.hpp | 8 +- include/session/core/callbacks.hpp | 2 +- include/session/core/globals.hpp | 7 +- include/session/network/session_network.hpp | 4 +- src/core.cpp | 140 +++++++++++++++++++- src/core/globals.cpp | 10 +- tests/CMakeLists.txt | 1 + tests/test_helper.hpp | 3 + 8 files changed, 165 insertions(+), 10 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index b59073962..3b3f6cd46 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -115,6 +115,7 @@ namespace oxen::quic { class Loop; +struct Ticker; } // namespace oxen::quic namespace session::pro_backend { @@ -165,6 +166,11 @@ class Core { // migrations, and then calls init() on each sub-component. void init(); + // Polling-related members and methods + std::shared_ptr _poll_ticker; + void _update_polling(); + void _poll(); + public: // Constructor taking a struct of callbacks to invoke on various events, and any number of // session::SQLite database options to open the core database. @@ -175,7 +181,7 @@ class Core { } /// Set an optional network interface that can be used to make network requests to swarm members - void set_network(std::shared_ptr network) { _network = std::move(network); } + void set_network(std::shared_ptr network); /// Returns the optional network interface, if set. const std::shared_ptr& network() const { return _network; } diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 66b9a2bc2..4367c3a28 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -39,7 +39,7 @@ struct callbacks { /// request. The first 7 are the standard display; all 21 are available for the extended /// view. Formatting and joining is left to the caller. std::function sas)> + int reqid, const device::Info& new_device, std::span sas)> device_link_request; /// Callback that is invoked when a new device has been linked to the account. If a batch diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index a52684411..09f30afa5 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,8 @@ class Globals final : detail::CoreComponent { // Read-only access is available via the account_seed() method, or via the `seed()` // CoreComponent base class method from other components. session::secure_buffer _account_seed; - std::array _pubkey_ed25519; + network::ed25519_pubkey _pubkey_ed25519; + network::x25519_pubkey _pubkey_x25519; std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix void init() override; @@ -74,7 +76,8 @@ class Globals final : detail::CoreComponent { session::secure_buffer::r_accessor account_seed() { return _account_seed.access(); } // These are computed from the account_seed during construction: std::span session_id() { return _session_id; } - std::span pubkey_ed25519() { return _pubkey_ed25519; } + const network::ed25519_pubkey& pubkey_ed25519() const { return _pubkey_ed25519; } + const network::x25519_pubkey& pubkey_x25519() const { return _pubkey_x25519; } }; } // namespace session::core diff --git a/include/session/network/session_network.hpp b/include/session/network/session_network.hpp index dd1f370cd..2dffdc89c 100644 --- a/include/session/network/session_network.hpp +++ b/include/session/network/session_network.hpp @@ -87,7 +87,7 @@ class Network : public std::enable_shared_from_this { /// retrieving the swarm. /// - 'callback' - [in] callback to be called with the retrieved swarm (in the case of an error /// the callback will be called with an empty list). - void get_swarm( + virtual void get_swarm( session::network::x25519_pubkey swarm_pubkey, bool ignore_strike_count, std::function swarm)> callback); @@ -104,7 +104,7 @@ class Network : public std::enable_shared_from_this { void get_random_nodes( uint16_t count, std::function nodes)> callback); - void send_request(Request request, network_response_callback_t callback); + virtual void send_request(Request request, network_response_callback_t callback); void upload(UploadRequest request); void download(DownloadRequest request); diff --git a/src/core.cpp b/src/core.cpp index 66a21635c..cbc1ebeb6 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,11 +1,19 @@ -#include +#include +#include #include +#include #include +#include #include #include +#include +#include #include #include +#include +#include +#include #include #include "session/core/component.hpp" @@ -32,12 +40,142 @@ void Core::init() { component->init(); _comp_init.clear(); + + _update_polling(); } void Core::register_comp_init(detail::CoreComponent* c) { _comp_init.push_back(c); } +void Core::set_network(std::shared_ptr network) { + _network = std::move(network); + _update_polling(); +} + +void Core::_update_polling() { + if (_network && !_poll_ticker) { + _poll_ticker = _loop->call_every(20s, [this] { _poll(); }); + } else if (!_network && _poll_ticker) { + _poll_ticker->stop(); + _poll_ticker.reset(); + } +} + +void Core::_poll() { + auto net = _network; + if (!net) + return; + + auto namespaces = {config::Namespace::Devices, config::Namespace::AccountPubkeys}; + nlohmann::json ns_list = nlohmann::json::array(); + nlohmann::json last_hashes = nlohmann::json::object(); + + auto conn = db.conn(); + for (auto ns : namespaces) { + auto ns_val = static_cast(ns); + ns_list.push_back(ns_val); + auto last_hash = conn.prepared_maybe_get( + "SELECT last_hash FROM namespace_sync WHERE namespace = ?", ns_val); + if (last_hash) + last_hashes[std::to_string(ns_val)] = *last_hash; + } + + auto now_ms = epoch_ms(sysclock_now_ms()); + auto session_id = oxenc::to_hex(globals.session_id()); + + nlohmann::json params = { + {"pubkey", session_id}, + {"namespaces", ns_list}, + {"timestamp", now_ms}, + }; + + if (!last_hashes.empty()) + params["last_hashes"] = last_hashes; + + std::string to_sign = fmt::format("retrieve{}{}", session_id, now_ms); + std::array sig; + auto seed = globals.account_seed(); + crypto_sign_ed25519_detached( + sig.data(), + nullptr, + reinterpret_cast(to_sign.data()), + to_sign.size(), + reinterpret_cast(seed.buf.data())); + + params["signature"] = oxenc::to_base64(sig); + + nlohmann::json req_body = { + {"method", "retrieve"}, + {"params", params}, + }; + + net->get_swarm(globals.pubkey_x25519(), false, [this, net, req_body](auto, auto swarm) { + if (swarm.empty()) + return; + + auto body_str = req_body.dump(); + net->send_request( + network::Request{ + swarm.front(), + "storage_rpc", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this](bool success, + bool /*timeout*/, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional body) { + if (!success || !body) + return; + + try { + auto json = nlohmann::json::parse(*body); + if (!json.contains("results") || !json["results"].is_array()) + return; + + auto conn = db.conn(); + for (const auto& res : json["results"]) { + if (!res.contains("namespace") || !res.contains("messages") || + !res["messages"].is_array()) + continue; + + auto ns_val = res["namespace"].get(); + auto ns = static_cast(ns_val); + + std::vector> messages_data; + std::string newest_hash; + + for (const auto& msg : res["messages"]) { + if (!msg.contains("data") || !msg["data"].is_string()) + continue; + auto b64_data = msg["data"].get(); + auto& decoded = messages_data.emplace_back(); + decoded.reserve(oxenc::from_base64_size(b64_data.size())); + oxenc::from_base64( + b64_data.begin(), b64_data.end(), std::back_inserter(decoded)); + + if (msg.contains("hash") && msg["hash"].is_string()) + newest_hash = msg["hash"].get(); + } + + if (!messages_data.empty()) { + if (!newest_hash.empty()) + conn.prepared_exec( + "INSERT INTO namespace_sync (namespace, last_hash) VALUES (?, ?) " + "ON CONFLICT(namespace) DO UPDATE SET last_hash = excluded.last_hash", + ns_val, newest_hash); + receive_messages(to_view_vector(messages_data), ns, true); + } + } + } catch (const std::exception& e) { + log::warning(cat, "Failed to parse poll response: {}", e.what()); + } + }); + }); +} + void Core::receive_messages( std::span> messages, config::Namespace ns, diff --git a/src/core/globals.cpp b/src/core/globals.cpp index 9b97e216e..b599750b3 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -134,11 +134,15 @@ void Globals::init() { _pubkey_ed25519.data(), reinterpret_cast(rw.buf.data()), reinterpret_cast(seed.data())); - _session_id[0] = 0x05; - if (0 != crypto_sign_ed25519_pk_to_curve25519(_session_id.data() + 1, _pubkey_ed25519.data())) + if (0 != + crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) // This *should* be impossible when starting from a seed because that would mean the seed // generation produced an invalid Ed pubkey! - log::critical(cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); + log::critical( + cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); + + _session_id[0] = 0x05; + std::copy(_pubkey_x25519.begin(), _pubkey_x25519.end(), _session_id.data() + 1); if (!have_seed) { log::info(cat, "Generated new Session account seed"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 47ba2c8da..9e5c6fc51 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -46,6 +46,7 @@ if(ENABLE_NETWORKING) list(APPEND LIB_SESSION_UTESTS_SOURCES test_onion_request_router.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_snode_pool.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_core_network.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_poll.cpp) endif() add_library(test_libs INTERFACE) diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 97663291b..a1f89345e 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -6,6 +6,9 @@ namespace session { class TestHelper { public: + static void poll(core::Core& core) { + core._poll(); + } }; } // namespace session From b9f3f21d656546807e1dd052d947df762e67dfa1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:46:55 -0300 Subject: [PATCH 032/193] update session-deps --- cmake/session-deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/session-deps b/cmake/session-deps index d66944abf..a6d07f473 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit d66944abf027dacc07d7cb0e58aae53aaecd8b8a +Subproject commit a6d07f47310bcabbe5ee194cc9f8b2d60d163b5d From 3a38ca136cd6febb179b39f6b7cb3c7fa8ebfd59 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 16:11:04 -0300 Subject: [PATCH 033/193] formatting & small code simplifications --- include/session/core.hpp | 2 +- src/core.cpp | 51 ++++++++++++++++++++++------------------ src/core/devices.cpp | 14 ++++------- src/core/globals.cpp | 6 ++--- tests/test_helper.hpp | 8 +++---- 5 files changed, 38 insertions(+), 43 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index 3b3f6cd46..dfa5f08c2 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -139,7 +139,7 @@ namespace detail { } class Core { - friend class session::TestHelper; // for unit tests + friend class session::TestHelper; // for unit tests // Constructed first (in init()), destroyed last: must outlive all components that use it. // Custom deleter allows quic::Loop to remain an incomplete type in this header. diff --git a/src/core.cpp b/src/core.cpp index cbc1ebeb6..e00c8367b 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -7,8 +9,6 @@ #include #include #include -#include -#include #include #include #include @@ -111,22 +111,22 @@ void Core::_poll() { }; net->get_swarm(globals.pubkey_x25519(), false, [this, net, req_body](auto, auto swarm) { - if (swarm.empty()) - return; - - auto body_str = req_body.dump(); - net->send_request( - network::Request{ - swarm.front(), - "storage_rpc", - to_vector(body_str), - network::RequestCategory::standard_small, - 20s}, - [this](bool success, - bool /*timeout*/, - int16_t /*status_code*/, - std::vector> /*headers*/, - std::optional body) { + if (swarm.empty()) + return; + + auto body_str = req_body.dump(); + net->send_request( + network::Request{ + swarm.front(), + "storage_rpc", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this](bool success, + bool /*timeout*/, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional body) { if (!success || !body) return; @@ -154,7 +154,9 @@ void Core::_poll() { auto& decoded = messages_data.emplace_back(); decoded.reserve(oxenc::from_base64_size(b64_data.size())); oxenc::from_base64( - b64_data.begin(), b64_data.end(), std::back_inserter(decoded)); + b64_data.begin(), + b64_data.end(), + std::back_inserter(decoded)); if (msg.contains("hash") && msg["hash"].is_string()) newest_hash = msg["hash"].get(); @@ -163,9 +165,12 @@ void Core::_poll() { if (!messages_data.empty()) { if (!newest_hash.empty()) conn.prepared_exec( - "INSERT INTO namespace_sync (namespace, last_hash) VALUES (?, ?) " - "ON CONFLICT(namespace) DO UPDATE SET last_hash = excluded.last_hash", - ns_val, newest_hash); + R"( +INSERT INTO namespace_sync (namespace, last_hash) VALUES (?, ?) +ON CONFLICT(namespace) DO UPDATE SET last_hash = excluded.last_hash +)", + ns_val, + newest_hash); receive_messages(to_view_vector(messages_data), ns, true); } } @@ -173,7 +178,7 @@ void Core::_poll() { log::warning(cat, "Failed to parse poll response: {}", e.what()); } }); - }); + }); } void Core::receive_messages( diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 72ccb0334..2c272605d 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -561,10 +561,7 @@ namespace { e.append("c", k.created); if (k.rotated) e.append("r", *k.rotated); - e.append( - "s", - std::string_view{ - reinterpret_cast(k.seed.data()), k.seed.size()}); + e.append("s", k.seed); } } @@ -575,10 +572,7 @@ namespace { std::span device_id, const device::Info& info) { oxenc::bt_dict_producer out; // "I" (device id) sorts before "i" (info dict) - out.append( - "I", - std::string_view{ - reinterpret_cast(device_id.data()), device_id.size()}); + out.append("I", device_id); encode_device_info(out.append_dict("i"), info); return std::move(out).str(); } @@ -1250,9 +1244,9 @@ void Devices::receive_link_request(std::span data) { // Skip any unknown keys between "I" and "i" oxenc::bt_dict extra_outer; - while (!pt.is_finished() && std::string_view{pt.key()} < "i") + while (!pt.is_finished() && pt.key() < "i") consume_extra(pt, extra_outer); - if (pt.is_finished() || std::string_view{pt.key()} != "i") + if (pt.is_finished() || pt.key() != "i") throw std::runtime_error{"missing 'i' device info dict"}; decode_one(info, pt.consume_dict_consumer(), device::State::Pending); } catch (const std::exception& e) { diff --git a/src/core/globals.cpp b/src/core/globals.cpp index b599750b3..bb336cb3d 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -134,12 +134,10 @@ void Globals::init() { _pubkey_ed25519.data(), reinterpret_cast(rw.buf.data()), reinterpret_cast(seed.data())); - if (0 != - crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) + if (0 != crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) // This *should* be impossible when starting from a seed because that would mean the seed // generation produced an invalid Ed pubkey! - log::critical( - cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); + log::critical(cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); _session_id[0] = 0x05; std::copy(_pubkey_x25519.begin(), _pubkey_x25519.end(), _session_id.data() + 1); diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index a1f89345e..a56558f99 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -5,10 +5,8 @@ namespace session { class TestHelper { -public: - static void poll(core::Core& core) { - core._poll(); - } + public: + static void poll(core::Core& core) { core._poll(); } }; -} // namespace session +} // namespace session From 04f1e624a5fb402f7fc4dd1cca4cf11fa3b45385 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 16:14:05 -0300 Subject: [PATCH 034/193] add missing clock header --- include/session/clock.hpp | 103 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 include/session/clock.hpp diff --git a/include/session/clock.hpp b/include/session/clock.hpp new file mode 100644 index 000000000..434f7daf6 --- /dev/null +++ b/include/session/clock.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include + +namespace session { + +/// A clock satisfying the C++ Clock named requirement whose time_point is identical to +/// std::chrono::system_clock::time_point. Returns system time adjusted by a configurable offset, +/// making it suitable for both production use (where the offset is learned from the network) and +/// unit testing (where the offset can be set to any desired value). +struct AdjustedClock { + using duration = std::chrono::system_clock::duration; + using rep = duration::rep; + using period = duration::period; + using time_point = std::chrono::system_clock::time_point; + static constexpr bool is_steady = false; + + /// Returns the current time, adjusted by the current offset. + static time_point now() noexcept { + return std::chrono::system_clock::now() + duration{_offset.load(std::memory_order_relaxed)}; + } + + /// Sets the clock offset. Accepts any duration implicitly convertible to + /// system_clock::duration (e.g. std::chrono::milliseconds, seconds, nanoseconds). + static void set_offset(duration offset) { + _offset.store(offset.count(), std::memory_order_relaxed); + } + + /// Returns the current clock offset. + static duration get_offset() { return duration{_offset.load(std::memory_order_relaxed)}; } + + private: + inline static std::atomic _offset{0}; +}; + +// Returns the current time from AdjustedClock, optionally floored to the given precision. +// E.g. clock_now() gives a timepoint with seconds precision (aka +// std::chrono::sys_seconds). +template +inline std::chrono::sys_time clock_now() { + return std::chrono::floor(AdjustedClock::now()); +} +// Shortcut for clock_now(); +inline std::chrono::sys_seconds clock_now_s() { + return clock_now(); +} +using sys_ms = std::chrono::sys_time; +// Shortcut for clock_now(); +inline sys_ms clock_now_ms() { + return clock_now(); +} + +// Returns the duration count of the given duration cast into ToDuration. Example: +// duration_count(30000ms) // returns 30 +// This function requires that the target type is no more precise than d, that is, it will not allow +// you to cast from seconds to milliseconds because such a cast indicates that the sub-second +// precision has already been lost. +template + requires std::is_convertible_v> +constexpr int64_t duration_count(const std::chrono::duration& d) { + return std::chrono::duration_cast(d).count(); +} +// Returns the seconds count of the given duration +template + requires std::is_convertible_v> +constexpr int64_t duration_seconds(const std::chrono::duration& d) { + return duration_count(d); +} +// Returns the milliseconds count of the given duration +template + requires std::is_convertible_v> +constexpr int64_t duration_ms(const std::chrono::duration& d) { + return duration_count(d); +} + +// Returns the time-since-epoch count of the given time point, cast into ToDuration. The given time +// point must be at least as precise as ToDuration, i.e. this will not allow you to cast to a more +// precise time point as that would mean the intended precision has already been lost by an earlier +// cast. +template + requires std::is_convertible_v +constexpr int64_t epoch_count(const std::chrono::time_point& t) { + return duration_count(t.time_since_epoch()); +} +// Returns the seconds-since-epoch count of the given time point. The given time point must be at +// least as precise as seconds. +template + requires std::is_convertible_v +constexpr int64_t epoch_seconds(const std::chrono::time_point& t) { + return duration_seconds(t.time_since_epoch()); +} +// Returns the milliseconds-since-epoch count of the given time point. The given time point must +// have at least milliseconds precision. +template + requires std::is_convertible_v +constexpr int64_t epoch_ms(const std::chrono::time_point& t) { + return duration_ms(t.time_since_epoch()); +} + +} // namespace session From f799b2a7df31a6a8480c5799c0d0688d33d68a4a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:00:46 -0300 Subject: [PATCH 035/193] Devices tests; various fixes Adds initial devices tests to test some basic device key and account key handling, and fixes various bugs found along the way. --- external/session-sqlite | 2 +- include/session/core/devices.hpp | 5 + src/core/devices.cpp | 17 +- src/core/schema/000_devices.sql | 2 +- src/core/schema/apply_schema.cpp.in | 2 +- tests/CMakeLists.txt | 1 + tests/test_core_devices.cpp | 399 ++++++++++++++++++++++++++++ tests/test_helper.hpp | 15 ++ 8 files changed, 432 insertions(+), 11 deletions(-) create mode 100644 tests/test_core_devices.cpp diff --git a/external/session-sqlite b/external/session-sqlite index e3105baec..f96ae2041 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit e3105baec20ca7a2c8f5e9ad50817e6a24ee2bf4 +Subproject commit f96ae20411e75eed45a12abbfaed469ac456b011 diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 0f0848e78..2e7980877 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -17,6 +17,10 @@ #include "component.hpp" +namespace session { +class TestHelper; +} // namespace session + namespace session::core { using namespace std::literals; @@ -126,6 +130,7 @@ class Devices final : detail::CoreComponent { public: private: friend class Core; + friend class session::TestHelper; explicit Devices(Core& c) : detail::CoreComponent{c} {} void init() override; diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 2c272605d..e4be4ce87 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -77,7 +77,7 @@ static Keys keys_from_seed(std::span seed) { crypto_xof_turboshake256_update( &st, reinterpret_cast(seed.data()), seed.size()); crypto_xof_turboshake256_squeeze(&st, x_sec.data(), x_sec.size()); - crypto_scalarmult_curve25519_base(x_pub.data(), x_pub.data()); + crypto_scalarmult_curve25519_base(x_pub.data(), x_sec.data()); static_assert(MLKEM768_PUBLICKEYBYTES == sizeof(ml_pub)); static_assert(MLKEM768_SECRETKEYBYTES == sizeof(ml_sec)); @@ -170,13 +170,14 @@ void Devices::rotate_account_keys() { std::vector Devices::active_device_keys() { std::vector keys; auto c = conn(); - SQLite::Transaction tx{c.sql}; bool have_active = false; - for (auto [seed, active] : c.prepared_results, int>( - "SELECT seed, rotated IS NULL FROM device_privkeys" + for (auto [seed, rotated] : c.prepared_results, std::optional>( + "SELECT seed, rotated FROM device_privkeys" " ORDER BY rotated DESC NULLS FIRST, created DESC")) { - keys.push_back(keys_from_seed(seed)); - if (active) + auto& k = keys.emplace_back(keys_from_seed(seed)); + if (rotated) + k.rotated.emplace(std::chrono::seconds{*rotated}); + else have_active = true; } @@ -214,8 +215,8 @@ std::vector Devices::active_account_keys() { k.rotated.emplace(std::chrono::seconds{*rotated}); if (!rotated) have_active = true; - if (0 != std::memcmp(k.mlkem768_pub.data(), pk_ml.data(), pk_ml.size()) || - 0 != std::memcmp(k.x25519_pub.data(), pk_x.data(), pk_x.size())) { + if (std::memcmp(k.mlkem768_pub.data(), pk_ml.data(), pk_ml.size()) != 0 || + std::memcmp(k.x25519_pub.data(), pk_x.data(), pk_x.size()) != 0) { log::warning( cat, "device_account_keys row with id={} ignored: row contains invalid precomputed " diff --git a/src/core/schema/000_devices.sql b/src/core/schema/000_devices.sql index c8b2772e0..148587417 100644 --- a/src/core/schema/000_devices.sql +++ b/src/core/schema/000_devices.sql @@ -49,7 +49,7 @@ CREATE TABLE device_privkeys ( id INTEGER PRIMARY KEY NOT NULL, created INTEGER NOT NULL, -- unix timestamp rotated INTEGER, -- timestamp when a newer key was added, superceding this key - seed BLOB NOT NULL CHECK(length(seed) == 32), + seed BLOB NOT NULL CHECK(length(seed) == 32) ) STRICT; -- This trigger handles key rotation: whenever we insert a new key, any existing keys are diff --git a/src/core/schema/apply_schema.cpp.in b/src/core/schema/apply_schema.cpp.in index 38c9f65fe..048b0b913 100644 --- a/src/core/schema/apply_schema.cpp.in +++ b/src/core/schema/apply_schema.cpp.in @@ -6,7 +6,7 @@ namespace session::core { class Core; } namespace session::core::schema { void @FUNC_NAME@(session::sqlite::Connection& conn, Core&) { - session::sqlite::exec_query(conn.sql, R"_SQL_DELIM_( + conn.sql.exec(R"_SQL_DELIM_( @SCHEMA_SQL@ )_SQL_DELIM_"); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9e5c6fc51..1d0d8864f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release") endif() set(LIB_SESSION_UTESTS_SOURCES + test_core_devices.cpp test_attachment_encrypt.cpp test_blinding.cpp test_bt_merge.cpp diff --git a/tests/test_core_devices.cpp b/tests/test_core_devices.cpp new file mode 100644 index 000000000..e627ff3ad --- /dev/null +++ b/tests/test_core_devices.cpp @@ -0,0 +1,399 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_helper.hpp" +#include "utils.hpp" + +using namespace session; +using namespace session::core; +using namespace std::literals; + +static constexpr std::array test_key_bytes{}; + +// Smart-pointer-like wrapper around a unique_ptr with RAII cleanup of the temp DB file. +struct TempCore { + std::filesystem::path path; + std::unique_ptr core; + + explicit TempCore(core::callbacks cb = {}) : + path{[] { + static std::atomic n{0}; + return std::filesystem::temp_directory_path() / + fmt::format("test_core_devices_{}.db", ++n); + }()}, + core{std::make_unique(std::move(cb), path, sqlite::raw_key{test_key_bytes})} {} + + ~TempCore() { + core.reset(); // close DB before removing the file + std::error_code ec; + std::filesystem::remove(path, ec); + } + + Core* operator->() { return core.get(); } + Core& operator*() { return *core; } +}; + +TEST_CASE("Devices - identity", "[core][devices]") { + TempCore c; + + SECTION("device_id is 64-char hex") { + auto id = c->devices.device_id(); + REQUIRE(id.size() == 64); + CHECK(std::all_of(id.begin(), id.end(), [](char ch) { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'); + })); + } + + SECTION("device_id is stable") { + CHECK(c->devices.device_id() == c->devices.device_id()); + } + + SECTION("two independent cores have different device IDs") { + TempCore c2; + CHECK(c->devices.device_id() != c2->devices.device_id()); + } +} + +TEST_CASE("Devices - initial state", "[core][devices]") { + TempCore c; + + SECTION("device_info defaults") { + auto [info, is_registered] = c->devices.device_info(); + // seqno == 0 is the sentinel meaning no row exists yet + CHECK(info.seqno == 0); + CHECK_FALSE(is_registered); + } + + SECTION("devices() is empty") { + CHECK(c->devices.devices(true, true, true).empty()); + } + + SECTION("needs_push is false") { + auto np = c->devices.needs_push(); + CHECK_FALSE(np.device_group); + CHECK_FALSE(np.account_pubkey); + } +} + +TEST_CASE("Devices - update_info and same_user_fields", "[core][devices]") { + TempCore c; + + SECTION("update_info persists fields and sets seqno=1") { + device::Info info{}; + info.type = device::Type::Session_iOS; + info.description = "test phone"; + info.version = {1, 2, 3}; + + c->devices.update_info(info); + + auto [got, is_registered] = c->devices.device_info(); + CHECK(got.seqno == 1); + CHECK(got.type == device::Type::Session_iOS); + CHECK(got.description == "test phone"); + CHECK(got.version == std::array{1, 2, 3}); + CHECK(got.state == device::State::Unregistered); + CHECK_FALSE(is_registered); + } + + SECTION("identical update does not bump seqno") { + device::Info info{}; + info.type = device::Type::Session_Desktop; + info.description = "desktop"; + info.version = {0, 1, 0}; + + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 1); + + c->devices.update_info(info); // identical — should not bump + CHECK(c->devices.device_info().first.seqno == 1); + } + + SECTION("changed description bumps seqno") { + device::Info info{}; + info.description = "first"; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 1); + + info.description = "second"; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 2); + } + + SECTION("changed type bumps seqno") { + device::Info info{}; + info.type = device::Type::Session_Android; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 1); + + info.type = device::Type::Session_Desktop; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 2); + } + + SECTION("changed version bumps seqno") { + device::Info info{}; + info.version = {1, 0, 0}; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 1); + + info.version = {2, 0, 0}; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 2); + } + + SECTION("extra fields round-trip and participate in comparison") { + device::Info info{}; + info.extra["custom_key"] = std::string{"hello"}; + c->devices.update_info(info); + + auto [got, _] = c->devices.device_info(); + CHECK(got.seqno == 1); + REQUIRE(got.extra.count("custom_key")); + CHECK(std::get(got.extra.at("custom_key")) == "hello"); + + // Same extra — no bump + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 1); + + // Changed extra — bump + info.extra["custom_key"] = std::string{"world"}; + c->devices.update_info(info); + CHECK(c->devices.device_info().first.seqno == 2); + } + + SECTION("same_user_fields ignores state/seqno/pk_*") { + device::Info a{}, b{}; + a.type = device::Type::Session_iOS; + a.description = "foo"; + a.version = {1, 2, 3}; + b = a; + + CHECK(a.same_user_fields(b)); + + // Differ in seqno — should still be "same" user fields + b.seqno = 99; + CHECK(a.same_user_fields(b)); + + // Differ in description — not same + b.seqno = a.seqno; + b.description = "bar"; + CHECK_FALSE(a.same_user_fields(b)); + } + + SECTION("update_info device appears in devices(include_unregistered=true)") { + device::Info info{}; + info.description = "my device"; + c->devices.update_info(info); + + auto devs = c->devices.devices(false, false, true); + CHECK(devs.size() == 1); + CHECK(devs.begin()->second.description == "my device"); + } +} + +TEST_CASE("Devices - device keys", "[core][devices]") { + TempCore c; + + SECTION("active_device_keys returns at least one key with correct sizes") { + auto keys = c->devices.active_device_keys(); + REQUIRE_FALSE(keys.empty()); + CHECK(keys.front().x25519_pub.size() == 32); + CHECK(keys.front().mlkem768_pub.size() == 1184); + CHECK_FALSE(keys.front().rotated.has_value()); + } + + SECTION("rotate_device_keys produces a distinct key") { + auto before = c->devices.active_device_keys(); + REQUIRE_FALSE(before.empty()); + + c->devices.rotate_device_keys(); + auto after = c->devices.active_device_keys(); + + CHECK(after.front().x25519_pub != before.front().x25519_pub); + CHECK(after.front().mlkem768_pub != before.front().mlkem768_pub); + CHECK_FALSE(after.front().rotated.has_value()); + } + + SECTION("after one rotation active_device_keys has two entries") { + auto initial = c->devices.active_device_keys(); // ensure initial key exists + REQUIRE(initial.size() == 1); + c->devices.rotate_device_keys(); + auto keys = c->devices.active_device_keys(); + CHECK(keys.size() == 2); + CHECK_FALSE(keys.front().rotated.has_value()); + CHECK(keys.back().rotated.has_value()); + } + + SECTION("after two rotations active_device_keys has three entries") { + c->devices.active_device_keys(); // ensure initial key exists + c->devices.rotate_device_keys(); + c->devices.rotate_device_keys(); + auto keys = c->devices.active_device_keys(); + CHECK(keys.size() == 3); + CHECK_FALSE(keys[0].rotated.has_value()); + CHECK(keys[1].rotated.has_value()); + CHECK(keys[2].rotated.has_value()); + } +} + +TEST_CASE("Devices - account keys", "[core][devices]") { + TempCore c; + + SECTION("active_account_keys returns at least one key with correct sizes") { + auto keys = c->devices.active_account_keys(); + REQUIRE_FALSE(keys.empty()); + CHECK(keys.front().x25519_pub.size() == 32); + CHECK(keys.front().mlkem768_pub.size() == 1184); + CHECK_FALSE(keys.front().rotated.has_value()); + } + + SECTION("rotate_account_keys produces a distinct key: newer timestamp wins") { + auto before = c->devices.active_account_keys(); + REQUIRE(before.size() == 1); + + // Advance clock by 1s so the new key has a strictly later created timestamp and + // deterministically wins tie-breaking (created DESC, seed ASC). + ScopedClockOffset adv{1s}; + c->devices.rotate_account_keys(); + auto after = c->devices.active_account_keys(); + + REQUIRE(after.size() == 2); + CHECK_FALSE(after.front().rotated.has_value()); + CHECK(after.back().rotated.has_value()); + CHECK(after.front().x25519_pub != before.front().x25519_pub); + } + + SECTION("rotate_account_keys produces a distinct key: same timestamp, seed tiebreak") { + // Snap the adjusted clock to the start of the next second so both key-creation calls + // land in the same second with no risk of spanning a second boundary. + ScopedClockOffset pin{(clock_now_s() + 1s) - std::chrono::system_clock::now()}; + + c->devices.active_account_keys(); // ensure initial key exists at pinned second + c->devices.rotate_account_keys(); // new key created at same second + auto keys = c->devices.active_account_keys(); + REQUIRE(keys.size() == 2); + CHECK_FALSE(keys.front().rotated.has_value()); + CHECK(keys.back().rotated.has_value()); + + // Look up each key's seed via its x25519 pubkey and verify the tie-breaking rule: + // the active key must have the lexicographically smaller seed. + auto active_seed = TestHelper::account_key_seed(c->devices, keys.front().x25519_pub); + auto rotated_seed = TestHelper::account_key_seed(c->devices, keys.back().x25519_pub); + CHECK(active_seed < rotated_seed); + } + + SECTION("after one rotation active_account_keys has two entries") { + c->devices.active_account_keys(); // ensure initial key exists + c->devices.rotate_account_keys(); + auto keys = c->devices.active_account_keys(); + CHECK(keys.size() == 2); + CHECK_FALSE(keys.front().rotated.has_value()); + CHECK(keys.back().rotated.has_value()); + } + + SECTION("old key pruned after ACCOUNT_KEY_RETENTION") { + c->devices.active_account_keys(); // ensure initial key exists + c->devices.rotate_account_keys(); + { + auto keys = c->devices.active_account_keys(); + CHECK(keys.size() == 2); + } + + // Advance clock past retention window: old rotated key should be pruned + ScopedClockOffset adv{Devices::ACCOUNT_KEY_RETENTION + 1s}; + auto keys = c->devices.active_account_keys(); + CHECK(keys.size() == 1); + CHECK_FALSE(keys.front().rotated.has_value()); + } + + SECTION("next_account_rotation returns nullopt when not in device group") { + CHECK_FALSE(c->devices.next_account_rotation().has_value()); + CHECK_FALSE(c->devices.account_rotation_due()); + } + + SECTION("next_device_rotation returns nullopt when not in device group") { + CHECK_FALSE(c->devices.next_device_rotation().has_value()); + CHECK_FALSE(c->devices.device_rotation_due()); + } +} + +TEST_CASE("Devices - build_link_request", "[core][devices]") { + TempCore c; + + SECTION("returns non-empty message and 21-entry SAS") { + auto result = c->devices.build_link_request(); + CHECK_FALSE(result.message.empty()); + CHECK(result.sas.size() == 21); + for (const auto& s : result.sas) + CHECK_FALSE(s.empty()); + } + + SECTION("consecutive calls produce different messages") { + auto r1 = c->devices.build_link_request(); + auto r2 = c->devices.build_link_request(); + CHECK(r1.message != r2.message); + } +} + +TEST_CASE("Devices - build_account_pubkey_message", "[core][devices]") { + TempCore c; + + SECTION("non-empty output with correct structure") { + auto msg = c->devices.build_account_pubkey_message(); + REQUIRE_FALSE(msg.empty()); + + auto dict = oxenc::bt_dict_consumer{msg}; + + // "M" — mlkem768 pubkey (1184 bytes) + CHECK(dict.require("M").size() == 1184); + + // "X" — x25519 pubkey (32 bytes) + CHECK(dict.require("X").size() == 32); + + // "~" — XEd25519 signature (64 bytes) + CHECK(dict.require("~").size() == 64); + } + + SECTION("M and X match active account keys") { + auto keys = c->devices.active_account_keys(); + REQUIRE_FALSE(keys.empty()); + + auto msg = c->devices.build_account_pubkey_message(); + auto dict = oxenc::bt_dict_consumer{msg}; + + auto M = dict.require("M"); + auto X = dict.require("X"); + + CHECK(std::memcmp(M.data(), keys.front().mlkem768_pub.data(), 1184) == 0); + CHECK(std::memcmp(X.data(), keys.front().x25519_pub.data(), 32) == 0); + } + + SECTION("signature verifies against account x25519 pubkey") { + auto msg = c->devices.build_account_pubkey_message(); + auto dict = oxenc::bt_dict_consumer{msg}; + + dict.require("M"); + dict.require("X"); + + // Use require_signature to correctly extract the signed body (everything in the dict + // before the "~" key) and the signature value. + auto x25519_pub = c->globals.session_id().template subspan<1>(); // skip 0x05 prefix + bool sig_valid = false; + dict.require_signature( + "~", [&](std::span body, std::span sig) { + sig_valid = xed25519::verify(sig, x25519_pub, body); + }); + CHECK(sig_valid); + } +} diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index a56558f99..f37812146 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -1,12 +1,27 @@ #pragma once #include +#include +#include +#include namespace session { class TestHelper { public: static void poll(core::Core& core) { core._poll(); } + + // Returns the raw 32-byte seed for the account key identified by the given x25519 public key. + static cleared_b32 account_key_seed( + core::Devices& d, std::span x25519_pub) { + cleared_b32 seed; + auto c = d.conn(); + auto blob = c.prepared_get>>( + "SELECT seed FROM device_account_keys WHERE pubkey_x25519 = ?", + std::as_bytes(x25519_pub)); + std::ranges::copy(blob, seed.begin()); + return seed; + } }; } // namespace session From d20ade5df31fc5b77a842c0a05530b54a3554c53 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:09:10 -0300 Subject: [PATCH 036/193] add missing files --- src/core/schema/000_namespaces.sql | 4 + tests/test_poll.cpp | 140 +++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/core/schema/000_namespaces.sql create mode 100644 tests/test_poll.cpp diff --git a/src/core/schema/000_namespaces.sql b/src/core/schema/000_namespaces.sql new file mode 100644 index 000000000..6e1819601 --- /dev/null +++ b/src/core/schema/000_namespaces.sql @@ -0,0 +1,4 @@ +CREATE TABLE namespace_sync ( + namespace INTEGER PRIMARY KEY NOT NULL, + last_hash TEXT NOT NULL +) STRICT; diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp new file mode 100644 index 000000000..4f886314a --- /dev/null +++ b/tests/test_poll.cpp @@ -0,0 +1,140 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "test_helper.hpp" +#include "utils.hpp" + +using namespace session; + +class MockNetwork : public network::Network { + public: + MockNetwork() : network::Network(network::config::Config{}) {} + + struct SentRequest { + network::Request request; + network::network_response_callback_t callback; + }; + std::vector sent_requests; + + void send_request( + network::Request request, network::network_response_callback_t callback) override { + sent_requests.push_back({std::move(request), std::move(callback)}); + } + + void get_swarm( + session::network::x25519_pubkey /*swarm_pubkey*/, + bool /*ignore_strike_count*/, + std::function< + void(network::swarm_id_t swarm_id, std::vector swarm)> + callback) override { + // Return a dummy swarm + std::vector swarm; + network::service_node node; + node.remote_pubkey = network::ed25519_pubkey{}; // zeroed key + swarm.push_back(node); + callback(0, swarm); + } +}; + +TEST_CASE("Core automatic polling", "[core][poll]") { + auto db_path = std::filesystem::temp_directory_path() / "test_poll.db"; + if (std::filesystem::exists(db_path)) + std::filesystem::remove(db_path); + + auto test_keys = get_deterministic_test_keys(); + + bool received = false; + core::callbacks callbacks; + callbacks.device_link_request = [&](int, + const core::device::Info&, + std::span) { received = true; }; + + { + core::Core core{callbacks, db_path, sqlite::argon2id_password{"test"}}; + auto mock_net = std::make_shared(); + + core.set_network(mock_net); + + // Trigger poll via TestHelper + TestHelper::poll(core); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto& sent = mock_net->sent_requests[0]; + + auto req_json = nlohmann::json::parse(*sent.request.body); + CHECK(req_json["method"] == "retrieve"); + auto& params = req_json["params"]; + CHECK(params["pubkey"] == oxenc::to_hex(core.globals.session_id())); + CHECK(params["namespaces"] == nlohmann::json::array({21, -21})); + CHECK(params.contains("timestamp")); + CHECK(params.contains("signature")); + + // Prepare a valid encrypted link request message using the core's actual seed + std::vector outer_msg; + { + // Plaintext: {"I": , "i": {"n": "test", "p": , "k": }} + std::vector plaintext; + oxenc::bt_dict_producer p; + auto dev_id = + "0101010101010101010101010101010101010101010101010101010101010101"_hexbytes; + p.append("I", dev_id); + { + auto sub = p.append_dict("i"); + sub.append("k", test_keys.ed_pk1); + sub.append("n", "test device"); + sub.append("p", test_keys.curve_pk1); + } + auto p_span = p.span(); + plaintext.assign(p_span.begin(), p_span.end()); + + // Encrypt with the core's actual seed + auto core_seed = core.globals.account_seed(); + auto seed_span = session::to_span(core_seed.buf).first(32); + auto encrypted = config::encrypt(plaintext, seed_span, "link-request"); + + // Outer: {"": "L", "L": } + oxenc::bt_dict_producer outer; + outer.append("", "L"); + outer.append("L", encrypted); + auto outer_span = outer.span(); + outer_msg.assign(outer_span.begin(), outer_span.end()); + } + + // Prepare a mock response + nlohmann::json response; + response["results"] = nlohmann::json::array(); + nlohmann::json res_item; + res_item["namespace"] = 21; + res_item["messages"] = nlohmann::json::array(); + nlohmann::json msg_item; + msg_item["data"] = oxenc::to_base64(outer_msg); + msg_item["hash"] = "hash1"; + res_item["messages"].push_back(msg_item); + response["results"].push_back(res_item); + + sent.callback(true, false, 200, {}, response.dump()); + + // Verify last_hash was updated in DB + CHECK(core.globals.get_text("_last_hash_21") == "hash1"); + CHECK(received); + + // Poll again, should include last_hash + mock_net->sent_requests.clear(); + TestHelper::poll(core); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto req_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + CHECK(req_json2["params"]["last_hashes"]["21"] == "hash1"); + } + + if (std::filesystem::exists(db_path)) + std::filesystem::remove(db_path); +} From 5cda501dbeca935bfbdc21cce4dbc28b3d101367 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:15:55 -0300 Subject: [PATCH 037/193] fix merge conflict --- src/core.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core.cpp b/src/core.cpp index e00c8367b..a8bae4922 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -81,7 +82,7 @@ void Core::_poll() { last_hashes[std::to_string(ns_val)] = *last_hash; } - auto now_ms = epoch_ms(sysclock_now_ms()); + auto now_ms = epoch_ms(clock_now_ms()); auto session_id = oxenc::to_hex(globals.session_id()); nlohmann::json params = { From 473812f8e85c94477d7361f39ae918e1d846f014 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:32:31 -0300 Subject: [PATCH 038/193] Update session-deps --- cmake/session-deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/session-deps b/cmake/session-deps index a6d07f473..3763407c5 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit a6d07f47310bcabbe5ee194cc9f8b2d60d163b5d +Subproject commit 3763407c57d4fefce2b6e764be709d7af9a951c3 From 7133598e27a66fac4d4a66d5e182588e0ffe9d36 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 14:19:36 -0300 Subject: [PATCH 039/193] Add Electrum word lists --- include/session/mnemonics.hpp | 100 + src/CMakeLists.txt | 1 + src/core.cpp | 2 +- src/mnemonics/CMakeLists.txt | 114 ++ src/mnemonics/languages/README.md | 46 + .../languages/chinese_simplified.txt | 1629 +++++++++++++++++ src/mnemonics/languages/dutch.txt | 1629 +++++++++++++++++ src/mnemonics/languages/english.txt | 1629 +++++++++++++++++ src/mnemonics/languages/esperanto.txt | 1629 +++++++++++++++++ src/mnemonics/languages/french.txt | 1629 +++++++++++++++++ src/mnemonics/languages/german.txt | 1629 +++++++++++++++++ src/mnemonics/languages/italian.txt | 1629 +++++++++++++++++ src/mnemonics/languages/japanese.txt | 1629 +++++++++++++++++ src/mnemonics/languages/lojban.txt | 1629 +++++++++++++++++ src/mnemonics/languages/portuguese.txt | 1629 +++++++++++++++++ src/mnemonics/languages/russian.txt | 1629 +++++++++++++++++ src/mnemonics/languages/spanish.txt | 1629 +++++++++++++++++ src/mnemonics/mnemonics.cpp | 189 ++ tests/CMakeLists.txt | 1 + tests/test_mnemonics.cpp | 129 ++ utils/verify_mnemonics.py | 78 + 21 files changed, 20207 insertions(+), 1 deletion(-) create mode 100644 include/session/mnemonics.hpp create mode 100644 src/mnemonics/CMakeLists.txt create mode 100644 src/mnemonics/languages/README.md create mode 100644 src/mnemonics/languages/chinese_simplified.txt create mode 100644 src/mnemonics/languages/dutch.txt create mode 100644 src/mnemonics/languages/english.txt create mode 100644 src/mnemonics/languages/esperanto.txt create mode 100644 src/mnemonics/languages/french.txt create mode 100644 src/mnemonics/languages/german.txt create mode 100644 src/mnemonics/languages/italian.txt create mode 100644 src/mnemonics/languages/japanese.txt create mode 100644 src/mnemonics/languages/lojban.txt create mode 100644 src/mnemonics/languages/portuguese.txt create mode 100644 src/mnemonics/languages/russian.txt create mode 100644 src/mnemonics/languages/spanish.txt create mode 100644 src/mnemonics/mnemonics.cpp create mode 100644 tests/test_mnemonics.cpp create mode 100644 utils/verify_mnemonics.py diff --git a/include/session/mnemonics.hpp b/include/session/mnemonics.hpp new file mode 100644 index 000000000..559ff78d6 --- /dev/null +++ b/include/session/mnemonics.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace session::mnemonics { + +/** + * The number of words in each mnemonic language word list. + * + * This value (1626) is chosen because 1626^24 is just enough to represent a 256-bit (32-byte) + * random value. + * + * The math: + * log2(1626^24) = 24 * log2(1626) ≈ 24 * 10.6669... ≈ 256.006... bits. + * + * Thus, 24 words from a 1626-word dictionary can represent 2^256 states with a tiny amount of + * extra space that is simply unused. + */ +constexpr size_t NWORDS = 1626; + +/** + * A struct containing information about a mnemonic language. + * + * All string values (english_name, native_name, and words) are encoded in UTF-8. + * + * prefix_len represents the number of unique UTF-8 codepoints (not bytes) required + * to uniquely identify a word in this language. + */ +struct Mnemonics { + std::string_view english_name; + std::string_view native_name; + int prefix_len; + std::array words; +}; + +/** + * Returns a list of all supported mnemonic languages. + * English is always the first element, followed by other languages sorted by name. + */ +std::span get_languages(); + +/** + * Finds a language by its English or native name. + * + * @param name The name to look for. + * @return A pointer to the Mnemonics struct if found, otherwise nullptr. + */ +const Mnemonics* find_language(std::string_view name); + +/** + * Converts a byte span to a mnemonic word list using the specified language. + * + * @param bytes The input byte span. Its length must be a multiple of 4. + * @param lang The language to use for the mnemonic. + * @return A vector of words representing the input bytes. + * @throws std::invalid_argument if the input length is not a multiple of 4. + */ +std::vector bytes_to_words( + std::span bytes, const Mnemonics& lang); + +/** + * Converts a byte span to a mnemonic word list using the specified language. + * + * @param bytes The input byte span. Its length must be a multiple of 4. + * @param lang_name The name of the language (English or native) to use. + * @return A vector of words representing the input bytes. + * @throws std::invalid_argument if the language is unknown or the input length is invalid. + */ +std::vector bytes_to_words( + std::span bytes, std::string_view lang_name); + +/** + * Converts a mnemonic word list to a byte span using the specified language. + * + * @param words The input word list. Its length must be a multiple of 3. + * @param lang The language used for the mnemonic. + * @return A vector of bytes representing the input mnemonic. + * @throws std::invalid_argument if the input length is not a multiple of 3, or if a word + * is not found in the language dictionary. + */ +std::vector words_to_bytes( + std::span words, const Mnemonics& lang); + +/** + * Converts a mnemonic word list to a byte span using the specified language. + * + * @param words The input word list. Its length must be a multiple of 3. + * @param lang_name The name of the language (English or native) used. + * @return A vector of bytes representing the input mnemonic. + * @throws std::invalid_argument if the language is unknown or the input is invalid. + */ +std::vector words_to_bytes( + std::span words, std::string_view lang_name); + +} // namespace session::mnemonics diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 864665488..b38560638 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -110,6 +110,7 @@ add_libsession_util_library(core core/pro.cpp ) add_subdirectory(core/schema) +add_subdirectory(mnemonics) target_link_libraries( core diff --git a/src/core.cpp b/src/core.cpp index a8bae4922..b198ac901 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -9,11 +9,11 @@ #include #include #include +#include #include #include #include #include -#include #include #include diff --git a/src/mnemonics/CMakeLists.txt b/src/mnemonics/CMakeLists.txt new file mode 100644 index 000000000..15f8f1e34 --- /dev/null +++ b/src/mnemonics/CMakeLists.txt @@ -0,0 +1,114 @@ +# Watch the languages directory so CMake re-runs if files are added/removed: +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "languages") + +file(GLOB LANG_FILES "languages/*.txt") +list(SORT LANG_FILES) + +# Reorder to put english first +set(LANG_FILES_ORDERED "") +set(ENGLISH_FILE "") +foreach(f IN LISTS LANG_FILES) + get_filename_component(filename "${f}" NAME_WE) + if(filename STREQUAL "english") + set(ENGLISH_FILE "${f}") + else() + list(APPEND LANG_FILES_ORDERED "${f}") + endif() +endforeach() + +if(NOT ENGLISH_FILE STREQUAL "") + set(LANG_FILES "${ENGLISH_FILE}" ${LANG_FILES_ORDERED}) +else() + set(LANG_FILES ${LANG_FILES_ORDERED}) +endif() + +set(MNEMONIC_SOURCES "") +set(LANG_EXTERNS "") +set(LANG_LIST "") +set(LANG_COUNT 0) + +foreach(f IN LISTS LANG_FILES) + get_filename_component(lang_var "${f}" NAME_WE) + + # Read file and split by newline manually to avoid semicolon/parentheses issues + file(READ "${f}" RAW_CONTENT) + string(REPLACE "\n" ";" LINES "${RAW_CONTENT}") + + list(LENGTH LINES LINE_COUNT) + # Check if we have exactly 1629 lines. + # If the file ends with a newline, file(READ) + string(REPLACE) might produce 1630 elements. + # We'll normalize this by checking for an empty last element if LINE_COUNT is 1630. + if(LINE_COUNT EQUAL 1630) + list(GET LINES 1629 LAST_LINE) + if(LAST_LINE STREQUAL "") + list(REMOVE_AT LINES 1629) + set(LINE_COUNT 1629) + endif() + endif() + + if(NOT LINE_COUNT EQUAL 1629) + message(FATAL_ERROR "Language file ${f} has ${LINE_COUNT} lines, expected 1629.") + endif() + + list(GET LINES 0 ENGLISH_NAME) + list(GET LINES 1 NATIVE_NAME) + list(GET LINES 2 PREFIX_LEN) + + set(WORDS_CODE "") + # Exactly 1626 words starting at index 3. + foreach(I RANGE 3 1628) + list(GET LINES ${I} word) + if(word STREQUAL "") + message(FATAL_ERROR "Empty word found in ${f} at index ${I}") + endif() + if(word MATCHES "[ \t]") + message(FATAL_ERROR "Word '${word}' in ${f} contains whitespace") + endif() + string(APPEND WORDS_CODE " \"${word}\",\n") + endforeach() + + set(GENERATED_CPP "${CMAKE_CURRENT_BINARY_DIR}/lang_${lang_var}.cpp") + file(WRITE "${GENERATED_CPP}" +"#include + +namespace session::mnemonics { + +extern const Mnemonics ${lang_var}; +const Mnemonics ${lang_var} = { + \"${ENGLISH_NAME}\", + \"${NATIVE_NAME}\", + ${PREFIX_LEN}, + {{ +${WORDS_CODE} }} +}; + +} // namespace session::mnemonics +") + list(APPEND MNEMONIC_SOURCES "${GENERATED_CPP}") + string(APPEND LANG_EXTERNS "extern const Mnemonics ${lang_var};\n") + string(APPEND LANG_LIST " &${lang_var},\n") + math(EXPR LANG_COUNT "${LANG_COUNT} + 1") +endforeach() + +set(MASTER_CPP "${CMAKE_CURRENT_BINARY_DIR}/mnemonics_registry.cpp") +file(WRITE "${MASTER_CPP}" +"#include +#include + +namespace session::mnemonics { + +${LANG_EXTERNS} +static constexpr std::array all_languages = {{ +${LANG_LIST}}}; + +std::span get_languages() { + return all_languages; +} + +} // namespace session::mnemonics +") + +list(APPEND MNEMONIC_SOURCES "${MASTER_CPP}") + +# Add the generated sources to the crypto library +target_sources(crypto PRIVATE ${MNEMONIC_SOURCES} mnemonics.cpp) diff --git a/src/mnemonics/languages/README.md b/src/mnemonics/languages/README.md new file mode 100644 index 000000000..d8b08b4ee --- /dev/null +++ b/src/mnemonics/languages/README.md @@ -0,0 +1,46 @@ +# Mnemonic Language Word Lists + +This directory contains word lists for different languages used in mnemonic seed generation. + +A mnemonic seed phrase consists of a multiple of 3 words (typically 12 or 24 words), optionally +followed by a checksum, where each group of 3 words encodes a 4 byte (32 bit) value. Thus 12 words +is used for a 128-bit value and 24 words for a 256-bit value. + +Each language has a unique "prefix length" which indicates the word prefix required: i.e. if +set to 3 then any 3-character sequence should match at most one word in the list. This also allows +faster seed word input by allowing a user to simply provide the first three letters (e.g. "ver" +instead of "verification"). + +For unjustifiable by fixed historical reasons, the encoding also uses a pointless complication in +the actual calculation: rather than each 32-bit chunk being computed as `A + B·1626 + C·1626²` +(where A, B, C are the 0-1625 indices of the words) it is instead computed as: + + V = A + + ((1626 - A + B) % 1626) × 1626 + + ((1626 - B + C) % 1626) × 1626² + +The little-endian encoding of this 4-byte value becomes the 32-bit value. + +(Note that that are a relatively small number of "impossible" seed values here that would overflow +this calculation: these are explicitly not allowed as valid seeds by failing if the above +calculation overflows a 32-bit integer). + +This entirely pointless complication has some misguided historical reasoning about trying to make +poor entropy values not look so poor (e.g. by repeating words), but that is just so incredibly +misguided that it should be given no weight. Unfortunately, however, this is already in use and we +are stuck with it. + +Computing A, B, and C *from* a 32-bit value X is performed by interpreting X as a little-endian, +unsigned 32-bit value V and then: + + A = V % 1626 + B = ((V / 1626) + A) % 1626 + C = ((V / 1626²) + B) % 1626 + +## File Format + +Each `.txt` file consists of 1629 lines, following this structure: +1. English name of the language (e.g., `German`) +2. Native name of the language (e.g., `Deutsch`) +3. Unique prefix length (e.g., `4`). (The script utils/verify_mnemonics.py can verify this.) +4. 1626 lines, each containing a single word from the word list, in order. diff --git a/src/mnemonics/languages/chinese_simplified.txt b/src/mnemonics/languages/chinese_simplified.txt new file mode 100644 index 000000000..59a5adfe2 --- /dev/null +++ b/src/mnemonics/languages/chinese_simplified.txt @@ -0,0 +1,1629 @@ +Chinese (simplified) +简体中文 (中国) +1 +的 +一 +是 +在 +不 +了 +有 +和 +人 +这 +中 +大 +为 +上 +个 +国 +我 +以 +要 +他 +时 +来 +用 +们 +生 +到 +作 +地 +于 +出 +就 +分 +对 +成 +会 +可 +主 +发 +年 +动 +同 +工 +也 +能 +下 +过 +子 +说 +产 +种 +面 +而 +方 +后 +多 +定 +行 +学 +法 +所 +民 +得 +经 +十 +三 +之 +进 +着 +等 +部 +度 +家 +电 +力 +里 +如 +水 +化 +高 +自 +二 +理 +起 +小 +物 +现 +实 +加 +量 +都 +两 +体 +制 +机 +当 +使 +点 +从 +业 +本 +去 +把 +性 +好 +应 +开 +它 +合 +还 +因 +由 +其 +些 +然 +前 +外 +天 +政 +四 +日 +那 +社 +义 +事 +平 +形 +相 +全 +表 +间 +样 +与 +关 +各 +重 +新 +线 +内 +数 +正 +心 +反 +你 +明 +看 +原 +又 +么 +利 +比 +或 +但 +质 +气 +第 +向 +道 +命 +此 +变 +条 +只 +没 +结 +解 +问 +意 +建 +月 +公 +无 +系 +军 +很 +情 +者 +最 +立 +代 +想 +已 +通 +并 +提 +直 +题 +党 +程 +展 +五 +果 +料 +象 +员 +革 +位 +入 +常 +文 +总 +次 +品 +式 +活 +设 +及 +管 +特 +件 +长 +求 +老 +头 +基 +资 +边 +流 +路 +级 +少 +图 +山 +统 +接 +知 +较 +将 +组 +见 +计 +别 +她 +手 +角 +期 +根 +论 +运 +农 +指 +几 +九 +区 +强 +放 +决 +西 +被 +干 +做 +必 +战 +先 +回 +则 +任 +取 +据 +处 +队 +南 +给 +色 +光 +门 +即 +保 +治 +北 +造 +百 +规 +热 +领 +七 +海 +口 +东 +导 +器 +压 +志 +世 +金 +增 +争 +济 +阶 +油 +思 +术 +极 +交 +受 +联 +什 +认 +六 +共 +权 +收 +证 +改 +清 +美 +再 +采 +转 +更 +单 +风 +切 +打 +白 +教 +速 +花 +带 +安 +场 +身 +车 +例 +真 +务 +具 +万 +每 +目 +至 +达 +走 +积 +示 +议 +声 +报 +斗 +完 +类 +八 +离 +华 +名 +确 +才 +科 +张 +信 +马 +节 +话 +米 +整 +空 +元 +况 +今 +集 +温 +传 +土 +许 +步 +群 +广 +石 +记 +需 +段 +研 +界 +拉 +林 +律 +叫 +且 +究 +观 +越 +织 +装 +影 +算 +低 +持 +音 +众 +书 +布 +复 +容 +儿 +须 +际 +商 +非 +验 +连 +断 +深 +难 +近 +矿 +千 +周 +委 +素 +技 +备 +半 +办 +青 +省 +列 +习 +响 +约 +支 +般 +史 +感 +劳 +便 +团 +往 +酸 +历 +市 +克 +何 +除 +消 +构 +府 +称 +太 +准 +精 +值 +号 +率 +族 +维 +划 +选 +标 +写 +存 +候 +毛 +亲 +快 +效 +斯 +院 +查 +江 +型 +眼 +王 +按 +格 +养 +易 +置 +派 +层 +片 +始 +却 +专 +状 +育 +厂 +京 +识 +适 +属 +圆 +包 +火 +住 +调 +满 +县 +局 +照 +参 +红 +细 +引 +听 +该 +铁 +价 +严 +首 +底 +液 +官 +德 +随 +病 +苏 +失 +尔 +死 +讲 +配 +女 +黄 +推 +显 +谈 +罪 +神 +艺 +呢 +席 +含 +企 +望 +密 +批 +营 +项 +防 +举 +球 +英 +氧 +势 +告 +李 +台 +落 +木 +帮 +轮 +破 +亚 +师 +围 +注 +远 +字 +材 +排 +供 +河 +态 +封 +另 +施 +减 +树 +溶 +怎 +止 +案 +言 +士 +均 +武 +固 +叶 +鱼 +波 +视 +仅 +费 +紧 +爱 +左 +章 +早 +朝 +害 +续 +轻 +服 +试 +食 +充 +兵 +源 +判 +护 +司 +足 +某 +练 +差 +致 +板 +田 +降 +黑 +犯 +负 +击 +范 +继 +兴 +似 +余 +坚 +曲 +输 +修 +故 +城 +夫 +够 +送 +笔 +船 +占 +右 +财 +吃 +富 +春 +职 +觉 +汉 +画 +功 +巴 +跟 +虽 +杂 +飞 +检 +吸 +助 +升 +阳 +互 +初 +创 +抗 +考 +投 +坏 +策 +古 +径 +换 +未 +跑 +留 +钢 +曾 +端 +责 +站 +简 +述 +钱 +副 +尽 +帝 +射 +草 +冲 +承 +独 +令 +限 +阿 +宣 +环 +双 +请 +超 +微 +让 +控 +州 +良 +轴 +找 +否 +纪 +益 +依 +优 +顶 +础 +载 +倒 +房 +突 +坐 +粉 +敌 +略 +客 +袁 +冷 +胜 +绝 +析 +块 +剂 +测 +丝 +协 +诉 +念 +陈 +仍 +罗 +盐 +友 +洋 +错 +苦 +夜 +刑 +移 +频 +逐 +靠 +混 +母 +短 +皮 +终 +聚 +汽 +村 +云 +哪 +既 +距 +卫 +停 +烈 +央 +察 +烧 +迅 +境 +若 +印 +洲 +刻 +括 +激 +孔 +搞 +甚 +室 +待 +核 +校 +散 +侵 +吧 +甲 +游 +久 +菜 +味 +旧 +模 +湖 +货 +损 +预 +阻 +毫 +普 +稳 +乙 +妈 +植 +息 +扩 +银 +语 +挥 +酒 +守 +拿 +序 +纸 +医 +缺 +雨 +吗 +针 +刘 +啊 +急 +唱 +误 +训 +愿 +审 +附 +获 +茶 +鲜 +粮 +斤 +孩 +脱 +硫 +肥 +善 +龙 +演 +父 +渐 +血 +欢 +械 +掌 +歌 +沙 +刚 +攻 +谓 +盾 +讨 +晚 +粒 +乱 +燃 +矛 +乎 +杀 +药 +宁 +鲁 +贵 +钟 +煤 +读 +班 +伯 +香 +介 +迫 +句 +丰 +培 +握 +兰 +担 +弦 +蛋 +沉 +假 +穿 +执 +答 +乐 +谁 +顺 +烟 +缩 +征 +脸 +喜 +松 +脚 +困 +异 +免 +背 +星 +福 +买 +染 +井 +概 +慢 +怕 +磁 +倍 +祖 +皇 +促 +静 +补 +评 +翻 +肉 +践 +尼 +衣 +宽 +扬 +棉 +希 +伤 +操 +垂 +秋 +宜 +氢 +套 +督 +振 +架 +亮 +末 +宪 +庆 +编 +牛 +触 +映 +雷 +销 +诗 +座 +居 +抓 +裂 +胞 +呼 +娘 +景 +威 +绿 +晶 +厚 +盟 +衡 +鸡 +孙 +延 +危 +胶 +屋 +乡 +临 +陆 +顾 +掉 +呀 +灯 +岁 +措 +束 +耐 +剧 +玉 +赵 +跳 +哥 +季 +课 +凯 +胡 +额 +款 +绍 +卷 +齐 +伟 +蒸 +殖 +永 +宗 +苗 +川 +炉 +岩 +弱 +零 +杨 +奏 +沿 +露 +杆 +探 +滑 +镇 +饭 +浓 +航 +怀 +赶 +库 +夺 +伊 +灵 +税 +途 +灭 +赛 +归 +召 +鼓 +播 +盘 +裁 +险 +康 +唯 +录 +菌 +纯 +借 +糖 +盖 +横 +符 +私 +努 +堂 +域 +枪 +润 +幅 +哈 +竟 +熟 +虫 +泽 +脑 +壤 +碳 +欧 +遍 +侧 +寨 +敢 +彻 +虑 +斜 +薄 +庭 +纳 +弹 +饲 +伸 +折 +麦 +湿 +暗 +荷 +瓦 +塞 +床 +筑 +恶 +户 +访 +塔 +奇 +透 +梁 +刀 +旋 +迹 +卡 +氯 +遇 +份 +毒 +泥 +退 +洗 +摆 +灰 +彩 +卖 +耗 +夏 +择 +忙 +铜 +献 +硬 +予 +繁 +圈 +雪 +函 +亦 +抽 +篇 +阵 +阴 +丁 +尺 +追 +堆 +雄 +迎 +泛 +爸 +楼 +避 +谋 +吨 +野 +猪 +旗 +累 +偏 +典 +馆 +索 +秦 +脂 +潮 +爷 +豆 +忽 +托 +惊 +塑 +遗 +愈 +朱 +替 +纤 +粗 +倾 +尚 +痛 +楚 +谢 +奋 +购 +磨 +君 +池 +旁 +碎 +骨 +监 +捕 +弟 +暴 +割 +贯 +殊 +释 +词 +亡 +壁 +顿 +宝 +午 +尘 +闻 +揭 +炮 +残 +冬 +桥 +妇 +警 +综 +招 +吴 +付 +浮 +遭 +徐 +您 +摇 +谷 +赞 +箱 +隔 +订 +男 +吹 +园 +纷 +唐 +败 +宋 +玻 +巨 +耕 +坦 +荣 +闭 +湾 +键 +凡 +驻 +锅 +救 +恩 +剥 +凝 +碱 +齿 +截 +炼 +麻 +纺 +禁 +废 +盛 +版 +缓 +净 +睛 +昌 +婚 +涉 +筒 +嘴 +插 +岸 +朗 +庄 +街 +藏 +姑 +贸 +腐 +奴 +啦 +惯 +乘 +伙 +恢 +匀 +纱 +扎 +辩 +耳 +彪 +臣 +亿 +璃 +抵 +脉 +秀 +萨 +俄 +网 +舞 +店 +喷 +纵 +寸 +汗 +挂 +洪 +贺 +闪 +柬 +爆 +烯 +津 +稻 +墙 +软 +勇 +像 +滚 +厘 +蒙 +芳 +肯 +坡 +柱 +荡 +腿 +仪 +旅 +尾 +轧 +冰 +贡 +登 +黎 +削 +钻 +勒 +逃 +障 +氨 +郭 +峰 +币 +港 +伏 +轨 +亩 +毕 +擦 +莫 +刺 +浪 +秘 +援 +株 +健 +售 +股 +岛 +甘 +泡 +睡 +童 +铸 +汤 +阀 +休 +汇 +舍 +牧 +绕 +炸 +哲 +磷 +绩 +朋 +淡 +尖 +启 +陷 +柴 +呈 +徒 +颜 +泪 +稍 +忘 +泵 +蓝 +拖 +洞 +授 +镜 +辛 +壮 +锋 +贫 +虚 +弯 +摩 +泰 +幼 +廷 +尊 +窗 +纲 +弄 +隶 +疑 +氏 +宫 +姐 +震 +瑞 +怪 +尤 +琴 +循 +描 +膜 +违 +夹 +腰 +缘 +珠 +穷 +森 +枝 +竹 +沟 +催 +绳 +忆 +邦 +剩 +幸 +浆 +栏 +拥 +牙 +贮 +礼 +滤 +钠 +纹 +罢 +拍 +咱 +喊 +袖 +埃 +勤 +罚 +焦 +潜 +伍 +墨 +欲 +缝 +姓 +刊 +饱 +仿 +奖 +铝 +鬼 +丽 +跨 +默 +挖 +链 +扫 +喝 +袋 +炭 +污 +幕 +诸 +弧 +励 +梅 +奶 +洁 +灾 +舟 +鉴 +苯 +讼 +抱 +毁 +懂 +寒 +智 +埔 +寄 +届 +跃 +渡 +挑 +丹 +艰 +贝 +碰 +拔 +爹 +戴 +码 +梦 +芽 +熔 +赤 +渔 +哭 +敬 +颗 +奔 +铅 +仲 +虎 +稀 +妹 +乏 +珍 +申 +桌 +遵 +允 +隆 +螺 +仓 +魏 +锐 +晓 +氮 +兼 +隐 +碍 +赫 +拨 +忠 +肃 +缸 +牵 +抢 +博 +巧 +壳 +兄 +杜 +讯 +诚 +碧 +祥 +柯 +页 +巡 +矩 +悲 +灌 +龄 +伦 +票 +寻 +桂 +铺 +圣 +恐 +恰 +郑 +趣 +抬 +荒 +腾 +贴 +柔 +滴 +猛 +阔 +辆 +妻 +填 +撤 +储 +签 +闹 +扰 +紫 +砂 +递 +戏 +吊 +陶 +伐 +喂 +疗 +瓶 +婆 +抚 +臂 +摸 +忍 +虾 +蜡 +邻 +胸 +巩 +挤 +偶 +弃 +槽 +劲 +乳 +邓 +吉 +仁 +烂 +砖 +租 +乌 +舰 +伴 +瓜 +浅 +丙 +暂 +燥 +橡 +柳 +迷 +暖 +牌 +秧 +胆 +详 +簧 +踏 +瓷 +谱 +呆 +宾 +糊 +洛 +辉 +愤 +竞 +隙 +怒 +粘 +乃 +绪 +肩 +籍 +敏 +涂 +熙 +皆 +侦 +悬 +掘 +享 +纠 +醒 +狂 +锁 +淀 +恨 +牲 +霸 +爬 +赏 +逆 +玩 +陵 +祝 +秒 +浙 +貌 diff --git a/src/mnemonics/languages/dutch.txt b/src/mnemonics/languages/dutch.txt new file mode 100644 index 000000000..e162ee07e --- /dev/null +++ b/src/mnemonics/languages/dutch.txt @@ -0,0 +1,1629 @@ +Dutch +Nederlands +4 +aalglad +aalscholver +aambeeld +aangeef +aanlandig +aanvaard +aanwakker +aapmens +aarten +abdicatie +abnormaal +abrikoos +accu +acuut +adjudant +admiraal +advies +afbidding +afdracht +affaire +affiche +afgang +afkick +afknap +aflees +afmijner +afname +afpreekt +afrader +afspeel +aftocht +aftrek +afzijdig +ahornboom +aktetas +akzo +alchemist +alcohol +aldaar +alexander +alfabet +alfredo +alice +alikruik +allrisk +altsax +alufolie +alziend +amai +ambacht +ambieer +amina +amnestie +amok +ampul +amuzikaal +angela +aniek +antje +antwerpen +anya +aorta +apache +apekool +appelaar +arganolie +argeloos +armoede +arrenslee +artritis +arubaan +asbak +ascii +asgrauw +asjes +asml +aspunt +asurn +asveld +aterling +atomair +atrium +atsma +atypisch +auping +aura +avifauna +axiaal +azoriaan +azteek +azuur +bachelor +badderen +badhotel +badmantel +badsteden +balie +ballans +balvers +bamibal +banneling +barracuda +basaal +batelaan +batje +beambte +bedlamp +bedwelmd +befaamd +begierd +begraaf +behield +beijaard +bejaagd +bekaaid +beks +bektas +belaad +belboei +belderbos +beloerd +beluchten +bemiddeld +benadeeld +benijd +berechten +beroemd +besef +besseling +best +betichten +bevind +bevochten +bevraagd +bewust +bidplaats +biefstuk +biemans +biezen +bijbaan +bijeenkom +bijfiguur +bijkaart +bijlage +bijpaard +bijtgaar +bijweg +bimmel +binck +bint +biobak +biotisch +biseks +bistro +bitter +bitumen +bizar +blad +bleken +blender +bleu +blief +blijven +blozen +bock +boef +boei +boks +bolder +bolus +bolvormig +bomaanval +bombarde +bomma +bomtapijt +bookmaker +boos +borg +bosbes +boshuizen +bosloop +botanicus +bougie +bovag +boxspring +braad +brasem +brevet +brigade +brinckman +bruid +budget +buffel +buks +bulgaar +buma +butaan +butler +buuf +cactus +cafeetje +camcorder +cannabis +canyon +capoeira +capsule +carkit +casanova +catalaan +ceintuur +celdeling +celplasma +cement +censeren +ceramisch +cerberus +cerebraal +cesium +cirkel +citeer +civiel +claxon +clenbuterol +clicheren +clijsen +coalitie +coassistentschap +coaxiaal +codetaal +cofinanciering +cognac +coltrui +comfort +commandant +condensaat +confectie +conifeer +convector +copier +corfu +correct +coup +couvert +creatie +credit +crematie +cricket +croupier +cruciaal +cruijff +cuisine +culemborg +culinair +curve +cyrano +dactylus +dading +dagblind +dagje +daglicht +dagprijs +dagranden +dakdekker +dakpark +dakterras +dalgrond +dambord +damkat +damlengte +damman +danenberg +debbie +decibel +defect +deformeer +degelijk +degradant +dejonghe +dekken +deppen +derek +derf +derhalve +detineren +devalueer +diaken +dicht +dictaat +dief +digitaal +dijbreuk +dijkmans +dimbaar +dinsdag +diode +dirigeer +disbalans +dobermann +doenbaar +doerak +dogma +dokhaven +dokwerker +doling +dolphijn +dolven +dombo +dooraderd +dopeling +doping +draderig +drama +drenkbak +dreumes +drol +drug +duaal +dublin +duplicaat +durven +dusdanig +dutchbat +dutje +dutten +duur +duwwerk +dwaal +dweil +dwing +dyslexie +ecostroom +ecotaks +educatie +eeckhout +eede +eemland +eencellig +eeneiig +eenruiter +eenwinter +eerenberg +eerrover +eersel +eetmaal +efteling +egaal +egtberts +eickhoff +eidooier +eiland +eind +eisden +ekster +elburg +elevatie +elfkoppig +elfrink +elftal +elimineer +elleboog +elma +elodie +elsa +embleem +embolie +emoe +emonds +emplooi +enduro +enfin +engageer +entourage +entstof +epileer +episch +eppo +erasmus +erboven +erebaan +erelijst +ereronden +ereteken +erfhuis +erfwet +erger +erica +ermitage +erna +ernie +erts +ertussen +eruitzien +ervaar +erven +erwt +esbeek +escort +esdoorn +essing +etage +eter +ethanol +ethicus +etholoog +eufonisch +eurocent +evacuatie +exact +examen +executant +exen +exit +exogeen +exotherm +expeditie +expletief +expres +extase +extinctie +faal +faam +fabel +facultair +fakir +fakkel +faliekant +fallisch +famke +fanclub +fase +fatsoen +fauna +federaal +feedback +feest +feilbaar +feitelijk +felblauw +figurante +fiod +fitheid +fixeer +flap +fleece +fleur +flexibel +flits +flos +flow +fluweel +foezelen +fokkelman +fokpaard +fokvee +folder +follikel +folmer +folteraar +fooi +foolen +forfait +forint +formule +fornuis +fosfaat +foxtrot +foyer +fragiel +frater +freak +freddie +fregat +freon +frijnen +fructose +frunniken +fuiven +funshop +furieus +fysica +gadget +galder +galei +galg +galvlieg +galzuur +ganesh +gaswet +gaza +gazelle +geaaid +gebiecht +gebufferd +gedijd +geef +geflanst +gefreesd +gegaan +gegijzeld +gegniffel +gegraaid +gehikt +gehobbeld +gehucht +geiser +geiten +gekaakt +gekheid +gekijf +gekmakend +gekocht +gekskap +gekte +gelubberd +gemiddeld +geordend +gepoederd +gepuft +gerda +gerijpt +geseald +geshockt +gesierd +geslaagd +gesnaaid +getracht +getwijfel +geuit +gevecht +gevlagd +gewicht +gezaagd +gezocht +ghanees +giebelen +giechel +giepmans +gips +giraal +gistachtig +gitaar +glaasje +gletsjer +gleuf +glibberen +glijbaan +gloren +gluipen +gluren +gluur +gnoe +goddelijk +godgans +godschalk +godzalig +goeierd +gogme +goklustig +gokwereld +gonggrijp +gonje +goor +grabbel +graf +graveer +grif +grolleman +grom +groosman +grubben +gruijs +grut +guacamole +guido +guppy +haazen +hachelijk +haex +haiku +hakhout +hakken +hanegem +hans +hanteer +harrie +hazebroek +hedonist +heil +heineken +hekhuis +hekman +helbig +helga +helwegen +hengelaar +herkansen +hermafrodiet +hertaald +hiaat +hikspoors +hitachi +hitparade +hobo +hoeve +holocaust +hond +honnepon +hoogacht +hotelbed +hufter +hugo +huilbier +hulk +humus +huwbaar +huwelijk +hype +iconisch +idema +ideogram +idolaat +ietje +ijker +ijkheid +ijklijn +ijkmaat +ijkwezen +ijmuiden +ijsbox +ijsdag +ijselijk +ijskoud +ilse +immuun +impliceer +impuls +inbijten +inbuigen +indijken +induceer +indy +infecteer +inhaak +inkijk +inluiden +inmijnen +inoefenen +inpolder +inrijden +inslaan +invitatie +inwaaien +ionisch +isaac +isolatie +isotherm +isra +italiaan +ivoor +jacobs +jakob +jammen +jampot +jarig +jehova +jenever +jezus +joana +jobdienst +josua +joule +juich +jurk +juut +kaas +kabelaar +kabinet +kagenaar +kajuit +kalebas +kalm +kanjer +kapucijn +karregat +kart +katvanger +katwijk +kegelaar +keiachtig +keizer +kenletter +kerdijk +keus +kevlar +kezen +kickback +kieviet +kijken +kikvors +kilheid +kilobit +kilsdonk +kipschnitzel +kissebis +klad +klagelijk +klak +klapbaar +klaver +klene +klets +klijnhout +klit +klok +klonen +klotefilm +kluif +klumper +klus +knabbel +knagen +knaven +kneedbaar +knmi +knul +knus +kokhals +komiek +komkommer +kompaan +komrij +komvormig +koning +kopbal +kopklep +kopnagel +koppejan +koptekst +kopwand +koraal +kosmisch +kostbaar +kram +kraneveld +kras +kreling +krengen +kribbe +krik +kruid +krulbol +kuijper +kuipbank +kuit +kuiven +kutsmoes +kuub +kwak +kwatong +kwetsbaar +kwezelaar +kwijnen +kwik +kwinkslag +kwitantie +lading +lakbeits +lakken +laklaag +lakmoes +lakwijk +lamheid +lamp +lamsbout +lapmiddel +larve +laser +latijn +latuw +lawaai +laxeerpil +lebberen +ledeboer +leefbaar +leeman +lefdoekje +lefhebber +legboor +legsel +leguaan +leiplaat +lekdicht +lekrijden +leksteen +lenen +leraar +lesbienne +leugenaar +leut +lexicaal +lezing +lieten +liggeld +lijdzaam +lijk +lijmstang +lijnschip +likdoorn +likken +liksteen +limburg +link +linoleum +lipbloem +lipman +lispelen +lissabon +litanie +liturgie +lochem +loempia +loesje +logheid +lonen +lonneke +loom +loos +losbaar +loslaten +losplaats +loting +lotnummer +lots +louie +lourdes +louter +lowbudget +luijten +luikenaar +luilak +luipaard +luizenbos +lulkoek +lumen +lunzen +lurven +lutjeboer +luttel +lutz +luuk +luwte +luyendijk +lyceum +lynx +maakbaar +magdalena +malheid +manchet +manfred +manhaftig +mank +mantel +marion +marxist +masmeijer +massaal +matsen +matverf +matze +maude +mayonaise +mechanica +meifeest +melodie +meppelink +midvoor +midweeks +midzomer +miezel +mijnraad +minus +mirck +mirte +mispakken +misraden +miswassen +mitella +moker +molecule +mombakkes +moonen +mopperaar +moraal +morgana +mormel +mosselaar +motregen +mouw +mufheid +mutueel +muzelman +naaidoos +naald +nadeel +nadruk +nagy +nahon +naima +nairobi +napalm +napels +napijn +napoleon +narigheid +narratief +naseizoen +nasibal +navigatie +nawijn +negatief +nekletsel +nekwervel +neolatijn +neonataal +neptunus +nerd +nest +neuzelaar +nihiliste +nijenhuis +nijging +nijhoff +nijl +nijptang +nippel +nokkenas +noordam +noren +normaal +nottelman +notulant +nout +nuance +nuchter +nudorp +nulde +nullijn +nulmeting +nunspeet +nylon +obelisk +object +oblie +obsceen +occlusie +oceaan +ochtend +ockhuizen +oerdom +oergezond +oerlaag +oester +okhuijsen +olifant +olijfboer +omaans +ombudsman +omdat +omdijken +omdoen +omgebouwd +omkeer +omkomen +ommegaand +ommuren +omroep +omruil +omslaan +omsmeden +omvaar +onaardig +onedel +onenig +onheilig +onrecht +onroerend +ontcijfer +onthaal +ontvallen +ontzadeld +onzacht +onzin +onzuiver +oogappel +ooibos +ooievaar +ooit +oorarts +oorhanger +oorijzer +oorklep +oorschelp +oorworm +oorzaak +opdagen +opdien +opdweilen +opel +opgebaard +opinie +opjutten +opkijken +opklaar +opkuisen +opkwam +opnaaien +opossum +opsieren +opsmeer +optreden +opvijzel +opvlammen +opwind +oraal +orchidee +orkest +ossuarium +ostendorf +oublie +oudachtig +oudbakken +oudnoors +oudshoorn +oudtante +oven +over +oxidant +pablo +pacht +paktafel +pakzadel +paljas +panharing +papfles +paprika +parochie +paus +pauze +paviljoen +peek +pegel +peigeren +pekela +pendant +penibel +pepmiddel +peptalk +periferie +perron +pessarium +peter +petfles +petgat +peuk +pfeifer +picknick +pief +pieneman +pijlkruid +pijnacker +pijpelink +pikdonker +pikeer +pilaar +pionier +pipet +piscine +pissebed +pitchen +pixel +plamuren +plan +plausibel +plegen +plempen +pleonasme +plezant +podoloog +pofmouw +pokdalig +ponywagen +popachtig +popidool +porren +positie +potten +pralen +prezen +prijzen +privaat +proef +prooi +prozawerk +pruik +prul +publiceer +puck +puilen +pukkelig +pulveren +pupil +puppy +purmerend +pustjens +putemmer +puzzelaar +queenie +quiche +raam +raar +raat +raes +ralf +rally +ramona +ramselaar +ranonkel +rapen +rapunzel +rarekiek +rarigheid +rattenhol +ravage +reactie +recreant +redacteur +redster +reewild +regie +reijnders +rein +replica +revanche +rigide +rijbaan +rijdansen +rijgen +rijkdom +rijles +rijnwijn +rijpma +rijstafel +rijtaak +rijzwepen +rioleer +ripdeal +riphagen +riskant +rits +rivaal +robbedoes +robot +rockact +rodijk +rogier +rohypnol +rollaag +rolpaal +roltafel +roof +roon +roppen +rosbief +rosharig +rosielle +rotan +rotleven +rotten +rotvaart +royaal +royeer +rubato +ruby +ruche +rudge +ruggetje +rugnummer +rugpijn +rugtitel +rugzak +ruilbaar +ruis +ruit +rukwind +rulijs +rumoeren +rumsdorp +rumtaart +runnen +russchen +ruwkruid +saboteer +saksisch +salade +salpeter +sambabal +samsam +satelliet +satineer +saus +scampi +scarabee +scenario +schobben +schubben +scout +secessie +secondair +seculair +sediment +seeland +settelen +setwinst +sheriff +shiatsu +siciliaan +sidderaal +sigma +sijben +silvana +simkaart +sinds +situatie +sjaak +sjardijn +sjezen +sjor +skinhead +skylab +slamixen +sleijpen +slijkerig +slordig +slowaak +sluieren +smadelijk +smiecht +smoel +smos +smukken +snackcar +snavel +sneaker +sneu +snijdbaar +snit +snorder +soapbox +soetekouw +soigneren +sojaboon +solo +solvabel +somber +sommatie +soort +soppen +sopraan +soundbar +spanen +spawater +spijgat +spinaal +spionage +spiraal +spleet +splijt +spoed +sporen +spul +spuug +spuw +stalen +standaard +star +stefan +stencil +stijf +stil +stip +stopdas +stoten +stoven +straat +strobbe +strubbel +stucadoor +stuif +stukadoor +subhoofd +subregent +sudoku +sukade +sulfaat +surinaams +suus +syfilis +symboliek +sympathie +synagoge +synchroon +synergie +systeem +taanderij +tabak +tachtig +tackelen +taiwanees +talman +tamheid +tangaslip +taps +tarkan +tarwe +tasman +tatjana +taxameter +teil +teisman +telbaar +telco +telganger +telstar +tenant +tepel +terzet +testament +ticket +tiesinga +tijdelijk +tika +tiksel +tilleman +timbaal +tinsteen +tiplijn +tippelaar +tjirpen +toezeggen +tolbaas +tolgeld +tolhek +tolo +tolpoort +toltarief +tolvrij +tomaat +tondeuse +toog +tooi +toonbaar +toos +topclub +toppen +toptalent +topvrouw +toque +torment +tornado +tosti +totdat +toucheer +toulouse +tournedos +tout +trabant +tragedie +trailer +traject +traktaat +trauma +tray +trechter +tred +tref +treur +troebel +tros +trucage +truffel +tsaar +tucht +tuenter +tuitelig +tukje +tuktuk +tulp +tuma +tureluurs +twijfel +twitteren +tyfoon +typograaf +ugandees +uiachtig +uier +uisnipper +ultiem +unitair +uranium +urbaan +urendag +ursula +uurcirkel +uurglas +uzelf +vaat +vakantie +vakleraar +valbijl +valpartij +valreep +valuatie +vanmiddag +vanonder +varaan +varken +vaten +veenbes +veeteler +velgrem +vellekoop +velvet +veneberg +venlo +vent +venusberg +venw +veredeld +verf +verhaaf +vermaak +vernaaid +verraad +vers +veruit +verzaagd +vetachtig +vetlok +vetmesten +veto +vetrek +vetstaart +vetten +veurink +viaduct +vibrafoon +vicariaat +vieux +vieveen +vijfvoud +villa +vilt +vimmetje +vindbaar +vips +virtueel +visdieven +visee +visie +vlaag +vleugel +vmbo +vocht +voesenek +voicemail +voip +volg +vork +vorselaar +voyeur +vracht +vrekkig +vreten +vrije +vrozen +vrucht +vucht +vugt +vulkaan +vulmiddel +vulva +vuren +waas +wacht +wadvogel +wafel +waffel +walhalla +walnoot +walraven +wals +walvis +wandaad +wanen +wanmolen +want +warklomp +warm +wasachtig +wasteil +watt +webhandel +weblog +webpagina +webzine +wedereis +wedstrijd +weeda +weert +wegmaaien +wegscheer +wekelijks +wekken +wekroep +wektoon +weldaad +welwater +wendbaar +wenkbrauw +wens +wentelaar +wervel +wesseling +wetboek +wetmatig +whirlpool +wijbrands +wijdbeens +wijk +wijnbes +wijting +wild +wimpelen +wingebied +winplaats +winter +winzucht +wipstaart +wisgerhof +withaar +witmaker +wokkel +wolf +wonenden +woning +worden +worp +wortel +wrat +wrijf +wringen +yoghurt +ypsilon +zaaijer +zaak +zacharias +zakelijk +zakkam +zakwater +zalf +zalig +zaniken +zebracode +zeeblauw +zeef +zeegaand +zeeuw +zege +zegje +zeil +zesbaans +zesenhalf +zeskantig +zesmaal +zetbaas +zetpil +zeulen +ziezo +zigzag +zijaltaar +zijbeuk +zijlijn +zijmuur +zijn +zijwaarts +zijzelf +zilt +zimmerman +zinledig +zinnelijk +zionist +zitdag +zitruimte +zitzak +zoal +zodoende +zoekbots +zoem +zoiets +zojuist +zondaar +zotskap +zottebol +zucht +zuivel +zulk +zult +zuster +zuur +zweedijk +zwendel +zwepen +zwiep +zwijmel +zworen diff --git a/src/mnemonics/languages/english.txt b/src/mnemonics/languages/english.txt new file mode 100644 index 000000000..755b15442 --- /dev/null +++ b/src/mnemonics/languages/english.txt @@ -0,0 +1,1629 @@ +English +English +3 +abbey +abducts +ability +ablaze +abnormal +abort +abrasive +absorb +abyss +academy +aces +aching +acidic +acoustic +acquire +across +actress +acumen +adapt +addicted +adept +adhesive +adjust +adopt +adrenalin +adult +adventure +aerial +afar +affair +afield +afloat +afoot +afraid +after +against +agenda +aggravate +agile +aglow +agnostic +agony +agreed +ahead +aided +ailments +aimless +airport +aisle +ajar +akin +alarms +album +alchemy +alerts +algebra +alkaline +alley +almost +aloof +alpine +already +also +altitude +alumni +always +amaze +ambush +amended +amidst +ammo +amnesty +among +amply +amused +anchor +android +anecdote +angled +ankle +annoyed +answers +antics +anvil +anxiety +anybody +apart +apex +aphid +aplomb +apology +apply +apricot +aptitude +aquarium +arbitrary +archer +ardent +arena +argue +arises +army +around +arrow +arsenic +artistic +ascend +ashtray +aside +asked +asleep +aspire +assorted +asylum +athlete +atlas +atom +atrium +attire +auburn +auctions +audio +august +aunt +austere +autumn +avatar +avidly +avoid +awakened +awesome +awful +awkward +awning +awoken +axes +axis +axle +aztec +azure +baby +bacon +badge +baffles +bagpipe +bailed +bakery +balding +bamboo +banjo +baptism +basin +batch +bawled +bays +because +beer +befit +begun +behind +being +below +bemused +benches +berries +bested +betting +bevel +beware +beyond +bias +bicycle +bids +bifocals +biggest +bikini +bimonthly +binocular +biology +biplane +birth +biscuit +bite +biweekly +blender +blip +bluntly +boat +bobsled +bodies +bogeys +boil +boldly +bomb +border +boss +both +bounced +bovine +bowling +boxes +boyfriend +broken +brunt +bubble +buckets +budget +buffet +bugs +building +bulb +bumper +bunch +business +butter +buying +buzzer +bygones +byline +bypass +cabin +cactus +cadets +cafe +cage +cajun +cake +calamity +camp +candy +casket +catch +cause +cavernous +cease +cedar +ceiling +cell +cement +cent +certain +chlorine +chrome +cider +cigar +cinema +circle +cistern +citadel +civilian +claim +click +clue +coal +cobra +cocoa +code +coexist +coffee +cogs +cohesive +coils +colony +comb +cool +copy +corrode +costume +cottage +cousin +cowl +criminal +cube +cucumber +cuddled +cuffs +cuisine +cunning +cupcake +custom +cycling +cylinder +cynical +dabbing +dads +daft +dagger +daily +damp +dangerous +dapper +darted +dash +dating +dauntless +dawn +daytime +dazed +debut +decay +dedicated +deepest +deftly +degrees +dehydrate +deity +dejected +delayed +demonstrate +dented +deodorant +depth +desk +devoid +dewdrop +dexterity +dialect +dice +diet +different +digit +dilute +dime +dinner +diode +diplomat +directed +distance +ditch +divers +dizzy +doctor +dodge +does +dogs +doing +dolphin +domestic +donuts +doorway +dormant +dosage +dotted +double +dove +down +dozen +dreams +drinks +drowning +drunk +drying +dual +dubbed +duckling +dude +duets +duke +dullness +dummy +dunes +duplex +duration +dusted +duties +dwarf +dwelt +dwindling +dying +dynamite +dyslexic +each +eagle +earth +easy +eating +eavesdrop +eccentric +echo +eclipse +economics +ecstatic +eden +edgy +edited +educated +eels +efficient +eggs +egotistic +eight +either +eject +elapse +elbow +eldest +eleven +elite +elope +else +eluded +emails +ember +emerge +emit +emotion +empty +emulate +energy +enforce +enhanced +enigma +enjoy +enlist +enmity +enough +enraged +ensign +entrance +envy +epoxy +equip +erase +erected +erosion +error +eskimos +espionage +essential +estate +etched +eternal +ethics +etiquette +evaluate +evenings +evicted +evolved +examine +excess +exhale +exit +exotic +exquisite +extra +exult +fabrics +factual +fading +fainted +faked +fall +family +fancy +farming +fatal +faulty +fawns +faxed +fazed +feast +february +federal +feel +feline +females +fences +ferry +festival +fetches +fever +fewest +fiat +fibula +fictional +fidget +fierce +fifteen +fight +films +firm +fishing +fitting +five +fixate +fizzle +fleet +flippant +flying +foamy +focus +foes +foggy +foiled +folding +fonts +foolish +fossil +fountain +fowls +foxes +foyer +framed +friendly +frown +fruit +frying +fudge +fuel +fugitive +fully +fuming +fungal +furnished +fuselage +future +fuzzy +gables +gadget +gags +gained +galaxy +gambit +gang +gasp +gather +gauze +gave +gawk +gaze +gearbox +gecko +geek +gels +gemstone +general +geometry +germs +gesture +getting +geyser +ghetto +ghost +giant +giddy +gifts +gigantic +gills +gimmick +ginger +girth +giving +glass +gleeful +glide +gnaw +gnome +goat +goblet +godfather +goes +goggles +going +goldfish +gone +goodbye +gopher +gorilla +gossip +gotten +gourmet +governing +gown +greater +grunt +guarded +guest +guide +gulp +gumball +guru +gusts +gutter +guys +gymnast +gypsy +gyrate +habitat +hacksaw +haggled +hairy +hamburger +happens +hashing +hatchet +haunted +having +hawk +haystack +hazard +hectare +hedgehog +heels +hefty +height +hemlock +hence +heron +hesitate +hexagon +hickory +hiding +highway +hijack +hiker +hills +himself +hinder +hippo +hire +history +hitched +hive +hoax +hobby +hockey +hoisting +hold +honked +hookup +hope +hornet +hospital +hotel +hounded +hover +howls +hubcaps +huddle +huge +hull +humid +hunter +hurried +husband +huts +hybrid +hydrogen +hyper +iceberg +icing +icon +identity +idiom +idled +idols +igloo +ignore +iguana +illness +imagine +imbalance +imitate +impel +inactive +inbound +incur +industrial +inexact +inflamed +ingested +initiate +injury +inkling +inline +inmate +innocent +inorganic +input +inquest +inroads +insult +intended +inundate +invoke +inwardly +ionic +irate +iris +irony +irritate +island +isolated +issued +italics +itches +items +itinerary +itself +ivory +jabbed +jackets +jaded +jagged +jailed +jamming +january +jargon +jaunt +javelin +jaws +jazz +jeans +jeers +jellyfish +jeopardy +jerseys +jester +jetting +jewels +jigsaw +jingle +jittery +jive +jobs +jockey +jogger +joining +joking +jolted +jostle +journal +joyous +jubilee +judge +juggled +juicy +jukebox +july +jump +junk +jury +justice +juvenile +kangaroo +karate +keep +kennel +kept +kernels +kettle +keyboard +kickoff +kidneys +king +kiosk +kisses +kitchens +kiwi +knapsack +knee +knife +knowledge +knuckle +koala +laboratory +ladder +lagoon +lair +lakes +lamb +language +laptop +large +last +later +launching +lava +lawsuit +layout +lazy +lectures +ledge +leech +left +legion +leisure +lemon +lending +leopard +lesson +lettuce +lexicon +liar +library +licks +lids +lied +lifestyle +light +likewise +lilac +limits +linen +lion +lipstick +liquid +listen +lively +loaded +lobster +locker +lodge +lofty +logic +loincloth +long +looking +lopped +lordship +losing +lottery +loudly +love +lower +loyal +lucky +luggage +lukewarm +lullaby +lumber +lunar +lurk +lush +luxury +lymph +lynx +lyrics +macro +madness +magically +mailed +major +makeup +malady +mammal +maps +masterful +match +maul +maverick +maximum +mayor +maze +meant +mechanic +medicate +meeting +megabyte +melting +memoir +menu +merger +mesh +metro +mews +mice +midst +mighty +mime +mirror +misery +mittens +mixture +moat +mobile +mocked +mohawk +moisture +molten +moment +money +moon +mops +morsel +mostly +motherly +mouth +movement +mowing +much +muddy +muffin +mugged +mullet +mumble +mundane +muppet +mural +musical +muzzle +myriad +mystery +myth +nabbing +nagged +nail +names +nanny +napkin +narrate +nasty +natural +nautical +navy +nearby +necklace +needed +negative +neither +neon +nephew +nerves +nestle +network +neutral +never +newt +nexus +nibs +niche +niece +nifty +nightly +nimbly +nineteen +nirvana +nitrogen +nobody +nocturnal +nodes +noises +nomad +noodles +northern +nostril +noted +nouns +novelty +nowhere +nozzle +nuance +nucleus +nudged +nugget +nuisance +null +number +nuns +nurse +nutshell +nylon +oaks +oars +oasis +oatmeal +obedient +object +obliged +obnoxious +observant +obtains +obvious +occur +ocean +october +odds +odometer +offend +often +oilfield +ointment +okay +older +olive +olympics +omega +omission +omnibus +onboard +oncoming +oneself +ongoing +onion +online +onslaught +onto +onward +oozed +opacity +opened +opposite +optical +opus +orange +orbit +orchid +orders +organs +origin +ornament +orphans +oscar +ostrich +otherwise +otter +ouch +ought +ounce +ourselves +oust +outbreak +oval +oven +owed +owls +owner +oxidant +oxygen +oyster +ozone +pact +paddles +pager +pairing +palace +pamphlet +pancakes +paper +paradise +pastry +patio +pause +pavements +pawnshop +payment +peaches +pebbles +peculiar +pedantic +peeled +pegs +pelican +pencil +people +pepper +perfect +pests +petals +phase +pheasants +phone +phrases +physics +piano +picked +pierce +pigment +piloted +pimple +pinched +pioneer +pipeline +pirate +pistons +pitched +pivot +pixels +pizza +playful +pledge +pliers +plotting +plus +plywood +poaching +pockets +podcast +poetry +point +poker +polar +ponies +pool +popular +portents +possible +potato +pouch +poverty +powder +pram +present +pride +problems +pruned +prying +psychic +public +puck +puddle +puffin +pulp +pumpkins +punch +puppy +purged +push +putty +puzzled +pylons +pyramid +python +queen +quick +quote +rabbits +racetrack +radar +rafts +rage +railway +raking +rally +ramped +randomly +rapid +rarest +rash +rated +ravine +rays +razor +react +rebel +recipe +reduce +reef +refer +regular +reheat +reinvest +rejoices +rekindle +relic +remedy +renting +reorder +repent +request +reruns +rest +return +reunion +revamp +rewind +rhino +rhythm +ribbon +richly +ridges +rift +rigid +rims +ringing +riots +ripped +rising +ritual +river +roared +robot +rockets +rodent +rogue +roles +romance +roomy +roped +roster +rotate +rounded +rover +rowboat +royal +ruby +rudely +ruffled +rugged +ruined +ruling +rumble +runway +rural +rustled +ruthless +sabotage +sack +sadness +safety +saga +sailor +sake +salads +sample +sanity +sapling +sarcasm +sash +satin +saucepan +saved +sawmill +saxophone +sayings +scamper +scenic +school +science +scoop +scrub +scuba +seasons +second +sedan +seeded +segments +seismic +selfish +semifinal +sensible +september +sequence +serving +session +setup +seventh +sewage +shackles +shelter +shipped +shocking +shrugged +shuffled +shyness +siblings +sickness +sidekick +sieve +sifting +sighting +silk +simplest +sincerely +sipped +siren +situated +sixteen +sizes +skater +skew +skirting +skulls +skydive +slackens +sleepless +slid +slower +slug +smash +smelting +smidgen +smog +smuggled +snake +sneeze +sniff +snout +snug +soapy +sober +soccer +soda +software +soggy +soil +solved +somewhere +sonic +soothe +soprano +sorry +southern +sovereign +sowed +soya +space +speedy +sphere +spiders +splendid +spout +sprig +spud +spying +square +stacking +stellar +stick +stockpile +strained +stunning +stylishly +subtly +succeed +suddenly +suede +suffice +sugar +suitcase +sulking +summon +sunken +superior +surfer +sushi +suture +swagger +swept +swiftly +sword +swung +syllabus +symptoms +syndrome +syringe +system +taboo +tacit +tadpoles +tagged +tail +taken +talent +tamper +tanks +tapestry +tarnished +tasked +tattoo +taunts +tavern +tawny +taxi +teardrop +technical +tedious +teeming +tell +template +tender +tepid +tequila +terminal +testing +tether +textbook +thaw +theatrics +thirsty +thorn +threaten +thumbs +thwart +ticket +tidy +tiers +tiger +tilt +timber +tinted +tipsy +tirade +tissue +titans +toaster +tobacco +today +toenail +toffee +together +toilet +token +tolerant +tomorrow +tonic +toolbox +topic +torch +tossed +total +touchy +towel +toxic +toyed +trash +trendy +tribal +trolling +truth +trying +tsunami +tubes +tucks +tudor +tuesday +tufts +tugs +tuition +tulips +tumbling +tunnel +turnip +tusks +tutor +tuxedo +twang +tweezers +twice +twofold +tycoon +typist +tyrant +ugly +ulcers +ultimate +umbrella +umpire +unafraid +unbending +uncle +under +uneven +unfit +ungainly +unhappy +union +unjustly +unknown +unlikely +unmask +unnoticed +unopened +unplugs +unquoted +unrest +unsafe +until +unusual +unveil +unwind +unzip +upbeat +upcoming +update +upgrade +uphill +upkeep +upload +upon +upper +upright +upstairs +uptight +upwards +urban +urchins +urgent +usage +useful +usher +using +usual +utensils +utility +utmost +utopia +uttered +vacation +vague +vain +value +vampire +vane +vapidly +vary +vastness +vats +vaults +vector +veered +vegan +vehicle +vein +velvet +venomous +verification +vessel +veteran +vexed +vials +vibrate +victim +video +viewpoint +vigilant +viking +village +vinegar +violin +vipers +virtual +visited +vitals +vivid +vixen +vocal +vogue +voice +volcano +vortex +voted +voucher +vowels +voyage +vulture +wade +waffle +wagtail +waist +waking +wallets +wanted +warped +washing +water +waveform +waxing +wayside +weavers +website +wedge +weekday +weird +welders +went +wept +were +western +wetsuit +whale +when +whipped +whole +wickets +width +wield +wife +wiggle +wildly +winter +wipeout +wiring +wise +withdrawn +wives +wizard +wobbly +woes +woken +wolf +womanly +wonders +woozy +worry +wounded +woven +wrap +wrist +wrong +yacht +yahoo +yanks +yard +yawning +yearbook +yellow +yesterday +yeti +yields +yodel +yoga +younger +yoyo +zapped +zeal +zebra +zero +zesty +zigzags +zinger +zippers +zodiac +zombie +zones +zoom diff --git a/src/mnemonics/languages/esperanto.txt b/src/mnemonics/languages/esperanto.txt new file mode 100644 index 000000000..3238b89c2 --- /dev/null +++ b/src/mnemonics/languages/esperanto.txt @@ -0,0 +1,1629 @@ +Esperanto +Esperanto +3 +abako +abdiki +abelo +abituriento +ablativo +abnorma +abonantoj +abrikoto +absoluta +abunda +acetono +acida +adapti +adekvata +adheri +adicii +adjektivo +administri +adolesko +adreso +adstringa +adulto +advokato +adzo +aeroplano +aferulo +afgana +afiksi +aflaba +aforismo +afranki +aftozo +afusto +agavo +agento +agiti +aglo +agmaniero +agnoski +agordo +agrabla +agtipo +agutio +aikido +ailanto +aina +ajatolo +ajgenvaloro +ajlobulbo +ajnlitera +ajuto +ajzi +akademio +akcepti +akeo +akiri +aklamado +akmeo +akno +akompani +akrobato +akselo +aktiva +akurata +akvofalo +alarmo +albumo +alcedo +aldoni +aleo +alfabeto +algo +alhasti +aligatoro +alkoholo +almozo +alnomo +alojo +alpinisto +alrigardi +alskribi +alta +alumeto +alveni +alzaca +amaso +ambasado +amdeklaro +amebo +amfibio +amhara +amiko +amkanto +amletero +amnestio +amoranto +amplekso +amrakonto +amsterdama +amuzi +ananaso +androido +anekdoto +anfrakto +angulo +anheli +animo +anjono +ankro +anonci +anpriskribo +ansero +antikva +anuitato +aorto +aparta +aperti +apika +aplikado +apneo +apogi +aprobi +apsido +apterigo +apudesto +araneo +arbo +ardeco +aresti +argilo +aristokrato +arko +arlekeno +armi +arniko +aromo +arpio +arsenalo +artisto +aruba +arvorto +asaio +asbesto +ascendi +asekuri +asfalto +asisti +askalono +asocio +aspekti +astro +asulo +atakonto +atendi +atingi +atleto +atmosfero +atomo +atropino +atuto +avataro +aventuro +aviadilo +avokado +azaleo +azbuko +azenino +azilpetanto +azoto +azteka +babili +bacilo +badmintono +bagatelo +bahama +bajoneto +baki +balai +bambuo +bani +baobabo +bapti +baro +bastono +batilo +bavara +bazalto +beata +bebofono +bedo +begonio +behaviorismo +bejlo +bekero +belarto +bemolo +benko +bereto +besto +betulo +bevelo +bezoni +biaso +biblioteko +biciklo +bidaro +bieno +bifsteko +bigamiulo +bijekcio +bikino +bildo +bimetalismo +bindi +biografio +birdo +biskvito +bitlibro +bivako +bizara +bjalistoka +blanka +bleki +blinda +blovi +blua +boato +bobsledo +bocvanano +bodisatvo +bofratino +bogefratoj +bohema +boji +bokalo +boli +bombono +bona +bopatrino +bordo +bosko +botelo +bovido +brakpleno +bretaro +brikmuro +broso +brulema +bubalo +buctrapi +budo +bufedo +bugio +bujabeso +buklo +buldozo +bumerango +bunta +burokrataro +busbileto +butero +buzuko +caro +cebo +ceceo +cedro +cefalo +cejana +cekumo +celebri +cemento +cent +cepo +certa +cetera +cezio +ciano +cibeto +cico +cidro +cifero +cigaredo +ciklo +cilindro +cimbalo +cinamo +cipreso +cirkonstanco +cisterno +citrono +ciumi +civilizado +colo +congo +cunamo +cvana +dabi +daco +dadaismo +dafodilo +dago +daimio +dajmono +daktilo +dalio +damo +danki +darmo +datumoj +dazipo +deadmoni +debeto +decidi +dedukti +deerigi +defendi +degeli +dehaki +deirpunkto +deklaracio +delikata +demandi +dento +dependi +derivi +desegni +detrui +devi +deziri +dialogo +dicentro +didaktika +dieto +diferenci +digesti +diino +dikfingro +diligenta +dimensio +dinamo +diodo +diplomo +direkte +diskuti +diurno +diversa +dizajno +dobrogitaro +docento +dogano +dojeno +doktoro +dolori +domego +donaci +dopado +dormi +dosierujo +dotita +dozeno +drato +dresi +drinki +droni +druido +duaranga +dubi +ducent +dudek +duelo +dufoje +dugongo +duhufa +duilo +dujare +dukato +duloka +dumtempe +dungi +duobla +dupiedulo +dura +dusenca +dutaga +duuma +duvalvuloj +duzo +ebena +eblecoj +ebono +ebria +eburo +ecaro +ecigi +ecoj +edelvejso +editoro +edro +eduki +edzino +efektiva +efiki +efloreski +egala +egeco +egiptologo +eglefino +egoista +egreto +ejakuli +ejlo +ekarto +ekbruligi +ekceli +ekde +ekesti +ekfirmao +ekgliti +ekhavi +ekipi +ekkapti +eklezio +ekmalsati +ekonomio +ekpluvi +ekrano +ekster +ektiri +ekumeno +ekvilibro +ekzemplo +elasta +elbalai +elcento +eldoni +elektro +elfari +elgliti +elhaki +elipso +elkovi +ellasi +elmeti +elnutri +elokventa +elparoli +elrevigi +elstari +elteni +eluzita +elvoki +elzasa +emajlo +embaraso +emerito +emfazo +eminenta +emocio +empiria +emulsio +enarkivigi +enboteligi +enciklopedio +endorfino +energio +enfermi +engluti +enhavo +enigmo +enjekcio +enketi +enlanda +enmeti +enorma +enplanti +enradiki +enspezo +entrepreni +enui +envolvi +enzimo +eono +eosto +epitafo +epoko +epriskribebla +epsilono +erari +erbio +erco +erekti +ergonomia +erikejo +ermito +erotika +erpilo +erupcio +esameno +escepti +esenco +eskapi +esotera +esperi +estonto +etapo +etendi +etfingro +etikedo +etlitero +etmakleristo +etnika +etoso +etradio +etskala +etullernejo +evakui +evento +eviti +evolui +ezoko +fabriko +facila +fadeno +fagoto +fajro +fakto +fali +familio +fanatiko +farbo +fasko +fatala +favora +fazeolo +febro +federacio +feino +fekunda +felo +femuro +fenestro +fermi +festi +fetora +fezo +fiasko +fibro +fidela +fiera +fifama +figuro +fiherbo +fiinsekto +fiksa +filmo +fimensa +finalo +fiolo +fiparoli +firmao +fisko +fitingo +fiuzanto +fivorto +fiziko +fjordo +flago +flegi +flirti +floro +flugi +fobio +foceno +foirejo +fojfoje +fokuso +folio +fomenti +fonto +formulo +fosforo +fotografi +fratino +fremda +friti +frosto +frua +ftizo +fuelo +fugo +fuksia +fulmilo +fumanto +fundamento +fuorto +furioza +fusilo +futbalo +fuzio +gabardino +gado +gaela +gafo +gagato +gaja +gaki +galanta +gamao +ganto +gapulo +gardi +gasto +gavio +gazeto +geamantoj +gebani +geedzeco +gefratoj +geheno +gejsero +geko +gelateno +gemisto +geniulo +geografio +gepardo +geranio +gestolingvo +geto +geumo +gibono +giganta +gildo +gimnastiko +ginekologo +gipsi +girlando +gistfungo +gitaro +glazuro +glebo +gliti +globo +gluti +gnafalio +gnejso +gnomo +gnuo +gobio +godetio +goeleto +gojo +golfludejo +gombo +gondolo +gorilo +gospelo +gotika +granda +greno +griza +groto +grupo +guano +gubernatoro +gudrotuko +gufo +gujavo +guldeno +gumi +gupio +guruo +gusto +guto +guvernistino +gvardio +gverilo +gvidanto +habitato +hadito +hafnio +hagiografio +haitiano +hajlo +hakbloko +halti +hamstro +hangaro +hapalo +haro +hasta +hati +havebla +hazardo +hebrea +hedero +hegemonio +hejmo +hektaro +helpi +hemisfero +heni +hepato +herbo +hesa +heterogena +heziti +hiacinto +hibrida +hidrogeno +hieroglifo +higieno +hihii +hilumo +himno +hindino +hiperteksto +hirundo +historio +hobio +hojli +hokeo +hologramo +homido +honesta +hopi +horizonto +hospitalo +hotelo +huadi +hubo +hufumo +hugenoto +hukero +huligano +humana +hundo +huoj +hupilo +hurai +husaro +hutuo +huzo +iafoje +iagrade +iamaniere +iarelate +iaspeca +ibekso +ibiso +idaro +ideala +idiomo +idolo +iele +igluo +ignori +iguamo +igvano +ikono +iksodo +ikto +iliaflanke +ilkomputilo +ilobreto +ilremedo +ilumini +imagi +imitado +imperio +imuna +incidento +industrio +inerta +infano +ingenra +inhali +iniciati +injekti +inklino +inokuli +insekto +inteligenta +inundi +inviti +ioma +ionosfero +iperito +ipomeo +irana +irejo +irigacio +ironio +isato +islamo +istempo +itinero +itrio +iuloke +iumaniere +iutempe +izolita +jado +jaguaro +jakto +jama +januaro +japano +jarringo +jazo +jenoj +jesulo +jetavio +jezuito +jodli +joviala +juano +jubileo +judismo +jufto +juki +julio +juneca +jupo +juristo +juste +juvelo +kabineto +kadrato +kafo +kahelo +kajako +kakao +kalkuli +kampo +kanti +kapitalo +karaktero +kaserolo +katapulto +kaverna +kazino +kebabo +kefiro +keglo +kejlo +kekso +kelka +kemio +kerno +kesto +kiamaniere +kibuco +kidnapi +kielo +kikero +kilogramo +kimono +kinejo +kiosko +kirurgo +kisi +kitelo +kivio +klavaro +klerulo +klini +klopodi +klubo +knabo +knedi +koalo +kobalto +kodigi +kofro +kohera +koincidi +kojoto +kokoso +koloro +komenci +kontrakto +kopio +korekte +kosti +kotono +kovri +krajono +kredi +krii +krom +kruco +ksantino +ksenono +ksilofono +ksosa +kubuto +kudri +kuglo +kuiri +kuko +kulero +kumuluso +kuneco +kupro +kuri +kuseno +kutimo +kuvo +kuzino +kvalito +kverko +kvin +kvoto +labori +laculo +ladbotelo +lafo +laguno +laikino +laktobovino +lampolumo +landkarto +laosa +lapono +larmoguto +lastjare +latitudo +lavejo +lazanjo +leciono +ledosako +leganto +lekcio +lemura +lentuga +leopardo +leporo +lerni +lesivo +letero +levilo +lezi +liano +libera +liceo +lieno +lifto +ligilo +likvoro +lila +limono +lingvo +lipo +lirika +listo +literatura +liveri +lobio +logika +lojala +lokalo +longa +lordo +lotado +loza +luanto +lubriki +lucida +ludema +luigi +lukso +luli +lumbilda +lunde +lupago +lustro +lutilo +luzerno +maato +maceri +madono +mafiano +magazeno +mahometano +maizo +majstro +maketo +malgranda +mamo +mandareno +maorio +mapigi +marini +masko +mateno +mazuto +meandro +meblo +mecenato +medialo +mefito +megafono +mejlo +mekanika +melodia +membro +mendi +mergi +mespilo +metoda +mevo +mezuri +miaflanke +micelio +mielo +migdalo +mikrofilmo +militi +mimiko +mineralo +miopa +miri +mistera +mitralo +mizeri +mjelo +mnemoniko +mobilizi +mocio +moderna +mohajro +mokadi +molaro +momento +monero +mopso +mordi +moskito +motoro +movimento +mozaiko +mueli +mukozo +muldi +mumio +munti +muro +muskolo +mutacio +muzikisto +nabo +nacio +nadlo +nafto +naiva +najbaro +nanometro +napo +narciso +naski +naturo +navigi +naztruo +neatendite +nebulo +necesa +nedankinde +neebla +nefari +negoco +nehavi +neimagebla +nektaro +nelonga +nematura +nenia +neordinara +nepra +nervuro +nesto +nete +neulo +nevino +nifo +nigra +nihilisto +nikotino +nilono +nimfeo +nitrogeno +nivelo +nobla +nocio +nodozo +nokto +nomkarto +norda +nostalgio +notbloko +novico +nuanco +nuboza +nuda +nugato +nuklea +nuligi +numero +nuntempe +nupto +nura +nutri +oazo +obei +objekto +oblikva +obolo +observi +obtuza +obuso +oceano +odekolono +odori +oferti +oficiala +ofsajdo +ofte +ogivo +ogro +ojstredoj +okaze +okcidenta +okro +oksido +oktobro +okulo +oldulo +oleo +olivo +omaro +ombro +omego +omikrono +omleto +omnibuso +onagro +ondo +oneco +onidire +onklino +onlajna +onomatopeo +ontologio +opaka +operacii +opinii +oportuna +opresi +optimisto +oratoro +orbito +ordinara +orelo +orfino +organizi +orienta +orkestro +orlo +orminejo +ornami +ortangulo +orumi +oscedi +osmozo +ostocerbo +ovalo +ovingo +ovoblanko +ovri +ovulado +ozono +pacama +padeli +pafilo +pagigi +pajlo +paketo +palaco +pampelmo +pantalono +papero +paroli +pasejo +patro +pavimo +peco +pedalo +peklita +pelikano +pensiono +peplomo +pesilo +petanto +pezoforto +piano +picejo +piede +pigmento +pikema +pilkoludo +pimento +pinglo +pioniro +pipromento +pirato +pistolo +pitoreska +piulo +pivoti +pizango +planko +plektita +plibonigi +ploradi +plurlingva +pobo +podio +poeto +pogranda +pohora +pokalo +politekniko +pomarbo +ponevosto +populara +porcelana +postkompreno +poteto +poviga +pozitiva +prapatroj +precize +pridemandi +probable +pruntanto +psalmo +psikologio +psoriazo +pterido +publiko +pudro +pufo +pugnobato +pulovero +pumpi +punkto +pupo +pureo +puso +putrema +puzlo +rabate +racionala +radiko +rafinado +raguo +rajto +rakonti +ralio +rampi +rando +rapida +rastruma +ratifiki +raviolo +razeno +reakcio +rebildo +recepto +redakti +reenigi +reformi +regiono +rehavi +reinspekti +rejesi +reklamo +relativa +rememori +renkonti +reorganizado +reprezenti +respondi +retumilo +reuzebla +revidi +rezulti +rialo +ribeli +ricevi +ridiga +rifuginto +rigardi +rikolti +rilati +rimarki +rinocero +ripozi +riski +ritmo +rivero +rizokampo +roboto +rododendro +rojo +rokmuziko +rolvorto +romantika +ronroni +rosino +rotondo +rovero +rozeto +rubando +rudimenta +rufa +rugbeo +ruino +ruleto +rumoro +runo +rupio +rura +rustimuna +ruzulo +sabato +sadismo +safario +sagaca +sakfluto +salti +samtage +sandalo +sapejo +sarongo +satelito +savano +sbiro +sciado +seanco +sebo +sedativo +segligno +sekretario +selektiva +semajno +senpeza +separeo +servilo +sesangulo +setli +seurigi +severa +sezono +sfagno +sfero +sfinkso +siatempe +siblado +sidejo +siesto +sifono +signalo +siklo +silenti +simpla +sinjoro +siropo +sistemo +situacio +siverto +sizifa +skatolo +skemo +skianto +sklavo +skorpio +skribisto +skulpti +skvamo +slango +sledeto +sliparo +smeraldo +smirgi +smokingo +smuto +snoba +snufegi +sobra +sociano +sodakvo +sofo +soifi +sojlo +soklo +soldato +somero +sonilo +sopiri +sorto +soulo +soveto +sparkado +speciala +spiri +splito +sporto +sprita +spuro +stabila +stelfiguro +stimulo +stomako +strato +studanto +subgrupo +suden +suferanta +sugesti +suito +sukero +sulko +sume +sunlumo +super +surskribeto +suspekti +suturo +svati +svenfali +svingi +svopo +tabako +taglumo +tajloro +taksimetro +talento +tamen +tanko +taoismo +tapioko +tarifo +tasko +tatui +taverno +teatro +tedlaboro +tegmento +tehoro +teknika +telefono +tempo +tenisejo +teorie +teraso +testudo +tetablo +teujo +tezo +tialo +tibio +tielnomata +tifono +tigro +tikli +timida +tinkturo +tiom +tiparo +tirkesto +titolo +tiutempe +tizano +tobogano +tofeo +togo +toksa +tolerema +tombolo +tondri +topografio +tordeti +tosti +totalo +traduko +tredi +triangulo +tropika +trumpeto +tualeto +tubisto +tufgrebo +tuja +tukano +tulipo +tumulto +tunelo +turisto +tusi +tutmonda +tvisto +udono +uesto +ukazo +ukelelo +ulcero +ulmo +ultimato +ululi +umbiliko +unco +ungego +uniformo +unkti +unukolora +uragano +urbano +uretro +urino +ursido +uskleco +usonigi +utero +utila +utopia +uverturo +uzadi +uzeblo +uzino +uzkutimo +uzofini +uzurpi +uzvaloro +vadejo +vafleto +vagono +vahabismo +vajco +vakcino +valoro +vampiro +vangharoj +vaporo +varma +vasta +vato +vazaro +veaspekta +vedismo +vegetalo +vehiklo +vejno +vekita +velstango +vemieno +vendi +vepro +verando +vespero +veturi +veziko +viando +vibri +vico +videbla +vifio +vigla +viktimo +vila +vimeno +vintro +violo +vippuno +virtuala +viskoza +vitro +viveca +viziti +vobli +vodko +vojeto +vokegi +volbo +vomema +vono +vortaro +vosto +voti +vrako +vringi +vualo +vulkano +vundo +vuvuzelo +zamenhofa +zapi +zebro +zefiro +zeloto +zenismo +zeolito +zepelino +zeto +zigzagi +zinko +zipo +zirkonio +zodiako +zoeto +zombio +zono +zoologio +zorgi +zukino +zumilo diff --git a/src/mnemonics/languages/french.txt b/src/mnemonics/languages/french.txt new file mode 100644 index 000000000..13b8996b9 --- /dev/null +++ b/src/mnemonics/languages/french.txt @@ -0,0 +1,1629 @@ +French +Français +4 +abandon +abattre +aboi +abolir +aborder +abri +absence +absolu +abuser +acacia +acajou +accent +accord +accrocher +accuser +acerbe +achat +acheter +acide +acier +acquis +acte +action +adage +adepte +adieu +admettre +admis +adorer +adresser +aduler +affaire +affirmer +afin +agacer +agent +agir +agiter +agonie +agrafe +agrume +aider +aigle +aigre +aile +ailleurs +aimant +aimer +ainsi +aise +ajouter +alarme +album +alcool +alerte +algue +alibi +aller +allumer +alors +amande +amener +amie +amorcer +amour +ample +amuser +ananas +ancien +anglais +angoisse +animal +anneau +annoncer +apercevoir +apparence +appel +apporter +apprendre +appuyer +arbre +arcade +arceau +arche +ardeur +argent +argile +aride +arme +armure +arracher +arriver +article +asile +aspect +assaut +assez +assister +assurer +astre +astuce +atlas +atroce +attacher +attente +attirer +aube +aucun +audace +auparavant +auquel +aurore +aussi +autant +auteur +autoroute +autre +aval +avant +avec +avenir +averse +aveu +avide +avion +avis +avoir +avouer +avril +azote +azur +badge +bagage +bague +bain +baisser +balai +balcon +balise +balle +bambou +banane +banc +bandage +banjo +banlieue +bannir +banque +baobab +barbe +barque +barrer +bassine +bataille +bateau +battre +baver +bavoir +bazar +beau +beige +berger +besoin +beurre +biais +biceps +bidule +bien +bijou +bilan +billet +blanc +blason +bleu +bloc +blond +bocal +boire +boiserie +boiter +bonbon +bondir +bonheur +bordure +borgne +borner +bosse +bouche +bouder +bouger +boule +bourse +bout +boxe +brader +braise +branche +braquer +bras +brave +brebis +brevet +brider +briller +brin +brique +briser +broche +broder +bronze +brosser +brouter +bruit +brute +budget +buffet +bulle +bureau +buriner +buste +buter +butiner +cabas +cabinet +cabri +cacao +cacher +cadeau +cadre +cage +caisse +caler +calme +camarade +camion +campagne +canal +canif +capable +capot +carat +caresser +carie +carpe +cartel +casier +casque +casserole +cause +cavale +cave +ceci +cela +celui +cendre +cent +cependant +cercle +cerise +cerner +certes +cerveau +cesser +chacun +chair +chaleur +chamois +chanson +chaque +charge +chasse +chat +chaud +chef +chemin +cheveu +chez +chicane +chien +chiffre +chiner +chiot +chlore +choc +choix +chose +chou +chute +cibler +cidre +ciel +cigale +cinq +cintre +cirage +cirque +ciseau +citation +citer +citron +civet +clairon +clan +classe +clavier +clef +climat +cloche +cloner +clore +clos +clou +club +cobra +cocon +coiffer +coin +colline +colon +combat +comme +compte +conclure +conduire +confier +connu +conseil +contre +convenir +copier +cordial +cornet +corps +cosmos +coton +couche +coude +couler +coupure +cour +couteau +couvrir +crabe +crainte +crampe +cran +creuser +crever +crier +crime +crin +crise +crochet +croix +cruel +cuisine +cuite +culot +culte +cumul +cure +curieux +cuve +dame +danger +dans +davantage +debout +dedans +dehors +delta +demain +demeurer +demi +dense +dent +depuis +dernier +descendre +dessus +destin +dette +deuil +deux +devant +devenir +devin +devoir +dicton +dieu +difficile +digestion +digue +diluer +dimanche +dinde +diode +dire +diriger +discours +disposer +distance +divan +divers +docile +docteur +dodu +dogme +doigt +dominer +donation +donjon +donner +dopage +dorer +dormir +doseur +douane +double +douche +douleur +doute +doux +douzaine +draguer +drame +drap +dresser +droit +duel +dune +duper +durant +durcir +durer +eaux +effacer +effet +effort +effrayant +elle +embrasser +emmener +emparer +empire +employer +emporter +enclos +encore +endive +endormir +endroit +enduit +enfant +enfermer +enfin +enfler +enfoncer +enfuir +engager +engin +enjeu +enlever +ennemi +ennui +ensemble +ensuite +entamer +entendre +entier +entourer +entre +envelopper +envie +envoyer +erreur +escalier +espace +espoir +esprit +essai +essor +essuyer +estimer +exact +examiner +excuse +exemple +exiger +exil +exister +exode +expliquer +exposer +exprimer +extase +fable +facette +facile +fade +faible +faim +faire +fait +falloir +famille +faner +farce +farine +fatigue +faucon +faune +faute +faux +faveur +favori +faxer +feinter +femme +fendre +fente +ferme +festin +feuille +feutre +fiable +fibre +ficher +fier +figer +figure +filet +fille +filmer +fils +filtre +final +finesse +finir +fiole +firme +fixe +flacon +flair +flamme +flan +flaque +fleur +flocon +flore +flot +flou +fluide +fluor +flux +focus +foin +foire +foison +folie +fonction +fondre +fonte +force +forer +forger +forme +fort +fosse +fouet +fouine +foule +four +foyer +frais +franc +frapper +freiner +frimer +friser +frite +froid +froncer +fruit +fugue +fuir +fuite +fumer +fureur +furieux +fuser +fusil +futile +futur +gagner +gain +gala +galet +galop +gamme +gant +garage +garde +garer +gauche +gaufre +gaule +gaver +gazon +geler +genou +genre +gens +gercer +germer +geste +gibier +gicler +gilet +girafe +givre +glace +glisser +globe +gloire +gluant +gober +golf +gommer +gorge +gosier +goutte +grain +gramme +grand +gras +grave +gredin +griffure +griller +gris +gronder +gros +grotte +groupe +grue +guerrier +guetter +guider +guise +habiter +hache +haie +haine +halte +hamac +hanche +hangar +hanter +haras +hareng +harpe +hasard +hausse +haut +havre +herbe +heure +hibou +hier +histoire +hiver +hochet +homme +honneur +honte +horde +horizon +hormone +houle +housse +hublot +huile +huit +humain +humble +humide +humour +hurler +idole +igloo +ignorer +illusion +image +immense +immobile +imposer +impression +incapable +inconnu +index +indiquer +infime +injure +inox +inspirer +instant +intention +intime +inutile +inventer +inviter +iode +iris +issue +ivre +jade +jadis +jamais +jambe +janvier +jardin +jauge +jaunisse +jeter +jeton +jeudi +jeune +joie +joindre +joli +joueur +journal +judo +juge +juillet +juin +jument +jungle +jupe +jupon +jurer +juron +jury +jusque +juste +kayak +ketchup +kilo +kiwi +koala +label +lacet +lacune +laine +laisse +lait +lame +lancer +lande +laque +lard +largeur +larme +larve +lasso +laver +lendemain +lentement +lequel +lettre +leur +lever +levure +liane +libre +lien +lier +lieutenant +ligne +ligoter +liguer +limace +limer +limite +lingot +lion +lire +lisser +litre +livre +lobe +local +logis +loin +loisir +long +loque +lors +lotus +louer +loup +lourd +louve +loyer +lubie +lucide +lueur +luge +luire +lundi +lune +lustre +lutin +lutte +luxe +machine +madame +magie +magnifique +magot +maigre +main +mairie +maison +malade +malheur +malin +manche +manger +manier +manoir +manquer +marche +mardi +marge +mariage +marquer +mars +masque +masse +matin +mauvais +meilleur +melon +membre +menacer +mener +mensonge +mentir +menu +merci +merlu +mesure +mettre +meuble +meunier +meute +miche +micro +midi +miel +miette +mieux +milieu +mille +mimer +mince +mineur +ministre +minute +mirage +miroir +miser +mite +mixte +mobile +mode +module +moins +mois +moment +momie +monde +monsieur +monter +moquer +moral +morceau +mordre +morose +morse +mortier +morue +motif +motte +moudre +moule +mourir +mousse +mouton +mouvement +moyen +muer +muette +mugir +muguet +mulot +multiple +munir +muret +muse +musique +muter +nacre +nager +nain +naissance +narine +narrer +naseau +nasse +nation +nature +naval +navet +naviguer +navrer +neige +nerf +nerveux +neuf +neutre +neuve +neveu +niche +nier +niveau +noble +noce +nocif +noir +nomade +nombre +nommer +nord +norme +notaire +notice +notre +nouer +nougat +nourrir +nous +nouveau +novice +noyade +noyer +nuage +nuance +nuire +nuit +nulle +nuque +oasis +objet +obliger +obscur +observer +obtenir +obus +occasion +occuper +ocre +octet +odeur +odorat +offense +officier +offrir +ogive +oiseau +olive +ombre +onctueux +onduler +ongle +onze +opter +option +orageux +oral +orange +orbite +ordinaire +ordre +oreille +organe +orgie +orgueil +orient +origan +orner +orteil +ortie +oser +osselet +otage +otarie +ouate +oublier +ouest +ours +outil +outre +ouvert +ouvrir +ovale +ozone +pacte +page +paille +pain +paire +paix +palace +palissade +palmier +palpiter +panda +panneau +papa +papier +paquet +parc +pardi +parfois +parler +parmi +parole +partir +parvenir +passer +pastel +patin +patron +paume +pause +pauvre +paver +pavot +payer +pays +peau +peigne +peinture +pelage +pelote +pencher +pendre +penser +pente +percer +perdu +perle +permettre +personne +perte +peser +pesticide +petit +peuple +peur +phase +photo +phrase +piano +pied +pierre +pieu +pile +pilier +pilote +pilule +piment +pincer +pinson +pinte +pion +piquer +pirate +pire +piste +piton +pitre +pivot +pizza +placer +plage +plaire +plan +plaque +plat +plein +pleurer +pliage +plier +plonger +plot +pluie +plume +plus +pneu +poche +podium +poids +poil +point +poire +poison +poitrine +poivre +police +pollen +pomme +pompier +poncer +pondre +pont +portion +poser +position +possible +poste +potage +potin +pouce +poudre +poulet +poumon +poupe +pour +pousser +poutre +pouvoir +prairie +premier +prendre +presque +preuve +prier +primeur +prince +prison +priver +prix +prochain +produire +profond +proie +projet +promener +prononcer +propre +prose +prouver +prune +public +puce +pudeur +puiser +pull +pulpe +puma +punir +purge +putois +quand +quartier +quasi +quatre +quel +question +queue +quiche +quille +quinze +quitter +quoi +rabais +raboter +race +racheter +racine +racler +raconter +radar +radio +rafale +rage +ragot +raideur +raie +rail +raison +ramasser +ramener +rampe +rance +rang +rapace +rapide +rapport +rarement +rasage +raser +rasoir +rassurer +rater +ratio +rature +ravage +ravir +rayer +rayon +rebond +recevoir +recherche +record +reculer +redevenir +refuser +regard +regretter +rein +rejeter +rejoindre +relation +relever +religion +remarquer +remettre +remise +remonter +remplir +remuer +rencontre +rendre +renier +renoncer +rentrer +renverser +repas +repli +reposer +reproche +requin +respect +ressembler +reste +retard +retenir +retirer +retour +retrouver +revenir +revoir +revue +rhume +ricaner +riche +rideau +ridicule +rien +rigide +rincer +rire +risquer +rituel +rivage +rive +robe +robot +robuste +rocade +roche +rodeur +rogner +roman +rompre +ronce +rondeur +ronger +roque +rose +rosir +rotation +rotule +roue +rouge +rouler +route +ruban +rubis +ruche +rude +ruelle +ruer +rugby +rugir +ruine +rumeur +rural +ruse +rustre +sable +sabot +sabre +sacre +sage +saint +saisir +salade +salive +salle +salon +salto +salut +salve +samba +sandale +sanguin +sapin +sarcasme +satisfaire +sauce +sauf +sauge +saule +sauna +sauter +sauver +savoir +science +scoop +score +second +secret +secte +seigneur +sein +seize +selle +selon +semaine +sembler +semer +semis +sensuel +sentir +sept +serpe +serrer +sertir +service +seuil +seulement +short +sien +sigle +signal +silence +silo +simple +singe +sinon +sinus +sioux +sirop +site +situation +skier +snob +sobre +social +socle +sodium +soigner +soir +soixante +soja +solaire +soldat +soleil +solide +solo +solvant +sombre +somme +somnoler +sondage +songeur +sonner +sorte +sosie +sottise +souci +soudain +souffrir +souhaiter +soulever +soumettre +soupe +sourd +soustraire +soutenir +souvent +soyeux +spectacle +sport +stade +stagiaire +stand +star +statue +stock +stop +store +style +suave +subir +sucre +suer +suffire +suie +suite +suivre +sujet +sulfite +supposer +surf +surprendre +surtout +surveiller +tabac +table +tabou +tache +tacler +tacot +tact +taie +taille +taire +talon +talus +tandis +tango +tanin +tant +taper +tapis +tard +tarif +tarot +tarte +tasse +taureau +taux +taverne +taxer +taxi +tellement +temple +tendre +tenir +tenter +tenu +terme +ternir +terre +test +texte +thym +tibia +tiers +tige +tipi +tique +tirer +tissu +titre +toast +toge +toile +toiser +toiture +tomber +tome +tonne +tonte +toque +torse +tortue +totem +toucher +toujours +tour +tousser +tout +toux +trace +train +trame +tranquille +travail +trembler +trente +tribu +trier +trio +tripe +triste +troc +trois +tromper +tronc +trop +trotter +trouer +truc +truite +tuba +tuer +tuile +turbo +tutu +tuyau +type +union +unique +unir +unisson +untel +urne +usage +user +usiner +usure +utile +vache +vague +vaincre +valeur +valoir +valser +valve +vampire +vaseux +vaste +veau +veille +veine +velours +velu +vendre +venir +vent +venue +verbe +verdict +version +vertige +verve +veste +veto +vexer +vice +victime +vide +vieil +vieux +vigie +vigne +ville +vingt +violent +virer +virus +visage +viser +visite +visuel +vitamine +vitrine +vivant +vivre +vocal +vodka +vogue +voici +voile +voir +voisin +voiture +volaille +volcan +voler +volt +votant +votre +vouer +vouloir +vous +voyage +voyou +vrac +vrai +yacht +yeti +yeux +yoga +zeste +zinc +zone +zoom diff --git a/src/mnemonics/languages/german.txt b/src/mnemonics/languages/german.txt new file mode 100644 index 000000000..bec652510 --- /dev/null +++ b/src/mnemonics/languages/german.txt @@ -0,0 +1,1629 @@ +German +Deutsch +4 +Abakus +Abart +abbilden +Abbruch +Abdrift +Abendrot +Abfahrt +abfeuern +Abflug +abfragen +Abglanz +abhärten +abheben +Abhilfe +Abitur +Abkehr +Ablauf +ablecken +Ablösung +Abnehmer +abnutzen +Abonnent +Abrasion +Abrede +abrüsten +Absicht +Absprung +Abstand +absuchen +Abteil +Abundanz +abwarten +Abwurf +Abzug +Achse +Achtung +Acker +Aderlass +Adler +Admiral +Adresse +Affe +Affront +Afrika +Aggregat +Agilität +ähneln +Ahnung +Ahorn +Akazie +Akkord +Akrobat +Aktfoto +Aktivist +Albatros +Alchimie +Alemanne +Alibi +Alkohol +Allee +Allüre +Almosen +Almweide +Aloe +Alpaka +Alpental +Alphabet +Alpinist +Alraune +Altbier +Alter +Altflöte +Altruist +Alublech +Aludose +Amateur +Amazonas +Ameise +Amnesie +Amok +Ampel +Amphibie +Ampulle +Amsel +Amulett +Anakonda +Analogie +Ananas +Anarchie +Anatomie +Anbau +Anbeginn +anbieten +Anblick +ändern +andocken +Andrang +anecken +Anflug +Anfrage +Anführer +Angebot +Angler +Anhalter +Anhöhe +Animator +Anis +Anker +ankleben +Ankunft +Anlage +anlocken +Anmut +Annahme +Anomalie +Anonymus +Anorak +anpeilen +Anrecht +Anruf +Ansage +Anschein +Ansicht +Ansporn +Anteil +Antlitz +Antrag +Antwort +Anwohner +Aorta +Apfel +Appetit +Applaus +Aquarium +Arbeit +Arche +Argument +Arktis +Armband +Aroma +Asche +Askese +Asphalt +Asteroid +Ästhetik +Astronom +Atelier +Athlet +Atlantik +Atmung +Audienz +aufatmen +Auffahrt +aufholen +aufregen +Aufsatz +Auftritt +Aufwand +Augapfel +Auktion +Ausbruch +Ausflug +Ausgabe +Aushilfe +Ausland +Ausnahme +Aussage +Autobahn +Avocado +Axthieb +Bach +backen +Badesee +Bahnhof +Balance +Balkon +Ballett +Balsam +Banane +Bandage +Bankett +Barbar +Barde +Barett +Bargeld +Barkasse +Barriere +Bart +Bass +Bastler +Batterie +Bauch +Bauer +Bauholz +Baujahr +Baum +Baustahl +Bauteil +Bauweise +Bazar +beachten +Beatmung +beben +Becher +Becken +bedanken +beeilen +beenden +Beere +befinden +Befreier +Begabung +Begierde +begrüßen +Beiboot +Beichte +Beifall +Beigabe +Beil +Beispiel +Beitrag +beizen +bekommen +beladen +Beleg +bellen +belohnen +Bemalung +Bengel +Benutzer +Benzin +beraten +Bereich +Bergluft +Bericht +Bescheid +Besitz +besorgen +Bestand +Besuch +betanken +beten +betören +Bett +Beule +Beute +Bewegung +bewirken +Bewohner +bezahlen +Bezug +biegen +Biene +Bierzelt +bieten +Bikini +Bildung +Billard +binden +Biobauer +Biologe +Bionik +Biotop +Birke +Bison +Bitte +Biwak +Bizeps +blasen +Blatt +Blauwal +Blende +Blick +Blitz +Blockade +Blödelei +Blondine +Blues +Blume +Blut +Bodensee +Bogen +Boje +Bollwerk +Bonbon +Bonus +Boot +Bordarzt +Börse +Böschung +Boudoir +Boxkampf +Boykott +Brahms +Brandung +Brauerei +Brecher +Breitaxt +Bremse +brennen +Brett +Brief +Brigade +Brillanz +bringen +brodeln +Brosche +Brötchen +Brücke +Brunnen +Brüste +Brutofen +Buch +Büffel +Bugwelle +Bühne +Buletten +Bullauge +Bumerang +bummeln +Buntglas +Bürde +Burgherr +Bursche +Busen +Buslinie +Bussard +Butangas +Butter +Cabrio +campen +Captain +Cartoon +Cello +Chalet +Charisma +Chefarzt +Chiffon +Chipsatz +Chirurg +Chor +Chronik +Chuzpe +Clubhaus +Cockpit +Codewort +Cognac +Coladose +Computer +Coupon +Cousin +Cracking +Crash +Curry +Dach +Dackel +daddeln +daliegen +Dame +Dammbau +Dämon +Dampflok +Dank +Darm +Datei +Datsche +Datteln +Datum +Dauer +Daunen +Deckel +Decoder +Defekt +Degen +Dehnung +Deiche +Dekade +Dekor +Delfin +Demut +denken +Deponie +Design +Desktop +Dessert +Detail +Detektiv +Dezibel +Diadem +Diagnose +Dialekt +Diamant +Dichter +Dickicht +Diesel +Diktat +Diplom +Direktor +Dirne +Diskurs +Distanz +Docht +Dohle +Dolch +Domäne +Donner +Dorade +Dorf +Dörrobst +Dorsch +Dossier +Dozent +Drachen +Draht +Drama +Drang +Drehbuch +Dreieck +Dressur +Drittel +Drossel +Druck +Duell +Duft +Düne +Dünung +dürfen +Duschbad +Düsenjet +Dynamik +Ebbe +Echolot +Echse +Eckball +Edding +Edelweiß +Eden +Edition +Efeu +Effekte +Egoismus +Ehre +Eiablage +Eiche +Eidechse +Eidotter +Eierkopf +Eigelb +Eiland +Eilbote +Eimer +einatmen +Einband +Eindruck +Einfall +Eingang +Einkauf +einladen +Einöde +Einrad +Eintopf +Einwurf +Einzug +Eisbär +Eisen +Eishöhle +Eismeer +Eiweiß +Ekstase +Elan +Elch +Elefant +Eleganz +Element +Elfe +Elite +Elixier +Ellbogen +Eloquenz +Emigrant +Emission +Emotion +Empathie +Empfang +Endzeit +Energie +Engpass +Enkel +Enklave +Ente +entheben +Entität +entladen +Entwurf +Episode +Epoche +erachten +Erbauer +erblühen +Erdbeere +Erde +Erdgas +Erdkunde +Erdnuss +Erdöl +Erdteil +Ereignis +Eremit +erfahren +Erfolg +erfreuen +erfüllen +Ergebnis +erhitzen +erkalten +erkennen +erleben +Erlösung +ernähren +erneuern +Ernte +Eroberer +eröffnen +Erosion +Erotik +Erpel +erraten +Erreger +erröten +Ersatz +Erstflug +Ertrag +Eruption +erwarten +erwidern +Erzbau +Erzeuger +erziehen +Esel +Eskimo +Eskorte +Espe +Espresso +essen +Etage +Etappe +Etat +Ethik +Etikett +Etüde +Eule +Euphorie +Europa +Everest +Examen +Exil +Exodus +Extrakt +Fabel +Fabrik +Fachmann +Fackel +Faden +Fagott +Fahne +Faible +Fairness +Fakt +Fakultät +Falke +Fallobst +Fälscher +Faltboot +Familie +Fanclub +Fanfare +Fangarm +Fantasie +Farbe +Farmhaus +Farn +Fasan +Faser +Fassung +fasten +Faulheit +Fauna +Faust +Favorit +Faxgerät +Fazit +fechten +Federboa +Fehler +Feier +Feige +feilen +Feinripp +Feldbett +Felge +Fellpony +Felswand +Ferien +Ferkel +Fernweh +Ferse +Fest +Fettnapf +Feuer +Fiasko +Fichte +Fiktion +Film +Filter +Filz +Finanzen +Findling +Finger +Fink +Finnwal +Fisch +Fitness +Fixpunkt +Fixstern +Fjord +Flachbau +Flagge +Flamenco +Flanke +Flasche +Flaute +Fleck +Flegel +flehen +Fleisch +fliegen +Flinte +Flirt +Flocke +Floh +Floskel +Floß +Flöte +Flugzeug +Flunder +Flusstal +Flutung +Fockmast +Fohlen +Föhnlage +Fokus +folgen +Foliant +Folklore +Fontäne +Förde +Forelle +Format +Forscher +Fortgang +Forum +Fotograf +Frachter +Fragment +Fraktion +fräsen +Frauenpo +Freak +Fregatte +Freiheit +Freude +Frieden +Frohsinn +Frosch +Frucht +Frühjahr +Fuchs +Fügung +fühlen +Füller +Fundbüro +Funkboje +Funzel +Furnier +Fürsorge +Fusel +Fußbad +Futteral +Gabelung +gackern +Gage +gähnen +Galaxie +Galeere +Galopp +Gameboy +Gamsbart +Gandhi +Gang +Garage +Gardine +Garküche +Garten +Gasthaus +Gattung +gaukeln +Gazelle +Gebäck +Gebirge +Gebräu +Geburt +Gedanke +Gedeck +Gedicht +Gefahr +Gefieder +Geflügel +Gefühl +Gegend +Gehirn +Gehöft +Gehweg +Geige +Geist +Gelage +Geld +Gelenk +Gelübde +Gemälde +Gemeinde +Gemüse +genesen +Genuss +Gepäck +Geranie +Gericht +Germane +Geruch +Gesang +Geschenk +Gesetz +Gesindel +Gesöff +Gespan +Gestade +Gesuch +Getier +Getränk +Getümmel +Gewand +Geweih +Gewitter +Gewölbe +Geysir +Giftzahn +Gipfel +Giraffe +Gitarre +glänzen +Glasauge +Glatze +Gleis +Globus +Glück +glühen +Glutofen +Goldzahn +Gondel +gönnen +Gottheit +graben +Grafik +Grashalm +Graugans +greifen +Grenze +grillen +Groschen +Grotte +Grube +Grünalge +Gruppe +gruseln +Gulasch +Gummibär +Gurgel +Gürtel +Güterzug +Haarband +Habicht +hacken +hadern +Hafen +Hagel +Hähnchen +Haifisch +Haken +Halbaffe +Halsader +halten +Halunke +Handbuch +Hanf +Harfe +Harnisch +härten +Harz +Hasenohr +Haube +hauchen +Haupt +Haut +Havarie +Hebamme +hecheln +Heck +Hedonist +Heiler +Heimat +Heizung +Hektik +Held +helfen +Helium +Hemd +hemmen +Hengst +Herd +Hering +Herkunft +Hermelin +Herrchen +Herzdame +Heulboje +Hexe +Hilfe +Himbeere +Himmel +Hingabe +hinhören +Hinweis +Hirsch +Hirte +Hitzkopf +Hobel +Hochform +Hocker +hoffen +Hofhund +Hofnarr +Höhenzug +Hohlraum +Hölle +Holzboot +Honig +Honorar +horchen +Hörprobe +Höschen +Hotel +Hubraum +Hufeisen +Hügel +huldigen +Hülle +Humbug +Hummer +Humor +Hund +Hunger +Hupe +Hürde +Hurrikan +Hydrant +Hypnose +Ibis +Idee +Idiot +Igel +Illusion +Imitat +impfen +Import +Inferno +Ingwer +Inhalte +Inland +Insekt +Ironie +Irrfahrt +Irrtum +Isolator +Istwert +Jacke +Jade +Jagdhund +Jäger +Jaguar +Jahr +Jähzorn +Jazzfest +Jetpilot +jobben +Jochbein +jodeln +Jodsalz +Jolle +Journal +Jubel +Junge +Junimond +Jupiter +Jutesack +Juwel +Kabarett +Kabine +Kabuff +Käfer +Kaffee +Kahlkopf +Kaimauer +Kajüte +Kaktus +Kaliber +Kaltluft +Kamel +kämmen +Kampagne +Kanal +Känguru +Kanister +Kanone +Kante +Kanu +kapern +Kapitän +Kapuze +Karneval +Karotte +Käsebrot +Kasper +Kastanie +Katalog +Kathode +Katze +kaufen +Kaugummi +Kauz +Kehle +Keilerei +Keksdose +Kellner +Keramik +Kerze +Kessel +Kette +keuchen +kichern +Kielboot +Kindheit +Kinnbart +Kinosaal +Kiosk +Kissen +Klammer +Klang +Klapprad +Klartext +kleben +Klee +Kleinod +Klima +Klingel +Klippe +Klischee +Kloster +Klugheit +Klüngel +kneten +Knie +Knöchel +knüpfen +Kobold +Kochbuch +Kohlrabi +Koje +Kokosöl +Kolibri +Kolumne +Kombüse +Komiker +kommen +Konto +Konzept +Kopfkino +Kordhose +Korken +Korsett +Kosename +Krabbe +Krach +Kraft +Krähe +Kralle +Krapfen +Krater +kraulen +Kreuz +Krokodil +Kröte +Kugel +Kuhhirt +Kühnheit +Künstler +Kurort +Kurve +Kurzfilm +kuscheln +küssen +Kutter +Labor +lachen +Lackaffe +Ladeluke +Lagune +Laib +Lakritze +Lammfell +Land +Langmut +Lappalie +Last +Laterne +Latzhose +Laubsäge +laufen +Laune +Lausbub +Lavasee +Leben +Leder +Leerlauf +Lehm +Lehrer +leihen +Lektüre +Lenker +Lerche +Leseecke +Leuchter +Lexikon +Libelle +Libido +Licht +Liebe +liefern +Liftboy +Limonade +Lineal +Linoleum +List +Liveband +Lobrede +locken +Löffel +Logbuch +Logik +Lohn +Loipe +Lokal +Lorbeer +Lösung +löten +Lottofee +Löwe +Luchs +Luder +Luftpost +Luke +Lümmel +Lunge +lutschen +Luxus +Macht +Magazin +Magier +Magnet +mähen +Mahlzeit +Mahnmal +Maibaum +Maisbrei +Makel +malen +Mammut +Maniküre +Mantel +Marathon +Marder +Marine +Marke +Marmor +Märzluft +Maske +Maßanzug +Maßkrug +Mastkorb +Material +Matratze +Mauerbau +Maulkorb +Mäuschen +Mäzen +Medium +Meinung +melden +Melodie +Mensch +Merkmal +Messe +Metall +Meteor +Methode +Metzger +Mieze +Milchkuh +Mimose +Minirock +Minute +mischen +Missetat +mitgehen +Mittag +Mixtape +Möbel +Modul +mögen +Möhre +Molch +Moment +Monat +Mondflug +Monitor +Monokini +Monster +Monument +Moorhuhn +Moos +Möpse +Moral +Mörtel +Motiv +Motorrad +Möwe +Mühe +Mulatte +Müller +Mumie +Mund +Münze +Muschel +Muster +Mythos +Nabel +Nachtzug +Nackedei +Nagel +Nähe +Nähnadel +Namen +Narbe +Narwal +Nasenbär +Natur +Nebel +necken +Neffe +Neigung +Nektar +Nenner +Neptun +Nerz +Nessel +Nestbau +Netz +Neubau +Neuerung +Neugier +nicken +Niere +Nilpferd +nisten +Nocke +Nomade +Nordmeer +Notdurft +Notstand +Notwehr +Nudismus +Nuss +Nutzhanf +Oase +Obdach +Oberarzt +Objekt +Oboe +Obsthain +Ochse +Odyssee +Ofenholz +öffnen +Ohnmacht +Ohrfeige +Ohrwurm +Ökologie +Oktave +Ölberg +Olive +Ölkrise +Omelett +Onkel +Oper +Optiker +Orange +Orchidee +ordnen +Orgasmus +Orkan +Ortskern +Ortung +Ostasien +Ozean +Paarlauf +Packeis +paddeln +Paket +Palast +Pandabär +Panik +Panorama +Panther +Papagei +Papier +Paprika +Paradies +Parka +Parodie +Partner +Passant +Patent +Patzer +Pause +Pavian +Pedal +Pegel +peilen +Perle +Person +Pfad +Pfau +Pferd +Pfleger +Physik +Pier +Pilotwal +Pinzette +Piste +Plakat +Plankton +Platin +Plombe +plündern +Pobacke +Pokal +polieren +Popmusik +Porträt +Posaune +Postamt +Pottwal +Pracht +Pranke +Preis +Primat +Prinzip +Protest +Proviant +Prüfung +Pubertät +Pudding +Pullover +Pulsader +Punkt +Pute +Putsch +Puzzle +Python +quaken +Qualle +Quark +Quellsee +Querkopf +Quitte +Quote +Rabauke +Rache +Radclub +Radhose +Radio +Radtour +Rahmen +Rampe +Randlage +Ranzen +Rapsöl +Raserei +rasten +Rasur +Rätsel +Raubtier +Raumzeit +Rausch +Reaktor +Realität +Rebell +Rede +Reetdach +Regatta +Regen +Rehkitz +Reifen +Reim +Reise +Reizung +Rekord +Relevanz +Rennboot +Respekt +Restmüll +retten +Reue +Revolte +Rhetorik +Rhythmus +Richtung +Riegel +Rindvieh +Rippchen +Ritter +Robbe +Roboter +Rockband +Rohdaten +Roller +Roman +röntgen +Rose +Rosskur +Rost +Rotahorn +Rotglut +Rotznase +Rubrik +Rückweg +Rufmord +Ruhe +Ruine +Rumpf +Runde +Rüstung +rütteln +Saaltür +Saatguts +Säbel +Sachbuch +Sack +Saft +sagen +Sahneeis +Salat +Salbe +Salz +Sammlung +Samt +Sandbank +Sanftmut +Sardine +Satire +Sattel +Satzbau +Sauerei +Saum +Säure +Schall +Scheitel +Schiff +Schlager +Schmied +Schnee +Scholle +Schrank +Schulbus +Schwan +Seeadler +Seefahrt +Seehund +Seeufer +segeln +Sehnerv +Seide +Seilzug +Senf +Sessel +Seufzer +Sexgott +Sichtung +Signal +Silber +singen +Sinn +Sirup +Sitzbank +Skandal +Skikurs +Skipper +Skizze +Smaragd +Socke +Sohn +Sommer +Songtext +Sorte +Spagat +Spannung +Spargel +Specht +Speiseöl +Spiegel +Sport +spülen +Stadtbus +Stall +Stärke +Stativ +staunen +Stern +Stiftung +Stollen +Strömung +Sturm +Substanz +Südalpen +Sumpf +surfen +Tabak +Tafel +Tagebau +takeln +Taktung +Talsohle +Tand +Tanzbär +Tapir +Tarantel +Tarnname +Tasse +Tatnacht +Tatsache +Tatze +Taube +tauchen +Taufpate +Taumel +Teelicht +Teich +teilen +Tempo +Tenor +Terrasse +Testflug +Theater +Thermik +ticken +Tiefflug +Tierart +Tigerhai +Tinte +Tischler +toben +Toleranz +Tölpel +Tonband +Topf +Topmodel +Torbogen +Torlinie +Torte +Tourist +Tragesel +trampeln +Trapez +Traum +treffen +Trennung +Treue +Trick +trimmen +Trödel +Trost +Trumpf +tüfteln +Turban +Turm +Übermut +Ufer +Uhrwerk +umarmen +Umbau +Umfeld +Umgang +Umsturz +Unart +Unfug +Unimog +Unruhe +Unwucht +Uranerz +Urlaub +Urmensch +Utopie +Vakuum +Valuta +Vandale +Vase +Vektor +Ventil +Verb +Verdeck +Verfall +Vergaser +verhexen +Verlag +Vers +Vesper +Vieh +Viereck +Vinyl +Virus +Vitrine +Vollblut +Vorbote +Vorrat +Vorsicht +Vulkan +Wachstum +Wade +Wagemut +Wahlen +Wahrheit +Wald +Walhai +Wallach +Walnuss +Walzer +wandeln +Wanze +wärmen +Warnruf +Wäsche +Wasser +Weberei +wechseln +Wegegeld +wehren +Weiher +Weinglas +Weißbier +Weitwurf +Welle +Weltall +Werkbank +Werwolf +Wetter +wiehern +Wildgans +Wind +Wohl +Wohnort +Wolf +Wollust +Wortlaut +Wrack +Wunder +Wurfaxt +Wurst +Yacht +Yeti +Zacke +Zahl +zähmen +Zahnfee +Zäpfchen +Zaster +Zaumzeug +Zebra +zeigen +Zeitlupe +Zellkern +Zeltdach +Zensor +Zerfall +Zeug +Ziege +Zielfoto +Zimteis +Zobel +Zollhund +Zombie +Zöpfe +Zucht +Zufahrt +Zugfahrt +Zugvogel +Zündung +Zweck +Zyklop diff --git a/src/mnemonics/languages/italian.txt b/src/mnemonics/languages/italian.txt new file mode 100644 index 000000000..16613df41 --- /dev/null +++ b/src/mnemonics/languages/italian.txt @@ -0,0 +1,1629 @@ +Italian +Italiano +4 +abbinare +abbonato +abisso +abitare +abominio +accadere +accesso +acciaio +accordo +accumulo +acido +acqua +acrobata +acustico +adattare +addetto +addio +addome +adeguato +aderire +adorare +adottare +adozione +adulto +aereo +aerobica +affare +affetto +affidare +affogato +affronto +africano +afrodite +agenzia +aggancio +aggeggio +aggiunta +agio +agire +agitare +aglio +agnello +agosto +aiutare +albero +albo +alce +alchimia +alcool +alfabeto +algebra +alimento +allarme +alleanza +allievo +alloggio +alluce +alpi +alterare +altro +aluminio +amante +amarezza +ambiente +ambrosia +america +amico +ammalare +ammirare +amnesia +amnistia +amore +ampliare +amputare +analisi +anamnesi +ananas +anarchia +anatra +anca +ancorato +andare +androide +aneddoto +anello +angelo +angolino +anguilla +anidride +anima +annegare +anno +annuncio +anomalia +antenna +anticipo +aperto +apostolo +appalto +appello +appiglio +applauso +appoggio +appurare +aprile +aquila +arabo +arachidi +aragosta +arancia +arbitrio +archivio +arco +argento +argilla +aria +ariete +arma +armonia +aroma +arrivare +arrosto +arsenale +arte +artiglio +asfalto +asfissia +asino +asparagi +aspirina +assalire +assegno +assolto +assurdo +asta +astratto +atlante +atletica +atomo +atropina +attacco +attesa +attico +atto +attrarre +auguri +aula +aumento +aurora +auspicio +autista +auto +autunno +avanzare +avarizia +avere +aviatore +avido +avorio +avvenire +avviso +avvocato +azienda +azione +azzardo +azzurro +babbuino +bacio +badante +baffi +bagaglio +bagliore +bagno +balcone +balena +ballare +balordo +balsamo +bambola +bancomat +banda +barato +barba +barista +barriera +basette +basilico +bassista +bastare +battello +bavaglio +beccare +beduino +bellezza +bene +benzina +berretto +bestia +bevitore +bianco +bibbia +biberon +bibita +bici +bidone +bilancia +biliardo +binario +binocolo +biologia +biondina +biopsia +biossido +birbante +birra +biscotto +bisogno +bistecca +bivio +blindare +bloccare +bocca +bollire +bombola +bonifico +borghese +borsa +bottino +botulino +braccio +bradipo +branco +bravo +bresaola +bretelle +brevetto +briciola +brigante +brillare +brindare +brivido +broccoli +brontolo +bruciare +brufolo +bucare +buddista +budino +bufera +buffo +bugiardo +buio +buono +burrone +bussola +bustina +buttare +cabernet +cabina +cacao +cacciare +cactus +cadavere +caffe +calamari +calcio +caldaia +calmare +calunnia +calvario +calzone +cambiare +camera +camion +cammello +campana +canarino +cancello +candore +cane +canguro +cannone +canoa +cantare +canzone +caos +capanna +capello +capire +capo +capperi +capra +capsula +caraffa +carbone +carciofo +cardigan +carenza +caricare +carota +carrello +carta +casa +cascare +caserma +cashmere +casino +cassetta +castello +catalogo +catena +catorcio +cattivo +causa +cauzione +cavallo +caverna +caviglia +cavo +cazzotto +celibato +cemento +cenare +centrale +ceramica +cercare +ceretta +cerniera +certezza +cervello +cessione +cestino +cetriolo +chiave +chiedere +chilo +chimera +chiodo +chirurgo +chitarra +chiudere +ciabatta +ciao +cibo +ciccia +cicerone +ciclone +cicogna +cielo +cifra +cigno +ciliegia +cimitero +cinema +cinque +cintura +ciondolo +ciotola +cipolla +cippato +circuito +cisterna +citofono +ciuccio +civetta +civico +clausola +cliente +clima +clinica +cobra +coccole +cocktail +cocomero +codice +coesione +cogliere +cognome +colla +colomba +colpire +coltello +comando +comitato +commedia +comodino +compagna +comune +concerto +condotto +conforto +congiura +coniglio +consegna +conto +convegno +coperta +copia +coprire +corazza +corda +corleone +cornice +corona +corpo +corrente +corsa +cortesia +corvo +coso +costume +cotone +cottura +cozza +crampo +cratere +cravatta +creare +credere +crema +crescere +crimine +criterio +croce +crollare +cronaca +crostata +croupier +cubetto +cucciolo +cucina +cultura +cuoco +cuore +cupido +cupola +cura +curva +cuscino +custode +danzare +data +decennio +decidere +decollo +dedicare +dedurre +definire +delegare +delfino +delitto +demone +dentista +denuncia +deposito +derivare +deserto +designer +destino +detonare +dettagli +diagnosi +dialogo +diamante +diario +diavolo +dicembre +difesa +digerire +digitare +diluvio +dinamica +dipinto +diploma +diramare +dire +dirigere +dirupo +discesa +disdetta +disegno +disporre +dissenso +distacco +dito +ditta +diva +divenire +dividere +divorare +docente +dolcetto +dolore +domatore +domenica +dominare +donatore +donna +dorato +dormire +dorso +dosaggio +dottore +dovere +download +dragone +dramma +dubbio +dubitare +duetto +durata +ebbrezza +eccesso +eccitare +eclissi +economia +edera +edificio +editore +edizione +educare +effetto +egitto +egiziano +elastico +elefante +eleggere +elemento +elenco +elezione +elmetto +elogio +embrione +emergere +emettere +eminenza +emisfero +emozione +empatia +energia +enfasi +enigma +entrare +enzima +epidemia +epilogo +episodio +epoca +equivoco +erba +erede +eroe +erotico +errore +eruzione +esaltare +esame +esaudire +eseguire +esempio +esigere +esistere +esito +esperto +espresso +essere +estasi +esterno +estrarre +eterno +etica +euforico +europa +evacuare +evasione +evento +evidenza +evitare +evolvere +fabbrica +facciata +fagiano +fagotto +falco +fame +famiglia +fanale +fango +fantasia +farfalla +farmacia +faro +fase +fastidio +faticare +fatto +favola +febbre +femmina +femore +fenomeno +fermata +feromoni +ferrari +fessura +festa +fiaba +fiamma +fianco +fiat +fibbia +fidare +fieno +figa +figlio +figura +filetto +filmato +filosofo +filtrare +finanza +finestra +fingere +finire +finta +finzione +fiocco +fioraio +firewall +firmare +fisico +fissare +fittizio +fiume +flacone +flagello +flirtare +flusso +focaccia +foglio +fognario +follia +fonderia +fontana +forbici +forcella +foresta +forgiare +formare +fornace +foro +fortuna +forzare +fosforo +fotoni +fracasso +fragola +frantumi +fratello +frazione +freccia +freddo +frenare +fresco +friggere +frittata +frivolo +frizione +fronte +frullato +frumento +frusta +frutto +fucile +fuggire +fulmine +fumare +funzione +fuoco +furbizia +furgone +furia +furore +fusibile +fuso +futuro +gabbiano +galassia +gallina +gamba +gancio +garanzia +garofano +gasolio +gatto +gazebo +gazzetta +gelato +gemelli +generare +genitori +gennaio +geologia +germania +gestire +gettare +ghepardo +ghiaccio +giaccone +giaguaro +giallo +giappone +giardino +gigante +gioco +gioiello +giorno +giovane +giraffa +giudizio +giurare +giusto +globo +gloria +glucosio +gnocca +gocciola +godere +gomito +gomma +gonfiare +gorilla +governo +gradire +graffiti +granchio +grappolo +grasso +grattare +gridare +grissino +grondaia +grugnito +gruppo +guadagno +guaio +guancia +guardare +gufo +guidare +guscio +gusto +icona +idea +identico +idolo +idoneo +idrante +idrogeno +igiene +ignoto +imbarco +immagine +immobile +imparare +impedire +impianto +importo +impresa +impulso +incanto +incendio +incidere +incontro +incrocia +incubo +indagare +indice +indotto +infanzia +inferno +infinito +infranto +ingerire +inglese +ingoiare +ingresso +iniziare +innesco +insalata +inserire +insicuro +insonnia +insulto +interno +introiti +invasori +inverno +invito +invocare +ipnosi +ipocrita +ipotesi +ironia +irrigare +iscritto +isola +ispirare +isterico +istinto +istruire +italiano +jazz +labbra +labrador +ladro +lago +lamento +lampone +lancetta +lanterna +lapide +larva +lasagne +lasciare +lastra +latte +laurea +lavagna +lavorare +leccare +legare +leggere +lenzuolo +leone +lepre +letargo +lettera +levare +levitare +lezione +liberare +libidine +libro +licenza +lievito +limite +lince +lingua +liquore +lire +listino +litigare +litro +locale +lottare +lucciola +lucidare +luglio +luna +macchina +madama +madre +maestro +maggio +magico +maglione +magnolia +mago +maialino +maionese +malattia +male +malloppo +mancare +mandorla +mangiare +manico +manopola +mansarda +mantello +manubrio +manzo +mappa +mare +margine +marinaio +marmotta +marocco +martello +marzo +maschera +matrice +maturare +mazzetta +meandri +medaglia +medico +medusa +megafono +melone +membrana +menta +mercato +meritare +merluzzo +mese +mestiere +metafora +meteo +metodo +mettere +miele +miglio +miliardo +mimetica +minatore +minuto +miracolo +mirtillo +missile +mistero +misura +mito +mobile +moda +moderare +moglie +molecola +molle +momento +moneta +mongolia +monologo +montagna +morale +morbillo +mordere +mosaico +mosca +mostro +motivare +moto +mulino +mulo +muovere +muraglia +muscolo +museo +musica +mutande +nascere +nastro +natale +natura +nave +navigare +negare +negozio +nemico +nero +nervo +nessuno +nettare +neutroni +neve +nevicare +nicotina +nido +nipote +nocciola +noleggio +nome +nonno +norvegia +notare +notizia +nove +nucleo +nuda +nuotare +nutrire +obbligo +occhio +occupare +oceano +odissea +odore +offerta +officina +offrire +oggetto +oggi +olfatto +olio +oliva +ombelico +ombrello +omuncolo +ondata +onore +opera +opinione +opuscolo +opzione +orario +orbita +orchidea +ordine +orecchio +orgasmo +orgoglio +origine +orologio +oroscopo +orso +oscurare +ospedale +ospite +ossigeno +ostacolo +ostriche +ottenere +ottimo +ottobre +ovest +pacco +pace +pacifico +padella +pagare +pagina +pagnotta +palazzo +palestra +palpebre +pancetta +panfilo +panino +pannello +panorama +papa +paperino +paradiso +parcella +parente +parlare +parodia +parrucca +partire +passare +pasta +patata +patente +patogeno +patriota +pausa +pazienza +peccare +pecora +pedalare +pelare +pena +pendenza +penisola +pennello +pensare +pentirsi +percorso +perdono +perfetto +perizoma +perla +permesso +persona +pesare +pesce +peso +petardo +petrolio +pezzo +piacere +pianeta +piastra +piatto +piazza +piccolo +piede +piegare +pietra +pigiama +pigliare +pigrizia +pilastro +pilota +pinguino +pioggia +piombo +pionieri +piovra +pipa +pirata +pirolisi +piscina +pisolino +pista +pitone +piumino +pizza +plastica +platino +poesia +poiana +polaroid +polenta +polimero +pollo +polmone +polpetta +poltrona +pomodoro +pompa +popolo +porco +porta +porzione +possesso +postino +potassio +potere +poverino +pranzo +prato +prefisso +prelievo +premio +prendere +prestare +pretesa +prezzo +primario +privacy +problema +processo +prodotto +profeta +progetto +promessa +pronto +proposta +proroga +prossimo +proteina +prova +prudenza +pubblico +pudore +pugilato +pulire +pulsante +puntare +pupazzo +puzzle +quaderno +qualcuno +quarzo +quercia +quintale +rabbia +racconto +radice +raffica +ragazza +ragione +rammento +ramo +rana +randagio +rapace +rapinare +rapporto +rasatura +ravioli +reagire +realista +reattore +reazione +recitare +recluso +record +recupero +redigere +regalare +regina +regola +relatore +reliquia +remare +rendere +reparto +resina +resto +rete +retorica +rettile +revocare +riaprire +ribadire +ribelle +ricambio +ricetta +richiamo +ricordo +ridurre +riempire +riferire +riflesso +righello +rilancio +rilevare +rilievo +rimanere +rimborso +rinforzo +rinuncia +riparo +ripetere +riposare +ripulire +risalita +riscatto +riserva +riso +rispetto +ritaglio +ritmo +ritorno +ritratto +rituale +riunione +riuscire +riva +robotica +rondine +rosa +rospo +rosso +rotonda +rotta +roulotte +rubare +rubrica +ruffiano +rumore +ruota +ruscello +sabbia +sacco +saggio +sale +salire +salmone +salto +salutare +salvia +sangue +sanzioni +sapere +sapienza +sarcasmo +sardine +sartoria +sbalzo +sbarcare +sberla +sborsare +scadenza +scafo +scala +scambio +scappare +scarpa +scatola +scelta +scena +sceriffo +scheggia +schiuma +sciarpa +scienza +scimmia +sciopero +scivolo +sclerare +scolpire +sconto +scopa +scordare +scossa +scrivere +scrupolo +scuderia +scultore +scuola +scusare +sdraiare +secolo +sedativo +sedere +sedia +segare +segreto +seguire +semaforo +seme +senape +seno +sentiero +separare +sepolcro +sequenza +serata +serpente +servizio +sesso +seta +settore +sfamare +sfera +sfidare +sfiorare +sfogare +sgabello +sicuro +siepe +sigaro +silenzio +silicone +simbiosi +simpatia +simulare +sinapsi +sindrome +sinergia +sinonimo +sintonia +sirena +siringa +sistema +sito +smalto +smentire +smontare +soccorso +socio +soffitto +software +soggetto +sogliola +sognare +soldi +sole +sollievo +solo +sommario +sondare +sonno +sorpresa +sorriso +sospiro +sostegno +sovrano +spaccare +spada +spagnolo +spalla +sparire +spavento +spazio +specchio +spedire +spegnere +spendere +speranza +spessore +spezzare +spiaggia +spiccare +spiegare +spiffero +spingere +sponda +sporcare +spostare +spremuta +spugna +spumante +spuntare +squadra +squillo +staccare +stadio +stagione +stallone +stampa +stancare +starnuto +statura +stella +stendere +sterzo +stilista +stimolo +stinco +stiva +stoffa +storia +strada +stregone +striscia +studiare +stufa +stupendo +subire +successo +sudare +suono +superare +supporto +surfista +sussurro +svelto +svenire +sviluppo +svolta +svuotare +tabacco +tabella +tabu +tacchino +tacere +taglio +talento +tangente +tappeto +tartufo +tassello +tastiera +tavolo +tazza +teatro +tedesco +telaio +telefono +tema +temere +tempo +tendenza +tenebre +tensione +tentare +teologia +teorema +termica +terrazzo +teschio +tesi +tesoro +tessera +testa +thriller +tifoso +tigre +timbrare +timido +tinta +tirare +tisana +titano +titolo +toccare +togliere +topolino +torcia +torrente +tovaglia +traffico +tragitto +training +tramonto +transito +trapezio +trasloco +trattore +trazione +treccia +tregua +treno +triciclo +tridente +trilogia +tromba +troncare +trota +trovare +trucco +tubo +tulipano +tumulto +tunisia +tuono +turista +tuta +tutelare +tutore +ubriaco +uccello +udienza +udito +uffa +umanoide +umore +unghia +unguento +unicorno +unione +universo +uomo +uragano +uranio +urlare +uscire +utente +utilizzo +vacanza +vacca +vaglio +vagonata +valle +valore +valutare +valvola +vampiro +vaniglia +vanto +vapore +variante +vasca +vaselina +vassoio +vedere +vegetale +veglia +veicolo +vela +veleno +velivolo +velluto +vendere +venerare +venire +vento +veranda +verbo +verdura +vergine +verifica +vernice +vero +verruca +versare +vertebra +vescica +vespaio +vestito +vesuvio +veterano +vetro +vetta +viadotto +viaggio +vibrare +vicenda +vichingo +vietare +vigilare +vigneto +villa +vincere +violino +vipera +virgola +virtuoso +visita +vita +vitello +vittima +vivavoce +vivere +viziato +voglia +volare +volpe +volto +volume +vongole +voragine +vortice +votare +vulcano +vuotare +zabaione +zaffiro +zainetto +zampa +zanzara +zattera +zavorra +zenzero +zero +zingaro +zittire +zoccolo +zolfo +zombie +zucchero diff --git a/src/mnemonics/languages/japanese.txt b/src/mnemonics/languages/japanese.txt new file mode 100644 index 000000000..bbf1f4c4d --- /dev/null +++ b/src/mnemonics/languages/japanese.txt @@ -0,0 +1,1629 @@ +Japanese +日本語 +3 +あいこくしん +あいさつ +あいだ +あおぞら +あかちゃん +あきる +あけがた +あける +あこがれる +あさい +あさひ +あしあと +あじわう +あずかる +あずき +あそぶ +あたえる +あたためる +あたりまえ +あたる +あつい +あつかう +あっしゅく +あつまり +あつめる +あてな +あてはまる +あひる +あぶら +あぶる +あふれる +あまい +あまど +あまやかす +あまり +あみもの +あめりか +あやまる +あゆむ +あらいぐま +あらし +あらすじ +あらためる +あらゆる +あらわす +ありがとう +あわせる +あわてる +あんい +あんがい +あんこ +あんぜん +あんてい +あんない +あんまり +いいだす +いおん +いがい +いがく +いきおい +いきなり +いきもの +いきる +いくじ +いくぶん +いけばな +いけん +いこう +いこく +いこつ +いさましい +いさん +いしき +いじゅう +いじょう +いじわる +いずみ +いずれ +いせい +いせえび +いせかい +いせき +いぜん +いそうろう +いそがしい +いだい +いだく +いたずら +いたみ +いたりあ +いちおう +いちじ +いちど +いちば +いちぶ +いちりゅう +いつか +いっしゅん +いっせい +いっそう +いったん +いっち +いってい +いっぽう +いてざ +いてん +いどう +いとこ +いない +いなか +いねむり +いのち +いのる +いはつ +いばる +いはん +いびき +いひん +いふく +いへん +いほう +いみん +いもうと +いもたれ +いもり +いやがる +いやす +いよかん +いよく +いらい +いらすと +いりぐち +いりょう +いれい +いれもの +いれる +いろえんぴつ +いわい +いわう +いわかん +いわば +いわゆる +いんげんまめ +いんさつ +いんしょう +いんよう +うえき +うえる +うおざ +うがい +うかぶ +うかべる +うきわ +うくらいな +うくれれ +うけたまわる +うけつけ +うけとる +うけもつ +うける +うごかす +うごく +うこん +うさぎ +うしなう +うしろがみ +うすい +うすぎ +うすぐらい +うすめる +うせつ +うちあわせ +うちがわ +うちき +うちゅう +うっかり +うつくしい +うったえる +うつる +うどん +うなぎ +うなじ +うなずく +うなる +うねる +うのう +うぶげ +うぶごえ +うまれる +うめる +うもう +うやまう +うよく +うらがえす +うらぐち +うらない +うりあげ +うりきれ +うるさい +うれしい +うれゆき +うれる +うろこ +うわき +うわさ +うんこう +うんちん +うんてん +うんどう +えいえん +えいが +えいきょう +えいご +えいせい +えいぶん +えいよう +えいわ +えおり +えがお +えがく +えきたい +えくせる +えしゃく +えすて +えつらん +えのぐ +えほうまき +えほん +えまき +えもじ +えもの +えらい +えらぶ +えりあ +えんえん +えんかい +えんぎ +えんげき +えんしゅう +えんぜつ +えんそく +えんちょう +えんとつ +おいかける +おいこす +おいしい +おいつく +おうえん +おうさま +おうじ +おうせつ +おうたい +おうふく +おうべい +おうよう +おえる +おおい +おおう +おおどおり +おおや +おおよそ +おかえり +おかず +おがむ +おかわり +おぎなう +おきる +おくさま +おくじょう +おくりがな +おくる +おくれる +おこす +おこなう +おこる +おさえる +おさない +おさめる +おしいれ +おしえる +おじぎ +おじさん +おしゃれ +おそらく +おそわる +おたがい +おたく +おだやか +おちつく +おっと +おつり +おでかけ +おとしもの +おとなしい +おどり +おどろかす +おばさん +おまいり +おめでとう +おもいで +おもう +おもたい +おもちゃ +おやつ +おやゆび +およぼす +おらんだ +おろす +おんがく +おんけい +おんしゃ +おんせん +おんだん +おんちゅう +おんどけい +かあつ +かいが +がいき +がいけん +がいこう +かいさつ +かいしゃ +かいすいよく +かいぜん +かいぞうど +かいつう +かいてん +かいとう +かいふく +がいへき +かいほう +かいよう +がいらい +かいわ +かえる +かおり +かかえる +かがく +かがし +かがみ +かくご +かくとく +かざる +がぞう +かたい +かたち +がちょう +がっきゅう +がっこう +がっさん +がっしょう +かなざわし +かのう +がはく +かぶか +かほう +かほご +かまう +かまぼこ +かめれおん +かゆい +かようび +からい +かるい +かろう +かわく +かわら +がんか +かんけい +かんこう +かんしゃ +かんそう +かんたん +かんち +がんばる +きあい +きあつ +きいろ +ぎいん +きうい +きうん +きえる +きおう +きおく +きおち +きおん +きかい +きかく +きかんしゃ +ききて +きくばり +きくらげ +きけんせい +きこう +きこえる +きこく +きさい +きさく +きさま +きさらぎ +ぎじかがく +ぎしき +ぎじたいけん +ぎじにってい +ぎじゅつしゃ +きすう +きせい +きせき +きせつ +きそう +きぞく +きぞん +きたえる +きちょう +きつえん +ぎっちり +きつつき +きつね +きてい +きどう +きどく +きない +きなが +きなこ +きぬごし +きねん +きのう +きのした +きはく +きびしい +きひん +きふく +きぶん +きぼう +きほん +きまる +きみつ +きむずかしい +きめる +きもだめし +きもち +きもの +きゃく +きやく +ぎゅうにく +きよう +きょうりゅう +きらい +きらく +きりん +きれい +きれつ +きろく +ぎろん +きわめる +ぎんいろ +きんかくじ +きんじょ +きんようび +ぐあい +くいず +くうかん +くうき +くうぐん +くうこう +ぐうせい +くうそう +ぐうたら +くうふく +くうぼ +くかん +くきょう +くげん +ぐこう +くさい +くさき +くさばな +くさる +くしゃみ +くしょう +くすのき +くすりゆび +くせげ +くせん +ぐたいてき +くださる +くたびれる +くちこみ +くちさき +くつした +ぐっすり +くつろぐ +くとうてん +くどく +くなん +くねくね +くのう +くふう +くみあわせ +くみたてる +くめる +くやくしょ +くらす +くらべる +くるま +くれる +くろう +くわしい +ぐんかん +ぐんしょく +ぐんたい +ぐんて +けあな +けいかく +けいけん +けいこ +けいさつ +げいじゅつ +けいたい +げいのうじん +けいれき +けいろ +けおとす +けおりもの +げきか +げきげん +げきだん +げきちん +げきとつ +げきは +げきやく +げこう +げこくじょう +げざい +けさき +げざん +けしき +けしごむ +けしょう +げすと +けたば +けちゃっぷ +けちらす +けつあつ +けつい +けつえき +けっこん +けつじょ +けっせき +けってい +けつまつ +げつようび +げつれい +けつろん +げどく +けとばす +けとる +けなげ +けなす +けなみ +けぬき +げねつ +けねん +けはい +げひん +けぶかい +げぼく +けまり +けみかる +けむし +けむり +けもの +けらい +けろけろ +けわしい +けんい +けんえつ +けんお +けんか +げんき +けんげん +けんこう +けんさく +けんしゅう +けんすう +げんそう +けんちく +けんてい +けんとう +けんない +けんにん +げんぶつ +けんま +けんみん +けんめい +けんらん +けんり +こあくま +こいぬ +こいびと +ごうい +こうえん +こうおん +こうかん +ごうきゅう +ごうけい +こうこう +こうさい +こうじ +こうすい +ごうせい +こうそく +こうたい +こうちゃ +こうつう +こうてい +こうどう +こうない +こうはい +ごうほう +ごうまん +こうもく +こうりつ +こえる +こおり +ごかい +ごがつ +ごかん +こくご +こくさい +こくとう +こくない +こくはく +こぐま +こけい +こける +ここのか +こころ +こさめ +こしつ +こすう +こせい +こせき +こぜん +こそだて +こたい +こたえる +こたつ +こちょう +こっか +こつこつ +こつばん +こつぶ +こてい +こてん +ことがら +ことし +ことば +ことり +こなごな +こねこね +このまま +このみ +このよ +ごはん +こひつじ +こふう +こふん +こぼれる +ごまあぶら +こまかい +ごますり +こまつな +こまる +こむぎこ +こもじ +こもち +こもの +こもん +こやく +こやま +こゆう +こゆび +こよい +こよう +こりる +これくしょん +ころっけ +こわもて +こわれる +こんいん +こんかい +こんき +こんしゅう +こんすい +こんだて +こんとん +こんなん +こんびに +こんぽん +こんまけ +こんや +こんれい +こんわく +ざいえき +さいかい +さいきん +ざいげん +ざいこ +さいしょ +さいせい +ざいたく +ざいちゅう +さいてき +ざいりょう +さうな +さかいし +さがす +さかな +さかみち +さがる +さぎょう +さくし +さくひん +さくら +さこく +さこつ +さずかる +ざせき +さたん +さつえい +ざつおん +ざっか +ざつがく +さっきょく +ざっし +さつじん +ざっそう +さつたば +さつまいも +さてい +さといも +さとう +さとおや +さとし +さとる +さのう +さばく +さびしい +さべつ +さほう +さほど +さます +さみしい +さみだれ +さむけ +さめる +さやえんどう +さゆう +さよう +さよく +さらだ +ざるそば +さわやか +さわる +さんいん +さんか +さんきゃく +さんこう +さんさい +ざんしょ +さんすう +さんせい +さんそ +さんち +さんま +さんみ +さんらん +しあい +しあげ +しあさって +しあわせ +しいく +しいん +しうち +しえい +しおけ +しかい +しかく +じかん +しごと +しすう +じだい +したうけ +したぎ +したて +したみ +しちょう +しちりん +しっかり +しつじ +しつもん +してい +してき +してつ +じてん +じどう +しなぎれ +しなもの +しなん +しねま +しねん +しのぐ +しのぶ +しはい +しばかり +しはつ +しはらい +しはん +しひょう +しふく +じぶん +しへい +しほう +しほん +しまう +しまる +しみん +しむける +じむしょ +しめい +しめる +しもん +しゃいん +しゃうん +しゃおん +じゃがいも +しやくしょ +しゃくほう +しゃけん +しゃこ +しゃざい +しゃしん +しゃせん +しゃそう +しゃたい +しゃちょう +しゃっきん +じゃま +しゃりん +しゃれい +じゆう +じゅうしょ +しゅくはく +じゅしん +しゅっせき +しゅみ +しゅらば +じゅんばん +しょうかい +しょくたく +しょっけん +しょどう +しょもつ +しらせる +しらべる +しんか +しんこう +じんじゃ +しんせいじ +しんちく +しんりん +すあげ +すあし +すあな +ずあん +すいえい +すいか +すいとう +ずいぶん +すいようび +すうがく +すうじつ +すうせん +すおどり +すきま +すくう +すくない +すける +すごい +すこし +ずさん +すずしい +すすむ +すすめる +すっかり +ずっしり +ずっと +すてき +すてる +すねる +すのこ +すはだ +すばらしい +ずひょう +ずぶぬれ +すぶり +すふれ +すべて +すべる +ずほう +すぼん +すまい +すめし +すもう +すやき +すらすら +するめ +すれちがう +すろっと +すわる +すんぜん +すんぽう +せあぶら +せいかつ +せいげん +せいじ +せいよう +せおう +せかいかん +せきにん +せきむ +せきゆ +せきらんうん +せけん +せこう +せすじ +せたい +せたけ +せっかく +せっきゃく +ぜっく +せっけん +せっこつ +せっさたくま +せつぞく +せつだん +せつでん +せっぱん +せつび +せつぶん +せつめい +せつりつ +せなか +せのび +せはば +せびろ +せぼね +せまい +せまる +せめる +せもたれ +せりふ +ぜんあく +せんい +せんえい +せんか +せんきょ +せんく +せんげん +ぜんご +せんさい +せんしゅ +せんすい +せんせい +せんぞ +せんたく +せんちょう +せんてい +せんとう +せんぬき +せんねん +せんぱい +ぜんぶ +ぜんぽう +せんむ +せんめんじょ +せんもん +せんやく +せんゆう +せんよう +ぜんら +ぜんりゃく +せんれい +せんろ +そあく +そいとげる +そいね +そうがんきょう +そうき +そうご +そうしん +そうだん +そうなん +そうび +そうめん +そうり +そえもの +そえん +そがい +そげき +そこう +そこそこ +そざい +そしな +そせい +そせん +そそぐ +そだてる +そつう +そつえん +そっかん +そつぎょう +そっけつ +そっこう +そっせん +そっと +そとがわ +そとづら +そなえる +そなた +そふぼ +そぼく +そぼろ +そまつ +そまる +そむく +そむりえ +そめる +そもそも +そよかぜ +そらまめ +そろう +そんかい +そんけい +そんざい +そんしつ +そんぞく +そんちょう +ぞんび +ぞんぶん +そんみん +たあい +たいいん +たいうん +たいえき +たいおう +だいがく +たいき +たいぐう +たいけん +たいこ +たいざい +だいじょうぶ +だいすき +たいせつ +たいそう +だいたい +たいちょう +たいてい +だいどころ +たいない +たいねつ +たいのう +たいはん +だいひょう +たいふう +たいへん +たいほ +たいまつばな +たいみんぐ +たいむ +たいめん +たいやき +たいよう +たいら +たいりょく +たいる +たいわん +たうえ +たえる +たおす +たおる +たおれる +たかい +たかね +たきび +たくさん +たこく +たこやき +たさい +たしざん +だじゃれ +たすける +たずさわる +たそがれ +たたかう +たたく +ただしい +たたみ +たちばな +だっかい +だっきゃく +だっこ +だっしゅつ +だったい +たてる +たとえる +たなばた +たにん +たぬき +たのしみ +たはつ +たぶん +たべる +たぼう +たまご +たまる +だむる +ためいき +ためす +ためる +たもつ +たやすい +たよる +たらす +たりきほんがん +たりょう +たりる +たると +たれる +たれんと +たろっと +たわむれる +だんあつ +たんい +たんおん +たんか +たんき +たんけん +たんご +たんさん +たんじょうび +だんせい +たんそく +たんたい +だんち +たんてい +たんとう +だんな +たんにん +だんねつ +たんのう +たんぴん +だんぼう +たんまつ +たんめい +だんれつ +だんろ +だんわ +ちあい +ちあん +ちいき +ちいさい +ちえん +ちかい +ちから +ちきゅう +ちきん +ちけいず +ちけん +ちこく +ちさい +ちしき +ちしりょう +ちせい +ちそう +ちたい +ちたん +ちちおや +ちつじょ +ちてき +ちてん +ちぬき +ちぬり +ちのう +ちひょう +ちへいせん +ちほう +ちまた +ちみつ +ちみどろ +ちめいど +ちゃんこなべ +ちゅうい +ちゆりょく +ちょうし +ちょさくけん +ちらし +ちらみ +ちりがみ +ちりょう +ちるど +ちわわ +ちんたい +ちんもく +ついか +ついたち +つうか +つうじょう +つうはん +つうわ +つかう +つかれる +つくね +つくる +つけね +つける +つごう +つたえる +つづく +つつじ +つつむ +つとめる +つながる +つなみ +つねづね +つのる +つぶす +つまらない +つまる +つみき +つめたい +つもり +つもる +つよい +つるぼ +つるみく +つわもの +つわり +てあし +てあて +てあみ +ていおん +ていか +ていき +ていけい +ていこく +ていさつ +ていし +ていせい +ていたい +ていど +ていねい +ていひょう +ていへん +ていぼう +てうち +ておくれ +てきとう +てくび +でこぼこ +てさぎょう +てさげ +てすり +てそう +てちがい +てちょう +てつがく +てつづき +でっぱ +てつぼう +てつや +でぬかえ +てぬき +てぬぐい +てのひら +てはい +てぶくろ +てふだ +てほどき +てほん +てまえ +てまきずし +てみじか +てみやげ +てらす +てれび +てわけ +てわたし +でんあつ +てんいん +てんかい +てんき +てんぐ +てんけん +てんごく +てんさい +てんし +てんすう +でんち +てんてき +てんとう +てんない +てんぷら +てんぼうだい +てんめつ +てんらんかい +でんりょく +でんわ +どあい +といれ +どうかん +とうきゅう +どうぐ +とうし +とうむぎ +とおい +とおか +とおく +とおす +とおる +とかい +とかす +ときおり +ときどき +とくい +とくしゅう +とくてん +とくに +とくべつ +とけい +とける +とこや +とさか +としょかん +とそう +とたん +とちゅう +とっきゅう +とっくん +とつぜん +とつにゅう +とどける +ととのえる +とない +となえる +となり +とのさま +とばす +どぶがわ +とほう +とまる +とめる +ともだち +ともる +どようび +とらえる +とんかつ +どんぶり +ないかく +ないこう +ないしょ +ないす +ないせん +ないそう +なおす +ながい +なくす +なげる +なこうど +なさけ +なたでここ +なっとう +なつやすみ +ななおし +なにごと +なにもの +なにわ +なのか +なふだ +なまいき +なまえ +なまみ +なみだ +なめらか +なめる +なやむ +ならう +ならび +ならぶ +なれる +なわとび +なわばり +にあう +にいがた +にうけ +におい +にかい +にがて +にきび +にくしみ +にくまん +にげる +にさんかたんそ +にしき +にせもの +にちじょう +にちようび +にっか +にっき +にっけい +にっこう +にっさん +にっしょく +にっすう +にっせき +にってい +になう +にほん +にまめ +にもつ +にやり +にゅういん +にりんしゃ +にわとり +にんい +にんか +にんき +にんげん +にんしき +にんずう +にんそう +にんたい +にんち +にんてい +にんにく +にんぷ +にんまり +にんむ +にんめい +にんよう +ぬいくぎ +ぬかす +ぬぐいとる +ぬぐう +ぬくもり +ぬすむ +ぬまえび +ぬめり +ぬらす +ぬんちゃく +ねあげ +ねいき +ねいる +ねいろ +ねぐせ +ねくたい +ねくら +ねこぜ +ねこむ +ねさげ +ねすごす +ねそべる +ねだん +ねつい +ねっしん +ねつぞう +ねったいぎょ +ねぶそく +ねふだ +ねぼう +ねほりはほり +ねまき +ねまわし +ねみみ +ねむい +ねむたい +ねもと +ねらう +ねわざ +ねんいり +ねんおし +ねんかん +ねんきん +ねんぐ +ねんざ +ねんし +ねんちゃく +ねんど +ねんぴ +ねんぶつ +ねんまつ +ねんりょう +ねんれい +のいず +のおづま +のがす +のきなみ +のこぎり +のこす +のこる +のせる +のぞく +のぞむ +のたまう +のちほど +のっく +のばす +のはら +のべる +のぼる +のみもの +のやま +のらいぬ +のらねこ +のりもの +のりゆき +のれん +のんき +ばあい +はあく +ばあさん +ばいか +ばいく +はいけん +はいご +はいしん +はいすい +はいせん +はいそう +はいち +ばいばい +はいれつ +はえる +はおる +はかい +ばかり +はかる +はくしゅ +はけん +はこぶ +はさみ +はさん +はしご +ばしょ +はしる +はせる +ぱそこん +はそん +はたん +はちみつ +はつおん +はっかく +はづき +はっきり +はっくつ +はっけん +はっこう +はっさん +はっしん +はったつ +はっちゅう +はってん +はっぴょう +はっぽう +はなす +はなび +はにかむ +はぶらし +はみがき +はむかう +はめつ +はやい +はやし +はらう +はろうぃん +はわい +はんい +はんえい +はんおん +はんかく +はんきょう +ばんぐみ +はんこ +はんしゃ +はんすう +はんだん +ぱんち +ぱんつ +はんてい +はんとし +はんのう +はんぱ +はんぶん +はんぺん +はんぼうき +はんめい +はんらん +はんろん +ひいき +ひうん +ひえる +ひかく +ひかり +ひかる +ひかん +ひくい +ひけつ +ひこうき +ひこく +ひさい +ひさしぶり +ひさん +びじゅつかん +ひしょ diff --git a/src/mnemonics/languages/lojban.txt b/src/mnemonics/languages/lojban.txt new file mode 100644 index 000000000..a8b41c12b --- /dev/null +++ b/src/mnemonics/languages/lojban.txt @@ -0,0 +1,1629 @@ +Lojban +Lojban +4 +backi +bacru +badna +badri +bajra +bakfu +bakni +bakri +baktu +balji +balni +balre +balvi +bambu +bancu +bandu +banfi +bangu +banli +banro +banxa +banzu +bapli +barda +bargu +barja +barna +bartu +basfa +basna +basti +batci +batke +bavmi +baxso +bebna +bekpi +bemro +bende +bengo +benji +benre +benzo +bergu +bersa +berti +besna +besto +betfu +betri +bevri +bidju +bifce +bikla +bilga +bilma +bilni +bindo +binra +binxo +birje +birka +birti +bisli +bitmu +bitni +blabi +blaci +blanu +bliku +bloti +bolci +bongu +boske +botpi +boxfo +boxna +bradi +brano +bratu +brazo +bredi +bridi +brife +briju +brito +brivo +broda +bruna +budjo +bukpu +bumru +bunda +bunre +burcu +burna +cabna +cabra +cacra +cadga +cadzu +cafne +cagna +cakla +calku +calse +canci +cando +cange +canja +canko +canlu +canpa +canre +canti +carce +carfu +carmi +carna +cartu +carvi +casnu +catke +catlu +catni +catra +caxno +cecla +cecmu +cedra +cenba +censa +centi +cerda +cerni +certu +cevni +cfale +cfari +cfika +cfila +cfine +cfipu +ciblu +cicna +cidja +cidni +cidro +cifnu +cigla +cikna +cikre +ciksi +cilce +cilfu +cilmo +cilre +cilta +cimde +cimni +cinba +cindu +cinfo +cinje +cinki +cinla +cinmo +cinri +cinse +cinta +cinza +cipni +cipra +cirko +cirla +ciska +cisma +cisni +ciste +citka +citno +citri +citsi +civla +cizra +ckabu +ckafi +ckaji +ckana +ckape +ckasu +ckeji +ckiku +ckilu +ckini +ckire +ckule +ckunu +cladu +clani +claxu +cletu +clika +clinu +clira +clite +cliva +clupa +cmaci +cmalu +cmana +cmavo +cmene +cmeta +cmevo +cmila +cmima +cmoni +cnano +cnebo +cnemu +cnici +cnino +cnisa +cnita +cokcu +condi +conka +corci +cortu +cpacu +cpana +cpare +cpedu +cpina +cradi +crane +creka +crepu +cribe +crida +crino +cripu +crisa +critu +ctaru +ctebi +cteki +ctile +ctino +ctuca +cukla +cukre +cukta +culno +cumki +cumla +cunmi +cunso +cuntu +cupra +curmi +curnu +curve +cusku +cusna +cutci +cutne +cuxna +dacru +dacti +dadjo +dakfu +dakli +damba +damri +dandu +danfu +danlu +danmo +danre +dansu +danti +daplu +dapma +darca +dargu +darlu +darno +darsi +darxi +daski +dasni +daspo +dasri +datka +datni +datro +decti +degji +dejni +dekpu +dekto +delno +dembi +denci +denmi +denpa +dertu +derxi +desku +detri +dicma +dicra +didni +digno +dikca +diklo +dikni +dilcu +dilma +dilnu +dimna +dindi +dinju +dinko +dinso +dirba +dirce +dirgo +disko +ditcu +divzi +dizlo +djacu +djedi +djica +djine +djuno +donri +dotco +draci +drani +drata +drudi +dugri +dukse +dukti +dunda +dunja +dunku +dunli +dunra +dutso +dzena +dzipo +facki +fadni +fagri +falnu +famti +fancu +fange +fanmo +fanri +fanta +fanva +fanza +fapro +farka +farlu +farna +farvi +fasnu +fatci +fatne +fatri +febvi +fegli +femti +fendi +fengu +fenki +fenra +fenso +fepni +fepri +ferti +festi +fetsi +figre +filso +finpe +finti +firca +fisli +fizbu +flaci +flalu +flani +flecu +flese +fliba +flira +foldi +fonmo +fonxa +forca +forse +fraso +frati +fraxu +frica +friko +frili +frinu +friti +frumu +fukpi +fulta +funca +fusra +fuzme +gacri +gadri +galfi +galtu +galxe +ganlo +ganra +ganse +ganti +ganxo +ganzu +gapci +gapru +garna +gasnu +gaspo +gasta +genja +gento +genxu +gerku +gerna +gidva +gigdo +ginka +girzu +gismu +glare +gleki +gletu +glico +glife +glosa +gluta +gocti +gomsi +gotro +gradu +grafu +grake +grana +grasu +grava +greku +grusi +grute +gubni +gugde +gugle +gumri +gundi +gunka +gunma +gunro +gunse +gunta +gurni +guska +gusni +gusta +gutci +gutra +guzme +jabre +jadni +jakne +jalge +jalna +jalra +jamfu +jamna +janbe +janco +janli +jansu +janta +jarbu +jarco +jarki +jaspu +jatna +javni +jbama +jbari +jbena +jbera +jbini +jdari +jdice +jdika +jdima +jdini +jduli +jecta +jeftu +jegvo +jelca +jemna +jenca +jendu +jenmi +jensi +jerna +jersi +jerxo +jesni +jetce +jetnu +jgalu +jganu +jgari +jgena +jgina +jgira +jgita +jibni +jibri +jicla +jicmu +jijnu +jikca +jikfi +jikni +jikru +jilka +jilra +jimca +jimpe +jimte +jinci +jinda +jinga +jinku +jinme +jinru +jinsa +jinto +jinvi +jinzi +jipci +jipno +jirna +jisra +jitfa +jitro +jivbu +jivna +jmaji +jmifa +jmina +jmive +jonse +jordo +jorne +jubme +judri +jufra +jukni +jukpa +julne +julro +jundi +jungo +junla +junri +junta +jurme +jursa +jutsi +juxre +jvinu +jviso +kabri +kacma +kadno +kafke +kagni +kajde +kajna +kakne +kakpa +kalci +kalri +kalsa +kalte +kamju +kamni +kampu +kamre +kanba +kancu +kandi +kanji +kanla +kanpe +kanro +kansa +kantu +kanxe +karbi +karce +karda +kargu +karli +karni +katci +katna +kavbu +kazra +kecti +kekli +kelci +kelvo +kenka +kenra +kensa +kerfa +kerlo +kesri +ketco +ketsu +kevna +kibro +kicne +kijno +kilto +kinda +kinli +kisto +klaji +klaku +klama +klani +klesi +kliki +klina +kliru +kliti +klupe +kluza +kobli +kogno +kojna +kokso +kolme +komcu +konju +korbi +korcu +korka +korvo +kosmu +kosta +krali +kramu +krasi +krati +krefu +krici +krili +krinu +krixa +kruca +kruji +kruvi +kubli +kucli +kufra +kukte +kulnu +kumfa +kumte +kunra +kunti +kurfa +kurji +kurki +kuspe +kusru +labno +lacni +lacpu +lacri +ladru +lafti +lakne +lakse +laldo +lalxu +lamji +lanbi +lanci +landa +lanka +lanli +lanme +lante +lanxe +lanzu +larcu +larva +lasna +lastu +latmo +latna +lazni +lebna +lelxe +lenga +lenjo +lenku +lerci +lerfu +libjo +lidne +lifri +lijda +limfa +limna +lince +lindi +linga +linji +linsi +linto +lisri +liste +litce +litki +litru +livga +livla +logji +loglo +lojbo +loldi +lorxu +lubno +lujvo +luksi +lumci +lunbe +lunra +lunsa +luska +lusto +mabla +mabru +macnu +majga +makcu +makfa +maksi +malsi +mamta +manci +manfo +mango +manku +manri +mansa +manti +mapku +mapni +mapra +mapti +marbi +marce +marde +margu +marji +marna +marxa +masno +masti +matci +matli +matne +matra +mavji +maxri +mebri +megdo +mekso +melbi +meljo +melmi +menli +menre +mensi +mentu +merko +merli +metfo +mexno +midju +mifra +mikce +mikri +milti +milxe +minde +minji +minli +minra +mintu +mipri +mirli +misno +misro +mitre +mixre +mlana +mlatu +mleca +mledi +mluni +mogle +mokca +moklu +molki +molro +morji +morko +morna +morsi +mosra +mraji +mrilu +mruli +mucti +mudri +mugle +mukti +mulno +munje +mupli +murse +murta +muslo +mutce +muvdu +muzga +nabmi +nakni +nalci +namcu +nanba +nanca +nandu +nanla +nanmu +nanvi +narge +narju +natfe +natmi +natsi +navni +naxle +nazbi +nejni +nelci +nenri +nerde +nibli +nicfa +nicte +nikle +nilce +nimre +ninja +ninmu +nirna +nitcu +nivji +nixli +nobli +norgo +notci +nudle +nukni +nunmu +nupre +nurma +nusna +nutka +nutli +nuzba +nuzlo +pacna +pagbu +pagre +pajni +palci +palku +palma +palne +palpi +palta +pambe +pamga +panci +pandi +panje +panka +panlo +panpi +panra +pante +panzi +papri +parbi +pardu +parji +pastu +patfu +patlu +patxu +paznu +pelji +pelxu +pemci +penbi +pencu +pendo +penmi +pensi +pentu +perli +pesxu +petso +pevna +pezli +picti +pijne +pikci +pikta +pilda +pilji +pilka +pilno +pimlu +pinca +pindi +pinfu +pinji +pinka +pinsi +pinta +pinxe +pipno +pixra +plana +platu +pleji +plibu +plini +plipe +plise +plita +plixa +pluja +pluka +pluta +pocli +polje +polno +ponjo +ponse +poplu +porpi +porsi +porto +prali +prami +prane +preja +prenu +preri +preti +prije +prina +pritu +proga +prosa +pruce +pruni +pruri +pruxi +pulce +pulji +pulni +punji +punli +pupsu +purci +purdi +purmo +racli +ractu +radno +rafsi +ragbi +ragve +rakle +rakso +raktu +ralci +ralju +ralte +randa +rango +ranji +ranmi +ransu +ranti +ranxi +rapli +rarna +ratcu +ratni +rebla +rectu +rekto +remna +renro +renvi +respa +rexsa +ricfu +rigni +rijno +rilti +rimni +rinci +rindo +rinju +rinka +rinsa +rirci +rirni +rirxe +rismi +risna +ritli +rivbi +rokci +romge +romlo +ronte +ropno +rorci +rotsu +rozgu +ruble +rufsu +runme +runta +rupnu +rusko +rutni +sabji +sabnu +sacki +saclu +sadjo +sakci +sakli +sakta +salci +salpo +salri +salta +samcu +sampu +sanbu +sance +sanga +sanji +sanli +sanmi +sanso +santa +sarcu +sarji +sarlu +sarni +sarxe +saske +satci +satre +savru +sazri +sefsi +sefta +sekre +selci +selfu +semto +senci +sengi +senpi +senta +senva +sepli +serti +sesre +setca +sevzi +sfani +sfasa +sfofa +sfubu +sibli +siclu +sicni +sicpi +sidbo +sidju +sigja +sigma +sikta +silka +silna +simlu +simsa +simxu +since +sinma +sinso +sinxa +sipna +sirji +sirxo +sisku +sisti +sitna +sivni +skaci +skami +skapi +skari +skicu +skiji +skina +skori +skoto +skuba +skuro +slabu +slaka +slami +slanu +slari +slasi +sligu +slilu +sliri +slovo +sluji +sluni +smacu +smadi +smaji +smaka +smani +smela +smoka +smuci +smuni +smusu +snada +snanu +snidu +snime +snipa +snuji +snura +snuti +sobde +sodna +sodva +softo +solji +solri +sombo +sonci +sorcu +sorgu +sorni +sorta +sovda +spaji +spali +spano +spati +speni +spero +spisa +spita +spofu +spoja +spuda +sputu +sraji +sraku +sralo +srana +srasu +srera +srito +sruma +sruri +stace +stagi +staku +stali +stani +stapa +stasu +stati +steba +steci +stedu +stela +stero +stici +stidi +stika +stizu +stodi +stuna +stura +stuzi +sucta +sudga +sufti +suksa +sumji +sumne +sumti +sunga +sunla +surla +sutra +tabno +tabra +tadji +tadni +tagji +taksi +talsa +tamca +tamji +tamne +tanbo +tance +tanjo +tanko +tanru +tansi +tanxe +tapla +tarbi +tarci +tarla +tarmi +tarti +taske +tasmi +tasta +tatpi +tatru +tavla +taxfu +tcaci +tcadu +tcana +tcati +tcaxe +tcena +tcese +tcica +tcidu +tcika +tcila +tcima +tcini +tcita +temci +temse +tende +tenfa +tengu +terdi +terpa +terto +tifri +tigni +tigra +tikpa +tilju +tinbe +tinci +tinsa +tirna +tirse +tirxu +tisna +titla +tivni +tixnu +toknu +toldi +tonga +tordu +torni +torso +traji +trano +trati +trene +tricu +trina +trixe +troci +tsaba +tsali +tsani +tsapi +tsiju +tsina +tsuku +tubnu +tubra +tugni +tujli +tumla +tunba +tunka +tunlo +tunta +tuple +turko +turni +tutci +tutle +tutra +vacri +vajni +valsi +vamji +vamtu +vanbi +vanci +vanju +vasru +vasxu +vecnu +vedli +venfu +vensa +vente +vepre +verba +vibna +vidni +vidru +vifne +vikmi +viknu +vimcu +vindu +vinji +vinta +vipsi +virnu +viska +vitci +vitke +vitno +vlagi +vlile +vlina +vlipa +vofli +voksa +volve +vorme +vraga +vreji +vreta +vrici +vrude +vrusi +vubla +vujnu +vukna +vukro +xabju +xadba +xadji +xadni +xagji +xagri +xajmi +xaksu +xalbo +xalka +xalni +xamgu +xampo +xamsi +xance +xango +xanka +xanri +xansa +xanto +xarci +xarju +xarnu +xasli +xasne +xatra +xatsi +xazdo +xebni +xebro +xecto +xedja +xekri +xelso +xendo +xenru +xexso +xigzo +xindo +xinmo +xirma +xislu +xispo +xlali +xlura +xorbo +xorlo +xotli +xrabo +xrani +xriso +xrotu +xruba +xruki +xrula +xruti +xukmi +xulta +xunre +xurdo +xusra +xutla +zabna +zajba +zalvi +zanru +zarci +zargu +zasni +zasti +zbabu +zbani +zbasu +zbepi +zdani +zdile +zekri +zenba +zepti +zetro +zevla +zgadi +zgana +zgike +zifre +zinki +zirpu +zivle +zmadu +zmiku +zucna +zukte +zumri +zungi +zunle +zunti +zutse +zvati +zviki +jbobau +jbopre +karsna +cabdei +zunsna +gendra +glibau +nintadni +pavyseljirna +vlaste +selbri +latro'a +zdakemkulgu'a +mriste +selsku +fu'ivla +tolmo'i +snavei +xagmau +retsku +ckupau +skudji +smudra +prulamdei +vokta'a +tinju'i +jefyfa'o +bavlamdei +kinzga +jbocre +jbovla +xauzma +selkei +xuncku +spusku +jbogu'e +pampe'o +bripre +jbosnu +zi'evla +gimste +tolzdi +velski +samselpla +cnegau +velcki +selja'e +fasybau +zanfri +reisku +favgau +jbota'a +rejgau +malgli +zilkai +keidji +tersu'i +jbofi'e +cnima'o +mulgau +ningau +ponbau +mrobi'o +rarbau +zmanei +famyma'o +vacysai +jetmlu +jbonunsla +nunpe'i +fa'orma'o +crezenzu'e +jbojbe +cmicu'a +zilcmi +tolcando +zukcfu +depybu'i +mencre +matmau +nunctu +selma'o +titnanba +naldra +jvajvo +nunsnu +nerkla +cimjvo +muvgau +zipcpi +runbau +faumlu +terbri +balcu'e +dragau +smuvelcki +piksku +selpli +bregau +zvafa'i +ci'izra +noltruti'u +samtci +snaxa'a diff --git a/src/mnemonics/languages/portuguese.txt b/src/mnemonics/languages/portuguese.txt new file mode 100644 index 000000000..a50b53adf --- /dev/null +++ b/src/mnemonics/languages/portuguese.txt @@ -0,0 +1,1629 @@ +Portuguese +Português +4 +abaular +abdominal +abeto +abissinio +abjeto +ablucao +abnegar +abotoar +abrutalhar +absurdo +abutre +acautelar +accessorios +acetona +achocolatado +acirrar +acne +acovardar +acrostico +actinomicete +acustico +adaptavel +adeus +adivinho +adjunto +admoestar +adnominal +adotivo +adquirir +adriatico +adsorcao +adutora +advogar +aerossol +afazeres +afetuoso +afixo +afluir +afortunar +afrouxar +aftosa +afunilar +agentes +agito +aglutinar +aiatola +aimore +aino +aipo +airoso +ajeitar +ajoelhar +ajudante +ajuste +alazao +albumina +alcunha +alegria +alexandre +alforriar +alguns +alhures +alivio +almoxarife +alotropico +alpiste +alquimista +alsaciano +altura +aluviao +alvura +amazonico +ambulatorio +ametodico +amizades +amniotico +amovivel +amurada +anatomico +ancorar +anexo +anfora +aniversario +anjo +anotar +ansioso +anturio +anuviar +anverso +anzol +aonde +apaziguar +apito +aplicavel +apoteotico +aprimorar +aprumo +apto +apuros +aquoso +arauto +arbusto +arduo +aresta +arfar +arguto +aritmetico +arlequim +armisticio +aromatizar +arpoar +arquivo +arrumar +arsenio +arturiano +aruaque +arvores +asbesto +ascorbico +aspirina +asqueroso +assustar +astuto +atazanar +ativo +atletismo +atmosferico +atormentar +atroz +aturdir +audivel +auferir +augusto +aula +aumento +aurora +autuar +avatar +avexar +avizinhar +avolumar +avulso +axiomatico +azerbaijano +azimute +azoto +azulejo +bacteriologista +badulaque +baforada +baixote +bajular +balzaquiana +bambuzal +banzo +baoba +baqueta +barulho +bastonete +batuta +bauxita +bavaro +bazuca +bcrepuscular +beato +beduino +begonia +behaviorista +beisebol +belzebu +bemol +benzido +beocio +bequer +berro +besuntar +betume +bexiga +bezerro +biatlon +biboca +bicuspide +bidirecional +bienio +bifurcar +bigorna +bijuteria +bimotor +binormal +bioxido +bipolarizacao +biquini +birutice +bisturi +bituca +biunivoco +bivalve +bizarro +blasfemo +blenorreia +blindar +bloqueio +blusao +boazuda +bofete +bojudo +bolso +bombordo +bonzo +botina +boquiaberto +bostoniano +botulismo +bourbon +bovino +boximane +bravura +brevidade +britar +broxar +bruno +bruxuleio +bubonico +bucolico +buda +budista +bueiro +buffer +bugre +bujao +bumerangue +burundines +busto +butique +buzios +caatinga +cabuqui +cacunda +cafuzo +cajueiro +camurca +canudo +caquizeiro +carvoeiro +casulo +catuaba +cauterizar +cebolinha +cedula +ceifeiro +celulose +cerzir +cesto +cetro +ceus +cevar +chavena +cheroqui +chita +chovido +chuvoso +ciatico +cibernetico +cicuta +cidreira +cientistas +cifrar +cigarro +cilio +cimo +cinzento +cioso +cipriota +cirurgico +cisto +citrico +ciumento +civismo +clavicula +clero +clitoris +cluster +coaxial +cobrir +cocota +codorniz +coexistir +cogumelo +coito +colusao +compaixao +comutativo +contentamento +convulsivo +coordenativa +coquetel +correto +corvo +costureiro +cotovia +covil +cozinheiro +cretino +cristo +crivo +crotalo +cruzes +cubo +cucuia +cueiro +cuidar +cujo +cultural +cunilingua +cupula +curvo +custoso +cutucar +czarismo +dablio +dacota +dados +daguerreotipo +daiquiri +daltonismo +damista +dantesco +daquilo +darwinista +dasein +dativo +deao +debutantes +decurso +deduzir +defunto +degustar +dejeto +deltoide +demover +denunciar +deputado +deque +dervixe +desvirtuar +deturpar +deuteronomio +devoto +dextrose +dezoito +diatribe +dicotomico +didatico +dietista +difuso +digressao +diluvio +diminuto +dinheiro +dinossauro +dioxido +diplomatico +dique +dirimivel +disturbio +diurno +divulgar +dizivel +doar +dobro +docura +dodoi +doer +dogue +doloso +domo +donzela +doping +dorsal +dossie +dote +doutro +doze +dravidico +dreno +driver +dropes +druso +dubnio +ducto +dueto +dulija +dundum +duodeno +duquesa +durou +duvidoso +duzia +ebano +ebrio +eburneo +echarpe +eclusa +ecossistema +ectoplasma +ecumenismo +eczema +eden +editorial +edredom +edulcorar +efetuar +efigie +efluvio +egiptologo +egresso +egua +einsteiniano +eira +eivar +eixos +ejetar +elastomero +eldorado +elixir +elmo +eloquente +elucidativo +emaranhar +embutir +emerito +emfa +emitir +emotivo +empuxo +emulsao +enamorar +encurvar +enduro +enevoar +enfurnar +enguico +enho +enigmista +enlutar +enormidade +enpreendimento +enquanto +enriquecer +enrugar +entusiastico +enunciar +envolvimento +enxuto +enzimatico +eolico +epiteto +epoxi +epura +equivoco +erario +erbio +ereto +erguido +erisipela +ermo +erotizar +erros +erupcao +ervilha +esburacar +escutar +esfuziante +esguio +esloveno +esmurrar +esoterismo +esperanca +espirito +espurio +essencialmente +esturricar +esvoacar +etario +eterno +etiquetar +etnologo +etos +etrusco +euclidiano +euforico +eugenico +eunuco +europio +eustaquio +eutanasia +evasivo +eventualidade +evitavel +evoluir +exaustor +excursionista +exercito +exfoliado +exito +exotico +expurgo +exsudar +extrusora +exumar +fabuloso +facultativo +fado +fagulha +faixas +fajuto +faltoso +famoso +fanzine +fapesp +faquir +fartura +fastio +faturista +fausto +favorito +faxineira +fazer +fealdade +febril +fecundo +fedorento +feerico +feixe +felicidade +felpudo +feltro +femur +fenotipo +fervura +festivo +feto +feudo +fevereiro +fezinha +fiasco +fibra +ficticio +fiduciario +fiesp +fifa +figurino +fijiano +filtro +finura +fiorde +fiquei +firula +fissurar +fitoteca +fivela +fixo +flavio +flexor +flibusteiro +flotilha +fluxograma +fobos +foco +fofura +foguista +foie +foliculo +fominha +fonte +forum +fosso +fotossintese +foxtrote +fraudulento +frevo +frivolo +frouxo +frutose +fuba +fucsia +fugitivo +fuinha +fujao +fulustreco +fumo +funileiro +furunculo +fustigar +futurologo +fuxico +fuzue +gabriel +gado +gaelico +gafieira +gaguejo +gaivota +gajo +galvanoplastico +gamo +ganso +garrucha +gastronomo +gatuno +gaussiano +gaviao +gaxeta +gazeteiro +gear +geiser +geminiano +generoso +genuino +geossinclinal +gerundio +gestual +getulista +gibi +gigolo +gilete +ginseng +giroscopio +glaucio +glacial +gleba +glifo +glote +glutonia +gnostico +goela +gogo +goitaca +golpista +gomo +gonzo +gorro +gostou +goticula +gourmet +governo +gozo +graxo +grevista +grito +grotesco +gruta +guaxinim +gude +gueto +guizo +guloso +gume +guru +gustativo +grelhado +gutural +habitue +haitiano +halterofilista +hamburguer +hanseniase +happening +harpista +hastear +haveres +hebreu +hectometro +hedonista +hegira +helena +helminto +hemorroidas +henrique +heptassilabo +hertziano +hesitar +heterossexual +heuristico +hexagono +hiato +hibrido +hidrostatico +hieroglifo +hifenizar +higienizar +hilario +himen +hino +hippie +hirsuto +historiografia +hitlerista +hodometro +hoje +holograma +homus +honroso +hoquei +horto +hostilizar +hotentote +huguenote +humilde +huno +hurra +hutu +iaia +ialorixa +iambico +iansa +iaque +iara +iatista +iberico +ibis +icar +iceberg +icosagono +idade +ideologo +idiotice +idoso +iemenita +iene +igarape +iglu +ignorar +igreja +iguaria +iidiche +ilativo +iletrado +ilharga +ilimitado +ilogismo +ilustrissimo +imaturo +imbuzeiro +imerso +imitavel +imovel +imputar +imutavel +inaveriguavel +incutir +induzir +inextricavel +infusao +ingua +inhame +iniquo +injusto +inning +inoxidavel +inquisitorial +insustentavel +intumescimento +inutilizavel +invulneravel +inzoneiro +iodo +iogurte +ioio +ionosfera +ioruba +iota +ipsilon +irascivel +iris +irlandes +irmaos +iroques +irrupcao +isca +isento +islandes +isotopo +isqueiro +israelita +isso +isto +iterbio +itinerario +itrio +iuane +iugoslavo +jabuticabeira +jacutinga +jade +jagunco +jainista +jaleco +jambo +jantarada +japones +jaqueta +jarro +jasmim +jato +jaula +javel +jazz +jegue +jeitoso +jejum +jenipapo +jeova +jequitiba +jersei +jesus +jetom +jiboia +jihad +jilo +jingle +jipe +jocoso +joelho +joguete +joio +jojoba +jorro +jota +joule +joviano +jubiloso +judoca +jugular +juizo +jujuba +juliano +jumento +junto +jururu +justo +juta +juventude +labutar +laguna +laico +lajota +lanterninha +lapso +laquear +lastro +lauto +lavrar +laxativo +lazer +leasing +lebre +lecionar +ledo +leguminoso +leitura +lele +lemure +lento +leonardo +leopardo +lepton +leque +leste +letreiro +leucocito +levitico +lexicologo +lhama +lhufas +liame +licoroso +lidocaina +liliputiano +limusine +linotipo +lipoproteina +liquidos +lirismo +lisura +liturgico +livros +lixo +lobulo +locutor +lodo +logro +lojista +lombriga +lontra +loop +loquaz +lorota +losango +lotus +louvor +luar +lubrificavel +lucros +lugubre +luis +luminoso +luneta +lustroso +luto +luvas +luxuriante +luzeiro +maduro +maestro +mafioso +magro +maiuscula +majoritario +malvisto +mamute +manutencao +mapoteca +maquinista +marzipa +masturbar +matuto +mausoleu +mavioso +maxixe +mazurca +meandro +mecha +medusa +mefistofelico +megera +meirinho +melro +memorizar +menu +mequetrefe +mertiolate +mestria +metroviario +mexilhao +mezanino +miau +microssegundo +midia +migratorio +mimosa +minuto +miosotis +mirtilo +misturar +mitzvah +miudos +mixuruca +mnemonico +moagem +mobilizar +modulo +moer +mofo +mogno +moita +molusco +monumento +moqueca +morubixaba +mostruario +motriz +mouse +movivel +mozarela +muarra +muculmano +mudo +mugir +muitos +mumunha +munir +muon +muquira +murros +musselina +nacoes +nado +naftalina +nago +naipe +naja +nalgum +namoro +nanquim +napolitano +naquilo +nascimento +nautilo +navios +nazista +nebuloso +nectarina +nefrologo +negus +nelore +nenufar +nepotismo +nervura +neste +netuno +neutron +nevoeiro +newtoniano +nexo +nhenhenhem +nhoque +nigeriano +niilista +ninho +niobio +niponico +niquelar +nirvana +nisto +nitroglicerina +nivoso +nobreza +nocivo +noel +nogueira +noivo +nojo +nominativo +nonuplo +noruegues +nostalgico +noturno +nouveau +nuanca +nublar +nucleotideo +nudista +nulo +numismatico +nunquinha +nupcias +nutritivo +nuvens +oasis +obcecar +obeso +obituario +objetos +oblongo +obnoxio +obrigatorio +obstruir +obtuso +obus +obvio +ocaso +occipital +oceanografo +ocioso +oclusivo +ocorrer +ocre +octogono +odalisca +odisseia +odorifico +oersted +oeste +ofertar +ofidio +oftalmologo +ogiva +ogum +oigale +oitavo +oitocentos +ojeriza +olaria +oleoso +olfato +olhos +oliveira +olmo +olor +olvidavel +ombudsman +omeleteira +omitir +omoplata +onanismo +ondular +oneroso +onomatopeico +ontologico +onus +onze +opalescente +opcional +operistico +opio +oposto +oprobrio +optometrista +opusculo +oratorio +orbital +orcar +orfao +orixa +orla +ornitologo +orquidea +ortorrombico +orvalho +osculo +osmotico +ossudo +ostrogodo +otario +otite +ouro +ousar +outubro +ouvir +ovario +overnight +oviparo +ovni +ovoviviparo +ovulo +oxala +oxente +oxiuro +oxossi +ozonizar +paciente +pactuar +padronizar +paete +pagodeiro +paixao +pajem +paludismo +pampas +panturrilha +papudo +paquistanes +pastoso +patua +paulo +pauzinhos +pavoroso +paxa +pazes +peao +pecuniario +pedunculo +pegaso +peixinho +pejorativo +pelvis +penuria +pequno +petunia +pezada +piauiense +pictorico +pierro +pigmeu +pijama +pilulas +pimpolho +pintura +piorar +pipocar +piqueteiro +pirulito +pistoleiro +pituitaria +pivotar +pixote +pizzaria +plistoceno +plotar +pluviometrico +pneumonico +poco +podridao +poetisa +pogrom +pois +polvorosa +pomposo +ponderado +pontudo +populoso +poquer +porvir +posudo +potro +pouso +povoar +prazo +prezar +privilegios +proximo +prussiano +pseudopode +psoriase +pterossauros +ptialina +ptolemaico +pudor +pueril +pufe +pugilista +puir +pujante +pulverizar +pumba +punk +purulento +pustula +putsch +puxe +quatrocentos +quetzal +quixotesco +quotizavel +rabujice +racista +radonio +rafia +ragu +rajado +ralo +rampeiro +ranzinza +raptor +raquitismo +raro +rasurar +ratoeira +ravioli +razoavel +reavivar +rebuscar +recusavel +reduzivel +reexposicao +refutavel +regurgitar +reivindicavel +rejuvenescimento +relva +remuneravel +renunciar +reorientar +repuxo +requisito +resumo +returno +reutilizar +revolvido +rezonear +riacho +ribossomo +ricota +ridiculo +rifle +rigoroso +rijo +rimel +rins +rios +riqueza +respeito +rissole +ritualistico +rivalizar +rixa +robusto +rococo +rodoviario +roer +rogo +rojao +rolo +rompimento +ronronar +roqueiro +rorqual +rosto +rotundo +rouxinol +roxo +royal +ruas +rucula +rudimentos +ruela +rufo +rugoso +ruivo +rule +rumoroso +runico +ruptura +rural +rustico +rutilar +saariano +sabujo +sacudir +sadomasoquista +safra +sagui +sais +samurai +santuario +sapo +saquear +sartriano +saturno +saude +sauva +saveiro +saxofonista +sazonal +scherzo +script +seara +seborreia +secura +seduzir +sefardim +seguro +seja +selvas +sempre +senzala +sepultura +sequoia +sestercio +setuplo +seus +seviciar +sezonismo +shalom +siames +sibilante +sicrano +sidra +sifilitico +signos +silvo +simultaneo +sinusite +sionista +sirio +sisudo +situar +sivan +slide +slogan +soar +sobrio +socratico +sodomizar +soerguer +software +sogro +soja +solver +somente +sonso +sopro +soquete +sorveteiro +sossego +soturno +sousafone +sovinice +sozinho +suavizar +subverter +sucursal +sudoriparo +sufragio +sugestoes +suite +sujo +sultao +sumula +suntuoso +suor +supurar +suruba +susto +suturar +suvenir +tabuleta +taco +tadjique +tafeta +tagarelice +taitiano +talvez +tampouco +tanzaniano +taoista +tapume +taquion +tarugo +tascar +tatuar +tautologico +tavola +taxionomista +tchecoslovaco +teatrologo +tectonismo +tedioso +teflon +tegumento +teixo +telurio +temporas +tenue +teosofico +tepido +tequila +terrorista +testosterona +tetrico +teutonico +teve +texugo +tiara +tibia +tiete +tifoide +tigresa +tijolo +tilintar +timpano +tintureiro +tiquete +tiroteio +tisico +titulos +tive +toar +toboga +tofu +togoles +toicinho +tolueno +tomografo +tontura +toponimo +toquio +torvelinho +tostar +toto +touro +toxina +trazer +trezentos +trivialidade +trovoar +truta +tuaregue +tubular +tucano +tudo +tufo +tuiste +tulipa +tumultuoso +tunisino +tupiniquim +turvo +tutu +ucraniano +udenista +ufanista +ufologo +ugaritico +uiste +uivo +ulceroso +ulema +ultravioleta +umbilical +umero +umido +umlaut +unanimidade +unesco +ungulado +unheiro +univoco +untuoso +urano +urbano +urdir +uretra +urgente +urinol +urna +urologo +urro +ursulina +urtiga +urupe +usavel +usbeque +usei +usineiro +usurpar +utero +utilizar +utopico +uvular +uxoricidio +vacuo +vadio +vaguear +vaivem +valvula +vampiro +vantajoso +vaporoso +vaquinha +varziano +vasto +vaticinio +vaudeville +vazio +veado +vedico +veemente +vegetativo +veio +veja +veludo +venusiano +verdade +verve +vestuario +vetusto +vexatorio +vezes +viavel +vibratorio +victor +vicunha +vidros +vietnamita +vigoroso +vilipendiar +vime +vintem +violoncelo +viquingue +virus +visualizar +vituperio +viuvo +vivo +vizir +voar +vociferar +vodu +vogar +voile +volver +vomito +vontade +vortice +vosso +voto +vovozinha +voyeuse +vozes +vulva +vupt +western +xadrez +xale +xampu +xango +xarope +xaual +xavante +xaxim +xenonio +xepa +xerox +xicara +xifopago +xiita +xilogravura +xinxim +xistoso +xixi +xodo +xogum +xucro +zabumba +zagueiro +zambiano +zanzar +zarpar +zebu +zefiro +zeloso +zenite +zumbi diff --git a/src/mnemonics/languages/russian.txt b/src/mnemonics/languages/russian.txt new file mode 100644 index 000000000..18abd4a1b --- /dev/null +++ b/src/mnemonics/languages/russian.txt @@ -0,0 +1,1629 @@ +Russian +русский язык +3 +абажур +абзац +абонент +абрикос +абсурд +авангард +август +авиация +авоська +автор +агат +агент +агитатор +агнец +агония +агрегат +адвокат +адмирал +адрес +ажиотаж +азарт +азбука +азот +аист +айсберг +академия +аквариум +аккорд +акробат +аксиома +актер +акула +акция +алгоритм +алебарда +аллея +алмаз +алтарь +алфавит +алхимик +алый +альбом +алюминий +амбар +аметист +амнезия +ампула +амфора +анализ +ангел +анекдот +анимация +анкета +аномалия +ансамбль +антенна +апатия +апельсин +апофеоз +аппарат +апрель +аптека +арабский +арбуз +аргумент +арест +ария +арка +армия +аромат +арсенал +артист +архив +аршин +асбест +аскетизм +аспект +ассорти +астроном +асфальт +атака +ателье +атлас +атом +атрибут +аудитор +аукцион +аура +афера +афиша +ахинея +ацетон +аэропорт +бабушка +багаж +бадья +база +баклажан +балкон +бампер +банк +барон +бассейн +батарея +бахрома +башня +баян +бегство +бедро +бездна +бекон +белый +бензин +берег +беседа +бетонный +биатлон +библия +бивень +бигуди +бидон +бизнес +бикини +билет +бинокль +биология +биржа +бисер +битва +бицепс +благо +бледный +близкий +блок +блуждать +блюдо +бляха +бобер +богатый +бодрый +боевой +бокал +большой +борьба +босой +ботинок +боцман +бочка +боярин +брать +бревно +бригада +бросать +брызги +брюки +бублик +бугор +будущее +буква +бульвар +бумага +бунт +бурный +бусы +бутылка +буфет +бухта +бушлат +бывалый +быль +быстрый +быть +бюджет +бюро +бюст +вагон +важный +ваза +вакцина +валюта +вампир +ванная +вариант +вассал +вата +вафля +вахта +вдова +вдыхать +ведущий +веер +вежливый +везти +веко +великий +вена +верить +веселый +ветер +вечер +вешать +вещь +веяние +взаимный +взбучка +взвод +взгляд +вздыхать +взлетать +взмах +взнос +взор +взрыв +взывать +взятка +вибрация +визит +вилка +вино +вирус +висеть +витрина +вихрь +вишневый +включать +вкус +власть +влечь +влияние +влюблять +внешний +внимание +внук +внятный +вода +воевать +вождь +воздух +войти +вокзал +волос +вопрос +ворота +восток +впадать +впускать +врач +время +вручать +всадник +всеобщий +вспышка +встреча +вторник +вулкан +вурдалак +входить +въезд +выбор +вывод +выгодный +выделять +выезжать +выживать +вызывать +выигрыш +вылезать +выносить +выпивать +высокий +выходить +вычет +вышка +выяснять +вязать +вялый +гавань +гадать +газета +гаишник +галстук +гамма +гарантия +гастроли +гвардия +гвоздь +гектар +гель +генерал +геолог +герой +гешефт +гибель +гигант +гильза +гимн +гипотеза +гитара +глаз +глина +глоток +глубокий +глыба +глядеть +гнать +гнев +гнить +гном +гнуть +говорить +годовой +голова +гонка +город +гость +готовый +граница +грех +гриб +громкий +группа +грызть +грязный +губа +гудеть +гулять +гуманный +густой +гуща +давать +далекий +дама +данные +дарить +дать +дача +дверь +движение +двор +дебют +девушка +дедушка +дежурный +дезертир +действие +декабрь +дело +демократ +день +депутат +держать +десяток +детский +дефицит +дешевый +деятель +джаз +джинсы +джунгли +диалог +диван +диета +дизайн +дикий +динамика +диплом +директор +диск +дитя +дичь +длинный +дневник +добрый +доверие +договор +дождь +доза +документ +должен +домашний +допрос +дорога +доход +доцент +дочь +дощатый +драка +древний +дрожать +друг +дрянь +дубовый +дуга +дудка +дукат +дуло +думать +дупло +дурак +дуть +духи +душа +дуэт +дымить +дыня +дыра +дыханье +дышать +дьявол +дюжина +дюйм +дюна +дядя +дятел +егерь +единый +едкий +ежевика +ежик +езда +елка +емкость +ерунда +ехать +жадный +жажда +жалеть +жанр +жара +жать +жгучий +ждать +жевать +желание +жемчуг +женщина +жертва +жесткий +жечь +живой +жидкость +жизнь +жилье +жирный +житель +журнал +жюри +забывать +завод +загадка +задача +зажечь +зайти +закон +замечать +занимать +западный +зарплата +засыпать +затрата +захват +зацепка +зачет +защита +заявка +звать +звезда +звонить +звук +здание +здешний +здоровье +зебра +зевать +зеленый +земля +зенит +зеркало +зефир +зигзаг +зима +зиять +злак +злой +змея +знать +зной +зодчий +золотой +зомби +зона +зоопарк +зоркий +зрачок +зрение +зритель +зубной +зыбкий +зять +игла +иголка +играть +идея +идиот +идол +идти +иерархия +избрать +известие +изгонять +издание +излагать +изменять +износ +изоляция +изрядный +изучать +изымать +изящный +икона +икра +иллюзия +имбирь +иметь +имидж +иммунный +империя +инвестор +индивид +инерция +инженер +иномарка +институт +интерес +инфекция +инцидент +ипподром +ирис +ирония +искать +история +исходить +исчезать +итог +июль +июнь +кабинет +кавалер +кадр +казарма +кайф +кактус +калитка +камень +канал +капитан +картина +касса +катер +кафе +качество +каша +каюта +квартира +квинтет +квота +кедр +кекс +кенгуру +кепка +керосин +кетчуп +кефир +кибитка +кивнуть +кидать +километр +кино +киоск +кипеть +кирпич +кисть +китаец +класс +клетка +клиент +клоун +клуб +клык +ключ +клятва +книга +кнопка +кнут +князь +кобура +ковер +коготь +кодекс +кожа +козел +койка +коктейль +колено +компания +конец +копейка +короткий +костюм +котел +кофе +кошка +красный +кресло +кричать +кровь +крупный +крыша +крючок +кубок +кувшин +кудрявый +кузов +кукла +культура +кумир +купить +курс +кусок +кухня +куча +кушать +кювет +лабиринт +лавка +лагерь +ладонь +лазерный +лайнер +лакей +лампа +ландшафт +лапа +ларек +ласковый +лауреат +лачуга +лаять +лгать +лебедь +левый +легкий +ледяной +лежать +лекция +лента +лепесток +лесной +лето +лечь +леший +лживый +либерал +ливень +лига +лидер +ликовать +лиловый +лимон +линия +липа +лирика +лист +литр +лифт +лихой +лицо +личный +лишний +лобовой +ловить +логика +лодка +ложка +лозунг +локоть +ломать +лоно +лопата +лорд +лось +лоток +лохматый +лошадь +лужа +лукавый +луна +лупить +лучший +лыжный +лысый +львиный +льгота +льдина +любить +людской +люстра +лютый +лягушка +магазин +мадам +мазать +майор +максимум +мальчик +манера +март +масса +мать +мафия +махать +мачта +машина +маэстро +маяк +мгла +мебель +медведь +мелкий +мемуары +менять +мера +место +метод +механизм +мечтать +мешать +миграция +мизинец +микрофон +миллион +минута +мировой +миссия +митинг +мишень +младший +мнение +мнимый +могила +модель +мозг +мойка +мокрый +молодой +момент +монах +море +мост +мотор +мохнатый +мочь +мошенник +мощный +мрачный +мстить +мудрый +мужчина +музыка +мука +мумия +мундир +муравей +мусор +мутный +муфта +муха +мучить +мушкетер +мыло +мысль +мыть +мычать +мышь +мэтр +мюзикл +мягкий +мякиш +мясо +мятый +мячик +набор +навык +нагрузка +надежда +наемный +нажать +называть +наивный +накрыть +налог +намерен +наносить +написать +народ +натура +наука +нация +начать +небо +невеста +негодяй +неделя +нежный +незнание +нелепый +немалый +неправда +нервный +нести +нефть +нехватка +нечистый +неясный +нива +нижний +низкий +никель +нирвана +нить +ничья +ниша +нищий +новый +нога +ножницы +ноздря +ноль +номер +норма +нота +ночь +ноша +ноябрь +нрав +нужный +нутро +нынешний +нырнуть +ныть +нюанс +нюхать +няня +оазис +обаяние +обвинять +обгонять +обещать +обжигать +обзор +обида +область +обмен +обнимать +оборона +образ +обучение +обходить +обширный +общий +объект +обычный +обязать +овальный +овес +овощи +овраг +овца +овчарка +огненный +огонь +огромный +огурец +одежда +одинокий +одобрить +ожидать +ожог +озарение +озеро +означать +оказать +океан +оклад +окно +округ +октябрь +окурок +олень +опасный +операция +описать +оплата +опора +оппонент +опрос +оптимизм +опускать +опыт +орать +орбита +орган +орден +орел +оригинал +оркестр +орнамент +оружие +осадок +освещать +осень +осина +осколок +осмотр +основной +особый +осуждать +отбор +отвечать +отдать +отец +отзыв +открытие +отмечать +относить +отпуск +отрасль +отставка +оттенок +отходить +отчет +отъезд +офицер +охапка +охота +охрана +оценка +очаг +очередь +очищать +очки +ошейник +ошибка +ощущение +павильон +падать +паек +пакет +палец +память +панель +папка +партия +паспорт +патрон +пауза +пафос +пахнуть +пациент +пачка +пашня +певец +педагог +пейзаж +пельмень +пенсия +пепел +период +песня +петля +пехота +печать +пешеход +пещера +пианист +пиво +пиджак +пиковый +пилот +пионер +пирог +писать +пить +пицца +пишущий +пища +план +плечо +плита +плохой +плыть +плюс +пляж +победа +повод +погода +подумать +поехать +пожимать +позиция +поиск +покой +получать +помнить +пони +поощрять +попадать +порядок +пост +поток +похожий +поцелуй +почва +пощечина +поэт +пояснить +право +предмет +проблема +пруд +прыгать +прямой +психолог +птица +публика +пугать +пудра +пузырь +пуля +пункт +пурга +пустой +путь +пухлый +пучок +пушистый +пчела +пшеница +пыль +пытка +пыхтеть +пышный +пьеса +пьяный +пятно +работа +равный +радость +развитие +район +ракета +рамка +ранний +рапорт +рассказ +раунд +рация +рвать +реальный +ребенок +реветь +регион +редакция +реестр +режим +резкий +рейтинг +река +религия +ремонт +рента +реплика +ресурс +реформа +рецепт +речь +решение +ржавый +рисунок +ритм +рифма +робкий +ровный +рогатый +родитель +рождение +розовый +роковой +роль +роман +ронять +рост +рота +роща +рояль +рубль +ругать +руда +ружье +руины +рука +руль +румяный +русский +ручка +рыба +рывок +рыдать +рыжий +рынок +рысь +рыть +рыхлый +рыцарь +рычаг +рюкзак +рюмка +рябой +рядовой +сабля +садовый +сажать +салон +самолет +сани +сапог +сарай +сатира +сауна +сахар +сбегать +сбивать +сбор +сбыт +свадьба +свет +свидание +свобода +связь +сгорать +сдвигать +сеанс +северный +сегмент +седой +сезон +сейф +секунда +сельский +семья +сентябрь +сердце +сеть +сечение +сеять +сигнал +сидеть +сизый +сила +символ +синий +сирота +система +ситуация +сиять +сказать +скважина +скелет +скидка +склад +скорый +скрывать +скучный +слава +слеза +слияние +слово +случай +слышать +слюна +смех +смирение +смотреть +смутный +смысл +смятение +снаряд +снег +снижение +сносить +снять +событие +совет +согласие +сожалеть +сойти +сокол +солнце +сомнение +сонный +сообщать +соперник +сорт +состав +сотня +соус +социолог +сочинять +союз +спать +спешить +спина +сплошной +способ +спутник +средство +срок +срывать +стать +ствол +стена +стихи +сторона +страна +студент +стыд +субъект +сувенир +сугроб +судьба +суета +суждение +сукно +сулить +сумма +сунуть +супруг +суровый +сустав +суть +сухой +суша +существо +сфера +схема +сцена +счастье +счет +считать +сшивать +съезд +сынок +сыпать +сырье +сытый +сыщик +сюжет +сюрприз +таблица +таежный +таинство +тайна +такси +талант +таможня +танец +тарелка +таскать +тахта +тачка +таять +тварь +твердый +творить +театр +тезис +текст +тело +тема +тень +теория +теплый +терять +тесный +тетя +техника +течение +тигр +типичный +тираж +титул +тихий +тишина +ткань +товарищ +толпа +тонкий +топливо +торговля +тоска +точка +тощий +традиция +тревога +трибуна +трогать +труд +трюк +тряпка +туалет +тугой +туловище +туман +тундра +тупой +турнир +тусклый +туфля +туча +туша +тыкать +тысяча +тьма +тюльпан +тюрьма +тяга +тяжелый +тянуть +убеждать +убирать +убогий +убыток +уважение +уверять +увлекать +угнать +угол +угроза +удар +удивлять +удобный +уезд +ужас +ужин +узел +узкий +узнавать +узор +уйма +уклон +укол +уксус +улетать +улица +улучшать +улыбка +уметь +умиление +умный +умолять +умысел +унижать +уносить +уныние +упасть +уплата +упор +упрекать +упускать +уран +урна +уровень +усадьба +усердие +усилие +ускорять +условие +усмешка +уснуть +успеть +усыпать +утешать +утка +уточнять +утро +утюг +уходить +уцелеть +участие +ученый +учитель +ушко +ущерб +уютный +уяснять +фабрика +фаворит +фаза +файл +факт +фамилия +фантазия +фара +фасад +февраль +фельдшер +феномен +ферма +фигура +физика +фильм +финал +фирма +фишка +флаг +флейта +флот +фокус +фольклор +фонд +форма +фото +фраза +фреска +фронт +фрукт +функция +фуражка +футбол +фыркать +халат +хамство +хаос +характер +хата +хватать +хвост +хижина +хилый +химия +хирург +хитрый +хищник +хлам +хлеб +хлопать +хмурый +ходить +хозяин +хоккей +холодный +хороший +хотеть +хохотать +храм +хрен +хриплый +хроника +хрупкий +художник +хулиган +хутор +царь +цвет +цель +цемент +центр +цепь +церковь +цикл +цилиндр +циничный +цирк +цистерна +цитата +цифра +цыпленок +чадо +чайник +часть +чашка +человек +чемодан +чепуха +черный +честь +четкий +чехол +чиновник +число +читать +членство +чреватый +чтение +чувство +чугунный +чудо +чужой +чукча +чулок +чума +чуткий +чучело +чушь +шаблон +шагать +шайка +шакал +шалаш +шампунь +шанс +шапка +шарик +шасси +шатер +шахта +шашлык +швейный +швырять +шевелить +шедевр +шейка +шелковый +шептать +шерсть +шестерка +шикарный +шинель +шипеть +широкий +шить +шишка +шкаф +школа +шкура +шланг +шлем +шлюпка +шляпа +шнур +шоколад +шорох +шоссе +шофер +шпага +шпион +шприц +шрам +шрифт +штаб +штора +штраф +штука +штык +шуба +шуметь +шуршать +шутка +щадить +щедрый +щека +щель +щенок +щепка +щетка +щука +эволюция +эгоизм +экзамен +экипаж +экономия +экран +эксперт +элемент +элита +эмблема +эмигрант +эмоция +энергия +эпизод +эпоха +эскиз +эссе +эстрада +этап +этика +этюд +эфир +эффект +эшелон +юбилей +юбка +южный +юмор +юноша +юрист +яблоко +явление +ягода +ядерный +ядовитый +ядро +язва +язык +яйцо +якорь +январь +японец +яркий +ярмарка +ярость +ярус +ясный +яхта +ячейка +ящик diff --git a/src/mnemonics/languages/spanish.txt b/src/mnemonics/languages/spanish.txt new file mode 100644 index 000000000..a5891ebb6 --- /dev/null +++ b/src/mnemonics/languages/spanish.txt @@ -0,0 +1,1629 @@ +Spanish +Español +4 +ábaco +abdomen +abeja +abierto +abogado +abono +aborto +abrazo +abrir +abuelo +abuso +acabar +academia +acceso +acción +aceite +acelga +acento +aceptar +ácido +aclarar +acné +acoger +acoso +activo +acto +actriz +actuar +acudir +acuerdo +acusar +adicto +admitir +adoptar +adorno +aduana +adulto +aéreo +afectar +afición +afinar +afirmar +ágil +agitar +agonía +agosto +agotar +agregar +agrio +agua +agudo +águila +aguja +ahogo +ahorro +aire +aislar +ajedrez +ajeno +ajuste +alacrán +alambre +alarma +alba +álbum +alcalde +aldea +alegre +alejar +alerta +aleta +alfiler +alga +algodón +aliado +aliento +alivio +alma +almeja +almíbar +altar +alteza +altivo +alto +altura +alumno +alzar +amable +amante +amapola +amargo +amasar +ámbar +ámbito +ameno +amigo +amistad +amor +amparo +amplio +ancho +anciano +ancla +andar +andén +anemia +ángulo +anillo +ánimo +anís +anotar +antena +antiguo +antojo +anual +anular +anuncio +añadir +añejo +año +apagar +aparato +apetito +apio +aplicar +apodo +aporte +apoyo +aprender +aprobar +apuesta +apuro +arado +araña +arar +árbitro +árbol +arbusto +archivo +arco +arder +ardilla +arduo +área +árido +aries +armonía +arnés +aroma +arpa +arpón +arreglo +arroz +arruga +arte +artista +asa +asado +asalto +ascenso +asegurar +aseo +asesor +asiento +asilo +asistir +asno +asombro +áspero +astilla +astro +astuto +asumir +asunto +atajo +ataque +atar +atento +ateo +ático +atleta +átomo +atraer +atroz +atún +audaz +audio +auge +aula +aumento +ausente +autor +aval +avance +avaro +ave +avellana +avena +avestruz +avión +aviso +ayer +ayuda +ayuno +azafrán +azar +azote +azúcar +azufre +azul +baba +babor +bache +bahía +baile +bajar +balanza +balcón +balde +bambú +banco +banda +baño +barba +barco +barniz +barro +báscula +bastón +basura +batalla +batería +batir +batuta +baúl +bazar +bebé +bebida +bello +besar +beso +bestia +bicho +bien +bingo +blanco +bloque +blusa +boa +bobina +bobo +boca +bocina +boda +bodega +boina +bola +bolero +bolsa +bomba +bondad +bonito +bono +bonsái +borde +borrar +bosque +bote +botín +bóveda +bozal +bravo +brazo +brecha +breve +brillo +brinco +brisa +broca +broma +bronce +brote +bruja +brusco +bruto +buceo +bucle +bueno +buey +bufanda +bufón +búho +buitre +bulto +burbuja +burla +burro +buscar +butaca +buzón +caballo +cabeza +cabina +cabra +cacao +cadáver +cadena +caer +café +caída +caimán +caja +cajón +cal +calamar +calcio +caldo +calidad +calle +calma +calor +calvo +cama +cambio +camello +camino +campo +cáncer +candil +canela +canguro +canica +canto +caña +cañón +caoba +caos +capaz +capitán +capote +captar +capucha +cara +carbón +cárcel +careta +carga +cariño +carne +carpeta +carro +carta +casa +casco +casero +caspa +castor +catorce +catre +caudal +causa +cazo +cebolla +ceder +cedro +celda +célebre +celoso +célula +cemento +ceniza +centro +cerca +cerdo +cereza +cero +cerrar +certeza +césped +cetro +chacal +chaleco +champú +chancla +chapa +charla +chico +chiste +chivo +choque +choza +chuleta +chupar +ciclón +ciego +cielo +cien +cierto +cifra +cigarro +cima +cinco +cine +cinta +ciprés +circo +ciruela +cisne +cita +ciudad +clamor +clan +claro +clase +clave +cliente +clima +clínica +cobre +cocción +cochino +cocina +coco +código +codo +cofre +coger +cohete +cojín +cojo +cola +colcha +colegio +colgar +colina +collar +colmo +columna +combate +comer +comida +cómodo +compra +conde +conejo +conga +conocer +consejo +contar +copa +copia +corazón +corbata +corcho +cordón +corona +correr +coser +cosmos +costa +cráneo +cráter +crear +crecer +creído +crema +cría +crimen +cripta +crisis +cromo +crónica +croqueta +crudo +cruz +cuadro +cuarto +cuatro +cubo +cubrir +cuchara +cuello +cuento +cuerda +cuesta +cueva +cuidar +culebra +culpa +culto +cumbre +cumplir +cuna +cuneta +cuota +cupón +cúpula +curar +curioso +curso +curva +cutis +dama +danza +dar +dardo +dátil +deber +débil +década +decir +dedo +defensa +definir +dejar +delfín +delgado +delito +demora +denso +dental +deporte +derecho +derrota +desayuno +deseo +desfile +desnudo +destino +desvío +detalle +detener +deuda +día +diablo +diadema +diamante +diana +diario +dibujo +dictar +diente +dieta +diez +difícil +digno +dilema +diluir +dinero +directo +dirigir +disco +diseño +disfraz +diva +divino +doble +doce +dolor +domingo +don +donar +dorado +dormir +dorso +dos +dosis +dragón +droga +ducha +duda +duelo +dueño +dulce +dúo +duque +durar +dureza +duro +ébano +ebrio +echar +eco +ecuador +edad +edición +edificio +editor +educar +efecto +eficaz +eje +ejemplo +elefante +elegir +elemento +elevar +elipse +élite +elixir +elogio +eludir +embudo +emitir +emoción +empate +empeño +empleo +empresa +enano +encargo +enchufe +encía +enemigo +enero +enfado +enfermo +engaño +enigma +enlace +enorme +enredo +ensayo +enseñar +entero +entrar +envase +envío +época +equipo +erizo +escala +escena +escolar +escribir +escudo +esencia +esfera +esfuerzo +espada +espejo +espía +esposa +espuma +esquí +estar +este +estilo +estufa +etapa +eterno +ética +etnia +evadir +evaluar +evento +evitar +exacto +examen +exceso +excusa +exento +exigir +exilio +existir +éxito +experto +explicar +exponer +extremo +fábrica +fábula +fachada +fácil +factor +faena +faja +falda +fallo +falso +faltar +fama +familia +famoso +faraón +farmacia +farol +farsa +fase +fatiga +fauna +favor +fax +febrero +fecha +feliz +feo +feria +feroz +fértil +fervor +festín +fiable +fianza +fiar +fibra +ficción +ficha +fideo +fiebre +fiel +fiera +fiesta +figura +fijar +fijo +fila +filete +filial +filtro +fin +finca +fingir +finito +firma +flaco +flauta +flecha +flor +flota +fluir +flujo +flúor +fobia +foca +fogata +fogón +folio +folleto +fondo +forma +forro +fortuna +forzar +fosa +foto +fracaso +frágil +franja +frase +fraude +freír +freno +fresa +frío +frito +fruta +fuego +fuente +fuerza +fuga +fumar +función +funda +furgón +furia +fusil +fútbol +futuro +gacela +gafas +gaita +gajo +gala +galería +gallo +gamba +ganar +gancho +ganga +ganso +garaje +garza +gasolina +gastar +gato +gavilán +gemelo +gemir +gen +género +genio +gente +geranio +gerente +germen +gesto +gigante +gimnasio +girar +giro +glaciar +globo +gloria +gol +golfo +goloso +golpe +goma +gordo +gorila +gorra +gota +goteo +gozar +grada +gráfico +grano +grasa +gratis +grave +grieta +grillo +gripe +gris +grito +grosor +grúa +grueso +grumo +grupo +guante +guapo +guardia +guerra +guía +guiño +guion +guiso +guitarra +gusano +gustar +haber +hábil +hablar +hacer +hacha +hada +hallar +hamaca +harina +haz +hazaña +hebilla +hebra +hecho +helado +helio +hembra +herir +hermano +héroe +hervir +hielo +hierro +hígado +higiene +hijo +himno +historia +hocico +hogar +hoguera +hoja +hombre +hongo +honor +honra +hora +hormiga +horno +hostil +hoyo +hueco +huelga +huerta +hueso +huevo +huida +huir +humano +húmedo +humilde +humo +hundir +huracán +hurto +icono +ideal +idioma +ídolo +iglesia +iglú +igual +ilegal +ilusión +imagen +imán +imitar +impar +imperio +imponer +impulso +incapaz +índice +inerte +infiel +informe +ingenio +inicio +inmenso +inmune +innato +insecto +instante +interés +íntimo +intuir +inútil +invierno +ira +iris +ironía +isla +islote +jabalí +jabón +jamón +jarabe +jardín +jarra +jaula +jazmín +jefe +jeringa +jinete +jornada +joroba +joven +joya +juerga +jueves +juez +jugador +jugo +juguete +juicio +junco +jungla +junio +juntar +júpiter +jurar +justo +juvenil +juzgar +kilo +koala +labio +lacio +lacra +lado +ladrón +lagarto +lágrima +laguna +laico +lamer +lámina +lámpara +lana +lancha +langosta +lanza +lápiz +largo +larva +lástima +lata +látex +latir +laurel +lavar +lazo +leal +lección +leche +lector +leer +legión +legumbre +lejano +lengua +lento +leña +león +leopardo +lesión +letal +letra +leve +leyenda +libertad +libro +licor +líder +lidiar +lienzo +liga +ligero +lima +límite +limón +limpio +lince +lindo +línea +lingote +lino +linterna +líquido +liso +lista +litera +litio +litro +llaga +llama +llanto +llave +llegar +llenar +llevar +llorar +llover +lluvia +lobo +loción +loco +locura +lógica +logro +lombriz +lomo +lonja +lote +lucha +lucir +lugar +lujo +luna +lunes +lupa +lustro +luto +luz +maceta +macho +madera +madre +maduro +maestro +mafia +magia +mago +maíz +maldad +maleta +malla +malo +mamá +mambo +mamut +manco +mando +manejar +manga +maniquí +manjar +mano +manso +manta +mañana +mapa +máquina +mar +marco +marea +marfil +margen +marido +mármol +marrón +martes +marzo +masa +máscara +masivo +matar +materia +matiz +matriz +máximo +mayor +mazorca +mecha +medalla +medio +médula +mejilla +mejor +melena +melón +memoria +menor +mensaje +mente +menú +mercado +merengue +mérito +mes +mesón +meta +meter +método +metro +mezcla +miedo +miel +miembro +miga +mil +milagro +militar +millón +mimo +mina +minero +mínimo +minuto +miope +mirar +misa +miseria +misil +mismo +mitad +mito +mochila +moción +moda +modelo +moho +mojar +molde +moler +molino +momento +momia +monarca +moneda +monja +monto +moño +morada +morder +moreno +morir +morro +morsa +mortal +mosca +mostrar +motivo +mover +móvil +mozo +mucho +mudar +mueble +muela +muerte +muestra +mugre +mujer +mula +muleta +multa +mundo +muñeca +mural +muro +músculo +museo +musgo +música +muslo +nácar +nación +nadar +naipe +naranja +nariz +narrar +nasal +natal +nativo +natural +náusea +naval +nave +navidad +necio +néctar +negar +negocio +negro +neón +nervio +neto +neutro +nevar +nevera +nicho +nido +niebla +nieto +niñez +niño +nítido +nivel +nobleza +noche +nómina +noria +norma +norte +nota +noticia +novato +novela +novio +nube +nuca +núcleo +nudillo +nudo +nuera +nueve +nuez +nulo +número +nutria +oasis +obeso +obispo +objeto +obra +obrero +observar +obtener +obvio +oca +ocaso +océano +ochenta +ocho +ocio +ocre +octavo +octubre +oculto +ocupar +ocurrir +odiar +odio +odisea +oeste +ofensa +oferta +oficio +ofrecer +ogro +oído +oír +ojo +ola +oleada +olfato +olivo +olla +olmo +olor +olvido +ombligo +onda +onza +opaco +opción +ópera +opinar +oponer +optar +óptica +opuesto +oración +orador +oral +órbita +orca +orden +oreja +órgano +orgía +orgullo +oriente +origen +orilla +oro +orquesta +oruga +osadía +oscuro +osezno +oso +ostra +otoño +otro +oveja +óvulo +óxido +oxígeno +oyente +ozono +pacto +padre +paella +página +pago +país +pájaro +palabra +palco +paleta +pálido +palma +paloma +palpar +pan +panal +pánico +pantera +pañuelo +papá +papel +papilla +paquete +parar +parcela +pared +parir +paro +párpado +parque +párrafo +parte +pasar +paseo +pasión +paso +pasta +pata +patio +patria +pausa +pauta +pavo +payaso +peatón +pecado +pecera +pecho +pedal +pedir +pegar +peine +pelar +peldaño +pelea +peligro +pellejo +pelo +peluca +pena +pensar +peñón +peón +peor +pepino +pequeño +pera +percha +perder +pereza +perfil +perico +perla +permiso +perro +persona +pesa +pesca +pésimo +pestaña +pétalo +petróleo +pez +pezuña +picar +pichón +pie +piedra +pierna +pieza +pijama +pilar +piloto +pimienta +pino +pintor +pinza +piña +piojo +pipa +pirata +pisar +piscina +piso +pista +pitón +pizca +placa +plan +plata +playa +plaza +pleito +pleno +plomo +pluma +plural +pobre +poco +poder +podio +poema +poesía +poeta +polen +policía +pollo +polvo +pomada +pomelo +pomo +pompa +poner +porción +portal +posada +poseer +posible +poste +potencia +potro +pozo +prado +precoz +pregunta +premio +prensa +preso +previo +primo +príncipe +prisión +privar +proa +probar +proceso +producto +proeza +profesor +programa +prole +promesa +pronto +propio +próximo +prueba +público +puchero +pudor +pueblo +puerta +puesto +pulga +pulir +pulmón +pulpo +pulso +puma +punto +puñal +puño +pupa +pupila +puré +quedar +queja +quemar +querer +queso +quieto +química +quince +quitar +rábano +rabia +rabo +ración +radical +raíz +rama +rampa +rancho +rango +rapaz +rápido +rapto +rasgo +raspa +rato +rayo +raza +razón +reacción +realidad +rebaño +rebote +recaer +receta +rechazo +recoger +recreo +recto +recurso +red +redondo +reducir +reflejo +reforma +refrán +refugio +regalo +regir +regla +regreso +rehén +reino +reír +reja +relato +relevo +relieve +relleno +reloj +remar +remedio +remo +rencor +rendir +renta +reparto +repetir +reposo +reptil +res +rescate +resina +respeto +resto +resumen +retiro +retorno +retrato +reunir +revés +revista +rey +rezar +rico +riego +rienda +riesgo +rifa +rígido +rigor +rincón +riñón +río +riqueza +risa +ritmo +rito diff --git a/src/mnemonics/mnemonics.cpp b/src/mnemonics/mnemonics.cpp new file mode 100644 index 000000000..4a343169a --- /dev/null +++ b/src/mnemonics/mnemonics.cpp @@ -0,0 +1,189 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace session::mnemonics { + +const Mnemonics* find_language(std::string_view name) { + for (auto lang : get_languages()) { + if (lang->english_name == name || lang->native_name == name) + return lang; + } + return nullptr; +} + +namespace { + /** + * Converts ASCII characters in a UTF-8 string to lowercase. + * Non-ASCII characters are left unchanged. + */ + std::string to_lower_ascii(std::string_view s) { + std::string result; + result.reserve(s.size()); + for (char c : s) { + if (static_cast(c) < 128) + result.push_back(static_cast(std::tolower(static_cast(c)))); + else + result.push_back(c); + } + return result; + } + + /** + * Gets a prefix of a UTF-8 string containing at most n_codepoints. + */ + std::string_view get_prefix(std::string_view s, int n_codepoints) { + if (n_codepoints <= 0) + return ""; + + const char* it = s.data(); + const char* end = it + s.size(); + int count = 0; + while (it < end && count < n_codepoints) { + uint8_t b = static_cast(*it); + if (b < 0x80) // 0xxxxxxx: 1-byte ASCII + it += 1; + else if ((b & 0xE0) == 0xC0) // 110xxxxx: 2-byte sequence + it += 2; + else if ((b & 0xF0) == 0xE0) // 1110xxxx: 3-byte sequence + it += 3; + else if ((b & 0xF8) == 0xF0) // 11110xxx: 4-byte sequence + it += 4; + else // Invalid UTF-8 start byte or continuation byte + it += 1; + + count++; + } + return std::string_view(s.data(), std::min(it - s.data(), s.size())); + } + + struct WordMap { + std::unordered_map map; + std::unordered_map prefix_map; + std::mutex mutex; + bool initialized = false; + }; + + WordMap& get_word_map(const Mnemonics& lang) { + static std::mutex maps_mutex; + static std::unordered_map> maps; + + std::lock_guard lock(maps_mutex); + auto& ptr = maps[&lang]; + if (!ptr) + ptr = std::make_unique(); + return *ptr; + } + + int get_word_index(const Mnemonics& lang, std::string_view word) { + auto& wm = get_word_map(lang); + { + std::lock_guard lock(wm.mutex); + if (!wm.initialized) { + for (int i = 0; i < static_cast(NWORDS); ++i) { + std::string_view w = lang.words[i]; + std::string lower_w = to_lower_ascii(w); + wm.map[lower_w] = i; + + std::string lower_prefix = to_lower_ascii(get_prefix(w, lang.prefix_len)); + if (!lower_prefix.empty()) + wm.prefix_map[lower_prefix] = i; + } + wm.initialized = true; + } + } + + std::string lower_input = to_lower_ascii(word); + + auto it = wm.map.find(lower_input); + if (it != wm.map.end()) + return it->second; + + std::string lower_input_prefix = to_lower_ascii(get_prefix(lower_input, lang.prefix_len)); + if (!lower_input_prefix.empty()) { + auto pit = wm.prefix_map.find(lower_input_prefix); + if (pit != wm.prefix_map.end()) + return pit->second; + } + + return -1; + } +} // namespace + +std::vector bytes_to_words( + std::span bytes, const Mnemonics& lang) { + if (bytes.size() % 4 != 0) + throw std::invalid_argument("Input length must be a multiple of 4 bytes"); + + std::vector result; + result.reserve((bytes.size() / 4) * 3); + + for (size_t i = 0; i < bytes.size(); i += 4) { + uint32_t val = oxenc::load_little_to_host(&bytes[i]); + + uint32_t a = val % NWORDS; + uint32_t b = (val / NWORDS + a) % NWORDS; + uint32_t c = (val / NWORDS / NWORDS + b) % NWORDS; + + result.push_back(lang.words[a]); + result.push_back(lang.words[b]); + result.push_back(lang.words[c]); + } + + return result; +} + +std::vector bytes_to_words( + std::span bytes, std::string_view lang_name) { + auto lang = find_language(lang_name); + if (!lang) + throw std::invalid_argument("Unknown mnemonic language: " + std::string(lang_name)); + return bytes_to_words(bytes, *lang); +} + +std::vector words_to_bytes( + std::span words, const Mnemonics& lang) { + if (words.size() % 3 != 0) + throw std::invalid_argument("Input word count must be a multiple of 3"); + + std::vector result; + result.resize((words.size() / 3) * 4); + + for (size_t i = 0; i < words.size(); i += 3) { + int w1 = get_word_index(lang, words[i]); + int w2 = get_word_index(lang, words[i + 1]); + int w3 = get_word_index(lang, words[i + 2]); + + if (w1 < 0 || w2 < 0 || w3 < 0) + throw std::invalid_argument("Word not found in mnemonic dictionary"); + + uint32_t a = static_cast(w1); + uint32_t b = static_cast(w2); + uint32_t c = static_cast(w3); + + uint32_t x = a + ((NWORDS - a + b) % NWORDS) * NWORDS + + ((NWORDS - b + c) % NWORDS) * (NWORDS * NWORDS); + + oxenc::write_host_as_little(x, &result[(i / 3) * 4]); + } + + return result; +} + +std::vector words_to_bytes( + std::span words, std::string_view lang_name) { + auto lang = find_language(lang_name); + if (!lang) + throw std::invalid_argument("Unknown mnemonic language: " + std::string(lang_name)); + return words_to_bytes(words, *lang); +} + +} // namespace session::mnemonics diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1d0d8864f..245ca7368 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,7 @@ set(LIB_SESSION_UTESTS_SOURCES test_hash.cpp #test_logging.cpp # Handled separately, see below test_multi_encrypt.cpp + test_mnemonics.cpp test_proto.cpp test_pro_backend.cpp test_random.cpp diff --git a/tests/test_mnemonics.cpp b/tests/test_mnemonics.cpp new file mode 100644 index 000000000..57b2088e8 --- /dev/null +++ b/tests/test_mnemonics.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include + +using namespace session::mnemonics; + +TEST_CASE("Mnemonic round-trip tests", "[mnemonics]") { + std::vector data_128(16); + std::vector data_256(32); + + std::mt19937 gen(42); + std::uniform_int_distribution dist(0, 255); + + auto fill_random = [&](std::vector& v) { + for (auto& b : v) + b = static_cast(dist(gen)); + }; + + fill_random(data_128); + fill_random(data_256); + + for (auto lang : get_languages()) { + SECTION("Language: " + std::string(lang->english_name)) { + // 128-bit -> 12 words -> 128-bit + auto words12 = bytes_to_words(data_128, *lang); + CHECK(words12.size() == 12); + auto back12 = words_to_bytes(words12, *lang); + CHECK(back12 == data_128); + + // 256-bit -> 24 words -> 256-bit + auto words24 = bytes_to_words(data_256, *lang); + CHECK(words24.size() == 24); + auto back24 = words_to_bytes(words24, *lang); + CHECK(back24 == data_256); + } + } +} + +TEST_CASE("Mnemonic case-insensitivity and prefix matching", "[mnemonics]") { + auto english = find_language("English"); + REQUIRE(english); + + // 4 bytes: [0x01, 0x02, 0x03, 0x04] + // V = 0x04030201 = 67305985 + // A = 67305985 % 1626 = 1443 + // B = (67305985 / 1626 + 1443) % 1626 = (41393 + 1443) % 1626 = 42836 % 1626 = 180 + // C = (67305985 / 1626 / 1626 + 180) % 1626 = (25 + 180) % 1626 = 205 + + // Words for English at indices 1443, 180, 205 + std::vector data = { + std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + auto words = bytes_to_words(data, *english); + REQUIRE(words.size() == 3); + + SECTION("Exact match") { + auto back = words_to_bytes(words, *english); + CHECK(back == data); + } + + SECTION("Case-insensitive match (ASCII)") { + std::vector upper_words; + std::vector storage; + for (auto w : words) { + std::string upper(w); + for (auto& c : upper) + c = std::toupper(static_cast(c)); + storage.push_back(upper); + } + for (const auto& s : storage) + upper_words.push_back(s); + + auto back = words_to_bytes(upper_words, *english); + CHECK(back == data); + } + + SECTION("Prefix match") { + std::vector prefix_words; + std::vector storage; + for (auto w : words) { + storage.push_back(std::string(w.substr(0, english->prefix_len))); + } + for (const auto& s : storage) + prefix_words.push_back(s); + + auto back = words_to_bytes(prefix_words, *english); + CHECK(back == data); + } + + SECTION("Prefix match with typo after prefix") { + std::vector typo_words; + std::vector storage; + for (auto w : words) { + storage.push_back(std::string(w.substr(0, english->prefix_len)) + "xyz"); + } + for (const auto& s : storage) + typo_words.push_back(s); + + auto back = words_to_bytes(typo_words, *english); + CHECK(back == data); + } +} + +TEST_CASE("Mnemonic language lookup", "[mnemonics]") { + CHECK(find_language("English") != nullptr); + CHECK(find_language("German") != nullptr); + CHECK(find_language("Deutsch") != nullptr); + CHECK(find_language("русский язык") != nullptr); + CHECK(find_language("NonExistent") == nullptr); +} + +TEST_CASE("Mnemonic error handling", "[mnemonics]") { + auto english = find_language("English"); + + SECTION("Invalid byte length") { + std::vector data(15); + CHECK_THROWS_AS(bytes_to_words(data, *english), std::invalid_argument); + } + + SECTION("Invalid word count") { + std::vector words = {"abbey", "abducts"}; + CHECK_THROWS_AS(words_to_bytes(words, *english), std::invalid_argument); + } + + SECTION("Unknown word") { + std::vector words = {"abbey", "abducts", "zzzzzz"}; + CHECK_THROWS_AS(words_to_bytes(words, *english), std::invalid_argument); + } +} diff --git a/utils/verify_mnemonics.py b/utils/verify_mnemonics.py new file mode 100644 index 000000000..bee98d06b --- /dev/null +++ b/utils/verify_mnemonics.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os +import sys + +def verify_language(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + lines = [line.strip() for line in f.readlines() if line.strip()] + + if len(lines) != 1629: + print(f"[-] {filepath}: Invalid line count ({len(lines)}), expected 1629") + return False + + english_name = lines[0] + prefix_len = int(lines[2]) + words = lines[3:] + + prefixes = {} + collisions = [] + + for word in words: + # Take the prefix in codepoints, case-folded for case-insensitive comparison + prefix_cf = word[:prefix_len].casefold() + if prefix_cf in prefixes: + collisions.append((prefix_cf, prefixes[prefix_cf], word)) + else: + prefixes[prefix_cf] = word + + if collisions: + print(f"[-] {english_name} ({filepath}): Found {len(collisions)} CASE-INSENSITIVE collisions at prefix length {prefix_len}:") + for pref, word1, word2 in collisions[:10]: + print(f" Prefix '{pref}' matches both '{word1}' and '{word2}'") + if len(collisions) > 10: + print(f" ... and {len(collisions) - 10} more.") + return False + + # Check if prefix_len is larger than necessary (case-insensitive) + min_needed = 1 + while True: + test_prefixes = set() + collision_found = False + for word in words: + p = word[:min_needed].casefold() + if p in test_prefixes: + collision_found = True + break + test_prefixes.add(p) + if not collision_found: + break + min_needed += 1 + + if min_needed < prefix_len: + print(f"[!] {english_name}: prefix_len is {prefix_len}, but {min_needed} would suffice (case-insensitive).") + elif min_needed > prefix_len: + print(f"[-] {english_name}: prefix_len {prefix_len} is INSUFFICIENT for case-insensitive uniqueness (needs {min_needed})") + return False + + print(f"[+] {english_name}: Verified case-insensitive (prefix_len={prefix_len})") + return True + +def main(): + lang_dir = "src/mnemonics/languages" + if not os.path.exists(lang_dir): + print(f"Error: Directory {lang_dir} not found.") + sys.exit(1) + + files = [f for f in os.listdir(lang_dir) if f.endswith('.txt')] + files.sort() + + success = True + for filename in files: + if not verify_language(os.path.join(lang_dir, filename)): + success = False + + if not success: + sys.exit(1) + +if __name__ == "__main__": + main() From c2af9f06d6ce5240cebae0b8b1db3675567683a5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 16:48:49 -0300 Subject: [PATCH 040/193] Add ability to use a predefined seed Use it to fix tests that were failing to produce a valid account message. --- include/session/core.hpp | 71 +++++++++++++++++++++++++++++--- include/session/core/globals.hpp | 5 +++ src/core/devices.cpp | 4 +- src/core/globals.cpp | 20 ++++++--- tests/test_core_devices.cpp | 28 ------------- tests/test_core_network.cpp | 3 +- tests/test_helper.hpp | 34 +++++++++++++++ tests/test_poll.cpp | 48 ++++++--------------- 8 files changed, 134 insertions(+), 79 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index dfa5f08c2..fba036c3e 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -138,6 +138,26 @@ namespace detail { class CoreComponent; } +/// Wraps a predefined 32-byte account seed to pass to the Core constructor, overriding any seed +/// already stored in the database. Used when restoring an existing account from a seed. +struct predefined_seed { + cleared_b32 bytes; + explicit predefined_seed(std::span s) { + std::ranges::copy(s, bytes.begin()); + } + explicit predefined_seed(std::span s) { + std::ranges::copy(std::as_bytes(s), bytes.begin()); + } +}; + +/// Concept satisfied by any type usable as a Core constructor option: a sqlite database option +/// (encryption, behaviour), or a Core-specific option tag (predefined_seed, callbacks). All +/// options can be passed in any order after the db_path positional argument. +template +concept CoreOption = sqlite::DatabaseOption> || + std::same_as, predefined_seed> || + std::same_as, callbacks>; + class Core { friend class session::TestHelper; // for unit tests @@ -171,12 +191,53 @@ class Core { void _update_polling(); void _poll(); + // Extracts an option of type T from a pack; returns the first match wrapped in optional, or + // nullopt if not present. Mirrors the same helper in session::sqlite::Database. + template + static constexpr auto _maybe_instance(Opts&&... opts) { + using Ret = std::optional; + if constexpr (sizeof...(Opts) == 0) + return Ret{std::nullopt}; + else { + auto finder = []( + auto&& self, Opt&& o, More&&... more) -> Ret { + if constexpr (std::same_as, T>) + return std::make_optional(std::forward(o)); + else if constexpr (sizeof...(More) > 0) + return self(self, std::forward(more)...); + else + return std::nullopt; + }; + return finder(finder, std::forward(opts)...); + } + } + + // Constructs a sqlite::Database from the subset of opts that satisfy sqlite::DatabaseOption. + template + static sqlite::Database _make_db(std::filesystem::path path, Opts&&... opts) { + return std::apply( + [&](DBOpts&&... db_opts) { + return sqlite::Database{std::move(path), std::forward(db_opts)...}; + }, + std::tuple_cat([](T&& o) { + if constexpr (sqlite::DatabaseOption>) + return std::tuple>{std::forward(o)}; + else + return std::tuple<>{}; + }(std::forward(opts))...)); + } + public: - // Constructor taking a struct of callbacks to invoke on various events, and any number of - // session::SQLite database options to open the core database. - template - Core(core::callbacks callbacks, std::filesystem::path db_path, const DBOpts&... opts) : - callbacks{std::move(callbacks)}, db{std::move(db_path), opts...} { + // Constructor taking a db path and any mix of Core and database options in any order. + // - callbacks (optional, defaults to empty): event callbacks for the application + // - predefined_seed (optional): overrides any seed stored in the database + // - database options: see sqlite::DatabaseOption (encryption, behaviour flags, etc.) + template + Core(std::filesystem::path db_path, Opts&&... opts) : + callbacks{_maybe_instance(std::forward(opts)...).value_or({})}, + db{_make_db(std::move(db_path), std::forward(opts)...)} { + if (auto s = _maybe_instance(std::forward(opts)...)) + globals._predefined_seed = std::move(s->bytes); init(); } diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 09f30afa5..70c028c88 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,10 @@ class Globals final : detail::CoreComponent { void init() override; + // If set by the Core constructor before init(), used as the initial account seed when the + // database does not yet contain one. Cleared after use in init(). + std::optional _predefined_seed; + public: // Retrieval methods. These query for the given key and, if the type matches, return the given // value. You get back nullopt if the database key does not exist, or if it contains diff --git a/src/core/devices.cpp b/src/core/devices.cpp index e4be4ce87..a68cfce38 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -597,7 +597,7 @@ namespace { // Consumes and stores any unknown extra fields from `btdc` up to (but not including) `key` into // `extras` void read_extras(oxenc::bt_dict_consumer& btdc, std::string_view key, oxenc::bt_dict& extra) { - while (btdc.key() < key) + while (!btdc.is_finished() && btdc.key() < key) consume_extra(btdc, extra); } @@ -621,7 +621,7 @@ namespace { info.description = dev.maybe("d").value_or(""sv); read_extras(dev, "t", info.extra); - auto type = dev.require("t"); + auto type = dev.maybe("t").value_or(""sv); info.other_device.clear(); if (type == "i") info.type = device::Type::Session_iOS; diff --git a/src/core/globals.cpp b/src/core/globals.cpp index bb336cb3d..4e6b78f86 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -120,12 +120,18 @@ void Globals::init() { SQLite::Transaction tx{c.sql}; cleared_b32 seed; bool have_seed = get_blob_to("_seed", seed); + const cleared_b32* seed_to_use = &seed; if (!have_seed) { - // FIXME: we should allow full 32-byte seeds here, but for now this is compatible with the - // 16-byte/128-bit seed that Session accounts use which is 16 random bytes followed by 16 - // 0s: - randombytes_buf(seed.data(), 16); - std::memset(seed.data() + 16, 0, 16); + if (_predefined_seed) { + // Use the predefined seed directly, avoiding an extra copy+clear. + seed_to_use = &*_predefined_seed; + } else { + // FIXME: we should allow full 32-byte seeds here, but for now this is compatible with + // the 16-byte/128-bit seed that Session accounts use which is 16 random bytes followed + // by 16 0s: + randombytes_buf(seed.data(), 16); + std::memset(seed.data() + 16, 0, 16); + } } auto rw = _account_seed.resize(64); @@ -133,7 +139,9 @@ void Globals::init() { crypto_sign_ed25519_seed_keypair( _pubkey_ed25519.data(), reinterpret_cast(rw.buf.data()), - reinterpret_cast(seed.data())); + reinterpret_cast(seed_to_use->data())); + + _predefined_seed.reset(); // Clear now that it has been consumed if (0 != crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) // This *should* be impossible when starting from a seed because that would mean the seed // generation produced an invalid Ed pubkey! diff --git a/tests/test_core_devices.cpp b/tests/test_core_devices.cpp index e627ff3ad..9d49bed95 100644 --- a/tests/test_core_devices.cpp +++ b/tests/test_core_devices.cpp @@ -1,11 +1,8 @@ -#include #include #include #include -#include #include -#include #include #include #include @@ -19,31 +16,6 @@ using namespace session; using namespace session::core; using namespace std::literals; -static constexpr std::array test_key_bytes{}; - -// Smart-pointer-like wrapper around a unique_ptr with RAII cleanup of the temp DB file. -struct TempCore { - std::filesystem::path path; - std::unique_ptr core; - - explicit TempCore(core::callbacks cb = {}) : - path{[] { - static std::atomic n{0}; - return std::filesystem::temp_directory_path() / - fmt::format("test_core_devices_{}.db", ++n); - }()}, - core{std::make_unique(std::move(cb), path, sqlite::raw_key{test_key_bytes})} {} - - ~TempCore() { - core.reset(); // close DB before removing the file - std::error_code ec; - std::filesystem::remove(path, ec); - } - - Core* operator->() { return core.get(); } - Core& operator*() { return *core; } -}; - TEST_CASE("Devices - identity", "[core][devices]") { TempCore c; diff --git a/tests/test_core_network.cpp b/tests/test_core_network.cpp index 450f10a1d..7b1a8fcbc 100644 --- a/tests/test_core_network.cpp +++ b/tests/test_core_network.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include "utils.hpp" @@ -13,7 +12,7 @@ TEST_CASE("Core can hold an optional Network interface", "[core][network]") { if (std::filesystem::exists(db_path)) std::filesystem::remove(db_path); - core::Core core{callbacks, db_path, sqlite::argon2id_password{"test"}}; + core::Core core{db_path, callbacks}; SECTION("Network is initially null") { CHECK(core.network() == nullptr); diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index f37812146..f2fdc0ab1 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -1,5 +1,9 @@ #pragma once +#include + +#include +#include #include #include #include @@ -7,10 +11,40 @@ namespace session { +// Smart-pointer-like RAII wrapper around a Core backed by a unique temporary DB file. +// The DB file is removed on destruction. Default encryption uses a zeroed raw_key. +struct TempCore { + std::filesystem::path path; + std::unique_ptr core; + + template + explicit TempCore(Opts&&... opts) : + path{[] { + static std::atomic n{0}; + return std::filesystem::temp_directory_path() / fmt::format("test_core_{}.db", ++n); + }()}, + core{std::make_unique(path, std::forward(opts)...)} {} + + ~TempCore() { + core.reset(); // close DB before removing the file + std::error_code ec; + std::filesystem::remove(path, ec); + } + + core::Core* operator->() { return core.get(); } + core::Core& operator*() { return *core; } +}; + class TestHelper { public: static void poll(core::Core& core) { core._poll(); } + // Returns the last_hash stored for the given namespace (or nullopt if none). + static std::optional namespace_last_hash(core::Core& core, int16_t ns) { + return core.db.conn().prepared_maybe_get( + "SELECT last_hash FROM namespace_sync WHERE namespace = ?", ns); + } + // Returns the raw 32-byte seed for the account key identified by the given x25519 public key. static cleared_b32 account_key_seed( core::Devices& d, std::span x25519_pub) { diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 4f886314a..f5306a8d1 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -1,16 +1,12 @@ #include -#include #include #include #include -#include #include #include -#include #include "test_helper.hpp" -#include "utils.hpp" using namespace session; @@ -49,8 +45,6 @@ TEST_CASE("Core automatic polling", "[core][poll]") { if (std::filesystem::exists(db_path)) std::filesystem::remove(db_path); - auto test_keys = get_deterministic_test_keys(); - bool received = false; core::callbacks callbacks; callbacks.device_link_request = [&](int, @@ -58,7 +52,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { std::span) { received = true; }; { - core::Core core{callbacks, db_path, sqlite::argon2id_password{"test"}}; + core::Core core{db_path, callbacks}; auto mock_net = std::make_shared(); core.set_network(mock_net); @@ -77,36 +71,18 @@ TEST_CASE("Core automatic polling", "[core][poll]") { CHECK(params.contains("timestamp")); CHECK(params.contains("signature")); - // Prepare a valid encrypted link request message using the core's actual seed - std::vector outer_msg; + // Build a valid link request from a second device sharing the same account seed. + // The link request is encrypted with the account seed, so the receiving core can only + // decrypt it if both devices share that seed. + cleared_b32 seed_bytes; { - // Plaintext: {"I": , "i": {"n": "test", "p": , "k": }} - std::vector plaintext; - oxenc::bt_dict_producer p; - auto dev_id = - "0101010101010101010101010101010101010101010101010101010101010101"_hexbytes; - p.append("I", dev_id); - { - auto sub = p.append_dict("i"); - sub.append("k", test_keys.ed_pk1); - sub.append("n", "test device"); - sub.append("p", test_keys.curve_pk1); - } - auto p_span = p.span(); - plaintext.assign(p_span.begin(), p_span.end()); - - // Encrypt with the core's actual seed - auto core_seed = core.globals.account_seed(); - auto seed_span = session::to_span(core_seed.buf).first(32); - auto encrypted = config::encrypt(plaintext, seed_span, "link-request"); - - // Outer: {"": "L", "L": } - oxenc::bt_dict_producer outer; - outer.append("", "L"); - outer.append("L", encrypted); - auto outer_span = outer.span(); - outer_msg.assign(outer_span.begin(), outer_span.end()); + auto seed_acc = core.globals.account_seed(); + std::ranges::copy(seed_acc.buf.first<32>(), seed_bytes.begin()); } + TempCore linker{core::predefined_seed{std::span{seed_bytes}}}; + auto link_msg = linker->devices.build_link_request().message; + const auto* p = reinterpret_cast(link_msg.data()); + std::vector outer_msg{p, p + link_msg.size()}; // Prepare a mock response nlohmann::json response; @@ -123,7 +99,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { sent.callback(true, false, 200, {}, response.dump()); // Verify last_hash was updated in DB - CHECK(core.globals.get_text("_last_hash_21") == "hash1"); + CHECK(TestHelper::namespace_last_hash(core, 21) == "hash1"); CHECK(received); // Poll again, should include last_hash From de91f75797856ca1b9bf32a9a185e9f79210ba81 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 18:12:30 -0300 Subject: [PATCH 041/193] Seed word handling fixes and improvements - properly fail on invalid seed word sequences that overflow - simplify lookup code - use a custom exception for word-not-found so that the caller can deal with it specially (it includes a word() member to get the word in question). - fix the cmake watcher to watch the seed language files as well as the directory itself - More tests --- include/session/mnemonics.hpp | 36 +++++--- src/mnemonics/CMakeLists.txt | 6 +- src/mnemonics/mnemonics.cpp | 155 +++++++++++++-------------------- tests/test_mnemonics.cpp | 159 +++++++++++++++++++++++++++++++++- 4 files changed, 245 insertions(+), 111 deletions(-) diff --git a/include/session/mnemonics.hpp b/include/session/mnemonics.hpp index 559ff78d6..f25f6bea0 100644 --- a/include/session/mnemonics.hpp +++ b/include/session/mnemonics.hpp @@ -2,8 +2,9 @@ #include #include -#include #include +#include +#include #include #include @@ -12,14 +13,10 @@ namespace session::mnemonics { /** * The number of words in each mnemonic language word list. * - * This value (1626) is chosen because 1626^24 is just enough to represent a 256-bit (32-byte) - * random value. - * - * The math: - * log2(1626^24) = 24 * log2(1626) ≈ 24 * 10.6669... ≈ 256.006... bits. - * - * Thus, 24 words from a 1626-word dictionary can represent 2^256 states with a tiny amount of - * extra space that is simply unused. + * The encoding uses 3 words per 32-bit chunk, so 24 words encodes 256 bits. 1626 was chosen + * because 1626³ (≈ 4.299 × 10⁹) just barely exceeds 2³² (≈ 4.295 × 10⁹), meaning three words + * can represent any 32-bit value — with a small number of 3-word combinations (~0.09%) that + * exceed 2³²-1 and are therefore invalid. */ constexpr size_t NWORDS = 1626; @@ -38,6 +35,18 @@ struct Mnemonics { std::array words; }; +/// Exception thrown when a word is not found in the mnemonic dictionary. +class unknown_word_error : public std::invalid_argument { + public: + explicit unknown_word_error(std::string word); + + /// The word that was not found in the dictionary. + const std::string& word() const { return word_; } + + private: + std::string word_; +}; + /** * Returns a list of all supported mnemonic languages. * English is always the first element, followed by other languages sorted by name. @@ -80,8 +89,9 @@ std::vector bytes_to_words( * @param words The input word list. Its length must be a multiple of 3. * @param lang The language used for the mnemonic. * @return A vector of bytes representing the input mnemonic. - * @throws std::invalid_argument if the input length is not a multiple of 3, or if a word - * is not found in the language dictionary. + * @throws std::invalid_argument if the input length is not a multiple of 3, or if the word + * sequence encodes an invalid (overflowing) value. + * @throws unknown_word_error if a word is not found in the language dictionary. */ std::vector words_to_bytes( std::span words, const Mnemonics& lang); @@ -92,7 +102,9 @@ std::vector words_to_bytes( * @param words The input word list. Its length must be a multiple of 3. * @param lang_name The name of the language (English or native) used. * @return A vector of bytes representing the input mnemonic. - * @throws std::invalid_argument if the language is unknown or the input is invalid. + * @throws std::invalid_argument if the language is unknown, the input length is not a multiple + * of 3, or the word sequence encodes an invalid (overflowing) value. + * @throws unknown_word_error if a word is not found in the language dictionary. */ std::vector words_to_bytes( std::span words, std::string_view lang_name); diff --git a/src/mnemonics/CMakeLists.txt b/src/mnemonics/CMakeLists.txt index 15f8f1e34..681b1b594 100644 --- a/src/mnemonics/CMakeLists.txt +++ b/src/mnemonics/CMakeLists.txt @@ -1,9 +1,9 @@ -# Watch the languages directory so CMake re-runs if files are added/removed: -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "languages") - file(GLOB LANG_FILES "languages/*.txt") list(SORT LANG_FILES) +# Watch the languages directory (for added/removed files) and each individual file (for edits): +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "languages" ${LANG_FILES}) + # Reorder to put english first set(LANG_FILES_ORDERED "") set(ENGLISH_FILE "") diff --git a/src/mnemonics/mnemonics.cpp b/src/mnemonics/mnemonics.cpp index 4a343169a..52bdf47d9 100644 --- a/src/mnemonics/mnemonics.cpp +++ b/src/mnemonics/mnemonics.cpp @@ -1,10 +1,10 @@ #include -#include +#include #include -#include -#include #include +#include +#include #include #include #include @@ -12,6 +12,12 @@ namespace session::mnemonics { +using namespace oxen::log::literals; + +unknown_word_error::unknown_word_error(std::string word) : + std::invalid_argument{"Unknown mnemonic word: {}"_format(word)}, + word_{std::move(word)} {} + const Mnemonics* find_language(std::string_view name) { for (auto lang : get_languages()) { if (lang->english_name == name || lang->native_name == name) @@ -21,100 +27,58 @@ const Mnemonics* find_language(std::string_view name) { } namespace { - /** - * Converts ASCII characters in a UTF-8 string to lowercase. - * Non-ASCII characters are left unchanged. - */ - std::string to_lower_ascii(std::string_view s) { + // Returns the lowercased first `n_codepoints` UTF-8 codepoints of `s`. ASCII characters are + // lowercased; non-ASCII codepoints are copied as-is. + std::string word_prefix(std::string_view s, int n_codepoints) { std::string result; result.reserve(s.size()); - for (char c : s) { - if (static_cast(c) < 128) - result.push_back(static_cast(std::tolower(static_cast(c)))); - else - result.push_back(c); - } - return result; - } - - /** - * Gets a prefix of a UTF-8 string containing at most n_codepoints. - */ - std::string_view get_prefix(std::string_view s, int n_codepoints) { - if (n_codepoints <= 0) - return ""; - const char* it = s.data(); const char* end = it + s.size(); - int count = 0; - while (it < end && count < n_codepoints) { + for (int count = 0; it < end && count < n_codepoints; count++) { uint8_t b = static_cast(*it); - if (b < 0x80) // 0xxxxxxx: 1-byte ASCII - it += 1; - else if ((b & 0xE0) == 0xC0) // 110xxxxx: 2-byte sequence - it += 2; - else if ((b & 0xF0) == 0xE0) // 1110xxxx: 3-byte sequence - it += 3; - else if ((b & 0xF8) == 0xF0) // 11110xxx: 4-byte sequence - it += 4; - else // Invalid UTF-8 start byte or continuation byte - it += 1; - - count++; + int len; + if ((b & 0b11100000) == 0b11000000) // 110xxxxx: 2-byte sequence + len = 2; + else if ((b & 0b11110000) == 0b11100000) // 1110xxxx: 3-byte sequence + len = 3; + else if ((b & 0b11111000) == 0b11110000) // 11110xxx: 4-byte sequence + len = 4; + else // 0xxxxxxx: ASCII, or invalid byte + len = 1; + if (len == 1) { + result.push_back(static_cast(std::tolower(b))); + it++; + } else { + for (int k = 0; k < len && it < end; k++) + result.push_back(*it++); + } } - return std::string_view(s.data(), std::min(it - s.data(), s.size())); - } - - struct WordMap { - std::unordered_map map; - std::unordered_map prefix_map; - std::mutex mutex; - bool initialized = false; - }; - - WordMap& get_word_map(const Mnemonics& lang) { - static std::mutex maps_mutex; - static std::unordered_map> maps; - - std::lock_guard lock(maps_mutex); - auto& ptr = maps[&lang]; - if (!ptr) - ptr = std::make_unique(); - return *ptr; + return result; } - int get_word_index(const Mnemonics& lang, std::string_view word) { - auto& wm = get_word_map(lang); - { - std::lock_guard lock(wm.mutex); - if (!wm.initialized) { - for (int i = 0; i < static_cast(NWORDS); ++i) { - std::string_view w = lang.words[i]; - std::string lower_w = to_lower_ascii(w); - wm.map[lower_w] = i; - - std::string lower_prefix = to_lower_ascii(get_prefix(w, lang.prefix_len)); - if (!lower_prefix.empty()) - wm.prefix_map[lower_prefix] = i; - } - wm.initialized = true; - } - } + using WordMap = std::unordered_map; - std::string lower_input = to_lower_ascii(word); + const WordMap& get_word_map(const Mnemonics& lang) { + auto langs = get_languages(); + size_t idx = std::find(langs.begin(), langs.end(), &lang) - langs.begin(); - auto it = wm.map.find(lower_input); - if (it != wm.map.end()) - return it->second; + static std::vector maps(langs.size()); + static std::vector flags(langs.size()); - std::string lower_input_prefix = to_lower_ascii(get_prefix(lower_input, lang.prefix_len)); - if (!lower_input_prefix.empty()) { - auto pit = wm.prefix_map.find(lower_input_prefix); - if (pit != wm.prefix_map.end()) - return pit->second; - } + std::call_once(flags[idx], [&] { + for (int i = 0; i < static_cast(NWORDS); ++i) { + std::string prefix = word_prefix(lang.words[i], lang.prefix_len); + assert(!prefix.empty()); + maps[idx][prefix] = i; + } + }); + return maps[idx]; + } - return -1; + int get_word_index(const Mnemonics& lang, std::string_view word) { + const auto& wm = get_word_map(lang); + auto it = wm.find(word_prefix(word, lang.prefix_len)); + return it != wm.end() ? it->second : -1; } } // namespace @@ -158,20 +122,21 @@ std::vector words_to_bytes( result.resize((words.size() / 3) * 4); for (size_t i = 0; i < words.size(); i += 3) { - int w1 = get_word_index(lang, words[i]); - int w2 = get_word_index(lang, words[i + 1]); - int w3 = get_word_index(lang, words[i + 2]); - - if (w1 < 0 || w2 < 0 || w3 < 0) - throw std::invalid_argument("Word not found in mnemonic dictionary"); - - uint32_t a = static_cast(w1); - uint32_t b = static_cast(w2); - uint32_t c = static_cast(w3); + std::array w; + for (int j = 0; j < 3; j++) { + int idx = get_word_index(lang, words[i + j]); + if (idx < 0) + throw unknown_word_error{std::string(words[i + j])}; + w[j] = static_cast(idx); + } + auto [a, b, c] = w; uint32_t x = a + ((NWORDS - a + b) % NWORDS) * NWORDS + ((NWORDS - b + c) % NWORDS) * (NWORDS * NWORDS); + if (x % NWORDS != a) + throw std::invalid_argument("Mnemonic word sequence encodes an invalid value"); + oxenc::write_host_as_little(x, &result[(i / 3) * 4]); } diff --git a/tests/test_mnemonics.cpp b/tests/test_mnemonics.cpp index 57b2088e8..fc2b41094 100644 --- a/tests/test_mnemonics.cpp +++ b/tests/test_mnemonics.cpp @@ -1,10 +1,153 @@ #include #include #include +#include #include +#include "utils.hpp" + using namespace session::mnemonics; +// Test vectors: SHA-512("libsession-util mnemonic test vector") encoded as 48 words per language. +// These pin the exact word list contents and ordering; if any word list changes this test fails. +TEST_CASE("Mnemonic word list test vectors", "[mnemonics]") { + // seed = SHA-512("libsession-util mnemonic test vector") + auto seed = "0dd5d9bc3d68c25a396f4aacd922a4d620a19cf3c9054cb825dd8a2c5420f4f3" + "ca314c582ffef5388df36e2546cc9103dd1776a634f676e1e631289b8d280b2e"_hex_b; + + // clang-format off + const std::pair> expected[] = { + {"English", { + "threaten", "efficient", "wives", "skirting", "repent", "ashtray", + "rural", "ammo", "reunion", "yoyo", "already", "tucks", + "attire", "waxing", "uphill", "template", "ghetto", "anxiety", + "utensils", "newt", "safety", "paper", "quote", "pebbles", + "album", "gnaw", "puppy", "tidy", "foxes", "menu", + "evenings", "spying", "wallets", "plotting", "fuselage", "geometry", + "toilet", "cylinder", "swagger", "eels", "when", "tether", + "cowl", "saga", "gossip", "vats", "bias", "federal", + }}, + {"Chinese (simplified)", { + "忆", "众", "瓷", "坡", "残", "合", "麻", "度", "综", "淀", "得", "炭", + "四", "弃", "隆", "违", "亚", "物", "博", "娘", "缓", "薄", "纤", "暗", + "方", "减", "爷", "浆", "官", "乐", "称", "阀", "蜡", "予", "谈", "落", + "罚", "志", "蓝", "音", "浅", "森", "百", "净", "波", "灌", "无", "格", + }}, + {"Dutch", { + "tray", "erna", "zetbaas", "spijgat", "rits", "bedwelmd", + "salade", "arubaan", "rodijk", "zottebol", "aorta", "vanmiddag", + "belboei", "worp", "voip", "tosti", "glaasje", "auping", + "waas", "neuzelaar", "saus", "pacht", "ramselaar", "pauze", + "amnestie", "goeierd", "puzzelaar", "treur", "gegraaid", "mantel", + "feilbaar", "tabak", "witmaker", "plausibel", "gemiddeld", "giepmans", + "tyfoon", "derf", "ticket", "ermitage", "zalig", "trabant", + "danenberg", "scampi", "groosman", "warklomp", "bolvormig", "formule", + }}, + {"Esperanto", { + "sizifa", "ebena", "viskoza", "rapida", "optimisto", "anjono", + "pezoforto", "alfabeto", "orfino", "zeto", "akselo", "stomako", + "aplikado", "vazaro", "timida", "sidejo", "fermi", "alzaca", + "tosti", "latitudo", "pilkoludo", "moskito", "ofsajdo", "muro", + "akademio", "fimensa", "oblikva", "sklavo", "eskapi", "kisi", + "elektro", "rojo", "vampiro", "neulo", "etullernejo", "feino", + "sodakvo", "cigaredo", "safario", "duzo", "veziko", "simpla", + "cemento", "pimento", "flirti", "tunelo", "babili", "enciklopedio", + }}, + {"French", { + "skier", "devoir", "vingt", "rideau", "profond", "aucun", + "rail", "angoisse", "propre", "vous", "amener", "star", + "avant", "vampire", "tenter", "sigle", "fixe", "ardeur", + "toge", "navrer", "rang", "parmi", "pompier", "pause", + "album", "focus", "poids", "socle", "faible", "miel", + "eaux", "rustre", "vague", "pilote", "faveur", "final", + "songeur", "chiot", "sauge", "devin", "version", "sinon", + "chasse", "rapace", "fosse", "tour", "billet", "enlever", + }}, + {"German", { + "Salz", "Dezibel", "Wind", "plündern", "Mund", "Anker", + "Oberarzt", "Alter", "Nabel", "Zielfoto", "Almosen", "Skikurs", + "Anrecht", "Wahlen", "Tempo", "Rüstung", "Espe", "Amulett", + "Topmodel", "Kampagne", "Ofenholz", "Lavasee", "Milchkuh", "Lerche", + "Aktfoto", "Exil", "melden", "Sanftmut", "Erde", "Hufeisen", + "Edelweiß", "Rapsöl", "Vorrat", "Luxus", "erkalten", "Erzeuger", + "Schulbus", "Bogen", "Respekt", "Detektiv", "wechseln", "Sack", + "Blauwal", "öffnen", "Fakultät", "Trödel", "Bach", "Einzug", + }}, + {"Italian", { + "spegnere", "comune", "vigilare", "sartoria", "pulire", "arachidi", + "retorica", "amnistia", "quaderno", "zainetto", "amante", "subire", + "armonia", "velluto", "tirare", "sospiro", "enigma", "anello", + "trattore", "moglie", "ricambio", "panino", "porzione", "parodia", + "allarme", "esaltare", "polimero", "spezzare", "dorso", "madama", + "cupola", "seme", "vegetale", "pianeta", "eclissi", "emisfero", + "stadio", "cannone", "silicone", "compagna", "vertebra", "spalla", + "calzone", "ricetta", "estrarre", "tulipano", "bagaglio", "dialogo", + }}, + {"Japanese", { + "なさけ", "きかく", "はらう", "でこぼこ", "たんとう", "いとこ", + "ちゃんこなべ", "いさましい", "たんぴん", "ひかく", "いきもの", "にっさん", + "いふく", "はせる", "ねっしん", "どんぶり", "けちゃっぷ", "いそがしい", + "ねんかん", "せいよう", "ちらみ", "そめる", "たぼう", "そんぞく", + "あんてい", "けとばす", "だったい", "ななおし", "くめる", "しょっけん", + "きまる", "てんぷら", "はこぶ", "たいめん", "けいけん", "けしき", + "なれる", "おじさん", "とくしゅう", "きかい", "はったつ", "ないそう", + "おくる", "ちりがみ", "けみかる", "のがす", "うせつ", "くうき", + }}, + {"Lojban", { + "vasxu", "ferti", "rarbau", "tadji", "sisku", "cando", + "sobde", "bloti", "skami", "faumlu", "birka", "xabju", + "carna", "jbogu'e", "xruki", "tutci", "jicmu", "briju", + "zbabu", "panje", "sombo", "ransu", "senpi", "rekto", + "bifce", "jinku", "savru", "vensa", "jarco", "murta", + "gapru", "temse", "jbocre", "rupnu", "jdini", "jgira", + "viska", "dansu", "toldi", "fepri", "reisku", "vamji", + "dacti", "sonci", "jivbu", "zifre", "cinza", "grake", + }}, + {"Portuguese", { + "sonso", "druso", "vontade", "riacho", "paxa", "arlequim", + "porvir", "alvura", "pegaso", "xodo", "alhures", "tavola", + "ascorbico", "vetusto", "trovoar", "slide", "feto", "anotar", + "ufologo", "mausoleu", "prezar", "nouveau", "otite", "nutritivo", + "ajudante", "fiorde", "orla", "sossego", "exaustor", "lele", + "emulsao", "rural", "veja", "ojeriza", "faixas", "feltro", + "suor", "cluster", "seara", "dropes", "viquingue", "soerguer", + "cinzento", "privilegios", "foco", "unheiro", "bemol", "ereto", + }}, + {"Russian", { + "уровень", "древний", "эмблема", "тайна", "сельский", "бегство", + "согласие", "арсенал", "сечение", "язык", "аптека", "фишка", + "бивень", "шрам", "центр", "умолять", "исходить", "атрибут", + "чепуха", "отбор", "сонный", "пуля", "рюкзак", "пшеница", + "анкета", "капитан", "рыба", "ускорять", "иголка", "область", + "женщина", "трибуна", "шорох", "ресурс", "изоляция", "ипподром", + "ушко", "гамма", "тянуть", "драка", "щель", "уплата", + "выходить", "сообщать", "кенгуру", "чужой", "быстрый", "зачет", + }}, + {"Spanish", { + "pasta", "chiste", "relieve", "obtener", "mito", "anillo", + "músculo", "aleta", "moho", "riego", "alambre", "pésimo", + "añejo", "reacción", "pompa", "parcela", "diente", "altura", + "previo", "intuir", "nación", "llanto", "mensaje", "loción", + "aguja", "divino", "mecha", "pausa", "curva", "héroe", + "collar", "óptica", "rasgo", "mamut", "dejar", "diamante", + "pellejo", "brote", "otoño", "chico", "reflejo", "párrafo", + "bozal", "nadar", "droga", "pronto", "astro", "crear", + }}, + }; + // clang-format on + + for (auto& [lang_name, exp_words] : expected) { + SECTION(std::string(lang_name)) { + auto* lang = find_language(lang_name); + REQUIRE(lang); + auto words = bytes_to_words(seed, *lang); + REQUIRE(words.size() == 48); + for (size_t i = 0; i < 48; i++) + CHECK(words[i] == exp_words[i]); + } + } +} + TEST_CASE("Mnemonic round-trip tests", "[mnemonics]") { std::vector data_128(16); std::vector data_256(32); @@ -123,7 +266,21 @@ TEST_CASE("Mnemonic error handling", "[mnemonics]") { } SECTION("Unknown word") { - std::vector words = {"abbey", "abducts", "zzzzzz"}; + // Use mixed case to verify word() returns the original input, not a lowercased prefix. + // "ZZZ..." has prefix "zzz" which is not in the English word list. + std::vector words = {"abbey", "abducts", "ZZZunknown"}; + CHECK_THROWS_AS(words_to_bytes(words, *english), unknown_word_error); + try { + words_to_bytes(words, *english); + } catch (const unknown_word_error& e) { + CHECK(e.word() == "ZZZunknown"); + } + } + + SECTION("Overflow word triplet") { + // a=0 (abbey), b=0 (abbey), c=1625 (zoom): + // 0 + 0 + 1625*1626² = 4,296,298,500 > UINT32_MAX — must be rejected + std::vector words = {"abbey", "abbey", "zoom"}; CHECK_THROWS_AS(words_to_bytes(words, *english), std::invalid_argument); } } From fd980f88673e9b2a3e4f23694b4391b912bb413a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 19:04:40 -0300 Subject: [PATCH 042/193] Add checksum support to seed words --- include/session/mnemonics.hpp | 38 +++++++++++++++------ src/mnemonics/mnemonics.cpp | 40 +++++++++++++++++----- tests/test_mnemonics.cpp | 63 ++++++++++++++++++++++++++++++++--- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/include/session/mnemonics.hpp b/include/session/mnemonics.hpp index f25f6bea0..4f143400a 100644 --- a/include/session/mnemonics.hpp +++ b/include/session/mnemonics.hpp @@ -47,6 +47,12 @@ class unknown_word_error : public std::invalid_argument { std::string word_; }; +/// Exception thrown when a checksum word is present but does not match the expected value. +class checksum_error : public std::invalid_argument { + public: + checksum_error(); +}; + /** * Returns a list of all supported mnemonic languages. * English is always the first element, followed by other languages sorted by name. @@ -66,32 +72,40 @@ const Mnemonics* find_language(std::string_view name); * * @param bytes The input byte span. Its length must be a multiple of 4. * @param lang The language to use for the mnemonic. - * @return A vector of words representing the input bytes. + * @param checksum If true (the default), append a checksum word after the encoded words. The + * checksum word is the seed word at position (sum of all word indices) % N, where N is + * the number of encoded words. + * @return A vector of words representing the input bytes, plus a checksum word if requested. * @throws std::invalid_argument if the input length is not a multiple of 4. */ std::vector bytes_to_words( - std::span bytes, const Mnemonics& lang); + std::span bytes, const Mnemonics& lang, bool checksum = true); /** * Converts a byte span to a mnemonic word list using the specified language. * * @param bytes The input byte span. Its length must be a multiple of 4. * @param lang_name The name of the language (English or native) to use. - * @return A vector of words representing the input bytes. + * @param checksum If true (the default), append a checksum word after the encoded words. + * @return A vector of words representing the input bytes, plus a checksum word if requested. * @throws std::invalid_argument if the language is unknown or the input length is invalid. */ std::vector bytes_to_words( - std::span bytes, std::string_view lang_name); + std::span bytes, std::string_view lang_name, bool checksum = true); /** * Converts a mnemonic word list to a byte span using the specified language. * - * @param words The input word list. Its length must be a multiple of 3. + * Accepts a word count that is either a multiple of 3 (no checksum) or one more than a multiple + * of 3 (with checksum). If a checksum word is present it is validated. + * + * @param words The input word list. * @param lang The language used for the mnemonic. * @return A vector of bytes representing the input mnemonic. - * @throws std::invalid_argument if the input length is not a multiple of 3, or if the word - * sequence encodes an invalid (overflowing) value. + * @throws std::invalid_argument if the input length is invalid, or if the word sequence encodes + * an invalid (overflowing) value. * @throws unknown_word_error if a word is not found in the language dictionary. + * @throws checksum_error if a checksum word is present but does not match. */ std::vector words_to_bytes( std::span words, const Mnemonics& lang); @@ -99,12 +113,16 @@ std::vector words_to_bytes( /** * Converts a mnemonic word list to a byte span using the specified language. * - * @param words The input word list. Its length must be a multiple of 3. + * Accepts a word count that is either a multiple of 3 (no checksum) or one more than a multiple + * of 3 (with checksum). If a checksum word is present it is validated. + * + * @param words The input word list. * @param lang_name The name of the language (English or native) used. * @return A vector of bytes representing the input mnemonic. - * @throws std::invalid_argument if the language is unknown, the input length is not a multiple - * of 3, or the word sequence encodes an invalid (overflowing) value. + * @throws std::invalid_argument if the language is unknown, the input length is invalid, or the + * word sequence encodes an invalid (overflowing) value. * @throws unknown_word_error if a word is not found in the language dictionary. + * @throws checksum_error if a checksum word is present but does not match. */ std::vector words_to_bytes( std::span words, std::string_view lang_name); diff --git a/src/mnemonics/mnemonics.cpp b/src/mnemonics/mnemonics.cpp index 52bdf47d9..a13836056 100644 --- a/src/mnemonics/mnemonics.cpp +++ b/src/mnemonics/mnemonics.cpp @@ -18,6 +18,9 @@ unknown_word_error::unknown_word_error(std::string word) : std::invalid_argument{"Unknown mnemonic word: {}"_format(word)}, word_{std::move(word)} {} +checksum_error::checksum_error() : + std::invalid_argument{"Seed phrase checksum word does not match"} {} + const Mnemonics* find_language(std::string_view name) { for (auto lang : get_languages()) { if (lang->english_name == name || lang->native_name == name) @@ -83,13 +86,15 @@ namespace { } // namespace std::vector bytes_to_words( - std::span bytes, const Mnemonics& lang) { + std::span bytes, const Mnemonics& lang, bool checksum) { if (bytes.size() % 4 != 0) throw std::invalid_argument("Input length must be a multiple of 4 bytes"); + size_t n = (bytes.size() / 4) * 3; std::vector result; - result.reserve((bytes.size() / 4) * 3); + result.reserve(n + checksum); + uint32_t sum = 0; for (size_t i = 0; i < bytes.size(); i += 4) { uint32_t val = oxenc::load_little_to_host(&bytes[i]); @@ -100,28 +105,37 @@ std::vector bytes_to_words( result.push_back(lang.words[a]); result.push_back(lang.words[b]); result.push_back(lang.words[c]); + sum += a + b + c; } + if (checksum) + result.push_back(result[sum % n]); + return result; } std::vector bytes_to_words( - std::span bytes, std::string_view lang_name) { + std::span bytes, std::string_view lang_name, bool checksum) { auto lang = find_language(lang_name); if (!lang) throw std::invalid_argument("Unknown mnemonic language: " + std::string(lang_name)); - return bytes_to_words(bytes, *lang); + return bytes_to_words(bytes, *lang, checksum); } std::vector words_to_bytes( std::span words, const Mnemonics& lang) { - if (words.size() % 3 != 0) - throw std::invalid_argument("Input word count must be a multiple of 3"); + size_t n = words.size(); + bool has_checksum = n % 3 == 1; + if (!has_checksum && n % 3 != 0) + throw std::invalid_argument( + "Input word count must be a multiple of 3 (+1 with a checksum)"); + size_t seed_words = n - has_checksum; std::vector result; - result.resize((words.size() / 3) * 4); + result.resize((seed_words / 3) * 4); - for (size_t i = 0; i < words.size(); i += 3) { + uint32_t sum = 0; + for (size_t i = 0; i < seed_words; i += 3) { std::array w; for (int j = 0; j < 3; j++) { int idx = get_word_index(lang, words[i + j]); @@ -138,6 +152,16 @@ std::vector words_to_bytes( throw std::invalid_argument("Mnemonic word sequence encodes an invalid value"); oxenc::write_host_as_little(x, &result[(i / 3) * 4]); + sum += a + b + c; + } + + if (has_checksum) { + int checksum_idx = get_word_index(lang, words[n - 1]); + if (checksum_idx < 0) + throw unknown_word_error{std::string(words[n - 1])}; + int expected_idx = get_word_index(lang, words[sum % seed_words]); + if (checksum_idx != expected_idx) + throw checksum_error{}; } return result; diff --git a/tests/test_mnemonics.cpp b/tests/test_mnemonics.cpp index fc2b41094..2eb0a5b1e 100644 --- a/tests/test_mnemonics.cpp +++ b/tests/test_mnemonics.cpp @@ -140,7 +140,7 @@ TEST_CASE("Mnemonic word list test vectors", "[mnemonics]") { SECTION(std::string(lang_name)) { auto* lang = find_language(lang_name); REQUIRE(lang); - auto words = bytes_to_words(seed, *lang); + auto words = bytes_to_words(seed, *lang, false); REQUIRE(words.size() == 48); for (size_t i = 0; i < 48; i++) CHECK(words[i] == exp_words[i]); @@ -166,16 +166,28 @@ TEST_CASE("Mnemonic round-trip tests", "[mnemonics]") { for (auto lang : get_languages()) { SECTION("Language: " + std::string(lang->english_name)) { // 128-bit -> 12 words -> 128-bit - auto words12 = bytes_to_words(data_128, *lang); + auto words12 = bytes_to_words(data_128, *lang, false); CHECK(words12.size() == 12); auto back12 = words_to_bytes(words12, *lang); CHECK(back12 == data_128); + // 128-bit -> 13 words (with checksum) -> 128-bit + auto words13 = bytes_to_words(data_128, *lang); + CHECK(words13.size() == 13); + auto back13 = words_to_bytes(words13, *lang); + CHECK(back13 == data_128); + // 256-bit -> 24 words -> 256-bit - auto words24 = bytes_to_words(data_256, *lang); + auto words24 = bytes_to_words(data_256, *lang, false); CHECK(words24.size() == 24); auto back24 = words_to_bytes(words24, *lang); CHECK(back24 == data_256); + + // 256-bit -> 25 words (with checksum) -> 256-bit + auto words25 = bytes_to_words(data_256, *lang); + CHECK(words25.size() == 25); + auto back25 = words_to_bytes(words25, *lang); + CHECK(back25 == data_256); } } } @@ -193,7 +205,7 @@ TEST_CASE("Mnemonic case-insensitivity and prefix matching", "[mnemonics]") { // Words for English at indices 1443, 180, 205 std::vector data = { std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; - auto words = bytes_to_words(data, *english); + auto words = bytes_to_words(data, *english, false); REQUIRE(words.size() == 3); SECTION("Exact match") { @@ -252,6 +264,49 @@ TEST_CASE("Mnemonic language lookup", "[mnemonics]") { CHECK(find_language("NonExistent") == nullptr); } +TEST_CASE("Mnemonic checksum", "[mnemonics]") { + auto english = find_language("English"); + REQUIRE(english); + + std::vector data = { + std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + auto words3 = bytes_to_words(data, *english, false); + REQUIRE(words3.size() == 3); + auto words4 = bytes_to_words(data, *english); + REQUIRE(words4.size() == 4); + + SECTION("Checksum word is correct position duplicate") { + // sum of indices % 3 gives the position whose word is duplicated + int i0 = 0, i1 = 0, i2 = 0; + for (size_t i = 0; i < 3; i++) { + int idx = 0; + for (size_t j = 0; j < english->words.size(); j++) + if (english->words[j] == words3[i]) { idx = j; break; } + if (i == 0) i0 = idx; + else if (i == 1) i1 = idx; + else i2 = idx; + } + size_t expected_pos = (i0 + i1 + i2) % 3; + CHECK(words4[3] == words3[expected_pos]); + } + + SECTION("Checksum round-trip") { + auto back = words_to_bytes(words4, *english); + CHECK(back == data); + } + + SECTION("Bad checksum throws checksum_error") { + // Replace the checksum word with a different valid word + std::vector bad = {words4[0], words4[1], words4[2], words4[2] == words4[0] ? words4[1] : words4[0]}; + CHECK_THROWS_AS(words_to_bytes(bad, *english), checksum_error); + } + + SECTION("Unknown checksum word throws unknown_word_error") { + std::vector bad = {words4[0], words4[1], words4[2], "ZZZunknown"}; + CHECK_THROWS_AS(words_to_bytes(bad, *english), unknown_word_error); + } +} + TEST_CASE("Mnemonic error handling", "[mnemonics]") { auto english = find_language("English"); From 8e28520ccda55bd91b46b080aef0883c0633ea31 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 22:57:39 -0300 Subject: [PATCH 043/193] Mnemonics secure mem & integration with predefined_seed - use secure storage for seed<->byte conversions to ensure we don't leave anything around in memory - add a "secure_mnemonic" that wraps a secure buffer of string_views with accessor functions for the string_views containing the seed words. - Update predefined_seed to accept 12/13/24/25 length seed words (the 13/25 are with a checksum word). - 12/13 word seeds are deliberately compatible with the current 12/13-word Session seeds. - 24/25 word seeds are for optional full word (256-bit entropy) seeds that we may want to support at some point. - Obtaining the seed words checks to see if the seed is actually a 128-bit or 256-bit value and returns the 13 or 25-word seed based on what it finds (so that 13 word seeds will stay 13 words). --- external/session-sqlite | 2 +- include/session/core.hpp | 14 ++++ include/session/core/globals.hpp | 15 +++- include/session/mnemonics.hpp | 114 ++++++++++++++++++++++++------- src/CMakeLists.txt | 1 + src/core.cpp | 29 ++++++++ src/core/globals.cpp | 16 +++++ src/mnemonics/mnemonics.cpp | 114 +++++++++++++++++++++---------- tests/test_mnemonics.cpp | 73 ++++++++++++-------- 9 files changed, 285 insertions(+), 93 deletions(-) diff --git a/external/session-sqlite b/external/session-sqlite index f96ae2041..8ea436bbb 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit f96ae20411e75eed45a12abbfaed469ac456b011 +Subproject commit 8ea436bbb79deaf7fdcfed445ccd6e4b6cb78c40 diff --git a/include/session/core.hpp b/include/session/core.hpp index fba036c3e..4eefe4c5c 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -148,6 +149,19 @@ struct predefined_seed { explicit predefined_seed(std::span s) { std::ranges::copy(std::as_bytes(s), bytes.begin()); } + + /// Constructs a predefined_seed from a mnemonic word list. + /// + /// Accepts 12 or 13 words (128-bit seed; the upper 16 bytes are set to zero), or 24 or 25 + /// words (256-bit seed). 13- and 25-word inputs include a checksum word which is validated. + /// + /// @throws std::invalid_argument if the word count is not 12, 13, 24, or 25. + /// @throws mnemonics::unknown_word_error if a word is not found in the language dictionary. + /// @throws mnemonics::checksum_error if the checksum word (if present) does not match. + explicit predefined_seed( + std::span words, const mnemonics::Mnemonics& lang); + explicit predefined_seed( + std::span words, std::string_view lang_name = "English"); }; /// Concept satisfied by any type usable as a Core constructor option: a sqlite database option diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 70c028c88..804f4e2d0 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -36,8 +38,7 @@ class Globals final : detail::CoreComponent { // account seed is generated during initialization if the database doesn't contain one (e.g. if // brand new). // - // Read-only access is available via the account_seed() method, or via the `seed()` - // CoreComponent base class method from other components. + // Read-only access is available via the account_seed() method. session::secure_buffer _account_seed; network::ed25519_pubkey _pubkey_ed25519; network::x25519_pubkey _pubkey_x25519; @@ -83,6 +84,16 @@ class Globals final : detail::CoreComponent { std::span session_id() { return _session_id; } const network::ed25519_pubkey& pubkey_ed25519() const { return _pubkey_ed25519; } const network::x25519_pubkey& pubkey_x25519() const { return _pubkey_x25519; } + + /// Returns the account seed as a mnemonic word list with checksum, stored in secure memory. + /// + /// If `force_24` is false (the default), returns 13 words when the upper 16 bytes of the + /// seed are all zero (128-bit entropy), or 25 words otherwise. If `force_24` is true, + /// always returns 25 words. + mnemonics::secure_mnemonic seed_mnemonic( + const mnemonics::Mnemonics& lang, bool force_24 = false); + mnemonics::secure_mnemonic seed_mnemonic( + std::string_view lang_name = "English", bool force_24 = false); }; } // namespace session::core diff --git a/include/session/mnemonics.hpp b/include/session/mnemonics.hpp index 4f143400a..3149ec9f9 100644 --- a/include/session/mnemonics.hpp +++ b/include/session/mnemonics.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -53,6 +54,18 @@ class checksum_error : public std::invalid_argument { checksum_error(); }; +/// Exception thrown when a language name is not found in the language registry. +class unknown_language_error : public std::invalid_argument { + public: + explicit unknown_language_error(std::string name); + + /// The language name that was not found. + const std::string& name() const { return name_; } + + private: + std::string name_; +}; + /** * Returns a list of all supported mnemonic languages. * English is always the first element, followed by other languages sorted by name. @@ -68,63 +81,114 @@ std::span get_languages(); const Mnemonics* find_language(std::string_view name); /** - * Converts a byte span to a mnemonic word list using the specified language. + * Looks up a language by its English or native name, throwing if not found. + * + * @param name The name to look for. + * @return A reference to the Mnemonics struct. + * @throws unknown_language_error if the language name is not found. + */ +const Mnemonics& get_language(std::string_view name); + +/// Stores mnemonic string_view objects (each pointing into the language word list) in secure +/// (sodium) memory so that the word identities are zeroed on destruction. +/// +/// Call open() to iterate over the words. The returned opened_span holds a read accessor +/// that keeps the buffer readable for its own lifetime, so the following are both safe: +/// +/// for (auto w : m.open()) { ... } +/// auto s = m.open(); for (auto w : s.words) { ... } +/// +/// Do NOT do: `for (auto w : m.open().words)` — the opened_span (and its accessor) would be +/// destroyed before the loop body runs, re-locking the buffer and causing a crash. +struct secure_mnemonic { + session::secure_buffer storage; + + struct opened_span { + session::secure_buffer::r_accessor acc; + std::span words; + + const std::string_view& operator[](size_t i) const { return words[i]; } + const std::string_view* begin() const { return words.data(); } + const std::string_view* end() const { return words.data() + words.size(); } + }; + + opened_span open() { + auto acc = storage.access(); + std::span words{ + reinterpret_cast(acc.buf.data()), + acc.buf.size() / sizeof(std::string_view)}; + return {std::move(acc), words}; + } + + size_t size() const { return storage.size() / sizeof(std::string_view); } +}; + +/** + * Converts a byte span to a mnemonic word list using the specified language, stored in secure + * memory. * * @param bytes The input byte span. Its length must be a multiple of 4. * @param lang The language to use for the mnemonic. * @param checksum If true (the default), append a checksum word after the encoded words. The * checksum word is the seed word at position (sum of all word indices) % N, where N is * the number of encoded words. - * @return A vector of words representing the input bytes, plus a checksum word if requested. + * @return A secure_mnemonic containing the words, plus a checksum word if requested. * @throws std::invalid_argument if the input length is not a multiple of 4. */ -std::vector bytes_to_words( +secure_mnemonic bytes_to_words( std::span bytes, const Mnemonics& lang, bool checksum = true); -/** - * Converts a byte span to a mnemonic word list using the specified language. - * - * @param bytes The input byte span. Its length must be a multiple of 4. - * @param lang_name The name of the language (English or native) to use. - * @param checksum If true (the default), append a checksum word after the encoded words. - * @return A vector of words representing the input bytes, plus a checksum word if requested. - * @throws std::invalid_argument if the language is unknown or the input length is invalid. - */ -std::vector bytes_to_words( +/// Same as above, but takes a language by name instead of by reference. +/// @throws unknown_language_error if the language name is not found. +secure_mnemonic bytes_to_words( std::span bytes, std::string_view lang_name, bool checksum = true); /** - * Converts a mnemonic word list to a byte span using the specified language. + * Converts a mnemonic word list to bytes using the specified language, stored in secure memory. * * Accepts a word count that is either a multiple of 3 (no checksum) or one more than a multiple * of 3 (with checksum). If a checksum word is present it is validated. * * @param words The input word list. * @param lang The language used for the mnemonic. - * @return A vector of bytes representing the input mnemonic. + * @return A secure_buffer containing the decoded bytes. * @throws std::invalid_argument if the input length is invalid, or if the word sequence encodes * an invalid (overflowing) value. * @throws unknown_word_error if a word is not found in the language dictionary. * @throws checksum_error if a checksum word is present but does not match. */ -std::vector words_to_bytes( +session::secure_buffer words_to_bytes( std::span words, const Mnemonics& lang); +/// Same as above, but takes a language by name instead of by reference. +/// @throws unknown_language_error if the language name is not found. +session::secure_buffer words_to_bytes( + std::span words, std::string_view lang_name); + /** - * Converts a mnemonic word list to a byte span using the specified language. + * Converts a mnemonic word list to bytes, writing directly into a caller-provided output span. * - * Accepts a word count that is either a multiple of 3 (no checksum) or one more than a multiple - * of 3 (with checksum). If a checksum word is present it is validated. + * The size of `out` determines the expected number of seed words: out.size() must be a multiple + * of 4, and words.size() must equal (out.size() / 4 * 3) or (out.size() / 4 * 3) + 1 (the + * latter if a checksum word is appended). * * @param words The input word list. - * @param lang_name The name of the language (English or native) used. - * @return A vector of bytes representing the input mnemonic. - * @throws std::invalid_argument if the language is unknown, the input length is invalid, or the - * word sequence encodes an invalid (overflowing) value. + * @param lang The language used for the mnemonic. + * @param out Output span to write decoded bytes into; must be a multiple-of-4 size exactly + * matching the decoded byte count implied by the word count. + * @throws std::invalid_argument if the word count does not match the output size, the word + * sequence encodes an invalid (overflowing) value, or out.size() is not a multiple of 4. * @throws unknown_word_error if a word is not found in the language dictionary. * @throws checksum_error if a checksum word is present but does not match. */ -std::vector words_to_bytes( - std::span words, std::string_view lang_name); +void words_to_bytes( + std::span words, const Mnemonics& lang, std::span out); + +/// Same as above, but takes a language by name instead of by reference. +/// @throws unknown_language_error if the language name is not found. +void words_to_bytes( + std::span words, + std::string_view lang_name, + std::span out); } // namespace session::mnemonics diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b38560638..a607deaed 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -94,6 +94,7 @@ target_link_libraries(util target_link_libraries(crypto PUBLIC util + session::secure_buffer PRIVATE sessiondep::libsodium nlohmann_json::nlohmann_json diff --git a/src/core.cpp b/src/core.cpp index b198ac901..396761f5e 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -23,8 +24,36 @@ namespace session::core { namespace log = oxen::log; using namespace session::sqlite; +using namespace oxen::log::literals; static auto cat = log::Cat("core"); +static cleared_b32 seed_from_words( + std::span words, const mnemonics::Mnemonics& lang) { + auto n = words.size(); + if (n != 12 && n != 13 && n != 24 && n != 25) + throw std::invalid_argument{ + "Seed phrase must be 12, 13, 24, or 25 words (got {})"_format(n)}; + + cleared_b32 result; + if (n <= 13) { + // 12 or 13 words → 16-byte seed in the lower half; upper 16 bytes are zeroed + mnemonics::words_to_bytes(words, lang, std::span(result.data(), 16)); + std::memset(result.data() + 16, 0, 16); + } else { + // 24 or 25 words → full 32-byte seed + mnemonics::words_to_bytes(words, lang, std::span(result.data(), 32)); + } + return result; +} + +predefined_seed::predefined_seed( + std::span words, const mnemonics::Mnemonics& lang) : + predefined_seed{seed_from_words(words, lang)} {} + +predefined_seed::predefined_seed( + std::span words, std::string_view lang_name) : + predefined_seed{words, mnemonics::get_language(lang_name)} {} + void Core::LoopDeleter::operator()(quic::Loop* p) const { delete p; } diff --git a/src/core/globals.cpp b/src/core/globals.cpp index 4e6b78f86..6edcc4712 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -158,4 +159,19 @@ void Globals::init() { log::info(cat, "Initialized with Session ID: {}", oxenc::to_hex(_session_id)); } +mnemonics::secure_mnemonic Globals::seed_mnemonic(const mnemonics::Mnemonics& lang, bool force_24) { + auto seed = _account_seed.access(); + // _account_seed stores the 64-byte Ed25519 secret key, of which the first 32 bytes are the + // account seed. A Session account uses 128-bit entropy when the upper 16 bytes of that seed + // are all zero; in that case we encode only the lower 16 bytes. + auto seed32 = seed.buf.first(32); + bool is_128bit = !force_24 && + sodium_memcmp(seed32.data() + 16, std::array{}.data(), 16) == 0; + return mnemonics::bytes_to_words(is_128bit ? seed32.first(16) : seed32, lang); +} + +mnemonics::secure_mnemonic Globals::seed_mnemonic(std::string_view lang_name, bool force_24) { + return seed_mnemonic(mnemonics::get_language(lang_name), force_24); +} + } // namespace session::core diff --git a/src/mnemonics/mnemonics.cpp b/src/mnemonics/mnemonics.cpp index a13836056..9ae7c0a55 100644 --- a/src/mnemonics/mnemonics.cpp +++ b/src/mnemonics/mnemonics.cpp @@ -2,25 +2,29 @@ #include #include +#include #include -#include #include #include #include #include #include +#include namespace session::mnemonics { using namespace oxen::log::literals; unknown_word_error::unknown_word_error(std::string word) : - std::invalid_argument{"Unknown mnemonic word: {}"_format(word)}, - word_{std::move(word)} {} + std::invalid_argument{"Unknown mnemonic word: {}"_format(word)}, word_{std::move(word)} {} checksum_error::checksum_error() : std::invalid_argument{"Seed phrase checksum word does not match"} {} +unknown_language_error::unknown_language_error(std::string name) : + std::invalid_argument{"Unknown mnemonic language: {}"_format(name)}, + name_{std::move(name)} {} + const Mnemonics* find_language(std::string_view name) { for (auto lang : get_languages()) { if (lang->english_name == name || lang->native_name == name) @@ -29,6 +33,13 @@ const Mnemonics* find_language(std::string_view name) { return nullptr; } +const Mnemonics& get_language(std::string_view name) { + auto* lang = find_language(name); + if (!lang) + throw unknown_language_error{std::string(name)}; + return *lang; +} + namespace { // Returns the lowercased first `n_codepoints` UTF-8 codepoints of `s`. ASCII characters are // lowercased; non-ASCII codepoints are copied as-is. @@ -40,13 +51,13 @@ namespace { for (int count = 0; it < end && count < n_codepoints; count++) { uint8_t b = static_cast(*it); int len; - if ((b & 0b11100000) == 0b11000000) // 110xxxxx: 2-byte sequence + if ((b & 0b11100000) == 0b11000000) // 110xxxxx: 2-byte sequence len = 2; else if ((b & 0b11110000) == 0b11100000) // 1110xxxx: 3-byte sequence len = 3; else if ((b & 0b11111000) == 0b11110000) // 11110xxx: 4-byte sequence len = 4; - else // 0xxxxxxx: ASCII, or invalid byte + else // 0xxxxxxx: ASCII, or invalid byte len = 1; if (len == 1) { result.push_back(static_cast(std::tolower(b))); @@ -85,14 +96,22 @@ namespace { } } // namespace -std::vector bytes_to_words( +// string_view objects stored in secure_mnemonic::storage are placement-new constructed below. +// We rely on string_view being trivially destructible so that secure_buffer can zero and free +// the memory without needing to call destructors. +static_assert(std::is_trivially_destructible_v); + +secure_mnemonic bytes_to_words( std::span bytes, const Mnemonics& lang, bool checksum) { if (bytes.size() % 4 != 0) throw std::invalid_argument("Input length must be a multiple of 4 bytes"); size_t n = (bytes.size() / 4) * 3; - std::vector result; - result.reserve(n + checksum); + size_t total = n + checksum; + + secure_mnemonic result; + auto rw = result.storage.resize(total * sizeof(std::string_view)); + auto* out = reinterpret_cast(rw.buf.data()); uint32_t sum = 0; for (size_t i = 0; i < bytes.size(); i += 4) { @@ -102,40 +121,42 @@ std::vector bytes_to_words( uint32_t b = (val / NWORDS + a) % NWORDS; uint32_t c = (val / NWORDS / NWORDS + b) % NWORDS; - result.push_back(lang.words[a]); - result.push_back(lang.words[b]); - result.push_back(lang.words[c]); + std::construct_at(out + i / 4 * 3 + 0, lang.words[a]); + std::construct_at(out + i / 4 * 3 + 1, lang.words[b]); + std::construct_at(out + i / 4 * 3 + 2, lang.words[c]); sum += a + b + c; } if (checksum) - result.push_back(result[sum % n]); + std::construct_at(out + n, out[sum % n]); return result; } -std::vector bytes_to_words( +secure_mnemonic bytes_to_words( std::span bytes, std::string_view lang_name, bool checksum) { - auto lang = find_language(lang_name); - if (!lang) - throw std::invalid_argument("Unknown mnemonic language: " + std::string(lang_name)); - return bytes_to_words(bytes, *lang, checksum); + return bytes_to_words(bytes, get_language(lang_name), checksum); } -std::vector words_to_bytes( - std::span words, const Mnemonics& lang) { - size_t n = words.size(); - bool has_checksum = n % 3 == 1; - if (!has_checksum && n % 3 != 0) +// Validates the word count against `out.size()` and decodes words directly into `out`. +// out.size() must be a multiple of 4; words.size() must be (out.size()/4*3) or +1 with checksum. +static void words_to_bytes_impl( + std::span words, const Mnemonics& lang, std::span out) { + if (out.size() % 4 != 0) throw std::invalid_argument( - "Input word count must be a multiple of 3 (+1 with a checksum)"); + "Output buffer size must be a multiple of 4 (got {})"_format(out.size())); - size_t seed_words = n - has_checksum; - std::vector result; - result.resize((seed_words / 3) * 4); + size_t expected_seed_words = out.size() / 4 * 3; + size_t n = words.size(); + bool has_checksum = n == expected_seed_words + 1; + if (n != expected_seed_words && !has_checksum) + throw std::invalid_argument( + "Seed phrase word count ({}) does not match output buffer size ({} bytes, " + "expecting {} or {} words)"_format( + n, out.size(), expected_seed_words, expected_seed_words + 1)); uint32_t sum = 0; - for (size_t i = 0; i < seed_words; i += 3) { + for (size_t i = 0; i < expected_seed_words; i += 3) { std::array w; for (int j = 0; j < 3; j++) { int idx = get_word_index(lang, words[i + j]); @@ -149,9 +170,9 @@ std::vector words_to_bytes( ((NWORDS - b + c) % NWORDS) * (NWORDS * NWORDS); if (x % NWORDS != a) - throw std::invalid_argument("Mnemonic word sequence encodes an invalid value"); + throw std::invalid_argument("Seed phrase encodes an invalid value"); - oxenc::write_host_as_little(x, &result[(i / 3) * 4]); + oxenc::write_host_as_little(x, &out[(i / 3) * 4]); sum += a + b + c; } @@ -159,20 +180,43 @@ std::vector words_to_bytes( int checksum_idx = get_word_index(lang, words[n - 1]); if (checksum_idx < 0) throw unknown_word_error{std::string(words[n - 1])}; - int expected_idx = get_word_index(lang, words[sum % seed_words]); + int expected_idx = get_word_index(lang, words[sum % expected_seed_words]); if (checksum_idx != expected_idx) throw checksum_error{}; } +} + +session::secure_buffer words_to_bytes( + std::span words, const Mnemonics& lang) { + size_t n = words.size(); + bool has_checksum = n % 3 == 1; + if (n % 3 != 0 && !has_checksum) + throw std::invalid_argument( + "Seed phrase word count must be a multiple of 3, or a multiple of 3 plus one " + "checksum word (got {})"_format(n)); + size_t nbytes = ((n - has_checksum) / 3) * 4; + session::secure_buffer result; + auto rw = result.resize(nbytes); + words_to_bytes_impl(words, lang, rw.buf); return result; } -std::vector words_to_bytes( +session::secure_buffer words_to_bytes( std::span words, std::string_view lang_name) { - auto lang = find_language(lang_name); - if (!lang) - throw std::invalid_argument("Unknown mnemonic language: " + std::string(lang_name)); - return words_to_bytes(words, *lang); + return words_to_bytes(words, get_language(lang_name)); +} + +void words_to_bytes( + std::span words, const Mnemonics& lang, std::span out) { + words_to_bytes_impl(words, lang, out); +} + +void words_to_bytes( + std::span words, + std::string_view lang_name, + std::span out) { + words_to_bytes_impl(words, get_language(lang_name), out); } } // namespace session::mnemonics diff --git a/tests/test_mnemonics.cpp b/tests/test_mnemonics.cpp index 2eb0a5b1e..2103af48a 100644 --- a/tests/test_mnemonics.cpp +++ b/tests/test_mnemonics.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,8 +13,9 @@ using namespace session::mnemonics; // These pin the exact word list contents and ordering; if any word list changes this test fails. TEST_CASE("Mnemonic word list test vectors", "[mnemonics]") { // seed = SHA-512("libsession-util mnemonic test vector") - auto seed = "0dd5d9bc3d68c25a396f4aacd922a4d620a19cf3c9054cb825dd8a2c5420f4f3" - "ca314c582ffef5388df36e2546cc9103dd1776a634f676e1e631289b8d280b2e"_hex_b; + auto seed = + "0dd5d9bc3d68c25a396f4aacd922a4d620a19cf3c9054cb825dd8a2c5420f4f3" + "ca314c582ffef5388df36e2546cc9103dd1776a634f676e1e631289b8d280b2e"_hex_b; // clang-format off const std::pair> expected[] = { @@ -140,10 +142,11 @@ TEST_CASE("Mnemonic word list test vectors", "[mnemonics]") { SECTION(std::string(lang_name)) { auto* lang = find_language(lang_name); REQUIRE(lang); - auto words = bytes_to_words(seed, *lang, false); - REQUIRE(words.size() == 48); + auto mnemonic = bytes_to_words(seed, *lang, false); + REQUIRE(mnemonic.size() == 48); + auto wspan = mnemonic.open(); for (size_t i = 0; i < 48; i++) - CHECK(words[i] == exp_words[i]); + CHECK(wspan[i] == exp_words[i]); } } } @@ -168,26 +171,26 @@ TEST_CASE("Mnemonic round-trip tests", "[mnemonics]") { // 128-bit -> 12 words -> 128-bit auto words12 = bytes_to_words(data_128, *lang, false); CHECK(words12.size() == 12); - auto back12 = words_to_bytes(words12, *lang); - CHECK(back12 == data_128); + auto back12 = words_to_bytes(words12.open().words, *lang); + CHECK(std::ranges::equal(back12.access().buf, data_128)); // 128-bit -> 13 words (with checksum) -> 128-bit auto words13 = bytes_to_words(data_128, *lang); CHECK(words13.size() == 13); - auto back13 = words_to_bytes(words13, *lang); - CHECK(back13 == data_128); + auto back13 = words_to_bytes(words13.open().words, *lang); + CHECK(std::ranges::equal(back13.access().buf, data_128)); // 256-bit -> 24 words -> 256-bit auto words24 = bytes_to_words(data_256, *lang, false); CHECK(words24.size() == 24); - auto back24 = words_to_bytes(words24, *lang); - CHECK(back24 == data_256); + auto back24 = words_to_bytes(words24.open().words, *lang); + CHECK(std::ranges::equal(back24.access().buf, data_256)); // 256-bit -> 25 words (with checksum) -> 256-bit auto words25 = bytes_to_words(data_256, *lang); CHECK(words25.size() == 25); - auto back25 = words_to_bytes(words25, *lang); - CHECK(back25 == data_256); + auto back25 = words_to_bytes(words25.open().words, *lang); + CHECK(std::ranges::equal(back25.access().buf, data_256)); } } } @@ -209,14 +212,14 @@ TEST_CASE("Mnemonic case-insensitivity and prefix matching", "[mnemonics]") { REQUIRE(words.size() == 3); SECTION("Exact match") { - auto back = words_to_bytes(words, *english); - CHECK(back == data); + auto back = words_to_bytes(words.open().words, *english); + CHECK(std::ranges::equal(back.access().buf, data)); } SECTION("Case-insensitive match (ASCII)") { std::vector upper_words; std::vector storage; - for (auto w : words) { + for (auto w : words.open()) { std::string upper(w); for (auto& c : upper) c = std::toupper(static_cast(c)); @@ -226,33 +229,33 @@ TEST_CASE("Mnemonic case-insensitivity and prefix matching", "[mnemonics]") { upper_words.push_back(s); auto back = words_to_bytes(upper_words, *english); - CHECK(back == data); + CHECK(std::ranges::equal(back.access().buf, data)); } SECTION("Prefix match") { std::vector prefix_words; std::vector storage; - for (auto w : words) { + for (auto w : words.open()) { storage.push_back(std::string(w.substr(0, english->prefix_len))); } for (const auto& s : storage) prefix_words.push_back(s); auto back = words_to_bytes(prefix_words, *english); - CHECK(back == data); + CHECK(std::ranges::equal(back.access().buf, data)); } SECTION("Prefix match with typo after prefix") { std::vector typo_words; std::vector storage; - for (auto w : words) { + for (auto w : words.open()) { storage.push_back(std::string(w.substr(0, english->prefix_len)) + "xyz"); } for (const auto& s : storage) typo_words.push_back(s); auto back = words_to_bytes(typo_words, *english); - CHECK(back == data); + CHECK(std::ranges::equal(back.access().buf, data)); } } @@ -277,32 +280,42 @@ TEST_CASE("Mnemonic checksum", "[mnemonics]") { SECTION("Checksum word is correct position duplicate") { // sum of indices % 3 gives the position whose word is duplicated + auto s3 = words3.open(); + auto s4 = words4.open(); int i0 = 0, i1 = 0, i2 = 0; for (size_t i = 0; i < 3; i++) { int idx = 0; for (size_t j = 0; j < english->words.size(); j++) - if (english->words[j] == words3[i]) { idx = j; break; } - if (i == 0) i0 = idx; - else if (i == 1) i1 = idx; - else i2 = idx; + if (english->words[j] == s3[i]) { + idx = j; + break; + } + if (i == 0) + i0 = idx; + else if (i == 1) + i1 = idx; + else + i2 = idx; } size_t expected_pos = (i0 + i1 + i2) % 3; - CHECK(words4[3] == words3[expected_pos]); + CHECK(s4[3] == s3[expected_pos]); } SECTION("Checksum round-trip") { - auto back = words_to_bytes(words4, *english); - CHECK(back == data); + auto back = words_to_bytes(words4.open().words, *english); + CHECK(std::ranges::equal(back.access().buf, data)); } SECTION("Bad checksum throws checksum_error") { // Replace the checksum word with a different valid word - std::vector bad = {words4[0], words4[1], words4[2], words4[2] == words4[0] ? words4[1] : words4[0]}; + auto s4 = words4.open(); + std::vector bad = {s4[0], s4[1], s4[2], s4[2] == s4[0] ? s4[1] : s4[0]}; CHECK_THROWS_AS(words_to_bytes(bad, *english), checksum_error); } SECTION("Unknown checksum word throws unknown_word_error") { - std::vector bad = {words4[0], words4[1], words4[2], "ZZZunknown"}; + auto s4 = words4.open(); + std::vector bad = {s4[0], s4[1], s4[2], "ZZZunknown"}; CHECK_THROWS_AS(words_to_bytes(bad, *english), unknown_word_error); } } From e3220f9426fdaeea220cef2d1158b706e506a418 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 18:55:27 -0300 Subject: [PATCH 044/193] Make last_hash cache node-dependent If you change nodes, you can lose messages if you use last_hash from a different swarm member because not all swarm members necessarily receive messages in the exact same order. --- src/core.cpp | 207 ++++++++++++++++------------- src/core/schema/000_namespaces.sql | 6 +- tests/test_helper.hpp | 12 +- tests/test_poll.cpp | 124 +++++++++++++---- 4 files changed, 224 insertions(+), 125 deletions(-) diff --git a/src/core.cpp b/src/core.cpp index 396761f5e..c582d1e59 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -97,32 +97,15 @@ void Core::_poll() { if (!net) return; - auto namespaces = {config::Namespace::Devices, config::Namespace::AccountPubkeys}; + constexpr std::array namespaces = { + config::Namespace::Devices, config::Namespace::AccountPubkeys}; nlohmann::json ns_list = nlohmann::json::array(); - nlohmann::json last_hashes = nlohmann::json::object(); - - auto conn = db.conn(); - for (auto ns : namespaces) { - auto ns_val = static_cast(ns); - ns_list.push_back(ns_val); - auto last_hash = conn.prepared_maybe_get( - "SELECT last_hash FROM namespace_sync WHERE namespace = ?", ns_val); - if (last_hash) - last_hashes[std::to_string(ns_val)] = *last_hash; - } + for (auto ns : namespaces) + ns_list.push_back(static_cast(ns)); auto now_ms = epoch_ms(clock_now_ms()); auto session_id = oxenc::to_hex(globals.session_id()); - nlohmann::json params = { - {"pubkey", session_id}, - {"namespaces", ns_list}, - {"timestamp", now_ms}, - }; - - if (!last_hashes.empty()) - params["last_hashes"] = last_hashes; - std::string to_sign = fmt::format("retrieve{}{}", session_id, now_ms); std::array sig; auto seed = globals.account_seed(); @@ -133,82 +116,116 @@ void Core::_poll() { to_sign.size(), reinterpret_cast(seed.buf.data())); - params["signature"] = oxenc::to_base64(sig); - - nlohmann::json req_body = { - {"method", "retrieve"}, - {"params", params}, - }; - - net->get_swarm(globals.pubkey_x25519(), false, [this, net, req_body](auto, auto swarm) { - if (swarm.empty()) - return; - - auto body_str = req_body.dump(); - net->send_request( - network::Request{ - swarm.front(), - "storage_rpc", - to_vector(body_str), - network::RequestCategory::standard_small, - 20s}, - [this](bool success, - bool /*timeout*/, - int16_t /*status_code*/, - std::vector> /*headers*/, - std::optional body) { - if (!success || !body) - return; - - try { - auto json = nlohmann::json::parse(*body); - if (!json.contains("results") || !json["results"].is_array()) - return; - - auto conn = db.conn(); - for (const auto& res : json["results"]) { - if (!res.contains("namespace") || !res.contains("messages") || - !res["messages"].is_array()) - continue; - - auto ns_val = res["namespace"].get(); - auto ns = static_cast(ns_val); - - std::vector> messages_data; - std::string newest_hash; - - for (const auto& msg : res["messages"]) { - if (!msg.contains("data") || !msg["data"].is_string()) - continue; - auto b64_data = msg["data"].get(); - auto& decoded = messages_data.emplace_back(); - decoded.reserve(oxenc::from_base64_size(b64_data.size())); - oxenc::from_base64( - b64_data.begin(), - b64_data.end(), - std::back_inserter(decoded)); - - if (msg.contains("hash") && msg["hash"].is_string()) - newest_hash = msg["hash"].get(); - } - - if (!messages_data.empty()) { - if (!newest_hash.empty()) - conn.prepared_exec( - R"( -INSERT INTO namespace_sync (namespace, last_hash) VALUES (?, ?) -ON CONFLICT(namespace) DO UPDATE SET last_hash = excluded.last_hash + auto sig_b64 = oxenc::to_base64(sig); + + net->get_swarm( + globals.pubkey_x25519(), + false, + [this, net, namespaces, ns_list, session_id, now_ms, sig_b64](auto, auto swarm) { + if (swarm.empty()) + return; + + auto& node = swarm.front(); + + nlohmann::json last_hashes = nlohmann::json::object(); + { + auto conn = db.conn(); + for (auto ns : namespaces) { + auto ns_val = static_cast(ns); + auto last_hash = conn.prepared_maybe_get( + "SELECT last_hash FROM namespace_sync" + " WHERE namespace = ? AND sn_pubkey = ?", + ns_val, + node.remote_pubkey); + if (last_hash) + last_hashes[std::to_string(ns_val)] = *last_hash; + } + } + + nlohmann::json params = { + {"pubkey", session_id}, + {"namespaces", ns_list}, + {"timestamp", now_ms}, + {"signature", sig_b64}, + }; + if (!last_hashes.empty()) + params["last_hashes"] = last_hashes; + + nlohmann::json req_body = { + {"method", "retrieve"}, + {"params", params}, + }; + + auto body_str = req_body.dump(); + net->send_request( + network::Request{ + node, + "storage_rpc", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this, sn_pubkey = node.remote_pubkey]( + bool success, + bool /*timeout*/, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional body) { + if (!success || !body) + return; + + try { + auto json = nlohmann::json::parse(*body); + if (!json.contains("results") || !json["results"].is_array()) + return; + + auto conn = db.conn(); + for (const auto& res : json["results"]) { + if (!res.contains("namespace") || + !res.contains("messages") || + !res["messages"].is_array()) + continue; + + auto ns_val = res["namespace"].get(); + auto ns = static_cast(ns_val); + + std::vector> messages_data; + std::string newest_hash; + + for (const auto& msg : res["messages"]) { + if (!msg.contains("data") || !msg["data"].is_string()) + continue; + auto b64_data = msg["data"].get(); + auto& decoded = messages_data.emplace_back(); + decoded.reserve( + oxenc::from_base64_size(b64_data.size())); + oxenc::from_base64( + b64_data.begin(), + b64_data.end(), + std::back_inserter(decoded)); + + if (msg.contains("hash") && msg["hash"].is_string()) + newest_hash = msg["hash"].get(); + } + + if (!messages_data.empty()) { + if (!newest_hash.empty()) + conn.prepared_exec( + R"( +INSERT INTO namespace_sync (namespace, sn_pubkey, last_hash) VALUES (?, ?, ?) +ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash )", - ns_val, - newest_hash); - receive_messages(to_view_vector(messages_data), ns, true); + ns_val, + sn_pubkey, + newest_hash); + receive_messages( + to_view_vector(messages_data), ns, true); + } + } + } catch (const std::exception& e) { + log::warning(cat, "Failed to parse poll response: {}", e.what()); } - } - } catch (const std::exception& e) { - log::warning(cat, "Failed to parse poll response: {}", e.what()); - } - }); - }); + }); + }); } void Core::receive_messages( diff --git a/src/core/schema/000_namespaces.sql b/src/core/schema/000_namespaces.sql index 6e1819601..4d94660ba 100644 --- a/src/core/schema/000_namespaces.sql +++ b/src/core/schema/000_namespaces.sql @@ -1,4 +1,6 @@ CREATE TABLE namespace_sync ( - namespace INTEGER PRIMARY KEY NOT NULL, - last_hash TEXT NOT NULL + namespace INTEGER NOT NULL, + sn_pubkey BLOB NOT NULL CHECK(length(sn_pubkey) = 32), + last_hash TEXT NOT NULL, + PRIMARY KEY (namespace, sn_pubkey) ) STRICT; diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index f2fdc0ab1..15853ac2f 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -39,10 +40,15 @@ class TestHelper { public: static void poll(core::Core& core) { core._poll(); } - // Returns the last_hash stored for the given namespace (or nullopt if none). - static std::optional namespace_last_hash(core::Core& core, int16_t ns) { + // Returns the last_hash stored for the given namespace+sn_pubkey pair (or nullopt if none). + static std::optional namespace_last_hash( + core::Core& core, + int16_t ns, + const network::ed25519_pubkey& sn_pubkey) { return core.db.conn().prepared_maybe_get( - "SELECT last_hash FROM namespace_sync WHERE namespace = ?", ns); + "SELECT last_hash FROM namespace_sync WHERE namespace = ? AND sn_pubkey = ?", + ns, + sn_pubkey); } // Returns the raw 32-byte seed for the account key identified by the given x25519 public key. diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index f5306a8d1..b7be2e7ce 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -20,6 +20,9 @@ class MockNetwork : public network::Network { }; std::vector sent_requests; + // The node returned by get_swarm; tests can change this to simulate swarm-member switches. + network::service_node current_node; + void send_request( network::Request request, network::network_response_callback_t callback) override { sent_requests.push_back({std::move(request), std::move(callback)}); @@ -31,15 +34,26 @@ class MockNetwork : public network::Network { std::function< void(network::swarm_id_t swarm_id, std::vector swarm)> callback) override { - // Return a dummy swarm - std::vector swarm; - network::service_node node; - node.remote_pubkey = network::ed25519_pubkey{}; // zeroed key - swarm.push_back(node); - callback(0, swarm); + callback(0, {current_node}); } }; +// Helpers to build a mock retrieve response carrying a single message in one namespace. +static nlohmann::json make_response( + int16_t ns, std::vector msg_data, std::string hash) { + nlohmann::json response; + response["results"] = nlohmann::json::array(); + nlohmann::json res_item; + res_item["namespace"] = ns; + res_item["messages"] = nlohmann::json::array(); + nlohmann::json msg_item; + msg_item["data"] = oxenc::to_base64(msg_data); + msg_item["hash"] = std::move(hash); + res_item["messages"].push_back(msg_item); + response["results"].push_back(res_item); + return response; +} + TEST_CASE("Core automatic polling", "[core][poll]") { auto db_path = std::filesystem::temp_directory_path() / "test_poll.db"; if (std::filesystem::exists(db_path)) @@ -54,6 +68,8 @@ TEST_CASE("Core automatic polling", "[core][poll]") { { core::Core core{db_path, callbacks}; auto mock_net = std::make_shared(); + // Use a fixed non-zero pubkey for the node. + mock_net->current_node.remote_pubkey[0] = 0x01; core.set_network(mock_net); @@ -70,10 +86,10 @@ TEST_CASE("Core automatic polling", "[core][poll]") { CHECK(params["namespaces"] == nlohmann::json::array({21, -21})); CHECK(params.contains("timestamp")); CHECK(params.contains("signature")); + // No prior hash for this node yet, so no last_hashes in request. + CHECK_FALSE(params.contains("last_hashes")); // Build a valid link request from a second device sharing the same account seed. - // The link request is encrypted with the account seed, so the receiving core can only - // decrypt it if both devices share that seed. cleared_b32 seed_bytes; { auto seed_acc = core.globals.account_seed(); @@ -84,25 +100,15 @@ TEST_CASE("Core automatic polling", "[core][poll]") { const auto* p = reinterpret_cast(link_msg.data()); std::vector outer_msg{p, p + link_msg.size()}; - // Prepare a mock response - nlohmann::json response; - response["results"] = nlohmann::json::array(); - nlohmann::json res_item; - res_item["namespace"] = 21; - res_item["messages"] = nlohmann::json::array(); - nlohmann::json msg_item; - msg_item["data"] = oxenc::to_base64(outer_msg); - msg_item["hash"] = "hash1"; - res_item["messages"].push_back(msg_item); - response["results"].push_back(res_item); - - sent.callback(true, false, 200, {}, response.dump()); - - // Verify last_hash was updated in DB - CHECK(TestHelper::namespace_last_hash(core, 21) == "hash1"); + sent.callback( + true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); + + // Verify last_hash was stored under this specific node's pubkey. + CHECK(TestHelper::namespace_last_hash(core, 21, mock_net->current_node.remote_pubkey) == + "hash1"); CHECK(received); - // Poll again, should include last_hash + // Poll again with the same node — should include last_hash. mock_net->sent_requests.clear(); TestHelper::poll(core); @@ -114,3 +120,71 @@ TEST_CASE("Core automatic polling", "[core][poll]") { if (std::filesystem::exists(db_path)) std::filesystem::remove(db_path); } + +TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-member switch", + "[core][poll]") { + TempCore c; + auto mock_net = std::make_shared(); + + // Two distinct service nodes with different pubkeys. + network::service_node node_a, node_b; + node_a.remote_pubkey[0] = 0xAA; + node_b.remote_pubkey[0] = 0xBB; + + c->set_network(mock_net); + + // ── First poll: node A, no prior state ────────────────────────────────────── + mock_net->current_node = node_a; + TestHelper::poll(*c); + REQUIRE(mock_net->sent_requests.size() == 1); + { + auto params = + nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + // No prior hash for any node — must not send last_hashes. + CHECK_FALSE(params.contains("last_hashes")); + } + // Respond with hash "xyz" from node A. + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_response(21, {0x01}, "xyz").dump()); + CHECK(TestHelper::namespace_last_hash(*c, 21, node_a.remote_pubkey) == "xyz"); + CHECK_FALSE(TestHelper::namespace_last_hash(*c, 21, node_b.remote_pubkey).has_value()); + + // ── Second poll: still node A — must use A's stored hash ──────────────────── + mock_net->sent_requests.clear(); + TestHelper::poll(*c); + REQUIRE(mock_net->sent_requests.size() == 1); + { + auto params = + nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + CHECK(params["last_hashes"]["21"] == "xyz"); + } + + // ── Third poll: switch to node B — no stored hash for B, so request everything ── + mock_net->sent_requests.clear(); + mock_net->current_node = node_b; + TestHelper::poll(*c); + REQUIRE(mock_net->sent_requests.size() == 1); + { + auto params = + nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + // B has no recorded hash — must not send last_hashes so we get everything. + CHECK_FALSE(params.contains("last_hashes")); + } + // Respond with hash "zyx" from node B (the message that B happens to have seen first). + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_response(21, {0x02}, "zyx").dump()); + CHECK(TestHelper::namespace_last_hash(*c, 21, node_b.remote_pubkey) == "zyx"); + // A's hash is untouched. + CHECK(TestHelper::namespace_last_hash(*c, 21, node_a.remote_pubkey) == "xyz"); + + // ── Fourth poll: back to node A — must still use A's hash, not B's ────────── + mock_net->sent_requests.clear(); + mock_net->current_node = node_a; + TestHelper::poll(*c); + REQUIRE(mock_net->sent_requests.size() == 1); + { + auto params = + nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + CHECK(params["last_hashes"]["21"] == "xyz"); + } +} From 934704b34d88b029f09899d30ec767dd2868b725 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 22:26:34 -0300 Subject: [PATCH 045/193] Add PFS+PQ remote session id cache & prefetching The intention here (eventually) is for clients to pre-fetch other session id PFS+PQ keys when opening a conversation, so that recent keys are likely already loaded when a user wants to actually encrypt and send a message. (Prefetching does nothing if the session id has already been fetched within the last 24h, so this isn't going to be a big network hit). --- include/session/clock.hpp | 14 ++ include/session/core.hpp | 17 +++ include/session/xed25519.hpp | 15 ++ src/core.cpp | 204 +++++++++++++++++++++++++- src/core/schema/000_pfs_key_cache.sql | 9 ++ src/xed25519.cpp | 22 +++ tests/CMakeLists.txt | 1 + tests/test_helper.hpp | 73 ++++++++- tests/test_pfs_key_cache.cpp | 157 ++++++++++++++++++++ tests/test_poll.cpp | 48 +----- tests/test_utils.cpp | 33 +++++ tests/test_xed25519.cpp | 36 +++++ 12 files changed, 580 insertions(+), 49 deletions(-) create mode 100644 src/core/schema/000_pfs_key_cache.sql create mode 100644 tests/test_pfs_key_cache.cpp diff --git a/include/session/clock.hpp b/include/session/clock.hpp index 434f7daf6..f8941240c 100644 --- a/include/session/clock.hpp +++ b/include/session/clock.hpp @@ -100,4 +100,18 @@ constexpr int64_t epoch_ms(const std::chrono::time_point& t) { return duration_ms(t.time_since_epoch()); } +// Inverse of epoch_count/epoch_seconds/epoch_ms: reconstruct a sys_time with the given duration +// precision from a raw integer count of that duration since the epoch. +template +inline std::chrono::sys_time from_epoch(int64_t t) { + return std::chrono::sys_time{Duration{t}}; +} +// Shortcuts for the common cases: +inline std::chrono::sys_seconds from_epoch_s(int64_t t) { + return from_epoch(t); +} +inline sys_ms from_epoch_ms(int64_t t) { + return from_epoch(t); +} + } // namespace session diff --git a/include/session/core.hpp b/include/session/core.hpp index 4eefe4c5c..e32e90194 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -133,6 +133,7 @@ class TestHelper; namespace session::core { +using namespace std::literals; namespace quic = oxen::quic; namespace detail { @@ -258,6 +259,22 @@ class Core { /// Set an optional network interface that can be used to make network requests to swarm members void set_network(std::shared_ptr network); + /// How long a cached PFS key is considered fresh (no re-fetch needed). + static constexpr auto PFS_KEY_FRESH_DURATION = 24h; + /// How long a cached PFS key is usable as a fallback before it expires entirely. + static constexpr auto PFS_KEY_EXPIRY_DURATION = 48h; + + /// Initiates a background fetch of the X25519 and ML-KEM-768 account public keys for the + /// given remote session_id (33-byte 0x05-prefixed X25519 pubkey), caching the result in the + /// pfs_key_cache table. + /// + /// - If the cached entry is less than PFS_KEY_FRESH_DURATION (24h) old, does nothing. + /// - If the entry is stale (24–48h old) or absent, initiates a network fetch via the network + /// object (if present). Stale entries remain usable as a fallback until they expire. + /// - Entries older than PFS_KEY_EXPIRY_DURATION (48h) are overwritten on next successful + /// fetch. + void prefetch_pfs_keys(std::span session_id); + /// Returns the optional network interface, if set. const std::shared_ptr& network() const { return _network; } diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index ef62d9881..e0e314c2f 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -13,6 +14,11 @@ std::array sign( std::span curve25519_privkey /* 32 bytes */, std::span msg); +/// std::byte overload; returns a std::byte array. +std::array sign( + std::span curve25519_privkey /* 32 bytes */, + std::span msg); + /// "Softer" version that takes and returns strings of regular chars std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg); @@ -22,6 +28,12 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string std::span curve25519_pubkey /* 32 bytes */, std::span msg); +/// std::byte overload +[[nodiscard]] bool verify( + std::span signature /* 64 bytes */, + std::span curve25519_pubkey /* 32 bytes */, + std::span msg); + /// "Softer" version that takes strings of regular chars [[nodiscard]] bool verify( std::string_view signature /* 64 bytes */, @@ -34,6 +46,9 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string /// negative) by setting the sign bit, i.e. `returned_pubkey[31] |= 0x80`. std::array pubkey(std::span curve25519_pubkey) noexcept; +/// std::byte overload; returns a std::byte array. +std::array pubkey(std::span curve25519_pubkey) noexcept; + /// "Softer" version that takes/returns strings of regular chars. Throws invalid_argument if the /// input is not 32 bytes. std::string pubkey(std::string_view curve25519_pubkey); diff --git a/src/core.cpp b/src/core.cpp index c582d1e59..b8e39ebd5 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,6 +1,9 @@ +#include #include #include +#include #include +#include #include #include #include @@ -16,6 +19,7 @@ #include #include #include +#include #include #include "session/core/component.hpp" @@ -180,8 +184,7 @@ void Core::_poll() { auto conn = db.conn(); for (const auto& res : json["results"]) { - if (!res.contains("namespace") || - !res.contains("messages") || + if (!res.contains("namespace") || !res.contains("messages") || !res["messages"].is_array()) continue; @@ -196,8 +199,7 @@ void Core::_poll() { continue; auto b64_data = msg["data"].get(); auto& decoded = messages_data.emplace_back(); - decoded.reserve( - oxenc::from_base64_size(b64_data.size())); + decoded.reserve(oxenc::from_base64_size(b64_data.size())); oxenc::from_base64( b64_data.begin(), b64_data.end(), @@ -217,8 +219,7 @@ ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash ns_val, sn_pubkey, newest_hash); - receive_messages( - to_view_vector(messages_data), ns, true); + receive_messages(to_view_vector(messages_data), ns, true); } } } catch (const std::exception& e) { @@ -228,6 +229,197 @@ ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash }); } +void Core::prefetch_pfs_keys(std::span session_id) { + auto net = _network; + if (!net) + throw std::logic_error{"prefetch_pfs_keys called without a network object"}; + + // One copy of session_id for async use; subsequently moved into lambdas. + // sid must be unsigned char because xed25519::verify requires it for the x25519 pubkey span. + std::array sid; + std::ranges::copy(session_id, sid.begin()); + + // Skip the fetch if the cached entry is still fresh (< 24h old). + { + auto conn = db.conn(); + auto fetched_at = conn.prepared_maybe_get( + "SELECT fetched_at FROM pfs_key_cache WHERE session_id = ?", sid); + if (fetched_at) { + auto age = clock_now_s() - from_epoch_s(*fetched_at); + if (age < PFS_KEY_FRESH_DURATION) { + log::debug( + cat, + "prefetch_pfs_keys: cached key for {} is still fresh ({} old), skipping", + oxenc::to_hex(session_id.begin(), session_id.end()), + age); + return; + } + log::debug( + cat, + "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", + oxenc::to_hex(session_id.begin(), session_id.end()), + age); + } else { + log::debug( + cat, + "prefetch_pfs_keys: no cached key for {}, fetching", + oxenc::to_hex(session_id.begin(), session_id.end())); + } + } + + // The swarm is indexed by the x25519 pubkey — the session_id without its 0x05 prefix. + network::x25519_pubkey x25519_pub; + std::ranges::copy(session_id.subspan<1>(), x25519_pub.begin()); + + auto session_id_hex = oxenc::to_hex(session_id.begin(), session_id.end()); + auto now_ms = epoch_ms(clock_now_ms()); + + nlohmann::json req_body = { + {"method", "retrieve"}, + {"params", + {{"pubkey", session_id_hex}, + {"namespaces", {static_cast(config::Namespace::AccountPubkeys)}}, + {"timestamp", now_ms}}}, + }; + + net->get_swarm( + x25519_pub, false, [this, net, sid = std::move(sid), req_body](auto, auto swarm) { + if (swarm.empty()) { + log::debug(cat, "prefetch_pfs_keys: get_swarm returned empty swarm"); + return; + } + + auto body_str = req_body.dump(); + net->send_request( + network::Request{ + swarm.front(), + "storage_rpc", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this, sid = std::move(sid)]( + bool success, + bool /*timeout*/, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional body) { + if (!success || !body) { + log::debug(cat, "prefetch_pfs_keys: request failed"); + return; + } + + try { + auto json = nlohmann::json::parse(*body); + if (!json.contains("results") || !json["results"].is_array()) { + log::warning( + cat, + "prefetch_pfs_keys: response missing or invalid " + "'results' array"); + return; + } + + // Strip the 0x05 prefix to get the x25519 pubkey for + // signature verification. + std::span x25519_pub{sid.data() + 1, 32}; + + // Track the most recently valid pubkeys seen across all messages. + std::optional> pk_x25519; + std::optional> + pk_mlkem768; + + for (const auto& res : json["results"]) { + if (!res.contains("namespace")) { + log::warning( + cat, + "prefetch_pfs_keys: result entry missing " + "'namespace' field"); + continue; + } + if (res["namespace"].get() != + static_cast(config::Namespace::AccountPubkeys)) + continue; + if (!res.contains("messages") || !res["messages"].is_array()) { + log::warning( + cat, + "prefetch_pfs_keys: AccountPubkeys result " + "missing or invalid 'messages' array"); + continue; + } + + for (const auto& msg : res["messages"]) { + if (!msg.contains("data") || !msg["data"].is_string()) { + log::warning( + cat, + "prefetch_pfs_keys: message missing or " + "non-string 'data' field"); + continue; + } + auto b64 = msg["data"].get(); + std::vector decoded; + decoded.reserve(oxenc::from_base64_size(b64.size())); + oxenc::from_base64( + b64.begin(), + b64.end(), + std::back_inserter(decoded)); + try { + oxenc::bt_dict_consumer in{decoded}; + auto M = in.require_span< + std::byte, + MLKEM768_PUBLICKEYBYTES>("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub]( + std::span b, + std::span sig) { + if (!xed25519::verify(sig, x25519_pub, b)) + throw std::runtime_error{ + "signature verification " + "failed"}; + }); + std::ranges::copy(X, pk_x25519.emplace().begin()); + std::ranges::copy(M, pk_mlkem768.emplace().begin()); + } catch (const std::exception& e) { + log::warning( + cat, + "Ignoring malformed remote account pubkey " + "message: {}", + e.what()); + } + } + } + + if (!pk_x25519 || !pk_mlkem768) { + log::debug( + cat, + "prefetch_pfs_keys: no valid account pubkey message " + "found in response"); + return; + } + + db.conn().prepared_exec( + R"( +INSERT INTO pfs_key_cache (session_id, fetched_at, pubkey_x25519, pubkey_mlkem768) +VALUES (?, ?, ?, ?) +ON CONFLICT(session_id) DO UPDATE SET + fetched_at = excluded.fetched_at, + pubkey_x25519 = excluded.pubkey_x25519, + pubkey_mlkem768 = excluded.pubkey_mlkem768 +)", + sid, + epoch_seconds(clock_now_s()), + *pk_x25519, + *pk_mlkem768); + } catch (const std::exception& e) { + log::warning( + cat, + "Failed to process PFS key fetch response: {}", + e.what()); + } + }); + }); +} + void Core::receive_messages( std::span> messages, config::Namespace ns, diff --git a/src/core/schema/000_pfs_key_cache.sql b/src/core/schema/000_pfs_key_cache.sql new file mode 100644 index 000000000..d439d6914 --- /dev/null +++ b/src/core/schema/000_pfs_key_cache.sql @@ -0,0 +1,9 @@ +-- Cache of remote account public keys (X25519 + ML-KEM-768) used for PFS+PQ message encryption. +-- Keys are considered fresh for PFS_KEY_FRESH_DURATION (24h) and expire after +-- PFS_KEY_EXPIRY_DURATION (48h); stale entries (24-48h old) are still usable as a fallback. +CREATE TABLE pfs_key_cache ( + session_id BLOB NOT NULL PRIMARY KEY CHECK(length(session_id) = 33), + fetched_at INTEGER NOT NULL, -- unix timestamp (seconds) of last successful fetch + pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) = 32), + pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) = 1184) +) STRICT; diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 0d7411a9c..db35fce87 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -99,6 +100,12 @@ bytes<64> sign( return signature; } +std::array sign( + std::span curve25519_privkey, std::span msg) { + return std::bit_cast>( + sign(as_span(curve25519_privkey), as_span(msg))); +} + std::string sign(std::string_view curve25519_privkey, std::string_view msg) { auto sig = sign(to_span(curve25519_privkey), to_span(msg)); return std::string{reinterpret_cast(sig.data()), sig.size()}; @@ -115,12 +122,27 @@ bool verify( signature.data(), msg.data(), msg.size(), ed_pubkey.data()); } +bool verify( + std::span signature, + std::span curve25519_pubkey, + std::span msg) { + return verify( + as_span(signature), + as_span(curve25519_pubkey), + as_span(msg)); +} + bool verify(std::string_view signature, std::string_view curve25519_pubkey, std::string_view msg) { return verify(to_span(signature), to_span(curve25519_pubkey), to_span(msg)); } // pubkey(...) is in xed25519-tweetnacl.cpp +std::array pubkey(std::span curve25519_pubkey) noexcept { + return std::bit_cast>( + pubkey(as_span(curve25519_pubkey))); +} + std::string pubkey(std::string_view curve25519_pubkey) { if (curve25519_pubkey.size() != 32) throw std::invalid_argument{"Invalid X25519 pubkey"}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 245ca7368..5e1c2fcdb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -49,6 +49,7 @@ if(ENABLE_NETWORKING) list(APPEND LIB_SESSION_UTESTS_SOURCES test_snode_pool.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_core_network.cpp) list(APPEND LIB_SESSION_UTESTS_SOURCES test_poll.cpp) + list(APPEND LIB_SESSION_UTESTS_SOURCES test_pfs_key_cache.cpp) endif() add_library(test_libs INTERFACE) diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 15853ac2f..b47a9ee42 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -7,11 +7,43 @@ #include #include #include +#include #include #include namespace session { +/// A minimal in-process mock of network::Network for unit tests. +/// Tests can set `current_node` to control which node get_swarm returns, and inspect +/// `sent_requests` to observe outgoing requests and fire their callbacks. +class MockNetwork : public network::Network { + public: + MockNetwork() : network::Network(network::config::Config{}) {} + + struct SentRequest { + network::Request request; + network::network_response_callback_t callback; + }; + std::vector sent_requests; + + // The node returned by get_swarm; tests can change this to simulate swarm-member switches. + network::service_node current_node; + + void send_request( + network::Request request, network::network_response_callback_t callback) override { + sent_requests.push_back({std::move(request), std::move(callback)}); + } + + void get_swarm( + network::x25519_pubkey /*swarm_pubkey*/, + bool /*ignore_strike_count*/, + std::function< + void(network::swarm_id_t swarm_id, std::vector swarm)> + callback) override { + callback(0, {current_node}); + } +}; + // Smart-pointer-like RAII wrapper around a Core backed by a unique temporary DB file. // The DB file is removed on destruction. Default encryption uses a zeroed raw_key. struct TempCore { @@ -42,9 +74,7 @@ class TestHelper { // Returns the last_hash stored for the given namespace+sn_pubkey pair (or nullopt if none). static std::optional namespace_last_hash( - core::Core& core, - int16_t ns, - const network::ed25519_pubkey& sn_pubkey) { + core::Core& core, int16_t ns, const network::ed25519_pubkey& sn_pubkey) { return core.db.conn().prepared_maybe_get( "SELECT last_hash FROM namespace_sync WHERE namespace = ? AND sn_pubkey = ?", ns, @@ -62,6 +92,43 @@ class TestHelper { std::ranges::copy(blob, seed.begin()); return seed; } + + // Cached PFS key entry as stored in the pfs_key_cache table. + struct PfsCacheEntry { + int64_t fetched_at; + std::array pubkey_x25519; + std::array pubkey_mlkem768; + }; + + // Returns the {pubkey_x25519, pubkey_mlkem768} of the active (unrotated) account key. + static std::pair, std::array> active_account_pubkeys( + core::Core& core) { + auto [x25519, mlkem768] = + core.db.conn() + .prepared_get< + sqlite::blob_guts>, + sqlite::blob_guts>>( + "SELECT pubkey_x25519, pubkey_mlkem768" + " FROM device_account_keys WHERE rotated IS NULL"); + return {x25519, mlkem768}; + } + + // Returns the pfs_key_cache entry for the given session_id, or nullopt if absent. + static std::optional pfs_cache_entry( + core::Core& core, std::span session_id) { + auto conn = core.db.conn(); + if (!conn.prepared_maybe_get( + "SELECT fetched_at FROM pfs_key_cache WHERE session_id = ?", session_id)) + return std::nullopt; + auto [fetched_at, pk_x25519, pk_mlkem768] = conn.prepared_get< + int64_t, + sqlite::blob_guts>, + sqlite::blob_guts>>( + "SELECT fetched_at, pubkey_x25519, pubkey_mlkem768" + " FROM pfs_key_cache WHERE session_id = ?", + session_id); + return PfsCacheEntry{fetched_at, pk_x25519, pk_mlkem768}; + } }; } // namespace session diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp new file mode 100644 index 000000000..ea7c96214 --- /dev/null +++ b/tests/test_pfs_key_cache.cpp @@ -0,0 +1,157 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "test_helper.hpp" +#include "utils.hpp" + +using namespace session; +using namespace std::literals; + +// Wraps a single bt-dict message payload as a mock AccountPubkeys retrieve response. +static nlohmann::json make_pubkey_response(std::span msg_data) { + nlohmann::json resp; + resp["results"] = nlohmann::json::array(); + nlohmann::json res_item; + res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); + res_item["messages"] = nlohmann::json::array(); + nlohmann::json msg_item; + msg_item["data"] = oxenc::to_base64( + std::string_view{reinterpret_cast(msg_data.data()), msg_data.size()}); + res_item["messages"].push_back(msg_item); + resp["results"].push_back(res_item); + return resp; +} + +TEST_CASE("prefetch_pfs_keys throws without network", "[core][pfs]") { + TempCore c; + TempCore remote; + auto session_id = remote->globals.session_id(); + std::array sid; + std::ranges::copy(session_id, sid.begin()); + CHECK_THROWS_AS(c->prefetch_pfs_keys(sid), std::logic_error); +} + +TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core][pfs]") { + TempCore c; + auto mock_net = std::make_shared(); + c->set_network(mock_net); + + // Build a "remote" account whose pubkeys we want to fetch. + TempCore remote; + auto remote_msg = remote->devices.build_account_pubkey_message(); + + auto session_id_span = remote->globals.session_id(); + std::array sid; + std::ranges::copy(session_id_span, sid.begin()); + + SECTION("Fetches and stores pubkeys when cache is absent") { + c->prefetch_pfs_keys(sid); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto req = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + CHECK(req["method"] == "retrieve"); + CHECK(req["params"]["pubkey"] == oxenc::to_hex(sid)); + CHECK(req["params"]["namespaces"] == + nlohmann::json::array({static_cast(config::Namespace::AccountPubkeys)})); + + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(remote_msg).dump()); + + auto entry = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(entry.has_value()); + // fetched_at should be close to now. + auto age = clock_now_s() - from_epoch_s(entry->fetched_at); + CHECK(age >= 0s); + CHECK(age < 5s); + + // The stored pubkeys must match those from the remote's active account key. + auto [expected_x25519, expected_mlkem768] = TestHelper::active_account_pubkeys(*remote); + CHECK(entry->pubkey_x25519 == expected_x25519); + CHECK(entry->pubkey_mlkem768 == expected_mlkem768); + } + + SECTION("Skips fetch when cache is fresh") { + // First fetch: populates the cache. + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(remote_msg).dump()); + REQUIRE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + mock_net->sent_requests.clear(); + + // Second fetch within PFS_KEY_FRESH_DURATION: must not send another request. + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.empty()); + } + + SECTION("Re-fetches when cache is stale (older than PFS_KEY_FRESH_DURATION)") { + // First fetch. + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(remote_msg).dump()); + mock_net->sent_requests.clear(); + + // Advance clock past the fresh threshold. + ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.size() == 1); + } +} + +TEST_CASE("prefetch_pfs_keys handles malformed responses gracefully", "[core][pfs]") { + TempCore c; + auto mock_net = std::make_shared(); + c->set_network(mock_net); + + TempCore remote; + auto session_id_span = remote->globals.session_id(); + std::array sid; + std::ranges::copy(session_id_span, sid.begin()); + + SECTION("Garbage bt-dict data: no cache entry written") { + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + + nlohmann::json bad_response; + bad_response["results"] = nlohmann::json::array(); + nlohmann::json res_item; + res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); + res_item["messages"] = nlohmann::json::array(); + nlohmann::json msg_item; + // "not a bt-dict" is valid base64 but not a valid bt-dict. + msg_item["data"] = oxenc::to_base64("not a bt-dict"); + res_item["messages"].push_back(msg_item); + bad_response["results"].push_back(res_item); + + // Must not throw or crash; cache must remain empty. + mock_net->sent_requests[0].callback(true, false, 200, {}, bad_response.dump()); + CHECK_FALSE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + } + + SECTION("Bad signature: no cache entry written") { + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + + // Build a valid-looking message but sign it with the wrong key (our own account). + auto wrong_msg = c->devices.build_account_pubkey_message(); + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(wrong_msg).dump()); + CHECK_FALSE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + } + + SECTION("Network failure: no cache entry written") { + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + + mock_net->sent_requests[0].callback(false, false, 0, {}, std::nullopt); + CHECK_FALSE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + } +} diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index b7be2e7ce..d2846ea6a 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -10,34 +10,6 @@ using namespace session; -class MockNetwork : public network::Network { - public: - MockNetwork() : network::Network(network::config::Config{}) {} - - struct SentRequest { - network::Request request; - network::network_response_callback_t callback; - }; - std::vector sent_requests; - - // The node returned by get_swarm; tests can change this to simulate swarm-member switches. - network::service_node current_node; - - void send_request( - network::Request request, network::network_response_callback_t callback) override { - sent_requests.push_back({std::move(request), std::move(callback)}); - } - - void get_swarm( - session::network::x25519_pubkey /*swarm_pubkey*/, - bool /*ignore_strike_count*/, - std::function< - void(network::swarm_id_t swarm_id, std::vector swarm)> - callback) override { - callback(0, {current_node}); - } -}; - // Helpers to build a mock retrieve response carrying a single message in one namespace. static nlohmann::json make_response( int16_t ns, std::vector msg_data, std::string hash) { @@ -100,8 +72,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { const auto* p = reinterpret_cast(link_msg.data()); std::vector outer_msg{p, p + link_msg.size()}; - sent.callback( - true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); + sent.callback(true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); // Verify last_hash was stored under this specific node's pubkey. CHECK(TestHelper::namespace_last_hash(core, 21, mock_net->current_node.remote_pubkey) == @@ -121,8 +92,9 @@ TEST_CASE("Core automatic polling", "[core][poll]") { std::filesystem::remove(db_path); } -TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-member switch", - "[core][poll]") { +TEST_CASE( + "Polling uses per-node last_hash to avoid missing messages on swarm-member switch", + "[core][poll]") { TempCore c; auto mock_net = std::make_shared(); @@ -138,8 +110,7 @@ TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-me TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = - nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; // No prior hash for any node — must not send last_hashes. CHECK_FALSE(params.contains("last_hashes")); } @@ -154,8 +125,7 @@ TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-me TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = - nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; CHECK(params["last_hashes"]["21"] == "xyz"); } @@ -165,8 +135,7 @@ TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-me TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = - nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; // B has no recorded hash — must not send last_hashes so we get everything. CHECK_FALSE(params.contains("last_hashes")); } @@ -183,8 +152,7 @@ TEST_CASE("Polling uses per-node last_hash to avoid missing messages on swarm-me TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = - nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; + auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; CHECK(params["last_hashes"]["21"] == "xyz"); } } diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 5db8927e9..572229e11 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -1,4 +1,5 @@ #include +#include #include "utils.hpp" @@ -24,4 +25,36 @@ TEST_CASE("Network", "[network][parse_url]") { CHECK(path2.value_or("NULL") == "/test/123456"); CHECK(path3.value_or("NULL") == "NULL"); CHECK(path4.value_or("NULL") == "/test?value=test"); +} + +TEST_CASE("from_epoch helpers are the inverse of epoch_seconds/epoch_ms", "[clock]") { + using namespace std::chrono; + using namespace session; + + // Round-trip through epoch_seconds / from_epoch_s + auto t_s = clock_now_s(); + int64_t count_s = epoch_seconds(t_s); + auto t_s2 = from_epoch_s(count_s); + CHECK(t_s == t_s2); + + // Round-trip through epoch_ms / from_epoch_ms + auto t_ms = clock_now_ms(); + int64_t count_ms = epoch_ms(t_ms); + auto t_ms2 = from_epoch_ms(count_ms); + CHECK(t_ms == t_ms2); + + // Generic from_epoch with seconds precision + int64_t unix_s = 1'700'000'000; + auto tp_s = from_epoch_s(unix_s); + CHECK(epoch_seconds(tp_s) == unix_s); + + // Generic from_epoch with milliseconds precision + int64_t unix_ms = 1'700'000'000'000LL; + auto tp_ms = from_epoch_ms(unix_ms); + CHECK(epoch_ms(tp_ms) == unix_ms); + + // from_epoch template returns sys_time + auto tp_generic = from_epoch(unix_s); + static_assert(std::same_as); + CHECK(epoch_seconds(tp_generic) == unix_s); } \ No newline at end of file diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 74a9ae351..e31f9f9cc 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -164,6 +164,42 @@ TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { xed_sig2.data(), msg.data(), msg.size(), pub2_abs.data()); REQUIRE(rc == 0); // Flipped sign should work } +TEST_CASE("XEd25519 std::byte overloads", "[xed25519][byte]") { + std::array xsk1; + int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); + REQUIRE(rc == 0); + + const auto msg_uc = session::to_span("hello world"); + // Build std::byte versions of privkey, pubkey, and message. + auto xsk1_b = std::bit_cast>(xsk1); + auto xpub1_b = std::bit_cast>(xpub1); + std::array msg_b; + std::memcpy(msg_b.data(), msg_uc.data(), msg_b.size()); + + // sign() byte overload should return a std::byte array. + auto sig_b = session::xed25519::sign( + std::span{xsk1_b}, std::span{msg_b}); + static_assert(std::same_as>); + + // The signature must verify with the unsigned char overload. + auto sig_uc = std::bit_cast>(sig_b); + rc = crypto_sign_ed25519_verify_detached( + sig_uc.data(), msg_uc.data(), msg_uc.size(), pub1.data()); + REQUIRE(rc == 0); + + // verify() byte overload. + REQUIRE(session::xed25519::verify( + std::span{sig_b}, + std::span{xpub1_b}, + std::span{msg_b})); + + // pubkey() byte overload should return a std::byte array. + auto ed_pk_b = session::xed25519::pubkey(std::span{xpub1_b}); + static_assert(std::same_as>); + auto ed_pk_uc = std::bit_cast>(ed_pk_b); + REQUIRE(view_hex(ed_pk_uc) == oxenc::to_hex(pub1)); +} + TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { std::array xsk1; int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); From 5c1f64081bf46a2edc662b01c6bb77c53bb76ca0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 23:20:50 -0300 Subject: [PATCH 046/193] Add negative-ack caching to PFS+PQ pubkey prefetching If we go to look up some pubkey and get a successful, but empty, response then we want to cache that for a while (1h in the commit added here) so that we don't repeatedly try refetching. This commit adds that nak cache. --- external/session-sqlite | 2 +- include/session/core.hpp | 2 + src/core.cpp | 71 ++++++++---- src/core/schema/000_pfs_key_cache.sql | 13 ++- tests/test_helper.hpp | 46 ++++---- tests/test_pfs_key_cache.cpp | 149 +++++++++++++++++++++++--- 6 files changed, 225 insertions(+), 58 deletions(-) diff --git a/external/session-sqlite b/external/session-sqlite index 8ea436bbb..9d16f90e2 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit 8ea436bbb79deaf7fdcfed445ccd6e4b6cb78c40 +Subproject commit 9d16f90e2c39a42300f9b91e37be82a4cfbbfc30 diff --git a/include/session/core.hpp b/include/session/core.hpp index e32e90194..6c2b64522 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -263,6 +263,8 @@ class Core { static constexpr auto PFS_KEY_FRESH_DURATION = 24h; /// How long a cached PFS key is usable as a fallback before it expires entirely. static constexpr auto PFS_KEY_EXPIRY_DURATION = 48h; + /// How long a NAK (successful fetch that returned no keys) suppresses re-fetching. + static constexpr auto PFS_KEY_NAK_DURATION = 1h; /// Initiates a background fetch of the X25519 and ML-KEM-768 account public keys for the /// given remote session_id (33-byte 0x05-prefixed X25519 pubkey), caching the result in the diff --git a/src/core.cpp b/src/core.cpp index b8e39ebd5..b984fcac8 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -239,31 +239,48 @@ void Core::prefetch_pfs_keys(std::span session_id) { std::array sid; std::ranges::copy(session_id, sid.begin()); - // Skip the fetch if the cached entry is still fresh (< 24h old). + // Skip the fetch if the cached entry is still fresh, or a recent NAK suppresses retrying. { auto conn = db.conn(); - auto fetched_at = conn.prepared_maybe_get( - "SELECT fetched_at FROM pfs_key_cache WHERE session_id = ?", sid); - if (fetched_at) { - auto age = clock_now_s() - from_epoch_s(*fetched_at); - if (age < PFS_KEY_FRESH_DURATION) { + auto sid_hex = oxenc::to_hex(session_id.begin(), session_id.end()); + + if (auto row = conn.prepared_maybe_get, std::optional>( + "SELECT fetched_at, nak_at FROM pfs_key_cache WHERE session_id = ?", sid)) { + auto [fetched_at, nak_at] = *row; + if (fetched_at) { + auto age = clock_now_s() - from_epoch_s(*fetched_at); + if (age < PFS_KEY_FRESH_DURATION) { + log::debug( + cat, + "prefetch_pfs_keys: cached key for {} is still fresh ({} old), " + "skipping", + sid_hex, + age); + return; + } log::debug( cat, - "prefetch_pfs_keys: cached key for {} is still fresh ({} old), skipping", - oxenc::to_hex(session_id.begin(), session_id.end()), + "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", + sid_hex, + age); + } else if (nak_at) { + auto age = clock_now_s() - from_epoch_s(*nak_at); + if (age < PFS_KEY_NAK_DURATION) { + log::debug( + cat, + "prefetch_pfs_keys: recent NAK for {} ({} old), skipping", + sid_hex, + age); + return; + } + log::debug( + cat, + "prefetch_pfs_keys: expired NAK for {} ({} old), re-fetching", + sid_hex, age); - return; } - log::debug( - cat, - "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", - oxenc::to_hex(session_id.begin(), session_id.end()), - age); } else { - log::debug( - cat, - "prefetch_pfs_keys: no cached key for {}, fetching", - oxenc::to_hex(session_id.begin(), session_id.end())); + log::debug(cat, "prefetch_pfs_keys: no cached key for {}, fetching", sid_hex); } } @@ -389,25 +406,37 @@ void Core::prefetch_pfs_keys(std::span session_id) { } } + auto now_s = epoch_seconds(clock_now_s()); + if (!pk_x25519 || !pk_mlkem768) { log::debug( cat, "prefetch_pfs_keys: no valid account pubkey message " "found in response"); + // Record a NAK. If a valid entry already exists, update only + // nak_at and leave fetched_at and pubkeys untouched. + db.conn().prepared_exec( + R"( +INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) +VALUES (?, NULL, ?, NULL, NULL) +ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at +)", + sid, + now_s); return; } db.conn().prepared_exec( R"( -INSERT INTO pfs_key_cache (session_id, fetched_at, pubkey_x25519, pubkey_mlkem768) -VALUES (?, ?, ?, ?) +INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) +VALUES (?, ?, NULL, ?, ?) ON CONFLICT(session_id) DO UPDATE SET fetched_at = excluded.fetched_at, pubkey_x25519 = excluded.pubkey_x25519, pubkey_mlkem768 = excluded.pubkey_mlkem768 )", sid, - epoch_seconds(clock_now_s()), + now_s, *pk_x25519, *pk_mlkem768); } catch (const std::exception& e) { diff --git a/src/core/schema/000_pfs_key_cache.sql b/src/core/schema/000_pfs_key_cache.sql index d439d6914..62025ee5c 100644 --- a/src/core/schema/000_pfs_key_cache.sql +++ b/src/core/schema/000_pfs_key_cache.sql @@ -1,9 +1,16 @@ -- Cache of remote account public keys (X25519 + ML-KEM-768) used for PFS+PQ message encryption. -- Keys are considered fresh for PFS_KEY_FRESH_DURATION (24h) and expire after -- PFS_KEY_EXPIRY_DURATION (48h); stale entries (24-48h old) are still usable as a fallback. +-- +-- nak_at is set whenever a successful fetch returns no valid keys, and is never cleared. It +-- suppresses re-fetching for PFS_KEY_NAK_DURATION (1h) when no valid keys exist. When valid +-- keys are present nak_at may coexist with them (the keys are still usable as a fallback). +-- In SQLite, CHECK constraints with a NULL argument evaluate to NULL (not FALSE), so the length +-- checks do not reject NULL pubkeys. CREATE TABLE pfs_key_cache ( session_id BLOB NOT NULL PRIMARY KEY CHECK(length(session_id) = 33), - fetched_at INTEGER NOT NULL, -- unix timestamp (seconds) of last successful fetch - pubkey_x25519 BLOB NOT NULL CHECK(length(pubkey_x25519) = 32), - pubkey_mlkem768 BLOB NOT NULL CHECK(length(pubkey_mlkem768) = 1184) + fetched_at INTEGER, -- unix timestamp (seconds) of last fetch with valid keys; NULL if none + nak_at INTEGER, -- unix timestamp of last fetch returning no keys; NULL if none + pubkey_x25519 BLOB CHECK(length(pubkey_x25519) = 32), + pubkey_mlkem768 BLOB CHECK(length(pubkey_mlkem768) = 1184) ) STRICT; diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index b47a9ee42..407026df7 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -93,13 +93,6 @@ class TestHelper { return seed; } - // Cached PFS key entry as stored in the pfs_key_cache table. - struct PfsCacheEntry { - int64_t fetched_at; - std::array pubkey_x25519; - std::array pubkey_mlkem768; - }; - // Returns the {pubkey_x25519, pubkey_mlkem768} of the active (unrotated) account key. static std::pair, std::array> active_account_pubkeys( core::Core& core) { @@ -113,21 +106,38 @@ class TestHelper { return {x25519, mlkem768}; } + // Cached PFS key entry as stored in the pfs_key_cache table. + // fetched_at and pubkeys are nullopt when the entry is a NAK (no valid keys). + struct PfsCacheEntry { + std::optional fetched_at; + std::optional nak_at; + std::optional> pubkey_x25519; + std::optional> pubkey_mlkem768; + }; + // Returns the pfs_key_cache entry for the given session_id, or nullopt if absent. static std::optional pfs_cache_entry( core::Core& core, std::span session_id) { - auto conn = core.db.conn(); - if (!conn.prepared_maybe_get( - "SELECT fetched_at FROM pfs_key_cache WHERE session_id = ?", session_id)) + using X = sqlite::blob_guts>; + using M = sqlite::blob_guts>; + auto row = core.db.conn() + .prepared_maybe_get< + std::optional, + std::optional, + std::optional, + std::optional>( + "SELECT fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768" + " FROM pfs_key_cache WHERE session_id = ?", + session_id); + if (!row) return std::nullopt; - auto [fetched_at, pk_x25519, pk_mlkem768] = conn.prepared_get< - int64_t, - sqlite::blob_guts>, - sqlite::blob_guts>>( - "SELECT fetched_at, pubkey_x25519, pubkey_mlkem768" - " FROM pfs_key_cache WHERE session_id = ?", - session_id); - return PfsCacheEntry{fetched_at, pk_x25519, pk_mlkem768}; + auto [fetched_at, nak_at, pk_x25519, pk_mlkem768] = *row; + return PfsCacheEntry{ + fetched_at, + nak_at, + pk_x25519 ? std::optional{(std::array)*pk_x25519} : std::nullopt, + pk_mlkem768 ? std::optional{(std::array)*pk_mlkem768} + : std::nullopt}; } }; diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp index ea7c96214..b783d1f35 100644 --- a/tests/test_pfs_key_cache.cpp +++ b/tests/test_pfs_key_cache.cpp @@ -29,6 +29,17 @@ static nlohmann::json make_pubkey_response(std::span msg_data) return resp; } +// Returns an AccountPubkeys response body with no messages. +static nlohmann::json make_empty_response() { + nlohmann::json resp; + resp["results"] = nlohmann::json::array(); + nlohmann::json res_item; + res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); + res_item["messages"] = nlohmann::json::array(); + resp["results"].push_back(res_item); + return resp; +} + TEST_CASE("prefetch_pfs_keys throws without network", "[core][pfs]") { TempCore c; TempCore remote; @@ -66,15 +77,19 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] auto entry = TestHelper::pfs_cache_entry(*c, sid); REQUIRE(entry.has_value()); + REQUIRE(entry->fetched_at.has_value()); // fetched_at should be close to now. - auto age = clock_now_s() - from_epoch_s(entry->fetched_at); + auto age = clock_now_s() - from_epoch_s(*entry->fetched_at); CHECK(age >= 0s); CHECK(age < 5s); + CHECK_FALSE(entry->nak_at.has_value()); // The stored pubkeys must match those from the remote's active account key. auto [expected_x25519, expected_mlkem768] = TestHelper::active_account_pubkeys(*remote); - CHECK(entry->pubkey_x25519 == expected_x25519); - CHECK(entry->pubkey_mlkem768 == expected_mlkem768); + REQUIRE(entry->pubkey_x25519.has_value()); + REQUIRE(entry->pubkey_mlkem768.has_value()); + CHECK(*entry->pubkey_x25519 == expected_x25519); + CHECK(*entry->pubkey_mlkem768 == expected_mlkem768); } SECTION("Skips fetch when cache is fresh") { @@ -106,6 +121,106 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] } } +TEST_CASE("prefetch_pfs_keys NAK handling", "[core][pfs]") { + TempCore c; + auto mock_net = std::make_shared(); + c->set_network(mock_net); + + TempCore remote; + auto session_id_span = remote->globals.session_id(); + std::array sid; + std::ranges::copy(session_id_span, sid.begin()); + + // Helper: fire the pending request with an empty-messages response (NAK condition). + auto fire_nak = [&] { + REQUIRE(mock_net->sent_requests.size() == 1); + mock_net->sent_requests[0].callback(true, false, 200, {}, make_empty_response().dump()); + mock_net->sent_requests.clear(); + }; + + SECTION("Records NAK when fetch succeeds but returns no keys") { + c->prefetch_pfs_keys(sid); + fire_nak(); + + auto entry = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(entry.has_value()); + CHECK_FALSE(entry->fetched_at.has_value()); + REQUIRE(entry->nak_at.has_value()); + auto nak_age = clock_now_s() - from_epoch_s(*entry->nak_at); + CHECK(nak_age >= 0s); + CHECK(nak_age < 5s); + CHECK_FALSE(entry->pubkey_x25519.has_value()); + CHECK_FALSE(entry->pubkey_mlkem768.has_value()); + } + + SECTION("NAK suppresses re-fetch within PFS_KEY_NAK_DURATION") { + c->prefetch_pfs_keys(sid); + fire_nak(); + + // Should not issue another request while the NAK is fresh. + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.empty()); + } + + SECTION("NAK allows re-fetch after PFS_KEY_NAK_DURATION expires") { + c->prefetch_pfs_keys(sid); + fire_nak(); + + ScopedClockOffset expired{core::Core::PFS_KEY_NAK_DURATION + 1s}; + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.size() == 1); + } + + SECTION("NAK does not overwrite an existing valid entry") { + // Populate the cache with a valid entry. + auto remote_msg = remote->devices.build_account_pubkey_message(); + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(remote_msg).dump()); + mock_net->sent_requests.clear(); + + auto before = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(before.has_value()); + REQUIRE(before->pubkey_x25519.has_value()); + + // Advance clock to make the entry stale, then fire a re-fetch that returns nothing. + ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + c->prefetch_pfs_keys(sid); + fire_nak(); + + // Valid pubkeys must still be present; nak_at is also set. + auto after = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(after.has_value()); + CHECK(after->pubkey_x25519 == before->pubkey_x25519); + CHECK(after->pubkey_mlkem768 == before->pubkey_mlkem768); + CHECK(after->fetched_at == before->fetched_at); + REQUIRE(after->nak_at.has_value()); + } + + SECTION("Stale valid entry is not gated by a concurrent NAK") { + // Populate the cache with a valid entry. + auto remote_msg = remote->devices.build_account_pubkey_message(); + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + mock_net->sent_requests[0].callback( + true, false, 200, {}, make_pubkey_response(remote_msg).dump()); + mock_net->sent_requests.clear(); + + // Make stale and fire a NAK. + { + ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + c->prefetch_pfs_keys(sid); + fire_nak(); + + // With a fresh NAK and stale valid entry, a new call should still re-fetch because + // the NAK only gates the no-valid-keys path. + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.size() == 1); + } + } +} + TEST_CASE("prefetch_pfs_keys handles malformed responses gracefully", "[core][pfs]") { TempCore c; auto mock_net = std::make_shared(); @@ -116,38 +231,42 @@ TEST_CASE("prefetch_pfs_keys handles malformed responses gracefully", "[core][pf std::array sid; std::ranges::copy(session_id_span, sid.begin()); - SECTION("Garbage bt-dict data: no cache entry written") { + SECTION("Garbage bt-dict data: NAK written, no valid pubkeys stored") { c->prefetch_pfs_keys(sid); REQUIRE(mock_net->sent_requests.size() == 1); - nlohmann::json bad_response; - bad_response["results"] = nlohmann::json::array(); + nlohmann::json bad_resp; + bad_resp["results"] = nlohmann::json::array(); nlohmann::json res_item; res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); res_item["messages"] = nlohmann::json::array(); nlohmann::json msg_item; - // "not a bt-dict" is valid base64 but not a valid bt-dict. msg_item["data"] = oxenc::to_base64("not a bt-dict"); res_item["messages"].push_back(msg_item); - bad_response["results"].push_back(res_item); + bad_resp["results"].push_back(res_item); - // Must not throw or crash; cache must remain empty. - mock_net->sent_requests[0].callback(true, false, 200, {}, bad_response.dump()); - CHECK_FALSE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + mock_net->sent_requests[0].callback(true, false, 200, {}, bad_resp.dump()); + auto entry = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(entry.has_value()); + CHECK(entry->nak_at.has_value()); + CHECK_FALSE(entry->pubkey_x25519.has_value()); } - SECTION("Bad signature: no cache entry written") { + SECTION("Bad signature: NAK written, no valid pubkeys stored") { c->prefetch_pfs_keys(sid); REQUIRE(mock_net->sent_requests.size() == 1); - // Build a valid-looking message but sign it with the wrong key (our own account). + // A message signed with the wrong key (our own account instead of the remote's). auto wrong_msg = c->devices.build_account_pubkey_message(); mock_net->sent_requests[0].callback( true, false, 200, {}, make_pubkey_response(wrong_msg).dump()); - CHECK_FALSE(TestHelper::pfs_cache_entry(*c, sid).has_value()); + auto entry = TestHelper::pfs_cache_entry(*c, sid); + REQUIRE(entry.has_value()); + CHECK(entry->nak_at.has_value()); + CHECK_FALSE(entry->pubkey_x25519.has_value()); } - SECTION("Network failure: no cache entry written") { + SECTION("Network failure: nothing written") { c->prefetch_pfs_keys(sid); REQUIRE(mock_net->sent_requests.size() == 1); From 8f0b22473943156fc12f4fc84648bddbd996eb8c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 21 Mar 2026 00:25:31 -0300 Subject: [PATCH 047/193] improve ScopedClockOffset variable names --- tests/test_core_devices.cpp | 4 ++-- tests/test_pfs_key_cache.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_core_devices.cpp b/tests/test_core_devices.cpp index 9d49bed95..2aae98c7a 100644 --- a/tests/test_core_devices.cpp +++ b/tests/test_core_devices.cpp @@ -249,7 +249,7 @@ TEST_CASE("Devices - account keys", "[core][devices]") { SECTION("rotate_account_keys produces a distinct key: same timestamp, seed tiebreak") { // Snap the adjusted clock to the start of the next second so both key-creation calls // land in the same second with no risk of spanning a second boundary. - ScopedClockOffset pin{(clock_now_s() + 1s) - std::chrono::system_clock::now()}; + ScopedClockOffset pin_to_next_second{(clock_now_s() + 1s) - std::chrono::system_clock::now()}; c->devices.active_account_keys(); // ensure initial key exists at pinned second c->devices.rotate_account_keys(); // new key created at same second @@ -283,7 +283,7 @@ TEST_CASE("Devices - account keys", "[core][devices]") { } // Advance clock past retention window: old rotated key should be pruned - ScopedClockOffset adv{Devices::ACCOUNT_KEY_RETENTION + 1s}; + ScopedClockOffset advance_past_retention{Devices::ACCOUNT_KEY_RETENTION + 1s}; auto keys = c->devices.active_account_keys(); CHECK(keys.size() == 1); CHECK_FALSE(keys.front().rotated.has_value()); diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp index b783d1f35..7a42c4a14 100644 --- a/tests/test_pfs_key_cache.cpp +++ b/tests/test_pfs_key_cache.cpp @@ -115,7 +115,7 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] mock_net->sent_requests.clear(); // Advance clock past the fresh threshold. - ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + ScopedClockOffset advance_past_fresh{core::Core::PFS_KEY_FRESH_DURATION + 1s}; c->prefetch_pfs_keys(sid); CHECK(mock_net->sent_requests.size() == 1); } @@ -166,7 +166,7 @@ TEST_CASE("prefetch_pfs_keys NAK handling", "[core][pfs]") { c->prefetch_pfs_keys(sid); fire_nak(); - ScopedClockOffset expired{core::Core::PFS_KEY_NAK_DURATION + 1s}; + ScopedClockOffset advance_past_nak_expiry{core::Core::PFS_KEY_NAK_DURATION + 1s}; c->prefetch_pfs_keys(sid); CHECK(mock_net->sent_requests.size() == 1); } @@ -185,7 +185,7 @@ TEST_CASE("prefetch_pfs_keys NAK handling", "[core][pfs]") { REQUIRE(before->pubkey_x25519.has_value()); // Advance clock to make the entry stale, then fire a re-fetch that returns nothing. - ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + ScopedClockOffset advance_past_fresh{core::Core::PFS_KEY_FRESH_DURATION + 1s}; c->prefetch_pfs_keys(sid); fire_nak(); @@ -209,7 +209,7 @@ TEST_CASE("prefetch_pfs_keys NAK handling", "[core][pfs]") { // Make stale and fire a NAK. { - ScopedClockOffset stale{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + ScopedClockOffset advance_past_fresh{core::Core::PFS_KEY_FRESH_DURATION + 1s}; c->prefetch_pfs_keys(sid); fire_nak(); From 94a124d53569f25b3b5da21eb2d023de166dafa5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 23 Mar 2026 12:59:02 -0300 Subject: [PATCH 048/193] Add callback + status returns for remote PFS key prefetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows a client to determine the status so that it could have a "✅" for good keys, "⚠" for no PFS keys, "❌" for failure, and "…" when waiting for the fetch callback. --- include/session/core.hpp | 21 +++++++++-------- include/session/core/callbacks.hpp | 28 ++++++++++++++++++++++ src/core.cpp | 37 ++++++++++++++++++++++++++---- tests/test_core_devices.cpp | 3 ++- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index 6c2b64522..50c0c0c69 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -266,16 +266,19 @@ class Core { /// How long a NAK (successful fetch that returned no keys) suppresses re-fetching. static constexpr auto PFS_KEY_NAK_DURATION = 1h; - /// Initiates a background fetch of the X25519 and ML-KEM-768 account public keys for the - /// given remote session_id (33-byte 0x05-prefixed X25519 pubkey), caching the result in the - /// pfs_key_cache table. + /// Checks and/or initiates a background fetch of the X25519 and ML-KEM-768 account public keys + /// for the given remote session_id (33-byte 0x05-prefixed X25519 pubkey), caching the result + /// in the pfs_key_cache table. /// - /// - If the cached entry is less than PFS_KEY_FRESH_DURATION (24h) old, does nothing. - /// - If the entry is stale (24–48h old) or absent, initiates a network fetch via the network - /// object (if present). Stale entries remain usable as a fallback until they expire. - /// - Entries older than PFS_KEY_EXPIRY_DURATION (48h) are overwritten on next successful - /// fetch. - void prefetch_pfs_keys(std::span session_id); + /// Returns a PfsKeyStatus value describing the current cache state: + /// - fresh -- cached key is less than PFS_KEY_FRESH_DURATION (24h) old; no fetch initiated. + /// - stale -- cached key is 24–48h old; a background re-fetch has been initiated and the + /// pfs_keys_fetched callback will fire when it completes. + /// - fetching -- no usable key is cached; a background fetch has been initiated and the + /// pfs_keys_fetched callback will fire when it completes. + /// - nak -- a recent fetch returned no keys; re-fetching is suppressed for + /// PFS_KEY_NAK_DURATION (1h). + PfsKeyStatus prefetch_pfs_keys(std::span session_id); /// Returns the optional network interface, if set. const std::shared_ptr& network() const { return _network; } diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 4367c3a28..f09a4e45f 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -8,6 +8,24 @@ namespace session::core { class Core; +/// Return value of prefetch_pfs_keys() describing the current cache state at the time of the call. +enum class PfsKeyStatus { + fresh, ///< A fresh cached key exists; no fetch was initiated + stale, ///< A usable but stale key exists; a background re-fetch was initiated. + ///< The pfs_keys_fetched callback will fire when the fetch completes. + fetching, ///< No usable key is cached; a background fetch was initiated. + ///< The pfs_keys_fetched callback will fire when the fetch completes. + nak, ///< An unexpired NAK suppresses fetching; no usable key exists +}; + +/// Result passed to the pfs_keys_fetched callback when a background fetch completes. +enum class PfsKeyFetch { + new_key, ///< A key was retrieved and stored (new or changed from the previous cache entry) + unchanged, ///< Keys were retrieved but match what was already cached + not_found, ///< The fetch succeeded but the remote account pubkey namespace held no valid keys + failed, ///< The network request failed (swarm lookup or send_request) +}; + /// Struct holding application callbacks to fire when libsession Core events happen to allow the /// Core object to fire into the application front-end. struct callbacks { @@ -76,6 +94,16 @@ struct callbacks { /// Callback invoked when *this* device has been confirmed removed from the account (typically /// from another device) from an incoming device group update. std::function device_self_removed; + + /// Callback invoked when a background PFS key fetch initiated by prefetch_pfs_keys() completes. + /// Not invoked for cache hits or NAK suppressions (i.e. only fires when prefetch_pfs_keys() + /// returns stale or fetching). + /// + /// Parameters: + /// - session_id -- 33-byte session ID (0x05 prefix + X25519 pubkey) of the remote user + /// - result -- the outcome of the fetch: new_key, unchanged, not_found, or failed + std::function session_id, PfsKeyFetch result)> + pfs_keys_fetched; }; } // namespace session::core diff --git a/src/core.cpp b/src/core.cpp index b984fcac8..eabf6205b 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -229,7 +229,7 @@ ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash }); } -void Core::prefetch_pfs_keys(std::span session_id) { +PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) { auto net = _network; if (!net) throw std::logic_error{"prefetch_pfs_keys called without a network object"}; @@ -240,6 +240,8 @@ void Core::prefetch_pfs_keys(std::span session_id) { std::ranges::copy(session_id, sid.begin()); // Skip the fetch if the cached entry is still fresh, or a recent NAK suppresses retrying. + // Otherwise determine whether we have a stale (but usable) key or no key at all. + auto status = PfsKeyStatus::fetching; { auto conn = db.conn(); auto sid_hex = oxenc::to_hex(session_id.begin(), session_id.end()); @@ -256,13 +258,14 @@ void Core::prefetch_pfs_keys(std::span session_id) { "skipping", sid_hex, age); - return; + return PfsKeyStatus::fresh; } log::debug( cat, "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", sid_hex, age); + status = PfsKeyStatus::stale; } else if (nak_at) { auto age = clock_now_s() - from_epoch_s(*nak_at); if (age < PFS_KEY_NAK_DURATION) { @@ -271,7 +274,7 @@ void Core::prefetch_pfs_keys(std::span session_id) { "prefetch_pfs_keys: recent NAK for {} ({} old), skipping", sid_hex, age); - return; + return PfsKeyStatus::nak; } log::debug( cat, @@ -303,6 +306,8 @@ void Core::prefetch_pfs_keys(std::span session_id) { x25519_pub, false, [this, net, sid = std::move(sid), req_body](auto, auto swarm) { if (swarm.empty()) { log::debug(cat, "prefetch_pfs_keys: get_swarm returned empty swarm"); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); return; } @@ -322,6 +327,8 @@ void Core::prefetch_pfs_keys(std::span session_id) { std::optional body) { if (!success || !body) { log::debug(cat, "prefetch_pfs_keys: request failed"); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); return; } @@ -423,10 +430,25 @@ ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at )", sid, now_s); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); return; } - db.conn().prepared_exec( + auto conn = db.conn(); + SQLite::Transaction tx{conn.sql}; + + bool is_unchanged = conn.prepared_maybe_get( + "SELECT 1 FROM pfs_key_cache" + " WHERE session_id = ?" + " AND pubkey_x25519 = ? AND " + "pubkey_mlkem768 = ?", + sid, + *pk_x25519, + *pk_mlkem768) + .has_value(); + + conn.prepared_exec( R"( INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) VALUES (?, ?, NULL, ?, ?) @@ -439,6 +461,12 @@ ON CONFLICT(session_id) DO UPDATE SET now_s, *pk_x25519, *pk_mlkem768); + tx.commit(); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched( + sid, + is_unchanged ? PfsKeyFetch::unchanged + : PfsKeyFetch::new_key); } catch (const std::exception& e) { log::warning( cat, @@ -447,6 +475,7 @@ ON CONFLICT(session_id) DO UPDATE SET } }); }); + return status; } void Core::receive_messages( diff --git a/tests/test_core_devices.cpp b/tests/test_core_devices.cpp index 2aae98c7a..f62c4ec04 100644 --- a/tests/test_core_devices.cpp +++ b/tests/test_core_devices.cpp @@ -249,7 +249,8 @@ TEST_CASE("Devices - account keys", "[core][devices]") { SECTION("rotate_account_keys produces a distinct key: same timestamp, seed tiebreak") { // Snap the adjusted clock to the start of the next second so both key-creation calls // land in the same second with no risk of spanning a second boundary. - ScopedClockOffset pin_to_next_second{(clock_now_s() + 1s) - std::chrono::system_clock::now()}; + ScopedClockOffset pin_to_next_second{ + (clock_now_s() + 1s) - std::chrono::system_clock::now()}; c->devices.active_account_keys(); // ensure initial key exists at pinned second c->devices.rotate_account_keys(); // new key created at same second From d59771205c9e9dbd407f1d6d984ea90a36238691 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 23 Mar 2026 22:18:21 -0300 Subject: [PATCH 049/193] Switch to SHAKE256; improve SHAKE256 API The code was using TurboSHAKE256, based on libsodium suggestions, but after thinking about it I think I prefer SHAKE256 just because it is (moderately) more common and standardized. This switches the existing TurboSHAKE256 uses to SHAKE256, and uses defined domain string prefixes (instead of the TurboSHAKE256 single byte domain separator, which regular shake doesn't support). It also adds a `hash::shake256` API into session/hash.hpp that is vaguely similar in principle to the hash::blake2b(...) API, so that you can write: hash::shake256(input1, another, more, stuff)(x, y, z); to hash the first four arguments, and then squeeze the output into (pre-sized) x, y, and z outputs. --- include/session/hash.hpp | 118 +++++++++++++++++++++++++++++---------- src/core/devices.cpp | 26 ++++----- src/hash.cpp | 5 ++ 3 files changed, 105 insertions(+), 44 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 6eab27300..bd43158df 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -53,16 +54,21 @@ concept ByteContainer = template concept HashInput = ByteContainer || oxenc::endian_swappable_integer; -/// API: hash/update_all -/// -/// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte -/// containers *or* integer values and updates the hash state with them, in argument order. Integer -/// values are always written as raw bytes in little-endian encoding (i.e. they will be byte-swapped -/// if necessary). -template - requires(sizeof...(T) > 0) -void update_all(crypto_generichash_blake2b_state& st, const T&... args) { - auto make_hashable = [](const U& val) { +namespace detail { + + template + std::integral_constant extract_extent(const std::array&); + template + std::integral_constant extract_extent(std::span); + template + std::integral_constant extract_extent(const T (&)[N]); + std::integral_constant extract_extent(...); + + template + constexpr size_t container_extent_v = decltype(extract_extent(std::declval()))::value; + + template + auto make_hashable(const U& val) { if constexpr (ByteContainer) return std::span{ reinterpret_cast(std::ranges::data(val)), @@ -74,33 +80,33 @@ void update_all(crypto_generichash_blake2b_state& st, const T&... args) { oxenc::write_big_as_host(swapped.data(), val); return swapped; } - }; + } +} // namespace detail + +/// API: hash/update_all +/// +/// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte +/// containers *or* integer values and updates the hash state with them, in argument order. Integer +/// values are always written as raw bytes in little-endian encoding (i.e. they will be byte-swapped +/// if necessary). +template + requires(sizeof...(T) > 0) +void update_all(crypto_generichash_blake2b_state& st, const T&... args) { auto update_hash = [&st](std::span arg) { crypto_generichash_blake2b_update(&st, arg.data(), arg.size()); }; - (update_hash(make_hashable(args)), ...); + (update_hash(detail::make_hashable(args)), ...); } -namespace detail { - - template - std::integral_constant extract_extent(const std::array&); - template - std::integral_constant extract_extent(std::span); - template - std::integral_constant extract_extent(const T (&)[N]); - std::integral_constant extract_extent(...); - - template - constexpr size_t container_extent_v = decltype(extract_extent(std::declval()))::value; -} // namespace detail - +/// Concept for a fixed-size, writable byte container — the basic requirement for any hash output. template -concept Blake2BOutputContainer = +concept HashOutputContainer = std::ranges::contiguous_range && !std::is_const_v> && oxenc::basic_char> && - detail::container_extent_v != std::dynamic_extent && - detail::container_extent_v >= 1 && detail::container_extent_v <= 64; + detail::container_extent_v != std::dynamic_extent && detail::container_extent_v >= 1; + +template +concept Blake2BOutputContainer = HashOutputContainer && detail::container_extent_v <= 64; template concept Blake2BKey = @@ -183,6 +189,60 @@ void blake2b_pers(Out& out, std::span pers, const T&... return blake2b_key_pers(out, nullkey, pers, args...); } +/// API: hash/shake256 +/// +/// SHAKE256 XOF hasher/squeezer. Construct with any number of contiguous byte containers or +/// integer values to absorb their concatenation, then call operator() with one or more fixed-size +/// output containers to squeeze output. Multiple operator() calls squeeze sequentially. Integer +/// values are absorbed as their little-endian (fixed-size) byte representation. +/// +/// Unlike blake2b, SHAKE256 has no key or personalisation mechanism; callers achieve domain +/// separation by simply prepending a domain string as the first argument. +/// +/// The internal keccak state is zeroed on destruction. +/// +/// Example: +/// +/// // Squeeze two outputs in one call: +/// hash::shake256("SessionMyKey"_uc, seed)(out_a, out_b); +/// +/// // Or squeeze incrementally: +/// hash::shake256 sq{"SessionMyKey"_uc, seed}; +/// sq(out_a); +/// sq(out_b); +/// +struct [[nodiscard]] shake256 { + crypto_xof_shake256_state st; + + template + requires(sizeof...(T) > 0) + explicit shake256(const T&... args) { + crypto_xof_shake256_init(&st); + auto update = [this](std::span arg) { + crypto_xof_shake256_update(&st, arg.data(), arg.size()); + }; + (update(detail::make_hashable(args)), ...); + } + + ~shake256(); + + shake256(const shake256&) = delete; + shake256& operator=(const shake256&) = delete; + shake256(shake256&&) = delete; + shake256& operator=(shake256&&) = delete; + + template + requires(sizeof...(Outs) > 0) + shake256& operator()(Outs&... outs) { + (crypto_xof_shake256_squeeze( + &st, + reinterpret_cast(std::ranges::data(outs)), + std::ranges::size(outs)), + ...); + return *this; + } +}; + // Helper callable usable with unordered_map and similar to hash an array of chars by simply copying // the first sizeof(size_t) bytes, suitable for use with pre-hashed values. struct identity_hasher { diff --git a/src/core/devices.cpp b/src/core/devices.cpp index a68cfce38..e7c827ea1 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -35,6 +34,7 @@ namespace session::core { using namespace oxen::log::literals; +using namespace session::literals; using namespace std::literals; namespace log = oxen::log; @@ -57,14 +57,14 @@ std::string Devices::device_id() const { } template -consteval unsigned char KEY_DOMAIN() = delete; +consteval auto KEY_DOMAIN() = delete; template <> -consteval unsigned char KEY_DOMAIN() { - return static_cast('D'); +consteval auto KEY_DOMAIN() { + return "SessionDeviceKeys"_uc; } template <> -consteval unsigned char KEY_DOMAIN() { - return static_cast('A'); +consteval auto KEY_DOMAIN() { + return "SessionAccountKeys"_uc; } template Keys> @@ -72,18 +72,14 @@ static Keys keys_from_seed(std::span seed) { Keys keys; auto& [x_sec, x_pub, ml_sec, ml_pub] = static_cast(keys); - crypto_xof_turboshake256_state st; - crypto_xof_turboshake256_init_with_domain(&st, KEY_DOMAIN()); - crypto_xof_turboshake256_update( - &st, reinterpret_cast(seed.data()), seed.size()); - crypto_xof_turboshake256_squeeze(&st, x_sec.data(), x_sec.size()); - crypto_scalarmult_curve25519_base(x_pub.data(), x_sec.data()); - static_assert(MLKEM768_PUBLICKEYBYTES == sizeof(ml_pub)); static_assert(MLKEM768_SECRETKEYBYTES == sizeof(ml_sec)); + // Use SHAKE256 to expand the seed into separate X25519 and MLKEM-768 seeds. Domain + // separation is achieved by prepending the domain string before the seed. cleared_array ml_seed; - crypto_xof_turboshake256_squeeze(&st, ml_seed.data(), ml_seed.size()); + hash::shake256(KEY_DOMAIN(), seed)(x_sec, ml_seed); + crypto_scalarmult_curve25519_base(x_pub.data(), x_sec.data()); if (0 != sr_mlkem768_keypair_derand(ml_pub.data(), ml_sec.data(), ml_seed.data())) throw std::runtime_error{"ML-KEM-768 keygen failed!"}; @@ -117,7 +113,7 @@ std::string format_as(const Keys& k) { } Devices::DeviceKeys Devices::rotate_device_keys() { - // We store just one single seed value, then use TurboSHAKE256 to expand it into separate X25519 + // We store just one single seed value, then use SHAKE256 to expand it into separate X25519 // (32B) and MLKEM-768 (64B) seeds. cleared_b32 seed; random::fill(seed); diff --git a/src/hash.cpp b/src/hash.cpp index b698f6b31..4e6d467a3 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -1,12 +1,17 @@ #include "session/hash.hpp" #include +#include #include "session/export.h" #include "session/util.hpp" namespace session::hash { +shake256::~shake256() { + sodium_memzero(&st, sizeof(st)); +} + void hash( std::span hash, std::span msg, From 551a7b53a3ca2627be371588d671aef3dd430f12 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:07:47 -0300 Subject: [PATCH 050/193] Add SHA3-256 hasher libsodium 1.0.21 doesn't yet provide a sha3 interface, but SHAKE256 with a different prefix *is* SHA3-256, so we can use that. Includes tests with known test hash verifications. --- include/session/hash.hpp | 52 +++++++++++++++++++++++++++++---- src/hash.cpp | 5 ---- tests/test_hash.cpp | 63 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index bd43158df..3196cb2eb 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,20 @@ namespace detail { return swapped; } } + // Initializes a SHAKE-256 (or SHA3-256) keccak state with the given domain suffix byte and + // absorbs all of `args` into it. The domain byte distinguishes the hash function: + // - 0x1F = SHAKE-256 (crypto_xof_shake256_DOMAIN_STANDARD) + // - 0x06 = SHA3-256 + // See the sha3_256 API doc comment for the explanation of why this works. + template + requires(sizeof...(T) > 0) + void keccak_absorb(crypto_xof_shake256_state& st, unsigned char domain, const T&... args) { + crypto_xof_shake256_init_with_domain(&st, domain); + auto update = [&st](std::span arg) { + crypto_xof_shake256_update(&st, arg.data(), arg.size()); + }; + (update(make_hashable(args)), ...); + } } // namespace detail /// API: hash/update_all @@ -217,14 +232,10 @@ struct [[nodiscard]] shake256 { template requires(sizeof...(T) > 0) explicit shake256(const T&... args) { - crypto_xof_shake256_init(&st); - auto update = [this](std::span arg) { - crypto_xof_shake256_update(&st, arg.data(), arg.size()); - }; - (update(detail::make_hashable(args)), ...); + detail::keccak_absorb(st, crypto_xof_shake256_DOMAIN_STANDARD, args...); } - ~shake256(); + ~shake256() { sodium_memzero(&st, sizeof(st)); } shake256(const shake256&) = delete; shake256& operator=(const shake256&) = delete; @@ -243,6 +254,35 @@ struct [[nodiscard]] shake256 { } }; +/// API: hash/sha3_256 +/// +/// One-shot SHA3-256 (NIST FIPS 202) hasher. Takes a fixed-size 32-byte output container and any +/// number of contiguous byte containers or integer values, computes the SHA3-256 hash of their +/// concatenation (in argument order), and writes the result into the output container. Integer +/// values are hashed as their little-endian (fixed-size) byte representation. +/// +/// Implementation note: SHA3-256 and SHAKE-256 share identical Keccak-1600 sponge parameters +/// (state=1600 bits, rate=136 bytes, capacity=512 bits) and differ *only* in the domain suffix +/// byte absorbed into the state during padding before the final squeeze: +/// +/// - SHAKE-256: 0x1F (FIPS 202 §6.2 XOF suffix '11111') +/// - SHA3-256: 0x06 (FIPS 202 §6.1 hash suffix '01', plus the leading '1' of the Keccak +/// multi-rate padding '10*1', making the combined byte '0000 0110') +/// +/// Because the sponge parameters are identical, crypto_xof_shake256_init_with_domain(&st, 0x06) +/// followed by absorbing input and squeezing 32 bytes is exactly SHA3-256. This is the intended +/// use of init_with_domain, not a workaround. +/// +/// The temporary keccak state is zeroed before this function returns. +template + requires(detail::container_extent_v == 32 && sizeof...(T) > 0) +void sha3_256(Out& out, const T&... args) { + crypto_xof_shake256_state st; + detail::keccak_absorb(st, 0x06, args...); + crypto_xof_shake256_squeeze(&st, reinterpret_cast(std::ranges::data(out)), 32); + sodium_memzero(&st, sizeof(st)); +} + // Helper callable usable with unordered_map and similar to hash an array of chars by simply copying // the first sizeof(size_t) bytes, suitable for use with pre-hashed values. struct identity_hasher { diff --git a/src/hash.cpp b/src/hash.cpp index 4e6d467a3..b698f6b31 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -1,17 +1,12 @@ #include "session/hash.hpp" #include -#include #include "session/export.h" #include "session/util.hpp" namespace session::hash { -shake256::~shake256() { - sodium_memzero(&st, sizeof(st)); -} - void hash( std::span hash, std::span msg, diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index ab1e2bc15..d89786acf 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -1,3 +1,5 @@ +#include + #include #include "session/hash.h" @@ -5,6 +7,8 @@ #include "session/util.hpp" #include "utils.hpp" +using namespace session::literals; + TEST_CASE("Hash generation", "[hash][hash]") { auto hash1 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); auto hash2 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); @@ -49,3 +53,62 @@ TEST_CASE("Hash generation", "[hash][hash]") { CHECK(to_hex(hash5) == expected_hash5); CHECK(to_hex(hash6) == expected_hash6); } + +TEST_CASE("SHA3-256 and SHAKE-256 known-answer tests", "[hash][sha3_256][shake256]") { + // This test case serves two purposes: + // 1. Verify SHA3-256 against NIST FIPS 202 known-answer test vectors. + // 2. Verify SHAKE-256 against NIST FIPS 202 known-answer test vectors. + // 3. Confirm that SHA3-256 and SHAKE-256 produce different output on identical input, + // verifying that the domain suffix byte (0x06 vs 0x1F) is actually applied. + // + // SHA3-256 KATs: + // https://csrc.nist.gov/csrc/media/projects/cryptographic-algorithm-validation-program/documents/sha3/sha-3bittestvectors.zip + // SHAKE-256 KATs: NIST FIPS 202, Appendix A / CAVS test data + + using session::hash::sha3_256; + using session::hash::shake256; + + std::array sha3_out, shake_out; + + // --- SHA3-256 NIST vectors --- + + // Empty input + sha3_256(sha3_out, ""_uc); + CHECK(oxenc::to_hex(sha3_out) == + "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"); + + // "abc" (24 bits) + sha3_256(sha3_out, "abc"_uc); + CHECK(oxenc::to_hex(sha3_out) == + "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); + + // 448-bit message + sha3_256(sha3_out, "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"_uc); + CHECK(oxenc::to_hex(sha3_out) == + "41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376"); + + // 896-bit message + sha3_256( + sha3_out, + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklm" + "nopqklmnopqrlmnopqrsmnopqrstnopqrstu"_uc); + CHECK(oxenc::to_hex(sha3_out) == + "916f6061fe879741ca6469b43971dfdb28b1a32dc36cb3254e812be27aad1d18"); + + // --- SHAKE-256 NIST vectors (32-byte output) --- + + // Empty input; first 32 bytes from FIPS 202 Appendix B.2 sample output + shake256(""_uc)(shake_out); + CHECK(oxenc::to_hex(shake_out) == + "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f"); + + // "abc" (24 bits) + shake256("abc"_uc)(shake_out); + CHECK(oxenc::to_hex(shake_out) == + "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"); + + // --- Cross-check: same input must produce different output --- + sha3_256(sha3_out, "abc"_uc); + shake256("abc"_uc)(shake_out); + CHECK(sha3_out != shake_out); +} From f9391734f7da081b2f9412ab24db46fb740f8350 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:10:14 -0300 Subject: [PATCH 051/193] Abstract 32-or-64 seed input --- src/session_encrypt.cpp | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index e1ebb7645..7a10b245f 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -64,6 +64,22 @@ namespace detail { } // namespace detail +// Expands an Ed25519 privkey from a 32-byte seed if necessary, or passes through a 64-byte key. +// Returns a (span, storage) pair where the span is always the full 64-byte key and storage is +// non-null only when expansion was needed. Moving the returned pair is safe because storage is +// heap-allocated (unique_ptr), so the heap address — and thus the span — remain valid after a move. +static std::pair, std::unique_ptr> +expand_ed25519_privkey(std::span privkey) { + if (privkey.size() == 64) + return {std::span{privkey.data(), 64}, nullptr}; + if (privkey.size() != 32) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + auto buf = std::make_unique(); + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair(ignore_pk.data(), buf->data(), privkey.data()); + return {std::span{buf->data(), 64}, std::move(buf)}; +} + // Version tag we prepend to encrypted-for-blinded-user messages. This is here so we can detect if // some future version changes the format (and if not even try to load it). inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; @@ -72,15 +88,8 @@ std::vector sign_for_recipient( std::span ed25519_privkey, std::span recipient_pubkey, std::span message) { - cleared_uc64 ed_sk_from_seed; - if (ed25519_privkey.size() == 32) { - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - } else if (ed25519_privkey.size() != 64) { - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - } + auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(ed25519_privkey); + ed25519_privkey = ed_sk; // If prefixed, drop it (and do this for the caller, too) so that everything after this // doesn't need to worry about whether it is prefixed or not. if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) @@ -507,15 +516,8 @@ std::pair, std::string> decrypt_incoming_session_id( std::pair, std::vector> decrypt_incoming( std::span ed25519_privkey, std::span ciphertext) { - cleared_uc64 ed_sk_from_seed; - if (ed25519_privkey.size() == 32) { - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - } else if (ed25519_privkey.size() != 64) { - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - } + auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(ed25519_privkey); + ed25519_privkey = ed_sk; cleared_uc32 x_sec; uc32 x_pub; From 6f6233b79ab128c52efb6e01d5393a29f8c642c3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:30:47 -0300 Subject: [PATCH 052/193] bump session-router --- external/session-router | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/session-router b/external/session-router index e9cfe3bea..3a899845f 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit e9cfe3beaa2d9a9169f1ba99a7025aa534a6f2d0 +Subproject commit 3a899845f56e4293bffafbc82a8e9624420ba1b2 From 7ca2b47860694375e9a63d6d4534ed0aeff6a717 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 19:21:28 -0300 Subject: [PATCH 053/193] Refactor: compile-time safe Ed25519 privkey args This refactors the code taking dynamic extent spans to enforce that only compile-time-known 32 or 64-byte values can be passed. It also adds an intermediate class to hold the span on its way as an argument, as well an auto-expander in that intermediate argument class. --- external/CMakeLists.txt | 13 ++--- include/session/ed25519.hpp | 76 ++++++++++++++++++++++++- include/session/session_encrypt.hpp | 46 +++++++-------- include/session/session_protocol.hpp | 25 ++++---- src/blinding.cpp | 4 +- src/ed25519.cpp | 22 +++---- src/session_encrypt.cpp | 85 +++++----------------------- src/session_protocol.cpp | 42 +++++++------- tests/test_ed25519.cpp | 3 +- tests/test_session_encrypt.cpp | 20 +++---- 10 files changed, 174 insertions(+), 162 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 72d7c1be3..982d2ae1d 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -24,6 +24,12 @@ endfunction() session_dep(libsodium 1.0.21) libsession_static_bundle(sessiondep::libsodium) +if(NOT TARGET oxenc::oxenc) + set(OXENC_BUILD_TESTS OFF CACHE BOOL "") + set(OXENC_BUILD_DOCS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenc 1.6.0 session-router/external/oxen-libquic/external/oxen-encoding oxenc::oxenc) +endif() + if(ENABLE_NETWORKING) if(ENABLE_NETWORKING_SROUTER) set(SROUTER_FULL OFF CACHE BOOL "") @@ -39,13 +45,6 @@ if(ENABLE_NETWORKING) endif() endif() -if(NOT TARGET oxenc::oxenc) - # The oxenc target will already exist if we load libquic above via submodule - set(OXENC_BUILD_TESTS OFF CACHE BOOL "") - set(OXENC_BUILD_DOCS OFF CACHE BOOL "") - sessiondep_or_submodule(liboxenc 1.5.0 oxen-libquic/external/oxen-encoding oxenc::oxenc) -endif() - if(NOT TARGET oxen::logging) sessiondep_or_submodule(liboxen-logging 1.2.0 session-router/external/oxen-libquic/external/oxen-logging oxen::logging) diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index 8a6de9213..743c97a2e 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -4,6 +4,78 @@ #include #include +#include "session/sodium_array.hpp" + +namespace session { + +/// A span-like type representing a fully-expanded Ed25519 private key (always 64 bytes). +/// Implicitly constructible from any fixed-extent 32- or 64-byte unsigned char spannable: +/// - 32-byte input (seed): the 64-byte key is computed via libsodium and stored internally. +/// - 64-byte input: holds a non-owning span into the caller's data — no copy or allocation. +/// +/// Non-copyable and non-moveable to avoid dangling references to internal storage. +struct Ed25519PrivKeySpan { + template + requires( + std::constructible_from, const T&> || + std::constructible_from, const T&>) + Ed25519PrivKeySpan(const T& src) { + if constexpr (std::constructible_from, const T&>) + data_ = std::span{src}.data(); + else { + expand_seed(std::span{src}); + data_ = storage_->data(); + } + } + + // Explicit constructor for runtime-known sizes (e.g. at C API boundaries). + // Throws std::invalid_argument if size is not 32 or 64. + explicit Ed25519PrivKeySpan(const unsigned char* data, size_t size) { + if (size == 64) + data_ = data; + else if (size == 32) { + expand_seed(std::span{data, 32}); + data_ = storage_->data(); + } else + throw std::invalid_argument{"Ed25519 private key must be 32 or 64 bytes"}; + } + + // Named factory aliases for the explicit constructor above, for readability at call sites. + static Ed25519PrivKeySpan from(std::span key) { + return Ed25519PrivKeySpan{key.data(), key.size()}; + } + static Ed25519PrivKeySpan from(const unsigned char* data, size_t size) { + return Ed25519PrivKeySpan{data, size}; + } + + Ed25519PrivKeySpan(const Ed25519PrivKeySpan&) = delete; + Ed25519PrivKeySpan& operator=(const Ed25519PrivKeySpan&) = delete; + Ed25519PrivKeySpan(Ed25519PrivKeySpan&&) = delete; + Ed25519PrivKeySpan& operator=(Ed25519PrivKeySpan&&) = delete; + + std::span span() const { + return std::span(data_, 64); + } + operator std::span() const { return span(); } + operator std::span() const { return span(); } + const unsigned char* data() const { return data_; } + auto begin() const { return data_; } + auto end() const { return data_ + 64; } + static constexpr size_t size() { return 64; } + // Returns the 32-byte seed (first half of the libsodium key). + std::span seed() const { return span().first<32>(); } + // Returns the 32-byte Ed25519 public key (second half of the libsodium key). + std::span pubkey() const { return span().last<32>(); } + + private: + void expand_seed(std::span seed); + + const unsigned char* data_ = nullptr; + std::optional storage_; +}; + +} // namespace session + namespace session::ed25519 { /// Generates a random Ed25519 key pair @@ -32,13 +104,13 @@ std::array seed_for_ed_privkey(std::span /// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. /// /// Inputs: -/// - `ed25519_privkey` -- the libsodium-style secret key, 64 bytes. +/// - `ed25519_privkey` -- the Ed25519 private key; accepts a 32-byte seed or 64-byte libsodium key. /// - `msg` -- the data to generate a signature for. /// /// Outputs: /// - The ed25519 signature std::vector sign( - std::span ed25519_privkey, std::span msg); + const Ed25519PrivKeySpan& ed25519_privkey, std::span msg); /// API: ed25519/verify /// diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 7635c77e2..29e579efb 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -7,6 +7,8 @@ #include #include +#include "ed25519.hpp" + // Helper functions for the "Session Protocol" encryption mechanism. This is the encryption used // for DMs sent from one Session user to another. // @@ -53,9 +55,8 @@ namespace session { /// Performs session protocol encryption, typically for a DM sent between Session users. /// /// Inputs: -/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be -/// passed as a 32-byte seed, but the 64-byte value is preferrable (to avoid needing to -/// recompute the public key from the seed). +/// - `ed25519_privkey` -- the Ed25519 private key of the sender; accepts a 32-byte seed or +/// 64-byte libsodium key (the latter avoids recomputing the public key from the seed). /// - `recipient_pubkey` -- the recipient X25519 pubkey, either as a 0x05-prefixed session ID /// (33 bytes) or an unprefixed pubkey (32 bytes). /// - `message` -- the message to encrypt for the recipient. @@ -64,7 +65,7 @@ namespace session { /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) std::vector encrypt_for_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message); @@ -85,7 +86,7 @@ std::vector encrypt_for_recipient( /// Outputs: /// Identical to `encrypt_for_recipient`. std::vector encrypt_for_recipient_deterministic( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message); @@ -105,7 +106,7 @@ std::vector encrypt_for_recipient_deterministic( /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) std::vector encrypt_for_blinded_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span server_pk, std::span recipient_blinded_id, std::span message); @@ -163,8 +164,8 @@ static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// exhaustion attacks. /// /// Inputs: -/// - `user_ed25519_privkey` -- the private key of the user. Can be a 32-byte seed, or a 64-byte -/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// - `user_ed25519_privkey` -- the Ed25519 private key of the user; accepts a 32-byte seed or +/// 64-byte libsodium key (the latter avoids recomputing the public key from the seed). /// - `group_ed25519_pubkey` -- The 32 byte public key of the group /// - group_enc_key -- The group's encryption key (32 bytes or 64-byte libsodium key) for groups v2 /// messages, typically the latest key for the group (e.g., Keys::group_enc_key). @@ -178,7 +179,7 @@ static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// Outputs: /// - `ciphertext` -- the encrypted, etc. value to send to the swarm std::vector encrypt_for_group( - std::span user_ed25519_privkey, + const Ed25519PrivKeySpan& user_ed25519_privkey, std::span group_ed25519_pubkey, std::span group_enc_key, std::span plaintext, @@ -204,12 +205,13 @@ std::vector encrypt_for_group( /// signed message. /// /// Inputs: -/// - `ed25519_privkey` -- the seed (32 bytes) or secret key (64 bytes) of the sender +/// - `ed25519_privkey` -- the Ed25519 private key of the sender; accepts a 32-byte seed or +/// 64-byte libsodium key. /// - `recipient_pubkey` -- the recipient X25519 pubkey, which may or may not be prefixed with the /// 0x05 session id prefix (33 bytes if prefixed, 32 if not prefixed). /// - `message` -- the message to embed and sign. std::vector sign_for_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message); @@ -219,9 +221,8 @@ std::vector sign_for_recipient( /// pubkey, and verifies that the sender Ed25519 signature on the message. /// /// Inputs: -/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte -/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey -/// from the seed. +/// - `ed25519_privkey` -- the Ed25519 private key of the recipient; accepts a 32-byte seed or +/// 64-byte libsodium key. /// - `ciphertext` -- the encrypted data /// /// Outputs: @@ -230,7 +231,7 @@ std::vector sign_for_recipient( /// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on /// error. std::pair, std::vector> decrypt_incoming( - std::span ed25519_privkey, std::span ciphertext); + const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -261,9 +262,8 @@ std::pair, std::vector> decrypt_incomi /// signature on the message and converts the extracted sender's Ed25519 pubkey into a session ID. /// /// Inputs: -/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte -/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey -/// from the seed. +/// - `ed25519_privkey` -- the Ed25519 private key of the recipient; accepts a 32-byte seed or +/// 64-byte libsodium key. /// - `ciphertext` -- the encrypted data /// /// Outputs: @@ -271,7 +271,7 @@ std::pair, std::vector> decrypt_incomi /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. std::pair, std::string> decrypt_incoming_session_id( - std::span ed25519_privkey, std::span ciphertext); + const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -301,10 +301,8 @@ std::pair, std::string> decrypt_incoming_session_id( /// the `ciphertext` is an outgoing message and decrypts it as such. /// /// Inputs: -/// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a -/// 64-byte -/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey -/// from the seed. +/// - `ed25519_privkey` -- the Ed25519 private key of the receiver; accepts a 32-byte seed or +/// 64-byte libsodium key. /// - `server_pk` -- the public key of the community server to route the blinded message through /// (32 bytes). /// - `sender_id` -- the blinded id of the sender including the blinding prefix (33 bytes), @@ -318,7 +316,7 @@ std::pair, std::string> decrypt_incoming_session_id( /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. std::pair, std::string> decrypt_from_blinded_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span server_pk, std::span sender_id, std::span recipient_id, diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 1ff2e0939..bebb2b15d 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -406,8 +407,8 @@ std::vector pad_message(std::span payload); /// Inputs: /// - plaintext -- The protobuf serialized payload containing the Content to be encrypted. Must /// not be already encrypted and must not be padded. -/// - ed25519_privkey -- The sender's libsodium-style secret key (64 bytes). Can also be passed as -/// a 32-byte seed. Used to encrypt the plaintext. +/// - ed25519_privkey -- The sender's Ed25519 private key; accepts a 32-byte seed or 64-byte +/// libsodium key. Used to encrypt the plaintext. /// - sent_timestamp -- The timestamp to assign to the message envelope, in milliseconds. This /// should match the protobuf encoded Content's `sigtimestamp` in the given `plaintext`. /// - recipient_pubkey -- The recipient's Session public key (33 bytes). @@ -421,7 +422,7 @@ std::vector pad_message(std::span payload); /// (i.e: it has been protobuf encoded/wrapped if necessary). std::vector encode_for_1o1( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, std::optional> pro_rotating_ed25519_privkey); @@ -440,8 +441,8 @@ std::vector encode_for_1o1( /// Inputs: /// - plaintext -- The protobuf serialized payload containing the Content to be encrypted. Must /// not be already encrypted and must not be padded. -/// - ed25519_privkey -- The sender's libsodium-style secret key (64 bytes). Can also be passed as -/// a 32-byte seed. Used to encrypt the plaintext. +/// - ed25519_privkey -- The sender's Ed25519 private key; accepts a 32-byte seed or 64-byte +/// libsodium key. Used to encrypt the plaintext. /// - sent_timestamp -- The timestamp to assign to the message envelope, in milliseconds. /// - recipient_pubkey -- The recipient's Session public key (33 bytes). /// - community_pubkey -- The community inbox server's public key (32 bytes). @@ -455,7 +456,7 @@ std::vector encode_for_1o1( /// (i.e: it has been protobuf encoded/wrapped if necessary). std::vector encode_for_community_inbox( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, const uc32& community_pubkey, @@ -502,8 +503,8 @@ std::vector encode_for_community( /// Inputs: /// - plaintext -- The protobuf serialized payload containing the Content to be encrypted. Must /// not be already encrypted and must not be padded. -/// - ed25519_privkey -- The sender's libsodium-style secret key (64 bytes). Can also be passed as -/// a 32-byte seed. Used to encrypt the plaintext. +/// - ed25519_privkey -- The sender's Ed25519 private key; accepts a 32-byte seed or 64-byte +/// libsodium key. Used to encrypt the plaintext. /// - sent_timestamp -- The timestamp to assign to the message envelope, in milliseconds. /// - group_ed25519_pubkey -- The group's public key (33 bytes) for encryption with a 0x03 prefix /// - group_enc_key -- The group's encryption key (32 bytes) for groups v2 messages, typically the @@ -518,7 +519,7 @@ std::vector encode_for_community( /// (i.e: it has been protobuf encoded/wrapped if necessary). std::vector encode_for_group( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, @@ -543,8 +544,8 @@ std::vector encode_for_group( /// Inputs: /// - `plaintext` -- the protobuf serialised payload containing the protobuf encoded stream, /// `Content`. It must not be already be encrypted and must not be padded. -/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be -/// passed as a 32-byte seed. Used to encrypt the plaintext. +/// - `ed25519_privkey` -- the sender's Ed25519 private key; accepts a 32-byte seed or 64-byte +/// libsodium key. Null for community (unencrypted) destinations. /// - `dest` -- the extra metadata indicating the destination of the message and the necessary data /// to encrypt a message for that destination. /// @@ -553,7 +554,7 @@ std::vector encode_for_group( /// (i.e: it has been protobuf encoded/wrapped if necessary). std::vector encode_for_destination( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan* ed25519_privkey, const Destination& dest); /// API: session_protocol/decode_envelope diff --git a/src/blinding.cpp b/src/blinding.cpp index 81aa25c5e..21538d198 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -481,7 +481,7 @@ std::vector blind_version_sign_request( if (body) buf.insert(buf.end(), body->begin(), body->end()); - return ed25519::sign({sk.data(), sk.size()}, buf); + return ed25519::sign(sk, buf); } std::vector blind_version_sign( @@ -505,7 +505,7 @@ std::vector blind_version_sign( } buf.insert(buf.end(), url.begin(), url.end()); - return ed25519::sign({sk.data(), sk.size()}, buf); + return ed25519::sign(sk, buf); } bool session_id_matches_blinded_id( diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 943e2d27a..b03dce825 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -9,6 +9,16 @@ #include "session/hash.hpp" #include "session/sodium_array.hpp" +namespace session { + +void Ed25519PrivKeySpan::expand_seed(std::span seed) { + auto& buf = storage_.emplace(); + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair(ignore_pk.data(), buf.data(), seed.data()); +} + +} // namespace session + namespace session::ed25519 { std::pair, std::array> ed25519_key_pair() { @@ -47,17 +57,7 @@ std::array seed_for_ed_privkey(std::span } std::vector sign( - std::span ed25519_privkey, std::span msg) { - cleared_uc64 ed_sk_from_seed; - if (ed25519_privkey.size() == 32) { - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - } else if (ed25519_privkey.size() != 64) { - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - } - + const Ed25519PrivKeySpan& ed25519_privkey, std::span msg) { std::vector sig; sig.resize(64); diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 7a10b245f..72e12fa14 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -64,32 +64,14 @@ namespace detail { } // namespace detail -// Expands an Ed25519 privkey from a 32-byte seed if necessary, or passes through a 64-byte key. -// Returns a (span, storage) pair where the span is always the full 64-byte key and storage is -// non-null only when expansion was needed. Moving the returned pair is safe because storage is -// heap-allocated (unique_ptr), so the heap address — and thus the span — remain valid after a move. -static std::pair, std::unique_ptr> -expand_ed25519_privkey(std::span privkey) { - if (privkey.size() == 64) - return {std::span{privkey.data(), 64}, nullptr}; - if (privkey.size() != 32) - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - auto buf = std::make_unique(); - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair(ignore_pk.data(), buf->data(), privkey.data()); - return {std::span{buf->data(), 64}, std::move(buf)}; -} - // Version tag we prepend to encrypted-for-blinded-user messages. This is here so we can detect if // some future version changes the format (and if not even try to load it). inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; std::vector sign_for_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message) { - auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(ed25519_privkey); - ed25519_privkey = ed_sk; // If prefixed, drop it (and do this for the caller, too) so that everything after this // doesn't need to worry about whether it is prefixed or not. if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) @@ -122,7 +104,7 @@ std::vector sign_for_recipient( static constexpr auto BOX_HASHKEY = "SessionBoxEphemeralHashKey"_uc; std::vector encrypt_for_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message) { @@ -143,7 +125,7 @@ std::vector encrypt_for_recipient( } std::vector encrypt_for_recipient_deterministic( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, std::span message) { @@ -157,7 +139,7 @@ std::vector encrypt_for_recipient_deterministic( // keyed blake2b hash. cleared_uchars seed; hash::blake2b_key( - seed, BOX_HASHKEY, ed25519_privkey.first(32), recipient_pubkey.first(32), message); + seed, BOX_HASHKEY, ed25519_privkey.seed(), recipient_pubkey.first(32), message); cleared_uchars eph_sk; cleared_uchars eph_pk; @@ -277,12 +259,10 @@ static cleared_uc32 blinded_shared_secret( } std::vector encrypt_for_blinded_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span server_pk, std::span recipient_blinded_id, std::span message) { - if (ed25519_privkey.size() != 64 && ed25519_privkey.size() != 32) - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; if (server_pk.size() != 32) throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; if (recipient_blinded_id.size() != 33) @@ -315,15 +295,8 @@ std::vector encrypt_for_blinded_recipient( buf.insert(buf.end(), message.begin(), message.end()); // append A (pubkey) - if (ed25519_privkey.size() == 64) { - buf.insert(buf.end(), ed25519_privkey.begin() + 32, ed25519_privkey.end()); - } else { - cleared_uc64 ed_sk_from_seed; - uc32 ed_pk_buf; - crypto_sign_ed25519_seed_keypair( - ed_pk_buf.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - buf.insert(buf.end(), ed_pk_buf.begin(), ed_pk_buf.end()); - } + auto pk = ed25519_privkey.pubkey(); + buf.insert(buf.end(), pk.begin(), pk.end()); // Encrypt using xchacha20-poly1305 cleared_uchars nonce; @@ -363,7 +336,7 @@ static constexpr size_t GROUPS_ENCRYPT_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; std::vector encrypt_for_group( - std::span user_ed25519_privkey, + const Ed25519PrivKeySpan& user_ed25519_privkey, std::span group_ed25519_pubkey, std::span group_enc_key, std::span plaintext, @@ -372,21 +345,6 @@ std::vector encrypt_for_group( if (plaintext.size() > GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE) throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; - // Generate the user's pubkey if they passed in a 32 byte secret key instead of the - // libsodium-style 64 byte secret key. - cleared_uc64 user_ed25519_privkey_from_seed; - if (user_ed25519_privkey.size() == 32) { - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), - user_ed25519_privkey_from_seed.data(), - user_ed25519_privkey.data()); - user_ed25519_privkey = { - user_ed25519_privkey_from_seed.data(), user_ed25519_privkey_from_seed.size()}; - } else if (user_ed25519_privkey.size() != 64) { - throw std::invalid_argument{"Invalid user_ed25519_privkey: expected 32 or 64 bytes"}; - } - if (group_enc_key.size() != 32 && group_enc_key.size() != 64) throw std::invalid_argument{"Invalid group_enc_key: expected 32 or 64 bytes"}; if (group_ed25519_pubkey.size() != crypto_sign_ed25519_PUBLICKEYBYTES) @@ -473,7 +431,7 @@ std::vector encrypt_for_group( } std::pair, std::string> decrypt_incoming_session_id( - std::span ed25519_privkey, std::span ciphertext) { + const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(ed25519_privkey, ciphertext); // Convert the sender_ed_pk to the sender's session ID @@ -515,10 +473,7 @@ std::pair, std::string> decrypt_incoming_session_id( } std::pair, std::vector> decrypt_incoming( - std::span ed25519_privkey, std::span ciphertext) { - auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(ed25519_privkey); - ed25519_privkey = ed_sk; - + const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext) { cleared_uc32 x_sec; uc32 x_pub; crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); @@ -567,30 +522,20 @@ std::pair, std::vector> decrypt_incomi } std::pair, std::string> decrypt_from_blinded_recipient( - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::span server_pk, std::span sender_id, std::span recipient_id, std::span ciphertext) { - uc32 ed_pk_from_seed; - cleared_uc64 ed_sk_from_seed; - if (ed25519_privkey.size() == 32) { - crypto_sign_ed25519_seed_keypair( - ed_pk_from_seed.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); - ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; - } else if (ed25519_privkey.size() == 64) - std::memcpy(ed_pk_from_seed.data(), ed25519_privkey.data() + 32, 32); - else - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + auto ed_pk = ed25519_privkey.pubkey(); if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; cleared_uc32 dec_key; - auto blinded_id = recipient_id[0] == 0x25 - ? blinded25_id_from_ed(to_span(ed_pk_from_seed), server_pk) - : blinded15_id_from_ed(to_span(ed_pk_from_seed), server_pk); + auto blinded_id = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_span(ed_pk), server_pk) + : blinded15_id_from_ed(to_span(ed_pk), server_pk); if (to_string_view(sender_id) == to_string_view(blinded_id)) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); @@ -1091,7 +1036,7 @@ LIBSESSION_C_API session_encrypt_group_message session_encrypt_for_group( session_encrypt_group_message result = {}; try { std::vector result_cpp = encrypt_for_group( - {user_ed25519_privkey, user_ed25519_privkey_len}, + Ed25519PrivKeySpan::from(user_ed25519_privkey, user_ed25519_privkey_len), {group_ed25519_pubkey, group_ed25519_pubkey_len}, {group_enc_key, group_enc_key_len}, {plaintext, plaintext_len}, diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 061312344..f78d7880f 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -274,7 +274,7 @@ ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size) { std::vector encode_for_1o1( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, std::optional> pro_rotating_ed25519_privkey) { @@ -284,13 +284,12 @@ std::vector encode_for_1o1( : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - return result; + return encode_for_destination(plaintext, &ed25519_privkey, dest); } std::vector encode_for_community_inbox( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, const uc32& community_pubkey, @@ -302,8 +301,7 @@ std::vector encode_for_community_inbox( dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; dest.community_inbox_server_pubkey = community_pubkey; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - return result; + return encode_for_destination(plaintext, &ed25519_privkey, dest); } std::vector encode_for_community( @@ -313,14 +311,12 @@ std::vector encode_for_community( dest.type = DestinationType::Community; dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey : std::span{}; - std::span nil_ed25519_privkey; - std::vector result = encode_for_destination(plaintext, nil_ed25519_privkey, dest); - return result; + return encode_for_destination(plaintext, nullptr, dest); } std::vector encode_for_group( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, @@ -332,8 +328,7 @@ std::vector encode_for_group( dest.sent_timestamp_ms = sent_timestamp; dest.group_ed25519_pubkey = group_ed25519_pubkey; dest.group_enc_key = group_enc_key; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - return result; + return encode_for_destination(plaintext, &ed25519_privkey, dest); } // Interop between the C and CPP API. The C api will request malloc which writes to `ciphertext_c`. @@ -388,7 +383,7 @@ static std::span unpad_message(std::span payload) enum class UseMalloc { No, Yes }; static EncryptedForDestinationInternal encode_for_destination_internal( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan* ed25519_privkey, DestinationType dest_type, std::span dest_pro_rotating_ed25519_privkey, std::span dest_recipient_pubkey, @@ -411,10 +406,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( bool is_1o1 = dest_type == DestinationType::SyncOr1o1; bool is_community_inbox = dest_type == DestinationType::CommunityInbox; bool is_community = dest_type == DestinationType::Community; - if (!is_community) { - assert(ed25519_privkey.size() == crypto_sign_ed25519_SECRETKEYBYTES || - ed25519_privkey.size() == crypto_sign_ed25519_SEEDBYTES); - } + assert(is_community || ed25519_privkey); // Ensure the Session Pro rotating key is a 64 byte key if given cleared_uc64 pro_ed_sk_from_seed; @@ -456,7 +448,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (is_1o1) { // Encrypt the padded output std::vector padded_payload = pad_message(content); tmp_content_buffer = encrypt_for_recipient( - ed25519_privkey, dest_recipient_pubkey, padded_payload); + *ed25519_privkey, dest_recipient_pubkey, padded_payload); content = tmp_content_buffer; } @@ -504,7 +496,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( dest_group_ed25519_pubkey = dest_group_ed25519_pubkey.subspan(1); std::vector ciphertext = encrypt_for_group( - ed25519_privkey, + *ed25519_privkey, dest_group_ed25519_pubkey, dest_group_enc_key, to_span(bytes), @@ -607,7 +599,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (is_community_inbox) { std::vector ciphertext = encrypt_for_blinded_recipient( - ed25519_privkey, + *ed25519_privkey, dest_community_inbox_server_pubkey, dest_recipient_pubkey, // recipient blinded pubkey content); @@ -632,7 +624,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( std::vector encode_for_destination( std::span plaintext, - std::span ed25519_privkey, + const Ed25519PrivKeySpan* ed25519_privkey, const Destination& dest) { EncryptedForDestinationInternal result_internal = encode_for_destination_internal( @@ -1380,10 +1372,14 @@ LIBSESSION_C_API session_protocol_encoded_for_destination session_protocol_encod reinterpret_cast(dest->pro_rotating_ed25519_privkey) + dest->pro_rotating_ed25519_privkey_len); + std::optional privkey; + if (ed25519_privkey_len) + privkey.emplace( + static_cast(ed25519_privkey), ed25519_privkey_len); + EncryptedForDestinationInternal result_internal = encode_for_destination_internal( /*plaintext=*/{static_cast(plaintext), plaintext_len}, - /*ed25519_privkey=*/ - {static_cast(ed25519_privkey), ed25519_privkey_len}, + /*ed25519_privkey=*/privkey ? &*privkey : nullptr, /*dest_type=*/static_cast(dest->type), /*dest_pro_rotating_ed25519_privkey=*/dest_pro_rotating_ed25519_privkey, /*dest_recipient_pubkey=*/dest->recipient_pubkey.data, diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index e82092af1..1175d4c40 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -126,7 +126,8 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { constexpr auto ed_invalid = "010203040506070809"_hex_u; auto sig1 = session::ed25519::sign(ed_seed, to_span("hello")); - CHECK_THROWS(session::ed25519::sign(ed_invalid, to_span("hello"))); + CHECK_THROWS(session::ed25519::sign( + Ed25519PrivKeySpan::from(ed_invalid.data(), ed_invalid.size()), to_span("hello"))); auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 1ff6236e0..7b65418a5 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -67,7 +67,7 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = - encrypt_for_recipient({to_span(ed_sk).data(), 32}, sid_raw2, to_span(lorem_ipsum)); + encrypt_for_recipient(to_span(ed_sk).first<32>(), sid_raw2, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), enc.end(), @@ -242,7 +242,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_span(ed_sk).data(), 32}, + to_span(ed_sk).first<32>(), to_span(server_pk), {blind15_pk2_prefixed.data(), 33}, to_span(lorem_ipsum)); @@ -253,7 +253,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk).data(), 32}, + to_span(ed_sk).first<32>(), to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, @@ -264,7 +264,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_span(ed_sk).data(), 32}, + to_span(ed_sk).first<32>(), to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, @@ -279,7 +279,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_span(ed_sk).data(), 32}, + to_span(ed_sk).first<32>(), to_span(server_pk), {blind15_pk2_prefixed.data(), 33}, to_span(lorem_ipsum)); @@ -290,7 +290,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, + to_span(ed_sk2).first<32>(), to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, @@ -301,7 +301,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, + to_span(ed_sk2).first<32>(), to_span(server_pk), {blind15_pk_prefixed.data(), 33}, {blind15_pk2_prefixed.data(), 33}, @@ -394,7 +394,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - {to_span(ed_sk).data(), 32}, + to_span(ed_sk).first<32>(), to_span(server_pk), {blind25_pk2_prefixed.data(), 33}, to_span(lorem_ipsum)); @@ -405,7 +405,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, + to_span(ed_sk2).first<32>(), to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, @@ -416,7 +416,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto broken = enc; broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, + to_span(ed_sk2).first<32>(), to_span(server_pk), {blind25_pk_prefixed.data(), 33}, {blind25_pk2_prefixed.data(), 33}, From 7a8f1066b290a420097fc9b643e43b5409dadfb9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:22:56 -0300 Subject: [PATCH 054/193] Add live (testnet) network test script Adds a new `tests/testLive` binary that carries its own test suite that runs tests on testnet via the session networking code, and is capable of using direct, onion request, or session-router requests (the latter currently doesn't work and needs further investigation, but is out of scope of this PR). --- external/session-router | 2 +- include/session/network/session_network.hpp | 2 +- src/CMakeLists.txt | 2 + src/core.cpp | 179 ++++++++---------- src/core/devices.cpp | 1 + src/network/routing/session_router_router.cpp | 3 - src/network/transport/quic_transport.cpp | 2 +- tests/CMakeLists.txt | 16 ++ tests/live/live_utils.hpp | 130 +++++++++++++ tests/live/main.cpp | 53 ++++++ tests/live/test_pubkey_xfer.cpp | 59 ++++++ tests/live/test_swarm.cpp | 35 ++++ tests/log_setup.hpp | 40 ++++ tests/main.cpp | 27 +-- tests/test_helper.hpp | 19 ++ tests/utils.hpp | 84 ++++++++ 16 files changed, 530 insertions(+), 124 deletions(-) create mode 100644 tests/live/live_utils.hpp create mode 100644 tests/live/main.cpp create mode 100644 tests/live/test_pubkey_xfer.cpp create mode 100644 tests/live/test_swarm.cpp create mode 100644 tests/log_setup.hpp diff --git a/external/session-router b/external/session-router index 3a899845f..faa8c0e44 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit 3a899845f56e4293bffafbc82a8e9624420ba1b2 +Subproject commit faa8c0e44d1e295e05935ebfc1401275a57dc982 diff --git a/include/session/network/session_network.hpp b/include/session/network/session_network.hpp index 2dffdc89c..ba90e9961 100644 --- a/include/session/network/session_network.hpp +++ b/include/session/network/session_network.hpp @@ -53,7 +53,7 @@ class Network : public std::enable_shared_from_this { requires(!std::is_same_v< std::decay_t>>, config::Config>) - Network(Opt&&... opts) : Network(Config(std::forward(opts)...)){}; + Network(Opt&&... opts) : Network{config::Config{std::forward(opts)...}}{}; explicit Network(config::Config config); virtual ~Network(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a607deaed..9e1d16c4e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -118,6 +118,7 @@ target_link_libraries( PUBLIC crypto PRIVATE + config sessiondep::libsodium session::SQLite mlkem_native::mlkem768 @@ -173,6 +174,7 @@ if(ENABLE_NETWORKING) PRIVATE network/routing/session_router_router.cpp) target_link_libraries(network PUBLIC session-router::libsessionrouter) + target_compile_definitions(network PUBLIC ENABLE_NETWORKING_SROUTER) endif() if (BUILD_STATIC_DEPS) diff --git a/src/core.cpp b/src/core.cpp index eabf6205b..0e5247d62 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -103,14 +103,14 @@ void Core::_poll() { constexpr std::array namespaces = { config::Namespace::Devices, config::Namespace::AccountPubkeys}; - nlohmann::json ns_list = nlohmann::json::array(); - for (auto ns : namespaces) - ns_list.push_back(static_cast(ns)); auto now_ms = epoch_ms(clock_now_ms()); auto session_id = oxenc::to_hex(globals.session_id()); + auto ed25519_hex = globals.pubkey_ed25519().hex(); - std::string to_sign = fmt::format("retrieve{}{}", session_id, now_ms); + // Devices (21) requires auth: sign "retrieve" || namespace || timestamp (base-10 strings). + constexpr auto devices_ns_val = static_cast(config::Namespace::Devices); + std::string to_sign = fmt::format("retrieve{}{}", devices_ns_val, now_ms); std::array sig; auto seed = globals.account_seed(); crypto_sign_ed25519_detached( @@ -119,56 +119,56 @@ void Core::_poll() { reinterpret_cast(to_sign.data()), to_sign.size(), reinterpret_cast(seed.buf.data())); - auto sig_b64 = oxenc::to_base64(sig); net->get_swarm( globals.pubkey_x25519(), false, - [this, net, namespaces, ns_list, session_id, now_ms, sig_b64](auto, auto swarm) { + [this, net, namespaces, session_id, ed25519_hex, now_ms, sig_b64](auto, auto swarm) { if (swarm.empty()) return; auto& node = swarm.front(); - nlohmann::json last_hashes = nlohmann::json::object(); + // Build one batch subrequest per namespace. + nlohmann::json requests = nlohmann::json::array(); { auto conn = db.conn(); for (auto ns : namespaces) { auto ns_val = static_cast(ns); + nlohmann::json params = { + {"pubkey", session_id}, + {"namespace", ns_val}, + }; + + // Devices (21) requires a signed retrieve; AccountPubkeys (-21) does not. + if (ns == config::Namespace::Devices) { + params["pubkey_ed25519"] = ed25519_hex; + params["timestamp"] = now_ms; + params["signature"] = sig_b64; + } + auto last_hash = conn.prepared_maybe_get( "SELECT last_hash FROM namespace_sync" " WHERE namespace = ? AND sn_pubkey = ?", ns_val, node.remote_pubkey); if (last_hash) - last_hashes[std::to_string(ns_val)] = *last_hash; + params["last_hash"] = *last_hash; + + requests.push_back({{"method", "retrieve"}, {"params", std::move(params)}}); } } - nlohmann::json params = { - {"pubkey", session_id}, - {"namespaces", ns_list}, - {"timestamp", now_ms}, - {"signature", sig_b64}, - }; - if (!last_hashes.empty()) - params["last_hashes"] = last_hashes; - - nlohmann::json req_body = { - {"method", "retrieve"}, - {"params", params}, - }; - - auto body_str = req_body.dump(); + auto body_str = nlohmann::json{{"requests", std::move(requests)}}.dump(); net->send_request( network::Request{ node, - "storage_rpc", + "batch", to_vector(body_str), network::RequestCategory::standard_small, 20s}, - [this, sn_pubkey = node.remote_pubkey]( + [this, sn_pubkey = node.remote_pubkey, namespaces]( bool success, bool /*timeout*/, int16_t /*status_code*/, @@ -179,22 +179,31 @@ void Core::_poll() { try { auto json = nlohmann::json::parse(*body); - if (!json.contains("results") || !json["results"].is_array()) + auto it = json.find("results"); + if (it == json.end() || !it->is_array()) return; + auto& results = *it; auto conn = db.conn(); - for (const auto& res : json["results"]) { - if (!res.contains("namespace") || !res.contains("messages") || - !res["messages"].is_array()) + for (size_t i = 0; i < namespaces.size() && i < results.size(); + ++i) { + const auto& res = results[i]; + if (!res.contains("code") || res["code"].get() != 200) + continue; + auto body_it = res.find("body"); + if (body_it == res.end()) + continue; + auto msgs_it = body_it->find("messages"); + if (msgs_it == body_it->end() || !msgs_it->is_array()) continue; - auto ns_val = res["namespace"].get(); - auto ns = static_cast(ns_val); + auto ns = namespaces[i]; + auto ns_val = static_cast(ns); std::vector> messages_data; std::string newest_hash; - for (const auto& msg : res["messages"]) { + for (const auto& msg : *msgs_it) { if (!msg.contains("data") || !msg["data"].is_string()) continue; auto b64_data = msg["data"].get(); @@ -294,16 +303,14 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ auto session_id_hex = oxenc::to_hex(session_id.begin(), session_id.end()); auto now_ms = epoch_ms(clock_now_ms()); - nlohmann::json req_body = { - {"method", "retrieve"}, - {"params", - {{"pubkey", session_id_hex}, - {"namespaces", {static_cast(config::Namespace::AccountPubkeys)}}, - {"timestamp", now_ms}}}, + // AccountPubkeys (-21) allows unauthenticated retrieve: no signature needed. + nlohmann::json params = { + {"pubkey", session_id_hex}, + {"namespace", static_cast(config::Namespace::AccountPubkeys)}, }; net->get_swarm( - x25519_pub, false, [this, net, sid = std::move(sid), req_body](auto, auto swarm) { + x25519_pub, false, [this, net, sid = std::move(sid), params](auto, auto swarm) { if (swarm.empty()) { log::debug(cat, "prefetch_pfs_keys: get_swarm returned empty swarm"); if (callbacks.pfs_keys_fetched) @@ -311,11 +318,11 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ return; } - auto body_str = req_body.dump(); + auto body_str = params.dump(); net->send_request( network::Request{ swarm.front(), - "storage_rpc", + "retrieve", to_vector(body_str), network::RequestCategory::standard_small, 20s}, @@ -334,11 +341,12 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ try { auto json = nlohmann::json::parse(*body); - if (!json.contains("results") || !json["results"].is_array()) { + auto msgs_it = json.find("messages"); + if (msgs_it == json.end() || !msgs_it->is_array()) { log::warning( cat, "prefetch_pfs_keys: response missing or invalid " - "'results' array"); + "'messages' array"); return; } @@ -351,65 +359,44 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ std::optional> pk_mlkem768; - for (const auto& res : json["results"]) { - if (!res.contains("namespace")) { + for (const auto& msg : *msgs_it) { + auto data_it = msg.find("data"); + if (data_it == msg.end() || !data_it->is_string()) { log::warning( cat, - "prefetch_pfs_keys: result entry missing " - "'namespace' field"); + "prefetch_pfs_keys: message missing or " + "non-string 'data' field"); continue; } - if (res["namespace"].get() != - static_cast(config::Namespace::AccountPubkeys)) - continue; - if (!res.contains("messages") || !res["messages"].is_array()) { + auto b64 = data_it->get(); + std::vector decoded; + decoded.reserve(oxenc::from_base64_size(b64.size())); + oxenc::from_base64( + b64.begin(), b64.end(), std::back_inserter(decoded)); + try { + oxenc::bt_dict_consumer in{decoded}; + auto M = in.require_span< + std::byte, + MLKEM768_PUBLICKEYBYTES>("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub]( + std::span b, + std::span sig) { + if (!xed25519::verify(sig, x25519_pub, b)) + throw std::runtime_error{ + "signature verification " + "failed"}; + }); + std::ranges::copy(X, pk_x25519.emplace().begin()); + std::ranges::copy(M, pk_mlkem768.emplace().begin()); + } catch (const std::exception& e) { log::warning( cat, - "prefetch_pfs_keys: AccountPubkeys result " - "missing or invalid 'messages' array"); - continue; - } - - for (const auto& msg : res["messages"]) { - if (!msg.contains("data") || !msg["data"].is_string()) { - log::warning( - cat, - "prefetch_pfs_keys: message missing or " - "non-string 'data' field"); - continue; - } - auto b64 = msg["data"].get(); - std::vector decoded; - decoded.reserve(oxenc::from_base64_size(b64.size())); - oxenc::from_base64( - b64.begin(), - b64.end(), - std::back_inserter(decoded)); - try { - oxenc::bt_dict_consumer in{decoded}; - auto M = in.require_span< - std::byte, - MLKEM768_PUBLICKEYBYTES>("M"); - auto X = in.require_span("X"); - in.require_signature( - "~", - [&x25519_pub]( - std::span b, - std::span sig) { - if (!xed25519::verify(sig, x25519_pub, b)) - throw std::runtime_error{ - "signature verification " - "failed"}; - }); - std::ranges::copy(X, pk_x25519.emplace().begin()); - std::ranges::copy(M, pk_mlkem768.emplace().begin()); - } catch (const std::exception& e) { - log::warning( - cat, - "Ignoring malformed remote account pubkey " - "message: {}", - e.what()); - } + "Ignoring malformed remote account pubkey " + "message: {}", + e.what()); } } diff --git a/src/core/devices.cpp b/src/core/devices.cpp index e7c827ea1..7a48ef8b8 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index 73c9e3106..5c73d7e5b 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -111,9 +111,6 @@ void SessionRouter::_init() { data-dir={} [bind] listen=:0 - [logging] - type=none - level=*=debug,quic=info )"_format(opt::netid::to_string(_config.netid), _config.cache_directory); try { diff --git a/src/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index c9c161273..02c360302 100644 --- a/src/network/transport/quic_transport.cpp +++ b/src/network/transport/quic_transport.cpp @@ -538,7 +538,7 @@ void QuicTransport::_send_on_connection( final_timeout = result->second; } - log::debug(cat, "[Request {}] Failed with QUIC error: {}.", req_id, err_body); + log::warning(cat, "[Request {}] Failed with QUIC error: {}.", req_id, err_body); return cb( false, final_timeout, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5e1c2fcdb..ea7f38b4e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -95,6 +95,22 @@ if(NOT TARGET check) COMMAND testAll) endif() +option(BUILD_LIVE_TESTS "Build the live testnet integration tests (requires ENABLE_NETWORKING)" ${ENABLE_NETWORKING}) + +if(BUILD_LIVE_TESTS) + if(NOT ENABLE_NETWORKING) + message(FATAL_ERROR "BUILD_LIVE_TESTS requires ENABLE_NETWORKING") + endif() + + add_executable(testLive + live/main.cpp + live/test_swarm.cpp + live/test_pubkey_xfer.cpp) + target_link_libraries(testLive PRIVATE + test_libs + Catch2::Catch2) +endif() + add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp new file mode 100644 index 000000000..2b51c91d3 --- /dev/null +++ b/tests/live/live_utils.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../test_helper.hpp" + +using namespace std::literals; + +// Defined in live/main.cpp; consumed here and in all live test files. +extern session::network::opt::router live_router_mode; + +// Creates a Network instance pointed at testnet using the current live_router_mode. +inline std::shared_ptr make_testnet_network( + std::filesystem::path cache_dir) { + return std::make_shared( + session::network::opt::netid::testnet(), + live_router_mode, + session::network::opt::cache_directory{std::move(cache_dir)}); +} + +// Creates a session::TempCore connected to a fresh testnet Network. A unique temporary directory +// is created for the network's snode-pool cache and stored in session::TempCore::extra_dir so it is +// removed when the session::TempCore is destroyed. All CoreOption arguments are forwarded to the +// session::TempCore constructor (e.g. predefined_seed, encryption options). +template +inline session::TempCore make_live_core(Opts&&... opts) { + static std::atomic n{0}; + auto cache_dir = std::filesystem::temp_directory_path() / fmt::format("live_net_cache_{}", ++n); + std::filesystem::create_directories(cache_dir); + + session::TempCore tc{std::forward(opts)...}; + tc.extra_dir = cache_dir; + tc->set_network(make_testnet_network(std::move(cache_dir))); + return tc; +} + +// Builds the JSON params body for a signed "store" request targeting Core's AccountPubkeys +// namespace. The returned bytes are the raw params (no "method"/"params" wrapper); the network +// routing layer adds the wrapper as required by the transport. +// +// See session-storage-server client_rpc_endpoints.h for the store endpoint spec. +inline std::vector build_account_pubkeys_store_params(session::core::Core& core) { + auto session_id_hex = oxenc::to_hex(core.globals.session_id()); + auto now_ms = session::epoch_ms(session::clock_now_ms()); + constexpr auto ns = static_cast(session::config::Namespace::AccountPubkeys); + + // Signature covers: "store" || namespace (decimal) || sig_timestamp (decimal) + auto to_sign = fmt::format("store{}{}", ns, now_ms); + std::array sig; + auto seed = core.globals.account_seed(); + crypto_sign_ed25519_detached( + sig.data(), + nullptr, + reinterpret_cast(to_sign.data()), + to_sign.size(), + reinterpret_cast(seed.buf.data())); + + auto msg = core.devices.build_account_pubkey_message(); + + nlohmann::json params = { + {"pubkey", session_id_hex}, + {"pubkey_ed25519", core.globals.pubkey_ed25519().hex()}, + {"namespace", ns}, + {"data", + oxenc::to_base64( + std::string_view{reinterpret_cast(msg.data()), msg.size()})}, + {"timestamp", now_ms}, + {"sig_timestamp", now_ms}, + {"signature", oxenc::to_base64(sig)}, + {"ttl", int64_t{2592000000}}, // 30 days in ms + }; + return session::to_vector(params.dump()); +} + +// Pushes Core's AccountPubkeys message to its swarm. Resolves the swarm, sends the signed store +// request, and blocks until the response arrives or the timeout elapses. +// Returns true if the store was accepted by the swarm node. +inline bool store_account_pubkeys( + session::core::Core& core, std::chrono::milliseconds timeout = 30s) { + using namespace session::network; + using namespace std::chrono_literals; + + auto net = core.network(); + if (!net) + throw std::logic_error{"store_account_pubkeys called without a network object"}; + + auto promise = std::make_shared>(); + auto future = promise->get_future(); + + auto body = build_account_pubkeys_store_params(core); + + net->get_swarm( + core.globals.pubkey_x25519(), + false, + [promise, net, body = std::move(body)]( + swarm_id_t, std::vector swarm) mutable { + if (swarm.empty()) { + promise->set_value(false); + return; + } + net->send_request( + Request{swarm.front(), + "store", + std::move(body), + RequestCategory::standard_small, + 5s}, + [promise](bool success, bool, int16_t, auto, auto) { + promise->set_value(success); + }); + }); + + return future.wait_for(timeout) == std::future_status::ready && future.get(); +} diff --git a/tests/live/main.cpp b/tests/live/main.cpp new file mode 100644 index 000000000..1fd25fdc4 --- /dev/null +++ b/tests/live/main.cpp @@ -0,0 +1,53 @@ +#include +#include +#include + +#include "../log_setup.hpp" + +// Router selection: consumed by make_testnet_core() in live_utils.hpp. +// TODO FIXME: remove the `0 &&` once --srouter is working to restore session_router() as default. +session::network::opt::router live_router_mode = +#if 0 && defined(ENABLE_NETWORKING_SROUTER) + session::network::opt::router::session_router(); +#else + session::network::opt::router::onion_requests(); +#endif + +int main(int argc, char* argv[]) { + Catch::Session session; + + using namespace Catch::Clara; + using session::network::opt::router; + LogSetup log; + log.level = "warning"; + + bool use_srouter = false, use_onionreq = false, use_direct = false; + + auto cli = session.cli() | log.opts() | + Opt(use_srouter)["--srouter"]("route requests via session-router") | + Opt(use_onionreq)["--onionreq"]("route requests via onion requests") | + Opt(use_direct)["--direct"]("route requests directly (no onion routing)"); + + session.cli(cli); + + if (int rc = session.applyCommandLine(argc, argv); rc != 0) + return rc; + + if (int n = use_srouter + use_onionreq + use_direct; n > 1) { + oxen::log::critical( + oxen::log::Cat("live-test"), + "--srouter, --onionreq, and --direct are mutually exclusive"); + return 1; + } + + if (use_direct) + live_router_mode = router::direct(); + else if (use_onionreq) + live_router_mode = router::onion_requests(); + else if (use_srouter) + live_router_mode = router::session_router(); + + log.apply(); + + return session.run(); +} diff --git a/tests/live/test_pubkey_xfer.cpp b/tests/live/test_pubkey_xfer.cpp new file mode 100644 index 000000000..401b59a20 --- /dev/null +++ b/tests/live/test_pubkey_xfer.cpp @@ -0,0 +1,59 @@ +#include + +#include "../utils.hpp" +#include "live_utils.hpp" + +using namespace session; +using namespace std::literals; + +// Default timeout for live network operations. +static constexpr auto LIVE_TIMEOUT = 30s; + +TEST_CASE("Live: PFS key prefetch returns NAK for account with no published keys", "[live][pfs]") { + // Core A is a fresh account that has never published its AccountPubkeys to the swarm. + // Core B fetches keys for Core A's session id and should receive a NAK (empty namespace). + auto core_a = make_live_core(); + auto core_b = make_live_core(); + + std::array sid_a; + std::ranges::copy(core_a->globals.session_id(), sid_a.begin()); + + core_b->prefetch_pfs_keys(sid_a); + + auto entry = wait_for( + [&] { return session::TestHelper::pfs_cache_entry(*core_b, sid_a); }, LIVE_TIMEOUT); + REQUIRE(entry.has_value()); + // NAK: fetch completed but no keys were present. + CHECK_FALSE(entry->fetched_at.has_value()); + CHECK(entry->nak_at.has_value()); +} + +TEST_CASE("Live: PFS key prefetch retrieves keys after store to swarm", "[live][pfs]") { + // Core A stores its AccountPubkeys to the swarm; Core B then fetches them. + auto core_a = make_live_core(); + auto core_b = make_live_core(); + + // Store Core A's account pubkeys to its swarm. + REQUIRE(store_account_pubkeys(*core_a, LIVE_TIMEOUT)); + + // Now fetch from Core B's perspective. + std::array sid_a; + std::ranges::copy(core_a->globals.session_id(), sid_a.begin()); + + core_b->prefetch_pfs_keys(sid_a); + + auto entry = wait_for( + [&] { return session::TestHelper::pfs_cache_entry(*core_b, sid_a); }, LIVE_TIMEOUT); + REQUIRE(entry.has_value()); + // Successful fetch: keys present, no NAK. + REQUIRE(entry->fetched_at.has_value()); + CHECK_FALSE(entry->nak_at.has_value()); + + // The fetched pubkeys must match what Core A has as its active account keys. + auto [expected_x25519, expected_mlkem768] = + session::TestHelper::active_account_pubkeys(*core_a); + REQUIRE(entry->pubkey_x25519.has_value()); + REQUIRE(entry->pubkey_mlkem768.has_value()); + CHECK(*entry->pubkey_x25519 == expected_x25519); + CHECK(*entry->pubkey_mlkem768 == expected_mlkem768); +} diff --git a/tests/live/test_swarm.cpp b/tests/live/test_swarm.cpp new file mode 100644 index 000000000..75c7f1fe0 --- /dev/null +++ b/tests/live/test_swarm.cpp @@ -0,0 +1,35 @@ +#include + +#include "../utils.hpp" +#include "live_utils.hpp" + +using namespace session; +using namespace std::literals; + +// Default timeout for live network operations. +static constexpr auto LIVE_TIMEOUT = 30s; + +TEST_CASE("Live: network bootstraps snode pool from testnet", "[live][swarm]") { + auto core = make_live_core(); + auto& net = *core->network(); + + std::vector result; + callback_waiter waiter{ + [&](std::vector nodes) { result = std::move(nodes); }}; + net.get_random_nodes(5, waiter); + REQUIRE(waiter.wait(LIVE_TIMEOUT)); + CHECK(result.size() >= 1); +} + +TEST_CASE("Live: network resolves swarm for a locally-generated session id", "[live][swarm]") { + auto core = make_live_core(); + auto& net = *core->network(); + + std::vector swarm_result; + callback_waiter waiter{[&](network::swarm_id_t, std::vector swarm) { + swarm_result = std::move(swarm); + }}; + net.get_swarm(core->globals.pubkey_x25519(), false, waiter); + REQUIRE(waiter.wait(LIVE_TIMEOUT)); + CHECK(swarm_result.size() >= 1); +} diff --git a/tests/log_setup.hpp b/tests/log_setup.hpp new file mode 100644 index 000000000..f05ddd307 --- /dev/null +++ b/tests/log_setup.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include + +/// Holds the --log-level / --log-file option state and applies it after argument parsing. +struct LogSetup { + std::string level = "critical"; + std::string file = "stderr"; + + /// Returns a Clara option pipeline for --log-level and --log-file. + auto opts() { + using namespace Catch::Clara; + return Opt(level, + "level")["--log-level"]("oxen-logging log level to apply to the test run") | + Opt(file, "file")["--log-file"]( + "oxen-logging log file to output logs to, or one of " + "stdout/-/stderr/syslog."); + } + + /// Initialises the oxen-logging sink from the parsed level/file values. + void apply() const { + constexpr std::array print_vals = { + "stdout", "-", "", "stderr", "nocolor", "stdout-nocolor", "stderr-nocolor"}; + oxen::log::Type type; + if (std::count(print_vals.begin(), print_vals.end(), file)) + type = oxen::log::Type::Print; + else if (file == "syslog") + type = oxen::log::Type::System; + else + type = oxen::log::Type::File; + + oxen::log::add_sink( + type, file, "[%T.%f] [%*] [\x1b[1m%n\x1b[0m:%^%l%$|\x1b[3m%g:%#\x1b[0m] %v"); + oxen::log::apply_categories(level); + } +}; diff --git a/tests/main.cpp b/tests/main.cpp index 76c4cc49f..4a9d4e5f6 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,21 +1,18 @@ #include #include +#include "log_setup.hpp" + std::string g_test_pro_backend_dev_server_url = "http://127.0.0.1:5000"; int main(int argc, char* argv[]) { Catch::Session session; using namespace Catch::Clara; - std::string log_level = "critical", log_file = "stderr"; + LogSetup log; bool test_case_tracing = false; - auto cli = session.cli() | - Opt(log_level, - "level")["--log-level"]("oxen-logging log level to apply to the test run") | - Opt(log_file, "file")["--log-file"]( - "oxen-logging log file to output logs to, or one of or one of " - "stdout/-/stderr/syslog.") | + auto cli = session.cli() | log.opts() | Opt(test_case_tracing)["-T"]["--test-tracing"]( "enable oxen log tracing of test cases/sections") | Opt(g_test_pro_backend_dev_server_url, "url")["--pro-backend-dev-server-url"]( @@ -27,21 +24,7 @@ int main(int argc, char* argv[]) { if (int rc = session.applyCommandLine(argc, argv); rc != 0) return rc; - auto lvl = oxen::log::level_from_string(log_level); - - constexpr std::array print_vals = { - "stdout", "-", "", "stderr", "nocolor", "stdout-nocolor", "stderr-nocolor"}; - oxen::log::Type type; - if (std::count(print_vals.begin(), print_vals.end(), log_file)) - type = oxen::log::Type::Print; - else if (log_file == "syslog") - type = oxen::log::Type::System; - else - type = oxen::log::Type::File; - - oxen::log::add_sink( - type, log_file, "[%T.%f] [%*] [\x1b[1m%n\x1b[0m:%^%l%$|\x1b[3m%g:%#\x1b[0m] %v"); - oxen::log::reset_level(lvl); + log.apply(); oxen::log::set_level( oxen::log::Cat("testcase"), diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 407026df7..733aebbf0 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -46,8 +46,11 @@ class MockNetwork : public network::Network { // Smart-pointer-like RAII wrapper around a Core backed by a unique temporary DB file. // The DB file is removed on destruction. Default encryption uses a zeroed raw_key. +// If `extra_dir` is set, that directory tree is also removed recursively on destruction (used by +// make_live_core to clean up the network's cache directory). struct TempCore { std::filesystem::path path; + std::optional extra_dir; std::unique_ptr core; template @@ -58,10 +61,15 @@ struct TempCore { }()}, core{std::make_unique(path, std::forward(opts)...)} {} + TempCore(TempCore&&) = default; + TempCore& operator=(TempCore&&) = default; + ~TempCore() { core.reset(); // close DB before removing the file std::error_code ec; std::filesystem::remove(path, ec); + if (extra_dir) + std::filesystem::remove_all(*extra_dir, ec); } core::Core* operator->() { return core.get(); } @@ -115,6 +123,17 @@ class TestHelper { std::optional> pubkey_mlkem768; }; + // Returns true if the namespace_sync table has at least one row with a last_hash set for the + // given namespace (on any swarm node). Used by live tests to detect that a poll completed + // and delivered at least one message. + static bool has_any_namespace_sync(core::Core& core, config::Namespace ns) { + auto count = core.db.conn().prepared_get( + "SELECT COUNT(*) FROM namespace_sync" + " WHERE namespace = ? AND last_hash IS NOT NULL", + static_cast(ns)); + return count > 0; + } + // Returns the pfs_key_cache entry for the given session_id, or nullopt if absent. static std::optional pfs_cache_entry( core::Core& core, std::span session_id) { diff --git a/tests/utils.hpp b/tests/utils.hpp index a0795f32a..0db72c64b 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -5,12 +5,15 @@ #include #include +#include #include +#include #include #include #include #include #include +#include #include #include "session/clock.hpp" @@ -295,3 +298,84 @@ struct scope_exit { cleanup(); } }; + +// ── Async/callback helpers (adapted from oxen-libquic/tests/utils.hpp) ──────────────────────── + +template +struct functional_helper : public functional_helper {}; +template +struct functional_helper { + using return_type = Ret; + static constexpr bool is_void = std::is_void_v; + using type = std::function; +}; +template +using functional_helper_t = typename functional_helper::type; + +struct set_on_exit { + std::promise& p; + explicit set_on_exit(std::promise& p) : p{p} {} + ~set_on_exit() { p.set_value(); } +}; + +/// Wraps a callable in a promise/future pair. When passed as a std::function argument (via +/// implicit conversion), it calls the inner callable and then signals the promise, allowing tests +/// to block until an asynchronous callback fires. +/// +/// Usage: +/// bool got_it = false; +/// callback_waiter waiter{[&got_it](bool x) { got_it = x; }}; +/// async_operation(waiter); // waiter implicitly converts to std::function +/// REQUIRE(waiter.wait()); // blocks up to 5s +/// CHECK(got_it); +template +struct callback_waiter { + using Func_t = functional_helper_t; + + Func_t func; + std::shared_ptr> p{std::make_shared>()}; + std::future f{p->get_future()}; + + explicit callback_waiter(T f) : func{std::move(f)} {} + + [[nodiscard]] bool wait(std::chrono::milliseconds timeout = 5s) { + return f.wait_for(timeout) == std::future_status::ready; + } + + [[nodiscard]] bool is_ready() { return wait(0ms); } + + // Deliberate implicit conversion to std::function<...>: calls the inner callable then signals + // the promise. + operator Func_t() { + return [p = p, func = func](auto&&... args) { + set_on_exit prom_setter{*p}; + return func(std::forward(args)...); + }; + } + + void call() { this->operator Func_t()(); } +}; + +/// Polls a condition, sleeping between checks. Returns the last result of f() as soon as it is +/// truthy, or when the timeout expires (returning the last falsy result). +template Callback> +auto wait_for( + Callback f, + std::chrono::milliseconds timeout = 5s, + std::chrono::milliseconds check_interval = 25ms) { + auto end = std::chrono::steady_clock::now() + timeout; + for (;;) { + auto val = f(); + if (val || std::chrono::steady_clock::now() >= end) + return val; + std::this_thread::sleep_for(check_interval); + } +} + +// require_future(f) — asserts that std::future f becomes ready within 5s. +// require_future(f, timeout) — asserts that f becomes ready within the given timeout. +#define _require_future2(f, timeout) REQUIRE((f).wait_for(timeout) == std::future_status::ready) +#define _require_future1(f) _require_future2((f), 5s) +#define GET_REQUIRE_FUTURE_MACRO(_1, _2, NAME, ...) NAME +#define require_future(...) \ + GET_REQUIRE_FUTURE_MACRO(__VA_ARGS__, _require_future2, _require_future1)(__VA_ARGS__) From 5048553bae113f048ef711af0c25b48c6faa1bc3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 20:11:12 -0300 Subject: [PATCH 055/193] Add PFS + PQ encryption and decryption implementations --- include/session/hash.hpp | 2 +- include/session/session_encrypt.hpp | 123 ++++++++++++ src/CMakeLists.txt | 1 + src/session_encrypt.cpp | 292 ++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_session_encrypt.cpp | 106 ++++++++++ 6 files changed, 524 insertions(+), 1 deletion(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 3196cb2eb..680105409 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -244,7 +244,7 @@ struct [[nodiscard]] shake256 { template requires(sizeof...(Outs) > 0) - shake256& operator()(Outs&... outs) { + shake256& operator()(Outs&&... outs) { (crypto_xof_shake256_squeeze( &st, reinterpret_cast(std::ranges::data(outs)), diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 29e579efb..87837a8ad 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -2,8 +2,10 @@ #include +#include #include #include +#include #include #include @@ -111,6 +113,127 @@ std::vector encrypt_for_blinded_recipient( std::span recipient_blinded_id, std::span message); +/// API: crypto/encrypt_for_recipient_v2 +/// +/// Encrypts a v2 Session DM (PFS + post-quantum) for a recipient. +/// +/// The wire format of the returned ciphertext is: +/// 0x00 0x02 | ki (2B) | E (32B) | mlkem_ct (1088B) | xchacha20poly1305_ciphertext +/// +/// where: +/// - `ki` is an encrypted key indicator used by the recipient to cheaply identify which of +/// their current account keys was used, without revealing it to outside observers. +/// - `E` is an ephemeral X25519 pubkey. +/// - `mlkem_ct` is an ML-KEM-768 ciphertext. +/// - The xchacha20poly1305 ciphertext contains the signed, padded inner plaintext. +/// +/// The inner plaintext is a bt-encoded dict with: +/// - "S": the sender's Ed25519 pubkey (32 bytes) +/// - "c": the message content (typically a serialized protobuf Content) +/// - "~": a 64-byte Ed25519 signature over a BLAKE2b-64 hash of the preceding content, +/// keyed with the recipient's 33-byte Session ID (personalized "SessionV2Message") +/// - "~P": optional Session Pro Ed25519 signature verifying the sender's Pro status. The public +/// key for verifying this is embedded within the protobuf Content. Present only when the +/// message uses Session Pro features; absent otherwise. +/// +/// Inputs: +/// - `sender_ed25519_privkey` -- sender's 32-byte seed or 64-byte Ed25519 secret key +/// - `recipient_session_id` -- 33-byte 0x05-prefixed long-term X25519 pubkey (S with prefix) +/// - `recipient_account_x25519` -- 32-byte recently-fetched PFS account X25519 pubkey (X) +/// - `recipient_account_mlkem768` -- 1184-byte recently-fetched PFS account ML-KEM-768 pubkey (M) +/// - `content` -- the plaintext message content to encrypt (typically a serialized protobuf) +/// - `pro_signature` -- optional 64-byte Session Pro Ed25519 signature. Pass nullopt when the +/// sender is not using Session Pro features. +/// +/// Outputs: +/// - The encrypted v2 ciphertext to send to the swarm. +/// - Throws on invalid keys or encryption failure. +std::vector encrypt_for_recipient_v2( + std::span sender_ed25519_privkey, + std::span recipient_session_id, + std::span recipient_account_x25519, + std::span recipient_account_mlkem768, + std::span content, + std::optional> pro_signature = std::nullopt); + +/// Exception thrown when a v2 message could not be decrypted with a given account key. The +/// caller should catch this and try the next candidate key. Other exceptions (e.g., +/// std::runtime_error for invalid message format, or std::invalid_argument for bad keys) are +/// unrecoverable and should not be caught per-key. +struct DecryptV2Error : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +/// Result of decrypt_incoming_v2. +struct DecryptV2Result { + std::vector content; ///< Decrypted message content. + std::array sender_session_id; ///< 05-prefixed Session ID of the sender. + std::optional> pro_signature; ///< Pro sig, if present. +}; + +/// API: crypto/decrypt_incoming_v2_prefix +/// +/// Extracts and decrypts the 2-byte key indicator from a v2 Session DM ciphertext, returning +/// the first 2 bytes of the ML-KEM-768 public key that was used to encrypt the message. +/// +/// This is a cheap pre-filter step: the caller uses the returned prefix to look up which of +/// their PFS account keys match, then passes the matching key(s) to `decrypt_incoming_v2`. +/// +/// Inputs: +/// - `x25519_sec` -- 32-byte long-term X25519 secret key of the recipient (the raw key, *not* +/// the Ed25519 key). +/// - `x25519_pub` -- 32-byte long-term X25519 public key of the recipient (i.e. the Session ID +/// bytes without the `0x05` prefix). +/// - `ciphertext` -- wire-format v2 ciphertext as produced by `encrypt_for_recipient_v2`. +/// +/// Outputs: +/// - The recovered 2-byte ML-KEM-768 public key prefix. +/// - Throws `std::runtime_error` if the ciphertext is too short or has the wrong prefix bytes. +std::array decrypt_incoming_v2_prefix( + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext); + +/// API: crypto/decrypt_incoming_v2 +/// +/// Inverse of `encrypt_for_recipient_v2`: decrypts a v2 Session DM using a single PFS account +/// key. Verifies the X-Wing (ML-KEM-768 + X25519) shared secret derivation and the inner +/// Ed25519 message signature. +/// +/// Typical usage: call `decrypt_incoming_v2_prefix` to get the 2-byte ML-KEM prefix, look up +/// all PFS account keys whose ML-KEM-768 public key begins with that prefix, then call this +/// function for each candidate, catching `DecryptV2Error` and trying the next key on failure: +/// +/// auto prefix = decrypt_incoming_v2_prefix(x25519_sec, x25519_pub, ciphertext); +/// for (auto& key : pfs_keys_by_prefix(prefix)) { +/// try { +/// return decrypt_incoming_v2(session_id, key.x_sec, key.x_pub, +/// key.mlkem_sec, ciphertext); +/// } catch (const DecryptV2Error&) { continue; } +/// } +/// throw std::runtime_error{"no PFS account key could decrypt the message"}; +/// +/// Inputs: +/// - `recipient_session_id` -- 33-byte 0x05-prefixed Session ID of the recipient. Used to +/// verify the inner Ed25519 message signature; no private key material is needed here. +/// - `account_pfs_x25519_sec` -- 32-byte X25519 secret key of the PFS account key to try. +/// - `account_pfs_x25519_pub` -- 32-byte X25519 public key of the PFS account key to try. +/// - `account_pfs_mlkem768_sec` -- 2400-byte ML-KEM-768 secret key of the PFS account key +/// to try. +/// - `ciphertext` -- wire-format v2 ciphertext as produced by `encrypt_for_recipient_v2`. +/// +/// Outputs: +/// - `DecryptV2Result` with the decrypted content, 33-byte (05-prefixed) sender Session ID, +/// and an optional 64-byte Session Pro signature. +/// - Throws `DecryptV2Error` if the key did not decrypt the message (try the next candidate). +/// - Throws `std::runtime_error` for unrecoverable errors (invalid format, signature failure). +DecryptV2Result decrypt_incoming_v2( + std::span recipient_session_id, + std::span account_pfs_x25519_sec, + std::span account_pfs_x25519_pub, + std::span account_pfs_mlkem768_sec, + std::span ciphertext); + static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// API: crypto/encrypt_for_group diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9e1d16c4e..0a5d2d943 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,6 +97,7 @@ target_link_libraries(crypto session::secure_buffer PRIVATE sessiondep::libsodium + mlkem_native::mlkem768 nlohmann_json::nlohmann_json libsession::protos ) diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 72e12fa14..a95c2daf5 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,5 +1,6 @@ #include "session/session_encrypt.hpp" +#include #include #include #include @@ -28,10 +29,12 @@ #include "session/blinding.hpp" #include "session/clock.hpp" #include "session/hash.hpp" +#include "session/random.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" using namespace std::literals; +using namespace session::literals; namespace session { @@ -68,6 +71,73 @@ namespace detail { // some future version changes the format (and if not even try to load it). inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; +// Constants for v2 PFS+PQ message encryption/decryption + +// BLAKE2b personalization for the key indicator shared secret (KISS): a 2-byte hash that lets the +// recipient cheaply identify which of their account keys was used without revealing it externally. +constexpr auto V2_KISS_PERS = "Session-Msg-KISS"_b2b_pers; + +// BLAKE2b personalization for the inner-message signature hash. +constexpr auto V2_MSG_SIG_PERS = "SessionV2Message"_b2b_pers; + +// X-Wing KDF domain separator from draft-connolly-cfrg-xwing-kem: the 6 ASCII bytes '\.//^\'. +// SHA3-256(ssₘ || ssₓ || E || X || V2_XWING_LABEL) produces the combined X-Wing shared secret. +constexpr auto V2_XWING_LABEL = // + R"(\./)" + R"(/^\)"_uc; + +// SHAKE256 domain prefix for deriving the XChaCha20+Poly1305 key and nonce from the X-Wing SS. +constexpr auto V2_SS_DOMAIN = "SessionV2MessageSS"_uc; + +// Shared v2 wire-format layout constants (used in both encrypt and decrypt) +static constexpr size_t V2_AEAD_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_ABYTES; +static constexpr size_t V2_NONCE_SIZE = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; +static constexpr size_t V2_HEADER_SIZE = 2 + 2 + 32 + MLKEM768_CIPHERTEXTBYTES; +static constexpr size_t V2_OUTER_OVERHEAD = V2_HEADER_SIZE + V2_AEAD_OVERHEAD; +static constexpr size_t V2_MIN_FINAL_SIZE = ((V2_OUTER_OVERHEAD + 256 + 255) / 256) * 256; + +// Validates the v2 ciphertext prefix and minimum size; throws std::runtime_error on failure. +static void v2_check_header(std::span ciphertext) { + if (ciphertext.size() < V2_MIN_FINAL_SIZE) + throw std::runtime_error{"v2 ciphertext is too short"}; + if (ciphertext[0] != 0x00 || ciphertext[1] != 0x02) + throw std::runtime_error{"v2 ciphertext has wrong version prefix"}; +} + +// X-Wing KDF: computes ss = SHA3-256(ssm||ssx||E||X||V2_XWING_LABEL) from key_buf (=ssm) and +// nonce_buf (=ssx), then squeezes k (32B) back into key_buf and n (V2_NONCE_SIZE B) into the +// first V2_NONCE_SIZE bytes of nonce_buf, overwriting the shared secrets with derived key material. +// E is the ephemeral X25519 pubkey; X is the account PFS X25519 pubkey. +static void v2_derive_xwing_key_nonce( + cleared_uc32& key_buf, + cleared_uc32& nonce_buf, + std::span E, + std::span X) { + std::array ss; + hash::sha3_256(ss, key_buf, nonce_buf, E, X, V2_XWING_LABEL); + hash::shake256(V2_SS_DOMAIN, ss)( + key_buf, std::span{nonce_buf.data(), V2_NONCE_SIZE}); + sodium_memzero(ss.data(), ss.size()); +} + +// Computes the 2-byte Key Indicator Shared Secret (KISS): +// KISS = BLAKE2b_2(E || S, key=DH(sec, pub_for_dh), pers="Session-Msg-KISS") +// On encrypt: sender holds ephemeral secret e, DH partner is long-term S → call with +// encrypting=true On decrypt: recipient holds long-term secret s, DH partner is ephemeral E → call +// with encrypting=false +static std::array v2_kiss( + std::span sec, + std::span E, + std::span S, + bool encrypting) { + cleared_uc32 dh; + if (0 != crypto_scalarmult(dh.data(), sec.data(), encrypting ? S.data() : E.data())) + throw std::runtime_error{"X25519 DH (KISS) failed"}; + std::array kiss; + hash::blake2b_key_pers(kiss, dh, V2_KISS_PERS, E, S); + return kiss; +} + std::vector sign_for_recipient( const Ed25519PrivKeySpan& ed25519_privkey, std::span recipient_pubkey, @@ -171,6 +241,228 @@ std::vector encrypt_for_recipient_deterministic( return result; } +std::vector encrypt_for_recipient_v2( + std::span sender_ed25519_privkey, + std::span recipient_session_id, + std::span recipient_account_x25519, + std::span recipient_account_mlkem768, + std::span content, + std::optional> pro_signature) { + + // Expand 32-byte seed → 64-byte ed25519 key if needed. Storage is heap-allocated so the span + // remains valid if the pair is moved. + auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(sender_ed25519_privkey); + // In libsodium's full ed25519 key layout, bytes [32:64] are the public key + std::span sender_ed_pk{ed_sk.data() + 32, 32}; + + // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) + std::span S{recipient_session_id.data() + 1, 32}; + + // Step 1: Generate ephemeral X25519 keypair e/E + cleared_uc32 e; + uc32 E; + crypto_box_keypair(E.data(), e.data()); + + // Two multi-purpose key buffers — each plays sequential, non-overlapping roles: + // + // enc_key_buf: ML-KEM shared secret ssm (step 4) → SHAKE256-derived enc key k (step 8) + // enc_nonce_buf: eS DH result (step 2) → ML-KEM coins (step 4) → ssx DH result (step 5) + // → SHAKE256-derived enc nonce n in first 24 bytes (step 8) + cleared_uc32 enc_key_buf; + cleared_uc32 enc_nonce_buf; + + // Step 2: KISS = BLAKE2b_2(E || S, key=eS, pers="Session-Msg-KISS") + // eS is the X25519 DH with the long-term key, used only for cheap key indicator obfuscation + auto kiss = v2_kiss(e, E, S, /*encrypting=*/true); + + // Step 3: ki = M[0:2] ⊕ kiss (encrypted key indicator; lets recipient quickly identify key) + std::array ki{ + static_cast(recipient_account_mlkem768[0] ^ kiss[0]), + static_cast(recipient_account_mlkem768[1] ^ kiss[1])}; + + // Step 4: ML-KEM-768 encapsulate: ssₘ, mlkem_ct = Encapsulate(M) + std::array mlkem_ct; + random::fill(enc_nonce_buf); // repurpose enc_nonce_buf as random ML-KEM coins + if (0 != sr_mlkem768_enc_derand( + mlkem_ct.data(), + enc_key_buf.data(), + recipient_account_mlkem768.data(), + enc_nonce_buf.data())) + throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; + + // Step 5: ssx = eX (X25519 DH with account PFS key X, not long-term key S) + if (0 != crypto_scalarmult(enc_nonce_buf.data(), e.data(), recipient_account_x25519.data())) + throw std::runtime_error{"X25519 DH (account key) failed"}; + + // Step 6: X-Wing KDF → enc key k (in enc_key_buf) and enc nonce n (in enc_nonce_buf[0:24]) + v2_derive_xwing_key_nonce(enc_key_buf, enc_nonce_buf, E, recipient_account_x25519); + + // Step 7: Build inner bt-encoded dict directly into the final result buffer. + // + // bt_bytes_encoded(n): total bytes to represent an n-byte bt string: decimal digits of n + 1 + + // n + constexpr auto bt_bytes_encoded = [](size_t n) constexpr -> size_t { + size_t sz = 1 + n; // ':' + n data bytes + do { + ++sz; + } while (n /= 10); // decimal digits of n + return sz; + }; + // Keys must be in ascending lexicographic order: "S" < "c" < "~" < "~P" + constexpr size_t S_KEY_VAL = 3 + bt_bytes_encoded(32); // "1:S" + "32:" + constexpr size_t SIG_KEY_VAL = 3 + bt_bytes_encoded(64); // "1:~" + "64:" + constexpr size_t PRO_KEY_VAL = 4 + bt_bytes_encoded(64); // "2:~P" + "64:" + size_t inner_dict_size = 2 // d...e dict delimiters + + S_KEY_VAL + 3 + + bt_bytes_encoded(content.size()) // "1:c" + "" + + SIG_KEY_VAL + (pro_signature ? PRO_KEY_VAL : 0); + + // Total message must be a multiple of 256 bytes and at least V2_MIN_FINAL_SIZE bytes. + size_t final_size = + (std::max(V2_MIN_FINAL_SIZE, V2_OUTER_OVERHEAD + inner_dict_size) + 255) & ~size_t{255}; + size_t padded_inner_size = final_size - V2_OUTER_OVERHEAD; + + // Step 8: Allocate result (zero-initialized so padding bytes are already 0), write header, + // build inner dict directly into result buffer, then encrypt in-place. + // (c == m is explicitly supported by libsodium for AEAD functions) + std::vector result(final_size, 0); + + result[0] = 0x00; + result[1] = 0x02; + result[2] = ki[0]; + result[3] = ki[1]; + std::memcpy(result.data() + 4, E.data(), 32); + std::memcpy(result.data() + 36, mlkem_ct.data(), MLKEM768_CIPHERTEXTBYTES); + + { + oxenc::bt_dict_producer dict{ + reinterpret_cast(result.data() + V2_HEADER_SIZE), inner_dict_size}; + dict.append("S", sender_ed_pk); + dict.append("c", content); + // "~" signs BLAKE2b-64(body-so-far, key=recipient_session_id_33B, pers="SessionV2Message") + dict.append_signature("~", [&](std::span body) { + cleared_uc64 h; + hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); + uc64 sig; + if (0 != + crypto_sign_ed25519_detached(sig.data(), nullptr, h.data(), h.size(), ed_sk.data())) + throw std::runtime_error{"Failed to sign v2 message"}; + return sig; + }); + if (pro_signature) + dict.append("~P", *pro_signature); + assert(dict.view().size() == inner_dict_size); + } + + if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( + result.data() + V2_HEADER_SIZE, // c (output, in-place) + nullptr, + result.data() + V2_HEADER_SIZE, // m (input, same buffer) + padded_inner_size, + nullptr, + 0, + nullptr, + enc_nonce_buf.data(), // nonce (24B read from 32B buffer) + enc_key_buf.data())) // key + throw std::runtime_error{"v2 message encryption failed"}; + + return result; +} + +std::array decrypt_incoming_v2_prefix( + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext) { + v2_check_header(ciphertext); + auto E = ciphertext.subspan<4, 32>(); + auto kiss = v2_kiss(x25519_sec, E, x25519_pub, /*encrypting=*/false); + return {static_cast(ciphertext[2] ^ kiss[0]), + static_cast(ciphertext[3] ^ kiss[1])}; +} + +DecryptV2Result decrypt_incoming_v2( + std::span recipient_session_id, + std::span account_pfs_x25519_sec, + std::span account_pfs_x25519_pub, + std::span account_pfs_mlkem768_sec, + std::span ciphertext) { + v2_check_header(ciphertext); + + auto E = ciphertext.subspan<4, 32>(); + auto mlkem_ct = ciphertext.subspan<36, MLKEM768_CIPHERTEXTBYTES>(); + + cleared_uc32 key_buf; // ssm → k + cleared_uc32 nonce_buf; // ssx → n + + // Step 1: ML-KEM-768 decapsulate → shared secret ssm in key_buf + if (0 != sr_mlkem768_dec(key_buf.data(), mlkem_ct.data(), account_pfs_mlkem768_sec.data())) + throw DecryptV2Error{"ML-KEM-768 decapsulation failed"}; + + // Step 2: X25519 DH with account PFS key → shared secret ssx in nonce_buf + if (0 != crypto_scalarmult(nonce_buf.data(), account_pfs_x25519_sec.data(), E.data())) + throw DecryptV2Error{"X25519 DH (account key) failed"}; + + // Step 3: X-Wing KDF → enc key k (in key_buf) and enc nonce n (in nonce_buf[0:24]) + v2_derive_xwing_key_nonce(key_buf, nonce_buf, E, account_pfs_x25519_pub); + + // Step 4: AEAD decrypt the inner payload + size_t enc_size = ciphertext.size() - V2_HEADER_SIZE; + std::vector plain(enc_size - V2_AEAD_OVERHEAD); + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + plain.data(), + nullptr, + nullptr, + ciphertext.data() + V2_HEADER_SIZE, + enc_size, + nullptr, + 0, + nonce_buf.data(), + key_buf.data())) + throw DecryptV2Error{"v2 message decryption failed"}; + + // Strip zero padding from end (the plaintext was padded to a multiple of 256 bytes) + while (!plain.empty() && plain.back() == 0) + plain.pop_back(); + + // Step 5: Parse the bencoded inner dict + oxenc::bt_dict_consumer dict{plain}; + + auto sender_ed_pk = dict.require_span("S"); + auto content_sv = dict.require_span("c"); + + // Verify the Ed25519 signature over BLAKE2b(body, key=recipient_session_id, pers=…) + dict.require_signature( + "~", [&](std::span body, std::span sig) { + if (sig.size() != 64) + throw std::runtime_error{"v2 message signature has wrong size"}; + uc64 h; + hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); + if (0 != crypto_sign_ed25519_verify_detached( + sig.data(), h.data(), h.size(), sender_ed_pk.data())) + throw std::runtime_error{"v2 message signature verification failed"}; + }); + + // Optional "~P" pro signature (span into plain, copied once into result below) + std::optional> pro_sv; + if (dict.skip_until("~P")) + pro_sv = dict.consume_span(); + + dict.finish(); + + // Convert sender Ed25519 pubkey to X25519 and build the 33-byte session ID + std::array sender_x25519; + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x25519.data(), sender_ed_pk.data())) + throw std::runtime_error{"sender ed25519 pubkey is invalid"}; + + DecryptV2Result result; + result.content.assign(content_sv.begin(), content_sv.end()); + result.sender_session_id[0] = 0x05; + std::ranges::copy(sender_x25519, result.sender_session_id.begin() + 1); + if (pro_sv) + std::ranges::copy(*pro_sv, result.pro_signature.emplace().begin()); + return result; +} + // Calculate the shared encryption key, sending from blinded sender kS (k = S's blinding factor) to // blinded receiver jR (j = R's blinding factor). // diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea7f38b4e..9508537a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,6 +60,7 @@ target_link_libraries(test_libs INTERFACE libsession::crypto session::SQLite sessiondep::libsodium + mlkem_native::mlkem768 nlohmann_json::nlohmann_json oxen::logging) diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 7b65418a5..d445ceb89 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,4 +1,6 @@ +#include #include +#include #include #include @@ -497,3 +499,107 @@ TEST_CASE("xchacha20", "[session][xchacha20]") { CHECK(decrypt_xchacha20(ciphertext, enc_key) == to_vector("TestMessage")); CHECK_THROWS(encrypt_xchacha20(payload, to_span("invalid"))); } + +TEST_CASE("v2 PFS+PQ message encryption", "[session-protocol][encrypt][v2]") { + using namespace session; + + // Sender: existing well-known test keypair 1 + const auto seed1 = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + std::array sender_ed_pk; + std::array sender_ed_sk; + crypto_sign_ed25519_seed_keypair(sender_ed_pk.data(), sender_ed_sk.data(), seed1.data()); + + // Recipient: long-term session identity from test keypair 2 + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + std::array recip_ed_pk, recip_curve_pk; + std::array recip_ed_sk; + crypto_sign_ed25519_seed_keypair(recip_ed_pk.data(), recip_ed_sk.data(), seed2.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(recip_curve_pk.data(), recip_ed_pk.data())); + + std::array recip_x25519_sec; + REQUIRE(0 == crypto_sign_ed25519_sk_to_curve25519(recip_x25519_sec.data(), recip_ed_sk.data())); + + std::array recip_session_id; + recip_session_id[0] = 0x05; + std::copy(recip_curve_pk.begin(), recip_curve_pk.end(), recip_session_id.begin() + 1); + + // Recipient PFS X25519 account key (deterministic) + const auto pfs_x25519_seed = + "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff"_hexbytes; + std::array pfs_x25519_sec, pfs_x25519_pub; + std::copy(pfs_x25519_seed.begin(), pfs_x25519_seed.end(), pfs_x25519_sec.begin()); + crypto_scalarmult_curve25519_base(pfs_x25519_pub.data(), pfs_x25519_sec.data()); + + // Recipient PFS ML-KEM-768 account key (deterministic, needs 64-byte seed) + const auto pfs_mlkem_seed = + "deadbeefcafebabe0123456789abcdef0123456789abcdef0123456789abcdef" + "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"_hexbytes; + std::array pfs_mlkem_pub; + std::array pfs_mlkem_sec; + REQUIRE(0 == sr_mlkem768_keypair_derand( + pfs_mlkem_pub.data(), pfs_mlkem_sec.data(), pfs_mlkem_seed.data())); + + // Encrypt a message from sender to recipient + auto ct = encrypt_for_recipient_v2( + to_span(sender_ed_sk), + recip_session_id, + pfs_x25519_pub, + pfs_mlkem_pub, + to_span("hello world"), + std::nullopt); + + // Ciphertext is padded to a multiple of 256 bytes + CHECK(ct.size() % 256 == 0); + + // decrypt_incoming_v2_prefix recovers the 2-byte ML-KEM pubkey prefix using the + // recipient's long-term X25519 keys (cheap; no PFS keys needed at this stage) + auto prefix = decrypt_incoming_v2_prefix(recip_x25519_sec, recip_curve_pk, ct); + CHECK(prefix[0] == pfs_mlkem_pub[0]); + CHECK(prefix[1] == pfs_mlkem_pub[1]); + + // Decrypt with the correct keys succeeds + auto result = decrypt_incoming_v2( + recip_session_id, pfs_x25519_sec, pfs_x25519_pub, pfs_mlkem_sec, ct); + CHECK(result.content == to_vector("hello world")); + CHECK(result.sender_session_id[0] == 0x05); + CHECK(!result.pro_signature); + + // The recovered sender session ID matches the sender's X25519 pubkey + std::array sender_curve_pk; + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(sender_curve_pk.data(), sender_ed_pk.data())); + CHECK(std::equal( + result.sender_session_id.begin() + 1, + result.sender_session_id.end(), + sender_curve_pk.begin())); + + // Wrong X25519 key throws DecryptV2Error (wrong-key failure, not a format error) + auto wrong_x25519_sec = pfs_x25519_sec; + wrong_x25519_sec[0] ^= 0xff; + std::array wrong_x25519_pub; + crypto_scalarmult_curve25519_base(wrong_x25519_pub.data(), wrong_x25519_sec.data()); + CHECK_THROWS_AS( + decrypt_incoming_v2( + recip_session_id, wrong_x25519_sec, wrong_x25519_pub, pfs_mlkem_sec, ct), + DecryptV2Error); + + // Truncated ciphertext throws before key matching (unrecoverable format error) + auto truncated = std::vector(ct.begin(), ct.begin() + 100); + CHECK_THROWS_AS( + decrypt_incoming_v2_prefix(recip_x25519_sec, recip_curve_pk, truncated), + std::runtime_error); + + // Encrypting and decrypting with a pro_signature + std::array fake_pro_sig; + std::fill(fake_pro_sig.begin(), fake_pro_sig.end(), 0x42); + auto ct_pro = encrypt_for_recipient_v2( + to_span(sender_ed_sk), + recip_session_id, + pfs_x25519_pub, + pfs_mlkem_pub, + to_span("hello world"), + std::span{fake_pro_sig}); + auto result_pro = decrypt_incoming_v2( + recip_session_id, pfs_x25519_sec, pfs_x25519_pub, pfs_mlkem_sec, ct_pro); + REQUIRE(result_pro.pro_signature.has_value()); + CHECK(*result_pro.pro_signature == fake_pro_sig); +} From d0321f63b538f6aa0bb75094dc906851dfd18733 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:29:01 -0300 Subject: [PATCH 056/193] Improve test db naming to avoid parallel invocation overlap We write the test databases into /tmp, but that means running two test suites at once can clash and corrupt each other. This fixes it by using random::unique_id for the db filenames (which has a unique counter *and* a random value). Additionally the poll test was using a fixed filename, making it even more likely to break. This converts poll to use the same TempCore as the other various tests. --- include/session/random.hpp | 2 +- src/random.cpp | 10 ++-- tests/test_helper.hpp | 8 ++- tests/test_poll.cpp | 107 +++++++++++++++++-------------------- 4 files changed, 60 insertions(+), 67 deletions(-) diff --git a/include/session/random.hpp b/include/session/random.hpp index 553ae7f56..ef539f618 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -79,7 +79,7 @@ std::string random_base32(size_t size); /// /// Outputs: /// - generated id string. -std::string unique_id(std::string_view prefix); +std::string unique_id(std::string_view prefix, size_t random_len = 4); /// API: random/get_uniform_distribution /// diff --git a/src/random.cpp b/src/random.cpp index 7e325d589..e42b815af 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -48,10 +48,14 @@ std::string random_base32(size_t size) { return result; } -std::string unique_id(std::string_view prefix) { - static std::atomic counter{0}; +static std::atomic unique_id_counter{0}; + +std::string unique_id(std::string_view prefix, size_t random_len) { return fmt::format( - "{}-{}-{}", prefix, counter.fetch_add(1, std::memory_order_relaxed), random_base32(4)); + "{}-{}-{}", + prefix, + unique_id_counter.fetch_add(1, std::memory_order_relaxed), + random_base32(random_len)); } } // namespace session::random diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 733aebbf0..683fd1c45 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -2,8 +2,8 @@ #include -#include #include +#include #include #include #include @@ -55,10 +55,8 @@ struct TempCore { template explicit TempCore(Opts&&... opts) : - path{[] { - static std::atomic n{0}; - return std::filesystem::temp_directory_path() / fmt::format("test_core_{}.db", ++n); - }()}, + path{std::filesystem::temp_directory_path() / + fmt::format("{}.db", session::random::unique_id("test_core", 7))}, core{std::make_unique(path, std::forward(opts)...)} {} TempCore(TempCore&&) = default; diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index d2846ea6a..141cfcd22 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -27,69 +27,60 @@ static nlohmann::json make_response( } TEST_CASE("Core automatic polling", "[core][poll]") { - auto db_path = std::filesystem::temp_directory_path() / "test_poll.db"; - if (std::filesystem::exists(db_path)) - std::filesystem::remove(db_path); - bool received = false; - core::callbacks callbacks; - callbacks.device_link_request = [&](int, - const core::device::Info&, - std::span) { received = true; }; + core::callbacks cbs; + cbs.device_link_request = [&](int, + const core::device::Info&, + std::span) { received = true; }; - { - core::Core core{db_path, callbacks}; - auto mock_net = std::make_shared(); - // Use a fixed non-zero pubkey for the node. - mock_net->current_node.remote_pubkey[0] = 0x01; - - core.set_network(mock_net); - - // Trigger poll via TestHelper - TestHelper::poll(core); - - REQUIRE(mock_net->sent_requests.size() == 1); - auto& sent = mock_net->sent_requests[0]; - - auto req_json = nlohmann::json::parse(*sent.request.body); - CHECK(req_json["method"] == "retrieve"); - auto& params = req_json["params"]; - CHECK(params["pubkey"] == oxenc::to_hex(core.globals.session_id())); - CHECK(params["namespaces"] == nlohmann::json::array({21, -21})); - CHECK(params.contains("timestamp")); - CHECK(params.contains("signature")); - // No prior hash for this node yet, so no last_hashes in request. - CHECK_FALSE(params.contains("last_hashes")); + TempCore core{cbs}; + auto mock_net = std::make_shared(); + // Use a fixed non-zero pubkey for the node. + mock_net->current_node.remote_pubkey[0] = 0x01; - // Build a valid link request from a second device sharing the same account seed. - cleared_b32 seed_bytes; - { - auto seed_acc = core.globals.account_seed(); - std::ranges::copy(seed_acc.buf.first<32>(), seed_bytes.begin()); - } - TempCore linker{core::predefined_seed{std::span{seed_bytes}}}; - auto link_msg = linker->devices.build_link_request().message; - const auto* p = reinterpret_cast(link_msg.data()); - std::vector outer_msg{p, p + link_msg.size()}; - - sent.callback(true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); - - // Verify last_hash was stored under this specific node's pubkey. - CHECK(TestHelper::namespace_last_hash(core, 21, mock_net->current_node.remote_pubkey) == - "hash1"); - CHECK(received); - - // Poll again with the same node — should include last_hash. - mock_net->sent_requests.clear(); - TestHelper::poll(core); - - REQUIRE(mock_net->sent_requests.size() == 1); - auto req_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); - CHECK(req_json2["params"]["last_hashes"]["21"] == "hash1"); + core->set_network(mock_net); + + // Trigger poll via TestHelper + TestHelper::poll(*core); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto& sent = mock_net->sent_requests[0]; + + auto req_json = nlohmann::json::parse(*sent.request.body); + CHECK(req_json["method"] == "retrieve"); + auto& params = req_json["params"]; + CHECK(params["pubkey"] == oxenc::to_hex(core->globals.session_id())); + CHECK(params["namespaces"] == nlohmann::json::array({21, -21})); + CHECK(params.contains("timestamp")); + CHECK(params.contains("signature")); + // No prior hash for this node yet, so no last_hashes in request. + CHECK_FALSE(params.contains("last_hashes")); + + // Build a valid link request from a second device sharing the same account seed. + cleared_b32 seed_bytes; + { + auto seed_acc = core->globals.account_seed(); + std::ranges::copy(std::as_bytes(seed_acc.seed()), seed_bytes.begin()); } + TempCore linker{core::predefined_seed{std::span{seed_bytes}}}; + auto link_msg = linker->devices.build_link_request().message; + const auto* p = reinterpret_cast(link_msg.data()); + std::vector outer_msg{p, p + link_msg.size()}; + + sent.callback(true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); + + // Verify last_hash was stored under this specific node's pubkey. + CHECK(TestHelper::namespace_last_hash(*core, 21, mock_net->current_node.remote_pubkey) == + "hash1"); + CHECK(received); - if (std::filesystem::exists(db_path)) - std::filesystem::remove(db_path); + // Poll again with the same node — should include last_hash. + mock_net->sent_requests.clear(); + TestHelper::poll(*core); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto req_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + CHECK(req_json2["params"]["last_hashes"]["21"] == "hash1"); } TEST_CASE( From 7c24552c53eeba7df07de61ca7c6e3aa71d74246 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:31:37 -0300 Subject: [PATCH 057/193] Cache long-term x25519 private scalar alongside the seed We were only keeping the Ed25519 secret key in Core::Globals's secure root key buffer, but we actually frequently need the X25519 private key as well to perform various message decryptions. This expands that buffer by 32 bytes (to 96) to pack the precomputed x25519 key in there as well -- previously we had to do an unlock + convert, but with this change we just unlock and have the converted value cached. --- include/session/core/globals.hpp | 28 +++++++++++++++++++++++++++- include/session/session_encrypt.hpp | 2 +- src/core.cpp | 2 +- src/core/devices.cpp | 12 ++++-------- src/core/globals.cpp | 14 ++++++++------ src/session_encrypt.cpp | 11 ++++------- 6 files changed, 45 insertions(+), 24 deletions(-) diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 804f4e2d0..5bb759aac 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -79,7 +79,33 @@ class Globals final : detail::CoreComponent { void set(std::string_view key, std::string_view text); void set(std::string_view key, std::span blob); - session::secure_buffer::r_accessor account_seed() { return _account_seed.access(); } + /// RAII accessor returned by account_seed(). Holds the underlying secure buffer open for + /// reading while alive; the buffer becomes unreadable again when the last copy is destroyed. + struct AccountSeedAccess { + private: + friend class Globals; + explicit AccountSeedAccess(const session::secure_buffer::r_accessor& acc) : _acc{acc} {} + session::secure_buffer::r_accessor _acc; + + auto ubuf() const { + return std::span{ + reinterpret_cast(_acc.buf.data()), 96}; + } + + public: + /// The raw 32-byte account seed (identical to ed25519_secret().first<32>()). + std::span seed() const { return ubuf().first<32>(); } + /// The 64-byte Ed25519 secret key in libsodium format (seed || pubkey). + std::span ed25519_secret() const { return ubuf().first<64>(); } + /// The 32-byte X25519 secret key derived from the account seed. This is also the clamped + /// private scalar of the Ed25519 key, usable for advanced scalar-multiplication operations. + std::span x25519_key() const { return ubuf().last<32>(); } + }; + + AccountSeedAccess account_seed() { + auto acc = _account_seed.access(); + return AccountSeedAccess{acc}; + } // These are computed from the account_seed during construction: std::span session_id() { return _session_id; } const network::ed25519_pubkey& pubkey_ed25519() const { return _pubkey_ed25519; } diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 87837a8ad..11a55cd5e 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -149,7 +149,7 @@ std::vector encrypt_for_blinded_recipient( /// - The encrypted v2 ciphertext to send to the swarm. /// - Throws on invalid keys or encryption failure. std::vector encrypt_for_recipient_v2( - std::span sender_ed25519_privkey, + const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, std::span recipient_account_x25519, std::span recipient_account_mlkem768, diff --git a/src/core.cpp b/src/core.cpp index 0e5247d62..eb768dc48 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -118,7 +118,7 @@ void Core::_poll() { nullptr, reinterpret_cast(to_sign.data()), to_sign.size(), - reinterpret_cast(seed.buf.data())); + seed.ed25519_secret().data()); auto sig_b64 = oxenc::to_base64(sig); net->get_swarm( diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 7a48ef8b8..88fc7ad8f 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -905,7 +905,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) nullptr, body.data(), body.size(), - reinterpret_cast(seed.buf.data())); + seed.ed25519_secret().data()); return sig; }); @@ -1052,7 +1052,7 @@ Devices::LinkRequestResult Devices::build_link_request() { auto seed = core.globals.account_seed(); config::encrypt_prealloced( as_span(std::span{encrypted}), - as_span(seed.buf).first(32), + seed.seed(), "link-request"); // Wrap in outer bt-dict: {"": "L", "L": } @@ -1227,7 +1227,7 @@ void Devices::receive_link_request(std::span data) { try { auto seed = core.globals.account_seed(); plaintext = config::decrypt( - encrypted, as_span(seed.buf).first(32), "link-request"); + encrypted, seed.seed(), "link-request"); } catch (const config::decrypt_error& e) { log::warning(cat, "Ignoring incoming link request: decryption failed: {}", e.what()); return; @@ -1573,11 +1573,7 @@ std::vector Devices::build_account_pubkey_message() { o.append("X", k.x25519_pub); o.append_signature( "~", [seed = core.globals.account_seed()](std::span body) { - cleared_uc32 x25519_priv; - crypto_sign_ed25519_sk_to_curve25519( - x25519_priv.data(), - reinterpret_cast(seed.buf.data())); - return xed25519::sign(x25519_priv, body); + return xed25519::sign(seed.x25519_key(), body); }); assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above diff --git a/src/core/globals.cpp b/src/core/globals.cpp index 6edcc4712..67a51f77f 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -135,12 +135,14 @@ void Globals::init() { } } - auto rw = _account_seed.resize(64); + auto rw = _account_seed.resize(96); + auto* rw_uc = reinterpret_cast(rw.buf.data()); crypto_sign_ed25519_seed_keypair( _pubkey_ed25519.data(), - reinterpret_cast(rw.buf.data()), + rw_uc, reinterpret_cast(seed_to_use->data())); + crypto_sign_ed25519_sk_to_curve25519(rw_uc + 64, rw_uc); _predefined_seed.reset(); // Clear now that it has been consumed if (0 != crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) @@ -153,7 +155,7 @@ void Globals::init() { if (!have_seed) { log::info(cat, "Generated new Session account seed"); - set("_seed", rw.buf); + set("_seed", rw.buf.first(32)); } log::info(cat, "Initialized with Session ID: {}", oxenc::to_hex(_session_id)); @@ -161,9 +163,9 @@ void Globals::init() { mnemonics::secure_mnemonic Globals::seed_mnemonic(const mnemonics::Mnemonics& lang, bool force_24) { auto seed = _account_seed.access(); - // _account_seed stores the 64-byte Ed25519 secret key, of which the first 32 bytes are the - // account seed. A Session account uses 128-bit entropy when the upper 16 bytes of that seed - // are all zero; in that case we encode only the lower 16 bytes. + // _account_seed stores the 96-byte key material; the first 32 bytes are the account seed. + // A Session account uses 128-bit entropy when the last 16 bytes of that seed are all zero; + // in that case we encode only the first 16 bytes. auto seed32 = seed.buf.first(32); bool is_128bit = !force_24 && sodium_memcmp(seed32.data() + 16, std::array{}.data(), 16) == 0; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index a95c2daf5..3a1331887 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -242,18 +242,14 @@ std::vector encrypt_for_recipient_deterministic( } std::vector encrypt_for_recipient_v2( - std::span sender_ed25519_privkey, + const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, std::span recipient_account_x25519, std::span recipient_account_mlkem768, std::span content, std::optional> pro_signature) { - // Expand 32-byte seed → 64-byte ed25519 key if needed. Storage is heap-allocated so the span - // remains valid if the pair is moved. - auto [ed_sk, _ed_sk_storage] = expand_ed25519_privkey(sender_ed25519_privkey); - // In libsodium's full ed25519 key layout, bytes [32:64] are the public key - std::span sender_ed_pk{ed_sk.data() + 32, 32}; + auto sender_ed_pk = sender_ed25519_privkey.pubkey(); // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) std::span S{recipient_session_id.data() + 1, 32}; @@ -345,7 +341,8 @@ std::vector encrypt_for_recipient_v2( hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); uc64 sig; if (0 != - crypto_sign_ed25519_detached(sig.data(), nullptr, h.data(), h.size(), ed_sk.data())) + crypto_sign_ed25519_detached( + sig.data(), nullptr, h.data(), h.size(), sender_ed25519_privkey.data())) throw std::runtime_error{"Failed to sign v2 message"}; return sig; }); From 1a6583ccb83b6a7690c89dd5716302e93f78c051 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 17:16:31 -0300 Subject: [PATCH 058/193] Fix test suite --- tests/live/live_utils.hpp | 2 +- tests/test_pfs_key_cache.cpp | 32 +++++----------- tests/test_poll.cpp | 71 ++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp index 2b51c91d3..49c942d65 100644 --- a/tests/live/live_utils.hpp +++ b/tests/live/live_utils.hpp @@ -70,7 +70,7 @@ inline std::vector build_account_pubkeys_store_params(session::co nullptr, reinterpret_cast(to_sign.data()), to_sign.size(), - reinterpret_cast(seed.buf.data())); + seed.ed25519_secret().data()); auto msg = core.devices.build_account_pubkey_message(); diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp index 7a42c4a14..11f3a69c3 100644 --- a/tests/test_pfs_key_cache.cpp +++ b/tests/test_pfs_key_cache.cpp @@ -15,28 +15,20 @@ using namespace session; using namespace std::literals; // Wraps a single bt-dict message payload as a mock AccountPubkeys retrieve response. +// prefetch_pfs_keys() sends a plain "retrieve" and expects {messages: [...]} at the top level. static nlohmann::json make_pubkey_response(std::span msg_data) { - nlohmann::json resp; - resp["results"] = nlohmann::json::array(); - nlohmann::json res_item; - res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); - res_item["messages"] = nlohmann::json::array(); nlohmann::json msg_item; msg_item["data"] = oxenc::to_base64( std::string_view{reinterpret_cast(msg_data.data()), msg_data.size()}); - res_item["messages"].push_back(msg_item); - resp["results"].push_back(res_item); + nlohmann::json resp; + resp["messages"] = nlohmann::json::array({std::move(msg_item)}); return resp; } // Returns an AccountPubkeys response body with no messages. static nlohmann::json make_empty_response() { nlohmann::json resp; - resp["results"] = nlohmann::json::array(); - nlohmann::json res_item; - res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); - res_item["messages"] = nlohmann::json::array(); - resp["results"].push_back(res_item); + resp["messages"] = nlohmann::json::array(); return resp; } @@ -66,11 +58,10 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] c->prefetch_pfs_keys(sid); REQUIRE(mock_net->sent_requests.size() == 1); + CHECK(mock_net->sent_requests[0].request.endpoint == "retrieve"); auto req = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); - CHECK(req["method"] == "retrieve"); - CHECK(req["params"]["pubkey"] == oxenc::to_hex(sid)); - CHECK(req["params"]["namespaces"] == - nlohmann::json::array({static_cast(config::Namespace::AccountPubkeys)})); + CHECK(req["pubkey"] == oxenc::to_hex(sid)); + CHECK(req["namespace"] == static_cast(config::Namespace::AccountPubkeys)); mock_net->sent_requests[0].callback( true, false, 200, {}, make_pubkey_response(remote_msg).dump()); @@ -235,15 +226,10 @@ TEST_CASE("prefetch_pfs_keys handles malformed responses gracefully", "[core][pf c->prefetch_pfs_keys(sid); REQUIRE(mock_net->sent_requests.size() == 1); - nlohmann::json bad_resp; - bad_resp["results"] = nlohmann::json::array(); - nlohmann::json res_item; - res_item["namespace"] = static_cast(config::Namespace::AccountPubkeys); - res_item["messages"] = nlohmann::json::array(); nlohmann::json msg_item; msg_item["data"] = oxenc::to_base64("not a bt-dict"); - res_item["messages"].push_back(msg_item); - bad_resp["results"].push_back(res_item); + nlohmann::json bad_resp; + bad_resp["messages"] = nlohmann::json::array({std::move(msg_item)}); mock_net->sent_requests[0].callback(true, false, 200, {}, bad_resp.dump()); auto entry = TestHelper::pfs_cache_entry(*c, sid); diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 141cfcd22..b83ee2a5d 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -10,19 +10,25 @@ using namespace session; -// Helpers to build a mock retrieve response carrying a single message in one namespace. +// Helpers to build a mock batch response carrying a single message in the first result slot. +// _poll() sends a two-namespace batch (Devices at index 0, AccountPubkeys at index 1) and +// processes results positionally, so msg_data/hash go in results[0]; results[1] is empty. +// The ns parameter is accepted for readability at call sites but is otherwise unused. static nlohmann::json make_response( - int16_t ns, std::vector msg_data, std::string hash) { - nlohmann::json response; - response["results"] = nlohmann::json::array(); - nlohmann::json res_item; - res_item["namespace"] = ns; - res_item["messages"] = nlohmann::json::array(); + int16_t /*ns*/, std::vector msg_data, std::string hash) { nlohmann::json msg_item; msg_item["data"] = oxenc::to_base64(msg_data); msg_item["hash"] = std::move(hash); - res_item["messages"].push_back(msg_item); - response["results"].push_back(res_item); + + nlohmann::json body0; + body0["messages"] = nlohmann::json::array({std::move(msg_item)}); + nlohmann::json body1; + body1["messages"] = nlohmann::json::array(); + + nlohmann::json response; + response["results"] = nlohmann::json::array( + {nlohmann::json{{"code", 200}, {"body", std::move(body0)}}, + nlohmann::json{{"code", 200}, {"body", std::move(body1)}}}); return response; } @@ -46,15 +52,24 @@ TEST_CASE("Core automatic polling", "[core][poll]") { REQUIRE(mock_net->sent_requests.size() == 1); auto& sent = mock_net->sent_requests[0]; - auto req_json = nlohmann::json::parse(*sent.request.body); - CHECK(req_json["method"] == "retrieve"); - auto& params = req_json["params"]; + CHECK(sent.request.endpoint == "batch"); + auto batch_json = nlohmann::json::parse(*sent.request.body); + auto& reqs = batch_json["requests"]; + REQUIRE(reqs.size() == 2); + // Subrequest 0: Devices (ns 21) — requires auth. + CHECK(reqs[0]["method"] == "retrieve"); + auto& params = reqs[0]["params"]; CHECK(params["pubkey"] == oxenc::to_hex(core->globals.session_id())); - CHECK(params["namespaces"] == nlohmann::json::array({21, -21})); + CHECK(params["namespace"] == 21); + CHECK(params.contains("pubkey_ed25519")); CHECK(params.contains("timestamp")); CHECK(params.contains("signature")); - // No prior hash for this node yet, so no last_hashes in request. - CHECK_FALSE(params.contains("last_hashes")); + // No prior hash for this node yet, so no last_hash in either subrequest. + CHECK_FALSE(params.contains("last_hash")); + // Subrequest 1: AccountPubkeys (ns -21) — no auth required. + CHECK(reqs[1]["method"] == "retrieve"); + CHECK(reqs[1]["params"]["namespace"] == -21); + CHECK_FALSE(reqs[1]["params"].contains("signature")); // Build a valid link request from a second device sharing the same account seed. cleared_b32 seed_bytes; @@ -74,13 +89,13 @@ TEST_CASE("Core automatic polling", "[core][poll]") { "hash1"); CHECK(received); - // Poll again with the same node — should include last_hash. + // Poll again with the same node — should include last_hash in the Devices subrequest. mock_net->sent_requests.clear(); TestHelper::poll(*core); REQUIRE(mock_net->sent_requests.size() == 1); - auto req_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); - CHECK(req_json2["params"]["last_hashes"]["21"] == "hash1"); + auto batch_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + CHECK(batch_json2["requests"][0]["params"]["last_hash"] == "hash1"); } TEST_CASE( @@ -101,9 +116,9 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; - // No prior hash for any node — must not send last_hashes. - CHECK_FALSE(params.contains("last_hashes")); + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + // No prior hash for any node — must not send last_hash. + CHECK_FALSE(p.contains("last_hash")); } // Respond with hash "xyz" from node A. mock_net->sent_requests[0].callback( @@ -116,8 +131,8 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; - CHECK(params["last_hashes"]["21"] == "xyz"); + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + CHECK(p["last_hash"] == "xyz"); } // ── Third poll: switch to node B — no stored hash for B, so request everything ── @@ -126,9 +141,9 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; - // B has no recorded hash — must not send last_hashes so we get everything. - CHECK_FALSE(params.contains("last_hashes")); + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + // B has no recorded hash — must not send last_hash so we get everything. + CHECK_FALSE(p.contains("last_hash")); } // Respond with hash "zyx" from node B (the message that B happens to have seen first). mock_net->sent_requests[0].callback( @@ -143,7 +158,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto params = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["params"]; - CHECK(params["last_hashes"]["21"] == "xyz"); + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + CHECK(p["last_hash"] == "xyz"); } } From 551379fe6bfaf9f556d1d0108ef77ad549e6a89f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 00:06:47 -0300 Subject: [PATCH 059/193] Refactor to use fixed-extent spans for fixed-size quantities --- include/session/session_encrypt.h | 8 +- include/session/session_encrypt.hpp | 34 +++--- include/session/session_protocol.hpp | 2 +- src/session_encrypt.cpp | 159 +++++++++++++-------------- src/session_protocol.cpp | 38 +++---- tests/test_session_encrypt.cpp | 159 ++++++++++----------------- tests/test_session_protocol.cpp | 4 +- 7 files changed, 179 insertions(+), 225 deletions(-) diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index 4aa45e9c1..f19e80993 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -345,7 +345,7 @@ LIBSESSION_EXPORT bool session_decrypt_push_notification( /// Inputs: /// - `plaintext_in` -- [in] the data to encrypt. /// - `plaintext_len` -- [in] the length of `plaintext_in`. -/// - `enc_key_in` -- [in] the key to use for encryption (32 bytes). +/// - `key_in` -- [in] the 32-byte symmetric key. /// - `ciphertext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// encrypted data written to it, and then the pointer to that buffer is stored here. /// This buffer must be `free()`d by the caller when done with it *unless* the function returns @@ -358,7 +358,7 @@ LIBSESSION_EXPORT bool session_decrypt_push_notification( LIBSESSION_EXPORT bool session_encrypt_xchacha20( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* enc_key_in, /* 32 bytes */ + const unsigned char* key_in, /* 32 bytes */ unsigned char** ciphertext_out, size_t* ciphertext_len); @@ -369,7 +369,7 @@ LIBSESSION_EXPORT bool session_encrypt_xchacha20( /// Inputs: /// - `ciphertext_in` -- [in] the data to decrypt. /// - `ciphertext_len` -- [in] the length of `ciphertext_in`. -/// - `enc_key_in` -- [in] the key to use for decryption (32 bytes). +/// - `key_in` -- [in] the 32-byte symmetric key. /// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// decrypted data written to it, and then the pointer to that buffer is stored here. /// This buffer must be `free()`d by the caller when done with it *unless* the function returns @@ -382,7 +382,7 @@ LIBSESSION_EXPORT bool session_encrypt_xchacha20( LIBSESSION_EXPORT bool session_decrypt_xchacha20( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* enc_key_in, /* 32 bytes */ + const unsigned char* key_in, /* 32 bytes */ unsigned char** plaintext_out, size_t* plaintext_len); diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 11a55cd5e..408998888 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -109,8 +109,8 @@ std::vector encrypt_for_recipient_deterministic( /// - Throw if encryption fails or (which typically means invalid keys provided) std::vector encrypt_for_blinded_recipient( const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span recipient_blinded_id, + std::span server_pk, + std::span recipient_blinded_id, std::span message); /// API: crypto/encrypt_for_recipient_v2 @@ -303,7 +303,7 @@ static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// - `ciphertext` -- the encrypted, etc. value to send to the swarm std::vector encrypt_for_group( const Ed25519PrivKeySpan& user_ed25519_privkey, - std::span group_ed25519_pubkey, + std::span group_ed25519_pubkey, std::span group_enc_key, std::span plaintext, bool compress, @@ -375,8 +375,8 @@ std::pair, std::vector> decrypt_incomi /// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on /// error. std::pair, std::vector> decrypt_incoming( - std::span x25519_pubkey, - std::span x25519_seckey, + std::span x25519_pubkey, + std::span x25519_seckey, std::span ciphertext); /// API: crypto/decrypt_incoming @@ -413,8 +413,8 @@ std::pair, std::string> decrypt_incoming_session_id( /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. std::pair, std::string> decrypt_incoming_session_id( - std::span x25519_pubkey, - std::span x25519_seckey, + std::span x25519_pubkey, + std::span x25519_seckey, std::span ciphertext); /// API: crypto/decrypt_from_blinded_recipient @@ -440,9 +440,9 @@ std::pair, std::string> decrypt_incoming_session_id( /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. std::pair, std::string> decrypt_from_blinded_recipient( const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span sender_id, - std::span recipient_id, + std::span server_pk, + std::span sender_id, + std::span recipient_id, std::span ciphertext); struct DecryptGroupMessage { @@ -479,7 +479,7 @@ struct DecryptGroupMessage { /// it throws. DecryptGroupMessage decrypt_group_message( std::span> decrypt_ed25519_privkey_list, - std::span group_ed25519_pubkey, + std::span group_ed25519_pubkey, std::span ciphertext); /// API: crypto/decrypt_ons_response @@ -497,7 +497,7 @@ DecryptGroupMessage decrypt_group_message( std::string decrypt_ons_response( std::string_view lowercase_name, std::span ciphertext, - std::optional> nonce); + std::optional> nonce); /// API: crypto/decrypt_push_notification /// @@ -513,7 +513,7 @@ std::string decrypt_ons_response( /// was /// successful. Throws on error/failure. std::vector decrypt_push_notification( - std::span payload, std::span enc_key); + std::span payload, std::span enc_key); /// API: crypto/encrypt_xchacha20 /// @@ -521,12 +521,12 @@ std::vector decrypt_push_notification( /// /// Inputs: /// - `plaintext` -- the data to encrypt. -/// - `enc_key` -- the key to use for encryption (32 bytes). +/// - `key` -- the 32-byte symmetric key. /// /// Outputs: /// - `std::vector` -- the resulting ciphertext. std::vector encrypt_xchacha20( - std::span plaintext, std::span enc_key); + std::span plaintext, std::span key); /// API: crypto/decrypt_xchacha20 /// @@ -534,11 +534,11 @@ std::vector encrypt_xchacha20( /// /// Inputs: /// - `ciphertext` -- the data to decrypt. -/// - `enc_key` -- the key to use for decryption (32 bytes). +/// - `key` -- the 32-byte symmetric key. /// /// Outputs: /// - `std::vector` -- the resulting plaintext. std::vector decrypt_xchacha20( - std::span ciphertext, std::span enc_key); + std::span ciphertext, std::span key); } // namespace session diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index bebb2b15d..aa0a1534a 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -326,7 +326,7 @@ struct DecodeEnvelopeKey { // payload is encrypted (e.g. groups v2) and that the contents are unencrypted. If this key is // not set the it's assumed the envelope is not encrypted but the contents are encrypted (e.g.: // 1o1 or legacy group). - std::optional> group_ed25519_pubkey; + std::optional> group_ed25519_pubkey; // List of libsodium-style secret key to decrypt the envelope from. Can also be passed as a 32 // byte secret key. The public key component is not used. diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 3a1331887..067a1d584 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -490,9 +490,9 @@ DecryptV2Result decrypt_incoming_v2( // sending -- true if this for a message from A to B, false if this is from B to A. static cleared_uc32 blinded_shared_secret( std::span seed, - std::span kA, - std::span jB, - std::span server_pk, + std::span kA_prefixed, + std::span jB_prefixed, + std::span server_pk, bool sending) { // Because we're doing this generically, we use notation a/A/k for ourselves and b/jB for the @@ -505,23 +505,17 @@ static cleared_uc32 blinded_shared_secret( if (seed.size() != 64 && seed.size() != 32) throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - if (server_pk.size() != 32) - throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; - if (kA.size() != 33) - throw std::invalid_argument{"Invalid local blinded id: expected 33 bytes"}; - if (jB.size() != 33) - throw std::invalid_argument{"Invalid remote blinded id: expected 33 bytes"}; - if (kA[0] == 0x15 && jB[0] == 0x15) + if (kA_prefixed[0] == 0x15 && jB_prefixed[0] == 0x15) blinded_key_pair = blind15_key_pair(seed, server_pk, &k); - else if (kA[0] == 0x25 && jB[0] == 0x25) + else if (kA_prefixed[0] == 0x25 && jB_prefixed[0] == 0x25) blinded_key_pair = blind25_key_pair(seed, server_pk, &k); else throw std::invalid_argument{"Both ids must start with the same 0x15 or 0x25 prefix"}; - bool blind25 = kA[0] == 0x25; + bool blind25 = kA_prefixed[0] == 0x25; - kA = kA.subspan(1); - jB = jB.subspan(1); + auto kA = kA_prefixed.subspan<1>(); + auto jB = jB_prefixed.subspan<1>(); cleared_uc32 ka; // Not really switching to x25519 here, this is just an easy way to compute `a` @@ -549,13 +543,9 @@ static cleared_uc32 blinded_shared_secret( std::vector encrypt_for_blinded_recipient( const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span recipient_blinded_id, + std::span server_pk, + std::span recipient_blinded_id, std::span message) { - if (server_pk.size() != 32) - throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; - if (recipient_blinded_id.size() != 33) - throw std::invalid_argument{"Invalid recipient_blinded_id: expected 33 bytes"}; // Generate the blinded key pair & shared encryption key std::pair blinded_key_pair; @@ -576,7 +566,11 @@ std::vector encrypt_for_blinded_recipient( blinded_id.end(), blinded_key_pair.first.begin(), blinded_key_pair.first.end()); auto enc_key = blinded_shared_secret( - ed25519_privkey, blinded_id, recipient_blinded_id, server_pk, true); + ed25519_privkey, + std::span{blinded_id.data(), 33}, + recipient_blinded_id, + server_pk, + true); // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) std::vector buf; @@ -626,7 +620,7 @@ static constexpr size_t GROUPS_ENCRYPT_OVERHEAD = std::vector encrypt_for_group( const Ed25519PrivKeySpan& user_ed25519_privkey, - std::span group_ed25519_pubkey, + std::span group_ed25519_pubkey, std::span group_enc_key, std::span plaintext, bool compress, @@ -634,10 +628,9 @@ std::vector encrypt_for_group( if (plaintext.size() > GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE) throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; + static_assert(decltype(group_ed25519_pubkey)::extent == crypto_sign_ed25519_PUBLICKEYBYTES); if (group_enc_key.size() != 32 && group_enc_key.size() != 64) throw std::invalid_argument{"Invalid group_enc_key: expected 32 or 64 bytes"}; - if (group_ed25519_pubkey.size() != crypto_sign_ed25519_PUBLICKEYBYTES) - throw std::invalid_argument{"Invalid group_ed25519_pubkey: expected 32 bytes"}; std::vector _compressed; if (compress) { @@ -740,8 +733,8 @@ std::pair, std::string> decrypt_incoming_session_id( } std::pair, std::string> decrypt_incoming_session_id( - std::span x25519_pubkey, - std::span x25519_seckey, + std::span x25519_pubkey, + std::span x25519_seckey, std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(x25519_pubkey, x25519_seckey, ciphertext); @@ -772,8 +765,8 @@ std::pair, std::vector> decrypt_incomi } std::pair, std::vector> decrypt_incoming( - std::span x25519_pubkey, - std::span x25519_seckey, + std::span x25519_pubkey, + std::span x25519_seckey, std::span ciphertext) { if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) @@ -812,9 +805,9 @@ std::pair, std::vector> decrypt_incomi std::pair, std::string> decrypt_from_blinded_recipient( const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span sender_id, - std::span recipient_id, + std::span server_pk, + std::span sender_id, + std::span recipient_id, std::span ciphertext) { auto ed_pk = ed25519_privkey.pubkey(); if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + @@ -908,13 +901,12 @@ std::pair, std::string> decrypt_from_blinded_recipien DecryptGroupMessage decrypt_group_message( std::span> decrypt_ed25519_privkey_list, - std::span group_ed25519_pubkey, + std::span group_ed25519_pubkey, std::span ciphertext) { + static_assert(decltype(group_ed25519_pubkey)::extent == crypto_sign_ed25519_PUBLICKEYBYTES); DecryptGroupMessage result = {}; if (ciphertext.size() < GROUPS_ENCRYPT_OVERHEAD) throw std::runtime_error{"ciphertext is too small to be encrypted data"}; - if (group_ed25519_pubkey.size() != crypto_sign_ed25519_PUBLICKEYBYTES) - throw std::invalid_argument{"Invalid decrypt_ed25519_privkey: expected 32 bytes"}; // Note we only use the secret key of the decrypt_ed25519_privkey so we don't care about // generating the pubkey component if the user only passed in a 32 byte libsodium-style secret @@ -1044,7 +1036,7 @@ DecryptGroupMessage decrypt_group_message( std::string decrypt_ons_response( std::string_view lowercase_name, std::span ciphertext, - std::optional> nonce) { + std::optional> nonce) { // Handle old Argon2-based encryption used before HF16 if (!nonce) { if (ciphertext.size() < crypto_secretbox_MACBYTES) @@ -1077,10 +1069,9 @@ std::string decrypt_ons_response( return session_id; } + static_assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES == 24); if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; - if (nonce->size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) - throw std::invalid_argument{"Invalid nonce: expected to be 24 bytes"}; // Hash the ONS name using BLAKE2b // @@ -1123,12 +1114,10 @@ std::string decrypt_ons_response( } std::vector decrypt_push_notification( - std::span payload, std::span enc_key) { + std::span payload, std::span enc_key) { if (payload.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; - if (enc_key.size() != 32) - throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; std::vector buf; std::vector nonce; @@ -1193,9 +1182,7 @@ std::string compute_hash_blake2b_b64(std::vector parts) { } std::vector encrypt_xchacha20( - std::span plaintext, std::span enc_key) { - if (enc_key.size() != 32) - throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; + std::span plaintext, std::span key) { std::vector ciphertext; ciphertext.resize( @@ -1218,20 +1205,18 @@ std::vector encrypt_xchacha20( 0, // additional data nullptr, // nsec (always unused) reinterpret_cast(ciphertext.data()), - enc_key.data()); + key.data()); assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); return ciphertext; } std::vector decrypt_xchacha20( - std::span ciphertext, std::span enc_key) { + std::span ciphertext, std::span key) { if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; - if (enc_key.size() != 32) - throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -1250,7 +1235,7 @@ std::vector decrypt_xchacha20( nullptr, 0, // additional data nonce.data(), - enc_key.data())) + key.data())) throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; assert(mlen <= plaintext.size()); plaintext.resize(mlen); @@ -1259,6 +1244,16 @@ std::vector decrypt_xchacha20( } // namespace session +// Helpers: construct const unsigned char spans from raw C pointers. +// Used in C API wrappers to avoid verbose std::span{ptr, N} casts. +template +static constexpr std::span cspan(const unsigned char* p) noexcept { + return std::span(p, N); +} +static std::span cspan(const unsigned char* p, size_t n) noexcept { + return {p, n}; +} + extern "C" { using namespace session; @@ -1272,9 +1267,9 @@ LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_recipient_deterministic( - std::span{ed25519_privkey, 64}, - std::span{recipient_pubkey, 32}, - std::span{plaintext_in, plaintext_len}); + cspan<64>(ed25519_privkey), + cspan<32>(recipient_pubkey), + cspan(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1295,10 +1290,10 @@ LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_blinded_recipient( - std::span{ed25519_privkey, 64}, - std::span{community_pubkey, 32}, - std::span{recipient_blinded_id, 33}, - std::span{plaintext_in, plaintext_len}); + cspan<64>(ed25519_privkey), + cspan<32>(community_pubkey), + cspan<33>(recipient_blinded_id), + cspan(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1326,9 +1321,9 @@ LIBSESSION_C_API session_encrypt_group_message session_encrypt_for_group( try { std::vector result_cpp = encrypt_for_group( Ed25519PrivKeySpan::from(user_ed25519_privkey, user_ed25519_privkey_len), - {group_ed25519_pubkey, group_ed25519_pubkey_len}, - {group_enc_key, group_enc_key_len}, - {plaintext, plaintext_len}, + cspan<32>(group_ed25519_pubkey), + cspan(group_enc_key, group_enc_key_len), + cspan(plaintext, plaintext_len), compress, padding); result = { @@ -1354,8 +1349,8 @@ LIBSESSION_C_API bool session_decrypt_incoming( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - std::span{ed25519_privkey, 64}, - std::span{ciphertext_in, ciphertext_len}); + cspan<64>(ed25519_privkey), + cspan(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1378,9 +1373,9 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - std::span{x25519_pubkey, 32}, - std::span{x25519_seckey, 32}, - std::span{ciphertext_in, ciphertext_len}); + cspan<32>(x25519_pubkey), + cspan<32>(x25519_seckey), + cspan(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1405,11 +1400,11 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( size_t* plaintext_len) { try { auto result = session::decrypt_from_blinded_recipient( - std::span{ed25519_privkey, 64}, - std::span{community_pubkey, 32}, - std::span{sender_id, 33}, - std::span{recipient_id, 33}, - std::span{ciphertext_in, ciphertext_len}); + cspan<64>(ed25519_privkey), + cspan<32>(community_pubkey), + cspan<33>(sender_id), + cspan<33>(recipient_id), + cspan(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1440,8 +1435,8 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess try { result_cpp = decrypt_group_message( {&key, 1}, - {group_ed25519_pubkey, group_ed25519_pubkey_len}, - {ciphertext, ciphertext_len}); + cspan<32>(group_ed25519_pubkey), + cspan(ciphertext, ciphertext_len)); result = { .success = true, .index = index, @@ -1469,13 +1464,13 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* nonce_in, char* session_id_out) { try { - std::optional> nonce; + std::optional> + nonce; if (nonce_in) - nonce = std::span{ - nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + nonce = cspan(nonce_in); auto session_id = session::decrypt_ons_response( - name_in, std::span{ciphertext_in, ciphertext_len}, nonce); + name_in, cspan(ciphertext_in, ciphertext_len), nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -1492,8 +1487,8 @@ LIBSESSION_C_API bool session_decrypt_push_notification( size_t* plaintext_len) { try { auto plaintext = session::decrypt_push_notification( - std::span{payload_in, payload_len}, - std::span{enc_key_in, 32}); + cspan(payload_in, payload_len), + cspan<32>(enc_key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); @@ -1507,13 +1502,13 @@ LIBSESSION_C_API bool session_decrypt_push_notification( LIBSESSION_C_API bool session_encrypt_xchacha20( const unsigned char* plaintext_in, size_t plaintext_len, - const unsigned char* enc_key_in, + const unsigned char* key_in, unsigned char** ciphertext_out, size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_xchacha20( - std::span{plaintext_in, plaintext_len}, - std::span{enc_key_in, 32}); + cspan(plaintext_in, plaintext_len), + cspan<32>(key_in)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1527,13 +1522,13 @@ LIBSESSION_C_API bool session_encrypt_xchacha20( LIBSESSION_C_API bool session_decrypt_xchacha20( const unsigned char* ciphertext_in, size_t ciphertext_len, - const unsigned char* enc_key_in, + const unsigned char* key_in, unsigned char** plaintext_out, size_t* plaintext_len) { try { auto plaintext = session::decrypt_xchacha20( - std::span{ciphertext_in, ciphertext_len}, - std::span{enc_key_in, 32}); + cspan(ciphertext_in, ciphertext_len), + cspan<32>(key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index f78d7880f..a79733725 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -386,20 +386,14 @@ static EncryptedForDestinationInternal encode_for_destination_internal( const Ed25519PrivKeySpan* ed25519_privkey, DestinationType dest_type, std::span dest_pro_rotating_ed25519_privkey, - std::span dest_recipient_pubkey, + std::span dest_recipient_pubkey, std::chrono::milliseconds dest_sent_timestamp_ms, - std::span dest_community_inbox_server_pubkey, - std::span dest_group_ed25519_pubkey, + std::span + dest_community_inbox_server_pubkey, + std::span + dest_group_ed25519_pubkey, std::span dest_group_enc_key, UseMalloc use_malloc) { - // The following arguments are passed in from structs with fixed-sized arrays so we expect the - // sizes to be correct. It being wrong would be a development error - // - // The ed25519_privkey is passed into the lower level layer, session encrypt which has its own - // private key normalisation to 64 bytes for us. - assert(dest_recipient_pubkey.size() == 1 + crypto_sign_ed25519_PUBLICKEYBYTES); - assert(dest_community_inbox_server_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES); - assert(dest_group_ed25519_pubkey.size() == 1 + crypto_sign_ed25519_PUBLICKEYBYTES); assert(dest_group_enc_key.size() == 32 || dest_group_enc_key.size() == 64); bool is_group = dest_type == DestinationType::Group; @@ -492,12 +486,9 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (is_group) { std::string bytes = envelope.SerializeAsString(); - if (dest_group_ed25519_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES + 1) - dest_group_ed25519_pubkey = dest_group_ed25519_pubkey.subspan(1); - std::vector ciphertext = encrypt_for_group( *ed25519_privkey, - dest_group_ed25519_pubkey, + dest_group_ed25519_pubkey.subspan<1>(), dest_group_enc_key, to_span(bytes), /*compress*/ true, @@ -601,7 +592,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( std::vector ciphertext = encrypt_for_blinded_recipient( *ed25519_privkey, dest_community_inbox_server_pubkey, - dest_recipient_pubkey, // recipient blinded pubkey + dest_recipient_pubkey, // 33-byte blinded pubkey content); if (use_malloc == UseMalloc::Yes) { @@ -1444,9 +1435,18 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( // Setup decryption keys and decrypt DecodeEnvelopeKey keys_cpp = {}; - if (keys->group_ed25519_pubkey.size) { - keys_cpp.group_ed25519_pubkey = std::span( - keys->group_ed25519_pubkey.data, keys->group_ed25519_pubkey.size); + if (keys->group_ed25519_pubkey.size == crypto_sign_ed25519_PUBLICKEYBYTES) { + keys_cpp.group_ed25519_pubkey = std::span{ + keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; + } else if (keys->group_ed25519_pubkey.size) { + result.error_len_incl_null_terminator = + snprintf_clamped( + error, + error_len, + "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: %zu", + keys->group_ed25519_pubkey.size) + + 1; + return result; } DecodedEnvelope result_cpp = {}; diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index d445ceb89..337961d5f 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -155,8 +155,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e using namespace session; const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - const auto server_pk = - "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hexbytes; + constexpr auto server_pk = + "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hex_u; std::array ed_pk, curve_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); @@ -171,8 +171,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); REQUIRE(sid_raw == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); - auto [blind15_pk, blind15_sk] = blind15_key_pair(to_span(ed_sk), to_span(server_pk)); - auto [blind25_pk, blind25_sk] = blind25_key_pair(to_span(ed_sk), to_span(server_pk)); + auto [blind15_pk, blind15_sk] = blind15_key_pair(to_span(ed_sk), server_pk); + auto [blind25_pk, blind25_sk] = blind25_key_pair(to_span(ed_sk), server_pk); auto blind15_pk_prefixed = prefixed(0x15, blind15_pk); auto blind25_pk_prefixed = prefixed(0x25, blind25_pk); @@ -191,37 +191,24 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); REQUIRE(sid_raw2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); - auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_span(ed_sk2), to_span(server_pk)); - auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_span(ed_sk2), to_span(server_pk)); + auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_span(ed_sk2), server_pk); + auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_span(ed_sk2), server_pk); auto blind15_pk2_prefixed = prefixed(0x15, blind15_pk2); auto blind25_pk2_prefixed = prefixed(0x25, blind25_pk2); SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( to_span(ed_sk), - to_span(server_pk), - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - to_span(server_pk), - to_span(blind15_pk), - {blind15_pk2_prefixed.data(), 33}, - enc)); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - to_span(blind15_pk2), - enc)); - auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk2), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); @@ -230,9 +217,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk2), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, sender decrypt") { @@ -245,8 +232,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( to_span(ed_sk).first<32>(), - to_span(server_pk), - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), @@ -256,9 +243,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk).first<32>(), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == lorem_ipsum); @@ -267,9 +254,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk).first<32>(), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, recipient decrypt") { @@ -282,8 +269,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( to_span(ed_sk).first<32>(), - to_span(server_pk), - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), @@ -293,9 +280,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk2).first<32>(), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == lorem_ipsum); @@ -304,37 +291,24 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk2).first<32>(), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, broken)); } SECTION("blind25, full secret, sender decrypt") { auto enc = encrypt_for_blinded_recipient( to_span(ed_sk), - to_span(server_pk), - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk), - to_span(server_pk), - to_span(blind25_pk), - {blind25_pk2_prefixed.data(), 33}, - enc)); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - to_span(blind25_pk2), - enc)); - auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); @@ -343,37 +317,24 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, broken)); } SECTION("blind25, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( to_span(ed_sk), - to_span(server_pk), - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - to_span(server_pk), - to_span(blind25_pk), - {blind25_pk2_prefixed.data(), 33}, - enc)); - CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - to_span(blind25_pk2), - enc)); - auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk2), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); @@ -382,9 +343,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk2), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, broken)); } SECTION("blind25, only seed, recipient decrypt") { @@ -397,8 +358,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( to_span(ed_sk).first<32>(), - to_span(server_pk), - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), @@ -408,9 +369,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e auto [msg, sender] = decrypt_from_blinded_recipient( to_span(ed_sk2).first<32>(), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == lorem_ipsum); @@ -419,9 +380,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( to_span(ed_sk2).first<32>(), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, broken)); } } @@ -435,14 +396,13 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { "1580d9a8c9b8a64cacfec97"_hexbytes; auto ciphertext_legacy = "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; - auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + constexpr auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hex_u; CHECK(decrypt_ons_response(name, ciphertext, nonce) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK(decrypt_ons_response(name, ciphertext_legacy, std::nullopt) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK_THROWS(decrypt_ons_response(name, to_span("invalid"), nonce)); - CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_span("invalid"))); } TEST_CASE("Session ONS response decryption C API", "[session-ons][session_decrypt_ons_response]") { @@ -476,12 +436,12 @@ TEST_CASE("Session push notification decryption", "[session-notification][decryp auto payload_padded = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" "ab4aca5f0991e99eb0344ceeafa"_hexbytes; - auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + constexpr auto enc_key = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_u; CHECK(decrypt_push_notification(payload, enc_key) == to_vector("TestMessage")); CHECK(decrypt_push_notification(payload_padded, enc_key) == to_vector("TestMessage")); CHECK_THROWS(decrypt_push_notification(to_span("invalid"), enc_key)); - CHECK_THROWS(decrypt_push_notification(payload, to_span("invalid"))); } TEST_CASE("xchacha20", "[session][xchacha20]") { @@ -489,15 +449,14 @@ TEST_CASE("xchacha20", "[session][xchacha20]") { auto payload = "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; - auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + constexpr auto enc_key = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_u; CHECK(decrypt_xchacha20(payload, enc_key) == to_vector("TestMessage")); CHECK_THROWS(decrypt_xchacha20(to_span("invalid"), enc_key)); - CHECK_THROWS(decrypt_xchacha20(payload, to_span("invalid"))); auto ciphertext = encrypt_xchacha20(to_span("TestMessage"), enc_key); CHECK(decrypt_xchacha20(ciphertext, enc_key) == to_vector("TestMessage")); - CHECK_THROWS(encrypt_xchacha20(payload, to_span("invalid"))); } TEST_CASE("v2 PFS+PQ message encryption", "[session-protocol][encrypt][v2]") { diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index d1ed14be4..ff65e1eb8 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -895,8 +895,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { auto [decrypted_cipher, sender_id] = session::decrypt_from_blinded_recipient( keys.ed_sk1, community_pk, - {session_blind15_pk0.data, sizeof(session_blind15_pk0.data)}, - {session_blind15_pk1.data, sizeof(session_blind15_pk1.data)}, + session_blind15_pk0.data, + session_blind15_pk1.data, {encoded.ciphertext.data, encoded.ciphertext.size}); session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( From 24ac035b3d21c6efbfc93bc8b11be83c5c64d361 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 01:16:47 -0300 Subject: [PATCH 060/193] uint8_t -> unsigned char for byte values uint8_t is a typedef to unsigned char on all modern systems, but the *intent* is different: uint8_t says "I want an unsigned integer value", while "unsigned char" says "I want a consistent-signed byte value". This makes everything use the latter *except* for things that are specifically numeric values. --- include/session/pro_backend.h | 42 ++++---- include/session/pro_backend.hpp | 40 ++++---- include/session/random.hpp | 2 +- include/session/session_protocol.hpp | 54 +++++----- include/session/types.h | 11 +- src/attachments.cpp | 6 +- src/config/groups/keys.cpp | 14 +-- src/onionreq/hop_encryption.cpp | 8 +- src/pro_backend.cpp | 134 ++++++++++++------------ src/session_encrypt.cpp | 2 +- src/session_protocol.cpp | 147 +++++++++++++-------------- src/types.cpp | 2 +- tests/test_config_pro.cpp | 12 +-- tests/test_config_userprofile.cpp | 2 +- tests/test_pro_backend.cpp | 52 +++++----- tests/test_session_protocol.cpp | 6 +- 16 files changed, 265 insertions(+), 269 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index a07c9b087..1d73d5694 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -114,7 +114,7 @@ struct session_pro_backend_response_header { /// Array of error messages (NULL if no errors), with errors_count elements string8* errors; size_t errors_count; - uint8_t* internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch + unsigned char* internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch }; typedef struct session_pro_backend_to_json session_pro_backend_to_json; @@ -312,14 +312,14 @@ LIBSESSION_EXPORT session_pro_backend_master_rotating_signatures session_pro_backend_add_pro_payment_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) NON_NULL_ARG(2, 4, 7, 9); /// API: session_pro_backend/add_pro_payment_request_build_to_json @@ -332,14 +332,14 @@ session_pro_backend_add_pro_payment_request_build_sigs( LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) NON_NULL_ARG(2, 4, 7, 9); /// API: session_pro_backend/generate_pro_proof_request_build_sigs @@ -366,9 +366,9 @@ LIBSESSION_EXPORT session_pro_backend_master_rotating_signatures session_pro_backend_generate_pro_proof_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, uint64_t unix_ts_ms) NON_NULL_ARG(2, 4); @@ -382,9 +382,9 @@ session_pro_backend_generate_pro_proof_request_build_sigs( LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, uint64_t unix_ts_ms) NON_NULL_ARG(2, 4); @@ -408,7 +408,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build LIBSESSION_EXPORT session_pro_backend_signature session_pro_backend_get_pro_details_request_build_sig( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) NON_NULL_ARG(2); @@ -423,7 +423,7 @@ session_pro_backend_signature session_pro_backend_get_pro_details_request_build_ LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_get_pro_details_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) NON_NULL_ARG(2); @@ -533,14 +533,14 @@ session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details LIBSESSION_EXPORT session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint64_t refund_requested_unix_ts_ms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) NON_NULL_ARG(2, 7, 9); /// API: session_pro_backend/set_payment_refund_requested_request_build_to_json @@ -553,14 +553,14 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint64_t refund_requested_unix_ts_ms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) NON_NULL_ARG(2, 7, 9); /// API: session_pro_backend/set_payment_refund_requested_request_to_json diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 64d4b883a..2855f99fb 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -177,11 +177,11 @@ struct AddProPaymentRequest { /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. static MasterRotatingSignatures build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/AddProPaymentRequest::build_to_json /// @@ -203,11 +203,11 @@ struct AddProPaymentRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); }; /// The generated proof from the Session Pro backend that has been parsed from JSON. This structure @@ -270,8 +270,8 @@ struct GenerateProProofRequest { /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. static MasterRotatingSignatures build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, sys_ms unix_ts); /// API: pro/GenerateProProofRequest::build_to_json @@ -289,8 +289,8 @@ struct GenerateProProofRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, sys_ms unix_ts); /// API: pro/GenerateProProofRequest::to_json @@ -384,7 +384,7 @@ struct GetProDetailsRequest { /// - `uc64` - the 64-byte signature static uc64 build_sig( uint8_t version, - std::span master_privkey, + std::span master_privkey, sys_ms unix_ts, uint32_t count); @@ -403,7 +403,7 @@ struct GetProDetailsRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t version, - std::span master_privkey, + std::span master_privkey, sys_ms unix_ts, uint32_t count); @@ -607,12 +607,12 @@ struct SetPaymentRefundRequestedRequest { /// - `uc64` - the 64-byte signature static uc64 build_sig( uint8_t version, - std::span master_privkey, + std::span master_privkey, sys_ms unix_ts, sys_ms refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/SetPaymentRefundRequested::build_to_json /// @@ -637,12 +637,12 @@ struct SetPaymentRefundRequestedRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t version, - std::span master_privkey, + std::span master_privkey, sys_ms unix_ts, sys_ms refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/SetPaymentRefundRequested::to_json /// diff --git a/include/session/random.hpp b/include/session/random.hpp index ef539f618..8f1b3b2ac 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -24,7 +24,7 @@ struct CSRNG { uint64_t operator()() const { uint64_t i; - randombytes((uint8_t*)&i, sizeof(i)); + randombytes_buf(&i, sizeof(i)); return i; }; }; diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index aa0a1534a..9c00b88d5 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -78,8 +78,8 @@ enum class ProStatus { }; struct ProSignedMessage { - std::span sig; - std::span msg; + std::span sig; + std::span msg; }; class ProProof { @@ -117,7 +117,7 @@ class ProProof { /// /// Outputs: /// - `bool` - True if the given key was the signatory of the proof, false otherwise - bool verify_signature(const std::span& verify_pubkey) const; + bool verify_signature(const std::span& verify_pubkey) const; /// API: pro/Proof::verify_message /// @@ -132,7 +132,7 @@ class ProProof { /// /// Outputs: /// - `bool` - True if the message was signed by the embedded `rotating_pubkey` false otherwise. - bool verify_message(std::span sig, const std::span msg) const; + bool verify_message(std::span sig, const std::span msg) const; /// API: pro/Proof::is_active /// @@ -170,7 +170,7 @@ class ProProof { /// not set then this function can never return `ProStatus::InvalidUserSig` from the set of /// possible enum values. Otherwise this funtion can return all possible values. ProStatus status( - std::span verify_pubkey, + std::span verify_pubkey, sys_ms unix_ts, const std::optional& signed_msg); @@ -233,7 +233,7 @@ struct Destination { // Optional rotating Session Pro Ed25519 private key to sign the message with on behalf of the // caller. The Session Pro signature must _not_ be set in the plaintext content passed into the // encoding function. - std::span pro_rotating_ed25519_privkey; + std::span pro_rotating_ed25519_privkey; // The timestamp to assign to the message envelope std::chrono::milliseconds sent_timestamp_ms; @@ -283,7 +283,7 @@ struct DecodedEnvelope { Envelope envelope; // Decoded envelope content into plaintext with padding stripped - std::vector content_plaintext; + std::vector content_plaintext; // Sender public key extracted from the encrypted content payload. This is not set if the // envelope was a groups v2 envelope where the envelope was encrypted and only the x25519 pubkey @@ -307,7 +307,7 @@ struct DecodedCommunityMessage { std::optional envelope; // The protobuf encoded `Content` with padding stripped - std::vector content_plaintext; + std::vector content_plaintext; // The signature if it was present in the payload. If the envelope is set and the envelope has // the pro signature flag set, then this signature was extracted from the envelope. When the @@ -326,7 +326,7 @@ struct DecodeEnvelopeKey { // payload is encrypted (e.g. groups v2) and that the contents are unencrypted. If this key is // not set the it's assumed the envelope is not encrypted but the contents are encrypted (e.g.: // 1o1 or legacy group). - std::optional> group_ed25519_pubkey; + std::optional> group_ed25519_pubkey; // List of libsodium-style secret key to decrypt the envelope from. Can also be passed as a 32 // byte secret key. The public key component is not used. @@ -344,7 +344,7 @@ struct DecodeEnvelopeKey { // pass exactly 1 ed25519 private key for decryption but this function makes no pre // existing assumptions on the number of keys and will attempt all given keys specified // regardless until it finds one that successfully decrypts the envelope contents. - std::span> decrypt_keys; + std::span> decrypt_keys; }; /// API: session_protocol/pro_features_for_utf8 @@ -391,7 +391,7 @@ ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size); /// /// Pad a message to the required alignment for 1o1/community messages (160 bytes) including space /// for the padding-terminating byte. -std::vector pad_message(std::span payload); +std::vector pad_message(std::span payload); /// API: session_protocol/encode_for_1o1 /// @@ -420,12 +420,12 @@ std::vector pad_message(std::span payload); /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_1o1( - std::span plaintext, +std::vector encode_for_1o1( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, - std::optional> pro_rotating_ed25519_privkey); + std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_community_inbox /// @@ -454,13 +454,13 @@ std::vector encode_for_1o1( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_community_inbox( - std::span plaintext, +std::vector encode_for_community_inbox( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, const uc32& community_pubkey, - std::optional> pro_rotating_ed25519_privkey); + std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_community /// @@ -484,9 +484,9 @@ std::vector encode_for_community_inbox( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_community( - std::span plaintext, - std::optional> pro_rotating_ed25519_privkey); +std::vector encode_for_community( + std::span plaintext, + std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_group /// @@ -517,13 +517,13 @@ std::vector encode_for_community( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_group( - std::span plaintext, +std::vector encode_for_group( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, - std::optional> pro_rotating_ed25519_privkey); + std::optional> pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_destination /// @@ -552,8 +552,8 @@ std::vector encode_for_group( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_destination( - std::span plaintext, +std::vector encode_for_destination( + std::span plaintext, const Ed25519PrivKeySpan* ed25519_privkey, const Destination& dest); @@ -615,7 +615,7 @@ std::vector encode_for_destination( /// access to pro features if it's using any. DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, - std::span envelope_payload, + std::span envelope_payload, const uc32& pro_backend_pubkey); /// API: session_protocol/decode_for_community @@ -648,7 +648,7 @@ DecodedEnvelope decode_envelope( /// If the `status` is set to valid the the caller can proceed with entitling the envelope with /// access to pro features if it's using any. DecodedCommunityMessage decode_for_community( - std::span content_or_envelope_payload, + std::span content_or_envelope_payload, sys_ms unix_ts, const uc32& pro_backend_pubkey); diff --git a/include/session/types.h b/include/session/types.h index 5029cab33..57110522f 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -1,7 +1,6 @@ #pragma once #include -#include #ifdef __cplusplus extern "C" { @@ -17,7 +16,7 @@ extern "C" { /// C friendly buffer structure that is a pointer and length to a span of bytes. typedef struct span_u8 span_u8; struct span_u8 { - uint8_t* data; + unsigned char* data; size_t size; }; @@ -31,23 +30,23 @@ struct string8 { typedef struct bytes32 bytes32; struct bytes32 { - uint8_t data[32]; + unsigned char data[32]; }; typedef struct bytes33 bytes33; struct bytes33 { - uint8_t data[33]; + unsigned char data[33]; }; typedef struct bytes64 bytes64; struct bytes64 { - uint8_t data[64]; + unsigned char data[64]; }; /// Basic bump allocating arena typedef struct arena_t arena_t; struct arena_t { - uint8_t* data; + unsigned char* data; size_t size; size_t max; }; diff --git a/src/attachments.cpp b/src/attachments.cpp index 7df924a54..0ed3fb919 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -237,9 +237,9 @@ encrypt_buffer_init( std::span udata{ reinterpret_cast(data.data()), data.size()}; - const auto domain_byte = static_cast(domain); + const auto domain_byte = static_cast(domain); hash::blake2b_key( - nonce_key, std::span{&domain_byte, 1}, seed.first(32), udata); + nonce_key, std::span{&domain_byte, 1}, seed.first(32), udata); std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); inpos = udata.data(); @@ -311,7 +311,7 @@ std::array encrypt( std::array nonce_key; crypto_generichash_blake2b_state b_st; - const auto domain_byte = static_cast(domain); + const auto domain_byte = static_cast(domain); crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); crypto_generichash_blake2b_update( &b_st, reinterpret_cast(seed.data()), 32); diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index cbbc3bc4b..024795b91 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -1200,7 +1200,7 @@ std::pair> Keys::decrypt_message( bool decrypt_success = false; if (auto pending = pending_key(); pending) { try { - std::span> key_list = {&(*pending), 1}; + std::span> key_list = {&(*pending), 1}; decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); decrypt_success = true; } catch (const std::exception&) { @@ -1210,8 +1210,8 @@ std::pair> Keys::decrypt_message( if (!decrypt_success) { for (auto& k : keys_) { try { - std::span key = {k.key.data(), k.key.size()}; - std::span> key_list = {&key, 1}; + std::span key = {k.key.data(), k.key.size()}; + std::span> key_list = {&key, 1}; decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); decrypt_success = true; break; @@ -1352,9 +1352,9 @@ LIBSESSION_C_API size_t groups_keys_get_keys( auto keys = unbox(conf).group_keys(); size_t clamped_offset = std::min(keys.size(), offset); for (size_t index = clamped_offset; index < keys.size() && result < dest_size; index++) { - const std::span& src_key = keys[index]; + const auto& src_key = keys[index]; span_u8* dest_key = dest + result++; - dest_key->data = const_cast(src_key.data()); + dest_key->data = const_cast(src_key.data()); dest_key->size = src_key.size(); } } @@ -1364,8 +1364,8 @@ LIBSESSION_C_API size_t groups_keys_get_keys( LIBSESSION_C_API const span_u8 groups_keys_group_enc_key(const config_group_keys* conf) { span_u8 result = {}; try { - std::span key = unbox(conf).group_enc_key(); - result.data = const_cast(key.data()); + auto key = unbox(conf).group_enc_key(); + result.data = const_cast(key.data()); result.size = key.size(); assert(result.size == 32); } catch (const std::exception& e) { diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index 69a491c6c..5e0b8cdf7 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -27,9 +27,9 @@ namespace session::onionreq { namespace { // Derive shared secret from our (ephemeral) `seckey` and the other party's `pubkey` - std::array calculate_shared_secret( + std::array calculate_shared_secret( const network::x25519_seckey& seckey, const network::x25519_pubkey& pubkey) { - std::array secret; + std::array secret; if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); return secret; @@ -37,7 +37,7 @@ namespace { constexpr std::string_view salt{"LOKI"}; - std::array derive_symmetric_key( + std::array derive_symmetric_key( const network::x25519_seckey& seckey, const network::x25519_pubkey& pubkey) { auto key = calculate_shared_secret(seckey, pubkey); @@ -168,7 +168,7 @@ std::vector HopEncryption::decrypt_aesgcm( gcm_aes256_decrypt(&ctx, ciphertext.size(), plaintext.data(), ciphertext.data()); - std::array digest_out; + std::array digest_out; gcm_aes256_digest(&ctx, digest_out.size(), digest_out.data()); if (sodium_memcmp(digest_out.data(), digest_in.data(), GCM_DIGEST_SIZE) != 0) diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 95278e076..799446df0 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -134,7 +134,7 @@ bool json_require_fixed_bytes_from_hex( const nlohmann::json& j, std::string_view key, std::vector& errors, - std::span dest) { + std::span dest) { auto hex = json_require(j, key, errors); if (hex.starts_with("0X") || hex.starts_with("0x")) hex = hex.substr(2); @@ -186,11 +186,11 @@ std::string AddProPaymentRequest::to_json() const { MasterRotatingSignatures AddProPaymentRequest::build_sigs( std::uint8_t version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { uc32 master_pubkey; @@ -234,7 +234,7 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), rotating_privkey.subspan( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), - static_cast(payment_tx_provider), + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); @@ -257,11 +257,11 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( std::string AddProPaymentRequest::build_to_json( std::uint8_t version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { uc32 master_pubkey; @@ -361,8 +361,8 @@ std::string GenerateProProofRequest::to_json() const { MasterRotatingSignatures GenerateProProofRequest::build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, std::chrono::sys_time unix_ts) { cleared_uc64 master_from_seed; @@ -417,8 +417,8 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( std::string GenerateProProofRequest::build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + std::span master_privkey, + std::span rotating_privkey, std::chrono::sys_time unix_ts) { // Rederive keys from 32 byte seed if given cleared_uc64 master_from_seed; @@ -535,7 +535,7 @@ std::string GetProDetailsRequest::to_json() const { uc64 GetProDetailsRequest::build_sig( uint8_t version, - std::span master_privkey, + std::span master_privkey, std::chrono::sys_time unix_ts, uint32_t count) { cleared_uc64 master_from_seed; @@ -573,7 +573,7 @@ uc64 GetProDetailsRequest::build_sig( std::string GetProDetailsRequest::build_to_json( uint8_t version, - std::span master_privkey, + std::span master_privkey, std::chrono::sys_time unix_ts, uint32_t count) { cleared_uc64 master_from_seed; @@ -766,12 +766,12 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { uc64 SetPaymentRefundRequestedRequest::build_sig( uint8_t version, - std::span master_privkey, + std::span master_privkey, std::chrono::sys_time unix_ts, std::chrono::sys_time refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { cleared_uc64 master_from_seed; if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { uc32 master_pubkey; @@ -795,7 +795,7 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), unix_ts_ms, refund_requested_unix_ts_ms, - static_cast(payment_tx_provider), + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); @@ -812,12 +812,12 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( std::string SetPaymentRefundRequestedRequest::build_to_json( uint8_t version, - std::span master_privkey, + std::span master_privkey, std::chrono::sys_time unix_ts, std::chrono::sys_time refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { uc64 sig = SetPaymentRefundRequestedRequest::build_sig( version, master_privkey, @@ -910,22 +910,22 @@ static string8 C_PARSE_ERROR_INVALID_ARGS = STRING8_LIT("One or more C arguments LIBSESSION_C_API session_pro_backend_master_rotating_signatures session_pro_backend_add_pro_payment_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::span payment_tx_payment_id_span( + std::span master_span(master_privkey, master_privkey_len); + std::span rotating_span(rotating_privkey, rotating_privkey_len); + std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_master_rotating_signatures result = {}; try { @@ -954,23 +954,23 @@ session_pro_backend_add_pro_payment_request_build_sigs( LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { session_pro_backend_to_json result = {}; // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::span payment_tx_payment_id_span( + std::span master_span(master_privkey, master_privkey_len); + std::span rotating_span(rotating_privkey, rotating_privkey_len); + std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); try { std::string json = AddProPaymentRequest::build_to_json( @@ -998,15 +998,15 @@ session_pro_backend_add_pro_payment_request_build_to_json( LIBSESSION_C_API session_pro_backend_master_rotating_signatures session_pro_backend_generate_pro_proof_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, uint64_t unix_ts_ms) { // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); + std::span master_span(master_privkey, master_privkey_len); + std::span rotating_span(rotating_privkey, rotating_privkey_len); std::chrono::milliseconds ts{unix_ts_ms}; session_pro_backend_master_rotating_signatures result = {}; @@ -1034,14 +1034,14 @@ session_pro_backend_generate_pro_proof_request_build_sigs( LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, - const uint8_t* rotating_privkey, + const unsigned char* rotating_privkey, size_t rotating_privkey_len, uint64_t unix_ts_ms) { // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); + std::span master_span(master_privkey, master_privkey_len); + std::span rotating_span(rotating_privkey, rotating_privkey_len); std::chrono::milliseconds ts{unix_ts_ms}; session_pro_backend_to_json result = {}; @@ -1068,12 +1068,12 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build LIBSESSION_C_API session_pro_backend_signature session_pro_backend_get_pro_details_request_build_sig( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) { // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + std::span master_span{master_privkey, master_privkey_len}; std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; session_pro_backend_signature result = {}; @@ -1096,12 +1096,12 @@ session_pro_backend_get_pro_details_request_build_sig( LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) { // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + std::span master_span{master_privkey, master_privkey_len}; std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; session_pro_backend_to_json result = {}; @@ -1274,7 +1274,7 @@ session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); + arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { result.header.status = 1; @@ -1339,7 +1339,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); + arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { result.header.status = 1; @@ -1400,7 +1400,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); + arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { result.header.status = 1; @@ -1495,23 +1495,23 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json LIBSESSION_C_API session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint64_t refund_requested_unix_ts_ms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + std::span master_span{master_privkey, master_privkey_len}; std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; std::chrono::sys_time refund_requested_unix_ts{ std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - std::span payment_tx_payment_id_span( + std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_signature result = {}; try { @@ -1540,24 +1540,24 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_request_build_to_json( uint8_t request_version, - const uint8_t* master_privkey, + const unsigned char* master_privkey, size_t master_privkey_len, uint64_t unix_ts_ms, uint64_t refund_requested_unix_ts_ms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - const uint8_t* payment_tx_payment_id, + const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, - const uint8_t* payment_tx_order_id, + const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + std::span master_span{master_privkey, master_privkey_len}; std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; std::chrono::sys_time refund_requested_unix_ts{ std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - std::span payment_tx_payment_id_span( + std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_to_json result = {}; try { @@ -1643,7 +1643,7 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); + arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { result.header.status = 1; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 067a1d584..8bb30379a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1428,7 +1428,7 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess size_t error_len) { session_decrypt_group_message_result result = {}; for (size_t index = 0; index < decrypt_ed25519_privkey_len; index++) { - std::span key = { + std::span key = { decrypt_ed25519_privkey_list[index].data, decrypt_ed25519_privkey_list[index].size}; DecryptGroupMessage result_cpp = {}; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index a79733725..339b60097 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -50,8 +50,8 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { namespace { session::uc32 proof_hash_internal( std::uint8_t version, - std::span gen_index_hash, - std::span rotating_pubkey, + std::span gen_index_hash, + std::span rotating_pubkey, std::uint64_t expiry_unix_ts_ms) { // This must match the hashing routine at @@ -68,9 +68,9 @@ session::uc32 proof_hash_internal( } bool proof_verify_signature_internal( - std::span hash, - std::span sig, - std::span verify_pubkey) { + std::span hash, + std::span sig, + std::span verify_pubkey) { // The C/C++ interface verifies that the payloads are the correct size using the type system so // only need asserts here. assert(hash.size() == 32); @@ -84,19 +84,16 @@ bool proof_verify_signature_internal( } bool proof_verify_message_internal( - std::span rotating_pubkey, - std::span sig, - std::span msg) { + std::span rotating_pubkey, + std::span sig, + std::span msg) { // C++ throws on bad size, C uses a fixed sized array assert(rotating_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES); if (sig.size() != crypto_sign_ed25519_BYTES) return false; int verify_result = crypto_sign_ed25519_verify_detached( - reinterpret_cast(sig.data()), - msg.data(), - msg.size(), - reinterpret_cast(rotating_pubkey.data())); + sig.data(), msg.data(), msg.size(), rotating_pubkey.data()); bool result = verify_result == 0; return result; } @@ -154,7 +151,7 @@ static_assert(sizeof(((ProProof*)0)->gen_index_hash) == 32); static_assert(sizeof(((ProProof*)0)->rotating_pubkey) == crypto_sign_ed25519_PUBLICKEYBYTES); static_assert(sizeof(((ProProof*)0)->sig) == crypto_sign_ed25519_BYTES); -bool ProProof::verify_signature(const std::span& verify_pubkey) const { +bool ProProof::verify_signature(const std::span& verify_pubkey) const { if (verify_pubkey.size() != crypto_sign_ed25519_PUBLICKEYBYTES) throw std::invalid_argument{fmt::format( "Invalid verify_pubkey: Must be 32 byte Ed25519 public key (was: {})", @@ -165,7 +162,7 @@ bool ProProof::verify_signature(const std::span& verify_pubkey) c return result; } -bool ProProof::verify_message(std::span sig, std::span msg) const { +bool ProProof::verify_message(std::span sig, std::span msg) const { if (sig.size() != crypto_sign_ed25519_BYTES) throw std::invalid_argument{fmt::format( "Invalid signed_msg: Signature must be 64 bytes (was: {})", sig.size())}; @@ -178,7 +175,7 @@ bool ProProof::is_active(std::chrono::sys_time unix_t } ProStatus ProProof::status( - std::span verify_pubkey, + std::span verify_pubkey, std::chrono::sys_time unix_ts, const std::optional& signed_msg) { ProStatus result = ProStatus::Valid; @@ -272,59 +269,59 @@ ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size) { return result; } -std::vector encode_for_1o1( - std::span plaintext, +std::vector encode_for_1o1( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, - std::optional> pro_rotating_ed25519_privkey) { + std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::SyncOr1o1; dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; return encode_for_destination(plaintext, &ed25519_privkey, dest); } -std::vector encode_for_community_inbox( - std::span plaintext, +std::vector encode_for_community_inbox( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& recipient_pubkey, const uc32& community_pubkey, - std::optional> pro_rotating_ed25519_privkey) { + std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::CommunityInbox; dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; dest.community_inbox_server_pubkey = community_pubkey; return encode_for_destination(plaintext, &ed25519_privkey, dest); } -std::vector encode_for_community( - std::span plaintext, - std::optional> pro_rotating_ed25519_privkey) { +std::vector encode_for_community( + std::span plaintext, + std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::Community; dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + : std::span{}; return encode_for_destination(plaintext, nullptr, dest); } -std::vector encode_for_group( - std::span plaintext, +std::vector encode_for_group( + std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, const uc33& group_ed25519_pubkey, const cleared_uc32& group_enc_key, - std::optional> pro_rotating_ed25519_privkey) { + std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::Group; dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.group_ed25519_pubkey = group_ed25519_pubkey; dest.group_enc_key = group_enc_key; @@ -335,12 +332,12 @@ std::vector encode_for_group( // This pointer is taken verbatim and avoids requiring a copy from the CPP vector. The CPP api will // steal the contents from `ciphertext_cpp`. struct EncryptedForDestinationInternal { - std::vector ciphertext_cpp; + std::vector ciphertext_cpp; span_u8 ciphertext_c; }; constexpr char PADDING_TERMINATING_BYTE = 0x80; -std::vector pad_message(std::span payload) { +std::vector pad_message(std::span payload) { // Calculate amount of padding required size_t padded_content_size = payload.size() + 1 /*padding byte*/; @@ -351,14 +348,14 @@ std::vector pad_message(std::span payload) { assert(padded_content_size % SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING == 0); // Do the padding - std::vector result; + std::vector result; result.resize(padded_content_size); std::memcpy(result.data(), payload.data(), payload.size()); result[payload.size()] = PADDING_TERMINATING_BYTE; return result; } -static std::span unpad_message(std::span payload) { +static std::span unpad_message(std::span payload) { // Strip padding from content size_t size_without_padding = payload.size(); while (size_without_padding) { @@ -376,23 +373,23 @@ static std::span unpad_message(std::span payload) } assert(size_without_padding <= payload.size()); - auto result = std::span(payload.data(), payload.data() + size_without_padding); + auto result = std::span(payload.data(), payload.data() + size_without_padding); return result; } enum class UseMalloc { No, Yes }; static EncryptedForDestinationInternal encode_for_destination_internal( - std::span plaintext, + std::span plaintext, const Ed25519PrivKeySpan* ed25519_privkey, DestinationType dest_type, - std::span dest_pro_rotating_ed25519_privkey, + std::span dest_pro_rotating_ed25519_privkey, std::span dest_recipient_pubkey, std::chrono::milliseconds dest_sent_timestamp_ms, std::span dest_community_inbox_server_pubkey, std::span dest_group_ed25519_pubkey, - std::span dest_group_enc_key, + std::span dest_group_enc_key, UseMalloc use_malloc) { assert(dest_group_enc_key.size() == 32 || dest_group_enc_key.size() == 64); @@ -422,14 +419,14 @@ static EncryptedForDestinationInternal encode_for_destination_internal( } } - std::span content = plaintext; + std::span content = plaintext; EncryptedForDestinationInternal result = {}; switch (dest_type) { case DestinationType::Group: /*FALLTHRU*/ case DestinationType::SyncOr1o1: { if (is_group && - dest_group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { + dest_group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { // Legacy groups which have a 05 prefixed key throw std::runtime_error{ "Unsupported configuration, encrypting for a legacy group (0x05 prefix) is " @@ -438,9 +435,9 @@ static EncryptedForDestinationInternal encode_for_destination_internal( // For Sync or 1o1 mesasges, we need to pad the contents to 160 bytes, see: // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 - std::vector tmp_content_buffer; + std::vector tmp_content_buffer; if (is_1o1) { // Encrypt the padded output - std::vector padded_payload = pad_message(content); + std::vector padded_payload = pad_message(content); tmp_content_buffer = encrypt_for_recipient( *ed25519_privkey, dest_recipient_pubkey, padded_payload); content = tmp_content_buffer; @@ -469,14 +466,14 @@ static EncryptedForDestinationInternal encode_for_destination_internal( cleared_uc64 dummy_pro_ed_sk; crypto_sign_ed25519_keypair(ignore_pk.data(), dummy_pro_ed_sk.data()); crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), + reinterpret_cast(pro_sig->data()), nullptr, content.data(), content.size(), dummy_pro_ed_sk.data()); } else { crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), + reinterpret_cast(pro_sig->data()), nullptr, content.data(), content.size(), @@ -486,7 +483,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (is_group) { std::string bytes = envelope.SerializeAsString(); - std::vector ciphertext = encrypt_for_group( + std::vector ciphertext = encrypt_for_group( *ed25519_privkey, dest_group_ed25519_pubkey.subspan<1>(), dest_group_enc_key, @@ -530,7 +527,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( case DestinationType::Community: /*FALLTHRU*/ case DestinationType::CommunityInbox: { // Setup the pro signature for the community message - std::vector tmp_content_buffer; + std::vector tmp_content_buffer; // Sign the message with the Session Pro key if given and then pad the message (both // community message types require it) @@ -589,7 +586,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( // compat. We can remove this eventually, first step is to unify the clients. if (is_community_inbox) { - std::vector ciphertext = encrypt_for_blinded_recipient( + std::vector ciphertext = encrypt_for_blinded_recipient( *ed25519_privkey, dest_community_inbox_server_pubkey, dest_recipient_pubkey, // 33-byte blinded pubkey @@ -605,7 +602,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (use_malloc == UseMalloc::Yes) { result.ciphertext_c = span_u8_copy_or_throw(content.data(), content.size()); } else { - result.ciphertext_cpp = std::vector(content.begin(), content.end()); + result.ciphertext_cpp = std::vector(content.begin(), content.end()); } } } break; @@ -613,7 +610,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( return result; } -std::vector encode_for_destination( +std::vector encode_for_destination( std::span plaintext, const Ed25519PrivKeySpan* ed25519_privkey, const Destination& dest) { @@ -630,22 +627,22 @@ std::vector encode_for_destination( /*dest_group_enc_key=*/dest.group_enc_key, /*use_malloc=*/UseMalloc::No); - std::vector result = std::move(result_internal.ciphertext_cpp); + std::vector result = std::move(result_internal.ciphertext_cpp); return result; } DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, - std::span envelope_payload, + std::span envelope_payload, const uc32& pro_backend_pubkey) { DecodedEnvelope result = {}; SessionProtos::Envelope envelope = {}; - std::span envelope_plaintext = envelope_payload; + std::span envelope_plaintext = envelope_payload; // The caller is indicating that the envelope_payload is encrypted, if the group keys are // provided. We will decrypt the payload to get the plaintext. In all other cases, the envelope // is assumed to be websocket wrapped - std::vector envelope_from_decrypted_groups; + std::vector envelope_from_decrypted_groups; std::string envelope_from_websocket_message; if (keys.group_ed25519_pubkey) { // Decrypt using the keys @@ -762,8 +759,8 @@ DecodedEnvelope decode_envelope( } else { const std::string& content = envelope.content(); bool decrypt_success = false; - std::vector content_plaintext; - std::vector sender_ed25519_pubkey; + std::vector content_plaintext; + std::vector sender_ed25519_pubkey; for (const auto& privkey_it : keys.decrypt_keys) { try { std::tie(content_plaintext, sender_ed25519_pubkey) = @@ -782,7 +779,7 @@ DecodedEnvelope decode_envelope( } // Strip padding from content - std::span unpadded_content = unpad_message(content_plaintext); + std::span unpadded_content = unpad_message(content_plaintext); content_plaintext.resize(unpadded_content.size()); result.content_plaintext = std::move(content_plaintext); @@ -894,7 +891,7 @@ DecodedEnvelope decode_envelope( } DecodedCommunityMessage decode_for_community( - std::span content_or_envelope_payload, + std::span content_or_envelope_payload, std::chrono::sys_time unix_ts, const uc32& pro_backend_pubkey) { // TODO: Community message parsing requires a custom code path for now as we are planning to @@ -913,7 +910,7 @@ DecodedCommunityMessage decode_for_community( DecodedCommunityMessage result = {}; // Attempt to parse the blob as an envelope - std::optional> pro_sig; + std::optional> pro_sig; SessionProtos::Envelope pb_envelope = {}; { bool envelope_parsed = pb_envelope.ParseFromArray( @@ -922,7 +919,7 @@ DecodedCommunityMessage decode_for_community( if (envelope_parsed) { // Create the envelope Envelope& envelope = result.envelope.emplace(); - result.content_plaintext = std::vector( + result.content_plaintext = std::vector( pb_envelope.content().begin(), pb_envelope.content().end()); // Extract the envelope into our type @@ -958,13 +955,13 @@ DecodedCommunityMessage decode_for_community( } } else { // TODO: Do wasteful copy in the interim whilst transitioning protocol - result.content_plaintext = std::vector( + result.content_plaintext = std::vector( content_or_envelope_payload.begin(), content_or_envelope_payload.end()); } } // Parse the content blob - std::span unpadded_content = unpad_message(result.content_plaintext); + std::span unpadded_content = unpad_message(result.content_plaintext); SessionProtos::Content content = {}; if (!content.ParseFromArray(unpadded_content.data(), unpadded_content.size())) throw std::runtime_error{ @@ -1061,7 +1058,7 @@ DecodedCommunityMessage decode_for_community( assert(!content_copy_without_sig.has_prosigforcommunitymessageonly()); // Reserialise the payload without the signature, repad it then verify the signature - std::vector content_copy_without_sig_payload = + std::vector content_copy_without_sig_payload = pad_message(to_span(content_copy_without_sig.SerializeAsString())); signed_msg.msg = to_span(content_copy_without_sig_payload); @@ -1149,7 +1146,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( size_t verify_pubkey_len) { if (verify_pubkey_len != crypto_sign_ed25519_PUBLICKEYBYTES) return false; - auto verify_pubkey_span = std::span(verify_pubkey, verify_pubkey_len); + auto verify_pubkey_span = std::span(verify_pubkey, verify_pubkey_len); session::uc32 hash = proof_hash_internal( proof->version, proof->gen_index_hash.data, @@ -1165,8 +1162,8 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( size_t sig_len, uint8_t const* msg, size_t msg_len) { - std::span sig_span = {sig, sig_len}; - std::span msg_span = {msg, msg_len}; + std::span sig_span = {sig, sig_len}; + std::span msg_span = {msg, msg_len}; bool result = proof_verify_message_internal(proof->rotating_pubkey.data, sig_span, msg_span); return result; } @@ -1358,9 +1355,9 @@ LIBSESSION_C_API session_protocol_encoded_for_destination session_protocol_encod session_protocol_encoded_for_destination result = {}; try { - std::span dest_pro_rotating_ed25519_privkey = std::span( - reinterpret_cast(dest->pro_rotating_ed25519_privkey), - reinterpret_cast(dest->pro_rotating_ed25519_privkey) + + std::span dest_pro_rotating_ed25519_privkey = std::span( + reinterpret_cast(dest->pro_rotating_ed25519_privkey), + reinterpret_cast(dest->pro_rotating_ed25519_privkey) + dest->pro_rotating_ed25519_privkey_len); std::optional privkey; @@ -1369,7 +1366,7 @@ LIBSESSION_C_API session_protocol_encoded_for_destination session_protocol_encod static_cast(ed25519_privkey), ed25519_privkey_len); EncryptedForDestinationInternal result_internal = encode_for_destination_internal( - /*plaintext=*/{static_cast(plaintext), plaintext_len}, + /*plaintext=*/{static_cast(plaintext), plaintext_len}, /*ed25519_privkey=*/privkey ? &*privkey : nullptr, /*dest_type=*/static_cast(dest->type), /*dest_pro_rotating_ed25519_privkey=*/dest_pro_rotating_ed25519_privkey, @@ -1436,7 +1433,7 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( // Setup decryption keys and decrypt DecodeEnvelopeKey keys_cpp = {}; if (keys->group_ed25519_pubkey.size == crypto_sign_ed25519_PUBLICKEYBYTES) { - keys_cpp.group_ed25519_pubkey = std::span{ + keys_cpp.group_ed25519_pubkey = std::span{ keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; } else if (keys->group_ed25519_pubkey.size) { result.error_len_incl_null_terminator = @@ -1451,13 +1448,13 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( DecodedEnvelope result_cpp = {}; for (size_t index = 0; index < keys->decrypt_keys_len; index++) { - std::span key = { + std::span key = { keys->decrypt_keys[index].data, keys->decrypt_keys[index].size}; keys_cpp.decrypt_keys = {&key, 1}; try { result_cpp = decode_envelope( keys_cpp, - {static_cast(envelope_plaintext), envelope_plaintext_len}, + {static_cast(envelope_plaintext), envelope_plaintext_len}, pro_backend_pubkey_cpp.data); result.success = true; break; @@ -1535,8 +1532,8 @@ session_protocol_decoded_community_message session_protocol_decode_for_community OPTIONAL char* error, size_t error_len) { session_protocol_decoded_community_message result = {}; - auto content_or_envelope_payload_span = std::span( - reinterpret_cast(content_or_envelope_payload), + auto content_or_envelope_payload_span = std::span( + reinterpret_cast(content_or_envelope_payload), content_or_envelope_payload_len); auto unix_ts = std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); diff --git a/src/types.cpp b/src/types.cpp index 5efa976da..8f92b2017 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -8,7 +8,7 @@ namespace session { span_u8 span_u8_alloc_or_throw(size_t size) { span_u8 result = {}; result.size = size; - result.data = static_cast(malloc(size)); + result.data = static_cast(malloc(size)); if (!result.data) throw std::runtime_error( fmt::format("Failed to allocate {} bytes for span, out of memory", size)); diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 053905c26..93b8a524c 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -10,7 +10,7 @@ using namespace oxenc::literals; TEST_CASE("Pro", "[config][pro]") { // Setup keys - std::array rotating_pk, signing_pk; + std::array rotating_pk, signing_pk; session::cleared_uc64 rotating_sk, signing_sk; { crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); @@ -44,7 +44,7 @@ TEST_CASE("Pro", "[config][pro]") { { // Generate the hashes static_assert(crypto_sign_ed25519_BYTES == pro_cpp.proof.sig.max_size()); - std::array hash_to_sign_cpp = pro_cpp.proof.hash(); + std::array hash_to_sign_cpp = pro_cpp.proof.hash(); bytes32 hash_to_sign = session_protocol_pro_proof_hash(&pro.proof); static_assert(hash_to_sign_cpp.size() == sizeof(hash_to_sign)); @@ -82,11 +82,11 @@ TEST_CASE("Pro", "[config][pro]") { // Verify it can verify messages signed with the rotating public key { std::string_view body = "hello world"; - std::array sig = {}; + std::array sig = {}; int sign_result = crypto_sign_ed25519_detached( sig.data(), nullptr, - reinterpret_cast(body.data()), + reinterpret_cast(body.data()), body.size(), rotating_sk.data()); CHECK(sign_result == 0); @@ -95,7 +95,7 @@ TEST_CASE("Pro", "[config][pro]") { &pro.proof, sig.data(), sig.size(), - reinterpret_cast(body.data()), + reinterpret_cast(body.data()), body.size())); } @@ -128,7 +128,7 @@ TEST_CASE("Pro", "[config][pro]") { // Try loading a proof with a bad signature in it from dict { - std::array broken_sig = pro_cpp.proof.sig; + std::array broken_sig = pro_cpp.proof.sig; broken_sig[0] = ~broken_sig[0]; // Break the sig const session::ProProof& proof = pro_cpp.proof; diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index f307eae9b..630c9d06f 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -604,7 +604,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { } // Ensure the pro config is being stored correctly - std::array rotating_pk, signing_pk; + std::array rotating_pk, signing_pk; session::cleared_uc64 rotating_sk, signing_sk; { crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index ac8707f7e..a8fae2eda 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -87,12 +87,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { crypto_sign_ed25519_keypair(rotating_pubkey.data, rotating_privkey.data); { - std::array fake_google_payment_token; + std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); std::string fake_google_payment_token_hex = "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; + std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); @@ -119,9 +119,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { rotating_privkey.data, sizeof(rotating_privkey.data), payment_tx.provider, - reinterpret_cast(payment_tx.payment_id), + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count); INFO(result.error); REQUIRE(result.success); @@ -133,11 +133,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { master_privkey.data, rotating_privkey.data, payment_tx.provider, - std::span( - reinterpret_cast(payment_tx.payment_id), + std::span( + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count), - std::span( - reinterpret_cast(payment_tx.order_id), + std::span( + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count)); REQUIRE(std::memcmp( @@ -157,9 +157,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { rotating_privkey.data, sizeof(rotating_privkey.data), payment_tx.provider, - reinterpret_cast(payment_tx.payment_id), + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count); REQUIRE(!result.success); REQUIRE(result.error_count > 0); @@ -222,9 +222,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { rotating_privkey.data, sizeof(rotating_privkey.data), payment_tx.provider, - reinterpret_cast(payment_tx.payment_id), + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count); request.master_sig = sigs.master_sig; @@ -456,7 +456,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } SECTION("session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse") { - std::array fake_gen_index_hash; + std::array fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); nlohmann::json j; @@ -561,7 +561,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { j["result"]["ticket"] = 123; j["result"]["items"] = nlohmann::json::array(); - std::array fake_gen_index_hash; + std::array fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); auto obj = nlohmann::json::object(); @@ -788,9 +788,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.unix_ts_ms, request.refund_requested_unix_ts_ms, payment_tx.provider, - reinterpret_cast(payment_tx.payment_id), + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count); request.master_sig = sig.sig; REQUIRE(sig.success); @@ -949,12 +949,12 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Add pro payment session_protocol_pro_proof first_pro_proof = {}; { - std::array fake_google_payment_token; + std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); std::string fake_google_payment_token_hex = "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; + std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); @@ -978,9 +978,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { rotating_privkey.data, sizeof(rotating_privkey.data), payment_tx.provider, - reinterpret_cast(payment_tx.payment_id), + reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), + reinterpret_cast(payment_tx.order_id), payment_tx.order_id_count); session_pro_backend_add_pro_payment_request request = {}; @@ -1192,12 +1192,12 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Add _another_ payment, same details session_pro_backend_add_pro_payment_user_transaction another_payment_tx = {}; { - std::array fake_google_payment_token; + std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); std::string fake_google_payment_token_hex = "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; + std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); @@ -1222,9 +1222,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { rotating_privkey.data, sizeof(rotating_privkey.data), another_payment_tx.provider, - reinterpret_cast(another_payment_tx.payment_id), + reinterpret_cast(another_payment_tx.payment_id), another_payment_tx.payment_id_count, - reinterpret_cast(another_payment_tx.order_id), + reinterpret_cast(another_payment_tx.order_id), another_payment_tx.order_id_count); session_pro_backend_add_pro_payment_request request = {}; @@ -1314,9 +1314,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { /*unix_ts_ms*/ now_unix_ts_ms, /*refund_requested_unix_ts_ms*/ now_unix_ts_ms, another_payment_tx.provider, - reinterpret_cast(another_payment_tx.payment_id), + reinterpret_cast(another_payment_tx.payment_id), another_payment_tx.payment_id_count, - reinterpret_cast(another_payment_tx.order_id), + reinterpret_cast(another_payment_tx.order_id), another_payment_tx.order_id_count); scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index ff65e1eb8..856d1226f 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -16,7 +16,7 @@ using namespace session; struct SerialisedProtobufContentWithProForTesting { ProProof proof; std::string plaintext; - std::vector plaintext_padded; + std::vector plaintext_padded; uc64 sig_over_plaintext_with_user_pro_key; uc64 sig_over_plaintext_padded_with_user_pro_key; uc32 pro_proof_hash; @@ -83,14 +83,14 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se crypto_sign_ed25519_detached( result.sig_over_plaintext_with_user_pro_key.data(), nullptr, - reinterpret_cast(result.plaintext.data()), + reinterpret_cast(result.plaintext.data()), result.plaintext.size(), user_rotating_privkey.data()); crypto_sign_ed25519_detached( result.sig_over_plaintext_padded_with_user_pro_key.data(), nullptr, - reinterpret_cast(result.plaintext_padded.data()), + result.plaintext_padded.data(), result.plaintext_padded.size(), user_rotating_privkey.data()); From 1a325b9ce9d930cad849dc921b3ac33a535c6ce0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 15:37:49 -0300 Subject: [PATCH 061/193] Add DM v1+v2 parsing and polling support --- include/session/core.hpp | 5 +- include/session/core/callbacks.hpp | 46 +++++ include/session/core/devices.hpp | 15 +- include/session/core/swarm_message.hpp | 19 ++ include/session/pro_backend.hpp | 6 +- include/session/session_protocol.hpp | 4 +- src/core.cpp | 230 ++++++++++++++++++--- src/core/devices.cpp | 50 +++-- src/session_protocol.cpp | 4 +- tests/CMakeLists.txt | 1 + tests/test_dm_receive.cpp | 264 +++++++++++++++++++++++++ tests/test_poll.cpp | 52 ++--- 12 files changed, 610 insertions(+), 86 deletions(-) create mode 100644 include/session/core/swarm_message.hpp create mode 100644 tests/test_dm_receive.cpp diff --git a/include/session/core.hpp b/include/session/core.hpp index 50c0c0c69..cd480d5d4 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -206,6 +206,9 @@ class Core { void _update_polling(); void _poll(); + // Decrypts and dispatches one-to-one messages from Namespace::Default. + void _handle_direct_messages(std::span messages); + // Extracts an option of type T from a pack; returns the first match wrapped in optional, or // nullopt if not present. Mirrors the same helper in session::sqlite::Database. template @@ -300,7 +303,7 @@ class Core { // messages after a non-final call, the caller should still call this with an empty span and // is_final=true to flush any actions that are deferred until the end of a fetch. void receive_messages( - std::span> messages, + std::span messages, config::Namespace ns, bool is_final); }; diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index f09a4e45f..a8e3dfac2 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -1,8 +1,12 @@ #pragma once +#include #include +#include #include +#include #include #include +#include namespace session::core { @@ -26,6 +30,26 @@ enum class PfsKeyFetch { failed, ///< The network request failed (swarm lookup or send_request) }; +/// Reason code passed to the message_decrypt_failed callback. +enum class MessageDecryptFailure { + no_pfs_key, ///< Version 2 message but no account key with a matching key indicator exists + decrypt_failed, ///< Decryption failed (either version); key was found but did not work + bad_format, ///< Message is structurally malformed (e.g. invalid bencode, truncated fields) + unknown_version, ///< Message starts with 0x00 but carries an unrecognised version byte; + ///< likely a future protocol version this build does not understand +}; + +/// A successfully decrypted one-to-one message from Namespace::Default. +struct ReceivedMessage { + std::string hash; ///< Swarm-assigned message hash + sys_ms timestamp; ///< Server-reported upload timestamp + sys_ms expiry; ///< Server-reported expiry timestamp + std::array sender_session_id; ///< 0x05-prefixed sender session ID + int version; ///< Protocol version: 1 or 2 + std::vector content; ///< Decrypted protobuf-encoded payload + std::optional> pro_signature; ///< Session Pro signature, if present +}; + /// Struct holding application callbacks to fire when libsession Core events happen to allow the /// Core object to fire into the application front-end. struct callbacks { @@ -104,6 +128,28 @@ struct callbacks { /// - result -- the outcome of the fetch: new_key, unchanged, not_found, or failed std::function session_id, PfsKeyFetch result)> pfs_keys_fetched; + + /// Callback invoked when a one-to-one message from Namespace::Default is successfully + /// decrypted. The message is passed as an rvalue reference: the callback may move from it + /// (e.g. to take ownership of the content vector) or simply read it in place. + /// + /// Parameters: + /// - msg -- the decrypted message data + std::function message_received; + + /// Callback invoked when a one-to-one message from Namespace::Default could not be decrypted + /// or parsed. The raw swarm message and a reason code are provided so the caller can decide + /// how to handle it (e.g. log, queue for retry, surface to the user). + /// + /// When receive_messages() is called directly by the application, `msg` is a reference to one + /// of the SwarmMessage elements passed in, which the caller can identify exactly by comparing + /// pointers. When triggered by internal polling, `msg` refers to an internally-owned object. + /// + /// Parameters: + /// - msg -- the raw swarm message that could not be decrypted + /// - reason -- why decryption failed + std::function + message_decrypt_failed; }; } // namespace session::core diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 2e7980877..1fdcadb4b 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -16,6 +16,7 @@ #include #include "component.hpp" +#include "swarm_message.hpp" namespace session { class TestHelper; @@ -146,10 +147,8 @@ class Devices final : detail::CoreComponent { void receive_link_request(std::span data); // Handlers for incoming swarm messages by namespace, called from Core::receive_messages. - void parse_device_messages( - std::span> messages, bool is_final); - void parse_account_pubkeys( - std::span> messages, bool is_final); + void parse_device_messages(std::span messages, bool is_final); + void parse_account_pubkeys(std::span messages, bool is_final); // Decrypts an incoming encrypted device group ("G") message, returning the bt-encoded group // payload plaintext (a bt-dict containing at minimum a "D" devices subdict and optionally a @@ -244,7 +243,13 @@ class Devices final : detail::CoreComponent { // Returns the current active account keys after pruning obsolete ones: that is, the current key // plus all keys that were rotated away fewer than ACCOUNT_KEY_RETENTION ago. Keys are returned // sorted from newest to oldest. If there are no keys at all, generates an initial one. - std::vector active_account_keys(); + // Returns account keys, ordered with the active (unrotated) key first then most-recently-rotated + // first. Expired rotated keys are pruned before querying. If key_indicator is given, only + // keys whose ML-KEM-768 pubkey begins with those two bytes are returned (using the indexed + // key_indicator virtual column); otherwise all retained keys are returned and a new key is + // auto-generated if none is currently active. + std::vector active_account_keys( + std::optional> key_indicator = std::nullopt); // Returns the time when this device's unique device key is due to be rotated. Returns nullopt // if this device is not currently part of the device group. diff --git a/include/session/core/swarm_message.hpp b/include/session/core/swarm_message.hpp new file mode 100644 index 000000000..60e73a130 --- /dev/null +++ b/include/session/core/swarm_message.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace session::core { + +/// A single message retrieved from the swarm, as returned by a retrieve request. The data, +/// hash, timestamp, and expiry fields are exactly the four values the server returns per +/// message; data is owned externally and must remain valid for the lifetime of this struct. +struct SwarmMessage { + std::span data; + std::string hash; + sys_ms timestamp; + sys_ms expiry; +}; + +} // namespace session::core diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 2855f99fb..629a0f1f7 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -62,10 +63,7 @@ namespace session::pro_backend { /// TODO: Assign the Session Pro backend public key for verifying proofs to allow users of the /// library to have the pubkey available for verifying proofs. -constexpr uc32 PUBKEY = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; -static_assert(sizeof(PUBKEY) == uc32{}.size()); +constexpr auto PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000"_hex_u; enum struct AddProPaymentResponseStatus { /// Payment was claimed and the pro proof was successfully generated diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 9c00b88d5..61765775a 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -616,7 +616,7 @@ std::vector encode_for_destination( DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, std::span envelope_payload, - const uc32& pro_backend_pubkey); + std::span pro_backend_pubkey); /// API: session_protocol/decode_for_community /// @@ -650,6 +650,6 @@ DecodedEnvelope decode_envelope( DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, sys_ms unix_ts, - const uc32& pro_backend_pubkey); + std::span pro_backend_pubkey); } // namespace session diff --git a/src/core.cpp b/src/core.cpp index eb768dc48..63d7b4093 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -18,6 +18,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -31,6 +34,12 @@ using namespace session::sqlite; using namespace oxen::log::literals; static auto cat = log::Cat("core"); +// Returns true if the given namespace requires a signed retrieve request. A namespace does NOT +// require auth if and only if it satisfies: ns_val < 0 && (-ns_val) % 20 == 1 (i.e. -1, -21, ...). +static constexpr bool ns_requires_auth(int16_t ns_val) { + return !(ns_val < 0 && (-ns_val) % 20 == 1); +} + static cleared_b32 seed_from_words( std::span words, const mnemonics::Mnemonics& lang) { auto n = words.size(); @@ -102,29 +111,40 @@ void Core::_poll() { return; constexpr std::array namespaces = { - config::Namespace::Devices, config::Namespace::AccountPubkeys}; + config::Namespace::Default, + config::Namespace::Devices, + config::Namespace::AccountPubkeys}; auto now_ms = epoch_ms(clock_now_ms()); - auto session_id = oxenc::to_hex(globals.session_id()); + auto session_id_hex = oxenc::to_hex(globals.session_id()); auto ed25519_hex = globals.pubkey_ed25519().hex(); - // Devices (21) requires auth: sign "retrieve" || namespace || timestamp (base-10 strings). - constexpr auto devices_ns_val = static_cast(config::Namespace::Devices); - std::string to_sign = fmt::format("retrieve{}{}", devices_ns_val, now_ms); - std::array sig; - auto seed = globals.account_seed(); - crypto_sign_ed25519_detached( - sig.data(), - nullptr, - reinterpret_cast(to_sign.data()), - to_sign.size(), - seed.ed25519_secret().data()); - auto sig_b64 = oxenc::to_base64(sig); + // Build per-namespace signatures for namespaces that require authentication; index-aligned with + // `namespaces`. Empty string means no auth needed for that namespace. + std::vector ns_sig(namespaces.size()); + { + auto seed = globals.account_seed(); + for (size_t i = 0; i < namespaces.size(); ++i) { + auto ns_val = static_cast(namespaces[i]); + if (!ns_requires_auth(ns_val)) + continue; + std::string to_sign = "retrieve{}{}"_format(ns_val, now_ms); + std::array sig; + crypto_sign_ed25519_detached( + sig.data(), + nullptr, + reinterpret_cast(to_sign.data()), + to_sign.size(), + seed.ed25519_secret().data()); + ns_sig[i] = oxenc::to_base64(sig); + } + } net->get_swarm( globals.pubkey_x25519(), false, - [this, net, namespaces, session_id, ed25519_hex, now_ms, sig_b64](auto, auto swarm) { + [this, net, namespaces, session_id_hex, ed25519_hex, now_ms, + ns_sig = std::move(ns_sig)](auto, auto swarm) { if (swarm.empty()) return; @@ -134,18 +154,18 @@ void Core::_poll() { nlohmann::json requests = nlohmann::json::array(); { auto conn = db.conn(); - for (auto ns : namespaces) { + for (size_t i = 0; i < namespaces.size(); ++i) { + auto ns = namespaces[i]; auto ns_val = static_cast(ns); nlohmann::json params = { - {"pubkey", session_id}, + {"pubkey", session_id_hex}, {"namespace", ns_val}, }; - // Devices (21) requires a signed retrieve; AccountPubkeys (-21) does not. - if (ns == config::Namespace::Devices) { + if (!ns_sig[i].empty()) { params["pubkey_ed25519"] = ed25519_hex; params["timestamp"] = now_ms; - params["signature"] = sig_b64; + params["signature"] = ns_sig[i]; } auto last_hash = conn.prepared_maybe_get( @@ -188,7 +208,8 @@ void Core::_poll() { for (size_t i = 0; i < namespaces.size() && i < results.size(); ++i) { const auto& res = results[i]; - if (!res.contains("code") || res["code"].get() != 200) + auto code_it = res.find("code"); + if (code_it == res.end() || code_it->get() != 200) continue; auto body_it = res.find("body"); if (body_it == res.end()) @@ -200,25 +221,46 @@ void Core::_poll() { auto ns = namespaces[i]; auto ns_val = static_cast(ns); + // Decode each message; keep the decoded bytes alive until after + // receive_messages() returns, since SwarmMessage::data spans + // into them. std::vector> messages_data; + std::vector swarm_messages; std::string newest_hash; for (const auto& msg : *msgs_it) { - if (!msg.contains("data") || !msg["data"].is_string()) + auto data_it = msg.find("data"); + if (data_it == msg.end() || !data_it->is_string()) continue; - auto b64_data = msg["data"].get(); auto& decoded = messages_data.emplace_back(); - decoded.reserve(oxenc::from_base64_size(b64_data.size())); + auto b64 = data_it->get(); + decoded.reserve(oxenc::from_base64_size(b64.size())); oxenc::from_base64( - b64_data.begin(), - b64_data.end(), + b64.begin(), b64.end(), std::back_inserter(decoded)); - if (msg.contains("hash") && msg["hash"].is_string()) - newest_hash = msg["hash"].get(); + SwarmMessage swarm_msg; + swarm_msg.data = {decoded.data(), decoded.size()}; + + if (auto h = msg.find("hash"); + h != msg.end() && h->is_string()) { + swarm_msg.hash = h->get(); + newest_hash = swarm_msg.hash; + } + + if (auto t = msg.find("timestamp"); + t != msg.end() && t->is_number_integer()) + swarm_msg.timestamp = + from_epoch_ms(t->get()); + + if (auto e = msg.find("expiry"); + e != msg.end() && e->is_number_integer()) + swarm_msg.expiry = from_epoch_ms(e->get()); + + swarm_messages.push_back(std::move(swarm_msg)); } - if (!messages_data.empty()) { + if (!swarm_messages.empty()) { if (!newest_hash.empty()) conn.prepared_exec( R"( @@ -228,7 +270,7 @@ ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash ns_val, sn_pubkey, newest_hash); - receive_messages(to_view_vector(messages_data), ns, true); + receive_messages(swarm_messages, ns, true); } } } catch (const std::exception& e) { @@ -465,12 +507,138 @@ ON CONFLICT(session_id) DO UPDATE SET return status; } +void Core::_handle_direct_messages(std::span messages) { + if (!callbacks.message_received && !callbacks.message_decrypt_failed) + return; + + auto seed = globals.account_seed(); + auto session_id = globals.session_id(); + // Long-term X25519 pub/sec used for v2 key-indicator prefix decryption. + std::span x25519_pub{session_id.data() + 1, 32}; + auto x25519_sec = seed.x25519_key(); + + // Ed25519 secret key used for v1 envelope decryption. + // TODO: DecodeEnvelopeKey is an ugly API; refactor it (see project memory). + auto ed_sec = seed.ed25519_secret(); + std::array, 1> ed_sec_arr{ed_sec}; + DecodeEnvelopeKey v1_keys; + v1_keys.decrypt_keys = ed_sec_arr; + + auto fire_received = [&](ReceivedMessage out) { + if (!callbacks.message_received) + return; + try { + callbacks.message_received(std::move(out)); + } catch (const std::exception& e) { + log::warning(cat, "message_received callback threw: {}", e.what()); + } + }; + + auto fire_fail = [&](const SwarmMessage& msg, MessageDecryptFailure reason) { + if (!callbacks.message_decrypt_failed) + return; + try { + callbacks.message_decrypt_failed(msg, reason); + } catch (const std::exception& e) { + log::warning(cat, "message_decrypt_failed callback threw: {}", e.what()); + } + }; + + for (const auto& msg : messages) { + auto data = msg.data; + if (data.empty()) { + fire_fail(msg, MessageDecryptFailure::bad_format); + continue; + } + + if (data[0] == 0x00) { + // Version 2 (PFS+PQ) or an unrecognised future version. + if (data.size() < 2 || data[1] != 0x02) { + fire_fail(msg, MessageDecryptFailure::unknown_version); + continue; + } + + // Extract the 2-byte ML-KEM key indicator, then look up matching account keys. + std::array ki; + try { + ki = decrypt_incoming_v2_prefix(x25519_sec, x25519_pub, data); + } catch (const std::exception&) { + // Ciphertext is too short or otherwise structurally malformed. + fire_fail(msg, MessageDecryptFailure::bad_format); + continue; + } + + auto keys = devices.active_account_keys(ki); + if (keys.empty()) { + fire_fail(msg, MessageDecryptFailure::no_pfs_key); + continue; + } + + bool decrypted = false; + for (auto& key : keys) { + try { + auto result = decrypt_incoming_v2( + session_id, + key.x25519_sec, + key.x25519_pub, + key.mlkem768_sec, + data); + ReceivedMessage out; + out.hash = msg.hash; + out.timestamp = msg.timestamp; + out.expiry = msg.expiry; + out.sender_session_id = result.sender_session_id; + out.version = 2; + out.content = std::move(result.content); + out.pro_signature = result.pro_signature; + fire_received(std::move(out)); + decrypted = true; + break; + } catch (const DecryptV2Error&) { + // This key didn't work; try the next candidate. + } catch (const std::exception& e) { + // Unrecoverable structural error in the message itself. + log::warning(cat, "v2 direct message format error: {}", e.what()); + fire_fail(msg, MessageDecryptFailure::bad_format); + decrypted = true; // Prevent the generic "no key worked" fallback. + break; + } + } + if (!decrypted) + fire_fail(msg, MessageDecryptFailure::decrypt_failed); + + } else { + // Version 1: protobuf WebSocketMessage → Envelope wire format. + try { + auto decoded = decode_envelope(v1_keys, data, pro_backend::PUBKEY); + + ReceivedMessage out; + out.hash = msg.hash; + out.timestamp = msg.timestamp; + out.expiry = msg.expiry; + out.version = 1; + // Reconstruct the 33-byte (0x05-prefixed) session ID from the x25519 pubkey. + out.sender_session_id[0] = 0x05; + std::ranges::copy(decoded.sender_x25519_pubkey, out.sender_session_id.begin() + 1); + out.content = std::move(decoded.content_plaintext); + if (decoded.envelope.flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG) + out.pro_signature = decoded.envelope.pro_sig; + fire_received(std::move(out)); + } catch (const std::exception& e) { + log::warning(cat, "v1 direct message decryption error: {}", e.what()); + fire_fail(msg, MessageDecryptFailure::decrypt_failed); + } + } + } +} + void Core::receive_messages( - std::span> messages, + std::span messages, config::Namespace ns, bool is_final) { using config::Namespace; switch (ns) { + case Namespace::Default: _handle_direct_messages(messages); break; case Namespace::Devices: devices.parse_device_messages(messages, is_final); break; case Namespace::AccountPubkeys: devices.parse_account_pubkeys(messages, is_final); break; default: diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 88fc7ad8f..c7c462197 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -34,6 +34,7 @@ namespace session::core { +using namespace fmt::literals; using namespace oxen::log::literals; using namespace session::literals; using namespace std::literals; @@ -186,7 +187,8 @@ std::vector Devices::active_device_keys() { return keys; } -std::vector Devices::active_account_keys() { +std::vector Devices::active_account_keys( + std::optional> key_indicator) { auto c = conn(); SQLite::Transaction tx{c.sql}; @@ -196,16 +198,28 @@ std::vector Devices::active_account_keys() { std::vector keys; bool have_active = false; - for (auto [id, created, rotated, seed, pk_ml, pk_x] : c.prepared_results< - int64_t, - int64_t, - std::optional, - sqlite::blobn<32>, - sqlite::blobn, - sqlite::blobn<32>>( - "SELECT id, created, rotated, seed, pubkey_mlkem768, pubkey_x25519" - " FROM device_account_keys" - " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + + auto query_all = + "SELECT id, created, rotated, seed, pubkey_mlkem768, pubkey_x25519" + " FROM device_account_keys" + " ORDER BY rotated DESC NULLS FIRST, created DESC"; + auto query_ki = + "SELECT id, created, rotated, seed, pubkey_mlkem768, pubkey_x25519" + " FROM device_account_keys" + " WHERE key_indicator = ?" + " ORDER BY rotated DESC NULLS FIRST, created DESC"; + + using cols_t = sqlite::IterableStatementWrapper< + int64_t, + int64_t, + std::optional, + sqlite::blobn<32>, + sqlite::blobn, + sqlite::blobn<32>>; + + for (auto [id, created, rotated, seed, pk_ml, pk_x] : + key_indicator ? cols_t{c.prepared_bind(query_ki, *key_indicator)} + : cols_t{c.prepared_bind(query_all)}) { auto& k = keys.emplace_back(keys_from_seed(seed)); k.created = std::chrono::sys_seconds{std::chrono::seconds{created}}; if (rotated) @@ -225,7 +239,7 @@ std::vector Devices::active_account_keys() { tx.commit(); - if (!have_active) { + if (!key_indicator && !have_active) { log::info(cat, "No currently active account keys; generating a new one"); rotate_account_keys(); return active_account_keys(); @@ -1301,15 +1315,15 @@ void Devices::receive_link_request(std::span data) { } void Devices::parse_device_messages( - std::span> messages, bool is_final) { + std::span messages, bool is_final) { for (const auto& msg : messages) { try { - oxenc::bt_dict_consumer in{msg}; + oxenc::bt_dict_consumer in{msg.data}; auto type = in.require(""); if (type == "G") - receive_device_group_message(msg); + receive_device_group_message(msg.data); else if (type == "L") - receive_link_request(msg); + receive_link_request(msg.data); else log::warning(cat, "Ignoring device message with unknown type '{}'", type); } catch (const std::exception& e) { @@ -1439,7 +1453,7 @@ void Devices::parse_device_messages( } void Devices::parse_account_pubkeys( - std::span> messages, bool /*is_final*/) { + std::span messages, bool /*is_final*/) { if (messages.empty()) return; @@ -1449,7 +1463,7 @@ void Devices::parse_account_pubkeys( auto c = conn(); for (const auto& msg : messages) { try { - oxenc::bt_dict_consumer in{msg}; + oxenc::bt_dict_consumer in{msg.data}; auto M = in.require_span("M"); auto X = in.require_span("X"); in.require_signature( diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 339b60097..5cf66fba7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -634,7 +634,7 @@ std::vector encode_for_destination( DecodedEnvelope decode_envelope( const DecodeEnvelopeKey& keys, std::span envelope_payload, - const uc32& pro_backend_pubkey) { + std::span pro_backend_pubkey) { DecodedEnvelope result = {}; SessionProtos::Envelope envelope = {}; std::span envelope_plaintext = envelope_payload; @@ -893,7 +893,7 @@ DecodedEnvelope decode_envelope( DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, std::chrono::sys_time unix_ts, - const uc32& pro_backend_pubkey) { + std::span pro_backend_pubkey) { // TODO: Community message parsing requires a custom code path for now as we are planning to // migrate from sending plain `Content` to `Content` with a pro signature embedded in `Content` // (added exclusively for communities usecase), then, transitioning to sending an `Envelope` to diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9508537a2..d201d7433 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ endif() set(LIB_SESSION_UTESTS_SOURCES test_core_devices.cpp + test_dm_receive.cpp test_attachment_encrypt.cpp test_blinding.cpp test_bt_merge.cpp diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp new file mode 100644 index 000000000..8433d0df4 --- /dev/null +++ b/tests/test_dm_receive.cpp @@ -0,0 +1,264 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "test_helper.hpp" + +using namespace session; +using namespace session::core; +using namespace std::literals; +using namespace oxenc::literals; + +namespace { + +// Fixed sender seed, shared across all test cases. +constexpr auto SENDER_SEED = + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_u; + +struct SenderKeys { + std::array ed_pk; + std::array ed_sk; + std::array session_id; // 0x05-prefixed long-term X25519 pubkey + + SenderKeys() { + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), SENDER_SEED.data()); + std::array curve_pk; + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); + session_id[0] = 0x05; + std::ranges::copy(curve_pk, session_id.begin() + 1); + } +}; + +// Bundles an owned data buffer with a SwarmMessage whose data span points into it, +// keeping lifetime correct: the buffer must outlive any SwarmMessage referencing it. +struct OwnedMessage { + std::vector data; + SwarmMessage msg; + + explicit OwnedMessage( + std::span d, + std::string hash = "testhash", + sys_ms ts = from_epoch_ms(1000), + sys_ms exp = from_epoch_ms(9999)) : + data{d.begin(), d.end()}, msg{data, std::move(hash), ts, exp} {} +}; + +// Cast the std::byte pubkeys returned by TestHelper into unsigned-char spans. +template +std::span as_uc(const std::array& a) { + return std::span{ + reinterpret_cast(a.data()), N}; +} + +} // namespace + +// ── V1 happy path ──────────────────────────────────────────────────────────────────────────────── + +TEST_CASE("_handle_direct_messages: v1 receive", "[core][dm]") { + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + + uc33 recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. + constexpr auto plaintext = "7801"_hex_u; + auto encoded = encode_for_1o1(plaintext, sender.ed_sk, 1234ms, recip_session_id, std::nullopt); + + OwnedMessage om{std::span{encoded}, "hash_v1", from_epoch_ms(1234), from_epoch_ms(9999)}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + + REQUIRE(failures.empty()); + REQUIRE(received.size() == 1); + const auto& msg = received[0]; + CHECK(msg.hash == "hash_v1"); + CHECK(msg.timestamp == from_epoch_ms(1234)); + CHECK(msg.expiry == from_epoch_ms(9999)); + CHECK(msg.version == 1); + CHECK(msg.sender_session_id == sender.session_id); + CHECK(std::ranges::equal(msg.content, plaintext)); + CHECK_FALSE(msg.pro_signature.has_value()); +} + +// ── V2 happy path ──────────────────────────────────────────────────────────────────────────────── + +TEST_CASE("_handle_direct_messages: v2 receive", "[core][dm]") { + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + // Trigger account key generation before querying the pubkeys. + recipient->devices.active_account_keys(); + + auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); + std::array recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + constexpr auto content = "deadbeef"_hex_u; + auto ct = encrypt_for_recipient_v2( + sender.ed_sk, + recip_session_id, + as_uc(x25519_bytes), + as_uc(mlkem_bytes), + content, + std::nullopt); + + OwnedMessage om{std::span{ct}, "hash_v2", from_epoch_ms(5678), from_epoch_ms(8888)}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + + REQUIRE(failures.empty()); + REQUIRE(received.size() == 1); + const auto& msg = received[0]; + CHECK(msg.hash == "hash_v2"); + CHECK(msg.timestamp == from_epoch_ms(5678)); + CHECK(msg.expiry == from_epoch_ms(8888)); + CHECK(msg.version == 2); + CHECK(msg.sender_session_id == sender.session_id); + CHECK(std::ranges::equal(msg.content, content)); + CHECK_FALSE(msg.pro_signature.has_value()); +} + +// ── Failure paths ──────────────────────────────────────────────────────────────────────────────── + +TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + + auto deliver = [&](std::span data) { + OwnedMessage om{data}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + }; + + SECTION("empty data → bad_format") { + deliver(std::span{}); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::bad_format); + } + + SECTION("0x00 0x03 → unknown_version") { + deliver("0003010203"_hex_u); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::unknown_version); + } + + SECTION("v2 too short for prefix decryption → bad_format") { + // Prefix decryption needs at least version(2) + ki(2) + ephemeral_E(32) = 36 bytes. + deliver("00020102030405060708"_hex_u); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::bad_format); + } + + SECTION("v2 key indicator matches no account key → no_pfs_key") { + // Encrypt for a different Core; the key indicator will be unrecognisable to recipient. + TempCore other; + other->devices.active_account_keys(); + auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*other); + std::array other_session_id; + std::ranges::copy(other->globals.session_id(), other_session_id.begin()); + + auto ct = encrypt_for_recipient_v2( + sender.ed_sk, other_session_id, as_uc(x25519_bytes), as_uc(mlkem_bytes), + "01"_hex_u, std::nullopt); + deliver(std::span{ct}); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::no_pfs_key); + } + + SECTION("v2 key indicator matches but AEAD MAC fails → decrypt_failed") { + // Encrypt a valid v2 message for recipient, then corrupt the xchacha ciphertext tail to + // cause MAC authentication failure (DecryptV2Error), exhausting all candidate keys. + recipient->devices.active_account_keys(); + auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); + std::array recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + auto ct = encrypt_for_recipient_v2( + sender.ed_sk, recip_session_id, as_uc(x25519_bytes), as_uc(mlkem_bytes), + "01"_hex_u, std::nullopt); + // Wire format: [0,1]=version, [2,3]=ki, [4,35]=E, [36,1123]=mlkem_ct, [1124+]=xchacha. + // Flip the final byte of the xchacha ciphertext to corrupt the AEAD tag. + REQUIRE(ct.size() > 1124 + 16); + ct.back() ^= 0xff; + deliver(std::span{ct}); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::decrypt_failed); + } + + SECTION("v1 malformed ciphertext → decrypt_failed") { + deliver("0102030405060708"_hex_u); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::decrypt_failed); + } +} + +// ── Callback exception safety ──────────────────────────────────────────────────────────────────── + +TEST_CASE( + "_handle_direct_messages: exception in message_received is swallowed and processing " + "continues", + "[core][dm]") { + SenderKeys sender; + + int call_count = 0; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&&) { + ++call_count; + throw std::runtime_error("deliberate test exception"); + }; + + TempCore recipient{cbs}; + + uc33 recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. + constexpr auto plaintext = "7801"_hex_u; + auto e1 = encode_for_1o1(plaintext, sender.ed_sk, 1000ms, recip_session_id, std::nullopt); + auto e2 = encode_for_1o1(plaintext, sender.ed_sk, 2000ms, recip_session_id, std::nullopt); + + OwnedMessage om1{std::span{e1}, "h1"}; + OwnedMessage om2{std::span{e2}, "h2"}; + std::array msgs{om1.msg, om2.msg}; + + // The thrown exception must not propagate, and both messages must reach the callback. + CHECK_NOTHROW(recipient->receive_messages(msgs, config::Namespace::Default, true)); + CHECK(call_count == 2); +} diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index b83ee2a5d..621539411 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -10,25 +10,27 @@ using namespace session; -// Helpers to build a mock batch response carrying a single message in the first result slot. -// _poll() sends a two-namespace batch (Devices at index 0, AccountPubkeys at index 1) and -// processes results positionally, so msg_data/hash go in results[0]; results[1] is empty. -// The ns parameter is accepted for readability at call sites but is otherwise unused. +// Helpers to build a mock batch response carrying a single message in the Devices result slot. +// _poll() sends a three-namespace batch (Default at index 0, Devices at index 1, AccountPubkeys +// at index 2) and processes results positionally, so msg_data/hash go in results[1]; the other +// slots are empty. The ns parameter is accepted for readability at call sites but is otherwise +// unused. static nlohmann::json make_response( int16_t /*ns*/, std::vector msg_data, std::string hash) { nlohmann::json msg_item; msg_item["data"] = oxenc::to_base64(msg_data); msg_item["hash"] = std::move(hash); - nlohmann::json body0; - body0["messages"] = nlohmann::json::array({std::move(msg_item)}); - nlohmann::json body1; - body1["messages"] = nlohmann::json::array(); + nlohmann::json empty; + empty["messages"] = nlohmann::json::array(); + nlohmann::json devices_body; + devices_body["messages"] = nlohmann::json::array({std::move(msg_item)}); nlohmann::json response; response["results"] = nlohmann::json::array( - {nlohmann::json{{"code", 200}, {"body", std::move(body0)}}, - nlohmann::json{{"code", 200}, {"body", std::move(body1)}}}); + {nlohmann::json{{"code", 200}, {"body", empty}}, + nlohmann::json{{"code", 200}, {"body", std::move(devices_body)}}, + nlohmann::json{{"code", 200}, {"body", std::move(empty)}}}); return response; } @@ -55,21 +57,25 @@ TEST_CASE("Core automatic polling", "[core][poll]") { CHECK(sent.request.endpoint == "batch"); auto batch_json = nlohmann::json::parse(*sent.request.body); auto& reqs = batch_json["requests"]; - REQUIRE(reqs.size() == 2); - // Subrequest 0: Devices (ns 21) — requires auth. + REQUIRE(reqs.size() == 3); + // Subrequest 0: Default (ns 0) — requires auth. CHECK(reqs[0]["method"] == "retrieve"); - auto& params = reqs[0]["params"]; + CHECK(reqs[0]["params"]["namespace"] == 0); + CHECK(reqs[0]["params"].contains("signature")); + // Subrequest 1: Devices (ns 21) — requires auth. + CHECK(reqs[1]["method"] == "retrieve"); + auto& params = reqs[1]["params"]; CHECK(params["pubkey"] == oxenc::to_hex(core->globals.session_id())); CHECK(params["namespace"] == 21); CHECK(params.contains("pubkey_ed25519")); CHECK(params.contains("timestamp")); CHECK(params.contains("signature")); - // No prior hash for this node yet, so no last_hash in either subrequest. + // No prior hash for this node yet, so no last_hash in any subrequest. CHECK_FALSE(params.contains("last_hash")); - // Subrequest 1: AccountPubkeys (ns -21) — no auth required. - CHECK(reqs[1]["method"] == "retrieve"); - CHECK(reqs[1]["params"]["namespace"] == -21); - CHECK_FALSE(reqs[1]["params"].contains("signature")); + // Subrequest 2: AccountPubkeys (ns -21) — no auth required. + CHECK(reqs[2]["method"] == "retrieve"); + CHECK(reqs[2]["params"]["namespace"] == -21); + CHECK_FALSE(reqs[2]["params"].contains("signature")); // Build a valid link request from a second device sharing the same account seed. cleared_b32 seed_bytes; @@ -95,7 +101,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { REQUIRE(mock_net->sent_requests.size() == 1); auto batch_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); - CHECK(batch_json2["requests"][0]["params"]["last_hash"] == "hash1"); + CHECK(batch_json2["requests"][1]["params"]["last_hash"] == "hash1"); } TEST_CASE( @@ -116,7 +122,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // No prior hash for any node — must not send last_hash. CHECK_FALSE(p.contains("last_hash")); } @@ -131,7 +137,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } @@ -141,7 +147,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // B has no recorded hash — must not send last_hash so we get everything. CHECK_FALSE(p.contains("last_hash")); } @@ -158,7 +164,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][0]["params"]; + auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } } From 01f1c670636fc1caf7b8b2612e34f68c815acdea Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 18:25:46 -0300 Subject: [PATCH 062/193] formatting --- include/session/core.hpp | 4 +- include/session/core/callbacks.hpp | 15 +- include/session/core/devices.hpp | 10 +- include/session/network/session_network.hpp | 2 +- include/session/pro_backend.h | 3 +- include/session/session_protocol.hpp | 6 +- src/core.cpp | 296 ++++++++++---------- src/core/devices.cpp | 17 +- src/pro_backend.cpp | 12 +- src/session_encrypt.cpp | 24 +- src/session_protocol.cpp | 41 +-- tests/test_dm_receive.cpp | 22 +- tests/test_helper.hpp | 2 +- tests/test_poll.cpp | 16 +- tests/test_session_encrypt.cpp | 66 +---- tests/utils.hpp | 4 +- 16 files changed, 251 insertions(+), 289 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index cd480d5d4..f413c11dc 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -303,9 +303,7 @@ class Core { // messages after a non-final call, the caller should still call this with an empty span and // is_final=true to flush any actions that are deferred until the end of a fetch. void receive_messages( - std::span messages, - config::Namespace ns, - bool is_final); + std::span messages, config::Namespace ns, bool is_final); }; } // namespace session::core diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index a8e3dfac2..6c53b98bb 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -35,19 +35,20 @@ enum class MessageDecryptFailure { no_pfs_key, ///< Version 2 message but no account key with a matching key indicator exists decrypt_failed, ///< Decryption failed (either version); key was found but did not work bad_format, ///< Message is structurally malformed (e.g. invalid bencode, truncated fields) - unknown_version, ///< Message starts with 0x00 but carries an unrecognised version byte; - ///< likely a future protocol version this build does not understand + unknown_version, ///< Message starts with 0x00 but carries an unrecognised version byte; + ///< likely a future protocol version this build does not understand }; /// A successfully decrypted one-to-one message from Namespace::Default. struct ReceivedMessage { - std::string hash; ///< Swarm-assigned message hash - sys_ms timestamp; ///< Server-reported upload timestamp - sys_ms expiry; ///< Server-reported expiry timestamp + std::string hash; ///< Swarm-assigned message hash + sys_ms timestamp; ///< Server-reported upload timestamp + sys_ms expiry; ///< Server-reported expiry timestamp std::array sender_session_id; ///< 0x05-prefixed sender session ID - int version; ///< Protocol version: 1 or 2 + int version; ///< Protocol version: 1 or 2 std::vector content; ///< Decrypted protobuf-encoded payload - std::optional> pro_signature; ///< Session Pro signature, if present + std::optional> + pro_signature; ///< Session Pro signature, if present }; /// Struct holding application callbacks to fire when libsession Core events happen to allow the diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 1fdcadb4b..687613738 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -243,11 +243,11 @@ class Devices final : detail::CoreComponent { // Returns the current active account keys after pruning obsolete ones: that is, the current key // plus all keys that were rotated away fewer than ACCOUNT_KEY_RETENTION ago. Keys are returned // sorted from newest to oldest. If there are no keys at all, generates an initial one. - // Returns account keys, ordered with the active (unrotated) key first then most-recently-rotated - // first. Expired rotated keys are pruned before querying. If key_indicator is given, only - // keys whose ML-KEM-768 pubkey begins with those two bytes are returned (using the indexed - // key_indicator virtual column); otherwise all retained keys are returned and a new key is - // auto-generated if none is currently active. + // Returns account keys, ordered with the active (unrotated) key first then + // most-recently-rotated first. Expired rotated keys are pruned before querying. If + // key_indicator is given, only keys whose ML-KEM-768 pubkey begins with those two bytes are + // returned (using the indexed key_indicator virtual column); otherwise all retained keys are + // returned and a new key is auto-generated if none is currently active. std::vector active_account_keys( std::optional> key_indicator = std::nullopt); diff --git a/include/session/network/session_network.hpp b/include/session/network/session_network.hpp index ba90e9961..34f2f927b 100644 --- a/include/session/network/session_network.hpp +++ b/include/session/network/session_network.hpp @@ -53,7 +53,7 @@ class Network : public std::enable_shared_from_this { requires(!std::is_same_v< std::decay_t>>, config::Config>) - Network(Opt&&... opts) : Network{config::Config{std::forward(opts)...}}{}; + Network(Opt&&... opts) : Network{config::Config{std::forward(opts)...}} {}; explicit Network(config::Config config); virtual ~Network(); diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 1d73d5694..ae0256141 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -114,7 +114,8 @@ struct session_pro_backend_response_header { /// Array of error messages (NULL if no errors), with errors_count elements string8* errors; size_t errors_count; - unsigned char* internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch + unsigned char* + internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch }; typedef struct session_pro_backend_to_json session_pro_backend_to_json; diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 61765775a..61bb8bf47 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -132,7 +132,8 @@ class ProProof { /// /// Outputs: /// - `bool` - True if the message was signed by the embedded `rotating_pubkey` false otherwise. - bool verify_message(std::span sig, const std::span msg) const; + bool verify_message( + std::span sig, const std::span msg) const; /// API: pro/Proof::is_active /// @@ -326,7 +327,8 @@ struct DecodeEnvelopeKey { // payload is encrypted (e.g. groups v2) and that the contents are unencrypted. If this key is // not set the it's assumed the envelope is not encrypted but the contents are encrypted (e.g.: // 1o1 or legacy group). - std::optional> group_ed25519_pubkey; + std::optional> + group_ed25519_pubkey; // List of libsodium-style secret key to decrypt the envelope from. Can also be passed as a 32 // byte secret key. The public key component is not used. diff --git a/src/core.cpp b/src/core.cpp index 63d7b4093..62a659543 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -143,8 +143,13 @@ void Core::_poll() { net->get_swarm( globals.pubkey_x25519(), false, - [this, net, namespaces, session_id_hex, ed25519_hex, now_ms, - ns_sig = std::move(ns_sig)](auto, auto swarm) { + [this, + net, + namespaces, + session_id_hex, + ed25519_hex, + now_ms, + ns_sig = std::move(ns_sig)](auto, auto swarm) { if (swarm.empty()) return; @@ -236,25 +241,25 @@ void Core::_poll() { auto b64 = data_it->get(); decoded.reserve(oxenc::from_base64_size(b64.size())); oxenc::from_base64( - b64.begin(), b64.end(), + b64.begin(), + b64.end(), std::back_inserter(decoded)); SwarmMessage swarm_msg; swarm_msg.data = {decoded.data(), decoded.size()}; if (auto h = msg.find("hash"); - h != msg.end() && h->is_string()) { + h != msg.end() && h->is_string()) { swarm_msg.hash = h->get(); newest_hash = swarm_msg.hash; } if (auto t = msg.find("timestamp"); - t != msg.end() && t->is_number_integer()) - swarm_msg.timestamp = - from_epoch_ms(t->get()); + t != msg.end() && t->is_number_integer()) + swarm_msg.timestamp = from_epoch_ms(t->get()); if (auto e = msg.find("expiry"); - e != msg.end() && e->is_number_integer()) + e != msg.end() && e->is_number_integer()) swarm_msg.expiry = from_epoch_ms(e->get()); swarm_messages.push_back(std::move(swarm_msg)); @@ -351,134 +356,129 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ {"namespace", static_cast(config::Namespace::AccountPubkeys)}, }; - net->get_swarm( - x25519_pub, false, [this, net, sid = std::move(sid), params](auto, auto swarm) { - if (swarm.empty()) { - log::debug(cat, "prefetch_pfs_keys: get_swarm returned empty swarm"); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); - return; - } + net->get_swarm(x25519_pub, false, [this, net, sid = std::move(sid), params](auto, auto swarm) { + if (swarm.empty()) { + log::debug(cat, "prefetch_pfs_keys: get_swarm returned empty swarm"); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); + return; + } - auto body_str = params.dump(); - net->send_request( - network::Request{ - swarm.front(), - "retrieve", - to_vector(body_str), - network::RequestCategory::standard_small, - 20s}, - [this, sid = std::move(sid)]( - bool success, - bool /*timeout*/, - int16_t /*status_code*/, - std::vector> /*headers*/, - std::optional body) { - if (!success || !body) { - log::debug(cat, "prefetch_pfs_keys: request failed"); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); - return; - } + auto body_str = params.dump(); + net->send_request( + network::Request{ + swarm.front(), + "retrieve", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this, sid = std::move(sid)]( + bool success, + bool /*timeout*/, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional body) { + if (!success || !body) { + log::debug(cat, "prefetch_pfs_keys: request failed"); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); + return; + } - try { - auto json = nlohmann::json::parse(*body); - auto msgs_it = json.find("messages"); - if (msgs_it == json.end() || !msgs_it->is_array()) { - log::warning( - cat, - "prefetch_pfs_keys: response missing or invalid " - "'messages' array"); - return; - } + try { + auto json = nlohmann::json::parse(*body); + auto msgs_it = json.find("messages"); + if (msgs_it == json.end() || !msgs_it->is_array()) { + log::warning( + cat, + "prefetch_pfs_keys: response missing or invalid " + "'messages' array"); + return; + } - // Strip the 0x05 prefix to get the x25519 pubkey for - // signature verification. - std::span x25519_pub{sid.data() + 1, 32}; - - // Track the most recently valid pubkeys seen across all messages. - std::optional> pk_x25519; - std::optional> - pk_mlkem768; - - for (const auto& msg : *msgs_it) { - auto data_it = msg.find("data"); - if (data_it == msg.end() || !data_it->is_string()) { - log::warning( - cat, - "prefetch_pfs_keys: message missing or " - "non-string 'data' field"); - continue; - } - auto b64 = data_it->get(); - std::vector decoded; - decoded.reserve(oxenc::from_base64_size(b64.size())); - oxenc::from_base64( - b64.begin(), b64.end(), std::back_inserter(decoded)); - try { - oxenc::bt_dict_consumer in{decoded}; - auto M = in.require_span< - std::byte, - MLKEM768_PUBLICKEYBYTES>("M"); - auto X = in.require_span("X"); - in.require_signature( - "~", - [&x25519_pub]( - std::span b, - std::span sig) { - if (!xed25519::verify(sig, x25519_pub, b)) - throw std::runtime_error{ - "signature verification " - "failed"}; - }); - std::ranges::copy(X, pk_x25519.emplace().begin()); - std::ranges::copy(M, pk_mlkem768.emplace().begin()); - } catch (const std::exception& e) { - log::warning( - cat, - "Ignoring malformed remote account pubkey " - "message: {}", - e.what()); - } - } + // Strip the 0x05 prefix to get the x25519 pubkey for + // signature verification. + std::span x25519_pub{sid.data() + 1, 32}; - auto now_s = epoch_seconds(clock_now_s()); - - if (!pk_x25519 || !pk_mlkem768) { - log::debug( - cat, - "prefetch_pfs_keys: no valid account pubkey message " - "found in response"); - // Record a NAK. If a valid entry already exists, update only - // nak_at and leave fetched_at and pubkeys untouched. - db.conn().prepared_exec( - R"( + // Track the most recently valid pubkeys seen across all messages. + std::optional> pk_x25519; + std::optional> pk_mlkem768; + + for (const auto& msg : *msgs_it) { + auto data_it = msg.find("data"); + if (data_it == msg.end() || !data_it->is_string()) { + log::warning( + cat, + "prefetch_pfs_keys: message missing or " + "non-string 'data' field"); + continue; + } + auto b64 = data_it->get(); + std::vector decoded; + decoded.reserve(oxenc::from_base64_size(b64.size())); + oxenc::from_base64(b64.begin(), b64.end(), std::back_inserter(decoded)); + try { + oxenc::bt_dict_consumer in{decoded}; + auto M = in.require_span("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub]( + std::span b, + std::span sig) { + if (!xed25519::verify(sig, x25519_pub, b)) + throw std::runtime_error{ + "signature verification " + "failed"}; + }); + std::ranges::copy(X, pk_x25519.emplace().begin()); + std::ranges::copy(M, pk_mlkem768.emplace().begin()); + } catch (const std::exception& e) { + log::warning( + cat, + "Ignoring malformed remote account pubkey " + "message: {}", + e.what()); + } + } + + auto now_s = epoch_seconds(clock_now_s()); + + if (!pk_x25519 || !pk_mlkem768) { + log::debug( + cat, + "prefetch_pfs_keys: no valid account pubkey message " + "found in response"); + // Record a NAK. If a valid entry already exists, update only + // nak_at and leave fetched_at and pubkeys untouched. + db.conn().prepared_exec( + R"( INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) VALUES (?, NULL, ?, NULL, NULL) ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at )", - sid, - now_s); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); - return; - } + sid, + now_s); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); + return; + } - auto conn = db.conn(); - SQLite::Transaction tx{conn.sql}; - - bool is_unchanged = conn.prepared_maybe_get( - "SELECT 1 FROM pfs_key_cache" - " WHERE session_id = ?" - " AND pubkey_x25519 = ? AND " - "pubkey_mlkem768 = ?", - sid, - *pk_x25519, - *pk_mlkem768) - .has_value(); - - conn.prepared_exec( - R"( + auto conn = db.conn(); + SQLite::Transaction tx{conn.sql}; + + bool is_unchanged = conn.prepared_maybe_get( + R"( +SELECT 1 FROM pfs_key_cache +WHERE session_id = ? AND pubkey_x25519 = ? AND pubkey_mlkem768 = ? +)", + sid, + *pk_x25519, + *pk_mlkem768) + .has_value(); + + conn.prepared_exec( + R"( INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) VALUES (?, ?, NULL, ?, ?) ON CONFLICT(session_id) DO UPDATE SET @@ -486,24 +486,20 @@ ON CONFLICT(session_id) DO UPDATE SET pubkey_x25519 = excluded.pubkey_x25519, pubkey_mlkem768 = excluded.pubkey_mlkem768 )", - sid, - now_s, - *pk_x25519, - *pk_mlkem768); - tx.commit(); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched( - sid, - is_unchanged ? PfsKeyFetch::unchanged - : PfsKeyFetch::new_key); - } catch (const std::exception& e) { - log::warning( - cat, - "Failed to process PFS key fetch response: {}", - e.what()); - } - }); - }); + sid, + now_s, + *pk_x25519, + *pk_mlkem768); + tx.commit(); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched( + sid, + is_unchanged ? PfsKeyFetch::unchanged : PfsKeyFetch::new_key); + } catch (const std::exception& e) { + log::warning(cat, "Failed to process PFS key fetch response: {}", e.what()); + } + }); + }); return status; } @@ -578,11 +574,7 @@ void Core::_handle_direct_messages(std::span messages) { for (auto& key : keys) { try { auto result = decrypt_incoming_v2( - session_id, - key.x25519_sec, - key.x25519_pub, - key.mlkem768_sec, - data); + session_id, key.x25519_sec, key.x25519_pub, key.mlkem768_sec, data); ReceivedMessage out; out.hash = msg.hash; out.timestamp = msg.timestamp; @@ -633,9 +625,7 @@ void Core::_handle_direct_messages(std::span messages) { } void Core::receive_messages( - std::span messages, - config::Namespace ns, - bool is_final) { + std::span messages, config::Namespace ns, bool is_final) { using config::Namespace; switch (ns) { case Namespace::Default: _handle_direct_messages(messages); break; diff --git a/src/core/devices.cpp b/src/core/devices.cpp index c7c462197..ec586a71e 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -218,8 +218,8 @@ std::vector Devices::active_account_keys( sqlite::blobn<32>>; for (auto [id, created, rotated, seed, pk_ml, pk_x] : - key_indicator ? cols_t{c.prepared_bind(query_ki, *key_indicator)} - : cols_t{c.prepared_bind(query_all)}) { + key_indicator ? cols_t{c.prepared_bind(query_ki, *key_indicator)} + : cols_t{c.prepared_bind(query_all)}) { auto& k = keys.emplace_back(keys_from_seed(seed)); k.created = std::chrono::sys_seconds{std::chrono::seconds{created}}; if (rotated) @@ -1065,9 +1065,7 @@ Devices::LinkRequestResult Devices::build_link_request() { std::memcpy(encrypted.data(), plaintext.data(), plaintext.size()); auto seed = core.globals.account_seed(); config::encrypt_prealloced( - as_span(std::span{encrypted}), - seed.seed(), - "link-request"); + as_span(std::span{encrypted}), seed.seed(), "link-request"); // Wrap in outer bt-dict: {"": "L", "L": } std::vector out( @@ -1240,8 +1238,7 @@ void Devices::receive_link_request(std::span data) { std::vector plaintext; try { auto seed = core.globals.account_seed(); - plaintext = config::decrypt( - encrypted, seed.seed(), "link-request"); + plaintext = config::decrypt(encrypted, seed.seed(), "link-request"); } catch (const config::decrypt_error& e) { log::warning(cat, "Ignoring incoming link request: decryption failed: {}", e.what()); return; @@ -1314,8 +1311,7 @@ void Devices::receive_link_request(std::span data) { tx.commit(); } -void Devices::parse_device_messages( - std::span messages, bool is_final) { +void Devices::parse_device_messages(std::span messages, bool is_final) { for (const auto& msg : messages) { try { oxenc::bt_dict_consumer in{msg.data}; @@ -1452,8 +1448,7 @@ void Devices::parse_device_messages( epoch_seconds(clock_now_s() - LINK_REQUEST_MAX_AGE)); } -void Devices::parse_account_pubkeys( - std::span messages, bool /*is_final*/) { +void Devices::parse_account_pubkeys(std::span messages, bool /*is_final*/) { if (messages.empty()) return; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 799446df0..69ee0693c 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -925,7 +925,8 @@ session_pro_backend_add_pro_payment_request_build_sigs( std::span rotating_span(rotating_privkey, rotating_privkey_len); std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span( + payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_master_rotating_signatures result = {}; try { @@ -970,7 +971,8 @@ session_pro_backend_add_pro_payment_request_build_to_json( std::span rotating_span(rotating_privkey, rotating_privkey_len); std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span( + payment_tx_order_id, payment_tx_order_id_len); try { std::string json = AddProPaymentRequest::build_to_json( @@ -1511,7 +1513,8 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r std::chrono::milliseconds(refund_requested_unix_ts_ms)}; std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span( + payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_signature result = {}; try { @@ -1557,7 +1560,8 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( std::chrono::milliseconds(refund_requested_unix_ts_ms)}; std::span payment_tx_payment_id_span( payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span(payment_tx_order_id, payment_tx_order_id_len); + std::span payment_tx_order_id_span( + payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_to_json result = {}; try { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 8bb30379a..e2921a448 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1349,8 +1349,7 @@ LIBSESSION_C_API bool session_decrypt_incoming( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - cspan<64>(ed25519_privkey), - cspan(ciphertext_in, ciphertext_len)); + cspan<64>(ed25519_privkey), cspan(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1434,9 +1433,7 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess DecryptGroupMessage result_cpp = {}; try { result_cpp = decrypt_group_message( - {&key, 1}, - cspan<32>(group_ed25519_pubkey), - cspan(ciphertext, ciphertext_len)); + {&key, 1}, cspan<32>(group_ed25519_pubkey), cspan(ciphertext, ciphertext_len)); result = { .success = true, .index = index, @@ -1469,8 +1466,8 @@ LIBSESSION_C_API bool session_decrypt_ons_response( if (nonce_in) nonce = cspan(nonce_in); - auto session_id = session::decrypt_ons_response( - name_in, cspan(ciphertext_in, ciphertext_len), nonce); + auto session_id = + session::decrypt_ons_response(name_in, cspan(ciphertext_in, ciphertext_len), nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -1487,8 +1484,7 @@ LIBSESSION_C_API bool session_decrypt_push_notification( size_t* plaintext_len) { try { auto plaintext = session::decrypt_push_notification( - cspan(payload_in, payload_len), - cspan<32>(enc_key_in)); + cspan(payload_in, payload_len), cspan<32>(enc_key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); @@ -1506,9 +1502,8 @@ LIBSESSION_C_API bool session_encrypt_xchacha20( unsigned char** ciphertext_out, size_t* ciphertext_len) { try { - auto ciphertext = session::encrypt_xchacha20( - cspan(plaintext_in, plaintext_len), - cspan<32>(key_in)); + auto ciphertext = + session::encrypt_xchacha20(cspan(plaintext_in, plaintext_len), cspan<32>(key_in)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1526,9 +1521,8 @@ LIBSESSION_C_API bool session_decrypt_xchacha20( unsigned char** plaintext_out, size_t* plaintext_len) { try { - auto plaintext = session::decrypt_xchacha20( - cspan(ciphertext_in, ciphertext_len), - cspan<32>(key_in)); + auto plaintext = + session::decrypt_xchacha20(cspan(ciphertext_in, ciphertext_len), cspan<32>(key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 5cf66fba7..29e72f8a7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -162,7 +162,8 @@ bool ProProof::verify_signature(const std::span& verify_pub return result; } -bool ProProof::verify_message(std::span sig, std::span msg) const { +bool ProProof::verify_message( + std::span sig, std::span msg) const { if (sig.size() != crypto_sign_ed25519_BYTES) throw std::invalid_argument{fmt::format( "Invalid signed_msg: Signature must be 64 bytes (was: {})", sig.size())}; @@ -277,8 +278,9 @@ std::vector encode_for_1o1( std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::SyncOr1o1; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey + ? *pro_rotating_ed25519_privkey + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; return encode_for_destination(plaintext, &ed25519_privkey, dest); @@ -293,8 +295,9 @@ std::vector encode_for_community_inbox( std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::CommunityInbox; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey + ? *pro_rotating_ed25519_privkey + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.recipient_pubkey = recipient_pubkey; dest.community_inbox_server_pubkey = community_pubkey; @@ -306,8 +309,9 @@ std::vector encode_for_community( std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::Community; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey + ? *pro_rotating_ed25519_privkey + : std::span{}; return encode_for_destination(plaintext, nullptr, dest); } @@ -320,8 +324,9 @@ std::vector encode_for_group( std::optional> pro_rotating_ed25519_privkey) { Destination dest = {}; dest.type = DestinationType::Group; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey ? *pro_rotating_ed25519_privkey - : std::span{}; + dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey + ? *pro_rotating_ed25519_privkey + : std::span{}; dest.sent_timestamp_ms = sent_timestamp; dest.group_ed25519_pubkey = group_ed25519_pubkey; dest.group_enc_key = group_enc_key; @@ -373,7 +378,8 @@ static std::span unpad_message(std::span(payload.data(), payload.data() + size_without_padding); + auto result = + std::span(payload.data(), payload.data() + size_without_padding); return result; } @@ -383,7 +389,8 @@ static EncryptedForDestinationInternal encode_for_destination_internal( const Ed25519PrivKeySpan* ed25519_privkey, DestinationType dest_type, std::span dest_pro_rotating_ed25519_privkey, - std::span dest_recipient_pubkey, + std::span + dest_recipient_pubkey, std::chrono::milliseconds dest_sent_timestamp_ms, std::span dest_community_inbox_server_pubkey, @@ -425,8 +432,8 @@ static EncryptedForDestinationInternal encode_for_destination_internal( switch (dest_type) { case DestinationType::Group: /*FALLTHRU*/ case DestinationType::SyncOr1o1: { - if (is_group && - dest_group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { + if (is_group && dest_group_ed25519_pubkey[0] != + static_cast(SessionIDPrefix::group)) { // Legacy groups which have a 05 prefixed key throw std::runtime_error{ "Unsupported configuration, encrypting for a legacy group (0x05 prefix) is " @@ -602,7 +609,8 @@ static EncryptedForDestinationInternal encode_for_destination_internal( if (use_malloc == UseMalloc::Yes) { result.ciphertext_c = span_u8_copy_or_throw(content.data(), content.size()); } else { - result.ciphertext_cpp = std::vector(content.begin(), content.end()); + result.ciphertext_cpp = + std::vector(content.begin(), content.end()); } } } break; @@ -1433,8 +1441,9 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( // Setup decryption keys and decrypt DecodeEnvelopeKey keys_cpp = {}; if (keys->group_ed25519_pubkey.size == crypto_sign_ed25519_PUBLICKEYBYTES) { - keys_cpp.group_ed25519_pubkey = std::span{ - keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; + keys_cpp.group_ed25519_pubkey = + std::span{ + keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; } else if (keys->group_ed25519_pubkey.size) { result.error_len_incl_null_terminator = snprintf_clamped( diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index 8433d0df4..ba9dd52eb 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -7,8 +8,6 @@ #include #include -#include - #include "test_helper.hpp" using namespace session; @@ -53,8 +52,7 @@ struct OwnedMessage { // Cast the std::byte pubkeys returned by TestHelper into unsigned-char spans. template std::span as_uc(const std::array& a) { - return std::span{ - reinterpret_cast(a.data()), N}; + return std::span{reinterpret_cast(a.data()), N}; } } // namespace @@ -192,8 +190,12 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { std::ranges::copy(other->globals.session_id(), other_session_id.begin()); auto ct = encrypt_for_recipient_v2( - sender.ed_sk, other_session_id, as_uc(x25519_bytes), as_uc(mlkem_bytes), - "01"_hex_u, std::nullopt); + sender.ed_sk, + other_session_id, + as_uc(x25519_bytes), + as_uc(mlkem_bytes), + "01"_hex_u, + std::nullopt); deliver(std::span{ct}); CHECK(received.empty()); REQUIRE(failures.size() == 1); @@ -209,8 +211,12 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); auto ct = encrypt_for_recipient_v2( - sender.ed_sk, recip_session_id, as_uc(x25519_bytes), as_uc(mlkem_bytes), - "01"_hex_u, std::nullopt); + sender.ed_sk, + recip_session_id, + as_uc(x25519_bytes), + as_uc(mlkem_bytes), + "01"_hex_u, + std::nullopt); // Wire format: [0,1]=version, [2,3]=ki, [4,35]=E, [36,1123]=mlkem_ct, [1124+]=xchacha. // Flip the final byte of the xchacha ciphertext to corrupt the AEAD tag. REQUIRE(ct.size() > 1124 + 16); diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 683fd1c45..19cb28ee7 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -3,11 +3,11 @@ #include #include -#include #include #include #include #include +#include #include #include diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 621539411..23e250506 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -38,8 +38,8 @@ TEST_CASE("Core automatic polling", "[core][poll]") { bool received = false; core::callbacks cbs; cbs.device_link_request = [&](int, - const core::device::Info&, - std::span) { received = true; }; + const core::device::Info&, + std::span) { received = true; }; TempCore core{cbs}; auto mock_net = std::make_shared(); @@ -122,7 +122,8 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // No prior hash for any node — must not send last_hash. CHECK_FALSE(p.contains("last_hash")); } @@ -137,7 +138,8 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } @@ -147,7 +149,8 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // B has no recorded hash — must not send last_hash so we get everything. CHECK_FALSE(p.contains("last_hash")); } @@ -164,7 +167,8 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } } diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index 337961d5f..e86c9ed6b 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -198,29 +198,18 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), - server_pk, - blind15_pk2_prefixed, - to_span("hello")); + to_span(ed_sk), server_pk, blind15_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2), - server_pk, - blind15_pk_prefixed, - blind15_pk2_prefixed, - enc); + to_span(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - server_pk, - blind15_pk_prefixed, - blind15_pk2_prefixed, - broken)); + to_span(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, sender decrypt") { constexpr auto lorem_ipsum = @@ -231,10 +220,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), - server_pk, - blind15_pk2_prefixed, - to_span(lorem_ipsum)); + to_span(ed_sk).first<32>(), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), enc.end(), @@ -268,10 +254,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), - server_pk, - blind15_pk2_prefixed, - to_span(lorem_ipsum)); + to_span(ed_sk).first<32>(), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), enc.end(), @@ -298,55 +281,33 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e } SECTION("blind25, full secret, sender decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), - server_pk, - blind25_pk2_prefixed, - to_span("hello")); + to_span(ed_sk), server_pk, blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk), - server_pk, - blind25_pk_prefixed, - blind25_pk2_prefixed, - enc); + to_span(ed_sk), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk), - server_pk, - blind25_pk_prefixed, - blind25_pk2_prefixed, - broken)); + to_span(ed_sk), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); } SECTION("blind25, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), - server_pk, - blind25_pk2_prefixed, - to_span("hello")); + to_span(ed_sk), server_pk, blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2), - server_pk, - blind25_pk_prefixed, - blind25_pk2_prefixed, - enc); + to_span(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), - server_pk, - blind25_pk_prefixed, - blind25_pk2_prefixed, - broken)); + to_span(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); } SECTION("blind25, only seed, recipient decrypt") { constexpr auto lorem_ipsum = @@ -357,10 +318,7 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), - server_pk, - blind25_pk2_prefixed, - to_span(lorem_ipsum)); + to_span(ed_sk).first<32>(), server_pk, blind25_pk2_prefixed, to_span(lorem_ipsum)); CHECK(std::search( enc.begin(), enc.end(), diff --git a/tests/utils.hpp b/tests/utils.hpp index 0db72c64b..72515b0ad 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -325,8 +325,8 @@ struct set_on_exit { /// Usage: /// bool got_it = false; /// callback_waiter waiter{[&got_it](bool x) { got_it = x; }}; -/// async_operation(waiter); // waiter implicitly converts to std::function -/// REQUIRE(waiter.wait()); // blocks up to 5s +/// async_operation(waiter); // waiter implicitly converts to std::function +/// REQUIRE(waiter.wait()); // blocks up to 5s /// CHECK(got_it); template struct callback_waiter { From 89d779ab8002b2ffa9d692b213ceab2e05f0c31b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 18:44:35 -0300 Subject: [PATCH 063/193] Split up big nested functions --- include/session/core.hpp | 8 + src/core.cpp | 370 ++++++++++++++++++++------------------- 2 files changed, 200 insertions(+), 178 deletions(-) diff --git a/include/session/core.hpp b/include/session/core.hpp index f413c11dc..f79010f48 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -11,6 +11,7 @@ #include "core/devices.hpp" #include "core/globals.hpp" #include "core/pro.hpp" +#include "session/network/key_types.hpp" /// The "Core" class is intended to be used by a Session account instance to hold account state. It /// manages an encrypted sqlite database for storing account keys, messages, and other account @@ -205,10 +206,17 @@ class Core { std::shared_ptr _poll_ticker; void _update_polling(); void _poll(); + void _handle_poll_response( + const network::ed25519_pubkey& sn_pubkey, + std::span namespaces, + std::string body); // Decrypts and dispatches one-to-one messages from Namespace::Default. void _handle_direct_messages(std::span messages); + // Handles a PFS fetch response + void _handle_pfs_response(std::span sid, std::string body); + // Extracts an option of type T from a pack; returns the first match wrapped in optional, or // nullopt if not present. Mirrors the same helper in session::sqlite::Database. template diff --git a/src/core.cpp b/src/core.cpp index 62a659543..20348b4c3 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -25,6 +24,7 @@ #include #include +#include "session/config/namespaces.hpp" #include "session/core/component.hpp" namespace session::core { @@ -195,94 +195,101 @@ void Core::_poll() { 20s}, [this, sn_pubkey = node.remote_pubkey, namespaces]( bool success, - bool /*timeout*/, + bool timeout, int16_t /*status_code*/, std::vector> /*headers*/, std::optional body) { - if (!success || !body) + if (!success || !body) { + log::warning( + cat, + "Swarm poll request failed: {}", + timeout ? "timed out" + : body ? *body + : "request failed"); return; + } + + _handle_poll_response(sn_pubkey, namespaces, std::move(*body)); + }); + }); +} + +void Core::_handle_poll_response( + const network::ed25519_pubkey& sn_pubkey, + std::span namespaces, + std::string body) { + try { + auto json = nlohmann::json::parse(body); + auto it = json.find("results"); + if (it == json.end() || !it->is_array()) + return; + + auto& results = *it; + auto conn = db.conn(); + for (size_t i = 0; i < namespaces.size() && i < results.size(); ++i) { + const auto& res = results[i]; + auto code_it = res.find("code"); + if (code_it == res.end() || code_it->get() != 200) + continue; + auto body_it = res.find("body"); + if (body_it == res.end()) + continue; + auto msgs_it = body_it->find("messages"); + if (msgs_it == body_it->end() || !msgs_it->is_array()) + continue; + + auto ns = namespaces[i]; + auto ns_val = static_cast(ns); + + // Decode each message; keep the decoded bytes alive until after + // receive_messages() returns, since SwarmMessage::data spans + // into them. + std::vector> messages_data; + std::vector swarm_messages; + std::string newest_hash; + + for (const auto& msg : *msgs_it) { + auto data_it = msg.find("data"); + if (data_it == msg.end() || !data_it->is_string()) + continue; + auto& decoded = messages_data.emplace_back(); + auto b64 = data_it->get(); + decoded.reserve(oxenc::from_base64_size(b64.size())); + oxenc::from_base64(b64.begin(), b64.end(), std::back_inserter(decoded)); + + SwarmMessage swarm_msg; + swarm_msg.data = {decoded.data(), decoded.size()}; + + if (auto h = msg.find("hash"); h != msg.end() && h->is_string()) { + swarm_msg.hash = h->get(); + newest_hash = swarm_msg.hash; + } + + if (auto t = msg.find("timestamp"); t != msg.end() && t->is_number_integer()) + swarm_msg.timestamp = from_epoch_ms(t->get()); - try { - auto json = nlohmann::json::parse(*body); - auto it = json.find("results"); - if (it == json.end() || !it->is_array()) - return; - - auto& results = *it; - auto conn = db.conn(); - for (size_t i = 0; i < namespaces.size() && i < results.size(); - ++i) { - const auto& res = results[i]; - auto code_it = res.find("code"); - if (code_it == res.end() || code_it->get() != 200) - continue; - auto body_it = res.find("body"); - if (body_it == res.end()) - continue; - auto msgs_it = body_it->find("messages"); - if (msgs_it == body_it->end() || !msgs_it->is_array()) - continue; - - auto ns = namespaces[i]; - auto ns_val = static_cast(ns); - - // Decode each message; keep the decoded bytes alive until after - // receive_messages() returns, since SwarmMessage::data spans - // into them. - std::vector> messages_data; - std::vector swarm_messages; - std::string newest_hash; - - for (const auto& msg : *msgs_it) { - auto data_it = msg.find("data"); - if (data_it == msg.end() || !data_it->is_string()) - continue; - auto& decoded = messages_data.emplace_back(); - auto b64 = data_it->get(); - decoded.reserve(oxenc::from_base64_size(b64.size())); - oxenc::from_base64( - b64.begin(), - b64.end(), - std::back_inserter(decoded)); - - SwarmMessage swarm_msg; - swarm_msg.data = {decoded.data(), decoded.size()}; - - if (auto h = msg.find("hash"); - h != msg.end() && h->is_string()) { - swarm_msg.hash = h->get(); - newest_hash = swarm_msg.hash; - } - - if (auto t = msg.find("timestamp"); - t != msg.end() && t->is_number_integer()) - swarm_msg.timestamp = from_epoch_ms(t->get()); - - if (auto e = msg.find("expiry"); - e != msg.end() && e->is_number_integer()) - swarm_msg.expiry = from_epoch_ms(e->get()); - - swarm_messages.push_back(std::move(swarm_msg)); - } - - if (!swarm_messages.empty()) { - if (!newest_hash.empty()) - conn.prepared_exec( - R"( + if (auto e = msg.find("expiry"); e != msg.end() && e->is_number_integer()) + swarm_msg.expiry = from_epoch_ms(e->get()); + + swarm_messages.push_back(std::move(swarm_msg)); + } + + if (!swarm_messages.empty()) { + if (!newest_hash.empty()) + conn.prepared_exec( + R"( INSERT INTO namespace_sync (namespace, sn_pubkey, last_hash) VALUES (?, ?, ?) ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash )", - ns_val, - sn_pubkey, - newest_hash); - receive_messages(swarm_messages, ns, true); - } - } - } catch (const std::exception& e) { - log::warning(cat, "Failed to parse poll response: {}", e.what()); - } - }); - }); + ns_val, + sn_pubkey, + newest_hash); + receive_messages(swarm_messages, ns, true); + } + } + } catch (const std::exception& e) { + log::warning(cat, "Failed to parse poll response: {}", e.what()); + } } PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) { @@ -374,111 +381,122 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ 20s}, [this, sid = std::move(sid)]( bool success, - bool /*timeout*/, + bool timeout, int16_t /*status_code*/, std::vector> /*headers*/, std::optional body) { if (!success || !body) { - log::debug(cat, "prefetch_pfs_keys: request failed"); + log::warning( + cat, + "Failed to fetch PFS keys for {}: {}", + oxenc::to_hex(sid), + timeout ? "timed out" + : body ? *body + : "request failed"); if (callbacks.pfs_keys_fetched) callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); return; } - try { - auto json = nlohmann::json::parse(*body); - auto msgs_it = json.find("messages"); - if (msgs_it == json.end() || !msgs_it->is_array()) { - log::warning( - cat, - "prefetch_pfs_keys: response missing or invalid " - "'messages' array"); - return; - } + return _handle_pfs_response(sid, std::move(*body)); + }); + }); + return status; +} - // Strip the 0x05 prefix to get the x25519 pubkey for - // signature verification. - std::span x25519_pub{sid.data() + 1, 32}; +void Core::_handle_pfs_response(std::span sid, std::string body) { + try { + auto json = nlohmann::json::parse(body); + auto msgs_it = json.find("messages"); + if (msgs_it == json.end() || !msgs_it->is_array()) { + log::warning( + cat, + "prefetch_pfs_keys: response missing or invalid " + "'messages' array"); + return; + } - // Track the most recently valid pubkeys seen across all messages. - std::optional> pk_x25519; - std::optional> pk_mlkem768; + // Strip the 0x05 prefix to get the x25519 pubkey for + // signature verification. + auto x25519_pub = sid.subspan<1>(); - for (const auto& msg : *msgs_it) { - auto data_it = msg.find("data"); - if (data_it == msg.end() || !data_it->is_string()) { - log::warning( - cat, - "prefetch_pfs_keys: message missing or " - "non-string 'data' field"); - continue; - } - auto b64 = data_it->get(); - std::vector decoded; - decoded.reserve(oxenc::from_base64_size(b64.size())); - oxenc::from_base64(b64.begin(), b64.end(), std::back_inserter(decoded)); - try { - oxenc::bt_dict_consumer in{decoded}; - auto M = in.require_span("M"); - auto X = in.require_span("X"); - in.require_signature( - "~", - [&x25519_pub]( - std::span b, - std::span sig) { - if (!xed25519::verify(sig, x25519_pub, b)) - throw std::runtime_error{ - "signature verification " - "failed"}; - }); - std::ranges::copy(X, pk_x25519.emplace().begin()); - std::ranges::copy(M, pk_mlkem768.emplace().begin()); - } catch (const std::exception& e) { - log::warning( - cat, - "Ignoring malformed remote account pubkey " - "message: {}", - e.what()); - } - } + // Track the most recently valid pubkeys seen across all messages. + std::optional> pk_x25519; + std::optional> pk_mlkem768; + + for (const auto& msg : *msgs_it) { + auto data_it = msg.find("data"); + if (data_it == msg.end() || !data_it->is_string()) { + log::warning( + cat, + "prefetch_pfs_keys: message missing or " + "non-string 'data' field"); + continue; + } + auto b64 = data_it->get(); + std::vector decoded; + decoded.reserve(oxenc::from_base64_size(b64.size())); + oxenc::from_base64(b64.begin(), b64.end(), std::back_inserter(decoded)); + try { + oxenc::bt_dict_consumer in{decoded}; + auto M = in.require_span("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub]( + std::span b, + std::span sig) { + if (!xed25519::verify(sig, x25519_pub, b)) + throw std::runtime_error{"signature verification failed"}; + }); + std::ranges::copy(X, pk_x25519.emplace().begin()); + std::ranges::copy(M, pk_mlkem768.emplace().begin()); + } catch (const std::exception& e) { + log::warning( + cat, + "Ignoring malformed remote account pubkey " + "message: {}", + e.what()); + } + } + + auto now_s = epoch_seconds(clock_now_s()); - auto now_s = epoch_seconds(clock_now_s()); - - if (!pk_x25519 || !pk_mlkem768) { - log::debug( - cat, - "prefetch_pfs_keys: no valid account pubkey message " - "found in response"); - // Record a NAK. If a valid entry already exists, update only - // nak_at and leave fetched_at and pubkeys untouched. - db.conn().prepared_exec( - R"( + if (!pk_x25519 || !pk_mlkem768) { + log::debug( + cat, + "prefetch_pfs_keys: no valid account pubkey message " + "found in response"); + // Record a NAK. If a valid entry already exists, update only + // nak_at and leave fetched_at and pubkeys untouched. + db.conn().prepared_exec( + R"( INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) VALUES (?, NULL, ?, NULL, NULL) ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at )", - sid, - now_s); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); - return; - } + sid, + now_s); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); + return; + } - auto conn = db.conn(); - SQLite::Transaction tx{conn.sql}; + auto conn = db.conn(); + SQLite::Transaction tx{conn.sql}; - bool is_unchanged = conn.prepared_maybe_get( - R"( + bool is_unchanged = conn.prepared_maybe_get( + R"( SELECT 1 FROM pfs_key_cache WHERE session_id = ? AND pubkey_x25519 = ? AND pubkey_mlkem768 = ? )", - sid, - *pk_x25519, - *pk_mlkem768) - .has_value(); + sid, + *pk_x25519, + *pk_mlkem768) + .has_value(); - conn.prepared_exec( - R"( + conn.prepared_exec( + R"( INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) VALUES (?, ?, NULL, ?, ?) ON CONFLICT(session_id) DO UPDATE SET @@ -486,21 +504,17 @@ ON CONFLICT(session_id) DO UPDATE SET pubkey_x25519 = excluded.pubkey_x25519, pubkey_mlkem768 = excluded.pubkey_mlkem768 )", - sid, - now_s, - *pk_x25519, - *pk_mlkem768); - tx.commit(); - if (callbacks.pfs_keys_fetched) - callbacks.pfs_keys_fetched( - sid, - is_unchanged ? PfsKeyFetch::unchanged : PfsKeyFetch::new_key); - } catch (const std::exception& e) { - log::warning(cat, "Failed to process PFS key fetch response: {}", e.what()); - } - }); - }); - return status; + sid, + now_s, + *pk_x25519, + *pk_mlkem768); + tx.commit(); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched( + sid, is_unchanged ? PfsKeyFetch::unchanged : PfsKeyFetch::new_key); + } catch (const std::exception& e) { + log::warning(cat, "Failed to process PFS key fetch response: {}", e.what()); + } } void Core::_handle_direct_messages(std::span messages) { From d4fb0a414491382d9c0c3f7a843cb8d9d573e163 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 19:50:01 -0300 Subject: [PATCH 064/193] session_protocol: Replace destination-dispatch pattern with typed encode functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The encoding side of session_protocol previously used a runtime-dispatch pattern: callers populated a Destination struct (C++) or session_protocol_destination struct (C API) with a type tag (DestinationType / SESSION_PROTOCOL_DESTINATION_TYPE_*) and a union-like collection of fields, then passed the whole thing to a single encode_for_destination / session_protocol_encode_for_destination dispatcher that switched on the type tag to call the appropriate internal logic. This pattern had several problems: - The struct carried fields for all destination types simultaneously, making it unclear at the call site which fields were relevant for a given type. Callers had to know implicitly that e.g. community_pubkey is meaningless for a 1o1 message. - The type-tag switch was a source of silent bugs: adding a new destination type required updating the switch in multiple places, and an unhandled case would silently fall through. - The structs and enums leaked into the public API despite carrying no semantic value beyond routing — they existed purely as a workaround for having a single entry point. - On the C++ side there was also a redundant _impl layer: four static *_impl functions held the real logic, thin public wrappers forwarded to them, and C API wrappers also forwarded to them — three levels of indirection for no benefit. Replacement: Four typed, direct encode functions, one per message kind: - encode_dm_v1 / session_protocol_encode_dm_v1 — one-on-one and sync messages (renamed from encode_for_1o1 to better reflect the protocol layer) - encode_for_community_inbox / session_protocol_encode_for_community_inbox — blinded community DMs - encode_for_community / session_protocol_encode_for_community — community (open group) messages - encode_for_group / session_protocol_encode_for_group — closed group v2 messages Each function takes exactly the parameters it needs and nothing else. The type system enforces correctness at the call site: there is no way to accidentally omit a required field or supply an irrelevant one. The _impl layer is gone; each public function directly contains its implementation body, with encode_for_community_inbox delegating to encode_for_community for the shared pad-and-optionally-sign logic. The Destination, DestinationType, session_protocol_destination, and SESSION_PROTOCOL_DESTINATION_TYPE_* types are fully removed from both the C++ and C APIs. The single-dispatch encode_for_destination / session_protocol_encode_for_destination entry points are removed along with them. Related: Ed25519PrivKeySpan / OptionalEd25519PrivKeySpan improvements All four encode functions take the pro rotating key as const OptionalEd25519PrivKeySpan&. Two constructor improvements were made to these types to make call sites cleaner: - Ed25519PrivKeySpan(const unsigned char*, size_t) is now non-explicit, allowing {ptr, size} brace-init at call sites (safe with two parameters — no silent single-argument conversion risk). The redundant from(ptr, size) factory was removed. - OptionalEd25519PrivKeySpan(const unsigned char*, size_t) was added as a non-explicit constructor: size == 0 produces the null (no-pro) state; size == 32 or size == 64 constructs the key. This allows C API boundaries to pass {ptr, len} naturally without an explicit OptionalEd25519PrivKeySpan{...} wrapper. --- include/session/ed25519.hpp | 53 ++- include/session/session_protocol.h | 74 +--- include/session/session_protocol.hpp | 149 ++----- src/session_encrypt.cpp | 2 +- src/session_protocol.cpp | 641 +++++++++------------------ tests/test_dm_receive.cpp | 6 +- tests/test_ed25519.cpp | 3 +- tests/test_session_protocol.cpp | 120 ++--- 8 files changed, 362 insertions(+), 686 deletions(-) diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp index 743c97a2e..99901b9d2 100644 --- a/include/session/ed25519.hpp +++ b/include/session/ed25519.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "session/sodium_array.hpp" @@ -28,9 +29,9 @@ struct Ed25519PrivKeySpan { } } - // Explicit constructor for runtime-known sizes (e.g. at C API boundaries). + // Constructor for runtime-known sizes (e.g. at C API boundaries). // Throws std::invalid_argument if size is not 32 or 64. - explicit Ed25519PrivKeySpan(const unsigned char* data, size_t size) { + Ed25519PrivKeySpan(const unsigned char* data, size_t size) { if (size == 64) data_ = data; else if (size == 32) { @@ -40,12 +41,9 @@ struct Ed25519PrivKeySpan { throw std::invalid_argument{"Ed25519 private key must be 32 or 64 bytes"}; } - // Named factory aliases for the explicit constructor above, for readability at call sites. + // Named factory alias for dynamic-span input. static Ed25519PrivKeySpan from(std::span key) { - return Ed25519PrivKeySpan{key.data(), key.size()}; - } - static Ed25519PrivKeySpan from(const unsigned char* data, size_t size) { - return Ed25519PrivKeySpan{data, size}; + return {key.data(), key.size()}; } Ed25519PrivKeySpan(const Ed25519PrivKeySpan&) = delete; @@ -74,6 +72,47 @@ struct Ed25519PrivKeySpan { std::optional storage_; }; +/// Like Ed25519PrivKeySpan but with an optional (nullable) state. Use this when a private key +/// parameter is optional; Ed25519PrivKeySpan retains its always-has-value guarantee. +/// +/// Implicitly constructible from the same 32- or 64-byte sources as Ed25519PrivKeySpan, plus from +/// default/nullopt for the empty state. Non-copyable and non-moveable for the same reason as +/// Ed25519PrivKeySpan. +struct OptionalEd25519PrivKeySpan { + /// Constructs a null (no-key) state. + OptionalEd25519PrivKeySpan() = default; + OptionalEd25519PrivKeySpan(std::nullopt_t) {} + + template + requires( + std::constructible_from, const T&> || + std::constructible_from, const T&>) + OptionalEd25519PrivKeySpan(const T& src) : key_{std::in_place, src} {} + + // Constructor for runtime-known sizes (e.g. at C API boundaries). + // size == 0 produces the null state; size == 32 or 64 constructs the key. + // Throws std::invalid_argument if size is not 0, 32, or 64. + OptionalEd25519PrivKeySpan(const unsigned char* data, size_t size) { + if (size) + key_.emplace(data, size); + } + + OptionalEd25519PrivKeySpan(const OptionalEd25519PrivKeySpan&) = delete; + OptionalEd25519PrivKeySpan& operator=(const OptionalEd25519PrivKeySpan&) = delete; + OptionalEd25519PrivKeySpan(OptionalEd25519PrivKeySpan&&) = delete; + OptionalEd25519PrivKeySpan& operator=(OptionalEd25519PrivKeySpan&&) = delete; + + bool has_value() const { return key_.has_value(); } + explicit operator bool() const { return has_value(); } + + const Ed25519PrivKeySpan& value() const { return key_.value(); } + const Ed25519PrivKeySpan& operator*() const { return *key_; } + const Ed25519PrivKeySpan* operator->() const { return &*key_; } + + private: + std::optional key_; +}; + } // namespace session namespace session::ed25519 { diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 3fd062e6f..3ca2ede6e 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -127,25 +127,6 @@ typedef enum SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS { // See session::Pro SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_EXCEEDS_CHARACTER_LIMIT, } SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS; -typedef enum SESSION_PROTOCOL_DESTINATION_TYPE { // See session::DestinationType - SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1, - SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, - SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, - SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, -} SESSION_PROTOCOL_DESTINATION_TYPE; - -typedef struct session_protocol_destination session_protocol_destination; -struct session_protocol_destination { // See session::Destination - SESSION_PROTOCOL_DESTINATION_TYPE type; - const void* pro_rotating_ed25519_privkey; - size_t pro_rotating_ed25519_privkey_len; - bytes33 recipient_pubkey; - uint64_t sent_timestamp_ms; - bytes32 community_inbox_server_pubkey; - bytes33 group_ed25519_pubkey; - bytes32 group_enc_key; -}; - // Indicates which optional fields in the envelope has been populated out of the optional fields in // an envelope after it has been parsed off the wire. typedef uint32_t SESSION_PROTOCOL_ENVELOPE_FLAGS; @@ -405,13 +386,13 @@ LIBSESSION_EXPORT session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( uint16_t const* utf, size_t utf_size) NON_NULL_ARG(1); -/// API: session_protocol_encode_for_1o1 +/// API: session_protocol_encode_dm_v1 /// /// Encode a plaintext message for a one-on-one (1o1) conversation or sync message in the Session /// Protocol. This function wraps the plaintext in the necessary structures and encrypts it for /// transmission to a single recipient. /// -/// See: session_protocol/encode_for_1o1 for more information +/// See: session_protocol/encode_dm_v1 for more information /// /// The encoded result must be freed with session_protocol_encrypt_for_destination_free when /// the caller is done with the result. @@ -451,7 +432,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( /// required to write the error. Both counts include the null-terminator. The user must allocate /// at minimum the requested length for the error message to be preserved in full. LIBSESSION_EXPORT -session_protocol_encoded_for_destination session_protocol_encode_for_1o1( +session_protocol_encoded_for_destination session_protocol_encode_dm_v1( const void* plaintext, size_t plaintext_len, const void* ed25519_privkey, @@ -639,55 +620,6 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( OPTIONAL char* error, size_t error_len) NON_NULL_ARG(1, 3, 6, 7); -/// API: session_protocol/session_protocol_encrypt_for_destination -/// -/// Given an unencrypted plaintext representation of the content (i.e.: protobuf encoded stream of -/// `Content`), encrypt and/or wrap the plaintext in the necessary structures for transmission on -/// the Session Protocol. -/// -/// See: session_protocol/encrypt_for_destination for more information -/// -/// The encoded result must be freed with `session_protocol_encrypt_for_destination_free` when -/// the caller is done with the result. -/// -/// Inputs: -/// - `plaintext` -- the protobuf serialised payload containing the protobuf encoded stream, -/// `Content`. It must not be already be encrypted. -/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be -/// passed as a 32-byte seed. Used to encrypt the plaintext. -/// - `dest` -- the extra metadata indicating the destination of the message and the necessary data -/// to encrypt a message for that destination. -/// - `error` -- Pointer to the character buffer to be populated with the error message if the -/// returned `success` was false, untouched otherwise. If this is set to `NULL`, then on failure, -/// the returned `error_len_incl_null_terminator` is the number of bytes required by the user to -/// receive the error. The message may be truncated if the buffer is too small, but it's always -/// guaranteed that `error` is null-terminated on failure when a buffer is passed in even if the -/// error must be truncated to fit in the buffer. -/// - `error_len` -- The capacity of the character buffer passed by the user. This should be 0 if -/// `error` is NULL. This function will fill the buffer up to `error_len - 1` characters with the -/// last character reserved for the null-terminator. -/// -/// Outputs: -/// - `success` -- True if encoding was successful, if the underlying implementation threw -/// an exception then this is caught internally and success is set to false. All remaining fields -/// are to be ignored in the result on failure. -/// - `ciphertext` -- Encryption result for the plaintext. The retured payload is suitable for -/// sending on the wire (i.e: it has been protobuf encoded/wrapped if necessary). -/// - `error_len_incl_null_terminator` The length of the error message if `success` was false. If -/// the user passes in an non-`NULL` error buffer this is amount of characters written to the -/// error buffer. If the user passes in a `NULL` error buffer, this is the amount of characters -/// required to write the error. Both counts include the null-terminator. The user must allocate -/// at minimum the requested length for the error message to be preserved in full. -LIBSESSION_EXPORT -session_protocol_encoded_for_destination session_protocol_encode_for_destination( - const void* plaintext, - size_t plaintext_len, - OPTIONAL const void* ed25519_privkey, - size_t ed25519_privkey_len, - const session_protocol_destination* dest, - OPTIONAL char* error, - size_t error_len) NON_NULL_ARG(1, 5); - /// API: session_protocol/session_protocol_encrypt_for_destination_free /// /// Free the encryption result for a destination produced by diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 61bb8bf47..b967bbcbe 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -218,43 +218,6 @@ struct ProFeaturesForMsg { size_t codepoint_count; }; -enum class DestinationType { - SyncOr1o1 = SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1, - /// Both legacy and non-legacy groups are to be identified as `Group`. A non-legacy - /// group is detected by the (0x03) prefix byte on the given `dest_group_pubkey` specified in - /// Destination. - Group = SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, - CommunityInbox = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, - Community = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, -}; - -struct Destination { - DestinationType type; - - // Optional rotating Session Pro Ed25519 private key to sign the message with on behalf of the - // caller. The Session Pro signature must _not_ be set in the plaintext content passed into the - // encoding function. - std::span pro_rotating_ed25519_privkey; - - // The timestamp to assign to the message envelope - std::chrono::milliseconds sent_timestamp_ms; - - // When type => (CommunityInbox || SyncMessage || Contact): set to the recipient's Session - // public key - uc33 recipient_pubkey; - - // When type => CommunityInbox: set this pubkey to the server's key - uc32 community_inbox_server_pubkey; - - // When type => Group: set to the group public keys for a 0x03 prefix (e.g. groups v2) - // `group_pubkey` to encrypt the message for. - uc33 group_ed25519_pubkey; - - // When type => Group: Set the encryption key of the group for groups v2 messages. Typically - // the latest key for the group, e.g: `Keys::group_enc_key` or `groups_keys_group_enc_key` - cleared_uc32 group_enc_key; -}; - struct Envelope { SESSION_PROTOCOL_ENVELOPE_FLAGS flags; std::chrono::milliseconds timestamp; @@ -395,48 +358,41 @@ ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size); /// for the padding-terminating byte. std::vector pad_message(std::span payload); -/// API: session_protocol/encode_for_1o1 -/// -/// Encode a plaintext message for a one-on-one (1o1) conversation or sync message in the Session -/// Protocol. This function wraps the plaintext in the necessary structures and encrypts it for -/// transmission to a single recipient. +/// API: session_protocol/encode_dm_v1 /// -/// This is a high-level convenience function that internally calls encode_for_destination with -/// the appropriate Destination configuration for a 1o1 or sync message. +/// Encode a plaintext "v1" message for a one-on-one conversation message (either text message, or +/// conversation metadata) in the Session Protocol. This function wraps the plaintext in the +/// necessary structures and encrypts it for transmission to a single recipient. /// /// This function throws if any input argument is invalid (e.g., incorrect key sizes). /// /// Inputs: -/// - plaintext -- The protobuf serialized payload containing the Content to be encrypted. Must -/// not be already encrypted and must not be padded. +/// - plaintext -- The protobuf serialized Content plaintext, unpadded payload of the message. /// - ed25519_privkey -- The sender's Ed25519 private key; accepts a 32-byte seed or 64-byte -/// libsodium key. Used to encrypt the plaintext. -/// - sent_timestamp -- The timestamp to assign to the message envelope, in milliseconds. This -/// should match the protobuf encoded Content's `sigtimestamp` in the given `plaintext`. -/// - recipient_pubkey -- The recipient's Session public key (33 bytes). -/// - pro_rotating_ed25519_privkey -- Optional libsodium-style secret key (64 bytes) that is the -/// secret component of the user's Session Pro Proof `rotating_pubkey`. This key is authorised to -/// entitle the message with Pro features by signing it. Can also be passed as a 32-byte seed. -/// Pass in the empty span to opt-out of Pro feature entitlement. +/// libsodium "secret". Used to identify the sender and sign the payload. +/// - sent_timestamp -- The timestamp to assign to the message envelope, in unix epoch milliseconds. +/// This must match the protobuf encoded Content's `sigTimestamp` in the given `plaintext`. +/// - recipient_pubkey -- The recipient's Session ID (33 bytes: 0x05 prefix + X25519 pubkey). +/// - pro_rotating_ed25519_privkey -- Optional libsodium-style secret key (64 bytes) or seed (32 +/// bytes) of the user's Session Pro Proof `rotating_pubkey`. This key is authorised to entitle +/// the message with Pro features by signing it. Can also be passed as a 32-byte seed. Omit +/// (or pass std::nullopt) to opt-out of Pro feature entitlement. /// /// Outputs: -/// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire -/// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_1o1( +/// - Encrypted, encoded payload, with all required protobuf encoding and wrapping. +std::vector encode_dm_v1( std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const uc33& recipient_pubkey, - std::optional> pro_rotating_ed25519_privkey); + std::span recipient_pubkey, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey = std::nullopt); /// API: session_protocol/encode_for_community_inbox /// -/// Encode a plaintext message for a community inbox in the Session Protocol. This function wraps -/// the plaintext in the necessary structures and encrypts it for transmission to a community inbox -/// server. -/// -/// This is a high-level convenience function that internally calls encode_for_destination with -/// the appropriate Destination configuration for a community inbox message. +/// Encode a plaintext message for a community-handled direct message in the Session Protocol. Such +/// DMs are used to initiate contact with blinded users without needing to expose their Session ID +/// unless they accept the contact. This function wraps the plaintext in the necessary structures +/// and encrypts it for transmission to a community inbox server. /// /// This function throws if any input argument is invalid (e.g., incorrect key sizes). /// @@ -460,16 +416,16 @@ std::vector encode_for_community_inbox( std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const uc33& recipient_pubkey, - const uc32& community_pubkey, - std::optional> pro_rotating_ed25519_privkey); + std::span recipient_pubkey, + std::span community_pubkey, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); -/// API: session_protocol/encode_for_community +/// API: session_protocol/encode_community_message /// -/// Encode a plaintext `Content` message for a community in the Session Protocol. This function -/// encodes Session Pro metadata including generating and embedding the Session Pro signature, when -/// given a Session Pro rotating Ed25519 key into the final payload suitable for transmission on the -/// wire. +/// Encode a plaintext `Content` message for sending to a community in the Session Protocol. This +/// function encodes Session Pro metadata including generating and embedding the Session Pro +/// signature, when given a Session Pro rotating Ed25519 key into the final payload suitable for +/// transmission on the wire. /// /// This function throws if any input argument is invalid (e.g., incorrect key sizes). It also /// throws if the pro signature is already set in the plaintext `Content` or the `plaintext` cannot @@ -488,17 +444,14 @@ std::vector encode_for_community_inbox( /// (i.e: it has been protobuf encoded/wrapped if necessary). std::vector encode_for_community( std::span plaintext, - std::optional> pro_rotating_ed25519_privkey); + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_group /// /// Encode a plaintext message for a group in the Session Protocol. This function wraps the /// plaintext in the necessary structures and encrypts it for transmission to a group, using the -/// group's encryption key. Only v2 groups, (0x03) prefixed keys are supported. Passing a legacy -/// group (0x05) prefixed key will cause the function to throw. -/// -/// This is a high-level convenience function that internally calls encode_for_destination with -/// the appropriate Destination configuration for a group message. +/// group's encryption key. Only v2 groups (with 0x03 prefixed keys) are supported. Passing a legacy +/// group (0x05 prefix) will cause the function to throw. /// /// This function throws if any input argument is invalid (e.g., incorrect key sizes). /// @@ -523,41 +476,9 @@ std::vector encode_for_group( std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const uc33& group_ed25519_pubkey, - const cleared_uc32& group_enc_key, - std::optional> pro_rotating_ed25519_privkey); - -/// API: session_protocol/encode_for_destination -/// -/// Given an unencrypted plaintext representation of the content (i.e.: protobuf encoded stream of -/// `Content`), encrypt and/or wrap the plaintext in the necessary structures for transmission on -/// the Session Protocol. -/// -/// Calling this function requires filling out the options in the `Destination` struct with the -/// appropriate values for the desired destination. Check the annotation on `Destination` for more -/// information on how to fill this struct. Alternatively, there are higher level functions, encrypt -/// for 1o1, group and community functions which thunk into this low-level function for convenience. -/// -/// This function throws if the API is misused (i.e.: A field was not set, but was required to be -/// set for the given destination and namespace. For example the group keys not being set -/// when sending to a group prefixed [0x3] key in a group) -/// but otherwise returns a struct with values. -/// -/// Inputs: -/// - `plaintext` -- the protobuf serialised payload containing the protobuf encoded stream, -/// `Content`. It must not be already be encrypted and must not be padded. -/// - `ed25519_privkey` -- the sender's Ed25519 private key; accepts a 32-byte seed or 64-byte -/// libsodium key. Null for community (unencrypted) destinations. -/// - `dest` -- the extra metadata indicating the destination of the message and the necessary data -/// to encrypt a message for that destination. -/// -/// Outputs: -/// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire -/// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_destination( - std::span plaintext, - const Ed25519PrivKeySpan* ed25519_privkey, - const Destination& dest); + std::span group_ed25519_pubkey, + std::span group_enc_key, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/decode_envelope /// diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index e2921a448..31d6b4b31 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1320,7 +1320,7 @@ LIBSESSION_C_API session_encrypt_group_message session_encrypt_for_group( session_encrypt_group_message result = {}; try { std::vector result_cpp = encrypt_for_group( - Ed25519PrivKeySpan::from(user_ed25519_privkey, user_ed25519_privkey_len), + {user_ed25519_privkey, user_ed25519_privkey_len}, cspan<32>(group_ed25519_pubkey), cspan(group_enc_key, group_enc_key_len), cspan(plaintext, plaintext_len), diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 29e72f8a7..5c12fce76 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -270,77 +270,6 @@ ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size) { return result; } -std::vector encode_for_1o1( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const uc33& recipient_pubkey, - std::optional> pro_rotating_ed25519_privkey) { - Destination dest = {}; - dest.type = DestinationType::SyncOr1o1; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey - ? *pro_rotating_ed25519_privkey - : std::span{}; - dest.sent_timestamp_ms = sent_timestamp; - dest.recipient_pubkey = recipient_pubkey; - return encode_for_destination(plaintext, &ed25519_privkey, dest); -} - -std::vector encode_for_community_inbox( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const uc33& recipient_pubkey, - const uc32& community_pubkey, - std::optional> pro_rotating_ed25519_privkey) { - Destination dest = {}; - dest.type = DestinationType::CommunityInbox; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey - ? *pro_rotating_ed25519_privkey - : std::span{}; - dest.sent_timestamp_ms = sent_timestamp; - dest.recipient_pubkey = recipient_pubkey; - dest.community_inbox_server_pubkey = community_pubkey; - return encode_for_destination(plaintext, &ed25519_privkey, dest); -} - -std::vector encode_for_community( - std::span plaintext, - std::optional> pro_rotating_ed25519_privkey) { - Destination dest = {}; - dest.type = DestinationType::Community; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey - ? *pro_rotating_ed25519_privkey - : std::span{}; - return encode_for_destination(plaintext, nullptr, dest); -} - -std::vector encode_for_group( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const uc33& group_ed25519_pubkey, - const cleared_uc32& group_enc_key, - std::optional> pro_rotating_ed25519_privkey) { - Destination dest = {}; - dest.type = DestinationType::Group; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey - ? *pro_rotating_ed25519_privkey - : std::span{}; - dest.sent_timestamp_ms = sent_timestamp; - dest.group_ed25519_pubkey = group_ed25519_pubkey; - dest.group_enc_key = group_enc_key; - return encode_for_destination(plaintext, &ed25519_privkey, dest); -} - -// Interop between the C and CPP API. The C api will request malloc which writes to `ciphertext_c`. -// This pointer is taken verbatim and avoids requiring a copy from the CPP vector. The CPP api will -// steal the contents from `ciphertext_cpp`. -struct EncryptedForDestinationInternal { - std::vector ciphertext_cpp; - span_u8 ciphertext_c; -}; - constexpr char PADDING_TERMINATING_BYTE = 0x80; std::vector pad_message(std::span payload) { @@ -383,260 +312,163 @@ static std::span unpad_message(std::span plaintext, - const Ed25519PrivKeySpan* ed25519_privkey, - DestinationType dest_type, - std::span dest_pro_rotating_ed25519_privkey, - std::span - dest_recipient_pubkey, - std::chrono::milliseconds dest_sent_timestamp_ms, - std::span - dest_community_inbox_server_pubkey, - std::span - dest_group_ed25519_pubkey, - std::span dest_group_enc_key, - UseMalloc use_malloc) { - assert(dest_group_enc_key.size() == 32 || dest_group_enc_key.size() == 64); - - bool is_group = dest_type == DestinationType::Group; - bool is_1o1 = dest_type == DestinationType::SyncOr1o1; - bool is_community_inbox = dest_type == DestinationType::CommunityInbox; - bool is_community = dest_type == DestinationType::Community; - assert(is_community || ed25519_privkey); - - // Ensure the Session Pro rotating key is a 64 byte key if given - cleared_uc64 pro_ed_sk_from_seed; - if (dest_pro_rotating_ed25519_privkey.size()) { - if (dest_pro_rotating_ed25519_privkey.size() == 32) { - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair( - ignore_pk.data(), - pro_ed_sk_from_seed.data(), - dest_pro_rotating_ed25519_privkey.data()); - dest_pro_rotating_ed25519_privkey = to_span(pro_ed_sk_from_seed); - } else if (dest_pro_rotating_ed25519_privkey.size() == 64) { - dest_pro_rotating_ed25519_privkey = to_span(dest_pro_rotating_ed25519_privkey); - } else { - throw std::runtime_error{fmt::format( - "Invalid dest_pro_rotating_ed25519_privkey: expected 32 or 64 bytes, received " - "{}", - dest_pro_rotating_ed25519_privkey.size())}; - } +// Attaches a Session Pro signature to an envelope. If no pro key is provided, a dummy +// (unverifiable) signature from a throwaway key is used so that pro and non-pro messages are +// indistinguishable on the wire. +static void attach_pro_sig_to_envelope( + SessionProtos::Envelope& envelope, + std::span content, + const OptionalEd25519PrivKeySpan& pro_key) { + std::string* pro_sig = envelope.mutable_prosig(); + pro_sig->resize(crypto_sign_ed25519_BYTES); + if (!pro_key) { + uc32 ignore_pk; + cleared_uc64 dummy_ed_sk; + crypto_sign_ed25519_keypair(ignore_pk.data(), dummy_ed_sk.data()); + crypto_sign_ed25519_detached( + reinterpret_cast(pro_sig->data()), + nullptr, + content.data(), + content.size(), + dummy_ed_sk.data()); + } else { + crypto_sign_ed25519_detached( + reinterpret_cast(pro_sig->data()), + nullptr, + content.data(), + content.size(), + pro_key->data()); } +} - std::span content = plaintext; - - EncryptedForDestinationInternal result = {}; - switch (dest_type) { - case DestinationType::Group: /*FALLTHRU*/ - case DestinationType::SyncOr1o1: { - if (is_group && dest_group_ed25519_pubkey[0] != - static_cast(SessionIDPrefix::group)) { - // Legacy groups which have a 05 prefixed key - throw std::runtime_error{ - "Unsupported configuration, encrypting for a legacy group (0x05 prefix) is " - "no longer supported"}; - } - - // For Sync or 1o1 mesasges, we need to pad the contents to 160 bytes, see: - // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 - std::vector tmp_content_buffer; - if (is_1o1) { // Encrypt the padded output - std::vector padded_payload = pad_message(content); - tmp_content_buffer = encrypt_for_recipient( - *ed25519_privkey, dest_recipient_pubkey, padded_payload); - content = tmp_content_buffer; - } +// TODO: We don't need to actually pad the community message since that's unencrypted, +// there's no need to make the message sizes uniform but we need it for backwards +// compat. We can remove this eventually, first step is to unify the clients. +std::vector encode_for_community( + std::span plaintext, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { + if (!pro_rotating_ed25519_privkey) + return pad_message(plaintext); + + // TODO: Sub-optimal, but we parse the content again to make sure it's valid. Sign + // the blob then, fill in the signature in-place as part of the transitioning of + // open groups messages to envelopes. As part of that, libsession is going to take + // responsibility of constructing community messages so that eventually all + // platforms switch over to envelopes and we can change the implementation across + // all platforms in one swoop and remove this. + // + // Parse the content blob + SessionProtos::Content content_w_sig; + if (!content_w_sig.ParseFromArray(plaintext.data(), plaintext.size())) + throw std::runtime_error{"Parsing community message failed"}; - // Create envelope - // Set sourcedevice to 1 as per: - // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Utilities/MessageWrapper.swift#L57 - SessionProtos::Envelope envelope = {}; - envelope.set_type( - is_1o1 ? SessionProtos::Envelope_Type_SESSION_MESSAGE - : SessionProtos::Envelope_Type_CLOSED_GROUP_MESSAGE); - envelope.set_sourcedevice(1); - envelope.set_timestamp(dest_sent_timestamp_ms.count()); - envelope.set_content(content.data(), content.size()); - - // Generate the session pro signature. If there's no pro ed25519 key specified, we still - // fill out the pro signature with a valid but unverifiable signature by creating a - // throw-away key. This makes pro and non-pro messages indistinguishable on the wire. - { - std::string* pro_sig = envelope.mutable_prosig(); - pro_sig->resize(crypto_sign_ed25519_BYTES); - - if (dest_pro_rotating_ed25519_privkey.empty()) { - uc32 ignore_pk; - cleared_uc64 dummy_pro_ed_sk; - crypto_sign_ed25519_keypair(ignore_pk.data(), dummy_pro_ed_sk.data()); - crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), - nullptr, - content.data(), - content.size(), - dummy_pro_ed_sk.data()); - } else { - crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), - nullptr, - content.data(), - content.size(), - dest_pro_rotating_ed25519_privkey.data()); - } - } + if (content_w_sig.has_prosigforcommunitymessageonly()) + throw std::runtime_error{ + "Pro signature for community message must not be set. Libsession's " + "responsible for generating the signature and setting it"}; + + // We need to sign the padded content, so we pad the `Content` then sign it + std::vector padded = pad_message(plaintext); + uc64 pro_sig; + [[maybe_unused]] bool ok = crypto_sign_ed25519_detached( + pro_sig.data(), + nullptr, + padded.data(), + padded.size(), + pro_rotating_ed25519_privkey->data()) == 0; + assert(ok); + + // Now assign the community specific pro signature field, reserialize it and we have + // to, yes, pad it again. This is all temporary wasted work whilst transitioning + // open groups. + content_w_sig.set_prosigforcommunitymessageonly(pro_sig.data(), pro_sig.size()); + std::vector reserialized(content_w_sig.ByteSizeLong()); + ok = content_w_sig.SerializeToArray(reserialized.data(), reserialized.size()); + assert(ok); + return pad_message(reserialized); +} - if (is_group) { - std::string bytes = envelope.SerializeAsString(); - std::vector ciphertext = encrypt_for_group( - *ed25519_privkey, - dest_group_ed25519_pubkey.subspan<1>(), - dest_group_enc_key, - to_span(bytes), - /*compress*/ true, - /*padding*/ 256); - - if (use_malloc == UseMalloc::Yes) { - result.ciphertext_c = - session::span_u8_copy_or_throw(ciphertext.data(), ciphertext.size()); - } else { - result.ciphertext_cpp = std::move(ciphertext); - } - } else { - // 1o1, Wrap in websocket message - WebSocketProtos::WebSocketMessage msg = {}; - msg.set_type(WebSocketProtos::WebSocketMessage_Type::WebSocketMessage_Type_REQUEST); - - // Make request - WebSocketProtos::WebSocketRequestMessage* req_msg = msg.mutable_request(); - req_msg->set_verb(""); // Required but unused on iOS - req_msg->set_path(""); // Required but unused on iOS - req_msg->set_requestid(0); // Required but unused on iOS - req_msg->set_body(envelope.SerializeAsString()); - - // Write message as ciphertext - [[maybe_unused]] bool serialized = false; - if (use_malloc == UseMalloc::Yes) { - result.ciphertext_c = span_u8_alloc_or_throw(msg.ByteSizeLong()); - serialized = msg.SerializeToArray( - result.ciphertext_c.data, result.ciphertext_c.size); - } else { - result.ciphertext_cpp.resize(msg.ByteSizeLong()); - serialized = msg.SerializeToArray( - result.ciphertext_cpp.data(), result.ciphertext_cpp.size()); - } - assert(serialized); - } - } break; - - case DestinationType::Community: /*FALLTHRU*/ - case DestinationType::CommunityInbox: { - // Setup the pro signature for the community message - std::vector tmp_content_buffer; - - // Sign the message with the Session Pro key if given and then pad the message (both - // community message types require it) - // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Sending%20%26%20Receiving/MessageSender.swift#L398 - if (dest_pro_rotating_ed25519_privkey.size()) { - // Key should be verified by the time we hit this branch - assert(dest_pro_rotating_ed25519_privkey.size() == - crypto_sign_ed25519_SECRETKEYBYTES); - - // TODO: Sub-optimal, but we parse the content again to make sure it's valid. Sign - // the blob then, fill in the signature in-place as part of the transitioning of - // open groups messages to envelopes. As part of that, libsession is going to take - // responsibility of constructing community messages so that eventually all - // platforms switch over to envelopes and we can change the implementation across - // all platforms in one swoop and remove this. - // - // Parse the content blob - SessionProtos::Content content_w_sig = {}; - if (!content_w_sig.ParseFromArray(content.data(), content.size())) - throw std::runtime_error{"Parsing community message failed"}; - - if (content_w_sig.has_prosigforcommunitymessageonly()) - throw std::runtime_error{ - "Pro signature for community message must not be set. Libsession's " - "responsible for generating the signature and setting it"}; - - // We need to sign the padded content, so we pad the `Content` then sign it - tmp_content_buffer = pad_message(content); - uc64 pro_sig; - bool was_signed = crypto_sign_ed25519_detached( - pro_sig.data(), - nullptr, - tmp_content_buffer.data(), - tmp_content_buffer.size(), - dest_pro_rotating_ed25519_privkey.data()) == 0; - assert(was_signed); - - // Now assign the community specific pro signature field, reserialize it and we have - // to, yes, pad it again. This is all temporary wasted work whilst transitioning - // open groups. - content_w_sig.set_prosigforcommunitymessageonly(pro_sig.data(), pro_sig.size()); - tmp_content_buffer.resize(content_w_sig.ByteSizeLong()); - bool serialized = content_w_sig.SerializeToArray( - tmp_content_buffer.data(), tmp_content_buffer.size()); - assert(serialized); - - tmp_content_buffer = pad_message(tmp_content_buffer); - content = tmp_content_buffer; - } else { - tmp_content_buffer = pad_message(to_span(content)); - content = tmp_content_buffer; - } +std::vector encode_for_community_inbox( + std::span plaintext, + const Ed25519PrivKeySpan& ed25519_privkey, + std::chrono::milliseconds sent_timestamp, + std::span recipient_pubkey, + std::span community_pubkey, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { + std::vector content = + encode_for_community(plaintext, pro_rotating_ed25519_privkey); + return encrypt_for_blinded_recipient( + ed25519_privkey, community_pubkey, recipient_pubkey, content); +} - // TODO: We don't need to actually pad the community message since that's unencrypted, - // there's no need to make the message sizes uniform but we need it for backwards - // compat. We can remove this eventually, first step is to unify the clients. - - if (is_community_inbox) { - std::vector ciphertext = encrypt_for_blinded_recipient( - *ed25519_privkey, - dest_community_inbox_server_pubkey, - dest_recipient_pubkey, // 33-byte blinded pubkey - content); - - if (use_malloc == UseMalloc::Yes) { - result.ciphertext_c = - span_u8_copy_or_throw(ciphertext.data(), ciphertext.size()); - } else { - result.ciphertext_cpp = std::move(ciphertext); - } - } else { - if (use_malloc == UseMalloc::Yes) { - result.ciphertext_c = span_u8_copy_or_throw(content.data(), content.size()); - } else { - result.ciphertext_cpp = - std::vector(content.begin(), content.end()); - } - } - } break; - } +std::vector encode_dm_v1( + std::span plaintext, + const Ed25519PrivKeySpan& ed25519_privkey, + std::chrono::milliseconds sent_timestamp, + std::span recipient_pubkey, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { + // For 1o1 messages, encrypt the padded payload for the recipient. See: + // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 + std::vector encrypted = + encrypt_for_recipient(ed25519_privkey, recipient_pubkey, pad_message(plaintext)); + + // Create envelope. + // Set sourcedevice to 1 as per: + // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Utilities/MessageWrapper.swift#L57 + SessionProtos::Envelope envelope; + envelope.set_type(SessionProtos::Envelope_Type_SESSION_MESSAGE); + envelope.set_sourcedevice(1); + envelope.set_timestamp(sent_timestamp.count()); + envelope.set_content(encrypted.data(), encrypted.size()); + attach_pro_sig_to_envelope(envelope, encrypted, pro_rotating_ed25519_privkey); + + // Wrap in websocket message + WebSocketProtos::WebSocketMessage msg; + msg.set_type(WebSocketProtos::WebSocketMessage_Type::WebSocketMessage_Type_REQUEST); + WebSocketProtos::WebSocketRequestMessage* req_msg = msg.mutable_request(); + req_msg->set_verb(""); // Required but unused on iOS + req_msg->set_path(""); // Required but unused on iOS + req_msg->set_requestid(0); // Required but unused on iOS + req_msg->set_body(envelope.SerializeAsString()); + + std::vector result(msg.ByteSizeLong()); + [[maybe_unused]] bool ok = msg.SerializeToArray(result.data(), result.size()); + assert(ok); return result; } -std::vector encode_for_destination( +std::vector encode_for_group( std::span plaintext, - const Ed25519PrivKeySpan* ed25519_privkey, - const Destination& dest) { - - EncryptedForDestinationInternal result_internal = encode_for_destination_internal( - /*plaintext=*/plaintext, - /*ed25519_privkey=*/ed25519_privkey, - /*dest_type=*/dest.type, - /*dest_pro_rotating_ed25519_privkey=*/dest.pro_rotating_ed25519_privkey, - /*dest_recipient_pubkey=*/dest.recipient_pubkey, - /*dest_sent_timestamp_ms=*/dest.sent_timestamp_ms, - /*dest_community_inbox_server_pubkey=*/dest.community_inbox_server_pubkey, - /*dest_group_ed25519_pubkey=*/dest.group_ed25519_pubkey, - /*dest_group_enc_key=*/dest.group_enc_key, - /*use_malloc=*/UseMalloc::No); - - std::vector result = std::move(result_internal.ciphertext_cpp); - return result; + const Ed25519PrivKeySpan& ed25519_privkey, + std::chrono::milliseconds sent_timestamp, + std::span group_ed25519_pubkey, + std::span group_enc_key, + const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { + if (group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { + // Legacy groups which have a 05 prefixed key + throw std::runtime_error{ + "Unsupported configuration, encrypting for a legacy group (0x05 prefix) is " + "no longer supported"}; + } + + // Create envelope. + // Set sourcedevice to 1 as per: + // https://github.com/session-foundation/session-ios/blob/82deef869d0f7389b799295817f42ad14f8a1316/SessionMessagingKit/Utilities/MessageWrapper.swift#L57 + SessionProtos::Envelope envelope; + envelope.set_type(SessionProtos::Envelope_Type_CLOSED_GROUP_MESSAGE); + envelope.set_sourcedevice(1); + envelope.set_timestamp(sent_timestamp.count()); + envelope.set_content(plaintext.data(), plaintext.size()); + attach_pro_sig_to_envelope(envelope, plaintext, pro_rotating_ed25519_privkey); + + std::string bytes = envelope.SerializeAsString(); + return encrypt_for_group( + ed25519_privkey, + group_ed25519_pubkey.subspan<1>(), + group_enc_key, + to_span(bytes), + /*compress*/ true, + /*padding*/ 256); } DecodedEnvelope decode_envelope( @@ -1235,8 +1067,32 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( return result; } +// Shared try/catch wrapper for all C encode functions. +template +static session_protocol_encoded_for_destination c_encode_impl( + char* error, size_t error_len, Fn&& fn) { + session_protocol_encoded_for_destination result = {}; + try { + auto ciphertext = fn(); + result = { + .success = true, + .ciphertext = span_u8_copy_or_throw(ciphertext.data(), ciphertext.size()), + }; + } catch (const std::exception& e) { + std::string error_cpp = e.what(); + result.error_len_incl_null_terminator = snprintf_clamped( + error, + error_len, + "%.*s", + static_cast(error_cpp.size()), + error_cpp.data()) + + 1; + } + return result; +} + LIBSESSION_C_API -session_protocol_encoded_for_destination session_protocol_encode_for_1o1( +session_protocol_encoded_for_destination session_protocol_encode_dm_v1( const void* plaintext, size_t plaintext_len, const void* ed25519_privkey, @@ -1247,23 +1103,15 @@ session_protocol_encoded_for_destination session_protocol_encode_for_1o1( size_t pro_rotating_ed25519_privkey_len, char* error, size_t error_len) { - - session_protocol_destination dest = {}; - dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey; - dest.pro_rotating_ed25519_privkey_len = pro_rotating_ed25519_privkey_len; - dest.recipient_pubkey = *recipient_pubkey; - dest.sent_timestamp_ms = sent_timestamp_ms; - - session_protocol_encoded_for_destination result = session_protocol_encode_for_destination( - plaintext, - plaintext_len, - ed25519_privkey, - ed25519_privkey_len, - &dest, - error, - error_len); - return result; + return c_encode_impl(error, error_len, [&] { + return encode_dm_v1( + {static_cast(plaintext), plaintext_len}, + {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::chrono::milliseconds(sent_timestamp_ms), + recipient_pubkey->data, + {static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1279,24 +1127,16 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community_i size_t pro_rotating_ed25519_privkey_len, char* error, size_t error_len) { - - session_protocol_destination dest = {}; - dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey; - dest.pro_rotating_ed25519_privkey_len = pro_rotating_ed25519_privkey_len; - dest.sent_timestamp_ms = sent_timestamp_ms; - dest.recipient_pubkey = *recipient_pubkey; - dest.community_inbox_server_pubkey = *community_pubkey; - - session_protocol_encoded_for_destination result = session_protocol_encode_for_destination( - plaintext, - plaintext_len, - ed25519_privkey, - ed25519_privkey_len, - &dest, - error, - error_len); - return result; + return c_encode_impl(error, error_len, [&] { + return encode_for_community_inbox( + {static_cast(plaintext), plaintext_len}, + {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::chrono::milliseconds(sent_timestamp_ms), + recipient_pubkey->data, + community_pubkey->data, + {static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1307,15 +1147,12 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community( size_t pro_rotating_ed25519_privkey_len, char* error, size_t error_len) { - - session_protocol_destination dest = {}; - dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey; - dest.pro_rotating_ed25519_privkey_len = pro_rotating_ed25519_privkey_len; - - session_protocol_encoded_for_destination result = session_protocol_encode_for_destination( - plaintext, plaintext_len, nullptr, 0, &dest, error, error_len); - return result; + return c_encode_impl(error, error_len, [&] { + return encode_for_community( + {static_cast(plaintext), plaintext_len}, + {static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1331,78 +1168,16 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( size_t pro_rotating_ed25519_privkey_len, char* error, size_t error_len) { - - session_protocol_destination dest = {}; - dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_GROUP; - dest.pro_rotating_ed25519_privkey = pro_rotating_ed25519_privkey; - dest.pro_rotating_ed25519_privkey_len = pro_rotating_ed25519_privkey_len; - dest.group_ed25519_pubkey = *group_ed25519_pubkey; - dest.group_enc_key = *group_enc_key; - dest.sent_timestamp_ms = sent_timestamp_ms; - - session_protocol_encoded_for_destination result = session_protocol_encode_for_destination( - plaintext, - plaintext_len, - ed25519_privkey, - ed25519_privkey_len, - &dest, - error, - error_len); - return result; -} - -LIBSESSION_C_API session_protocol_encoded_for_destination session_protocol_encode_for_destination( - const void* plaintext, - size_t plaintext_len, - const void* ed25519_privkey, - size_t ed25519_privkey_len, - const session_protocol_destination* dest, - char* error, - size_t error_len) { - - session_protocol_encoded_for_destination result = {}; - - try { - std::span dest_pro_rotating_ed25519_privkey = std::span( - reinterpret_cast(dest->pro_rotating_ed25519_privkey), - reinterpret_cast(dest->pro_rotating_ed25519_privkey) + - dest->pro_rotating_ed25519_privkey_len); - - std::optional privkey; - if (ed25519_privkey_len) - privkey.emplace( - static_cast(ed25519_privkey), ed25519_privkey_len); - - EncryptedForDestinationInternal result_internal = encode_for_destination_internal( - /*plaintext=*/{static_cast(plaintext), plaintext_len}, - /*ed25519_privkey=*/privkey ? &*privkey : nullptr, - /*dest_type=*/static_cast(dest->type), - /*dest_pro_rotating_ed25519_privkey=*/dest_pro_rotating_ed25519_privkey, - /*dest_recipient_pubkey=*/dest->recipient_pubkey.data, - /*dest_sent_timestamp_ms=*/ - std::chrono::milliseconds(dest->sent_timestamp_ms), - /*dest_community_inbox_server_pubkey=*/ - dest->community_inbox_server_pubkey.data, - /*dest_group_ed25519_pubkey=*/dest->group_ed25519_pubkey.data, - /*dest_group_enc_key=*/dest->group_enc_key.data, - /*use_malloc=*/UseMalloc::Yes); - - result = { - .success = true, - .ciphertext = result_internal.ciphertext_c, - }; - } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "%.*s", - static_cast(error_cpp.size()), - error_cpp.data()) + - 1; - } - - return result; + return c_encode_impl(error, error_len, [&] { + return encode_for_group( + {static_cast(plaintext), plaintext_len}, + {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::chrono::milliseconds(sent_timestamp_ms), + group_ed25519_pubkey->data, + group_enc_key->data, + {static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API void session_protocol_encode_for_destination_free( diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index ba9dd52eb..a898d4fd5 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -77,7 +77,7 @@ TEST_CASE("_handle_direct_messages: v1 receive", "[core][dm]") { // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. constexpr auto plaintext = "7801"_hex_u; - auto encoded = encode_for_1o1(plaintext, sender.ed_sk, 1234ms, recip_session_id, std::nullopt); + auto encoded = encode_dm_v1(plaintext, sender.ed_sk, 1234ms, recip_session_id, std::nullopt); OwnedMessage om{std::span{encoded}, "hash_v1", from_epoch_ms(1234), from_epoch_ms(9999)}; recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); @@ -257,8 +257,8 @@ TEST_CASE( // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. constexpr auto plaintext = "7801"_hex_u; - auto e1 = encode_for_1o1(plaintext, sender.ed_sk, 1000ms, recip_session_id, std::nullopt); - auto e2 = encode_for_1o1(plaintext, sender.ed_sk, 2000ms, recip_session_id, std::nullopt); + auto e1 = encode_dm_v1(plaintext, sender.ed_sk, 1000ms, recip_session_id, std::nullopt); + auto e2 = encode_dm_v1(plaintext, sender.ed_sk, 2000ms, recip_session_id, std::nullopt); OwnedMessage om1{std::span{e1}, "h1"}; OwnedMessage om2{std::span{e2}, "h2"}; diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index 1175d4c40..98b715533 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -126,8 +126,7 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { constexpr auto ed_invalid = "010203040506070809"_hex_u; auto sig1 = session::ed25519::sign(ed_seed, to_span("hello")); - CHECK_THROWS(session::ed25519::sign( - Ed25519PrivKeySpan::from(ed_invalid.data(), ed_invalid.size()), to_span("hello"))); + CHECK_THROWS(session::ed25519::sign({ed_invalid.data(), ed_invalid.size()}, to_span("hello"))); auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 856d1226f..8dabe228e 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -192,7 +192,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Withhold the pro signature char error[256]; session_protocol_encoded_for_destination encrypt_without_pro_sig = - session_protocol_encode_for_1o1( + session_protocol_encode_dm_v1( data_body.data(), data_body.size(), keys.ed_sk0.data(), @@ -208,7 +208,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Set the pro signature session_protocol_encoded_for_destination encrypt_with_pro_sig = - session_protocol_encode_for_1o1( + session_protocol_encode_dm_v1( data_body.data(), data_body.size(), keys.ed_sk0.data(), @@ -252,7 +252,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { { bytes33 recipient_pubkey = {}; std::memcpy(recipient_pubkey.data, keys.session_pk1.data(), keys.session_pk1.size()); - encrypt_result = session_protocol_encode_for_1o1( + encrypt_result = session_protocol_encode_dm_v1( plaintext.data(), plaintext.size(), keys.ed_sk0.data(), @@ -327,41 +327,31 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { protobuf_content.sig_over_plaintext_with_user_pro_key.data(), sizeof(base_pro_sig.data)); - session_protocol_destination base_dest = {}; - base_dest.sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); - base_dest.pro_rotating_ed25519_privkey = user_pro_ed_sk.data(); - base_dest.pro_rotating_ed25519_privkey_len = user_pro_ed_sk.size(); - - REQUIRE(sizeof(base_dest.recipient_pubkey.data) == keys.session_pk1.size()); - std::memcpy(base_dest.recipient_pubkey.data, keys.session_pk1.data(), keys.session_pk1.size()); + uint64_t base_sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); + bytes33 base_recipient_pubkey = {}; + REQUIRE(sizeof(base_recipient_pubkey.data) == keys.session_pk1.size()); + std::memcpy(base_recipient_pubkey.data, keys.session_pk1.data(), keys.session_pk1.size()); SECTION("Check non-encryptable messages produce only plaintext") { - auto dest_list = { - SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, - SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1}; - - for (auto dest_type : dest_list) { - if (dest_type == SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX) - INFO("Trying community inbox"); - else - INFO("Trying contacts to non-default namespace"); - - session_protocol_destination dest = base_dest; - dest.type = dest_type; - if (dest_type == SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX) { - auto [blind15_pk, blind15_sk] = session::blind15_key_pair( - keys.ed_sk1, keys.ed_pk1, /*blind factor*/ nullptr); - dest.recipient_pubkey.data[0] = 0x15; - std::memcpy(dest.recipient_pubkey.data + 1, blind15_pk.data(), blind15_pk.size()); - } + SECTION("Community inbox") { + auto [blind15_pk, blind15_sk] = + session::blind15_key_pair(keys.ed_sk1, keys.ed_pk1, /*blind factor*/ nullptr); + bytes33 blind15_recipient = {}; + blind15_recipient.data[0] = 0x15; + std::memcpy(blind15_recipient.data + 1, blind15_pk.data(), blind15_pk.size()); + bytes32 community_pubkey = {}; session_protocol_encoded_for_destination encrypt_result = - session_protocol_encode_for_destination( + session_protocol_encode_for_community_inbox( protobuf_content.plaintext.data(), protobuf_content.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), - &dest, + base_sent_timestamp_ms, + &blind15_recipient, + &community_pubkey, + user_pro_ed_sk.data(), + user_pro_ed_sk.size(), error, sizeof(error)); INFO("ERROR: " << error); @@ -369,17 +359,35 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { REQUIRE(encrypt_result.error_len_incl_null_terminator == 0); session_protocol_encode_for_destination_free(&encrypt_result); } + + SECTION("Contact in non-default namespace") { + session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_dm_v1( + protobuf_content.plaintext.data(), + protobuf_content.plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + base_sent_timestamp_ms, + &base_recipient_pubkey, + user_pro_ed_sk.data(), + user_pro_ed_sk.size(), + error, + sizeof(error)); + INFO("ERROR: " << error); + REQUIRE(encrypt_result.ciphertext.size > 0); + REQUIRE(encrypt_result.error_len_incl_null_terminator == 0); + session_protocol_encode_for_destination_free(&encrypt_result); + } } SECTION("Encrypt/decrypt for contact in default namespace with Pro") { // Encrypt content - session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_1o1( + session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_dm_v1( protobuf_content.plaintext.data(), protobuf_content.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), - base_dest.sent_timestamp_ms, - &base_dest.recipient_pubkey, + base_sent_timestamp_ms, + &base_recipient_pubkey, user_pro_ed_sk.data(), user_pro_ed_sk.size(), error, @@ -446,13 +454,13 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*proilfe_bitset*/ profile_bitset); // Encrypt content - session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_1o1( + session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_dm_v1( protobuf_content_with_pro_and_features.plaintext.data(), protobuf_content_with_pro_and_features.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), - base_dest.sent_timestamp_ms, - &base_dest.recipient_pubkey, + base_sent_timestamp_ms, + &base_recipient_pubkey, user_pro_ed_sk.data(), user_pro_ed_sk.size(), error, @@ -476,7 +484,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { INFO("ERROR: " << error); REQUIRE(decrypt_result.success); REQUIRE(decrypt_result.error_len_incl_null_terminator == 0); - REQUIRE(decrypt_result.envelope.timestamp_ms == base_dest.sent_timestamp_ms); + REQUIRE(decrypt_result.envelope.timestamp_ms == base_sent_timestamp_ms); session_protocol_encode_for_destination_free(&encrypt_result); // Verify pro @@ -503,19 +511,21 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { } SECTION("Encrypt/decrypt for legacy groups is rejected") { - session_protocol_destination dest = base_dest; - dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_GROUP; - assert(dest.recipient_pubkey.data[0] == 0x05); + CHECK(base_recipient_pubkey.data[0] == 0x05); + bytes32 group_enc_key = {}; - session_protocol_encoded_for_destination encrypt_result = - session_protocol_encode_for_destination( - protobuf_content.plaintext.data(), - protobuf_content.plaintext.size(), - keys.ed_sk0.data(), - keys.ed_sk0.size(), - &dest, - error, - sizeof(error)); + session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_group( + protobuf_content.plaintext.data(), + protobuf_content.plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + base_sent_timestamp_ms, + &base_recipient_pubkey, + &group_enc_key, + nullptr, + 0, + error, + sizeof(error)); REQUIRE(encrypt_result.error_len_incl_null_terminator > 0); REQUIRE(encrypt_result.error_len_incl_null_terminator <= sizeof(error)); REQUIRE(!encrypt_result.success); @@ -546,7 +556,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { protobuf_content.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), - base_dest.sent_timestamp_ms, + base_sent_timestamp_ms, &group_v2_session_pk, &group_v2_session_sk, user_pro_ed_sk.data(), @@ -591,13 +601,13 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encrypt/decrypt for sync messages with Pro") { // Encrypt - session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_1o1( + session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_dm_v1( protobuf_content.plaintext.data(), protobuf_content.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), - base_dest.sent_timestamp_ms, - &base_dest.recipient_pubkey, + base_sent_timestamp_ms, + &base_recipient_pubkey, user_pro_ed_sk.data(), user_pro_ed_sk.size(), error, @@ -664,13 +674,13 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*profile_bitset*/ {}); session_protocol_encoded_for_destination encrypt_bad_result = - session_protocol_encode_for_1o1( + session_protocol_encode_dm_v1( bad_protobuf_content.plaintext.data(), bad_protobuf_content.plaintext.size(), keys.ed_sk0.data(), keys.ed_sk0.size(), bad_timestamp_ms.count(), - &base_dest.recipient_pubkey, + &base_recipient_pubkey, user_pro_ed_sk.data(), user_pro_ed_sk.size(), error, From 2dfa9a206fde81f3dfe03d4db7e0028660b5adf8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 22:33:12 -0300 Subject: [PATCH 065/193] Use hash::blake2b in more places - Deprecate hash::hash -- we have several hashes in use and the more specific one makes more sense. - Convert various remaining places still using raw C API calls. - Add hash::blake2b (and so on) overloads that take a templated hash size, e.g. blake2b<64>(...) and return a hash into an std::array of that size rather than needing the caller to preconstruct such an array to pass it in. --- include/session/hash.hpp | 82 +++++++++++++++++++- include/session/multi_encrypt.hpp | 11 +-- src/attachments.cpp | 10 +-- src/blinding.cpp | 17 ++-- src/config/base.cpp | 18 +---- src/config/contacts.cpp | 1 - src/config/convo_info_volatile.cpp | 1 - src/config/encrypt.cpp | 21 ++--- src/config/groups/info.cpp | 1 - src/config/groups/keys.cpp | 22 ++---- src/config/local.cpp | 2 - src/config/pro.cpp | 1 - src/config/user_groups.cpp | 1 - src/config/user_profile.cpp | 1 - src/core/devices.cpp | 12 +-- src/core/link_sas.cpp | 3 +- src/hash.cpp | 24 ++++-- src/multi_encrypt.cpp | 28 +++---- src/network/routing/onion_request_router.cpp | 5 +- src/network/snode_pool.cpp | 5 +- src/pro_backend.cpp | 22 ++---- src/session_encrypt.cpp | 20 +---- src/session_protocol.cpp | 11 +-- tests/test_blinding.cpp | 38 +++------ tests/test_configdata.cpp | 13 +--- tests/test_encrypt.cpp | 1 - 26 files changed, 172 insertions(+), 199 deletions(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 680105409..8ff7961ec 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -26,6 +26,9 @@ namespace session::hash { /// - `msg` -- the message to generate a hash for. /// - `key` -- an optional key to be used when generating the hash. Can be omitted or an empty /// string for an unkeyed hash. Must be less than 64 bytes long. +/// +/// Deprecated: prefer hash::blake2b (unkeyed) or hash::blake2b_key (keyed) instead. +[[deprecated("Use hash::blake2b or hash::blake2b_key instead")]] void hash( std::span hash, std::span msg, @@ -44,6 +47,9 @@ void hash( /// /// Outputs: /// - a `size` byte hash. +/// +/// Deprecated: prefer hash::blake2b (unkeyed) or hash::blake2b_key (keyed) instead. +[[deprecated("Use hash::blake2b or hash::blake2b_key instead")]] std::vector hash( const size_t size, std::span msg, @@ -126,7 +132,8 @@ concept Blake2BOutputContainer = HashOutputContainer && detail::container_ext template concept Blake2BKey = std::ranges::contiguous_range && oxenc::basic_char> && - detail::container_extent_v != std::dynamic_extent && detail::container_extent_v <= 64; + (detail::container_extent_v == std::dynamic_extent || + detail::container_extent_v <= 64); /// Helper value to pass a null `key` to blake2b or blake2b_pers. inline constexpr std::span nullkey{}; @@ -135,19 +142,32 @@ inline constexpr std::span nullkey{}; /// /// This version of blake2b() takes a key as the second argument and computes a keyed hash. The key /// must be between 0 and 64 characters long. (A 0-length key is equivalent to no key). +/// +/// Two overloads are provided: +/// - write-to-output: `blake2b_key(out, key, args...)` writes the hash into `out` +/// - return-value: `blake2b_key(key, args...)` returns a `std::array` template requires(sizeof...(T) > 0) void blake2b_key(Out& out, const Key& key, const T&... args) { crypto_generichash_blake2b_state st; + // Dynamic-extent keys are silently truncated to 64 bytes (the blake2b key size limit); static- + // extent keys are guaranteed ≤ 64 at compile time by the Blake2BKey concept. crypto_generichash_blake2b_init( &st, reinterpret_cast(std::ranges::data(key)), - std::ranges::size(key), + std::min(std::ranges::size(key), 64), std::ranges::size(out)); update_all(st, args...); crypto_generichash_blake2b_final( &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); } +template + requires(sizeof...(T) > 0 && N >= 1 && N <= 64) +std::array blake2b_key(const Key& key, const T&... args) { + std::array result; + blake2b_key(result, key, args...); + return result; +} /// API: hash/blake2b /// @@ -164,11 +184,22 @@ void blake2b_key(Out& out, const Key& key, const T&... args) { /// /// It is permitted for overlap between the output and input containers; the output container is not /// written until all input containers have been consumed. +/// +/// Two overloads are provided: +/// - write-to-output: `blake2b(out, args...)` writes the hash into `out` +/// - return-value: `blake2b(args...)` returns a `std::array` template requires(sizeof...(T) > 0) void blake2b(Out& out, const T&... args) { return blake2b_key(out, nullkey, args...); } +template + requires(sizeof...(T) > 0 && N >= 1 && N <= 64) +std::array blake2b(const T&... args) { + std::array result; + blake2b(result, args...); + return result; +} /// API: hash/blake2b_key_pers /// @@ -176,6 +207,11 @@ void blake2b(Out& out, const T&... args) { /// and third arguments and computes a keyed hash with a personalisation string. The /// personalisation string must be exact 16 bytes, and is typically constructed with "..."_b2b_pers /// for compile-time validation. The key must be between 0 and 64 bytes long. +/// +/// Two overloads are provided: +/// - write-to-output: `blake2b_key_pers(out, key, pers, args...)` writes the hash into `out` +/// - return-value: `blake2b_key_pers(key, pers, args...)` returns a `std::array` template requires(sizeof...(T) > 0) void blake2b_key_pers( @@ -184,7 +220,7 @@ void blake2b_key_pers( crypto_generichash_blake2b_init_salt_personal( &st, reinterpret_cast(std::ranges::data(key)), - std::ranges::size(key), + std::min(std::ranges::size(key), 64), std::ranges::size(out), /*salt=*/nullptr, pers.data()); @@ -192,17 +228,37 @@ void blake2b_key_pers( crypto_generichash_blake2b_final( &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); } +template + requires(sizeof...(T) > 0 && N >= 1 && N <= 64) +std::array blake2b_key_pers( + const Key& key, std::span pers, const T&... args) { + std::array result; + blake2b_key_pers(result, key, pers, args...); + return result; +} /// API: hash/blake2b_pers /// /// This version of blake2b() takes a 16-byte personality string as the second argument and computes /// a unkeyed hash with a personalisation string. The personalization string must be exact 16 /// bytes, and is typically constructed with "..."_b2b_pers for compile-time validation. +/// +/// Two overloads are provided: +/// - write-to-output: `blake2b_pers(out, pers, args...)` writes the hash into `out` +/// - return-value: `blake2b_pers(pers, args...)` returns a `std::array` template requires(sizeof...(T) > 0) void blake2b_pers(Out& out, std::span pers, const T&... args) { return blake2b_key_pers(out, nullkey, pers, args...); } +template + requires(sizeof...(T) > 0 && N >= 1 && N <= 64) +std::array blake2b_pers( + std::span pers, const T&... args) { + std::array result; + blake2b_pers(result, pers, args...); + return result; +} /// API: hash/shake256 /// @@ -252,6 +308,15 @@ struct [[nodiscard]] shake256 { ...); return *this; } + + /// Squeezes N bytes from the state and returns them as a `std::array`. + template + requires(N >= 1) + std::array squeeze() { + std::array result; + (*this)(result); + return result; + } }; /// API: hash/sha3_256 @@ -274,6 +339,10 @@ struct [[nodiscard]] shake256 { /// use of init_with_domain, not a workaround. /// /// The temporary keccak state is zeroed before this function returns. +/// +/// Two overloads are provided: +/// - write-to-output: `sha3_256(out, args...)` writes the hash into `out` +/// - return-value: `sha3_256<32>(args...)` returns a `std::array` template requires(detail::container_extent_v == 32 && sizeof...(T) > 0) void sha3_256(Out& out, const T&... args) { @@ -282,6 +351,13 @@ void sha3_256(Out& out, const T&... args) { crypto_xof_shake256_squeeze(&st, reinterpret_cast(std::ranges::data(out)), 32); sodium_memzero(&st, sizeof(st)); } +template + requires(N == 32 && sizeof...(T) > 0) +std::array sha3_256(const T&... args) { + std::array result; + sha3_256(result, args...); + return result; +} // Helper callable usable with unordered_map and similar to hash an array of chars by simply copying // the first sizeof(size_t) bytes, suitable for use with pre-hashed values. diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 08c78a4b5..89a95b733 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -34,9 +34,9 @@ namespace detail { void encrypt_multi_key( std::array& key_out, - const unsigned char* a, - const unsigned char* A, - const unsigned char* B, + std::span a, + std::span A, + std::span B, bool encrypting, std::string_view domain); @@ -139,7 +139,8 @@ void encrypt_for_multiple( if (messages.size() > 1) ++msg_it; try { - detail::encrypt_multi_key(key, privkey.data(), pubkey.data(), r.data(), true, domain); + detail::encrypt_multi_key( + key, privkey.first<32>(), pubkey.first<32>(), r.first<32>(), true, domain); } catch (const std::exception&) { if (ignore_invalid_recipient) continue; @@ -213,7 +214,7 @@ std::optional> decrypt_for_multiple( sodium_cleared> key; detail::encrypt_multi_key( - key, privkey.data(), pubkey.data(), sender_pubkey.data(), false, domain); + key, privkey.first<32>(), pubkey.first<32>(), sender_pubkey.first<32>(), false, domain); auto decrypted = std::make_optional>(); diff --git a/src/attachments.cpp b/src/attachments.cpp index 0ed3fb919..c8365c68a 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -313,19 +312,16 @@ std::array encrypt( crypto_generichash_blake2b_state b_st; const auto domain_byte = static_cast(domain); crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(seed.data()), 32); + hash::update_all(b_st, seed.first(32)); size_t in_size = 0; std::array chunk; while (in.read(reinterpret_cast(chunk.data()), chunk.size())) { - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(chunk.data()), chunk.size()); + hash::update_all(b_st, chunk); in_size += chunk.size(); } if (in.gcount() > 0) { - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(chunk.data()), in.gcount()); + hash::update_all(b_st, std::span{chunk}.first(in.gcount())); in_size += in.gcount(); } diff --git a/src/blinding.cpp b/src/blinding.cpp index 21538d198..d8ddae8a9 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -23,8 +22,7 @@ using namespace std::literals; std::array blind15_factor(std::span server_pk) { assert(server_pk.size() == 32); - uc64 blind_hash; - hash::blake2b(blind_hash, server_pk); + auto blind_hash = hash::blake2b<64>(server_pk); uc32 k; crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); @@ -36,16 +34,11 @@ std::array blind25_factor( assert(session_id.size() == 32 || session_id.size() == 33); assert(server_pk.size() == 32); - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init(&st, nullptr, 0, 64); - if (session_id.size() == 32) { - constexpr unsigned char prefix = 0x05; - crypto_generichash_blake2b_update(&st, &prefix, 1); - } - crypto_generichash_blake2b_update(&st, session_id.data(), session_id.size()); - crypto_generichash_blake2b_update(&st, server_pk.data(), server_pk.size()); uc64 blind_hash; - crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + if (session_id.size() == 32) + hash::blake2b(blind_hash, "05"_hex_b, session_id, server_pk); + else + hash::blake2b(blind_hash, session_id, server_pk); uc32 k; crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); diff --git a/src/config/base.cpp b/src/config/base.cpp index b5f0cd4a5..e1229ba91 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -25,6 +24,7 @@ #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" using namespace std::literals; @@ -214,8 +214,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span(recombined); if (actual_hash != final_hash) throw std::runtime_error{ "recombined message hash ({}) does not match part hash ({})"_format( @@ -729,8 +728,7 @@ ConfigBase::push() { // - element 3 is the chunk of data (and so, when ordered by sequence number, each data // chunk // concatenated together gives us the `msg` value we have right now in this function). - hash_t final_hash; - hash::hash(final_hash, msg); + auto final_hash = hash::blake2b<32>(msg); constexpr size_t ENCODE_OVERHEAD = 1 // The `m` prefix indicating a multipart message part @@ -1121,15 +1119,7 @@ void ConfigBase::set_signer(ConfigMessage::sign_callable s) { std::array ConfigSig::seed_hash(std::string_view key) const { if (!_sign_sk) throw std::runtime_error{"Cannot make a seed hash without a signing secret key"}; - std::array out; - crypto_generichash_blake2b( - out.data(), - out.size(), - _sign_sk.data(), - 32, // Just the seed part of the value, not the last half (which is just the pubkey) - reinterpret_cast(key.data()), - std::min(key.size(), 64)); - return out; + return hash::blake2b_key<32>(key, std::span{_sign_sk.data(), 32}); } void set_error(config_object* conf, std::string e) { diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 6fee7c956..b55c36aa8 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 4ec9ba58a..bee7de48b 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 8a37e20b2..12dfee9a5 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -57,34 +56,28 @@ void encrypt_prealloced( if (message.size() < ENCRYPT_DATA_OVERHEAD) throw std::invalid_argument{ "encrypt_prealloced: buffer is smaller than ENCRYPT_DATA_OVERHEAD"}; - size_t plaintext_len = message.size() - ENCRYPT_DATA_OVERHEAD; - auto key = make_encrypt_key(key_base, plaintext_len, domain); + auto plaintext = message.first(message.size() - ENCRYPT_DATA_OVERHEAD); + auto key = make_encrypt_key(key_base, plaintext.size(), domain); std::string nonce_key{NONCE_KEY_PREFIX}; nonce_key += domain; - std::array nonce; - crypto_generichash_blake2b( - nonce.data(), - nonce.size(), - message.data(), - plaintext_len, - to_unsigned(nonce_key.data()), - nonce_key.size()); + auto nonce = + hash::blake2b_key(nonce_key, plaintext); unsigned long long outlen = 0; crypto_aead_xchacha20poly1305_ietf_encrypt( message.data(), &outlen, - message.data(), - plaintext_len, + plaintext.data(), + plaintext.size(), nullptr, 0, nullptr, nonce.data(), key.data()); - assert(outlen == plaintext_len + crypto_aead_xchacha20poly1305_ietf_ABYTES); + assert(outlen == plaintext.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); std::memcpy(message.data() + outlen, nonce.data(), nonce.size()); } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index fb6b6e433..f526cacc3 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -1,7 +1,6 @@ #include "session/config/groups/info.hpp" #include -#include #include diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 024795b91..84857fca3 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -298,14 +297,11 @@ std::span Keys::rekey(Info& info, Members& members) { crypto_generichash_blake2b_init( &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); for (const auto& m : members) - crypto_generichash_blake2b_update( - &st, to_unsigned(m.session_id.data()), m.session_id.size()); + hash::update_all(st, m.session_id); auto gen = keys_.empty() ? 0 : keys_.back().generation + 1; auto gen_str = std::to_string(gen); - crypto_generichash_blake2b_update(&st, to_unsigned(gen_str.data()), gen_str.size()); - - crypto_generichash_blake2b_update(&st, h2.data(), 32); + hash::update_all(st, gen_str, h2); crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); @@ -373,8 +369,8 @@ std::span Keys::rekey(Info& info, Members& members) { std::vector junk_data; junk_data.resize(encrypted.size() * n_junk); - std::array rng_seed; - hash::blake2b_key(rng_seed, junk_seed_hash_key, h1, _sign_sk); + auto rng_seed = + hash::blake2b_key(junk_seed_hash_key, h1, _sign_sk); randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); std::string_view junk_view = to_string_view(junk_data); @@ -469,12 +465,10 @@ std::vector Keys::key_supplement(const std::vector& &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); for (const auto& sid : sids) - crypto_generichash_blake2b_update(&st, to_unsigned(sid.data()), sid.size()); - - crypto_generichash_blake2b_update(&st, to_unsigned(supp_keys.data()), supp_keys.size()); + hash::update_all(st, sid); std::array h2 = seed_hash(seed_hash_key); - crypto_generichash_blake2b_update(&st, h2.data(), h2.size()); + hash::update_all(st, supp_keys, h2); crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); @@ -537,9 +531,7 @@ std::array Keys::subaccount_blind_factor( auto mask = seed_hash("SessionGroupSubaccountMask"); static_assert(mask.size() == crypto_generichash_blake2b_KEYBYTES); - std::array h; - hash::blake2b_key( - h, + auto h = hash::blake2b_key<64>( mask, std::array{'\x05'}, session_xpk, diff --git a/src/config/local.cpp b/src/config/local.cpp index 3b6575d33..f95c2bb69 100644 --- a/src/config/local.cpp +++ b/src/config/local.cpp @@ -1,7 +1,5 @@ #include "session/config/local.h" -#include - #include "internal.hpp" #include "session/config/error.h" #include "session/config/local.hpp" diff --git a/src/config/pro.cpp b/src/config/pro.cpp index 192189ef2..4a53871e4 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index a87638d7f..26c5281c1 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 2edfb50f0..3de898a83 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -1,6 +1,5 @@ #include "session/config/user_profile.h" -#include #include #include "internal.hpp" diff --git a/src/core/devices.cpp b/src/core/devices.cpp index ec586a71e..16c78ce10 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -1140,8 +1139,7 @@ std::vector Devices::decrypt_device_data(std::span e auto active_keys = active_device_keys(); - std::array devices_nonce; - hash::blake2b_pers(devices_nonce, PERS_DEV_NONCE, ciphertext_raw); + auto devices_nonce = hash::blake2b_pers<24>(PERS_DEV_NONCE, ciphertext_raw); cleared_uc32 ml_ss, aB, ki, key_base; @@ -1159,8 +1157,7 @@ std::vector Devices::decrypt_device_data(std::span e auto ekey = enc_key[i]; auto eind = enc_indicator[i]; - std::array knonce; - hash::blake2b_key_pers(knonce, A, PERS_KEY_NONCE, ct, enc_devices); + auto knonce = hash::blake2b_key_pers<24>(A, PERS_KEY_NONCE, ct, enc_devices); for (int active_i = 0; active_i < active_keys.size(); active_i++) { const auto& k = active_keys[active_i]; @@ -1170,9 +1167,8 @@ std::vector Devices::decrypt_device_data(std::span e // First work out the checksum hash; the vast majority of the time this won't match for // a key other than our own (only 1/65535 chance of collision), and so we can short // circuit and save a bunch of calculations. - std::array our_ind; - hash::blake2b_pers(our_ind, PERS_KEY_KEY_IND, A, B, M, ct, ekey); - if (!std::ranges::equal(our_ind, eind)) + if (!std::ranges::equal( + hash::blake2b_pers<2>(PERS_KEY_KEY_IND, A, B, M, ct, ekey), eind)) continue; if (0 != crypto_scalarmult_curve25519(aB.data(), k.x25519_sec.data(), A.data())) { diff --git a/src/core/link_sas.cpp b/src/core/link_sas.cpp index c2a771979..fa5a39c55 100644 --- a/src/core/link_sas.cpp +++ b/src/core/link_sas.cpp @@ -10,8 +10,7 @@ using namespace session::literals; namespace session::core { std::array derive_sas_seed(std::span plaintext) { - std::array salt; - hash::blake2b_pers(salt, "SessionLinkEmoji"_b2b_pers, plaintext); + auto salt = hash::blake2b_pers<16>("SessionLinkEmoji"_b2b_pers, plaintext); std::array seed; if (0 != crypto_pwhash( diff --git a/src/hash.cpp b/src/hash.cpp index b698f6b31..2eb885ac6 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -5,9 +5,9 @@ #include "session/export.h" #include "session/util.hpp" -namespace session::hash { +namespace { -void hash( +void hash_impl( std::span hash, std::span msg, std::optional> key) { @@ -27,14 +27,23 @@ void hash( key ? key->size() : 0); } +} // namespace + +namespace session::hash { + +void hash( + std::span hash, + std::span msg, + std::optional> key) { + hash_impl(hash, msg, key); +} + std::vector hash( const size_t size, std::span msg, std::optional> key) { - std::vector result; - result.resize(size); - hash(result, msg, key); - + std::vector result(size); + hash_impl(result, msg, key); return result; } @@ -55,8 +64,7 @@ LIBSESSION_C_API bool session_hash( if (key_in && key_len) key = {key_in, key_len}; - std::vector result = session::hash::hash(size, {msg_in, msg_len}, key); - std::memcpy(hash_out, result.data(), size); + hash_impl({hash_out, size}, {msg_in, msg_len}, key); return true; } catch (...) { return false; diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 93c7f72cd..ae4b69f2c 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -11,6 +10,8 @@ #include #include +#include "session/hash.hpp" + namespace session { const size_t encrypt_multiple_message_overhead = crypto_aead_xchacha20poly1305_ietf_ABYTES; @@ -19,36 +20,25 @@ namespace detail { void encrypt_multi_key( std::array& key, - const unsigned char* a, - const unsigned char* A, - const unsigned char* B, + std::span a, + std::span A, + std::span B, bool encrypting, std::string_view domain) { std::array buf; - if (0 != crypto_scalarmult_curve25519(buf.data(), a, B)) + if (0 != crypto_scalarmult_curve25519(buf.data(), a.data(), B.data())) throw std::invalid_argument{"Unable to compute shared encrypted key: invalid pubkey?"}; static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init( - &st, - reinterpret_cast(domain.data()), - std::min(domain.size(), crypto_generichash_blake2b_KEYBYTES_MAX), - 32); - - crypto_generichash_blake2b_update(&st, buf.data(), buf.size()); - // If we're encrypting then a/A == sender, B = recipient // If we're decrypting then a/A = recipient, B = sender // We always need the same sR || S || R or rS || S || R, so if we're decrypting we need to // put B before A in the hash; - const auto* S = encrypting ? A : B; - const auto* R = encrypting ? B : A; - crypto_generichash_blake2b_update(&st, S, 32); - crypto_generichash_blake2b_update(&st, R, 32); - crypto_generichash_blake2b_final(&st, key.data(), 32); + const auto& S = encrypting ? A : B; + const auto& R = encrypting ? B : A; + hash::blake2b_key(key, domain, buf, S, R); } void encrypt_multi_impl( diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index 6ab0a6da6..639b019d8 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -313,8 +313,9 @@ OnionRequestRouter::OnionRequestRouter( for (const auto& node : _config.seed_nodes) node.to_disk(std::back_inserter(seed_node_data)); - auto hash_bytes = session::hash::hash(32, session::to_span(seed_node_data)); - cache_file_name = "edge_nodes_devnet_" + oxenc::to_hex(hash_bytes); + cache_file_name = + "edge_nodes_devnet_" + + oxenc::to_hex(session::hash::blake2b<32>(session::to_span(seed_node_data))); break; } diff --git a/src/network/snode_pool.cpp b/src/network/snode_pool.cpp index 5c271a22f..033b6a39d 100644 --- a/src/network/snode_pool.cpp +++ b/src/network/snode_pool.cpp @@ -70,8 +70,9 @@ SnodePool::SnodePool( for (const auto& node : _config.seed_nodes) node.to_disk(std::back_inserter(seed_node_data)); - auto hash_bytes = session::hash::hash(32, session::to_span(seed_node_data)); - cache_file_name = "snode_pool_devnet_" + oxenc::to_hex(hash_bytes); + cache_file_name = + "snode_pool_devnet_" + + oxenc::to_hex(session::hash::blake2b<32>(session::to_span(seed_node_data))); break; } diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 69ee0693c..10102f96c 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -225,9 +225,7 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L171 - uc32 hash_to_sign; - hash::blake2b_pers( - hash_to_sign, + auto hash_to_sign = hash::blake2b_pers<32>( ADD_PRO_PAYMENT_PERS, version, master_privkey.subspan( @@ -389,9 +387,7 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 uint8_t version = 0; uint64_t unix_ts_ms = epoch_ms(unix_ts); - uc32 hash_to_sign; - hash::blake2b_pers( - hash_to_sign, + auto hash_to_sign = hash::blake2b_pers<32>( GENERATE_PROOF_PERS, version, master_privkey.last<32>(), @@ -551,14 +547,8 @@ uc64 GetProDetailsRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 uint64_t unix_ts_ms = epoch_ms(unix_ts); - uc32 hash_to_sign; - hash::blake2b_pers( - hash_to_sign, - GET_PRO_DETAILS_PERS, - version, - master_privkey.last<32>(), - unix_ts_ms, - count); + auto hash_to_sign = hash::blake2b_pers<32>( + GET_PRO_DETAILS_PERS, version, master_privkey.last<32>(), unix_ts_ms, count); // Sign the hash uc64 result = {}; @@ -784,11 +774,9 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 - uc32 hash_to_sign; uint64_t unix_ts_ms = epoch_ms(unix_ts); uint64_t refund_requested_unix_ts_ms = epoch_ms(refund_requested_unix_ts); - hash::blake2b_pers( - hash_to_sign, + auto hash_to_sign = hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, version, master_privkey.subspan( diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 31d6b4b31..a359ff0fb 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -113,8 +112,7 @@ static void v2_derive_xwing_key_nonce( cleared_uc32& nonce_buf, std::span E, std::span X) { - std::array ss; - hash::sha3_256(ss, key_buf, nonce_buf, E, X, V2_XWING_LABEL); + auto ss = hash::sha3_256<32>(key_buf, nonce_buf, E, X, V2_XWING_LABEL); hash::shake256(V2_SS_DOMAIN, ss)( key_buf, std::span{nonce_buf.data(), V2_NONCE_SIZE}); sodium_memzero(ss.data(), ss.size()); @@ -133,9 +131,7 @@ static std::array v2_kiss( cleared_uc32 dh; if (0 != crypto_scalarmult(dh.data(), sec.data(), encrypting ? S.data() : E.data())) throw std::runtime_error{"X25519 DH (KISS) failed"}; - std::array kiss; - hash::blake2b_key_pers(kiss, dh, V2_KISS_PERS, E, S); - return kiss; + return hash::blake2b_key_pers<2>(dh, V2_KISS_PERS, E, S); } std::vector sign_for_recipient( @@ -1079,16 +1075,8 @@ std::string decrypt_ons_response( // key = H(name, key=H(name)) uc32 key; uc32 name_hash; - auto name_bytes = to_unsigned(lowercase_name.data()); - crypto_generichash_blake2b( - name_hash.data(), name_hash.size(), name_bytes, lowercase_name.size(), nullptr, 0); - crypto_generichash_blake2b( - key.data(), - key.size(), - name_bytes, - lowercase_name.size(), - name_hash.data(), - name_hash.size()); + hash::blake2b(name_hash, lowercase_name); + hash::blake2b_key(key, name_hash, lowercase_name); std::vector buf; unsigned long long buf_len = 0; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 5c12fce76..17864aa46 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -56,15 +56,8 @@ session::uc32 proof_hash_internal( // This must match the hashing routine at // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 - session::uc32 result = {}; - session::hash::blake2b_pers( - result, - session::BUILD_PROOF_PERS, - version, - gen_index_hash, - rotating_pubkey, - expiry_unix_ts_ms); - return result; + return session::hash::blake2b_pers<32>( + session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); } bool proof_verify_signature_internal( diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 959a8c0b1..97a64fb2b 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -1,40 +1,26 @@ #include #include -#include #include #include #include "session/blinding.hpp" +#include "session/hash.hpp" #include "session/util.hpp" #include "utils.hpp" using namespace session; -constexpr std::array seed1{ - 0xfe, 0xcd, 0x9a, 0x60, 0x34, 0xbc, 0x9a, 0xba, 0x27, 0x39, 0x25, 0xde, 0xe7, - 0x06, 0x2b, 0x12, 0x33, 0x34, 0x58, 0x7c, 0x3c, 0x62, 0x57, 0x34, 0x1a, 0xfa, - 0xe2, 0xd7, 0xfe, 0x85, 0xe1, 0x22, 0xf4, 0xef, 0x87, 0x39, 0x08, 0xf6, 0xa5, - 0x37, 0x7b, 0xa3, 0x85, 0x3f, 0x0e, 0x2f, 0xa3, 0x26, 0xee, 0xd9, 0xe7, 0x41, - 0xed, 0xf9, 0xf7, 0xd0, 0x31, 0x1a, 0x3e, 0xcc, 0x66, 0xa5, 0x7b, 0x32}; -constexpr std::array seed2{ - 0x86, 0x59, 0xef, 0xdc, 0xbe, 0x09, 0x49, 0xe0, 0xf8, 0x11, 0x41, 0xe6, 0xd3, - 0x97, 0xe8, 0xbe, 0x75, 0xf4, 0x5d, 0x09, 0x26, 0x2f, 0x20, 0x9d, 0x59, 0x50, - 0xe9, 0x79, 0x89, 0xeb, 0x43, 0xc7, 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, - 0x45, 0x44, 0xc1, 0xc5, 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, - 0xe8, 0xaa, 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x81}; - -constexpr std::array xpub1{ - 0xfe, 0x94, 0xb7, 0xad, 0x4b, 0x7f, 0x1c, 0xc1, 0xbb, 0x92, 0x67, - 0x1f, 0x1f, 0x0d, 0x24, 0x3f, 0x22, 0x6e, 0x11, 0x5b, 0x33, 0x77, - 0x04, 0x65, 0xe8, 0x2b, 0x50, 0x3f, 0xc3, 0xe9, 0x6e, 0x1f, -}; -constexpr std::array xpub2{ - 0x05, 0xc9, 0xa9, 0xbf, 0x17, 0x8f, 0xa6, 0x44, 0xd4, 0x4b, 0xeb, - 0xf6, 0x28, 0x71, 0x6d, 0xc7, 0xf2, 0xdf, 0x3d, 0x08, 0x42, 0xe9, - 0x78, 0x81, 0x96, 0x2c, 0x72, 0x36, 0x99, 0x15, 0x20, 0x73, -}; +constexpr auto seed1 = + "fecd9a6034bc9aba273925dee7062b123334587c3c6257341afae2d7fe85e122" + "f4ef873908f6a5377ba3853f0e2fa326eed9e741edf9f7d0311a3ecc66a57b32"_hex_u; +constexpr auto seed2 = + "8659efdcbe0949e0f81141e6d397e8be75f45d09262f209d5950e97989eb43c7" + "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_hex_u; + +constexpr auto xpub1 = "fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_u; +constexpr auto xpub2 = "05c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_u; const std::string session_id1 = "05" + oxenc::to_hex(xpub1.begin(), xpub1.end()); const std::string session_id2 = "05" + oxenc::to_hex(xpub2.begin(), xpub2.end()); @@ -273,9 +259,7 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { // Hash ourselves just to make sure we get what we expect for the seed part of the secret key: cleared_uc32 expect_seed; - static const auto hash_key = to_span("VersionCheckKey_sig"sv); - crypto_generichash_blake2b( - expect_seed.data(), 32, seed1.data(), 32, hash_key.data(), hash_key.size()); + hash::blake2b_key(expect_seed, "VersionCheckKey_sig"sv, seed1.first<32>()); CHECK(oxenc::to_hex(seckey.begin(), seckey.begin() + 32) == oxenc::to_hex(expect_seed.begin(), expect_seed.end())); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 24948943d..748a2458c 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -1,11 +1,11 @@ #include #include -#include #include #include #include #include +#include #include #include "session/bt_merge.hpp" @@ -107,13 +107,6 @@ auto& s(config::dict_value& v) { return std::get(v); } -std::vector blake2b(std::span data) { - std::vector result; - result.resize(32); - crypto_generichash_blake2b(result.data(), 32, data.data(), data.size(), nullptr, 0); - return result; -} - TEST_CASE("config diff", "[config][diff]") { MutableConfigMessage m; m.data()["foo"] = 123; @@ -556,7 +549,7 @@ TEST_CASE("config message example 1", "[config][example]") { CHECK(printable(m118.serialize()) == printable(m118_expected)); - CHECK(to_hex(m118.hash()) == to_hex(blake2b(m118_expected))); + CHECK(to_hex(m118.hash()) == to_hex(hash::blake2b<32>(m118_expected))); // Increment 5 times so that our diffs will be empty. auto m123 = m118.increment(); @@ -760,7 +753,7 @@ TEST_CASE("config message example 2", "[config][example]") { "l" "i122e" "32:"+to_string(h122)+ "de" "e" "l" "i123e" - "32:"+to_string(blake2b(m123_expected))+ + "32:"+to_string(hash::blake2b<32>(m123_expected))+ "d" "4:int0" "1:-" "4:int1" "0:" diff --git a/tests/test_encrypt.cpp b/tests/test_encrypt.cpp index 873837535..940db8a8a 100644 --- a/tests/test_encrypt.cpp +++ b/tests/test_encrypt.cpp @@ -1,5 +1,4 @@ #include -#include #include #include From cda7c98cfec323c46d0526e9278a913ae0002f9a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 22:09:22 -0300 Subject: [PATCH 066/193] unsigned char -> std::byte refactor, part 1 This adds wrappers around the C functions we use from sodium & mlkem_native so that we can always go through the wrappers taking std::byte spans, giving us compile-time length requirements (where appropriate), collapsing `data, size` argument pairs into single arguments, and just making it much less painful to use std::byte everywhere because with these changes we won't have to use endless reinterpret_casts. This is part 1: it adds the wrappers needed to make it work. --- include/session/crypto.hpp | 178 +++++++++++++++++++++++++++++++ include/session/encrypt.hpp | 183 ++++++++++++++++++++++++++++++++ include/session/hash.hpp | 41 +++++++ include/session/mlkem768.hpp | 49 +++++++++ include/session/util.hpp | 11 ++ src/onionreq/hop_encryption.cpp | 15 ++- src/session_encrypt.cpp | 33 ------ 7 files changed, 468 insertions(+), 42 deletions(-) create mode 100644 include/session/crypto.hpp create mode 100644 include/session/encrypt.hpp create mode 100644 include/session/mlkem768.hpp diff --git a/include/session/crypto.hpp b/include/session/crypto.hpp new file mode 100644 index 000000000..0b3de7e4c --- /dev/null +++ b/include/session/crypto.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util.hpp" + +namespace session::crypto { + +// ─── Ed25519 ───────────────────────────────────────────────────────────────── + +/// Generates a random Ed25519 keypair. +inline void ed25519_keypair( + std::span pk, + std::span sk) { + crypto_sign_ed25519_keypair(ucdata(pk), ucdata(sk)); +} + +/// Generates a deterministic Ed25519 keypair from a 32-byte seed. +inline void ed25519_seed_keypair( + std::span pk, + std::span sk, + std::span seed) { + crypto_sign_ed25519_seed_keypair(ucdata(pk), ucdata(sk), ucdata(seed)); +} + +/// Signs `msg` with `sk`, writing the 64-byte detached signature into `sig`. +inline void ed25519_sign( + std::span sig, + std::span msg, + std::span sk) { + crypto_sign_ed25519_detached(ucdata(sig), nullptr, ucdata(msg), msg.size(), ucdata(sk)); +} + +/// Verifies a detached Ed25519 signature. Returns true if valid. +inline bool ed25519_verify( + std::span sig, + std::span msg, + std::span pk) { + return 0 == + crypto_sign_ed25519_verify_detached(ucdata(sig), ucdata(msg), msg.size(), ucdata(pk)); +} + +/// Extracts the Ed25519 public key from a secret key. +inline void ed25519_sk_to_pk( + std::span pk, + std::span sk) { + crypto_sign_ed25519_sk_to_pk(ucdata(pk), ucdata(sk)); +} + +/// Converts an Ed25519 public key to an X25519 public key. Returns false on failure. +inline bool ed25519_pk_to_x25519( + std::span x25519_pk, + std::span ed25519_pk) { + return 0 == crypto_sign_ed25519_pk_to_curve25519(ucdata(x25519_pk), ucdata(ed25519_pk)); +} + +/// Converts an Ed25519 secret key to an X25519 secret key. Returns false on failure. +inline bool ed25519_sk_to_x25519( + std::span x25519_sk, + std::span ed25519_sk) { + return 0 == crypto_sign_ed25519_sk_to_curve25519(ucdata(x25519_sk), ucdata(ed25519_sk)); +} + +// ─── Ed25519 group / scalar operations ─────────────────────────────────────── + +/// Reduces a 64-byte value modulo the Ed25519 group order, writing 32 bytes into `out`. +inline void ed25519_scalar_reduce(std::span out, std::span in) { + crypto_core_ed25519_scalar_reduce(ucdata(out), ucdata(in)); +} + +/// Negates a scalar modulo the Ed25519 group order. +inline void ed25519_scalar_negate(std::span out, std::span in) { + crypto_core_ed25519_scalar_negate(ucdata(out), ucdata(in)); +} + +/// Multiplies two scalars modulo the Ed25519 group order. +inline void ed25519_scalar_mul( + std::span out, + std::span x, + std::span y) { + crypto_core_ed25519_scalar_mul(ucdata(out), ucdata(x), ucdata(y)); +} + +/// Adds two scalars modulo the Ed25519 group order. +inline void ed25519_scalar_add( + std::span out, + std::span x, + std::span y) { + crypto_core_ed25519_scalar_add(ucdata(out), ucdata(x), ucdata(y)); +} + +/// Computes `scalar * B` (clamped) where B is the Ed25519 base point. +/// Returns false if the scalar is zero. +inline bool ed25519_scalarmult_base( + std::span out, std::span scalar) { + return 0 == crypto_scalarmult_ed25519_base(ucdata(out), ucdata(scalar)); +} + +/// Computes `scalar * B` (unclamped) where B is the Ed25519 base point. +/// Returns false if the scalar is zero. +inline bool ed25519_scalarmult_base_noclamp( + std::span out, std::span scalar) { + return 0 == crypto_scalarmult_ed25519_base_noclamp(ucdata(out), ucdata(scalar)); +} + +/// Computes `scalar * point` (unclamped) on the Ed25519 curve. +/// Returns false if the result is the identity element. +inline bool ed25519_scalarmult_noclamp( + std::span out, + std::span scalar, + std::span point) { + return 0 == crypto_scalarmult_ed25519_noclamp(ucdata(out), ucdata(scalar), ucdata(point)); +} + +// ─── X25519 ────────────────────────────────────────────────────────────────── + +/// Generates a random X25519 keypair. +inline void x25519_keypair( + std::span pk, + std::span sk) { + crypto_box_keypair(ucdata(pk), ucdata(sk)); +} + +/// Generates a deterministic X25519 keypair from a 32-byte seed. +inline void x25519_seed_keypair( + std::span pk, + std::span sk, + std::span seed) { + crypto_box_seed_keypair(ucdata(pk), ucdata(sk), ucdata(seed)); +} + +/// Computes the X25519 public key corresponding to `sk`. +inline void x25519_scalarmult_base( + std::span pk, + std::span sk) { + crypto_scalarmult_curve25519_base(ucdata(pk), ucdata(sk)); +} + +/// Computes the X25519 shared secret from a secret key and a remote public key. +/// Returns false if the result is the all-zeros point (degenerate case). +inline bool x25519_scalarmult( + std::span shared, + std::span sk, + std::span pk) { + return 0 == crypto_scalarmult_curve25519(ucdata(shared), ucdata(sk), ucdata(pk)); +} + +// ─── Password hashing ──────────────────────────────────────────────────────── + +/// Derives a key from a password using Argon2. Returns false on failure (e.g. out of memory). +inline bool pwhash( + std::span out, + std::string_view passwd, + std::span salt, + unsigned long long opslimit, + size_t memlimit, + int alg) { + return 0 == crypto_pwhash( + ucdata(out), + out.size(), + passwd.data(), + passwd.size(), + ucdata(salt), + opslimit, + memlimit, + alg); +} + +} // namespace session::crypto diff --git a/include/session/encrypt.hpp b/include/session/encrypt.hpp new file mode 100644 index 000000000..2bbbbb60d --- /dev/null +++ b/include/session/encrypt.hpp @@ -0,0 +1,183 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "util.hpp" + +namespace session::encrypt { + +// ─── XChaCha20-Poly1305 AEAD ───────────────────────────────────────────────── + +/// Encrypts `msg` with `key` and `nonce`, writing ciphertext (msg.size() + ABYTES bytes) into +/// `out`. Additional data `ad` is authenticated but not encrypted; pass an empty span for none. +inline void xchacha20poly1305_encrypt( + std::span out, + std::span msg, + std::span ad, + std::span nonce, + std::span key) { + crypto_aead_xchacha20poly1305_ietf_encrypt( + ucdata(out), + nullptr, + ucdata(msg), + msg.size(), + ad.empty() ? nullptr : ucdata(ad), + ad.size(), + nullptr, + ucdata(nonce), + ucdata(key)); +} + +/// Decrypts `ciphertext` with `key` and `nonce`, writing plaintext (ciphertext.size() - ABYTES +/// bytes) into `out`. Additional data `ad` must match what was passed at encryption time. +/// Returns false if authentication fails. +inline bool xchacha20poly1305_decrypt( + std::span out, + std::span ciphertext, + std::span ad, + std::span nonce, + std::span key) { + return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( + ucdata(out), + nullptr, + nullptr, + ucdata(ciphertext), + ciphertext.size(), + ad.empty() ? nullptr : ucdata(ad), + ad.size(), + ucdata(nonce), + ucdata(key)); +} + +// ─── XChaCha20 stream ──────────────────────────────────────────────────────── + +/// XOR-encrypts/decrypts `in` with the XChaCha20 keystream derived from `nonce` and `key`, +/// writing the result into `out`. `out` and `in` must be the same size and may alias. +inline void xchacha20_xor( + std::span out, + std::span in, + std::span nonce, + std::span key) { + crypto_stream_xchacha20_xor(ucdata(out), ucdata(in), in.size(), ucdata(nonce), ucdata(key)); +} + +// ─── HChaCha20 ─────────────────────────────────────────────────────────────── + +/// Derives a 32-byte subkey from a 32-byte key and a 16-byte nonce prefix using HChaCha20. +/// This is the subkey-derivation step used internally by XChaCha20. +inline void hchacha20( + std::span out, + std::span nonce_prefix, + std::span key) { + crypto_core_hchacha20(ucdata(out), ucdata(nonce_prefix), ucdata(key), nullptr); +} + +// ─── Secretstream (streaming XChaCha20-Poly1305) ───────────────────────────── + +/// Initialises a secretstream pull (decryption) state from a header and key. +inline void secretstream_init_pull( + crypto_secretstream_xchacha20poly1305_state& st, + std::span header, + std::span key) { + crypto_secretstream_xchacha20poly1305_init_pull(&st, ucdata(header), ucdata(key)); +} + +/// Encrypts one chunk and appends it to the stream. `out` must be at least +/// `in.size() + crypto_secretstream_xchacha20poly1305_ABYTES` bytes. `ad` may be empty. +/// Returns the number of bytes written into `out`. +inline size_t secretstream_push( + crypto_secretstream_xchacha20poly1305_state& st, + std::span out, + std::span in, + std::span ad, + unsigned char tag) { + unsigned long long out_len; + crypto_secretstream_xchacha20poly1305_push( + &st, ucdata(out), &out_len, ucdata(in), in.size(), ucdata(ad), ad.size(), tag); + return static_cast(out_len); +} + +/// Decrypts one chunk from the stream. `out` must be at least +/// `in.size() - crypto_secretstream_xchacha20poly1305_ABYTES` bytes. `ad` may be empty. +/// Returns the number of bytes written and sets `tag_out` on success, or returns std::nullopt if +/// authentication fails. +inline std::optional secretstream_pull( + crypto_secretstream_xchacha20poly1305_state& st, + std::span out, + unsigned char& tag_out, + std::span in, + std::span ad = {}) { + unsigned long long out_len; + if (0 != + crypto_secretstream_xchacha20poly1305_pull( + &st, ucdata(out), &out_len, &tag_out, ucdata(in), in.size(), ucdata(ad), ad.size())) + return std::nullopt; + return static_cast(out_len); +} + +// ─── Box (X25519 + XSalsa20-Poly1305) ──────────────────────────────────────── + +/// Encrypts `msg` for `recipient_pk` from `sender_sk`, writing ciphertext into `out`. +/// `out` must be `msg.size() + crypto_box_MACBYTES` bytes. +inline void box_easy( + std::span out, + std::span msg, + std::span nonce, + std::span recipient_pk, + std::span sender_sk) { + if (0 != crypto_box_easy( + ucdata(out), + ucdata(msg), + msg.size(), + ucdata(nonce), + ucdata(recipient_pk), + ucdata(sender_sk))) + throw std::runtime_error{"crypto_box_easy failed (invalid public key?)"}; +} + +/// Seals `msg` for `pk` (anonymous sender), writing ciphertext into `out`. +/// `out` must be `msg.size() + crypto_box_SEALBYTES` bytes. +inline void box_seal( + std::span out, + std::span msg, + std::span pk) { + if (0 != crypto_box_seal(ucdata(out), ucdata(msg), msg.size(), ucdata(pk))) + throw std::runtime_error{"crypto_box_seal failed (invalid public key?)"}; +} + +/// Decrypts a sealed box. `out` must be `ciphertext.size() - crypto_box_SEALBYTES` bytes. +/// Returns false if authentication fails. +inline bool box_seal_open( + std::span out, + std::span ciphertext, + std::span pk, + std::span sk) { + return 0 == crypto_box_seal_open( + ucdata(out), ucdata(ciphertext), ciphertext.size(), ucdata(pk), ucdata(sk)); +} + +// ─── Secretbox (XSalsa20-Poly1305 with shared key) ─────────────────────────── + +/// Decrypts a secretbox ciphertext using a shared key. `out` must be +/// `ciphertext.size() - crypto_secretbox_MACBYTES` bytes. Returns false if authentication fails. +inline bool secretbox_open_easy( + std::span out, + std::span ciphertext, + std::span nonce, + std::span key) { + return 0 == + crypto_secretbox_open_easy( + ucdata(out), ucdata(ciphertext), ciphertext.size(), ucdata(nonce), ucdata(key)); +} + +} // namespace session::encrypt diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 8ff7961ec..f2dddc908 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include @@ -371,6 +373,45 @@ struct identity_hasher { } }; +// ─── SHA-512 ───────────────────────────────────────────────────────────────── + +/// One-shot SHA-512 hasher. Takes a fixed-size 64-byte output container and any number of +/// contiguous byte containers or integer values, computes the SHA-512 hash of their concatenation +/// (in argument order), and writes the result into the output container. +template + requires(detail::container_extent_v == crypto_hash_sha512_BYTES && sizeof...(T) > 0) +void sha512(Out& out, const T&... args) { + crypto_hash_sha512_state st; + crypto_hash_sha512_init(&st); + auto update = [&st](std::span arg) { + crypto_hash_sha512_update(&st, arg.data(), arg.size()); + }; + (update(detail::make_hashable(args)), ...); + crypto_hash_sha512_final(&st, reinterpret_cast(std::ranges::data(out))); + sodium_memzero(&st, sizeof(st)); +} + +// ─── HMAC-SHA-256 ──────────────────────────────────────────────────────────── + +/// One-shot HMAC-SHA-256. Takes a fixed-size 32-byte output container, a key (any byte +/// container), and any number of contiguous byte containers or integer values, computes the +/// HMAC-SHA-256 of their concatenation and writes the result into the output container. +template + requires(detail::container_extent_v == crypto_auth_hmacsha256_BYTES && sizeof...(T) > 0) +void hmac_sha256(Out& out, const Key& key, const T&... args) { + crypto_auth_hmacsha256_state st; + crypto_auth_hmacsha256_init( + &st, + reinterpret_cast(std::ranges::data(key)), + std::ranges::size(key)); + auto update = [&st](std::span arg) { + crypto_auth_hmacsha256_update(&st, arg.data(), arg.size()); + }; + (update(detail::make_hashable(args)), ...); + crypto_auth_hmacsha256_final(&st, reinterpret_cast(std::ranges::data(out))); + sodium_memzero(&st, sizeof(st)); +} + } // namespace session::hash namespace session { diff --git a/include/session/mlkem768.hpp b/include/session/mlkem768.hpp new file mode 100644 index 000000000..e9dc0c508 --- /dev/null +++ b/include/session/mlkem768.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include +#include + +namespace session::mlkem768 { + +/// Generates a keypair deterministically from a 64-byte seed. Throws on failure. +inline void keygen( + std::span pk, + std::span sk, + std::span seed) { + if (0 != sr_mlkem768_keypair_derand( + reinterpret_cast(pk.data()), + reinterpret_cast(sk.data()), + reinterpret_cast(seed.data()))) + throw std::runtime_error{"ML-KEM-768 keygen failed"}; +} + +/// Encapsulates a shared secret to `pk` using a 32-byte random seed, writing the ciphertext and +/// shared secret into the provided spans. Throws on failure. +inline void encapsulate( + std::span ciphertext, + std::span shared_secret, + std::span pk, + std::span seed) { + if (0 != sr_mlkem768_enc_derand( + reinterpret_cast(ciphertext.data()), + reinterpret_cast(shared_secret.data()), + reinterpret_cast(pk.data()), + reinterpret_cast(seed.data()))) + throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; +} + +/// Decapsulates a shared secret from `ciphertext` using `sk`. Returns false on failure. +inline bool decapsulate( + std::span shared_secret, + std::span ciphertext, + std::span sk) { + return 0 == sr_mlkem768_dec( + reinterpret_cast(shared_secret.data()), + reinterpret_cast(ciphertext.data()), + reinterpret_cast(sk.data())); +} + +} // namespace session::mlkem768 diff --git a/include/session/util.hpp b/include/session/util.hpp index 611eb2436..a4b0ddb1c 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -131,6 +131,17 @@ inline unsigned char* to_unsigned(unsigned char* x) { return x; } +/// Returns the data pointer of a std::byte span as an `unsigned char*`, for passing to C APIs +/// that expect `unsigned char*`. The const overload returns `const unsigned char*`. +template +inline unsigned char* ucdata(std::span sp) { + return reinterpret_cast(sp.data()); +} +template +inline const unsigned char* ucdata(std::span sp) { + return reinterpret_cast(sp.data()); +} + /// Returns true if the first string is equal to the second string, compared case-insensitively. inline bool string_iequal(std::string_view s1, std::string_view s2) { return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) { diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index 5e0b8cdf7..f03c39b7c 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include #include "session/export.h" +#include "session/hash.hpp" #include "session/network/key_types.hpp" #include "session/onionreq/builder.hpp" #include "session/util.hpp" @@ -67,14 +67,11 @@ namespace { local_sec.data(), remote_pub.data())) // Use key as tmp storage for aB throw std::runtime_error{"Failed to compute shared key for xchacha20"}; - crypto_generichash_state h; - crypto_generichash_init(&h, nullptr, 0, key.size()); - crypto_generichash_update(&h, key.data(), crypto_scalarmult_BYTES); - crypto_generichash_update( - &h, (local_first ? local_pub : remote_pub).data(), local_pub.size()); - crypto_generichash_update( - &h, (local_first ? remote_pub : local_pub).data(), local_pub.size()); - crypto_generichash_final(&h, key.data(), key.size()); + hash::blake2b( + key, + key, + local_first ? local_pub : remote_pub, + local_first ? remote_pub : local_pub); return key; } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index a359ff0fb..338a4ea69 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -1137,38 +1136,6 @@ std::vector decrypt_push_notification( return buf; } -template -std::string compute_hash(Func hasher, const T&... args) { - // Allocate a buffer of 20 bytes per integral value (which is the largest the any integral - // value can be when stringified). - std::array< - char, - (0 + ... + - (std::is_integral_v || std::is_same_v - ? 20 - : 0))> - buffer; - auto* b = buffer.data(); - return hasher({detail::to_hashable(args, b)...}); -} - -std::string compute_hash_blake2b_b64(std::vector parts) { - constexpr size_t HASH_SIZE = 32; - crypto_generichash_state state; - crypto_generichash_init(&state, nullptr, 0, HASH_SIZE); - for (const auto& s : parts) - crypto_generichash_update( - &state, reinterpret_cast(s.data()), s.size()); - std::array hash; - crypto_generichash_final(&state, hash.data(), HASH_SIZE); - - std::string b64hash = oxenc::to_base64(hash.begin(), hash.end()); - // Trim padding: - while (!b64hash.empty() && b64hash.back() == '=') - b64hash.pop_back(); - return b64hash; -} - std::vector encrypt_xchacha20( std::span plaintext, std::span key) { From f0054a90b6b4f39ac6b2ead4536b57af6f0f68ed Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 19:48:48 -0300 Subject: [PATCH 067/193] Add fallback "no-PFS" support This adds a encryption implementation *without* PFS that can be used if clients cannot find the recipient's PFS+PQ keys for some reason. Such decryption gets flagged as non-PFS (so it can be visually indicated, e.g. maybe with a yellow padlock or some such warning icon). --- include/session/core/callbacks.hpp | 7 +- include/session/core/globals.hpp | 9 +- include/session/session_encrypt.hpp | 54 ++++++ src/core.cpp | 32 +++- src/session_encrypt.cpp | 284 +++++++++++++++++++--------- tests/test_dm_receive.cpp | 180 +++++++++++++++++- 6 files changed, 464 insertions(+), 102 deletions(-) diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 6c53b98bb..063444462 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -32,7 +32,8 @@ enum class PfsKeyFetch { /// Reason code passed to the message_decrypt_failed callback. enum class MessageDecryptFailure { - no_pfs_key, ///< Version 2 message but no account key with a matching key indicator exists + no_pfs_key, ///< Version 2 message: no PFS account key matched the key indicator AND the + ///< non-PFS fallback decryption also failed; the message cannot be read. decrypt_failed, ///< Decryption failed (either version); key was found but did not work bad_format, ///< Message is structurally malformed (e.g. invalid bencode, truncated fields) unknown_version, ///< Message starts with 0x00 but carries an unrecognised version byte; @@ -48,7 +49,9 @@ struct ReceivedMessage { int version; ///< Protocol version: 1 or 2 std::vector content; ///< Decrypted protobuf-encoded payload std::optional> - pro_signature; ///< Session Pro signature, if present + pro_signature; ///< Session Pro signature, if present + bool pfs_encrypted = false; ///< True iff decrypted via PFS+PQ (X-Wing) key derivation; + ///< false for v1 messages and v2 non-PFS fallback messages. }; /// Struct holding application callbacks to fire when libsession Core events happen to allow the diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 5bb759aac..bd97c2548 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -94,12 +94,15 @@ class Globals final : detail::CoreComponent { public: /// The raw 32-byte account seed (identical to ed25519_secret().first<32>()). - std::span seed() const { return ubuf().first<32>(); } + std::span seed() const& { return ubuf().first<32>(); } + std::span seed() const&& = delete; /// The 64-byte Ed25519 secret key in libsodium format (seed || pubkey). - std::span ed25519_secret() const { return ubuf().first<64>(); } + std::span ed25519_secret() const& { return ubuf().first<64>(); } + std::span ed25519_secret() const&& = delete; /// The 32-byte X25519 secret key derived from the account seed. This is also the clamped /// private scalar of the Ed25519 key, usable for advanced scalar-multiplication operations. - std::span x25519_key() const { return ubuf().last<32>(); } + std::span x25519_key() const& { return ubuf().last<32>(); } + std::span x25519_key() const&& = delete; }; AccountSeedAccess account_seed() { diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 408998888..594ee9bc7 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -234,6 +234,60 @@ DecryptV2Result decrypt_incoming_v2( std::span account_pfs_mlkem768_sec, std::span ciphertext); +/// API: crypto/encrypt_for_recipient_v2_nopfs +/// +/// Encrypts a v2 Session DM using the non-PFS fallback (long-term X25519 DH only). +/// +/// Wire format is identical to `encrypt_for_recipient_v2`: +/// 0x00 0x02 | ki (2B) | E (32B) | outer_ct (1088B) | xchacha20poly1305_ciphertext +/// +/// but `ki` and `outer_ct` carry no key material — they are random bytes used only to make +/// non-PFS messages externally indistinguishable from PFS+PQ messages. The actual shared +/// secret is derived as: +/// ss = eR (X25519 DH with ephemeral secret e and recipient long-term pubkey R) +/// ss = SHA3-256(ss || R || E || "SessionV2NonPFS") +/// k,n = SHAKE256("SessionV2NonPFSSS", ss) → 32-byte key + 24-byte nonce +/// +/// Inputs: +/// - `sender_ed25519_privkey` -- sender's 32-byte seed or 64-byte Ed25519 secret key +/// - `recipient_session_id` -- 33-byte 0x05-prefixed long-term X25519 pubkey of the recipient +/// - `content` -- the plaintext message content to encrypt +/// - `pro_signature` -- optional 64-byte Session Pro signature +/// +/// Outputs: +/// - Wire-format v2 ciphertext (non-PFS). +/// - Throws on key or encryption failure. +std::vector encrypt_for_recipient_v2_nopfs( + const Ed25519PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span content, + std::optional> pro_signature = std::nullopt); + +/// API: crypto/decrypt_incoming_v2_nopfs +/// +/// Decrypts a v2 Session DM using the non-PFS fallback (long-term X25519 DH only). +/// +/// Performs the inverse of `encrypt_for_recipient_v2_nopfs`. The `ki` and ML-KEM fields in +/// the wire format are ignored; only the ephemeral pubkey E and the recipient's long-term +/// X25519 key pair are used. +/// +/// Inputs: +/// - `recipient_session_id` -- 33-byte 0x05-prefixed Session ID of the recipient (used to +/// verify the inner Ed25519 message signature). +/// - `x25519_sec` -- 32-byte long-term X25519 secret key of the recipient. +/// - `x25519_pub` -- 32-byte long-term X25519 public key of the recipient. +/// - `ciphertext` -- wire-format v2 ciphertext. +/// +/// Outputs: +/// - `DecryptV2Result` with the decrypted content, sender Session ID, and optional Pro sig. +/// - Throws `DecryptV2Error` if AEAD authentication fails (wrong key — try PFS path instead). +/// - Throws `std::runtime_error` for unrecoverable errors (invalid format, signature failure). +DecryptV2Result decrypt_incoming_v2_nopfs( + std::span recipient_session_id, + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext); + static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// API: crypto/encrypt_for_group diff --git a/src/core.cpp b/src/core.cpp index 20348b4c3..b97b4a629 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -579,10 +579,6 @@ void Core::_handle_direct_messages(std::span messages) { } auto keys = devices.active_account_keys(ki); - if (keys.empty()) { - fire_fail(msg, MessageDecryptFailure::no_pfs_key); - continue; - } bool decrypted = false; for (auto& key : keys) { @@ -597,6 +593,7 @@ void Core::_handle_direct_messages(std::span messages) { out.version = 2; out.content = std::move(result.content); out.pro_signature = result.pro_signature; + out.pfs_encrypted = true; fire_received(std::move(out)); decrypted = true; break; @@ -606,12 +603,33 @@ void Core::_handle_direct_messages(std::span messages) { // Unrecoverable structural error in the message itself. log::warning(cat, "v2 direct message format error: {}", e.what()); fire_fail(msg, MessageDecryptFailure::bad_format); - decrypted = true; // Prevent the generic "no key worked" fallback. + decrypted = true; // Prevent the non-PFS fallback attempt. break; } } - if (!decrypted) - fire_fail(msg, MessageDecryptFailure::decrypt_failed); + if (!decrypted) { + // No PFS key matched; try the non-PFS fallback (sender had no PFS keys). + try { + auto result = + decrypt_incoming_v2_nopfs(session_id, x25519_sec, x25519_pub, data); + ReceivedMessage out; + out.hash = msg.hash; + out.timestamp = msg.timestamp; + out.expiry = msg.expiry; + out.sender_session_id = result.sender_session_id; + out.version = 2; + out.content = std::move(result.content); + out.pro_signature = result.pro_signature; + // pfs_encrypted remains false (default) + fire_received(std::move(out)); + } catch (const DecryptV2Error&) { + // Non-PFS fallback also failed: message cannot be read. + fire_fail(msg, MessageDecryptFailure::no_pfs_key); + } catch (const std::exception& e) { + log::warning(cat, "v2 direct message format error: {}", e.what()); + fire_fail(msg, MessageDecryptFailure::bad_format); + } + } } else { // Version 1: protobuf WebSocketMessage → Envelope wire format. diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 338a4ea69..5ebdf7a7b 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -102,19 +102,18 @@ static void v2_check_header(std::span ciphertext) { throw std::runtime_error{"v2 ciphertext has wrong version prefix"}; } -// X-Wing KDF: computes ss = SHA3-256(ssm||ssx||E||X||V2_XWING_LABEL) from key_buf (=ssm) and -// nonce_buf (=ssx), then squeezes k (32B) back into key_buf and n (V2_NONCE_SIZE B) into the -// first V2_NONCE_SIZE bytes of nonce_buf, overwriting the shared secrets with derived key material. -// E is the ephemeral X25519 pubkey; X is the account PFS X25519 pubkey. +// X-Wing KDF: computes ss = SHA3-256(ssm||ssx||E||X||V2_XWING_LABEL), then squeezes k (32B) into +// key_buf (overwriting ssm) and n (V2_NONCE_SIZE B) into nonce_out. Callers are responsible for +// storing these outputs in cleared buffers. E is the ephemeral X25519 pubkey; X is the PFS pubkey. static void v2_derive_xwing_key_nonce( - cleared_uc32& key_buf, - cleared_uc32& nonce_buf, + std::span key_buf, + std::span nonce_out, + std::span ssx, std::span E, std::span X) { - auto ss = hash::sha3_256<32>(key_buf, nonce_buf, E, X, V2_XWING_LABEL); - hash::shake256(V2_SS_DOMAIN, ss)( - key_buf, std::span{nonce_buf.data(), V2_NONCE_SIZE}); - sodium_memzero(ss.data(), ss.size()); + cleared_uc32 ss; + hash::sha3_256(ss, key_buf, ssx, E, X, V2_XWING_LABEL); + hash::shake256(V2_SS_DOMAIN, ss)(key_buf, nonce_out); } // Computes the 2-byte Key Indicator Shared Secret (KISS): @@ -236,60 +235,22 @@ std::vector encrypt_for_recipient_deterministic( return result; } -std::vector encrypt_for_recipient_v2( +// Builds and returns a complete v2 DM wire-format ciphertext from already-derived header fields +// and encryption key material. Used by both encrypt_for_recipient_v2 (PFS+PQ) and +// encrypt_for_recipient_v2_nopfs (non-PFS fallback). +static std::vector v2_encrypt_inner( + std::array ki, + std::span E, + std::span outer_ct, + std::span enc_key, + std::span enc_nonce, const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, - std::span recipient_account_x25519, - std::span recipient_account_mlkem768, std::span content, std::optional> pro_signature) { auto sender_ed_pk = sender_ed25519_privkey.pubkey(); - // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) - std::span S{recipient_session_id.data() + 1, 32}; - - // Step 1: Generate ephemeral X25519 keypair e/E - cleared_uc32 e; - uc32 E; - crypto_box_keypair(E.data(), e.data()); - - // Two multi-purpose key buffers — each plays sequential, non-overlapping roles: - // - // enc_key_buf: ML-KEM shared secret ssm (step 4) → SHAKE256-derived enc key k (step 8) - // enc_nonce_buf: eS DH result (step 2) → ML-KEM coins (step 4) → ssx DH result (step 5) - // → SHAKE256-derived enc nonce n in first 24 bytes (step 8) - cleared_uc32 enc_key_buf; - cleared_uc32 enc_nonce_buf; - - // Step 2: KISS = BLAKE2b_2(E || S, key=eS, pers="Session-Msg-KISS") - // eS is the X25519 DH with the long-term key, used only for cheap key indicator obfuscation - auto kiss = v2_kiss(e, E, S, /*encrypting=*/true); - - // Step 3: ki = M[0:2] ⊕ kiss (encrypted key indicator; lets recipient quickly identify key) - std::array ki{ - static_cast(recipient_account_mlkem768[0] ^ kiss[0]), - static_cast(recipient_account_mlkem768[1] ^ kiss[1])}; - - // Step 4: ML-KEM-768 encapsulate: ssₘ, mlkem_ct = Encapsulate(M) - std::array mlkem_ct; - random::fill(enc_nonce_buf); // repurpose enc_nonce_buf as random ML-KEM coins - if (0 != sr_mlkem768_enc_derand( - mlkem_ct.data(), - enc_key_buf.data(), - recipient_account_mlkem768.data(), - enc_nonce_buf.data())) - throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; - - // Step 5: ssx = eX (X25519 DH with account PFS key X, not long-term key S) - if (0 != crypto_scalarmult(enc_nonce_buf.data(), e.data(), recipient_account_x25519.data())) - throw std::runtime_error{"X25519 DH (account key) failed"}; - - // Step 6: X-Wing KDF → enc key k (in enc_key_buf) and enc nonce n (in enc_nonce_buf[0:24]) - v2_derive_xwing_key_nonce(enc_key_buf, enc_nonce_buf, E, recipient_account_x25519); - - // Step 7: Build inner bt-encoded dict directly into the final result buffer. - // // bt_bytes_encoded(n): total bytes to represent an n-byte bt string: decimal digits of n + 1 + // n constexpr auto bt_bytes_encoded = [](size_t n) constexpr -> size_t { @@ -313,7 +274,7 @@ std::vector encrypt_for_recipient_v2( (std::max(V2_MIN_FINAL_SIZE, V2_OUTER_OVERHEAD + inner_dict_size) + 255) & ~size_t{255}; size_t padded_inner_size = final_size - V2_OUTER_OVERHEAD; - // Step 8: Allocate result (zero-initialized so padding bytes are already 0), write header, + // Allocate result (zero-initialized so padding bytes are already 0), write header, // build inner dict directly into result buffer, then encrypt in-place. // (c == m is explicitly supported by libsodium for AEAD functions) std::vector result(final_size, 0); @@ -323,7 +284,7 @@ std::vector encrypt_for_recipient_v2( result[2] = ki[0]; result[3] = ki[1]; std::memcpy(result.data() + 4, E.data(), 32); - std::memcpy(result.data() + 36, mlkem_ct.data(), MLKEM768_CIPHERTEXTBYTES); + std::memcpy(result.data() + 36, outer_ct.data(), MLKEM768_CIPHERTEXTBYTES); { oxenc::bt_dict_producer dict{ @@ -354,13 +315,75 @@ std::vector encrypt_for_recipient_v2( nullptr, 0, nullptr, - enc_nonce_buf.data(), // nonce (24B read from 32B buffer) - enc_key_buf.data())) // key + enc_nonce.data(), // 24-byte nonce + enc_key.data())) // key throw std::runtime_error{"v2 message encryption failed"}; return result; } +std::vector encrypt_for_recipient_v2( + const Ed25519PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span recipient_account_x25519, + std::span recipient_account_mlkem768, + std::span content, + std::optional> pro_signature) { + + // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) + std::span S{recipient_session_id.data() + 1, 32}; + + // Step 1: Generate ephemeral X25519 keypair e/E + cleared_uc32 e; + uc32 E; + crypto_box_keypair(E.data(), e.data()); + + // Three cleared buffers for key material: + // enc_key_buf: ML-KEM shared secret ssm (step 4) → SHAKE256-derived enc key k (step 6) + // ssx_buf: eS DH result (step 2) → ML-KEM coins (step 4) → ssx DH result (step 5) + // enc_nonce: SHAKE256-derived enc nonce n (step 6) + cleared_uc32 enc_key_buf; + cleared_uc32 ssx_buf; + cleared_uchars enc_nonce; + + // Step 2: KISS = BLAKE2b_2(E || S, key=eS, pers="Session-Msg-KISS") + // eS is the X25519 DH with the long-term key, used only for cheap key indicator obfuscation + auto kiss = v2_kiss(e, E, S, /*encrypting=*/true); + + // Step 3: ki = M[0:2] ⊕ kiss (encrypted key indicator; lets recipient quickly identify key) + std::array ki{ + static_cast(recipient_account_mlkem768[0] ^ kiss[0]), + static_cast(recipient_account_mlkem768[1] ^ kiss[1])}; + + // Step 4: ML-KEM-768 encapsulate: ssₘ, mlkem_ct = Encapsulate(M) + std::array mlkem_ct; + random::fill(ssx_buf); // repurpose ssx_buf as random ML-KEM coins + if (0 != sr_mlkem768_enc_derand( + mlkem_ct.data(), + enc_key_buf.data(), + recipient_account_mlkem768.data(), + ssx_buf.data())) + throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; + + // Step 5: ssx = eX (X25519 DH with account PFS key X, not long-term key S) + if (0 != crypto_scalarmult(ssx_buf.data(), e.data(), recipient_account_x25519.data())) + throw std::runtime_error{"X25519 DH (account key) failed"}; + + // Step 6: X-Wing KDF → enc key k (in enc_key_buf) and enc nonce n (in enc_nonce) + v2_derive_xwing_key_nonce(enc_key_buf, enc_nonce, ssx_buf, E, recipient_account_x25519); + + return v2_encrypt_inner( + ki, + E, + mlkem_ct, + enc_key_buf, + enc_nonce, + sender_ed25519_privkey, + recipient_session_id, + content, + pro_signature); +} + std::array decrypt_incoming_v2_prefix( std::span x25519_sec, std::span x25519_pub, @@ -372,32 +395,15 @@ std::array decrypt_incoming_v2_prefix( static_cast(ciphertext[3] ^ kiss[1])}; } -DecryptV2Result decrypt_incoming_v2( +// Decrypts the v2 AEAD payload and parses the inner bt-encoded dict. Used by both +// decrypt_incoming_v2 (PFS+PQ) and decrypt_incoming_v2_nopfs (non-PFS fallback). +// Throws DecryptV2Error on AEAD failure; std::runtime_error on structural/format errors. +static DecryptV2Result v2_aead_decrypt_and_parse( std::span recipient_session_id, - std::span account_pfs_x25519_sec, - std::span account_pfs_x25519_pub, - std::span account_pfs_mlkem768_sec, + std::span key, + std::span nonce, std::span ciphertext) { - v2_check_header(ciphertext); - - auto E = ciphertext.subspan<4, 32>(); - auto mlkem_ct = ciphertext.subspan<36, MLKEM768_CIPHERTEXTBYTES>(); - cleared_uc32 key_buf; // ssm → k - cleared_uc32 nonce_buf; // ssx → n - - // Step 1: ML-KEM-768 decapsulate → shared secret ssm in key_buf - if (0 != sr_mlkem768_dec(key_buf.data(), mlkem_ct.data(), account_pfs_mlkem768_sec.data())) - throw DecryptV2Error{"ML-KEM-768 decapsulation failed"}; - - // Step 2: X25519 DH with account PFS key → shared secret ssx in nonce_buf - if (0 != crypto_scalarmult(nonce_buf.data(), account_pfs_x25519_sec.data(), E.data())) - throw DecryptV2Error{"X25519 DH (account key) failed"}; - - // Step 3: X-Wing KDF → enc key k (in key_buf) and enc nonce n (in nonce_buf[0:24]) - v2_derive_xwing_key_nonce(key_buf, nonce_buf, E, account_pfs_x25519_pub); - - // Step 4: AEAD decrypt the inner payload size_t enc_size = ciphertext.size() - V2_HEADER_SIZE; std::vector plain(enc_size - V2_AEAD_OVERHEAD); if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( @@ -408,15 +414,15 @@ DecryptV2Result decrypt_incoming_v2( enc_size, nullptr, 0, - nonce_buf.data(), - key_buf.data())) + nonce.data(), + key.data())) throw DecryptV2Error{"v2 message decryption failed"}; // Strip zero padding from end (the plaintext was padded to a multiple of 256 bytes) while (!plain.empty() && plain.back() == 0) plain.pop_back(); - // Step 5: Parse the bencoded inner dict + // Parse the bencoded inner dict oxenc::bt_dict_consumer dict{plain}; auto sender_ed_pk = dict.require_span("S"); @@ -455,6 +461,110 @@ DecryptV2Result decrypt_incoming_v2( return result; } +DecryptV2Result decrypt_incoming_v2( + std::span recipient_session_id, + std::span account_pfs_x25519_sec, + std::span account_pfs_x25519_pub, + std::span account_pfs_mlkem768_sec, + std::span ciphertext) { + v2_check_header(ciphertext); + + auto E = ciphertext.subspan<4, 32>(); + auto mlkem_ct = ciphertext.subspan<36, MLKEM768_CIPHERTEXTBYTES>(); + + cleared_uc32 key_buf; // ssm → k + cleared_uc32 ssx_buf; + cleared_uchars nonce; + + // Step 1: ML-KEM-768 decapsulate → shared secret ssm in key_buf + if (0 != sr_mlkem768_dec(key_buf.data(), mlkem_ct.data(), account_pfs_mlkem768_sec.data())) + throw DecryptV2Error{"ML-KEM-768 decapsulation failed"}; + + // Step 2: X25519 DH with account PFS key → shared secret ssx in ssx_buf + if (0 != crypto_scalarmult(ssx_buf.data(), account_pfs_x25519_sec.data(), E.data())) + throw DecryptV2Error{"X25519 DH (account key) failed"}; + + // Step 3: X-Wing KDF → enc key k (in key_buf) and enc nonce n (in nonce) + v2_derive_xwing_key_nonce(key_buf, nonce, ssx_buf, E, account_pfs_x25519_pub); + + return v2_aead_decrypt_and_parse(recipient_session_id, key_buf, nonce, ciphertext); +} + +// Non-PFS fallback key derivation domain labels (private to this translation unit). +// The outer wire format is identical to a PFS+PQ v2 message; only the key derivation differs. +constexpr auto V2_NONPFS_KDF_LABEL = "SessionV2NonPFS"_uc; // SHA3-256 input domain +constexpr auto V2_NONPFS_SS_DOMAIN = "SessionV2NonPFSSS"_uc; // SHAKE256 output domain + +std::vector encrypt_for_recipient_v2_nopfs( + const Ed25519PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span content, + std::optional> pro_signature) { + + // R = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) + auto R = recipient_session_id.last<32>(); + + // Generate ephemeral X25519 keypair e/E + cleared_uc32 e; + uc32 E; + crypto_box_keypair(E.data(), e.data()); + + // ki and the outer "mlkem_ct" slot are random: the message is externally indistinguishable + // from a PFS+PQ v2 message, but carries no actual ML-KEM ciphertext. + std::array ki; + std::array outer_ct; + random::fill(ki); + random::fill(outer_ct); + + // ss = eR, then overwritten in-place with SHA3-256(ss || R || E || V2_NONPFS_KDF_LABEL). + // Using the output-buffer overload writes directly into the cleared buffer. + cleared_uc32 ss; + if (0 != crypto_scalarmult(ss.data(), e.data(), R.data())) + throw std::runtime_error{"X25519 DH (non-PFS) failed"}; + hash::sha3_256(ss, ss, R, E, V2_NONPFS_KDF_LABEL); + + // k, n = SHAKE256(V2_NONPFS_SS_DOMAIN, ss) → 32-byte key + 24-byte nonce + cleared_uc32 enc_key; + cleared_uchars enc_nonce; + hash::shake256(V2_NONPFS_SS_DOMAIN, ss)(enc_key, enc_nonce); + + return v2_encrypt_inner( + ki, + E, + outer_ct, + enc_key, + enc_nonce, + sender_ed25519_privkey, + recipient_session_id, + content, + pro_signature); +} + +DecryptV2Result decrypt_incoming_v2_nopfs( + std::span recipient_session_id, + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext) { + v2_check_header(ciphertext); + + // E = ephemeral X25519 pubkey from bytes 4-35; bytes 36-1123 (fake mlkem_ct) are ignored. + auto E = ciphertext.subspan<4, 32>(); + + // ss = rE, then overwritten in-place with SHA3-256(ss || R || E || V2_NONPFS_KDF_LABEL). + // Using the output-buffer overload writes directly into the cleared buffer. + cleared_uc32 ss; + if (0 != crypto_scalarmult(ss.data(), x25519_sec.data(), E.data())) + throw DecryptV2Error{"X25519 DH (non-PFS) failed"}; + hash::sha3_256(ss, ss, x25519_pub, E, V2_NONPFS_KDF_LABEL); + + // k, n = SHAKE256(V2_NONPFS_SS_DOMAIN, ss) → 32-byte key + 24-byte nonce + cleared_uc32 key; + cleared_uchars nonce; + hash::shake256(V2_NONPFS_SS_DOMAIN, ss)(key, nonce); + + return v2_aead_decrypt_and_parse(recipient_session_id, key, nonce, ciphertext); +} + // Calculate the shared encryption key, sending from blinded sender kS (k = S's blinding factor) to // blinded receiver jR (j = R's blinding factor). // diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index a898d4fd5..bb936c770 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -137,6 +137,7 @@ TEST_CASE("_handle_direct_messages: v2 receive", "[core][dm]") { CHECK(msg.sender_session_id == sender.session_id); CHECK(std::ranges::equal(msg.content, content)); CHECK_FALSE(msg.pro_signature.has_value()); + CHECK(msg.pfs_encrypted); } // ── Failure paths ──────────────────────────────────────────────────────────────────────────────── @@ -202,9 +203,10 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { CHECK(failures[0] == MessageDecryptFailure::no_pfs_key); } - SECTION("v2 key indicator matches but AEAD MAC fails → decrypt_failed") { + SECTION("v2 AEAD MAC corrupted → no_pfs_key") { // Encrypt a valid v2 message for recipient, then corrupt the xchacha ciphertext tail to - // cause MAC authentication failure (DecryptV2Error), exhausting all candidate keys. + // cause MAC authentication failure on both the PFS key loop and the non-PFS fallback. + // Both paths throw DecryptV2Error, so no_pfs_key is fired (nothing could decrypt it). recipient->devices.active_account_keys(); auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); std::array recip_session_id; @@ -224,7 +226,7 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { deliver(std::span{ct}); CHECK(received.empty()); REQUIRE(failures.size() == 1); - CHECK(failures[0] == MessageDecryptFailure::decrypt_failed); + CHECK(failures[0] == MessageDecryptFailure::no_pfs_key); } SECTION("v1 malformed ciphertext → decrypt_failed") { @@ -235,6 +237,178 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { } } +// ── Non-PFS fallback ───────────────────────────────────────────────────────────────────────────── + +TEST_CASE("_handle_direct_messages: v2 non-PFS fallback receive", "[core][dm]") { + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + // Do NOT call active_account_keys() — sender has no PFS keys for this recipient. + + std::array recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + constexpr auto content = "cafebabe"_hex_u; + auto ct = encrypt_for_recipient_v2_nopfs(sender.ed_sk, recip_session_id, content, std::nullopt); + + OwnedMessage om{std::span{ct}, "hash_nopfs", from_epoch_ms(3333), from_epoch_ms(7777)}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + + REQUIRE(failures.empty()); + REQUIRE(received.size() == 1); + const auto& msg = received[0]; + CHECK(msg.hash == "hash_nopfs"); + CHECK(msg.timestamp == from_epoch_ms(3333)); + CHECK(msg.expiry == from_epoch_ms(7777)); + CHECK(msg.version == 2); + CHECK(msg.sender_session_id == sender.session_id); + CHECK(std::ranges::equal(msg.content, content)); + CHECK_FALSE(msg.pro_signature.has_value()); + CHECK_FALSE(msg.pfs_encrypted); +} + +TEST_CASE( + "_handle_direct_messages: v2 non-PFS fallback succeeds when ki collides with PFS key", + "[core][dm]") { + // A non-PFS message whose decrypted ki happens to match the 2-byte ML-KEM prefix of one of + // the recipient's real PFS account keys. The PFS key loop runs but throws DecryptV2Error + // (wrong key derivation); the non-PFS fallback then succeeds. + // + // The ki is XOR-encrypted: wire_ki = plaintext_ki ⊕ kiss, where kiss is derived from the + // ephemeral key pair. decrypt_incoming_v2_prefix recovers plaintext_ki. We construct + // the collision deterministically by patching the wire_ki bytes after encrypting: + // new_wire_ki[i] = wire_ki[i] ⊕ plaintext_ki[i] ⊕ target_ki[i] + // which sets plaintext_ki to target_ki without touching E or the ciphertext body. + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + recipient->devices.active_account_keys(); + + std::array recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + auto seed_access = recipient->globals.account_seed(); + auto x25519_sec = seed_access.x25519_key(); + std::span x25519_pub{recip_session_id.data() + 1, 32}; + + // The target ki is the first 2 bytes of the recipient's active ML-KEM public key. + auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); + std::array target_ki{ + static_cast(mlkem_bytes[0]), static_cast(mlkem_bytes[1])}; + + constexpr auto content = "deadc0de"_hex_u; + auto ct = encrypt_for_recipient_v2_nopfs(sender.ed_sk, recip_session_id, content, std::nullopt); + + // Recover the current plaintext ki so we can XOR it out and XOR the target in. + auto current_ki = decrypt_incoming_v2_prefix(x25519_sec, x25519_pub, ct); + ct[2] ^= current_ki[0] ^ target_ki[0]; + ct[3] ^= current_ki[1] ^ target_ki[1]; + + // Verify the patch: the decrypted ki should now equal target_ki. + REQUIRE(decrypt_incoming_v2_prefix(x25519_sec, x25519_pub, ct) == target_ki); + + OwnedMessage om{std::span{ct}, "hash_ki_collision"}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + + REQUIRE(failures.empty()); + REQUIRE(received.size() == 1); + CHECK(std::ranges::equal(received[0].content, content)); + CHECK_FALSE(received[0].pfs_encrypted); +} + +// ── PFS ki-prefix collision within the loop ────────────────────────────────────────────────────── + +TEST_CASE( + "_handle_direct_messages: PFS decryption succeeds when ki collides within the PFS loop", + "[core][dm]") { + // Verify that when active_account_keys(ki) returns multiple candidates (because two account + // keys share the same 2-byte ML-KEM prefix), the loop continues past a DecryptV2Error on the + // wrong key and succeeds with the correct key. + // + // We find a colliding pair via the birthday paradox: rotating account keys until any two + // generated keys share the same 2-byte ML-KEM prefix. Expected ~321 rotations on average + // (sqrt(pi * 65536 / 2)), each taking < 1 ms, so the total cost is well under a second. + SenderKeys sender; + + std::vector received; + std::vector failures; + callbacks cbs; + cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + cbs.message_decrypt_failed = [&](const SwarmMessage&, MessageDecryptFailure r) { + failures.push_back(r); + }; + + TempCore recipient{cbs}; + + std::array recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + // Generate the first account key and record (prefix → pubkeys) as we rotate. + recipient->devices.active_account_keys(); + + using Prefix = std::array; + using PubkeyPair = std::pair, std::array>; + std::map seen; + + // Record the current active key; returns the earlier key's pubkeys if its prefix collides. + auto record_active = [&]() -> std::optional { + auto [x, m] = TestHelper::active_account_pubkeys(*recipient); + Prefix pfx{static_cast(m[0]), static_cast(m[1])}; + if (auto it = seen.find(pfx); it != seen.end()) + return it->second; + seen.emplace(pfx, PubkeyPair{x, m}); + return std::nullopt; + }; + + record_active(); + + PubkeyPair target_pubkeys; + bool found = false; + for (int i = 0; i < 500'000 && !found; ++i) { + recipient->devices.rotate_account_keys(); + if (auto match = record_active()) { + target_pubkeys = *match; + found = true; + } + } + REQUIRE(found); + + // Encrypt with the earlier (now-rotated) key that shares the active key's ki prefix. + // active_account_keys(ki) returns [active_key (wrong), rotated_target (right)], so Core + // tries the wrong key first (DecryptV2Error), then succeeds with the right one. + constexpr auto content = "feedface"_hex_u; + auto ct = encrypt_for_recipient_v2( + sender.ed_sk, + recip_session_id, + as_uc(target_pubkeys.first), + as_uc(target_pubkeys.second), + content, + std::nullopt); + + OwnedMessage om{std::span{ct}, "hash_ki_pfs_collision"}; + recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); + + REQUIRE(failures.empty()); + REQUIRE(received.size() == 1); + CHECK(std::ranges::equal(received[0].content, content)); + CHECK(received[0].pfs_encrypted); +} + // ── Callback exception safety ──────────────────────────────────────────────────────────────────── TEST_CASE( From 14956c73c0d5db606c1a93fc633ab18f6efad5d0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 23:40:25 -0300 Subject: [PATCH 068/193] Replace snprintf_clamped with fmt-based helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit snprintf_clamped was a C-style variadic wrapper around vsnprintf that existed to work around snprintf's confusing return value semantics. Meanwhile, fmt::format_to_n does *exactly* what is needed, and fmt is available everywhere in our codebase. Plus you get type safety, compile-time format checking, modern formatting, and no varargs. snprintf_clamped should never have been here. fmt is faster, too. Aside from being worse in every way, the snprintf_clamped usage was buggy: all three call sites in pro_backend.cpp passed sizeof(result.error_count) for the clamping size, i.e. sizeof(size_t), i.e. 8 or sometimes 4), instead of sizeof(result.error) (i.e. 256), silently truncating every error message to 7 characters. The call sites also required C-isms like `"%.*s", static_cast(s.size()), s.data()` just to copy a std::string into a buffer — a pattern that was copy-pasted across 13+ call sites. This replaces all the snprintf_clamped usage with two small helpers in internal-util.hpp: - copy_c_str: copies a string_view into a fixed char buffer, truncating and null-terminating (with a char[N] overload that deduces the size to simplify calling verbosity). - format_c_str: fmt::format_to_n into a fixed buffer with proper compile-time fmt format checking This also removes the bool-returning set_error(char*, exception&) overload (and its duplicate in session_network.cpp), replacing the callers with explicit copy_c_str + return statements. The bool return was invariant and was being used to put the actual function return value in some unrelated function. --- include/session/types.h | 18 ----- src/attachments.cpp | 19 +++-- src/internal-util.hpp | 42 +++++++--- src/network/session_network.cpp | 21 ++--- src/pro_backend.cpp | 137 +++++++------------------------- src/session_encrypt.cpp | 13 +-- src/session_protocol.cpp | 75 +++++------------ src/types.cpp | 13 --- 8 files changed, 101 insertions(+), 237 deletions(-) diff --git a/include/session/types.h b/include/session/types.h index 57110522f..6c2d59c84 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -51,24 +51,6 @@ struct arena_t { size_t max; }; -/// A wrapper around snprintf that fixes a common bug in the value the printing function returns -/// when a buffer is passed in. Irrespective of whether a buffer is passed in, snprintf is defined -/// to return: -/// -/// number of characters (not including the terminating null character) which would have been -/// written to buffer if bufsz was ignored -/// -/// This means if the user passes in a buffer to small, the return value is always the amount of -/// bytes required. This means the user always has to calculate the number of bytes written as: -/// -/// size_t bytes_written = min(snprintf(buffer, size, ...), size); -/// -/// This is error prone. This function does the `min(...)` for you so that this function -/// _always_ calculates the actual number of bytes written (not including the null-terminator). If a -/// NULL is passed in then this function returns the number of bytes actually needed to write the -/// entire string (as per normal snprintf behaviour). -int snprintf_clamped(char* buffer, size_t size, char const* fmt, ...); - /// Allocate memory from the basic bump allocating arena. Returns a null pointer on failure. void* arena_alloc(arena_t* arena, size_t bytes); diff --git a/src/attachments.cpp b/src/attachments.cpp index c8365c68a..44eba4ec7 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -935,7 +935,8 @@ LIBSESSION_C_API bool session_attachment_encrypt( sodium_zero_buffer(key.data(), key.size()); return true; } catch (const std::exception& e) { - return set_error(error, e); + copy_c_str(error, 256, e.what()); + return false; } } @@ -959,7 +960,8 @@ LIBSESSION_C_API bool session_attachment_decrypt( std::span{reinterpret_cast(out), *max_size}); return true; } catch (const std::exception& e) { - return set_error(error, e); + copy_c_str(error, 256, e.what()); + return false; } } @@ -987,7 +989,8 @@ LIBSESSION_C_API bool session_attachment_decrypt_alloc( } catch (const std::exception& e) { if (decrypted) std::free(decrypted); - return set_error(error, e); + copy_c_str(error, 256, e.what()); + return false; } } @@ -1021,7 +1024,7 @@ LIBSESSION_C_API size_t session_attachment_encrypt_file( sodium_zero_buffer(key.data(), key.size()); return enc_size; } catch (const std::exception& e) { - set_error(error, e); + copy_c_str(error, 256, e.what()); return 0; } } @@ -1046,7 +1049,7 @@ LIBSESSION_C_API size_t session_attachment_decrypt_file( return std::span{reinterpret_cast(buf), s}; }); } catch (const std::exception& e) { - set_error(error, e); + copy_c_str(error, 256, e.what()); return std::numeric_limits::max(); } } @@ -1066,7 +1069,8 @@ LIBSESSION_C_API bool session_attachment_decrypt_to_file( std::filesystem::path{file_out}); return true; } catch (const std::exception& e) { - return set_error(error, e); + copy_c_str(error, 256, e.what()); + return false; } } @@ -1081,7 +1085,8 @@ LIBSESSION_C_API bool session_attachment_decrypt_file_to_file( std::filesystem::path{file_out}); return true; } catch (const std::exception& e) { - return set_error(error, e); + copy_c_str(error, 256, e.what()); + return false; } } } diff --git a/src/internal-util.hpp b/src/internal-util.hpp index 2525666a3..05c857bc2 100644 --- a/src/internal-util.hpp +++ b/src/internal-util.hpp @@ -1,22 +1,40 @@ #pragma once +#include + +#include #include #include namespace session { -// Used by various C APIs with false returns to write a caught exception message into an error -// buffer (if provided) on the way out. The error buffer is expected to have at least 256 bytes -// available (the exception message will be truncated if longer than 255). -inline bool set_error(char* error, const std::exception& e) { - if (error) { - std::string_view err{e.what()}; - if (err.size() > 255) - err.remove_suffix(err.size() - 255); - std::memcpy(error, err.data(), err.size()); - error[err.size()] = 0; - } +// Copies `msg` into `buf`, truncating if necessary, always null-terminating. Returns the number +// of bytes written INCLUDING the null terminator (i.e. the number of bytes of `buf` that were +// touched), or 0 if buf is null/empty. +inline size_t copy_c_str(char* buf, size_t buf_len, std::string_view msg) { + if (!buf || !buf_len) + return 0; + auto n = std::min(msg.size(), buf_len - 1); + std::memcpy(buf, msg.data(), n); + buf[n++] = 0; + return n; +} + +// Overload for fixed-size char arrays; deduces the buffer size automatically. +template +size_t copy_c_str(char (&buf)[N], std::string_view msg) { + return copy_c_str(buf, N, msg); +} - return false; +// Formats a message directly into a buffer with compile-time format checking. Truncates if +// necessary, always null-terminates. Returns the number of bytes written INCLUDING the null +// terminator, or 0 if buf is null/empty. +template +size_t format_c_str(char* buf, size_t buf_len, fmt::format_string format, Args&&... args) { + if (!buf || !buf_len) + return 0; + auto result = fmt::format_to_n(buf, buf_len - 1, format, std::forward(args)...); + *result.out = '\0'; + return static_cast(result.out - buf) + 1; } } // namespace session diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 088b69998..985b4b448 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -11,6 +11,7 @@ #include #include +#include "../internal-util.hpp" #include "session/blinding.hpp" #include "session/network/backends/session_file_server.hpp" #include "session/network/network_config.hpp" @@ -1129,17 +1130,6 @@ inline std::shared_ptr unbox(network_object* network_ return *static_cast*>(network_->internals); } -inline bool set_error(char* error, const std::exception& e) { - if (!error) - return false; - - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - return false; -} - } // namespace extern "C" { @@ -1239,8 +1229,10 @@ LIBSESSION_C_API session_network_config session_network_config_default() { LIBSESSION_C_API bool session_network_init( network_object** network, const session_network_config* config, char* error) { - if (!network || !config) - return set_error(error, std::invalid_argument{"network or config were null."}); + if (!network || !config) { + session::copy_c_str(error, 256, "network or config were null."); + return false; + } try { // Build the configuration options (ordered this way for the debug logs to make the most @@ -1433,7 +1425,8 @@ LIBSESSION_C_API bool session_network_init( *network = n_object.release(); return true; } catch (const std::exception& e) { - return set_error(error, e); + session::copy_c_str(error, 256, e.what()); + return false; } } diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 10102f96c..bded938b3 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -13,6 +13,8 @@ #include #include +#include "internal-util.hpp" + // clang-format off const session_pro_backend_payment_provider_metadata SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT] = { /*SESSION_PRO_PAYMENT_PROVIDER_NIL*/ { @@ -929,13 +931,7 @@ session_pro_backend_add_pro_payment_request_build_sigs( std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -973,13 +969,7 @@ session_pro_backend_add_pro_payment_request_build_to_json( result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; @@ -1010,13 +1000,7 @@ session_pro_backend_generate_pro_proof_request_build_sigs( std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1044,13 +1028,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1072,13 +1050,7 @@ session_pro_backend_get_pro_details_request_build_sig( std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1100,13 +1072,7 @@ session_pro_backend_get_pro_details_request_build_to_json( result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1135,13 +1101,7 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; @@ -1168,13 +1128,7 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_generate_pro_pr result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; @@ -1197,13 +1151,7 @@ session_pro_backend_get_pro_revocations_request_to_json( result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; @@ -1230,13 +1178,7 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; @@ -1437,27 +1379,22 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { - dest.google_payment_token_count = snprintf_clamped( - dest.google_payment_token, - sizeof(dest.google_payment_token), - src.google_payment_token.data()); - dest.google_order_id_count = snprintf_clamped( - dest.google_order_id, - sizeof(dest.google_order_id), - src.google_order_id.data()); + dest.google_payment_token_count = + session::copy_c_str(dest.google_payment_token, src.google_payment_token) - + 1; + dest.google_order_id_count = + session::copy_c_str(dest.google_order_id, src.google_order_id) - 1; } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: { - dest.apple_original_tx_id_count = snprintf_clamped( - dest.apple_original_tx_id, - sizeof(dest.apple_original_tx_id), - src.apple_original_tx_id.data()); - dest.apple_tx_id_count = snprintf_clamped( - dest.apple_tx_id, sizeof(dest.apple_tx_id), src.apple_tx_id.data()); - dest.apple_web_line_order_id_count = snprintf_clamped( - dest.apple_web_line_order_id, - sizeof(dest.apple_web_line_order_id), - src.apple_web_line_order_id.data()); + dest.apple_original_tx_id_count = + session::copy_c_str(dest.apple_original_tx_id, src.apple_original_tx_id) - + 1; + dest.apple_tx_id_count = session::copy_c_str(dest.apple_tx_id, src.apple_tx_id) - 1; + dest.apple_web_line_order_id_count = + session::copy_c_str( + dest.apple_web_line_order_id, src.apple_web_line_order_id) - + 1; } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { dest.rangeproof_order_id_count = snprintf_clamped( @@ -1517,13 +1454,7 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1564,13 +1495,7 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; } @@ -1603,13 +1528,7 @@ session_pro_backend_set_payment_refund_requested_request_to_json( result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { - const std::string& error = e.what(); - result.error_count = snprintf_clamped( - result.error, - sizeof(result.error_count), - "%.*s", - static_cast(error.size()), - error.data()); + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } return result; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 5ebdf7a7b..599897918 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -24,6 +24,7 @@ #include #include +#include "internal-util.hpp" #include "session/blinding.hpp" #include "session/clock.hpp" #include "session/hash.hpp" @@ -1396,11 +1397,7 @@ LIBSESSION_C_API session_encrypt_group_message session_encrypt_for_group( .ciphertext = session::span_u8_copy_or_throw(result_cpp.data(), result_cpp.size()), }; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = - snprintf_clamped( - error, error_len, "%.*s", (int)error_cpp.size(), error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } return result; } @@ -1509,11 +1506,7 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess std::memcpy(result.session_id, result_cpp.session_id.data(), sizeof(result.session_id)); break; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = - snprintf_clamped( - error, error_len, "%.*s", (int)error_cpp.size(), error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } } return result; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 17864aa46..c66ff060a 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -15,6 +15,7 @@ #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" +#include "internal-util.hpp" #include "session/export.h" // clang-format off @@ -1072,14 +1073,7 @@ static session_protocol_encoded_for_destination c_encode_impl( .ciphertext = span_u8_copy_or_throw(ciphertext.data(), ciphertext.size()), }; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "%.*s", - static_cast(error_cpp.size()), - error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } return result; } @@ -1196,13 +1190,11 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( array_uc32_from_ptr_result pro_backend_pubkey_cpp = array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); if (!pro_backend_pubkey_cpp.success) { - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "Invalid pro_backend_pubkey: Key was " - "set but was not 32 bytes, was: %zu", - pro_backend_pubkey_len) + - 1; + result.error_len_incl_null_terminator = format_c_str( + error, + error_len, + "Invalid pro_backend_pubkey: Key was set but was not 32 bytes, was: {}", + pro_backend_pubkey_len); return result; } @@ -1213,13 +1205,11 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( std::span{ keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; } else if (keys->group_ed25519_pubkey.size) { - result.error_len_incl_null_terminator = - snprintf_clamped( - error, - error_len, - "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: %zu", - keys->group_ed25519_pubkey.size) + - 1; + result.error_len_incl_null_terminator = format_c_str( + error, + error_len, + "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: {}", + keys->group_ed25519_pubkey.size); return result; } @@ -1236,20 +1226,13 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( result.success = true; break; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "%.*s", - static_cast(error_cpp.size()), - error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } } if (keys->decrypt_keys_len == 0) { result.error_len_incl_null_terminator = - snprintf_clamped(error, error_len, "No keys ed25519_privkeys were provided") + 1; + copy_c_str(error, error_len, "No keys ed25519_privkeys were provided"); } // Marshall into c type @@ -1257,15 +1240,8 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( result.content_plaintext = session::span_u8_copy_or_throw( result_cpp.content_plaintext.data(), result_cpp.content_plaintext.size()); } catch (const std::exception& e) { - std::string error_cpp = e.what(); result.success = false; - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "%.*s", - static_cast(error_cpp.size()), - error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } result.envelope = envelope_from_cpp(result_cpp.envelope); @@ -1317,13 +1293,11 @@ session_protocol_decoded_community_message session_protocol_decode_for_community array_uc32_from_ptr_result pro_backend_pubkey_cpp = array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); if (!pro_backend_pubkey_cpp.success) { - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "Invalid pro_backend_pubkey: Key was " - "set but was not 32 bytes, was: %zu", - pro_backend_pubkey_len) + - 1; + result.error_len_incl_null_terminator = format_c_str( + error, + error_len, + "Invalid pro_backend_pubkey: Key was set but was not 32 bytes, was: {}", + pro_backend_pubkey_len); return result; } @@ -1342,15 +1316,8 @@ session_protocol_decoded_community_message session_protocol_decode_for_community result.pro = decoded_pro_from_cpp(*decoded.pro); result.success = true; } catch (const std::exception& e) { - std::string error_cpp = e.what(); result.success = false; - result.error_len_incl_null_terminator = snprintf_clamped( - error, - error_len, - "%.*s", - static_cast(error_cpp.size()), - error_cpp.data()) + - 1; + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); } return result; diff --git a/src/types.cpp b/src/types.cpp index 8f92b2017..cc8ec5e4a 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1,7 +1,6 @@ #include #include -#include #include namespace session { @@ -40,18 +39,6 @@ string8 string8_copy_or_throw(const void* data, size_t size) { } }; // namespace session -int snprintf_clamped(char* buffer, size_t size, char const* fmt, ...) { - va_list args; - va_start(args, fmt); - int bytes_required_not_incl_null = vsnprintf(buffer, size, fmt, args); - va_end(args); - - int result = bytes_required_not_incl_null; - if (buffer && size && bytes_required_not_incl_null >= (size - 1)) - result = size - 1; - return result; -} - void* arena_alloc(arena_t* arena, size_t bytes) { void* result = nullptr; size_t new_size = arena->size + bytes; From 55cc134cec969656e2b100c4d8e9c7c218754865 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 22:47:53 -0300 Subject: [PATCH 069/193] Add "v2" DM construction via Core This adds methods and callbacks to the Core object to support sending DMs through it, with "v2" direct messages used if the recipient has published PFS+PQ support (and thus v2 support). This supports both using a built-in Network object, and supplying an external callback to send the encrypted DM (which would be either v2 PFS, v2 no-pfs, or v1, depending on flags and whether PFS+PQ support was found). For both, the input is a serialized protobuf "Content", and the message that gets sent is padded, encrypted, and encoded as required (as v2 or v1), including (for v1) the extra 17 layers (approximately) of protobuf. This also fixes some bugs in the existing v2 helper methods that were taking a pre-existing Pro signature (which is impossible, because the caller doesn't know what to sign) and should instead take the key to sign with. Additionally fixed the new and existing API to take a proper sys_ms timepoint rather than abusing std::chrono::milliseconds as a timestamp. Adds unit tests for DM sending. --- include/session/core.hpp | 90 ++++++- include/session/core/callbacks.hpp | 48 +++- include/session/session_encrypt.hpp | 12 +- include/session/session_protocol.hpp | 2 +- src/core.cpp | 376 +++++++++++++++++++++++---- src/session_encrypt.cpp | 41 ++- src/session_protocol.cpp | 6 +- tests/CMakeLists.txt | 1 + tests/test_dm_receive.cpp | 7 +- tests/test_dm_send.cpp | 325 +++++++++++++++++++++++ tests/test_helper.hpp | 14 + tests/test_session_encrypt.cpp | 12 +- 12 files changed, 855 insertions(+), 79 deletions(-) create mode 100644 tests/test_dm_send.cpp diff --git a/include/session/core.hpp b/include/session/core.hpp index f79010f48..0d1b9b3d3 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -203,6 +205,7 @@ class Core { void init(); // Polling-related members and methods + std::chrono::milliseconds _poll_interval = 20s; std::shared_ptr _poll_ticker; void _update_polling(); void _poll(); @@ -215,7 +218,57 @@ class Core { void _handle_direct_messages(std::span messages); // Handles a PFS fetch response - void _handle_pfs_response(std::span sid, std::string body); + void _handle_pfs_response(std::span sid, std::string body); + + // Stores PFS keys for a remote session_id in the pfs_key_cache. Returns true if the keys + // differ from the previously cached entry (or no entry existed). + bool _store_pfs_keys( + std::span session_id, + std::span x25519_pub, + std::span mlkem768_pub); + + // Stores a NAK (no keys found) for a remote session_id in the pfs_key_cache. + void _store_pfs_nak(std::span session_id); + + // Monotonic message ID counter for send_dm(). + int64_t _next_message_id{1}; + + // Queued sends waiting for a PFS key fetch to complete. + struct PendingSend { + int64_t id; + std::array recipient; + std::vector content; + sys_ms sent_timestamp; + std::optional pro_privkey; + std::chrono::milliseconds ttl; + bool force_v2; + }; + std::vector _pending_sends; + + // Drains pending sends whose PFS key fetch has completed. + void _flush_pending_sends(std::span session_id); + + // Encrypts, envelopes, and dispatches a single DM. Called from send_dm() and from + // _flush_pending_sends() when a queued send is ready. Fires the message_send_status + // callback on completion. + void _do_send_dm( + int64_t message_id, + std::span recipient, + std::span content, + sys_ms sent_timestamp, + const OptionalEd25519PrivKeySpan& pro_privkey, + std::chrono::milliseconds ttl, + bool force_v2); + + // Dispatches a fully-encoded payload to a swarm for storage. Tries the send_to_swarm + // callback first; falls back to the attached network object; fires on_complete with the + // outcome. Throws std::logic_error if neither delivery path is available. + void _send_to_swarm( + std::span dest_pubkey, + config::Namespace ns, + std::vector payload, + std::chrono::milliseconds ttl, + std::function on_complete); // Extracts an option of type T from a pack; returns the first match wrapped in optional, or // nullopt if not present. Mirrors the same helper in session::sqlite::Database. @@ -291,6 +344,41 @@ class Core { /// PFS_KEY_NAK_DURATION (1h). PfsKeyStatus prefetch_pfs_keys(std::span session_id); + /// Sets the polling interval used when a network object is attached. The default is 20s. + /// Takes effect by replacing the active ticker (if any); safe to call at any time. + void set_poll_interval(std::chrono::milliseconds interval); + + /// Encrypt and send a direct message to the given recipient. + /// + /// Returns a unique message_id that will later be reported via the message_send_status + /// callback. + /// + /// Version selection: + /// - If fresh/stale PFS+PQ account keys are cached for the recipient, a v2 PFS message is + /// sent. + /// - If no keys are cached (NAK) and force_v2 is false, falls back to a v1 message. + /// - If no keys are cached and force_v2 is true, sends a v2 non-PFS message. + /// - If a key fetch is in progress, the send is queued and dispatched when the fetch + /// completes, using the above rules. + /// + /// Parameters: + /// - recipient_session_id -- 33-byte 0x05-prefixed session ID of the recipient + /// - content -- serialised SessionProtos::Content protobuf (the inner plaintext) + /// - sent_timestamp -- the message timestamp as a time point (should match sigTimestamp + /// inside the Content protobuf); used in the v1 Envelope when falling back to v1 + /// - pro_privkey -- optional Session Pro rotating Ed25519 private key (32-byte seed or + /// 64-byte libsodium key). When provided, a Pro signature is attached to the message. + /// - ttl -- time-to-live for the stored message; defaults to 14 days + /// - force_v2 -- when true, never fall back to v1; use v2 non-PFS if no PFS keys are + /// available + int64_t send_dm( + std::span recipient_session_id, + std::span content, + sys_ms sent_timestamp, + const OptionalEd25519PrivKeySpan& pro_privkey = std::nullopt, + std::chrono::milliseconds ttl = 14 * 24h, + bool force_v2 = false); + /// Returns the optional network interface, if set. const std::shared_ptr& network() const { return _network; } diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 063444462..8a60b8582 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include #include +#include #include #include #include @@ -54,6 +56,19 @@ struct ReceivedMessage { ///< false for v1 messages and v2 non-PFS fallback messages. }; +/// Status of a send operation initiated by Core::send_dm(). +enum class MessageSendStatus { + awaiting_keys, ///< Waiting for a PFS+PQ pubkey fetch to complete before encrypting. + sending, ///< Encryption complete; the store request has been dispatched. + retrying, ///< A previous send attempt failed; retrying. (Not yet implemented: + ///< currently a failed send goes directly to network_error. TODO: + ///< implement automatic retry with a maximum retry count.) + success, ///< The store request was accepted by a swarm node. + network_error, ///< The swarm lookup or store request failed (terminal). + no_network, ///< No send_to_swarm callback is set and no network object is attached. + encrypt_failed, ///< Encryption failed (should not normally happen). +}; + /// Struct holding application callbacks to fire when libsession Core events happen to allow the /// Core object to fire into the application front-end. struct callbacks { @@ -130,7 +145,7 @@ struct callbacks { /// Parameters: /// - session_id -- 33-byte session ID (0x05 prefix + X25519 pubkey) of the remote user /// - result -- the outcome of the fetch: new_key, unchanged, not_found, or failed - std::function session_id, PfsKeyFetch result)> + std::function session_id, PfsKeyFetch result)> pfs_keys_fetched; /// Callback invoked when a one-to-one message from Namespace::Default is successfully @@ -154,6 +169,37 @@ struct callbacks { /// - reason -- why decryption failed std::function message_decrypt_failed; + + /// Application-provided swarm store function. If set, Core calls this to deliver outgoing + /// messages instead of using its attached network object. + /// + /// The implementation MUST call on_stored with success (true) or failure (false) when the + /// store operation completes or fails. + /// + /// Parameters: + /// - recipient_pubkey -- 33-byte (0x05-prefixed) session ID whose swarm should receive the + /// message + /// - ns -- the swarm namespace to store into (e.g. Namespace::Default for DMs) + /// - payload -- the fully-encoded message bytes (envelope-wrapped, encrypted) + /// - ttl -- requested time-to-live for the stored message + /// - on_stored -- callback that MUST be invoked with the outcome + std::function recipient_pubkey, + config::Namespace ns, + std::vector payload, + std::chrono::milliseconds ttl, + std::function on_stored)> + send_to_swarm; + + /// Callback fired as a send operation initiated by Core::send_dm() progresses. This is + /// typically invoked multiple times for a single message — once or more for intermediate + /// states (awaiting_keys, sending, retrying) followed by a terminal state (success, + /// network_error, no_network, or encrypt_failed). + /// + /// Parameters: + /// - message_id -- the value returned by the originating send_dm() call + /// - status -- the current state of the send + std::function message_send_status; }; } // namespace session::core diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 594ee9bc7..0fd37becf 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -142,8 +142,9 @@ std::vector encrypt_for_blinded_recipient( /// - `recipient_account_x25519` -- 32-byte recently-fetched PFS account X25519 pubkey (X) /// - `recipient_account_mlkem768` -- 1184-byte recently-fetched PFS account ML-KEM-768 pubkey (M) /// - `content` -- the plaintext message content to encrypt (typically a serialized protobuf) -/// - `pro_signature` -- optional 64-byte Session Pro Ed25519 signature. Pass nullopt when the -/// sender is not using Session Pro features. +/// - `pro_ed25519_privkey` -- optional Session Pro rotating Ed25519 private key (32-byte seed or +/// 64-byte libsodium key). When provided, a `~P` signature is appended to the inner bt-dict, +/// signing all preceding dict content. Pass nullopt / omit when not using Session Pro. /// /// Outputs: /// - The encrypted v2 ciphertext to send to the swarm. @@ -154,7 +155,7 @@ std::vector encrypt_for_recipient_v2( std::span recipient_account_x25519, std::span recipient_account_mlkem768, std::span content, - std::optional> pro_signature = std::nullopt); + const OptionalEd25519PrivKeySpan& pro_ed25519_privkey = std::nullopt); /// Exception thrown when a v2 message could not be decrypted with a given account key. The /// caller should catch this and try the next candidate key. Other exceptions (e.g., @@ -252,7 +253,8 @@ DecryptV2Result decrypt_incoming_v2( /// - `sender_ed25519_privkey` -- sender's 32-byte seed or 64-byte Ed25519 secret key /// - `recipient_session_id` -- 33-byte 0x05-prefixed long-term X25519 pubkey of the recipient /// - `content` -- the plaintext message content to encrypt -/// - `pro_signature` -- optional 64-byte Session Pro signature +/// - `pro_ed25519_privkey` -- optional Session Pro rotating Ed25519 private key. When provided, +/// a `~P` signature is appended to the inner bt-dict. Pass nullopt / omit when not using Pro. /// /// Outputs: /// - Wire-format v2 ciphertext (non-PFS). @@ -261,7 +263,7 @@ std::vector encrypt_for_recipient_v2_nopfs( const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, std::span content, - std::optional> pro_signature = std::nullopt); + const OptionalEd25519PrivKeySpan& pro_ed25519_privkey = std::nullopt); /// API: crypto/decrypt_incoming_v2_nopfs /// diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index b967bbcbe..6f2a23711 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -383,7 +383,7 @@ std::vector pad_message(std::span payload); std::vector encode_dm_v1( std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, - std::chrono::milliseconds sent_timestamp, + sys_ms sent_timestamp, std::span recipient_pubkey, const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey = std::nullopt); diff --git a/src/core.cpp b/src/core.cpp index b97b4a629..116aaa5eb 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -98,13 +98,23 @@ void Core::set_network(std::shared_ptr network) { void Core::_update_polling() { if (_network && !_poll_ticker) { - _poll_ticker = _loop->call_every(20s, [this] { _poll(); }); + _poll_ticker = _loop->call_every(_poll_interval, [this] { _poll(); }); } else if (!_network && _poll_ticker) { _poll_ticker->stop(); _poll_ticker.reset(); } } +void Core::set_poll_interval(std::chrono::milliseconds interval) { + _poll_interval = interval; + if (_poll_ticker) { + _poll_ticker->stop(); + _poll_ticker.reset(); + } + if (_network) + _poll_ticker = _loop->call_every(_poll_interval, [this] { _poll(); }); +} + void Core::_poll() { auto net = _network; if (!net) @@ -298,9 +308,8 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ throw std::logic_error{"prefetch_pfs_keys called without a network object"}; // One copy of session_id for async use; subsequently moved into lambdas. - // sid must be unsigned char because xed25519::verify requires it for the x25519 pubkey span. - std::array sid; - std::ranges::copy(session_id, sid.begin()); + std::array sid; + std::memcpy(sid.data(), session_id.data(), 33); // Skip the fetch if the cached entry is still fresh, or a recent NAK suppresses retrying. // Otherwise determine whether we have a stale (but usable) key or no key at all. @@ -404,7 +413,54 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ return status; } -void Core::_handle_pfs_response(std::span sid, std::string body) { +bool Core::_store_pfs_keys( + std::span session_id, + std::span x25519_pub, + std::span mlkem768_pub) { + auto now_s = epoch_seconds(clock_now_s()); + auto conn = db.conn(); + SQLite::Transaction tx{conn.sql}; + + bool is_unchanged = conn.prepared_maybe_get( + R"( +SELECT 1 FROM pfs_key_cache +WHERE session_id = ? AND pubkey_x25519 = ? AND pubkey_mlkem768 = ? +)", + session_id, + x25519_pub, + mlkem768_pub) + .has_value(); + + conn.prepared_exec( + R"( +INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) +VALUES (?, ?, NULL, ?, ?) +ON CONFLICT(session_id) DO UPDATE SET + fetched_at = excluded.fetched_at, + pubkey_x25519 = excluded.pubkey_x25519, + pubkey_mlkem768 = excluded.pubkey_mlkem768 +)", + session_id, + now_s, + x25519_pub, + mlkem768_pub); + tx.commit(); + return !is_unchanged; +} + +void Core::_store_pfs_nak(std::span session_id) { + auto now_s = epoch_seconds(clock_now_s()); + db.conn().prepared_exec( + R"( +INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) +VALUES (?, NULL, ?, NULL, NULL) +ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at +)", + session_id, + now_s); +} + +void Core::_handle_pfs_response(std::span sid, std::string body) { try { auto json = nlohmann::json::parse(body); auto msgs_it = json.find("messages"); @@ -444,8 +500,7 @@ void Core::_handle_pfs_response(std::span sid, std::str in.require_signature( "~", [&x25519_pub]( - std::span b, - std::span sig) { + std::span b, std::span sig) { if (!xed25519::verify(sig, x25519_pub, b)) throw std::runtime_error{"signature verification failed"}; }); @@ -460,63 +515,290 @@ void Core::_handle_pfs_response(std::span sid, std::str } } - auto now_s = epoch_seconds(clock_now_s()); - if (!pk_x25519 || !pk_mlkem768) { log::debug( cat, "prefetch_pfs_keys: no valid account pubkey message " "found in response"); - // Record a NAK. If a valid entry already exists, update only - // nak_at and leave fetched_at and pubkeys untouched. - db.conn().prepared_exec( - R"( -INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) -VALUES (?, NULL, ?, NULL, NULL) -ON CONFLICT(session_id) DO UPDATE SET nak_at = excluded.nak_at -)", - sid, - now_s); + _store_pfs_nak(sid); if (callbacks.pfs_keys_fetched) callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); return; } - auto conn = db.conn(); - SQLite::Transaction tx{conn.sql}; - - bool is_unchanged = conn.prepared_maybe_get( - R"( -SELECT 1 FROM pfs_key_cache -WHERE session_id = ? AND pubkey_x25519 = ? AND pubkey_mlkem768 = ? -)", - sid, - *pk_x25519, - *pk_mlkem768) - .has_value(); - - conn.prepared_exec( - R"( -INSERT INTO pfs_key_cache (session_id, fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768) -VALUES (?, ?, NULL, ?, ?) -ON CONFLICT(session_id) DO UPDATE SET - fetched_at = excluded.fetched_at, - pubkey_x25519 = excluded.pubkey_x25519, - pubkey_mlkem768 = excluded.pubkey_mlkem768 -)", - sid, - now_s, - *pk_x25519, - *pk_mlkem768); - tx.commit(); + bool changed = _store_pfs_keys(sid, *pk_x25519, *pk_mlkem768); if (callbacks.pfs_keys_fetched) callbacks.pfs_keys_fetched( - sid, is_unchanged ? PfsKeyFetch::unchanged : PfsKeyFetch::new_key); + sid, changed ? PfsKeyFetch::new_key : PfsKeyFetch::unchanged); } catch (const std::exception& e) { log::warning(cat, "Failed to process PFS key fetch response: {}", e.what()); } } +void Core::_send_to_swarm( + std::span dest_pubkey, + config::Namespace ns, + std::vector payload, + std::chrono::milliseconds ttl, + std::function on_complete) { + if (callbacks.send_to_swarm) { + callbacks.send_to_swarm(dest_pubkey, ns, std::move(payload), ttl, std::move(on_complete)); + return; + } + + auto net = _network; + if (!net) + throw std::logic_error{"_send_to_swarm: no send_to_swarm callback and no network object"}; + + // Build signed store request. + auto session_id_hex = oxenc::to_hex(globals.session_id()); + auto ed25519_hex = globals.pubkey_ed25519().hex(); + auto ns_val = static_cast(ns); + auto now_ms = epoch_ms(clock_now_ms()); + + std::string to_sign = "store{}{}"_format(ns_val, now_ms); + std::array sig; + { + auto seed = globals.account_seed(); + crypto_sign_ed25519_detached( + sig.data(), + nullptr, + reinterpret_cast(to_sign.data()), + to_sign.size(), + seed.ed25519_secret().data()); + } + + nlohmann::json params = { + {"pubkey", session_id_hex}, + {"pubkey_ed25519", ed25519_hex}, + {"namespace", ns_val}, + {"data", oxenc::to_base64(payload)}, + {"timestamp", now_ms}, + {"sig_timestamp", now_ms}, + {"signature", oxenc::to_base64(sig)}, + {"ttl", ttl.count()}, + }; + + auto body = to_vector(params.dump()); + + // Resolve the recipient's swarm and send. + network::x25519_pubkey x25519_pub; + std::memcpy(x25519_pub.data(), dest_pubkey.data() + 1, 32); + + net->get_swarm( + x25519_pub, + false, + [net, body = std::move(body), on_complete = std::move(on_complete)]( + auto, auto swarm) mutable { + if (swarm.empty()) { + if (on_complete) + on_complete(false); + return; + } + net->send_request( + network::Request{ + swarm.front(), + "store", + std::move(body), + network::RequestCategory::standard_small, + 20s}, + [on_complete = std::move(on_complete)]( + bool success, bool, int16_t, auto, auto) { + if (on_complete) + on_complete(success); + }); + }); +} + +void Core::_do_send_dm( + int64_t message_id, + std::span recipient, + std::span content, + sys_ms sent_timestamp, + const OptionalEd25519PrivKeySpan& pro_privkey, + std::chrono::milliseconds ttl, + bool force_v2) { + auto fire_status = [&](MessageSendStatus status) { + if (callbacks.message_send_status) { + try { + callbacks.message_send_status(message_id, status); + } catch (const std::exception& e) { + log::warning(cat, "message_send_status callback threw: {}", e.what()); + } + } + }; + + // TODO: remove these casts once the encrypt/encode APIs are migrated to std::byte. + std::span recipient_uc{ + reinterpret_cast(recipient.data()), 33}; + std::span content_uc{ + reinterpret_cast(content.data()), content.size()}; + + // Look up cached PFS keys for the recipient. + using X = sqlite::blob_guts>; + using M = sqlite::blob_guts>; + auto row = db.conn() + .prepared_maybe_get< + std::optional, + std::optional, + std::optional, + std::optional>( + "SELECT fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768" + " FROM pfs_key_cache WHERE session_id = ?", + recipient_uc); + + const std::array* pfs_x25519 = nullptr; + const std::array* pfs_mlkem768 = nullptr; + if (row) { + auto& [fetched_at, nak_at, pk_x, pk_m] = *row; + if (fetched_at && pk_x && pk_m) { + pfs_x25519 = &static_cast&>(*pk_x); + pfs_mlkem768 = &static_cast&>(*pk_m); + } + } + + // Encrypt the message. v2 (PFS or nopfs) produces the complete wire format directly + // (0x00 0x02 | ki | E | mlkem_ct | encrypted_inner) — no protobuf wrapping. v1 uses + // encode_dm_v1 which wraps in Envelope + WebSocketMessage protobufs. + std::vector encrypted; + try { + auto seed = globals.account_seed(); + auto ed_sec = seed.ed25519_secret(); + + if (pfs_x25519) + encrypted = encrypt_for_recipient_v2( + ed_sec, recipient_uc, *pfs_x25519, *pfs_mlkem768, content_uc, pro_privkey); + else if (force_v2) + encrypted = + encrypt_for_recipient_v2_nopfs(ed_sec, recipient_uc, content_uc, pro_privkey); + else + encrypted = encode_dm_v1(content_uc, ed_sec, sent_timestamp, recipient_uc, pro_privkey); + } catch (const std::exception& e) { + log::warning(cat, "send_dm: encryption failed for message {}: {}", message_id, e.what()); + fire_status(MessageSendStatus::encrypt_failed); + return; + } + + std::vector payload(encrypted.size()); + std::memcpy(payload.data(), encrypted.data(), encrypted.size()); + + // Dispatch to swarm. + fire_status(MessageSendStatus::sending); + try { + _send_to_swarm( + recipient, + config::Namespace::Default, + std::move(payload), + ttl, + [this, message_id](bool success) { + if (callbacks.message_send_status) { + try { + callbacks.message_send_status( + message_id, + success ? MessageSendStatus::success + : MessageSendStatus::network_error); + } catch (const std::exception& e) { + log::warning(cat, "message_send_status callback threw: {}", e.what()); + } + } + }); + } catch (const std::logic_error&) { + fire_status(MessageSendStatus::no_network); + } +} + +void Core::_flush_pending_sends(std::span session_id) { + auto it = _pending_sends.begin(); + while (it != _pending_sends.end()) { + if (std::ranges::equal(it->recipient, session_id)) { + auto pending = std::move(*it); + it = _pending_sends.erase(it); + _do_send_dm( + pending.id, + pending.recipient, + pending.content, + pending.sent_timestamp, + pending.pro_privkey ? OptionalEd25519PrivKeySpan{*pending.pro_privkey} + : OptionalEd25519PrivKeySpan{}, + pending.ttl, + pending.force_v2); + } else { + ++it; + } + } +} + +int64_t Core::send_dm( + std::span recipient_session_id, + std::span content, + sys_ms sent_timestamp, + const OptionalEd25519PrivKeySpan& pro_privkey, + std::chrono::milliseconds ttl, + bool force_v2) { + auto id = _next_message_id++; + + // Reinterpret for the DB query which still takes unsigned char. + std::span recipient_uc{ + reinterpret_cast(recipient_session_id.data()), 33}; + + // Check cache state to decide whether we can send immediately or must queue. + auto conn = db.conn(); + auto row = conn.prepared_maybe_get, std::optional>( + "SELECT fetched_at, nak_at FROM pfs_key_cache WHERE session_id = ?", recipient_uc); + + bool have_cached_key = false; + bool is_nak = false; + + if (row) { + auto& [fetched_at, nak_at] = *row; + if (fetched_at) + have_cached_key = true; + else if (nak_at) + is_nak = true; + } + + if (have_cached_key || is_nak) { + // Can send immediately: either we have keys (use v2 PFS) or it's a NAK (use v1 or v2 + // nopfs). + _do_send_dm(id, recipient_session_id, content, sent_timestamp, pro_privkey, ttl, force_v2); + } else if (_network) { + // No cache entry at all: need to fetch keys first. Queue the send and initiate a + // prefetch. The pfs_keys_fetched callback will flush it. + PendingSend pending; + pending.id = id; + std::ranges::copy(recipient_session_id, pending.recipient.begin()); + pending.content.assign(content.begin(), content.end()); + pending.sent_timestamp = sent_timestamp; + if (pro_privkey) { + auto& stored = pending.pro_privkey.emplace(); + std::memcpy(stored.data(), pro_privkey->data(), 64); + } + pending.ttl = ttl; + pending.force_v2 = force_v2; + _pending_sends.push_back(std::move(pending)); + + if (callbacks.message_send_status) + callbacks.message_send_status(id, MessageSendStatus::awaiting_keys); + + // Wire up flushing: wrap the existing callback to also flush pending sends. + auto existing_cb = callbacks.pfs_keys_fetched; + callbacks.pfs_keys_fetched = + [this, existing_cb](std::span sid, PfsKeyFetch result) { + if (existing_cb) + existing_cb(sid, result); + _flush_pending_sends(sid); + }; + + prefetch_pfs_keys(recipient_uc); + } else { + // No cache and no network: fire immediate failure. + if (callbacks.message_send_status) + callbacks.message_send_status(id, MessageSendStatus::no_network); + } + + return id; +} + void Core::_handle_direct_messages(std::span messages) { if (!callbacks.message_received && !callbacks.message_decrypt_failed) return; diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 599897918..10ee85584 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -248,7 +248,7 @@ static std::vector v2_encrypt_inner( const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, std::span content, - std::optional> pro_signature) { + const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { auto sender_ed_pk = sender_ed25519_privkey.pubkey(); @@ -268,7 +268,7 @@ static std::vector v2_encrypt_inner( size_t inner_dict_size = 2 // d...e dict delimiters + S_KEY_VAL + 3 + bt_bytes_encoded(content.size()) // "1:c" + "" - + SIG_KEY_VAL + (pro_signature ? PRO_KEY_VAL : 0); + + SIG_KEY_VAL + (pro_ed25519_privkey ? PRO_KEY_VAL : 0); // Total message must be a multiple of 256 bytes and at least V2_MIN_FINAL_SIZE bytes. size_t final_size = @@ -303,8 +303,18 @@ static std::vector v2_encrypt_inner( throw std::runtime_error{"Failed to sign v2 message"}; return sig; }); - if (pro_signature) - dict.append("~P", *pro_signature); + if (pro_ed25519_privkey) + dict.append_signature("~P", [&](std::span body) { + uc64 sig; + if (0 != crypto_sign_ed25519_detached( + sig.data(), + nullptr, + body.data(), + body.size(), + pro_ed25519_privkey->data())) + throw std::runtime_error{"Failed to sign v2 pro signature"}; + return sig; + }); assert(dict.view().size() == inner_dict_size); } @@ -329,7 +339,7 @@ std::vector encrypt_for_recipient_v2( std::span recipient_account_x25519, std::span recipient_account_mlkem768, std::span content, - std::optional> pro_signature) { + const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) std::span S{recipient_session_id.data() + 1, 32}; @@ -382,7 +392,7 @@ std::vector encrypt_for_recipient_v2( sender_ed25519_privkey, recipient_session_id, content, - pro_signature); + pro_ed25519_privkey); } std::array decrypt_incoming_v2_prefix( @@ -441,10 +451,15 @@ static DecryptV2Result v2_aead_decrypt_and_parse( throw std::runtime_error{"v2 message signature verification failed"}; }); - // Optional "~P" pro signature (span into plain, copied once into result below) - std::optional> pro_sv; + // Optional "~P" pro signature. Extracted but not verified here — the Pro public key is + // inside the protobuf Content, so verification is deferred to the message parsing layer. + std::optional> pro_sig; if (dict.skip_until("~P")) - pro_sv = dict.consume_span(); + dict.consume_signature([&](std::span, std::span sig) { + if (sig.size() != 64) + throw std::runtime_error{"v2 ~P pro signature has wrong size"}; + std::memcpy(pro_sig.emplace().data(), sig.data(), 64); + }); dict.finish(); @@ -457,8 +472,8 @@ static DecryptV2Result v2_aead_decrypt_and_parse( result.content.assign(content_sv.begin(), content_sv.end()); result.sender_session_id[0] = 0x05; std::ranges::copy(sender_x25519, result.sender_session_id.begin() + 1); - if (pro_sv) - std::ranges::copy(*pro_sv, result.pro_signature.emplace().begin()); + if (pro_sig) + std::memcpy(result.pro_signature.emplace().data(), pro_sig->data(), 64); return result; } @@ -500,7 +515,7 @@ std::vector encrypt_for_recipient_v2_nopfs( const Ed25519PrivKeySpan& sender_ed25519_privkey, std::span recipient_session_id, std::span content, - std::optional> pro_signature) { + const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { // R = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) auto R = recipient_session_id.last<32>(); @@ -538,7 +553,7 @@ std::vector encrypt_for_recipient_v2_nopfs( sender_ed25519_privkey, recipient_session_id, content, - pro_signature); + pro_ed25519_privkey); } DecryptV2Result decrypt_incoming_v2_nopfs( diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index c66ff060a..f1d49bd3f 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -398,7 +398,7 @@ std::vector encode_for_community_inbox( std::vector encode_dm_v1( std::span plaintext, const Ed25519PrivKeySpan& ed25519_privkey, - std::chrono::milliseconds sent_timestamp, + sys_ms sent_timestamp, std::span recipient_pubkey, const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { // For 1o1 messages, encrypt the padded payload for the recipient. See: @@ -412,7 +412,7 @@ std::vector encode_dm_v1( SessionProtos::Envelope envelope; envelope.set_type(SessionProtos::Envelope_Type_SESSION_MESSAGE); envelope.set_sourcedevice(1); - envelope.set_timestamp(sent_timestamp.count()); + envelope.set_timestamp(epoch_ms(sent_timestamp)); envelope.set_content(encrypted.data(), encrypted.size()); attach_pro_sig_to_envelope(envelope, encrypted, pro_rotating_ed25519_privkey); @@ -1094,7 +1094,7 @@ session_protocol_encoded_for_destination session_protocol_encode_dm_v1( return encode_dm_v1( {static_cast(plaintext), plaintext_len}, {static_cast(ed25519_privkey), ed25519_privkey_len}, - std::chrono::milliseconds(sent_timestamp_ms), + from_epoch_ms(sent_timestamp_ms), recipient_pubkey->data, {static_cast(pro_rotating_ed25519_privkey), pro_rotating_ed25519_privkey_len}); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d201d7433..d2f6501c7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ endif() set(LIB_SESSION_UTESTS_SOURCES test_core_devices.cpp test_dm_receive.cpp + test_dm_send.cpp test_attachment_encrypt.cpp test_blinding.cpp test_bt_merge.cpp diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index bb936c770..d0c2ec4a0 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -77,7 +77,8 @@ TEST_CASE("_handle_direct_messages: v1 receive", "[core][dm]") { // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. constexpr auto plaintext = "7801"_hex_u; - auto encoded = encode_dm_v1(plaintext, sender.ed_sk, 1234ms, recip_session_id, std::nullopt); + auto encoded = + encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); OwnedMessage om{std::span{encoded}, "hash_v1", from_epoch_ms(1234), from_epoch_ms(9999)}; recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); @@ -431,8 +432,8 @@ TEST_CASE( // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. constexpr auto plaintext = "7801"_hex_u; - auto e1 = encode_dm_v1(plaintext, sender.ed_sk, 1000ms, recip_session_id, std::nullopt); - auto e2 = encode_dm_v1(plaintext, sender.ed_sk, 2000ms, recip_session_id, std::nullopt); + auto e1 = encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); + auto e2 = encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); OwnedMessage om1{std::span{e1}, "h1"}; OwnedMessage om2{std::span{e2}, "h2"}; diff --git a/tests/test_dm_send.cpp b/tests/test_dm_send.cpp new file mode 100644 index 000000000..c4e82c621 --- /dev/null +++ b/tests/test_dm_send.cpp @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include + +#include "test_helper.hpp" + +using namespace session; +using namespace session::core; +using namespace std::literals; +using namespace oxenc::literals; + +namespace { + +// Returns the session_id of a Core as a std::byte array. +std::array sid_bytes(Core& c) { + std::array result; + auto sid = c.globals.session_id(); + std::memcpy(result.data(), sid.data(), 33); + return result; +} + +// Minimal valid SessionProtos::Content protobuf: field 15 (sigTimestamp) = 1. +constexpr auto MINIMAL_CONTENT = "7801"_hex_u; + +// A valid session ID for tests that don't need actual decryption on the other side. +constexpr auto DUMMY_SID = + "05fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; + +std::span content_bytes() { + static const auto bytes = std::as_bytes(std::span{MINIMAL_CONTENT}); + return bytes; +} + +} // namespace + +// ── V2 PFS send + receive round-trip ──────────────────────────────────────────────────────────── + +TEST_CASE("send_dm: v2 PFS round-trip", "[core][send_dm]") { + std::vector received; + std::vector statuses; + std::vector captured_payload; + + callbacks sender_cbs; + sender_cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + sender_cbs.send_to_swarm = [&](std::span, + config::Namespace ns, + std::vector payload, + std::chrono::milliseconds, + std::function on_stored) { + CHECK(ns == config::Namespace::Default); + captured_payload = std::move(payload); + on_stored(true); + }; + + callbacks recip_cbs; + recip_cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + + TempCore sender{sender_cbs}; + TempCore recipient{recip_cbs}; + + recipient->devices.active_account_keys(); + auto [x25519_pub, mlkem_pub] = TestHelper::active_account_pubkeys(*recipient); + auto recip_sid = sid_bytes(*recipient); + TestHelper::seed_pfs_cache(*sender, recip_sid, x25519_pub, mlkem_pub); + + auto msg_id = sender->send_dm(recip_sid, content_bytes(), clock_now_ms()); + CHECK(msg_id == 1); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[0] == MessageSendStatus::sending); + CHECK(statuses[1] == MessageSendStatus::success); + + // Feed the captured payload into recipient to verify decryption. + REQUIRE(!captured_payload.empty()); + SwarmMessage sm; + sm.data = to_span(captured_payload); + sm.hash = "send_test_hash"; + sm.timestamp = clock_now_ms(); + sm.expiry = clock_now_ms() + 24h; + + recipient->receive_messages({&sm, 1}, config::Namespace::Default, true); + + REQUIRE(received.size() == 1); + CHECK(received[0].version == 2); + CHECK(received[0].pfs_encrypted); + CHECK(std::ranges::equal(received[0].content, MINIMAL_CONTENT)); +} + +// ── V1 fallback (NAK, force_v2=false) ─────────────────────────────────────────────────────────── + +TEST_CASE("send_dm: v1 fallback on NAK", "[core][send_dm]") { + std::vector received; + std::vector statuses; + std::vector captured_payload; + + callbacks sender_cbs; + sender_cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + sender_cbs.send_to_swarm = [&](std::span, + config::Namespace, + std::vector payload, + std::chrono::milliseconds, + std::function on_stored) { + captured_payload = std::move(payload); + on_stored(true); + }; + + callbacks recip_cbs; + recip_cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + + TempCore sender{sender_cbs}; + TempCore recipient{recip_cbs}; + + auto recip_sid = sid_bytes(*recipient); + TestHelper::seed_pfs_nak(*sender, recip_sid); + + sender->send_dm(recip_sid, content_bytes(), clock_now_ms()); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[0] == MessageSendStatus::sending); + CHECK(statuses[1] == MessageSendStatus::success); + + REQUIRE(!captured_payload.empty()); + SwarmMessage sm; + sm.data = to_span(captured_payload); + sm.hash = "v1_hash"; + sm.timestamp = clock_now_ms(); + sm.expiry = clock_now_ms() + 24h; + + recipient->receive_messages({&sm, 1}, config::Namespace::Default, true); + + REQUIRE(received.size() == 1); + CHECK(received[0].version == 1); + CHECK_FALSE(received[0].pfs_encrypted); +} + +// ── V2 non-PFS (force_v2=true, NAK) ──────────────────────────────────────────────────────────── + +TEST_CASE("send_dm: v2 non-PFS with force_v2", "[core][send_dm]") { + std::vector received; + std::vector statuses; + std::vector captured_payload; + + callbacks sender_cbs; + sender_cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + sender_cbs.send_to_swarm = [&](std::span, + config::Namespace, + std::vector payload, + std::chrono::milliseconds, + std::function on_stored) { + captured_payload = std::move(payload); + on_stored(true); + }; + + callbacks recip_cbs; + recip_cbs.message_received = [&](ReceivedMessage&& m) { received.push_back(std::move(m)); }; + + TempCore sender{sender_cbs}; + TempCore recipient{recip_cbs}; + + auto recip_sid = sid_bytes(*recipient); + TestHelper::seed_pfs_nak(*sender, recip_sid); + + sender->send_dm( + recip_sid, content_bytes(), clock_now_ms(), std::nullopt, 14 * 24h, /*force_v2=*/true); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[0] == MessageSendStatus::sending); + CHECK(statuses[1] == MessageSendStatus::success); + + REQUIRE(!captured_payload.empty()); + SwarmMessage sm; + sm.data = to_span(captured_payload); + sm.hash = "nopfs_hash"; + sm.timestamp = clock_now_ms(); + sm.expiry = clock_now_ms() + 24h; + + recipient->receive_messages({&sm, 1}, config::Namespace::Default, true); + + REQUIRE(received.size() == 1); + CHECK(received[0].version == 2); + CHECK_FALSE(received[0].pfs_encrypted); +} + +// ── No network error (NAK, no send_to_swarm, no network) ─────────────────────────────────────── + +TEST_CASE("send_dm: no_network when no callback and no network", "[core][send_dm]") { + std::vector statuses; + + callbacks cbs; + cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + + TempCore sender{cbs}; + TestHelper::seed_pfs_nak(*sender, DUMMY_SID); + + sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[0] == MessageSendStatus::sending); + CHECK(statuses[1] == MessageSendStatus::no_network); +} + +// ── No network, no cache → immediate no_network ──────────────────────────────────────────────── + +TEST_CASE("send_dm: no_network when no cache and no network", "[core][send_dm]") { + std::vector statuses; + + callbacks cbs; + cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + + TempCore sender{cbs}; + + sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + + // No cache entry and no network → immediate no_network. + REQUIRE(statuses.size() == 1); + CHECK(statuses[0] == MessageSendStatus::no_network); +} + +// ── send_to_swarm callback receives correct arguments ─────────────────────────────────────────── + +TEST_CASE("send_dm: send_to_swarm callback receives expected args", "[core][send_dm]") { + config::Namespace captured_ns{}; + std::chrono::milliseconds captured_ttl{}; + std::array captured_pubkey{}; + bool callback_called = false; + + callbacks cbs; + cbs.send_to_swarm = [&](std::span pk, + config::Namespace ns, + std::vector, + std::chrono::milliseconds ttl, + std::function on_stored) { + std::ranges::copy(pk, captured_pubkey.begin()); + captured_ns = ns; + captured_ttl = ttl; + callback_called = true; + on_stored(true); + }; + + TempCore sender{cbs}; + TestHelper::seed_pfs_nak(*sender, DUMMY_SID); + + auto custom_ttl = std::chrono::milliseconds{7 * 24h}; + sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms(), std::nullopt, custom_ttl); + + REQUIRE(callback_called); + CHECK(captured_ns == config::Namespace::Default); + CHECK(captured_ttl == custom_ttl); + CHECK(std::ranges::equal(captured_pubkey, DUMMY_SID)); +} + +// ── Network error status ──────────────────────────────────────────────────────────────────────── + +TEST_CASE("send_dm: network_error when store fails", "[core][send_dm]") { + std::vector statuses; + + callbacks cbs; + cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + cbs.send_to_swarm = [](std::span, + config::Namespace, + std::vector, + std::chrono::milliseconds, + std::function on_stored) { on_stored(false); }; + + TempCore sender{cbs}; + TestHelper::seed_pfs_nak(*sender, DUMMY_SID); + + sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[0] == MessageSendStatus::sending); + CHECK(statuses[1] == MessageSendStatus::network_error); +} + +// ── Monotonic message IDs ─────────────────────────────────────────────────────────────────────── + +TEST_CASE("send_dm: message IDs are monotonically increasing", "[core][send_dm]") { + callbacks cbs; + cbs.send_to_swarm = [](auto, auto, auto, auto, auto on_stored) { on_stored(true); }; + + TempCore sender{cbs}; + TestHelper::seed_pfs_nak(*sender, DUMMY_SID); + + auto id1 = sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + auto id2 = sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + auto id3 = sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + + CHECK(id1 == 1); + CHECK(id2 == 2); + CHECK(id3 == 3); +} + +// ── Network fallback when send_to_swarm is not set ────────────────────────────────────────────── + +TEST_CASE("send_dm: dispatches via network when send_to_swarm is not set", "[core][send_dm]") { + std::vector statuses; + + callbacks cbs; + cbs.message_send_status = [&](int64_t, MessageSendStatus s) { statuses.push_back(s); }; + // Deliberately not setting send_to_swarm — should fall back to the network object. + + TempCore sender{cbs}; + auto mock_net = std::make_shared(); + sender->set_network(mock_net); + + TestHelper::seed_pfs_nak(*sender, DUMMY_SID); + + sender->send_dm(DUMMY_SID, content_bytes(), clock_now_ms()); + + // The sending status fires immediately; the network dispatch is async via MockNetwork. + REQUIRE(statuses.size() == 1); + CHECK(statuses[0] == MessageSendStatus::sending); + + // MockNetwork should have captured a store request. + REQUIRE(mock_net->sent_requests.size() == 1); + CHECK(mock_net->sent_requests[0].request.endpoint == "store"); + + // Simulate a successful store response. + mock_net->sent_requests[0].callback(true, false, 200, {}, "{}"); + + REQUIRE(statuses.size() == 2); + CHECK(statuses[1] == MessageSendStatus::success); +} diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 19cb28ee7..15cbc420d 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -132,6 +132,20 @@ class TestHelper { return count > 0; } + // Seeds the pfs_key_cache with PFS keys for a remote session_id. + static void seed_pfs_cache( + core::Core& core, + std::span remote_session_id, + std::span x25519_pub, + std::span mlkem768_pub) { + core._store_pfs_keys(remote_session_id, x25519_pub, mlkem768_pub); + } + + // Seeds a NAK entry in the pfs_key_cache (remote has no published PFS keys). + static void seed_pfs_nak(core::Core& core, std::span remote_session_id) { + core._store_pfs_nak(remote_session_id); + } + // Returns the pfs_key_cache entry for the given session_id, or nullopt if absent. static std::optional pfs_cache_entry( core::Core& core, std::span session_id) { diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index e86c9ed6b..bbe4b524b 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -505,18 +505,20 @@ TEST_CASE("v2 PFS+PQ message encryption", "[session-protocol][encrypt][v2]") { decrypt_incoming_v2_prefix(recip_x25519_sec, recip_curve_pk, truncated), std::runtime_error); - // Encrypting and decrypting with a pro_signature - std::array fake_pro_sig; - std::fill(fake_pro_sig.begin(), fake_pro_sig.end(), 0x42); + // Encrypting and decrypting with a pro private key + uc32 pro_pk; + cleared_uc64 pro_sk; + crypto_sign_ed25519_keypair(pro_pk.data(), pro_sk.data()); auto ct_pro = encrypt_for_recipient_v2( to_span(sender_ed_sk), recip_session_id, pfs_x25519_pub, pfs_mlkem_pub, to_span("hello world"), - std::span{fake_pro_sig}); + pro_sk); auto result_pro = decrypt_incoming_v2( recip_session_id, pfs_x25519_sec, pfs_x25519_pub, pfs_mlkem_sec, ct_pro); REQUIRE(result_pro.pro_signature.has_value()); - CHECK(*result_pro.pro_signature == fake_pro_sig); + // The signature should be 64 bytes and verifiable with the pro public key. + CHECK(result_pro.pro_signature->size() == 64); } From 5f51bf0189417f6588bb4b8da7e7c9a4e3ce93d8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 00:34:18 -0300 Subject: [PATCH 070/193] QUIC file server over session-router support This adds support for the (still experimental) "quic-files" protocol for streaming uploads and downloads (see session-file-server PR #7). This has several components: Session Network w/ Session Router upload/download ================================================= session::network when in session-router mode now has a quic file client backend that looks for known pubkey URLs and, if found, makes the upload or download request over session-router to a configured mainnet or testnet session-router address with a running quic-files server. Download URLs with Session Router addresses =========================================== Download URLs now support an optional sr= fragment for specifying a session-router address, such as: http://host/file/ID#sr=address.sesh:11235 (port is optional, 11235 is the default). This allows upgraded clients to include session-router addresses for custom file servers, while remaining backwards-compatible with older clients that ignore unknown fragments. Upload/download test script =========================== A new tests/quic-files target is built that allows command-line upload/download testing, using mainnet or testnet; direct connections or session-router. Live tests now using session-router =================================== The "liveTest" script now supports upload and download tests (on testnet), using direct, onion requests, and session-router. A new CI job is added that runs all three. --- .drone.jsonnet | 29 ++ external/session-router | 2 +- include/session/attachments.hpp | 2 +- .../network/backends/quic_file_client.hpp | 116 +++++ .../network/backends/session_file_server.hpp | 20 + include/session/network/network_config.hpp | 10 + include/session/network/network_opt.hpp | 20 + .../session/network/routing/direct_router.hpp | 13 + .../network/routing/session_router_router.hpp | 19 + .../session/network/session_network_types.hpp | 4 +- include/session/util.hpp | 11 + src/CMakeLists.txt | 1 + src/network/backends/quic_file_client.cpp | 422 +++++++++++++++++ src/network/backends/session_file_server.cpp | 42 ++ src/network/network_config.cpp | 25 + src/network/routing/direct_router.cpp | 227 +++++++-- src/network/routing/onion_request_router.cpp | 2 +- src/network/routing/session_router_router.cpp | 310 +++++++++++-- src/network/session_network.cpp | 20 +- src/util.cpp | 14 + tests/CMakeLists.txt | 10 +- tests/dns_utils.hpp | 44 ++ tests/live/live_utils.hpp | 28 +- tests/live/test_file_transfer.cpp | 150 ++++++ tests/quic-files.cpp | 435 ++++++++++++++++++ 25 files changed, 1901 insertions(+), 75 deletions(-) create mode 100644 include/session/network/backends/quic_file_client.hpp create mode 100644 src/network/backends/quic_file_client.cpp create mode 100644 tests/dns_utils.hpp create mode 100644 tests/live/test_file_transfer.cpp create mode 100644 tests/quic-files.cpp diff --git a/.drone.jsonnet b/.drone.jsonnet index 9d9444af0..c15b740b8 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -197,6 +197,21 @@ local windows_cross_pipeline(name, }] else []) ); +local live_test_step(image, mode) = { + name: 'live tests (' + mode + ')', + image: image, + pull: 'always', + commands: + [apt_get_quiet + ' install -y eatmydata'] + + add_stf_repo(image) + [ + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', default_test_deps), + 'cd build', + './tests/testLive --' + mode + ' --log-level warning --colour-mode ansi -d yes "[file]"', + ], +}; + local clang(version) = debian_build( 'Debian sid/clang-' + version, docker_base + 'debian-sid-clang', @@ -351,6 +366,20 @@ local static_build(name, // Various debian builds debian_build('Debian sid', docker_base + 'debian-sid'), + + // Debian sid with session-router + live file transfer tests + local live_image = docker_base + 'debian-sid'; + debian_build( + 'Debian sid (live tests)', + live_image, + cmake_extra='-DENABLE_NETWORKING=ON -DENABLE_NETWORKING_SROUTER=ON -DBUILD_LIVE_TESTS=ON', + ) + { + steps: super.steps + [ + live_test_step(live_image, 'onionreq'), + live_test_step(live_image, 'srouter'), + live_test_step(live_image, 'direct'), + ], + }, debian_build('Debian sid/Debug', docker_base + 'debian-sid', build_type='Debug'), debian_build('Debian testing', docker_base + 'debian-testing'), clang(19), diff --git a/external/session-router b/external/session-router index faa8c0e44..07f70d4e4 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit faa8c0e44d1e295e05935ebfc1401275a57dc982 +Subproject commit 07f70d4e401705b6d70c1ef5f77724d2904dd2f6 diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp index ba03e92e7..612a75492 100644 --- a/include/session/attachments.hpp +++ b/include/session/attachments.hpp @@ -110,7 +110,7 @@ std::optional decrypted_max_size(size_t encrypted_size); /// /// - `data` -- the buffer of data to encrypt. /// -/// - `domain` -- domain separator; uploads of funamentally different types should use a different +/// - `domain` -- domain separator; uploads of fundamentally different types should use a different /// value, so that an identical upload used for different purposes will have unrelated key/nonce /// values. /// diff --git a/include/session/network/backends/quic_file_client.hpp b/include/session/network/backends/quic_file_client.hpp new file mode 100644 index 000000000..43f3e9eb2 --- /dev/null +++ b/include/session/network/backends/quic_file_client.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "session/network/key_types.hpp" +#include "session/network/session_network_types.hpp" + +namespace oxen::quic { +class Loop; +class Endpoint; +class Connection; +class BTRequestStream; +class Stream; +class GNUTLSCreds; +class Ticker; +class Address; +struct RemoteAddress; +} // namespace oxen::quic + +namespace session::network { + +/// ALPN used by the QUIC file server protocol. +constexpr auto QUIC_FILES_ALPN = "quic-files"; + +/// QUIC stream error code sent when the client aborts a download (e.g. due to a decryption error +/// in the on_data callback). +constexpr uint64_t QUIC_FILES_CLIENT_ABORT = 499; + +/// A self-contained QUIC client that speaks the "quic-files" protocol for streaming file +/// uploads and downloads to a single file server. Manages its own endpoint, connection lifecycle +/// (with idle timeout), and optional 0-RTT session resumption. +/// +/// The caller is responsible for determining the connection address (which may be a direct address +/// or a session-router tunnel proxy port) and the Ed25519 pubkey of the file server. +class QuicFileClient { + public: + using ticket_store_cb = std::function ticket_data, + std::chrono::sys_seconds expiry)>; + using ticket_extract_cb = + std::function>(std::string_view remote_key_hex)>; + + /// Construct a QuicFileClient for the given file server. + /// \param loop The event loop to use. + /// \param ed_pubkey Ed25519 pubkey of the file server (for TLS verification). + /// \param address Host/IP to connect to (e.g. "::1" for session-router proxy, or direct IP). + /// \param port Port to connect to. + QuicFileClient( + std::shared_ptr loop, + ed25519_pubkey ed_pubkey, + std::string address, + uint16_t port, + ticket_store_cb ticket_store = nullptr, + ticket_extract_cb ticket_extract = nullptr); + + ~QuicFileClient(); + + /// Update the connection target (e.g. when a session-router tunnel port changes). + /// Closes the current connection if the target changed. + void set_target(ed25519_pubkey ed_pubkey, std::string address, uint16_t port); + + /// Upload pre-accumulated encrypted data to the file server. The on_complete callback + /// receives either file_metadata on success or an int16_t error code on failure. + void upload( + std::vector data, + std::optional ttl, + std::function result)> on_complete); + + /// Download a file by ID from the file server. on_data is called as data chunks arrive + /// with a non-owning view of the data; on_complete signals completion or failure. + void download( + std::string file_id, + std::function data)> on_data, + std::function result)> on_complete); + + /// Close the current connection (if any). + void close(); + + private: + std::shared_ptr _loop; + std::shared_ptr _ep; + std::shared_ptr _conn; + std::shared_ptr _creds; + + // Stream 0: opened as BTRequestStream on each connection and held for the connection + // lifetime. TODO: use this for metadata requests (file info, extend TTL, etc.) + std::shared_ptr _bt_stream; + + ed25519_pubkey _ed_pubkey; + std::string _address; + uint16_t _port; + + // 0RTT ticket callbacks (optional; if not provided, 0RTT is not used) + ticket_store_cb _ticket_store; + ticket_extract_cb _ticket_extract; + + // Idle timeout: close the connection after this much inactivity + static constexpr auto IDLE_TIMEOUT = std::chrono::seconds{30}; + static constexpr auto IDLE_CHECK_INTERVAL = std::chrono::seconds{5}; + std::shared_ptr _idle_timer; + std::chrono::steady_clock::time_point _last_activity; + + // Returns the active connection, establishing one if needed. + std::shared_ptr _ensure_connection(); + void _start_idle_timer(); + void _touch(); +}; + +} // namespace session::network diff --git a/include/session/network/backends/session_file_server.hpp b/include/session/network/backends/session_file_server.hpp index d72ce9acb..61dc0b1a4 100644 --- a/include/session/network/backends/session_file_server.hpp +++ b/include/session/network/backends/session_file_server.hpp @@ -1,6 +1,7 @@ #pragma once #include "session/network/key_types.hpp" +#include "session/network/network_opt.hpp" #include "session/network/session_network_types.hpp" #include "session/platform.hpp" @@ -18,7 +19,17 @@ struct FileServer { namespace session::network::file_server { +/// Default QUIC file server port (first 5 non-zero Fibonacci digits). +constexpr uint16_t QUIC_DEFAULT_PORT = 11235; + extern const config::FileServer DEFAULT_CONFIG; +extern const config::FileServer TESTNET_CONFIG; + +/// Parsed session-router address from an `sr=` URL fragment, e.g. `sr=abcdef.sesh:11235`. +struct SRouterTarget { + std::string address; // e.g. "abcdef...xyz.sesh" or "name.loki" + uint16_t port; +}; struct DownloadInfo { std::string scheme; @@ -26,6 +37,7 @@ struct DownloadInfo { std::string file_id; std::optional custom_pubkey_hex; // If 'p' fragment present bool wants_stream_decryption; // If 'd' fragment present + std::optional srouter_target; // If 'sr' fragment present }; /// API: file_server/parse_download_url @@ -39,6 +51,14 @@ struct DownloadInfo { /// - returns struct containing the information required to download the file. std::optional parse_download_url(std::string_view url); +/// Returns a default session-router target for the QUIC file server, if the given HTTP file +/// server config matches a known default. This provides the fallback mapping when a download +/// URL doesn't contain an explicit `sr=` fragment. +/// +/// Returns nullopt if the HTTP file server is not a known QUIC-capable server. +std::optional default_quic_target( + const config::FileServer& http_config, opt::netid::Target netid); + /// API: file_server/generate_download_url /// /// Generates a download url to the configured file server for a given file id. diff --git a/include/session/network/network_config.hpp b/include/session/network/network_config.hpp index b3f417c9c..339aa78e5 100644 --- a/include/session/network/network_config.hpp +++ b/include/session/network/network_config.hpp @@ -27,6 +27,11 @@ struct Config { std::optional custom_file_server_max_file_size = std::nullopt; bool file_server_use_stream_encryption = false; + // QUIC file server options + std::optional quic_file_server_ed_pubkey; + std::optional quic_file_server_address; + std::optional quic_file_server_port; + // General options bool increase_no_file_limit = false; uint8_t path_length = 3; @@ -118,6 +123,11 @@ struct Config { void handle_config_opt(opt::cache_min_num_refresh_presence_to_include_node mnrp); void handle_config_opt(opt::cache_node_strike_threshold nst); + // QUIC file server options + void handle_config_opt(opt::quic_file_server_ed_pubkey qfep); + void handle_config_opt(opt::quic_file_server_address qfa); + void handle_config_opt(opt::quic_file_server_port qfp); + // Quic transport options void handle_config_opt(opt::quic_handshake_timeout qht); void handle_config_opt(opt::quic_keep_alive qka); diff --git a/include/session/network/network_opt.hpp b/include/session/network/network_opt.hpp index b89f58f30..af6d0b753 100644 --- a/include/session/network/network_opt.hpp +++ b/include/session/network/network_opt.hpp @@ -362,6 +362,26 @@ namespace opt { cache_node_strike_threshold(uint16_t count) : count{count} {} }; + // MARK: QUIC File Server Options + + /// Can be used to override the default QUIC file server Ed25519 pubkey (hex). + struct quic_file_server_ed_pubkey : base { + std::string pubkey_hex; + quic_file_server_ed_pubkey(std::string pubkey_hex) : pubkey_hex{std::move(pubkey_hex)} {} + }; + + /// Can be used to specify the direct address (IP:PORT) of the QUIC file server for direct mode. + struct quic_file_server_address : base { + std::string address; + quic_file_server_address(std::string address) : address{std::move(address)} {} + }; + + /// Can be used to override the default (11235) QUIC file server port. + struct quic_file_server_port : base { + uint16_t port; + quic_file_server_port(uint16_t port) : port{port} {} + }; + // MARK: Quic Transport Options /// Can be used to override the default (10s) handshake timeout duration for Quic connections. diff --git a/include/session/network/routing/direct_router.hpp b/include/session/network/routing/direct_router.hpp index 0743e4273..16e13c56d 100644 --- a/include/session/network/routing/direct_router.hpp +++ b/include/session/network/routing/direct_router.hpp @@ -9,6 +9,7 @@ #include #include +#include "session/network/backends/quic_file_client.hpp" #include "session/network/backends/session_file_server.hpp" #include "session/network/request_queue.hpp" #include "session/network/routing/network_router.hpp" @@ -19,6 +20,13 @@ namespace session::network { namespace config { struct DirectRouter { FileServer file_server_config; + opt::netid::Target netid; + + // When set, DirectRouter uses the QUIC file server protocol for uploads/downloads + // instead of the legacy HTTP path. All three must be set for the QUIC path to activate. + std::optional quic_file_server_address; + std::optional quic_file_server_ed_pubkey; + uint16_t quic_file_server_port = file_server::QUIC_DEFAULT_PORT; }; } // namespace config @@ -28,6 +36,7 @@ class DirectRouter : public IRouter, public std::enable_shared_from_this _loop; std::weak_ptr _transport; + std::unordered_map> _file_clients; std::unordered_map> _active_uploads; std::unordered_map _active_downloads; @@ -54,7 +63,11 @@ class DirectRouter : public IRouter, public std::enable_shared_from_this #include +#include "session/network/backends/quic_file_client.hpp" #include "session/network/backends/session_file_server.hpp" #include "session/network/request_queue.hpp" #include "session/network/routing/network_router.hpp" @@ -41,6 +42,9 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this _snode_pool; std::weak_ptr _transport; + // Pool of QUIC file server clients, keyed by Ed25519 pubkey. Multiple requests to the + // same server share one client (and thus one connection with idle timeout). + std::unordered_map> _file_clients; std::unordered_map _active_tunnels; std::unordered_map>> _pending_requests; @@ -84,7 +88,22 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this data, + session::router::tunnel_info info); + void _quic_download_via_tunnel( + DownloadRequest request, + std::string download_id, + std::string file_id, + session::router::tunnel_info info); void _establish_tunnel( std::span& remote_pubkey, const uint16_t remote_port, diff --git a/include/session/network/session_network_types.hpp b/include/session/network/session_network_types.hpp index 2bcefa298..7e49afd0d 100644 --- a/include/session/network/session_network_types.hpp +++ b/include/session/network/session_network_types.hpp @@ -241,8 +241,8 @@ struct UploadRequest : FileTransferRequest { struct DownloadRequest : FileTransferRequest { std::string download_url; - // Called as data arrives (can be called multiple times) - std::function data)> on_data; + // Called as data arrives (can be called multiple times) with a non-owning view of the chunk + std::function data)> on_data; // Minimum interval between on_data calls (to control callback overhead vs memory usage) std::chrono::milliseconds partial_min_interval = 250ms; diff --git a/include/session/util.hpp b/include/session/util.hpp index a4b0ddb1c..b9066f963 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -197,6 +197,9 @@ std::vector> to_view_vector(const Container& c) { /// vector of string_views each viewing one character. If `trim` is true then leading and trailing /// empty values will be suppressed. /// +/// The returned vector always contains at least one element when `trim` is false (even for an empty +/// input string). With `trim` true, an empty input returns an empty vector. +/// /// auto v = split("ab--c----de", "--"); // v is {"ab", "c", "", "de"} /// auto v = split("abc", ""); // v is {"a", "b", "c"} /// auto v = split("abc", "c"); // v is {"ab", ""} @@ -320,6 +323,14 @@ std::vector zstd_compress( /// then this returns nullopt if the decompressed size would exceed that limit. std::optional> zstd_decompress( std::span data, size_t max_size = 0); +/// Wrapper for formatting byte sizes with SI prefixes via fmt/oxen-logging. Provides a +/// `format_as` friend function discoverable via ADL, so no fmt headers are needed here. +/// Usage: `log::info(cat, "Size: {}", human_size{12345});` => "Size: 12.3 kB" +struct human_size { + int64_t bytes; + friend std::string format_as(human_size s); +}; + } // namespace session #ifndef _WIN32 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0a5d2d943..a9b12358f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -151,6 +151,7 @@ if(ENABLE_NETWORKING) network/session_network.cpp network/snode_pool.cpp network/swarm.cpp + network/backends/quic_file_client.cpp network/backends/session_file_server.cpp network/backends/session_open_group_server.cpp network/transport/quic_transport.cpp diff --git a/src/network/backends/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp new file mode 100644 index 000000000..2e22a2152 --- /dev/null +++ b/src/network/backends/quic_file_client.cpp @@ -0,0 +1,422 @@ +#include "session/network/backends/quic_file_client.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "session/ed25519.hpp" + +using namespace oxen; +using namespace std::literals; +using namespace oxen::log::literals; + +namespace session::network { + +namespace { + auto cat = log::Cat("quic-file-client"); +} + +// -- QuicFileClient -- + +QuicFileClient::QuicFileClient( + std::shared_ptr loop, + ed25519_pubkey ed_pubkey, + std::string address, + uint16_t port, + ticket_store_cb ticket_store, + ticket_extract_cb ticket_extract) : + _loop{std::move(loop)}, + _ed_pubkey{std::move(ed_pubkey)}, + _address{std::move(address)}, + _port{port}, + _ticket_store{std::move(ticket_store)}, + _ticket_extract{std::move(ticket_extract)}, + _last_activity{std::chrono::steady_clock::now()} { + + // Create a dedicated endpoint for file server connections + _ep = quic::Endpoint::endpoint(*_loop, quic::Address{}); + + // Set up TLS credentials + auto key_pair = ed25519::ed25519_key_pair(); + _creds = quic::GNUTLSCreds::make_from_ed_seckey( + std::string_view{reinterpret_cast(key_pair.second.data()), + key_pair.second.size()}); + + // Enable 0RTT if callbacks are provided + if (_ticket_store && _ticket_extract) { + _creds->enable_outbound_0rtt( + [store = _ticket_store]( + quic::RemoteAddress remote, + std::vector data, + std::chrono::sys_seconds expiry) { + store(oxenc::to_hex(remote.view_remote_key()), std::move(data), expiry); + }, + [extract = _ticket_extract]( + const quic::RemoteAddress& remote) -> std::optional> { + return extract(oxenc::to_hex(remote.view_remote_key())); + }); + } + + log::debug(cat, "QuicFileClient created for target {}:{}", _address, _port); +} + +QuicFileClient::~QuicFileClient() { + close(); +} + +void QuicFileClient::set_target(ed25519_pubkey ed_pubkey, std::string address, uint16_t port) { + if (_address != address || _port != port || _ed_pubkey != ed_pubkey) { + close(); + _ed_pubkey = std::move(ed_pubkey); + _address = std::move(address); + _port = port; + log::debug(cat, "Target updated to {}:{}", _address, _port); + } +} + +void QuicFileClient::close() { + _idle_timer.reset(); + _bt_stream.reset(); + if (_conn) { + _conn->close_connection(); + _conn.reset(); + } +} + +void QuicFileClient::_touch() { + _last_activity = std::chrono::steady_clock::now(); +} + +void QuicFileClient::_start_idle_timer() { + if (_idle_timer) + return; + + _idle_timer = _loop->call_every(IDLE_CHECK_INTERVAL, [this] { + if (!_conn) + return; + auto idle_duration = std::chrono::steady_clock::now() - _last_activity; + if (idle_duration >= IDLE_TIMEOUT) { + log::debug(cat, "Connection idle for {}s, closing.", idle_duration / 1s); + close(); + } + }); +} + +std::shared_ptr QuicFileClient::_ensure_connection() { + if (_conn) + return _conn; + + auto remote = quic::RemoteAddress{ + oxenc::from_hex(_ed_pubkey.hex()), + _address, + _port}; + + log::info(cat, "Connecting to QUIC file server at {}:{}", _address, _port); + + _conn = _ep->connect( + remote, + _creds, + quic::opt::outbound_alpn(QUIC_FILES_ALPN), + quic::opt::handshake_timeout{10s}, + quic::opt::keep_alive{10s}, + [this](quic::Connection&) { + log::info(cat, "Connected to QUIC file server."); + }, + [this](quic::Connection&, uint64_t ec) { + if (ec) + log::warning(cat, "Connection to QUIC file server failed (error {}).", ec); + else + log::debug(cat, "Connection to QUIC file server closed."); + _conn.reset(); + _bt_stream.reset(); + }); + + // Open stream 0 as BTRequestStream (required by file server protocol — subsequent file + // transfer streams get IDs 4, 8, etc.). + // TODO: use this stream for metadata requests (file info, extend TTL, etc.) + _bt_stream = _conn->open_stream(); + + _touch(); + _start_idle_timer(); + + return _conn; +} + +void QuicFileClient::upload( + std::vector data, + std::optional ttl, + std::function result)> on_complete) { + _loop->call([this, + data = std::make_shared>(std::move(data)), + ttl, + on_complete = std::move(on_complete)]() mutable { + try { + auto conn = _ensure_connection(); + if (!conn) { + on_complete(static_cast(ERROR_UNKNOWN)); + return; + } + + // State shared between the stream callbacks + struct upload_state { + int64_t upload_size; + std::string response_data; + std::function)> on_complete; + }; + auto state = std::make_shared(); + state->upload_size = static_cast(data->size()); + state->on_complete = std::move(on_complete); + + auto on_data = [state](quic::Stream&, std::span incoming) { + state->response_data += std::string_view{ + reinterpret_cast(incoming.data()), incoming.size()}; + }; + + auto on_close = [this, state](quic::Stream&, uint64_t error_code) { + _touch(); + + if (error_code != 0) { + log::warning(cat, "Upload stream closed with error {}.", error_code); + state->on_complete(static_cast(error_code)); + return; + } + + if (state->response_data.empty()) { + log::warning(cat, "Upload stream closed with no response data."); + state->on_complete(static_cast(ERROR_UNKNOWN)); + return; + } + + try { + // The upload response is a raw bt-dict, not size-prefixed. + log::trace( + cat, + "Upload response ({} bytes): {}", + state->response_data.size(), + state->response_data); + + oxenc::bt_dict_consumer resp{state->response_data}; + file_metadata metadata{}; + metadata.id = resp.require("#"); + metadata.size = state->upload_size; + metadata.uploaded = std::chrono::sys_seconds{ + std::chrono::seconds{resp.require("u")}}; + metadata.expiry = std::chrono::sys_seconds{ + std::chrono::seconds{resp.require("x")}}; + + log::info( + cat, + "Upload complete: file ID={}, expiry={}", + metadata.id, + metadata.expiry); + state->on_complete(std::move(metadata)); + } catch (const std::exception& e) { + log::error(cat, "Failed to parse upload response: {}", e.what()); + state->on_complete(static_cast(ERROR_UNKNOWN)); + } + }; + + auto str = conn->open_stream(on_data, on_close); + + // Build and send the PUT command + oxenc::bt_dict_producer cmd; + cmd.append("!", "PUT"); + cmd.append("s", static_cast(data->size())); + if (ttl) + cmd.append("t", static_cast(ttl->count())); + + auto cmd_view = cmd.view(); + str->send(fmt::format("{}:{}", cmd_view.size(), cmd_view)); + + // Send the file data, keeping the shared_ptr alive until the send completes + str->send(*data, data); + str->send_fin(); + + _touch(); + log::debug(cat, "Upload started: {} bytes.", data->size()); + + } catch (const std::exception& e) { + log::error(cat, "Upload failed: {}", e.what()); + on_complete(static_cast(ERROR_UNKNOWN)); + } + }); +} + +void QuicFileClient::download( + std::string file_id, + std::function data)> on_data, + std::function result)> on_complete) { + _loop->call([this, + file_id = std::move(file_id), + on_data = std::move(on_data), + on_complete = std::move(on_complete)]() mutable { + try { + auto conn = _ensure_connection(); + if (!conn) { + on_complete(static_cast(ERROR_UNKNOWN)); + return; + } + + // State shared between the stream callbacks + struct download_state { + std::string file_id; + file_metadata metadata{}; + bool metadata_parsed = false; + int meta_size = -1; + std::string partial; + std::vector meta_buf; + int64_t received = 0; + std::function)> on_data; + std::function)> on_complete; + }; + auto state = std::make_shared(); + state->file_id = file_id; + state->on_data = std::move(on_data); + state->on_complete = std::move(on_complete); + + auto data_cb = [this, state](quic::Stream& s, std::span data) { + _touch(); + + // Phase 1: parse the size prefix of the metadata block + if (state->meta_size < 0) { + try { + auto size = quic::prefix_accumulator(state->partial, data); + if (!size) + return; + if (*size == 0) + throw std::runtime_error{"Invalid 0-byte metadata block"}; + state->meta_size = static_cast(*size); + } catch (const std::exception& e) { + log::error(cat, "Download metadata prefix error: {}", e.what()); + s.close(400); + return; + } + state->meta_buf.reserve(state->meta_size); + } + + // Phase 2: accumulate metadata bytes + if (!state->metadata_parsed) { + try { + if (!quic::data_accumulator( + state->meta_buf, data, state->meta_size)) + return; + } catch (const std::exception& e) { + log::error(cat, "Download metadata accumulation error: {}", e.what()); + s.close(400); + return; + } + + // Parse metadata dict + try { + oxenc::bt_dict_consumer d{state->meta_buf}; + auto file_size = d.require("s"); + if (file_size <= 0) + throw std::runtime_error{ + fmt::format("Invalid file size {}", file_size)}; + state->metadata.id = state->file_id; + state->metadata.size = file_size; + state->metadata.uploaded = std::chrono::sys_seconds{ + std::chrono::seconds{d.require("u")}}; + state->metadata.expiry = std::chrono::sys_seconds{ + std::chrono::seconds{d.require("x")}}; + d.finish(); + state->metadata_parsed = true; + + log::debug( + cat, + "Download metadata: {} bytes, expiry={}", + state->metadata.size, + state->metadata.expiry); + } catch (const std::exception& e) { + log::error(cat, "Download metadata parse error: {}", e.what()); + s.close(444); + return; + } + } + + // Phase 3: deliver file data + if (!data.empty()) { + state->received += data.size(); + if (state->on_data) { + try { + state->on_data(state->metadata, data); + } catch (const std::exception& e) { + log::warning( + cat, + "Download aborted by on_data callback: {}", + e.what()); + s.close(QUIC_FILES_CLIENT_ABORT); + return; + } + } + } + }; + + auto close_cb = [this, state](quic::Stream&, uint64_t error_code) { + _touch(); + + if (error_code != 0) { + log::warning( + cat, + "Download stream for {} closed with error {}.", + state->file_id, + error_code); + state->on_complete(static_cast(error_code)); + return; + } + + if (!state->metadata_parsed) { + log::warning(cat, "Download stream closed before metadata received."); + state->on_complete(static_cast(ERROR_UNKNOWN)); + return; + } + + if (state->received < state->metadata.size) { + log::warning( + cat, + "Download incomplete: received {}/{} bytes.", + state->received, + state->metadata.size); + state->on_complete(static_cast(ERROR_UNKNOWN)); + return; + } + + log::info( + cat, + "Download complete: {} ({} bytes).", + state->file_id, + state->received); + state->on_complete(state->metadata); + }; + + auto str = conn->open_stream(data_cb, close_cb); + + // Build and send the GET command + oxenc::bt_dict_producer cmd; + cmd.append("!", "GET"); + cmd.append("#", file_id); + + auto cmd_view = cmd.view(); + str->send(fmt::format("{}:{}", cmd_view.size(), cmd_view)); + str->send_fin(); + + _touch(); + log::debug(cat, "Download started for file {}.", file_id); + + } catch (const std::exception& e) { + log::error(cat, "Download failed: {}", e.what()); + on_complete(static_cast(ERROR_UNKNOWN)); + } + }); +} + +} // namespace session::network diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index d2af4f3a0..21140c2b8 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -34,6 +34,16 @@ const config::FileServer DEFAULT_CONFIG = { .pubkey_hex = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59", .max_file_size = 10'000'000}; +// Testnet file server config. The X25519 pubkey is derived from the Ed25519 key +// 929e33ded05e653fec04b49645117f51851f102a947e04806791be416ed76602 via +// crypto_sign_ed25519_pk_to_curve25519. +const config::FileServer TESTNET_CONFIG = { + .scheme = "http", + .host = "superduperfiles.oxen.io", + .port = 80, + .pubkey_hex = "16d6c60aebb0851de7e6f4dc0a4734671dbf80f73664c008596511454cb6576d", + .max_file_size = 10'000'000}; + constexpr std::string_view HEADER_CONTENT_TYPE = "Content-Type"; constexpr std::string_view HEADER_CONTENT_DISPOSITION = "Content-Disposition"; constexpr std::string_view HEADER_PUBKEY = "X-FS-Pubkey"; @@ -93,12 +103,44 @@ std::optional parse_download_url(std::string_view url) { oxenc::is_hex(fragment.substr(2)) && fragment.substr(2) != file_server::DEFAULT_CONFIG.pubkey_hex) info.custom_pubkey_hex = fragment.substr(2); + else if (fragment.starts_with("sr=")) { + // sr=address or sr=address:port (port defaults to 11235 if omitted) + auto parts = split(fragment.substr(3), ":"); + if (parts.size() <= 2 && !parts[0].empty()) { + uint16_t port = QUIC_DEFAULT_PORT; + if (parts.size() == 2 && (!quic::parse_int(parts[1], port) || port == 0)) + continue; // Invalid port, skip + info.srouter_target = SRouterTarget{std::string{parts[0]}, port}; + } + } // else ignore (unknown or invalid fragment) } return info; } +// Default QUIC file server session-router addresses for known networks. +// Mainnet: Ed25519 pubkey b8eef9821445ae16e2e97ef8aa6fe782fd11ad5253cd6723b281341dba22e371 +static constexpr auto MAINNET_QUIC_FS_SESH_ADDRESS = + "zdzxuyoweszbpazjx5hkw598om6tdmk1kxgsqe71or4b5qtnhpao.sesh"sv; +// Testnet: Ed25519 pubkey 929e33ded05e653fec04b49645117f51851f102a947e04806791be416ed76602 +static constexpr auto TESTNET_QUIC_FS_SESH_ADDRESS = + "1kxd8zsom31u95yrs1mrkrm9kgnt6rbk1t9yjyd81g9rn5szcaby.sesh"sv; + +std::optional default_quic_target( + const config::FileServer& http_config, opt::netid::Target netid) { + // Map known HTTP file server pubkeys to their QUIC file server .sesh addresses. + if (http_config.pubkey_hex == DEFAULT_CONFIG.pubkey_hex && + netid == opt::netid::Target::mainnet) + return SRouterTarget{std::string{MAINNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; + + if (http_config.pubkey_hex == TESTNET_CONFIG.pubkey_hex && + netid == opt::netid::Target::testnet) + return SRouterTarget{std::string{TESTNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; + + return std::nullopt; +} + std::string generate_download_url(std::string_view file_id, const config::FileServer& config) { const auto has_custom_pubkey = (config.pubkey_hex != file_server::DEFAULT_CONFIG.pubkey_hex); diff --git a/src/network/network_config.cpp b/src/network/network_config.cpp index 1ca6a7c1e..3ea275e6d 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -31,6 +31,11 @@ Config::Config(const std::vector& opts) { HANDLE_TYPE(opt::file_server_max_file_size); HANDLE_TYPE(opt::file_server_use_stream_encryption); + // QUIC file server options + HANDLE_TYPE(opt::quic_file_server_ed_pubkey); + HANDLE_TYPE(opt::quic_file_server_address); + HANDLE_TYPE(opt::quic_file_server_port); + // General options HANDLE_TYPE(opt::increase_no_file_limit); HANDLE_TYPE(opt::path_length); @@ -160,6 +165,26 @@ void Config::handle_config_opt(opt::file_server_use_stream_encryption fsuse) { fsuse.use_stream_encryption); } +// MARK: QUIC file server options + +void Config::handle_config_opt(opt::quic_file_server_ed_pubkey qfep) { + quic_file_server_ed_pubkey = std::move(qfep.pubkey_hex); + log::debug(cat, "Network config QUIC file server Ed25519 pubkey set"); +} + +void Config::handle_config_opt(opt::quic_file_server_address qfa) { + quic_file_server_address = std::move(qfa.address); + log::debug( + cat, + "Network config QUIC file server address set to {}", + *quic_file_server_address); +} + +void Config::handle_config_opt(opt::quic_file_server_port qfp) { + quic_file_server_port = qfp.port; + log::debug(cat, "Network config QUIC file server port set to {}", qfp.port); +} + // MARK: General options void Config::handle_config_opt(opt::increase_no_file_limit dsd) { diff --git a/src/network/routing/direct_router.cpp b/src/network/routing/direct_router.cpp index acba03026..c7293eadb 100644 --- a/src/network/routing/direct_router.cpp +++ b/src/network/routing/direct_router.cpp @@ -165,21 +165,151 @@ void DirectRouter::_send_request_internal(Request request, network_response_call }); } +void DirectRouter::_cleanup_upload(const std::string& upload_id) { + auto node = _active_uploads.extract(upload_id); + if (!node.empty()) { + auto& thread = node.mapped().second; + if (thread.joinable()) + thread.join(); + } +} + +QuicFileClient& DirectRouter::_get_file_client( + const ed25519_pubkey& pubkey, std::string_view address, uint16_t port) { + auto [it, inserted] = _file_clients.try_emplace(pubkey, nullptr); + if (inserted) + it->second = std::make_unique( + _loop, pubkey, std::string{address}, port); + else + it->second->set_target(pubkey, std::string{address}, port); + return *it->second; +} + void DirectRouter::_upload_internal(UploadRequest request) { const std::string upload_id = random::unique_id("UP"); log::info(cat, "[Upload {}]: Starting upload.", upload_id); - // Make the callback atomic so we don't need to worry about it being called multiple times (eg. - // network shutdown cancelling the request and the transport shutdown automatically triggering - // callbacks) request.on_complete = make_callback_atomic(std::move(request.on_complete)); - auto& [_, upload_thread] = + + // Use the QUIC file server path if configured, otherwise fall back to the legacy HTTP path + if (!_config.quic_file_server_address || !_config.quic_file_server_ed_pubkey) { + _upload_internal_legacy(std::move(request), std::move(upload_id)); + return; + } + + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second; + .first->second.second; + + auto address = *_config.quic_file_server_address; + auto pubkey_hex = *_config.quic_file_server_ed_pubkey; + auto port = _config.quic_file_server_port; + + upload_thread = std::thread([weak_self = weak_from_this(), + this, + upload_request = request, + upload_id, + address, + pubkey_hex, + port] { + auto self = weak_self.lock(); + if (!self) + return; + + try { + std::vector all_data; + while (true) { + if (upload_request.is_cancelled()) + throw cancellation_exception{"Cancelled during data accumulation."}; + auto chunk = upload_request.next_data(); + if (chunk.empty()) + break; + auto* p = reinterpret_cast(chunk.data()); + all_data.insert(all_data.end(), p, p + chunk.size()); + } + + if (all_data.empty()) + throw std::runtime_error{"No data to upload"}; + + log::debug( + cat, + "[Upload {}]: Accumulated {} bytes, uploading to {}:{}.", + upload_id, + all_data.size(), + address, + port); + + _loop->call([weak_self, + this, + upload_request, + upload_id, + address, + pubkey_hex, + port, + data = std::move(all_data)]() mutable { + auto self = weak_self.lock(); + if (!self) + return; + + if (upload_request.is_cancelled()) { + upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); + _cleanup_upload(upload_id); + return; + } + + auto pubkey = ed25519_pubkey::from_hex(pubkey_hex); + auto& client = _get_file_client(pubkey, address, port); + + client.upload( + std::move(data), + upload_request.ttl, + [weak_self, this, upload_request, upload_id]( + std::variant result) { + auto self = weak_self.lock(); + if (!self) + return; + + if (auto* meta = std::get_if(&result)) + log::info( + cat, + "[Upload {}]: Success, file ID: {}", + upload_id, + meta->id); + else + log::error( + cat, + "[Upload {}]: Failed with error {}", + upload_id, + std::get(result)); + + upload_request.on_complete(std::move(result), false); + _cleanup_upload(upload_id); + }); + }); + } catch (const cancellation_exception&) { + _loop->call([weak_self = weak_from_this(), this, upload_request, upload_id] { + if (auto self = weak_self.lock()) { + upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); + _cleanup_upload(upload_id); + } + }); + } catch (const std::exception& e) { + log::error(cat, "[Upload {}]: Exception: {}", upload_id, e.what()); + _loop->call([weak_self = weak_from_this(), this, upload_request, upload_id] { + if (auto self = weak_self.lock()) { + upload_request.on_complete(ERROR_UNKNOWN, false); + _cleanup_upload(upload_id); + } + }); + } + }); +} + +void DirectRouter::_upload_internal_legacy(UploadRequest request, std::string upload_id) { + auto& upload_thread = + _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; - // Accumulate data on a background thread as we don't know whether `next_data` is doing file I/O - // or just reading from memory (it's a bit of a waste if it's in-memory data but loading from - // disk should be prioritised) upload_thread = std::thread([weak_self = weak_from_this(), this, upload_request = request, @@ -189,8 +319,6 @@ void DirectRouter::_upload_internal(UploadRequest request) { if (!self) return; - // Onion requests don't support streaming data so we need to load all the data from the - // streaming source into memory try { Request request = file_server::to_request(upload_id, file_server_config, upload_request); @@ -203,11 +331,7 @@ void DirectRouter::_upload_internal(UploadRequest request) { if (upload_request.is_cancelled() || !req.body) { log::debug(cat, "[Upload {}]: Cancelled before sending request.", upload_id); upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); - - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); + _cleanup_upload(upload_id); return; } @@ -230,11 +354,7 @@ void DirectRouter::_upload_internal(UploadRequest request) { if (!self) return; - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); + _cleanup_upload(upload_id); try { if (upload_request.is_cancelled()) @@ -290,12 +410,7 @@ void DirectRouter::_upload_internal(UploadRequest request) { auto self = weak_self.lock(); if (!self) return; - - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); - + _cleanup_upload(upload_id); upload_request.on_complete(ERROR_UNKNOWN, false); }); } @@ -306,10 +421,62 @@ void DirectRouter::_download_internal(DownloadRequest request) { const std::string download_id = random::unique_id("DL"); log::info(cat, "[Download {}]: Starting download.", download_id); - // Make the callback atomic so we don't need to worry about it being called multiple times (eg. - // network shutdown cancelling the request and the transport shutdown automatically triggering - // callbacks) request.on_complete = make_callback_atomic(std::move(request.on_complete)); + + if (!_config.quic_file_server_address || !_config.quic_file_server_ed_pubkey) { + _download_internal_legacy(std::move(request), std::move(download_id)); + return; + } + + // QUIC download: parse file_id from URL, connect directly to configured file server + auto download_info = file_server::parse_download_url(request.download_url); + if (!download_info) { + log::error( + cat, "[Download {}]: Invalid download URL: {}", download_id, request.download_url); + request.on_complete(ERROR_INVALID_DOWNLOAD_URL, false); + return; + } + + _active_downloads[download_id] = request; + auto file_id = download_info->file_id; + auto address = *_config.quic_file_server_address; + auto pubkey = ed25519_pubkey::from_hex(*_config.quic_file_server_ed_pubkey); + auto port = _config.quic_file_server_port; + + auto& client = _get_file_client(pubkey, address, port); + + log::debug(cat, "[Download {}]: Downloading {} from {}:{}.", download_id, file_id, address, port); + + client.download( + std::move(file_id), + request.on_data, + [weak_self = weak_from_this(), this, request, download_id]( + std::variant result) { + auto self = weak_self.lock(); + if (!self) + return; + + _active_downloads.erase(download_id); + + if (auto* meta = std::get_if(&result)) + log::info( + cat, + "[Download {}]: Success, file ID: {} ({} bytes)", + download_id, + meta->id, + meta->size); + else + log::error( + cat, + "[Download {}]: Failed with error {}", + download_id, + std::get(result)); + + request.on_complete(std::move(result), false); + }); +} + +void DirectRouter::_download_internal_legacy(DownloadRequest request, std::string download_id) { _active_downloads[download_id] = request; try { @@ -355,7 +522,7 @@ void DirectRouter::_download_internal(DownloadRequest request) { metadata.id); if (request.on_data) - request.on_data(metadata, std::move(data)); + request.on_data(metadata, to_span(data)); request.on_complete(std::move(metadata), false); } catch (const cancellation_exception&) { diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index 639b019d8..5e4c0706b 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -1040,7 +1040,7 @@ void OnionRequestRouter::_download_internal(DownloadRequest request) { metadata.id); if (request.on_data) - request.on_data(metadata, std::move(data)); + request.on_data(metadata, to_span(data)); request.on_complete(std::move(metadata), false); } catch (const cancellation_exception&) { diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index 5c73d7e5b..76c32772b 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -551,22 +551,246 @@ void SessionRouter::_send_proxy_request(Request request, network_response_callba _send_direct_request(std::move(proxy_request), std::move(proxy_callback)); } +// Extracts the Ed25519 pubkey from a resolved session-router address like "b32zpubkey.sesh" +// or "b32zpubkey.snode". Returns nullopt if the address is not a valid pubkey-based address. +static std::optional pubkey_from_srouter_address(std::string_view address) { + auto dot = address.find('.'); + if (dot == std::string_view::npos || dot == 0) + return std::nullopt; + + auto b32z = address.substr(0, dot); + if (!oxenc::is_base32z(b32z) || oxenc::from_base32z_size(b32z.size()) != 32) + return std::nullopt; + + std::optional result{std::in_place}; + oxenc::from_base32z(b32z.begin(), b32z.end(), result->begin()); + return result; +} + +void SessionRouter::_cleanup_upload(const std::string& upload_id) { + auto node = _active_uploads.extract(upload_id); + if (!node.empty()) { + auto& thread = node.mapped().second; + if (thread.joinable()) + thread.join(); + } +} + +QuicFileClient& SessionRouter::_get_file_client( + const ed25519_pubkey& pubkey, std::string_view address, uint16_t port) { + auto [it, inserted] = _file_clients.try_emplace(pubkey, nullptr); + if (inserted) + it->second = std::make_unique( + _loop, pubkey, std::string{address}, port); + else + it->second->set_target(pubkey, std::string{address}, port); + return *it->second; +} + +void SessionRouter::_quic_upload_via_tunnel( + UploadRequest upload_request, + std::string upload_id, + std::vector data, + router::tunnel_info info) { + auto pubkey = pubkey_from_srouter_address(info.remote); + if (!pubkey) { + log::error( + cat, + "[Upload {}]: Could not extract pubkey from resolved address {}", + upload_id, + info.remote); + upload_request.on_complete(ERROR_UNKNOWN, false); + _cleanup_upload(upload_id); + return; + } + + _get_file_client(*pubkey, "::1", info.local_port) + .upload( + std::move(data), + upload_request.ttl, + [weak_self = weak_from_this(), this, upload_request, upload_id]( + std::variant result) { + auto self = weak_self.lock(); + if (!self) + return; + + if (auto* meta = std::get_if(&result)) + log::info( + cat, + "[Upload {}]: Success, file ID: {}", + upload_id, + meta->id); + else + log::error( + cat, + "[Upload {}]: Failed with error {}", + upload_id, + std::get(result)); + + upload_request.on_complete(std::move(result), false); + _cleanup_upload(upload_id); + }); +} + +void SessionRouter::_quic_download_via_tunnel( + DownloadRequest request, + std::string download_id, + std::string file_id, + router::tunnel_info info) { + auto pubkey = pubkey_from_srouter_address(info.remote); + if (!pubkey) { + log::error( + cat, + "[Download {}]: Could not extract pubkey from resolved address {}", + download_id, + info.remote); + _active_downloads.erase(download_id); + request.on_complete(ERROR_UNKNOWN, false); + return; + } + + _get_file_client(*pubkey, "::1", info.local_port) + .download( + std::move(file_id), + request.on_data, + [weak_self = weak_from_this(), this, request, download_id]( + std::variant result) { + auto self = weak_self.lock(); + if (!self) + return; + + _active_downloads.erase(download_id); + + if (auto* meta = std::get_if(&result)) + log::info( + cat, + "[Download {}]: Success, file ID: {} ({} bytes)", + download_id, + meta->id, + meta->size); + else + log::error( + cat, + "[Download {}]: Failed with error {}", + download_id, + std::get(result)); + + request.on_complete(std::move(result), false); + }); +} + void SessionRouter::_upload_internal(UploadRequest request) { - // TODO: Update this to use streaming approach const std::string upload_id = random::unique_id("UP"); log::info(cat, "[Upload {}]: Starting upload.", upload_id); - // Make the callback atomic so we don't need to worry about it being called multiple times (eg. - // network shutdown cancelling the request and the transport shutdown automatically triggering - // callbacks) request.on_complete = make_callback_atomic(std::move(request.on_complete)); - auto& [_, upload_thread] = + + auto quic_target = file_server::default_quic_target(_config.file_server_config, _config.netid); + if (!quic_target) { + _upload_internal_legacy(std::move(request), std::move(upload_id)); + return; + } + + // QUIC upload: accumulate data on background thread, then tunnel and upload + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second; + .first->second.second; + + upload_thread = std::thread([weak_self = weak_from_this(), + this, + upload_request = request, + upload_id, + target = std::move(*quic_target)] { + auto self = weak_self.lock(); + if (!self) + return; + + try { + std::vector all_data; + while (true) { + if (upload_request.is_cancelled()) + throw cancellation_exception{"Cancelled during data accumulation."}; + auto chunk = upload_request.next_data(); + if (chunk.empty()) + break; + auto* p = reinterpret_cast(chunk.data()); + all_data.insert(all_data.end(), p, p + chunk.size()); + } + + if (all_data.empty()) + throw std::runtime_error{"No data to upload"}; + + log::debug( + cat, + "[Upload {}]: Accumulated {} bytes, establishing tunnel to {}.", + upload_id, + all_data.size(), + target.address); + + _loop->call([weak_self, + this, + upload_request, + upload_id, + target, + data = std::move(all_data)]() mutable { + auto self = weak_self.lock(); + if (!self) + return; + + if (upload_request.is_cancelled()) { + upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); + _cleanup_upload(upload_id); + return; + } + + srouter->establish_udp( + target.address, + target.port, + [weak_self, this, upload_request, upload_id, data = std::move(data)]( + router::tunnel_info info) mutable { + if (auto self = weak_self.lock()) + _quic_upload_via_tunnel( + upload_request, + upload_id, + std::move(data), + std::move(info)); + }, + [weak_self, this, upload_request, upload_id]() { + if (auto self = weak_self.lock()) { + log::error( + cat, + "[Upload {}]: Tunnel establishment timed out.", + upload_id); + upload_request.on_complete(ERROR_BUILD_TIMEOUT, true); + _cleanup_upload(upload_id); + } + }); + }); + } catch (const cancellation_exception&) { + _loop->call([weak_self = weak_from_this(), this, upload_request, upload_id] { + if (auto self = weak_self.lock()) { + upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); + _cleanup_upload(upload_id); + } + }); + } catch (const std::exception& e) { + log::error(cat, "[Upload {}]: Exception: {}", upload_id, e.what()); + _loop->call([weak_self = weak_from_this(), this, upload_request, upload_id] { + if (auto self = weak_self.lock()) { + upload_request.on_complete(ERROR_UNKNOWN, false); + _cleanup_upload(upload_id); + } + }); + } + }); +} + +// Legacy HTTP-based upload path, used when no QUIC file server target is available. +void SessionRouter::_upload_internal_legacy(UploadRequest request, std::string upload_id) { + auto& upload_thread = + _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; - // Accumulate data on a background thread as we don't know whether `next_data` is doing file I/O - // or just reading from memory (it's a bit of a waste if it's in-memory data but loading from - // disk should be prioritised) upload_thread = std::thread([weak_self = weak_from_this(), this, upload_request = request, @@ -576,8 +800,6 @@ void SessionRouter::_upload_internal(UploadRequest request) { if (!self) return; - // Onion requests don't support streaming data so we need to load all the data from the - // streaming source into memory try { Request request = file_server::to_request(upload_id, file_server_config, upload_request); @@ -590,11 +812,7 @@ void SessionRouter::_upload_internal(UploadRequest request) { if (upload_request.is_cancelled() || !req.body) { log::debug(cat, "[Upload {}]: Cancelled before sending request.", upload_id); upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); - - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); + _cleanup_upload(upload_id); return; } @@ -617,11 +835,7 @@ void SessionRouter::_upload_internal(UploadRequest request) { if (!self) return; - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); + _cleanup_upload(upload_id); try { if (upload_request.is_cancelled()) @@ -677,12 +891,7 @@ void SessionRouter::_upload_internal(UploadRequest request) { auto self = weak_self.lock(); if (!self) return; - - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); - + _cleanup_upload(upload_id); upload_request.on_complete(ERROR_UNKNOWN, false); }); } @@ -693,10 +902,49 @@ void SessionRouter::_download_internal(DownloadRequest request) { const std::string download_id = random::unique_id("DL"); log::info(cat, "[Download {}]: Starting download.", download_id); - // Make the callback atomic so we don't need to worry about it being called multiple times (eg. - // network shutdown cancelling the request and the transport shutdown automatically triggering - // callbacks) request.on_complete = make_callback_atomic(std::move(request.on_complete)); + + // Check for a QUIC target: first from the URL's sr= fragment, then from default mapping + std::optional quic_target; + auto download_info = file_server::parse_download_url(request.download_url); + if (download_info && download_info->srouter_target) + quic_target = std::move(download_info->srouter_target); + else + quic_target = + file_server::default_quic_target(_config.file_server_config, _config.netid); + + if (!quic_target || !download_info) { + _download_internal_legacy(std::move(request), std::move(download_id)); + return; + } + + // QUIC download path + _active_downloads[download_id] = request; + auto file_id = download_info->file_id; + + srouter->establish_udp( + quic_target->address, + quic_target->port, + [weak_self = weak_from_this(), this, request, download_id, file_id]( + router::tunnel_info info) mutable { + if (auto self = weak_self.lock()) + _quic_download_via_tunnel( + request, download_id, std::move(file_id), std::move(info)); + }, + [weak_self = weak_from_this(), this, request, download_id]() { + if (auto self = weak_self.lock()) { + log::error( + cat, + "[Download {}]: Tunnel establishment timed out.", + download_id); + _active_downloads.erase(download_id); + request.on_complete(ERROR_BUILD_TIMEOUT, true); + } + }); +} + +// Legacy HTTP-based download path, used when no QUIC file server target is available. +void SessionRouter::_download_internal_legacy(DownloadRequest request, std::string download_id) { _active_downloads[download_id] = request; try { @@ -742,7 +990,7 @@ void SessionRouter::_download_internal(DownloadRequest request) { metadata.id); if (request.on_data) - request.on_data(metadata, std::move(data)); + request.on_data(metadata, to_span(data)); request.on_complete(std::move(metadata), false); } catch (const cancellation_exception&) { diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 985b4b448..0e878ac13 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -43,7 +43,10 @@ namespace { constexpr auto clock_out_of_sync_error = "Clock out of sync"; config::FileServer build_file_server_config(const config::Config& main_config) { - config::FileServer file_server_config = file_server::DEFAULT_CONFIG; + config::FileServer file_server_config = + main_config.netid == opt::netid::Target::testnet + ? file_server::TESTNET_CONFIG + : file_server::DEFAULT_CONFIG; file_server_config.use_stream_encryption = main_config.file_server_use_stream_encryption; if (main_config.custom_file_server_scheme) @@ -85,7 +88,12 @@ namespace { config::DirectRouter build_direct_router_config( const config::Config& main_config, const config::FileServer& file_server_config) { - return {file_server_config}; + return {file_server_config, + main_config.netid, + main_config.quic_file_server_address, + main_config.quic_file_server_ed_pubkey, + main_config.quic_file_server_port.value_or( + file_server::QUIC_DEFAULT_PORT)}; } config::SessionRouter build_session_router_config( @@ -1894,7 +1902,7 @@ LIBSESSION_C_API session_download_handle_t* session_network_download( if (on_data_fn) cpp_request.on_data = [on_data_fn, ctx]( const file_metadata& metadata, - std::vector data) { + std::span data) { session_file_metadata c_meta{}; std::strncpy(c_meta.file_id, metadata.id.c_str(), sizeof(c_meta.file_id) - 1); c_meta.file_id[sizeof(c_meta.file_id) - 1] = '\0'; @@ -1902,7 +1910,11 @@ LIBSESSION_C_API session_download_handle_t* session_network_download( c_meta.uploaded_timestamp = epoch_seconds(metadata.uploaded); c_meta.expiry_timestamp = epoch_seconds(metadata.expiry); - on_data_fn(&c_meta, data.data(), data.size(), ctx); + on_data_fn( + &c_meta, + reinterpret_cast(data.data()), + data.size(), + ctx); }; cpp_request.on_complete = [on_complete_fn, diff --git a/src/util.cpp b/src/util.cpp index afe3d4411..ab290a9b4 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -39,6 +40,19 @@ std::vector split(std::string_view str, const std::string_view return results; } +std::string format_as(human_size s) { + if (s.bytes < 1000) + return fmt::format("{} B", s.bytes); + constexpr std::array prefixes = {'k', 'M', 'G', 'T'}; + double b = s.bytes; + for (auto prefix : prefixes) { + b /= 1000.; + if (b < 1000.) + return fmt::format("{:.{}f} {}B", b, b < 10. ? 2 : b < 100. ? 1 : 0, prefix); + } + return fmt::format("{:.0f} {}B", b, prefixes.back()); +} + std::tuple, std::optional> parse_url( std::string_view url) { std::tuple, std::optional> diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d2f6501c7..e8181390c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -108,12 +108,20 @@ if(BUILD_LIVE_TESTS) add_executable(testLive live/main.cpp live/test_swarm.cpp - live/test_pubkey_xfer.cpp) + live/test_pubkey_xfer.cpp + live/test_file_transfer.cpp) target_link_libraries(testLive PRIVATE test_libs Catch2::Catch2) endif() +if(BUILD_LIVE_TESTS) + add_executable(quic-files EXCLUDE_FROM_ALL quic-files.cpp) + target_link_libraries(quic-files PRIVATE test_libs) + target_include_directories(quic-files PRIVATE + ${CMAKE_SOURCE_DIR}/external/session-router/external/CLI11/include) +endif() + add_executable(swarm-auth-test EXCLUDE_FROM_ALL swarm-auth-test.cpp) target_link_libraries(swarm-auth-test PRIVATE config) diff --git a/tests/dns_utils.hpp b/tests/dns_utils.hpp new file mode 100644 index 000000000..377961081 --- /dev/null +++ b/tests/dns_utils.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +extern "C" { +#include +#include +} + +namespace session::test { + +// Resolves a hostname to an IP address string via getaddrinfo. This is needed because libquic +// does not perform DNS resolution. +inline std::string resolve_host(const std::string& host) { + struct addrinfo hints {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + struct addrinfo* res = nullptr; + if (int rc = getaddrinfo(host.c_str(), nullptr, &hints, &res); rc != 0 || !res) + throw std::runtime_error{ + "Failed to resolve '" + host + "': " + + (rc ? gai_strerror(rc) : "no results")}; + + char buf[INET6_ADDRSTRLEN]{}; + if (res->ai_family == AF_INET6) + inet_ntop( + AF_INET6, + &reinterpret_cast(res->ai_addr)->sin6_addr, + buf, + sizeof(buf)); + else + inet_ntop( + AF_INET, + &reinterpret_cast(res->ai_addr)->sin_addr, + buf, + sizeof(buf)); + + freeaddrinfo(res); + return buf; +} + +} // namespace session::test diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp index 49c942d65..167f66222 100644 --- a/tests/live/live_utils.hpp +++ b/tests/live/live_utils.hpp @@ -19,6 +19,7 @@ #include #include +#include "../dns_utils.hpp" #include "../test_helper.hpp" using namespace std::literals; @@ -26,13 +27,32 @@ using namespace std::literals; // Defined in live/main.cpp; consumed here and in all live test files. extern session::network::opt::router live_router_mode; +// Testnet QUIC file server direct-connect hostname and Ed25519 pubkey (for --direct mode). +// These are test-only values; the library code uses .sesh addresses for session-router mode. +inline constexpr auto TESTNET_QUIC_FS_HOST = "angus.oxen.io"; +inline constexpr auto TESTNET_QUIC_FS_ED_PUBKEY = + "929e33ded05e653fec04b49645117f51851f102a947e04806791be416ed76602"; + // Creates a Network instance pointed at testnet using the current live_router_mode. inline std::shared_ptr make_testnet_network( std::filesystem::path cache_dir) { - return std::make_shared( - session::network::opt::netid::testnet(), - live_router_mode, - session::network::opt::cache_directory{std::move(cache_dir)}); + namespace opt = session::network::opt; + + std::vector net_opts; + net_opts.push_back(opt::netid::testnet()); + net_opts.push_back(live_router_mode); + net_opts.push_back(opt::cache_directory{std::move(cache_dir)}); + + // For direct mode, configure the QUIC file server address so DirectRouter uses the + // quic-files protocol instead of the legacy HTTP path. We resolve the hostname here + // because libquic does not do DNS resolution. + if (live_router_mode.type == opt::router::Type::direct) { + net_opts.push_back(opt::quic_file_server_ed_pubkey{TESTNET_QUIC_FS_ED_PUBKEY}); + net_opts.push_back(opt::quic_file_server_address{ + session::test::resolve_host(TESTNET_QUIC_FS_HOST)}); + } + + return std::make_shared(net_opts); } // Creates a session::TempCore connected to a fresh testnet Network. A unique temporary directory diff --git a/tests/live/test_file_transfer.cpp b/tests/live/test_file_transfer.cpp new file mode 100644 index 000000000..b7966dc84 --- /dev/null +++ b/tests/live/test_file_transfer.cpp @@ -0,0 +1,150 @@ +#include +#include + +#include +#include +#include +#include + +#include "live_utils.hpp" + +using namespace session; +using namespace std::literals; + +// Default timeout for live network operations. +static constexpr auto LIVE_TIMEOUT = 60s; + +// These tests require a QUIC file server and only work under --srouter or --direct mode +// (not --onionreq, which cannot support the QUIC file server protocol). + +TEST_CASE("Live: file upload via QUIC", "[live][file]") { + // Skip if running under onion request mode (no QUIC support) + // Works under all routing modes: --srouter and --direct use the QUIC file server protocol, + // --onionreq falls back to the legacy HTTP proxy path. + + auto core = make_live_core(); + auto net = core->network(); + REQUIRE(net); + + // Generate small test data and encrypt it + std::vector plaintext(4096); + randombytes_buf(plaintext.data(), plaintext.size()); + + auto seed_acc = core->globals.account_seed(); + auto seed = seed_acc.seed(); + auto [encrypted, key] = attachment::encrypt( + std::span{ + reinterpret_cast(seed.data()), seed.size()}, + plaintext, + attachment::Domain::ATTACHMENT, + true); + + // Upload + std::promise> promise; + auto future = promise.get_future(); + + network::UploadRequest req; + req.request_timeout = 30s; + req.overall_timeout = LIVE_TIMEOUT; + + bool consumed = false; + req.next_data = [&]() -> std::vector { + if (consumed) + return {}; + consumed = true; + return {reinterpret_cast(encrypted.data()), + reinterpret_cast(encrypted.data() + encrypted.size())}; + }; + req.ttl = 1min; + req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; + + net->upload(std::move(req)); + + REQUIRE(future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); + auto result = future.get(); + REQUIRE(std::holds_alternative(result)); + + auto& meta = std::get(result); + CHECK(!meta.id.empty()); + CHECK(meta.size > 0); +} + +TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") { + // Works under all routing modes: --srouter and --direct use the QUIC file server protocol, + // --onionreq falls back to the legacy HTTP proxy path. + + auto core = make_live_core(); + auto net = core->network(); + REQUIRE(net); + + // Generate test data + std::vector plaintext(16384); + randombytes_buf(plaintext.data(), plaintext.size()); + + auto seed_acc = core->globals.account_seed(); + auto seed = seed_acc.seed(); + auto [encrypted, key] = attachment::encrypt( + std::span{ + reinterpret_cast(seed.data()), seed.size()}, + plaintext, + attachment::Domain::ATTACHMENT, + true); + + // Upload + std::promise> upload_promise; + auto upload_future = upload_promise.get_future(); + + network::UploadRequest upload_req; + upload_req.request_timeout = 30s; + upload_req.overall_timeout = LIVE_TIMEOUT; + + bool consumed = false; + upload_req.next_data = [&]() -> std::vector { + if (consumed) + return {}; + consumed = true; + return {reinterpret_cast(encrypted.data()), + reinterpret_cast(encrypted.data() + encrypted.size())}; + }; + upload_req.ttl = 1min; + upload_req.on_complete = [&](auto result, bool) { upload_promise.set_value(std::move(result)); }; + + net->upload(std::move(upload_req)); + + REQUIRE(upload_future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); + auto upload_result = upload_future.get(); + REQUIRE(std::holds_alternative(upload_result)); + auto& upload_meta = std::get(upload_result); + + // Download + auto download_url = + network::file_server::generate_download_url(upload_meta.id, net->file_server_config); + + std::promise> download_promise; + auto download_future = download_promise.get_future(); + std::vector downloaded_data; + + network::DownloadRequest download_req; + download_req.download_url = download_url; + download_req.request_timeout = 30s; + download_req.overall_timeout = LIVE_TIMEOUT; + download_req.on_data = [&](auto&, std::span data) { + downloaded_data.insert( + downloaded_data.end(), data.begin(), data.end()); + }; + download_req.on_complete = [&](auto result, bool) { + download_promise.set_value(std::move(result)); + }; + + net->download(std::move(download_req)); + + REQUIRE(download_future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); + auto download_result = download_future.get(); + REQUIRE(std::holds_alternative(download_result)); + + // Decrypt and verify + auto decrypted = attachment::decrypt( + std::span{downloaded_data}, key); + REQUIRE(decrypted.size() == plaintext.size()); + CHECK(decrypted == plaintext); +} diff --git a/tests/quic-files.cpp b/tests/quic-files.cpp new file mode 100644 index 000000000..1c05e5bf8 --- /dev/null +++ b/tests/quic-files.cpp @@ -0,0 +1,435 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dns_utils.hpp" + +using namespace std::literals; + +static constexpr auto STATUS_CAT = "file"; + +namespace { + +namespace log = oxen::log; +static auto logcat = log::Cat(STATUS_CAT); + +using session::human_size; +using clock = std::chrono::steady_clock; + +namespace net = session::network; +namespace fs = net::file_server; +namespace attachment = session::attachment; + +using transfer_result = std::variant; +using on_complete_t = std::function; +using on_data_t = + std::function)>; + +std::vector read_file(const std::filesystem::path& path) { + std::ifstream f{path, std::ios::binary | std::ios::ate}; + if (!f) + throw std::runtime_error{fmt::format("Failed to open {}", path.string())}; + auto size = f.tellg(); + f.seekg(0); + std::vector data(size); + f.read(reinterpret_cast(data.data()), size); + return data; +} + +using session::test::resolve_host; + +// --- Generic upload/download that take a transport-initiation callback --- + +int do_upload( + const std::string& filename, + attachment::Domain domain, + std::optional ttl, + std::function, std::optional, on_complete_t)> + initiate, + std::string download_cmd_hint) { + auto path = std::filesystem::path{filename}; + log::info(logcat, "Reading {}...", path.string()); + auto plaintext = read_file(path); + log::info(logcat, "Plaintext size: {}", human_size{static_cast(plaintext.size())}); + + std::array seed; + randombytes_buf(seed.data(), seed.size()); + + log::info( + logcat, + "Encrypting (domain: {})...", + domain == attachment::Domain::PROFILE_PIC ? "profile-pic" : "attachment"); + auto [encrypted, key] = attachment::encrypt(seed, plaintext, domain, true); + auto enc_size = static_cast(encrypted.size()); + log::info(logcat, "Encrypted size: {}", human_size{enc_size}); + + auto start = clock::now(); + std::promise promise; + auto future = promise.get_future(); + + log::info(logcat, "Uploading {}...", human_size{enc_size}); + initiate( + std::move(encrypted), + ttl, + [&](transfer_result r) { promise.set_value(std::move(r)); }); + + auto result = future.get(); + auto elapsed_s = std::chrono::duration(clock::now() - start).count(); + + if (auto* meta = std::get_if(&result)) { + auto key_hex = oxenc::to_hex(key.begin(), key.end()); + auto speed = human_size{static_cast(enc_size / std::max(elapsed_s, 0.001))}; + + log::info( + logcat, + "\nUpload complete!\n" + " File ID: {}\n" + " Key: {}\n" + " Size: {}\n" + " Time: {:.1f}s\n" + " Speed: {}/s\n" + "\n" + "To download:\n" + "{} {} {}", + meta->id, + key_hex, + human_size{meta->size}, + elapsed_s, + speed, + download_cmd_hint, + meta->id, + key_hex); + return 0; + } + + log::error(logcat, "Upload failed with error {}", std::get(result)); + return 1; +} + +int do_download( + const std::string& key_hex, + const std::string& output, + std::function initiate) { + if (key_hex.size() != 64 || !oxenc::is_hex(key_hex)) { + log::error(logcat, "Invalid key: expected 64 hex characters"); + return 1; + } + + std::array key; + oxenc::from_hex(key_hex.begin(), key_hex.end(), reinterpret_cast(key.data())); + + std::ofstream out_file; + std::ostream* out_stream = &std::cout; + if (!output.empty()) { + out_file.open(output, std::ios::binary); + if (!out_file) + throw std::runtime_error{fmt::format("Failed to open {} for writing", output)}; + out_stream = &out_file; + } + + int64_t decrypted_bytes = 0; + attachment::Decryptor decryptor{key, [&](std::span decrypted) { + out_stream->write(reinterpret_cast(decrypted.data()), decrypted.size()); + decrypted_bytes += decrypted.size(); + }}; + + auto start = clock::now(); + std::promise promise; + auto future = promise.get_future(); + int64_t received_bytes = 0; + bool first_data = true; + auto last_progress = start; + int64_t last_progress_bytes = 0; + + initiate( + [&](const net::file_metadata& info, std::span data) { + auto now = clock::now(); + + if (first_data) { + first_data = false; + auto latency = std::chrono::duration(now - start); + log::info( + logcat, + "Transfer started after {:.0f}ms (file size: {})", + latency.count(), + human_size{info.size}); + last_progress = now; + } + + received_bytes += data.size(); + + if (!decryptor.update(data)) + throw std::runtime_error{ + fmt::format("Decryption failed at byte {}", received_bytes)}; + + auto since_last = now - last_progress; + if (since_last >= 2s) { + auto since_last_s = std::chrono::duration(since_last).count(); + auto recent_speed = human_size{static_cast( + (received_bytes - last_progress_bytes) / since_last_s)}; + log::info( + logcat, + "[{}/{}] {}/s", + human_size{received_bytes}, + human_size{info.size}, + recent_speed); + last_progress = now; + last_progress_bytes = received_bytes; + } + }, + [&](transfer_result r) { promise.set_value(std::move(r)); }); + + auto result = future.get(); + auto elapsed_s = std::chrono::duration(clock::now() - start).count(); + + if (auto* meta = std::get_if(&result)) { + if (!decryptor.finalize()) { + if (out_file.is_open()) { + out_file.close(); + std::filesystem::remove(output); + } + log::error(logcat, "Download succeeded but decryption finalization failed"); + return 1; + } + + auto speed = human_size{ + static_cast(received_bytes / std::max(elapsed_s, 0.001))}; + log::info( + logcat, + "Download complete: {} encrypted, {} decrypted in {:.1f}s ({}/s)", + human_size{received_bytes}, + human_size{decrypted_bytes}, + elapsed_s, + speed); + + if (!output.empty()) + log::info(logcat, "Written to {}", output); + return 0; + } + + if (out_file.is_open()) { + out_file.close(); + std::filesystem::remove(output); + } + log::error(logcat, "Download failed with error {}", std::get(result)); + return 1; +} + +// --- Mode-specific runners --- + +struct CliArgs { + // Mode + bool srouter = false; + bool testnet = false; + + // Direct mode + std::string server_pubkey_hex; + std::string server_address = "::1"; + uint16_t server_port = fs::QUIC_DEFAULT_PORT; + + // Upload + std::string upload_filename; + attachment::Domain domain = attachment::Domain::ATTACHMENT; + std::optional ttl{3600s}; + + // Download + std::string dl_source; + std::string dl_key_hex; + std::string dl_output; + + const char* argv0; +}; + +int run(const CliArgs& args, bool is_upload) { + auto netid = args.testnet ? net::opt::netid::testnet() : net::opt::netid::mainnet(); + auto router = args.srouter ? net::opt::router::session_router() : net::opt::router::direct(); + auto cache_dir = std::filesystem::temp_directory_path() / "quic_files_cache"; + std::filesystem::create_directories(cache_dir); + + std::vector net_opts; + net_opts.push_back(netid); + net_opts.push_back(router); + net_opts.push_back(net::opt::cache_directory{cache_dir}); + + // For direct mode, pass the QUIC file server address/pubkey/port so that DirectRouter + // uses the QUIC protocol instead of the legacy HTTP path. + if (!args.srouter) { + if (args.server_pubkey_hex.empty() || args.server_pubkey_hex.size() != 64 || + !oxenc::is_hex(args.server_pubkey_hex)) { + fmt::print(stderr, "Error: --server (64 hex Ed25519 pubkey) is required for --direct\n"); + return 1; + } + if (args.server_address.empty()) { + fmt::print(stderr, "Error: --address is required for --direct\n"); + return 1; + } + + auto resolved = resolve_host(args.server_address); + if (resolved != args.server_address) + log::info(logcat, "Resolved {} -> {}", args.server_address, resolved); + + net_opts.push_back(net::opt::quic_file_server_ed_pubkey{args.server_pubkey_hex}); + net_opts.push_back(net::opt::quic_file_server_address{resolved}); + net_opts.push_back(net::opt::quic_file_server_port{args.server_port}); + } + + log::info( + logcat, + "Starting network ({}, {})...", + args.testnet ? "testnet" : "mainnet", + args.srouter ? "session-router" : "direct"); + + auto network = std::make_shared(net_opts); + + std::string mode_hint = args.srouter + ? fmt::format("{} --srouter{}", args.argv0, args.testnet ? " --testnet" : "") + : fmt::format("{} --direct --server {} --address {} --port {}", + args.argv0, args.server_pubkey_hex, args.server_address, args.server_port); + + if (is_upload) { + return do_upload( + args.upload_filename, + args.domain, + args.ttl, + [&](auto data, auto ttl, auto cb) { + auto uc = std::make_shared>( + reinterpret_cast(data.data()), + reinterpret_cast(data.data() + data.size())); + bool consumed = false; + net::UploadRequest req; + req.request_timeout = 60s; + req.overall_timeout = 300s; + req.ttl = ttl; + req.next_data = [uc, consumed]() mutable -> std::vector { + if (consumed) + return {}; + consumed = true; + return std::move(*uc); + }; + req.on_complete = [cb = std::move(cb)](auto r, bool) { + cb(std::move(r)); + }; + network->upload(std::move(req)); + }, + fmt::format("{} download", mode_hint)); + } + + // Build download URL from file ID if not already a URL + std::string download_url; + if (args.dl_source.find("://") != std::string::npos) + download_url = args.dl_source; + else + download_url = fs::generate_download_url(args.dl_source, network->file_server_config); + + log::info(logcat, "Downloading: {}", download_url); + return do_download(args.dl_key_hex, args.dl_output, [&](auto on_data, auto cb) { + net::DownloadRequest req; + req.download_url = download_url; + req.request_timeout = 60s; + req.overall_timeout = 300s; + req.on_data = std::move(on_data); + req.on_complete = [cb = std::move(cb)](auto r, bool) { cb(std::move(r)); }; + network->download(std::move(req)); + }); +} + +} // namespace + +int main(int argc, char* argv[]) { + CLI::App app{"QUIC file server upload/download tool"}; + app.require_subcommand(1); + + CliArgs args; + args.argv0 = argv[0]; + + bool use_direct = false; + app.add_flag("--srouter", args.srouter, "Route via session-router (onion-routed)"); + app.add_flag("--direct", use_direct, "Connect directly to the file server (no routing)"); + app.add_flag("--testnet", args.testnet, "Use testnet (default: mainnet; only for --srouter)"); + + app.add_option("--server", args.server_pubkey_hex, "Ed25519 pubkey of the file server (hex)"); + app.add_option( + "--address", + args.server_address, + "Server address; hostnames are resolved via DNS (default: ::1)"); + app.add_option( + "--port", + args.server_port, + fmt::format("Server port (default: {})", fs::QUIC_DEFAULT_PORT)); + + std::string log_level = "warning"; + std::string log_file = "stderr"; + app.add_option( + "--log-level", + log_level, + "Log level/categories (e.g. warning, debug, quic-file-client=trace)"); + app.add_option("--log-file", log_file, "Log output: stderr, stdout, -, or a file path"); + + bool profile_pic = false, max_ttl = false; + int64_t ttl_seconds = 3600; + auto* upload_cmd = app.add_subcommand("upload", "Encrypt and upload a file"); + upload_cmd->add_option("filename", args.upload_filename, "File to upload")->required(); + upload_cmd->add_flag("--profile-pic", profile_pic, "Use PROFILE_PIC encryption domain"); + upload_cmd->add_flag("--max-ttl", max_ttl, "Use server's maximum TTL instead of default 1h"); + upload_cmd->add_option("--ttl", ttl_seconds, "TTL in seconds (default: 3600; ignored if --max-ttl)"); + + auto* download_cmd = app.add_subcommand("download", "Download and decrypt a file"); + download_cmd + ->add_option( + "source", + args.dl_source, + "File ID or download URL (e.g. http://host/file/ID#sr=addr.sesh:port)") + ->required(); + download_cmd->add_option("key", args.dl_key_hex, "Decryption key (hex)")->required(); + download_cmd->add_option("output", args.dl_output, "Output filename (default: stdout)"); + + CLI11_PARSE(app, argc, argv); + + if (args.srouter + use_direct > 1) { + fmt::print(stderr, "Error: --srouter and --direct are mutually exclusive\n"); + return 1; + } + if (!args.srouter && !use_direct) + use_direct = true; + + if (profile_pic) + args.domain = attachment::Domain::PROFILE_PIC; + if (max_ttl) + args.ttl.reset(); + else + args.ttl.emplace(ttl_seconds); + + // Set up logging + { + constexpr std::array print_vals = {"stdout"sv, "-"sv, ""sv, "stderr"sv}; + namespace log = oxen::log; + auto log_type = std::count(print_vals.begin(), print_vals.end(), log_file) + ? log::Type::Print + : log_file == "syslog" ? log::Type::System + : log::Type::File; + log::add_sink(log_type, log_file); + + auto cats = log::extract_categories(log_level); + if (!cats.cat_levels.count(STATUS_CAT)) + cats.cat_levels[STATUS_CAT] = log::Level::info; + cats.apply(); + } + + return run(args, upload_cmd->parsed()); +} From beb69326fa6bc9b7bed728884e7324e438f08b2d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 16:44:03 -0300 Subject: [PATCH 071/193] Add a streaming decryptor; tweak encrypt keygen This adds a streaming encryptor: it still has to read the file twice to be used (once to find the key, then once while encrypting), but it never has to hold the entire file in memory at once. Also while in there I found some awkwardness in the encryption keygen, where we were using: [key, nonce] = blake2(size=56, domain=[0|1], seed || content) which is better constructed as: [key, nonce] = blake2b(size=56, domain=seed, pers="...", content) where pers is either SessionAttachmnt or Session_Prof_Pic, depending on the attachment type. This change *does* change all the encryption keys (and nonce) from what they would be before this change, but doesn't affect decryption, and is conceptually cleaner, so seems worthwhile. --- include/session/attachments.hpp | 106 +++++- src/attachments.cpp | 536 +++++++++++++++--------------- tests/test_attachment_encrypt.cpp | 91 +++++ 3 files changed, 457 insertions(+), 276 deletions(-) diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp index 612a75492..dfcd91ed7 100644 --- a/include/session/attachments.hpp +++ b/include/session/attachments.hpp @@ -125,7 +125,7 @@ std::optional decrypted_max_size(size_t encrypted_size); /// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than /// MAX_REGULAR_SIZE (unless `allow_large` is true). /// -std::pair, std::array> encrypt( +std::pair, cleared_b32> encrypt( std::span seed, std::span data, Domain domain, @@ -150,7 +150,7 @@ std::pair, std::array> encry /// /// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than /// MAX_REGULAR_SIZE (unless `allow_large` is true). -std::array encrypt( +cleared_b32 encrypt( std::span seed, std::span data, Domain domain, @@ -173,7 +173,7 @@ std::array encrypt( /// /// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if the file is larger than /// MAX_REGULAR_SIZE. -std::pair, std::array> encrypt( +std::pair, cleared_b32> encrypt( std::span seed, const std::filesystem::path& file, Domain domain, @@ -197,7 +197,7 @@ std::pair, std::array> encry /// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if the file is larger than /// MAX_REGULAR_SIZE. /// Throws std::runtime_error if the file size changes between first and second passes. -std::array encrypt( +cleared_b32 encrypt( std::span seed, const std::filesystem::path& file, Domain domain, @@ -220,7 +220,7 @@ std::array encrypt( /// Throws std::invalid_argument if `seed` is shorter than 32 bytes, or if data is larger than /// MAX_REGULAR_SIZE (unless `allow_large` is given). Throws on I/O error. If decryption fails /// then any partially written output file will be removed. -std::array encrypt( +cleared_b32 encrypt( std::span seed, std::span data, Domain domain, @@ -317,6 +317,102 @@ class Decryptor { [[nodiscard]] bool finalize(); }; +/// API: crypto/attachment::Encryptor +/// +/// Streaming two-phase encryptor for attachments. Encryption is deterministic: the same seed and +/// data always produce the same key, nonce, and ciphertext, which allows the file server to +/// deduplicate identical uploads. +/// +/// **Phase 1 (key derivation):** Construct the object and call `update()` with the plaintext data +/// (in any number of pieces). This hashes the data to derive the encryption key and nonce. +/// Normally this is the file contents itself (so that the same file always produces the same +/// encryption key); however, feeding different data (e.g. random bytes) is permitted for +/// non-deterministic encryption where deduplication is not desired. No encrypted output is +/// produced during this phase. +/// +/// **Phase 2 (encryption):** Call `start_encryption()` to finalize key derivation and transition to +/// encryption mode. Then call `next()` repeatedly to pull encrypted chunks (the encryptor reads +/// from the data source provided to `start_encryption()`). Each call returns a span of encrypted +/// output valid until the next `next()` call, or an empty span when encryption is complete. +/// +/// The `from_file()` factory handles the common case of encrypting a file: it opens the file, runs +/// phase 1, seeks back, and returns an Encryptor ready for `next()` calls with the file as the +/// data source. +class Encryptor { + alignas(64) std::byte hash_st_data[384]; // crypto_generichash_blake2b_state + cleared_array nonce_key; + std::byte ss_st_data[52]; // crypto_secretstream_xchacha20poly1305_state + + // Phase 1 state + size_t hashed_size = 0; + bool phase1_done = false; + + // Phase 2 state + std::function buffer)> source; + size_t encrypt_size = 0; + size_t encrypted_so_far = 0; + size_t padding = 0; + size_t padding_remaining = 0; + bool header_emitted = false; + bool done = false; + + // Internal buffers for producing encrypted output + std::vector plaintext_buf; + std::array out_buf; + size_t out_size = 0; + + // Produces the next chunk of encrypted output into out_buf. Returns false when done. + bool produce_next(); + + public: + /// Returns the data size: during phase 1 this is the number of bytes fed to update_key(); + /// after start_encryption() this is the target plaintext size for phase 2 (either the + /// phase 1 total, or the override if one was given to start_encryption()). + size_t data_size() const { return phase1_done ? encrypt_size : hashed_size; } + + /// Constructs an encryptor for the given seed and domain. + /// + /// `seed` must be at least 32 bytes; typically the user's Session seed. `domain` is the + /// domain separator (ATTACHMENT or PROFILE_PIC). + Encryptor(std::span seed, Domain domain); + + /// Phase 1: feed plaintext data into key derivation (hashing). + /// The data is hashed to derive the encryption key; normally this should be the actual file + /// contents that will be encrypted in phase 2. + void update_key(std::span data); + + /// Transition from phase 1 to phase 2. Finalizes the key derivation and prepares for + /// encryption. + /// + /// `allow_large` permits data larger than MAX_REGULAR_SIZE. + /// + /// `encrypt_size` overrides the expected plaintext size for phase 2. If omitted, the size + /// from phase 1 (sum of update() calls) is used. + /// + /// `source` is a pull-based data source for phase 2: it is called with a buffer to fill and + /// must fill it completely; returning fewer bytes than requested signals the end of data. + /// Phase 2 is then driven by next() calls which pull from this source. + /// + /// Returns the decryption key (in a cleared buffer). + cleared_b32 start_encryption( + std::function buffer)> source, + bool allow_large = false, + std::optional encrypt_size = std::nullopt); + + /// Pull the next chunk of encrypted output. Returns a non-owning span that is valid until + /// the next call to next(). Returns an empty span when all data has been encrypted. + std::span next(); + + /// Factory: opens a file, runs phase 1 (hashing), and returns an Encryptor ready for + /// next() calls along with the decryption key. The file remains open as the data source + /// for phase 2. + static std::pair from_file( + std::span seed, + Domain domain, + const std::filesystem::path& file, + bool allow_large = false); +}; + /// API: crypto/attachment::decrypt /// /// Decrypts an attachment allegedly produced by attachment::encrypt to an output file. Overwrites diff --git a/src/attachments.cpp b/src/attachments.cpp index 44eba4ec7..428e9c85b 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -124,249 +124,81 @@ secretstream_xchacha20poly1305_init_push_with_nonce( return st; } -// Encryption implementation function. `get_chunk(N)` returns a pair of [span, bool] of the next N (max ENCRYPT_CHUNK_SIZE) bytes, less than N only at the end of the -// input, where the bool is true if there is at least 1 byte more of data to be retrieved (i.e. -// false means the end of the data). It may not return an empty chunk except for the very first -// call. -template ReadData> -static void encrypt_impl( - std::span out, - size_t data_size, - std::span nonce_key, - ReadData get_chunk) { - size_t padding = encrypted_padding(data_size); - assert(padding >= 1); - size_t padded_size = data_size + padding; - - assert(out.size() == encrypted_size(data_size)); - out[0] = std::byte{'S'}; - - std::span uout{reinterpret_cast(out.data()), out.size()}; - - std::span header{uout.data() + 1, ENCRYPT_HEADER}; - - auto st = secretstream_xchacha20poly1305_init_push_with_nonce( - header, nonce_key.last(), nonce_key.first()); - - auto* outpos = uout.data() + 1 + ENCRYPT_HEADER; - auto* const outend = uout.data() + uout.size(); - - // Now we build a buffer containing padding, plus whatever initial actual data goes on the end - // of the last chunk of padding: - bool done = false; - { - std::vector buf; - buf.reserve(std::min(ENCRYPT_CHUNK_SIZE, padded_size)); - for (size_t padding_remaining = padding; padding_remaining;) { - unsigned char tag = 0; - if (padding_remaining > ENCRYPT_CHUNK_SIZE) { - // Full chunk of 0x00 padding (with more padding in the next chunk) - buf.resize(ENCRYPT_CHUNK_SIZE); - padding_remaining -= ENCRYPT_CHUNK_SIZE; - } else { - buf.resize(padding_remaining - 1); // 0x00 padding - buf.push_back(0x01); // padding terminator - auto [chunk, more] = get_chunk(ENCRYPT_CHUNK_SIZE - padding_remaining); - assert(chunk.size() == ENCRYPT_CHUNK_SIZE - padding_remaining || !more); - if (!chunk.empty()) - buf.insert(buf.end(), chunk.begin(), chunk.end()); - padding_remaining = 0; - if (!more) { - tag = crypto_secretstream_xchacha20poly1305_TAG_FINAL; - done = true; - } - } - - assert(outpos + buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES <= outend); - - unsigned long long out_len; - crypto_secretstream_xchacha20poly1305_push( - &st, outpos, &out_len, buf.data(), buf.size(), nullptr, 0, tag); - assert(out_len == buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES); - outpos += out_len; - } - } - - // Now we're through the initial padding (and probably some initial data): now all we need to do - // is push the rest of the data - - while (!done) { - auto [chunk, more] = get_chunk(ENCRYPT_CHUNK_SIZE); - assert(!chunk.empty()); - assert(chunk.size() == ENCRYPT_CHUNK_SIZE || !more); - assert(outpos + chunk.size() + crypto_secretstream_xchacha20poly1305_ABYTES <= outend); - - unsigned char tag = more ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; - - unsigned long long out_len; - crypto_secretstream_xchacha20poly1305_push( - &st, outpos, &out_len, chunk.data(), chunk.size(), nullptr, 0, tag); - assert(out_len == chunk.size() + crypto_secretstream_xchacha20poly1305_ABYTES); - outpos += out_len; - if (!more) - done = true; - } -} - -static std::tuple< - std::array, - std::array, - const unsigned char*, - const unsigned char*> -encrypt_buffer_init( +// Helper: creates an Encryptor from in-memory data, encrypts, and writes output into a span. +static std::pair encrypt_to_span( std::span seed, std::span data, Domain domain, + std::span out, bool allow_large) { - std::tuple< - std::array, - std::array, - const unsigned char*, - const unsigned char*> - result; - auto& [nonce_key, key, inpos, inend] = result; - - if (seed.size() < 32) - throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; - - if (data.size() > MAX_REGULAR_SIZE && !allow_large) - throw std::invalid_argument{"data to encrypt is too large"}; - - std::span udata{ - reinterpret_cast(data.data()), data.size()}; - - const auto domain_byte = static_cast(domain); - hash::blake2b_key( - nonce_key, std::span{&domain_byte, 1}, seed.first(32), udata); - std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + Encryptor enc{seed, domain}; + enc.update_key(data); + + size_t pos = 0; + auto key = enc.start_encryption( + [&](std::span buf) -> size_t { + size_t avail = std::min(buf.size(), data.size() - pos); + std::memcpy(buf.data(), data.data() + pos, avail); + pos += avail; + return avail; + }, + allow_large); - inpos = udata.data(); - inend = inpos + udata.size(); + size_t written = 0; + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) { + assert(written + chunk.size() <= out.size()); + std::memcpy(out.data() + written, chunk.data(), chunk.size()); + written += chunk.size(); + } - return result; + return {std::move(key), written}; } -std::array encrypt( +cleared_b32 encrypt( std::span seed, std::span data, Domain domain, std::span out, bool allow_large) { - - auto [nonce_key, key, inpos, inend] = encrypt_buffer_init(seed, data, domain, allow_large); - - encrypt_impl( - out, - data.size(), - nonce_key, - [&inpos, &inend](size_t size) -> std::pair, bool> { - auto* start = inpos; - auto* end = std::min(inpos + size, inend); - inpos = end; - return {{start, end}, inpos != inend}; - }); - - return key; + return encrypt_to_span(seed, data, domain, out, allow_large).first; } -std::pair, std::array> encrypt( +std::pair, cleared_b32> encrypt( std::span seed, std::span data, Domain domain, bool allow_large) { - - if (seed.size() < 32) - throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; - - if (data.size() > MAX_REGULAR_SIZE && !allow_large) - throw std::invalid_argument{"data to encrypt is too large"}; - - std::pair, std::array> result; - auto& [out, key] = result; - - out.resize(encrypted_size(data.size())); - - key = encrypt(seed, data, domain, out, allow_large); - - return result; + std::vector out(encrypted_size(data.size())); + auto key = encrypt_to_span(seed, data, domain, out, allow_large).first; + return {std::move(out), std::move(key)}; } -std::array encrypt( +cleared_b32 encrypt( std::span seed, const std::filesystem::path& file, Domain domain, std::function(size_t enc_size)> make_buffer, bool allow_large) { + auto [enc, key] = Encryptor::from_file(seed, domain, file, allow_large); - std::ifstream in; - in.exceptions(std::ios::badbit); - in.open(file, std::ios::binary | std::ios::ate); - size_t size = in.tellg(); - in.seekg(0, std::ios::beg); - - size = encrypted_size(size); - - std::array nonce_key; + auto out = make_buffer(encrypted_size(enc.data_size())); - crypto_generichash_blake2b_state b_st; - const auto domain_byte = static_cast(domain); - crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); - hash::update_all(b_st, seed.first(32)); - - size_t in_size = 0; - std::array chunk; - while (in.read(reinterpret_cast(chunk.data()), chunk.size())) { - hash::update_all(b_st, chunk); - in_size += chunk.size(); - } - if (in.gcount() > 0) { - hash::update_all(b_st, std::span{chunk}.first(in.gcount())); - in_size += in.gcount(); + size_t written = 0; + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) { + assert(written + chunk.size() <= out.size()); + std::memcpy(out.data() + written, chunk.data(), chunk.size()); + written += chunk.size(); } - crypto_generichash_blake2b_final(&b_st, nonce_key.data(), nonce_key.size()); - - std::array key; - std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); - - in.clear(); - in.exceptions(std::ios::badbit | std::ios::failbit); - in.seekg(0, std::ios::beg); - - auto encrypted = make_buffer(size); - if (encrypted.size() != size) - throw std::logic_error{ - "make_buffer returned span of invalid size: expected {}, got {}"_format( - size, encrypted.size())}; - - std::array buf; - encrypt_impl( - encrypted, - in_size, - nonce_key, - [&in, &in_size, &buf](size_t size) -> std::pair, bool> { - size_t consumed = in.tellg(); - if (consumed + size > in_size) - size = in_size - consumed; - - if (size > 0) - in.read(reinterpret_cast(buf.data()), size); - - in.peek(); - return {std::span{buf}.first(size), !in.eof()}; - }); - return key; } -std::pair, std::array> encrypt( +std::pair, cleared_b32> encrypt( std::span seed, const std::filesystem::path& file, Domain domain, bool allow_large) { - - std::pair, std::array> result; + std::pair, cleared_b32> result; auto& [encrypted, key] = result; key = encrypt( @@ -382,82 +214,33 @@ std::pair, std::array> encry return result; } -std::array encrypt( +cleared_b32 encrypt( std::span seed, std::span data, Domain domain, const std::filesystem::path& file, bool allow_large) { - - auto [nonce_key, key, inpos, inend] = encrypt_buffer_init(seed, data, domain, allow_large); - - size_t padding = encrypted_padding(data.size()); - assert(padding >= 1); - size_t padded_size = data.size() + padding; + Encryptor enc{seed, domain}; + enc.update_key(data); + + size_t pos = 0; + auto key = enc.start_encryption( + [&](std::span buf) -> size_t { + size_t avail = std::min(buf.size(), data.size() - pos); + std::memcpy(buf.data(), data.data() + pos, avail); + pos += avail; + return avail; + }, + allow_large); try { std::ofstream out; out.exceptions(std::ios::failbit | std::ios::badbit); out.open(file, std::ios::binary | std::ios::trunc); - out.write("S", 1); - - std::array cbuf; - std::span ubuf{reinterpret_cast(cbuf.data()), cbuf.size()}; - - auto st = secretstream_xchacha20poly1305_init_push_with_nonce( - ubuf.first(), - std::span{nonce_key}.last(), - std::span{nonce_key}.first()); - - out.write(cbuf.data(), ENCRYPT_HEADER); - - // Now we build a buffer containing padding, plus whatever initial actual data goes on the - // end of the last chunk of padding, and write those encrypted padding chunks to the file: - { - std::vector buf; - buf.reserve(std::min(ENCRYPT_CHUNK_SIZE, padded_size)); - for (size_t padding_remaining = padding; padding_remaining;) { - if (padding_remaining > ENCRYPT_CHUNK_SIZE) { - // Full chunk of 0x00 padding (with more padding in the next chunk) - buf.resize(ENCRYPT_CHUNK_SIZE); - padding_remaining -= ENCRYPT_CHUNK_SIZE; - } else { - buf.resize(padding_remaining - 1); // 0x00 padding - buf.push_back(0x01); // padding terminator - if (size_t first_data = - std::min(ENCRYPT_CHUNK_SIZE - padding_remaining, data.size())) { - buf.insert(buf.end(), inpos, inpos + first_data); - inpos += first_data; - } - padding_remaining = 0; - } - - unsigned char tag = - inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; - - unsigned long long out_len; - crypto_secretstream_xchacha20poly1305_push( - &st, ubuf.data(), &out_len, buf.data(), buf.size(), nullptr, 0, tag); - assert(out_len == buf.size() + crypto_secretstream_xchacha20poly1305_ABYTES); - out.write(cbuf.data(), out_len); - } - } - // Now we're through the initial padding (and probably some initial data): now all we need - // to do is write the rest of the data in chunks - while (inpos < inend) { - auto* chunk_start = inpos; - inpos = std::min(chunk_start + ENCRYPT_CHUNK_SIZE, inend); - unsigned char tag = inpos < inend ? 0 : crypto_secretstream_xchacha20poly1305_TAG_FINAL; - - unsigned long long out_len; - crypto_secretstream_xchacha20poly1305_push( - &st, ubuf.data(), &out_len, chunk_start, inpos - chunk_start, nullptr, 0, tag); - assert(out_len == inpos - chunk_start + crypto_secretstream_xchacha20poly1305_ABYTES); - - out.write(cbuf.data(), out_len); - } - } catch (const std::exception& e) { + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) + out.write(reinterpret_cast(chunk.data()), chunk.size()); + } catch (const std::exception&) { std::error_code ec; std::filesystem::remove(file, ec); throw; @@ -898,6 +681,217 @@ void decrypt( } } +// -- Encryptor -- + +static_assert( + sizeof(crypto_generichash_blake2b_state) == 384 && + alignof(crypto_generichash_blake2b_state) == 64, + "blake2b state size/alignment changed; update Encryptor::hash_st_data"); + +static_assert( + sizeof(crypto_secretstream_xchacha20poly1305_state) == 52, + "secretstream state size changed; update Encryptor::ss_st_data"); + +namespace { + using namespace session::literals; + + constexpr auto PERS_ATTACHMENT = "SessionAttachmnt"_b2b_pers; + constexpr auto PERS_PROFILE_PIC = "Session_Prof_Pic"_b2b_pers; + + const auto& domain_pers(Domain domain) { + switch (domain) { + case Domain::ATTACHMENT: return PERS_ATTACHMENT; + case Domain::PROFILE_PIC: return PERS_PROFILE_PIC; + } + throw std::invalid_argument{"Invalid encryption domain"}; + } + + auto& b2b_st(std::byte (&data)[384]) { + return *reinterpret_cast(data); + } + auto& ss_st(std::byte (&data)[52]) { + return *reinterpret_cast(data); + } + auto* uc(std::byte* p) { return reinterpret_cast(p); } + auto* uc(const std::byte* p) { return reinterpret_cast(p); } +} // namespace + +Encryptor::Encryptor(std::span seed, Domain domain) { + if (seed.size() < 32) + throw std::invalid_argument{"attachment::Encryptor requires a 32-byte uploader seed"}; + + const auto& pers = domain_pers(domain); + crypto_generichash_blake2b_init_salt_personal( + &b2b_st(hash_st_data), + uc(seed.data()), + 32, + nonce_key.size(), + nullptr, + pers.data()); +} + +void Encryptor::update_key(std::span data) { + if (phase1_done) + throw std::logic_error{"Encryptor::update() called after start_encryption()"}; + + crypto_generichash_blake2b_update(&b2b_st(hash_st_data), uc(data.data()), data.size()); + hashed_size += data.size(); +} + +cleared_b32 Encryptor::start_encryption( + std::function buffer)> src, + bool allow_large, + std::optional enc_size) { + if (phase1_done) + throw std::logic_error{"start_encryption() called twice"}; + phase1_done = true; + + crypto_generichash_blake2b_final(&b2b_st(hash_st_data), uc(nonce_key.data()), nonce_key.size()); + + cleared_b32 key; + std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + + encrypt_size = enc_size.value_or(hashed_size); + + if (encrypt_size > MAX_REGULAR_SIZE && !allow_large) + throw std::invalid_argument{"data to encrypt is too large"}; + + source = std::move(src); + padding = encrypted_padding(encrypt_size); + padding_remaining = padding; + + // Write 'S' prefix + header into out_buf; initialize secretstream + out_buf[0] = std::byte{'S'}; + auto* header = uc(out_buf.data()) + 1; + ss_st(ss_st_data) = secretstream_xchacha20poly1305_init_push_with_nonce( + std::span{header, ENCRYPT_HEADER}, + std::span{ + uc(nonce_key.data()) + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE}, + std::span{uc(nonce_key.data()), ENCRYPT_HEADER}); + out_size = 1 + ENCRYPT_HEADER; + + plaintext_buf.reserve(ENCRYPT_CHUNK_SIZE); + + return key; +} + +bool Encryptor::produce_next() { + if (done) + return false; + if (!source) + throw std::logic_error{"Encryptor::next() requires a data source"}; + + plaintext_buf.clear(); + + size_t need = ENCRYPT_CHUNK_SIZE; + + // Fill with padding first + if (padding_remaining > 0) { + if (padding_remaining > ENCRYPT_CHUNK_SIZE) { + plaintext_buf.resize(ENCRYPT_CHUNK_SIZE, std::byte{0}); + padding_remaining -= ENCRYPT_CHUNK_SIZE; + need = 0; + } else { + plaintext_buf.resize(padding_remaining - 1, std::byte{0}); + plaintext_buf.push_back(std::byte{0x01}); + need = ENCRYPT_CHUNK_SIZE - padding_remaining; + padding_remaining = 0; + } + } + + // Fill the rest from the data source + if (need > 0) { + size_t before = plaintext_buf.size(); + plaintext_buf.resize(before + need); + size_t got = source(std::span{plaintext_buf}.subspan(before, need)); + plaintext_buf.resize(before + got); + encrypted_so_far += got; + + bool source_done = got < need; + + if (encrypted_so_far > encrypt_size) + throw std::runtime_error{ + "Encryptor data source provided too much data: expected {} bytes, got at least {}"_format( + encrypt_size, encrypted_so_far)}; + + if (source_done && encrypted_so_far < encrypt_size) + throw std::runtime_error{ + "Encryptor data source ended prematurely: expected {} bytes, got {}"_format( + encrypt_size, encrypted_so_far)}; + } + + if (plaintext_buf.empty()) { + done = true; + return false; + } + + bool is_final = encrypted_so_far >= encrypt_size && padding_remaining == 0; + unsigned char tag = is_final ? crypto_secretstream_xchacha20poly1305_TAG_FINAL : 0; + + unsigned long long enc_len; + crypto_secretstream_xchacha20poly1305_push( + &ss_st(ss_st_data), + uc(out_buf.data()), + &enc_len, + uc(plaintext_buf.data()), + plaintext_buf.size(), + nullptr, + 0, + tag); + out_size = static_cast(enc_len); + + if (is_final) + done = true; + + return true; +} + +std::span Encryptor::next() { + if (!phase1_done) + throw std::logic_error{"Encryptor::next() called before start_encryption()"}; + + // First call returns the 'S' prefix + header + if (!header_emitted) { + header_emitted = true; + return {out_buf.data(), out_size}; + } + + if (!produce_next()) + return {}; + + return {out_buf.data(), out_size}; +} + +std::pair Encryptor::from_file( + std::span seed, + Domain domain, + const std::filesystem::path& file, + bool allow_large) { + Encryptor enc{seed, domain}; + + auto in = std::make_shared(); + in->exceptions(std::ios::badbit); + in->open(file, std::ios::binary); + + std::array chunk; + while (in->read(reinterpret_cast(chunk.data()), chunk.size())) + enc.update_key(chunk); + if (in->gcount() > 0) + enc.update_key(std::span{chunk}.first(in->gcount())); + + in->clear(); + in->seekg(0, std::ios::beg); + + auto key = enc.start_encryption( + [in](std::span buffer) -> size_t { + in->read(reinterpret_cast(buffer.data()), buffer.size()); + return in->gcount(); + }, + allow_large); + + return {std::move(enc), std::move(key)}; +} + } // namespace session::attachment extern "C" { diff --git a/tests/test_attachment_encrypt.cpp b/tests/test_attachment_encrypt.cpp index 91f18ef43..2de3e46bd 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -461,3 +461,94 @@ TEST_CASE( CHECK_FALSE(std::filesystem::exists(out.path)); } } + +TEST_CASE("Streaming Encryptor", "[attachments][encryptor]") { + + auto DATA_SIZE = GENERATE( + 0, + 1, + 100, + 1000, + 4053, + 8150, + 32768, + 65536, + 100000); + + auto seed = "9123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; + const auto data = make_data(DATA_SIZE); + + SECTION("pull-based encryption with manual source") { + attachment::Encryptor enc{seed, attachment::Domain::ATTACHMENT}; + + // Phase 1: feed data in chunks to derive key + for (size_t pos = 0; pos < data.size();) { + size_t chunk = std::min(1000, data.size() - pos); + enc.update_key(std::span{data}.subspan(pos, chunk)); + pos += chunk; + } + if (data.empty()) + enc.update_key({}); + + // Phase 2: start encryption with a pull source + size_t src_pos = 0; + auto key = enc.start_encryption( + [&](std::span buf) -> size_t { + size_t avail = std::min(buf.size(), data.size() - src_pos); + std::memcpy(buf.data(), data.data() + src_pos, avail); + src_pos += avail; + return avail; + }); + + // Collect all encrypted output + std::vector encrypted; + while (true) { + auto chunk = enc.next(); + if (chunk.empty()) + break; + encrypted.insert(encrypted.end(), chunk.begin(), chunk.end()); + } + + CHECK(encrypted.size() == attachment::encrypted_size(DATA_SIZE)); + + // Decrypt with the streaming Decryptor and verify round-trip + std::vector decrypted; + attachment::Decryptor dec{key, [&](std::span d) { + decrypted.insert(decrypted.end(), d.begin(), d.end()); + }}; + REQUIRE(dec.update(encrypted)); + REQUIRE(dec.finalize()); + REQUIRE(decrypted.size() == data.size()); + CHECK(!!(decrypted == data)); + } + + SECTION("from_file factory") { + if (DATA_SIZE == 0) + return; // Can't write an empty file for this test + + // Write test data to a temp file + temp_data_file tmp; + { + std::ofstream f{tmp.path, std::ios::binary}; + f.write(reinterpret_cast(data.data()), data.size()); + } + + auto [enc, key] = attachment::Encryptor::from_file( + seed, attachment::Domain::ATTACHMENT, tmp.path, true); + + std::vector encrypted; + while (true) { + auto chunk = enc.next(); + if (chunk.empty()) + break; + encrypted.insert(encrypted.end(), chunk.begin(), chunk.end()); + } + + CHECK(encrypted.size() == attachment::encrypted_size(DATA_SIZE)); + + // Decrypt and verify + auto decrypted = attachment::decrypt(encrypted, key); + REQUIRE(decrypted.size() == data.size()); + CHECK(!!(decrypted == data)); + } +} From 02663c06a2dcd5e814ee39cf33b8589802a0ef23 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 1 Apr 2026 18:13:30 -0300 Subject: [PATCH 072/193] Make quic file uploader use streaming encryption This allows on-the-fly encryption and transfer to the big file server without ever needing to store the entire file in memory. --- include/session/attachments.hpp | 19 +- .../network/backends/quic_file_client.hpp | 29 +- .../session/network/routing/direct_router.hpp | 6 +- .../network/routing/network_router.hpp | 4 + .../network/routing/onion_request_router.hpp | 13 +- .../network/routing/session_router_router.hpp | 3 +- include/session/network/session_network.hpp | 2 + .../session/network/session_network_types.hpp | 17 ++ src/attachments.cpp | 60 ++-- src/network/backends/quic_file_client.cpp | 207 +++++++++++-- src/network/backends/session_file_server.cpp | 6 +- src/network/network_config.cpp | 5 +- src/network/routing/direct_router.cpp | 59 +++- src/network/routing/onion_request_router.cpp | 278 ++++++++++++------ src/network/routing/session_router_router.cpp | 86 ++++-- src/network/session_network.cpp | 90 ++++-- tests/dns_utils.hpp | 10 +- tests/live/live_utils.hpp | 4 +- tests/live/test_file_transfer.cpp | 61 +++- tests/quic-files.cpp | 129 ++++---- tests/test_attachment_encrypt.cpp | 28 +- 21 files changed, 789 insertions(+), 327 deletions(-) diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp index dfcd91ed7..ebacecfac 100644 --- a/include/session/attachments.hpp +++ b/include/session/attachments.hpp @@ -403,9 +403,22 @@ class Encryptor { /// the next call to next(). Returns an empty span when all data has been encrypted. std::span next(); - /// Factory: opens a file, runs phase 1 (hashing), and returns an Encryptor ready for - /// next() calls along with the decryption key. The file remains open as the data source - /// for phase 2. + /// Runs both phases from a file: hashes the file contents (phase 1), then sets up + /// streaming encryption with the file as the data source (phase 2). After this call, + /// next() returns encrypted chunks. The file is held open internally for phase 2 reads. + /// + /// Must be called on a freshly constructed Encryptor (i.e. before any update_key() calls). + /// + /// If `progress` is provided, it is called periodically during phase 1 with (bytes_read, + /// total_size). If the callback throws, the operation is aborted and the exception + /// propagates to the caller. + cleared_b32 load_key_from_file( + const std::filesystem::path& file, + bool allow_large = false, + std::function progress = nullptr); + + /// Factory: constructs an Encryptor, runs load_from_file, and returns the ready Encryptor + /// along with the decryption key. static std::pair from_file( std::span seed, Domain domain, diff --git a/include/session/network/backends/quic_file_client.hpp b/include/session/network/backends/quic_file_client.hpp index 43f3e9eb2..93a0b5ea2 100644 --- a/include/session/network/backends/quic_file_client.hpp +++ b/include/session/network/backends/quic_file_client.hpp @@ -39,13 +39,19 @@ constexpr uint64_t QUIC_FILES_CLIENT_ABORT = 499; /// The caller is responsible for determining the connection address (which may be a direct address /// or a session-router tunnel proxy port) and the Ed25519 pubkey of the file server. class QuicFileClient { + friend void streaming_file_upload( + std::shared_ptr, + attachment::Encryptor, + FileUploadRequest, + std::function); + public: using ticket_store_cb = std::function ticket_data, std::chrono::sys_seconds expiry)>; - using ticket_extract_cb = - std::function>(std::string_view remote_key_hex)>; + using ticket_extract_cb = std::function>( + std::string_view remote_key_hex)>; /// Construct a QuicFileClient for the given file server. /// \param loop The event loop to use. @@ -113,4 +119,23 @@ class QuicFileClient { void _touch(); }; +/// Performs a complete streaming file upload from a background thread. This function blocks +/// until the upload completes or fails. It: +/// 1. Reads the file to derive the encryption key (Encryptor phase 1) +/// 2. Opens a QUIC stream on the loop thread and sends the PUT command +/// 3. Pulls encrypted chunks from the Encryptor and pushes them to the stream, +/// using watermarks for backpressure +/// 4. Waits for the server response +/// +/// Must be called from a background thread (not the loop thread). The `on_complete` callback +/// fires on the loop thread when done. +/// +/// `get_client` is called on the loop thread to obtain the QuicFileClient to use; this allows +/// the caller to do any router-specific setup (e.g. tunnel establishment) before the upload. +void streaming_file_upload( + std::shared_ptr loop, + attachment::Encryptor enc, + FileUploadRequest request, + std::function get_client); + } // namespace session::network diff --git a/include/session/network/routing/direct_router.hpp b/include/session/network/routing/direct_router.hpp index 16e13c56d..5d5efbe4b 100644 --- a/include/session/network/routing/direct_router.hpp +++ b/include/session/network/routing/direct_router.hpp @@ -54,7 +54,8 @@ class DirectRouter : public IRouter, public std::enable_shared_from_this seed) override; void download(DownloadRequest request) override; private: @@ -67,7 +68,8 @@ class DirectRouter : public IRouter, public std::enable_shared_from_this get_active_paths() { return {}; }; virtual std::vector get_all_used_nodes() { return {}; }; virtual void send_request(Request request, network_response_callback_t callback) = 0; + [[deprecated("use upload_file() instead")]] virtual void upload(UploadRequest request) = 0; + /// Upload a file from disk with streaming encryption. The seed is consumed immediately + /// (before this returns) to initialize the encryption key derivation state. + virtual void upload_file(FileUploadRequest request, std::span seed) = 0; virtual void download(DownloadRequest request) = 0; }; diff --git a/include/session/network/routing/onion_request_router.hpp b/include/session/network/routing/onion_request_router.hpp index 1dbbbcee3..e57d23718 100644 --- a/include/session/network/routing/onion_request_router.hpp +++ b/include/session/network/routing/onion_request_router.hpp @@ -153,7 +153,8 @@ class OnionRequestRouter : public IRouter, public std::enable_shared_from_this get_active_paths() override; std::vector get_all_used_nodes() override; void send_request(Request request, network_response_callback_t callback) override; - void upload(UploadRequest request) override; + void upload(UploadRequest request) override; // deprecated: use upload_file() + void upload_file(FileUploadRequest request, std::span seed) override; void download(DownloadRequest request) override; private: @@ -172,6 +173,16 @@ class OnionRequestRouter : public IRouter, public std::enable_shared_from_this is_cancelled, + std::function, bool)> on_result); void _download_internal(DownloadRequest request); void _build_path( diff --git a/include/session/network/routing/session_router_router.hpp b/include/session/network/routing/session_router_router.hpp index d20526d64..c7fd06b60 100644 --- a/include/session/network/routing/session_router_router.hpp +++ b/include/session/network/routing/session_router_router.hpp @@ -67,7 +67,8 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this get_active_paths() override; void send_request(Request request, network_response_callback_t callback) override; - void upload(UploadRequest request) override; + void upload(UploadRequest request) override; // deprecated: use upload_file() + void upload_file(FileUploadRequest request, std::span seed) override; void download(DownloadRequest request) override; private: diff --git a/include/session/network/session_network.hpp b/include/session/network/session_network.hpp index 34f2f927b..ed557c49a 100644 --- a/include/session/network/session_network.hpp +++ b/include/session/network/session_network.hpp @@ -105,7 +105,9 @@ class Network : public std::enable_shared_from_this { uint16_t count, std::function nodes)> callback); virtual void send_request(Request request, network_response_callback_t callback); + [[deprecated("use upload_file() instead")]] void upload(UploadRequest request); + void upload_file(FileUploadRequest request, std::span seed); void download(DownloadRequest request); private: diff --git a/include/session/network/session_network_types.hpp b/include/session/network/session_network_types.hpp index 7e49afd0d..f36815935 100644 --- a/include/session/network/session_network_types.hpp +++ b/include/session/network/session_network_types.hpp @@ -1,13 +1,16 @@ #pragma once +#include #include #include #include #include +#include "session/attachments.hpp" #include "session/network/key_types.hpp" #include "session/network/service_node.hpp" #include "session/network/session_network_types.h" +#include "session/sodium_array.hpp" namespace session::network { @@ -30,6 +33,7 @@ constexpr int16_t ERROR_FAILED_GENERATE_ONION_PAYLOAD = -10010; constexpr int16_t ERROR_FAILED_TO_GET_STREAM = -10011; constexpr int16_t ERROR_BUILD_TIMEOUT = -10100; constexpr int16_t ERROR_REQUEST_CANCELLED = -10200; +constexpr int16_t ERROR_FILE_SERVER_UNAVAILABLE = -10300; constexpr int16_t ERROR_UNKNOWN = -11000; const std::pair content_type_plain_text = { @@ -238,6 +242,19 @@ struct UploadRequest : FileTransferRequest { std::optional ttl; }; +struct FileUploadRequest : FileTransferRequest { + std::filesystem::path file; + attachment::Domain domain = attachment::Domain::ATTACHMENT; + bool allow_large = false; + std::optional ttl; + + // Hides FileTransferRequest::on_complete: this version includes the decryption key + // alongside the file metadata on success. + std::function, int16_t> result, bool timeout)> + on_complete; +}; + struct DownloadRequest : FileTransferRequest { std::string download_url; diff --git a/src/attachments.cpp b/src/attachments.cpp index 428e9c85b..55df70488 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -712,8 +712,12 @@ namespace { auto& ss_st(std::byte (&data)[52]) { return *reinterpret_cast(data); } - auto* uc(std::byte* p) { return reinterpret_cast(p); } - auto* uc(const std::byte* p) { return reinterpret_cast(p); } + auto* uc(std::byte* p) { + return reinterpret_cast(p); + } + auto* uc(const std::byte* p) { + return reinterpret_cast(p); + } } // namespace Encryptor::Encryptor(std::span seed, Domain domain) { @@ -722,12 +726,7 @@ Encryptor::Encryptor(std::span seed, Domain domain) { const auto& pers = domain_pers(domain); crypto_generichash_blake2b_init_salt_personal( - &b2b_st(hash_st_data), - uc(seed.data()), - 32, - nonce_key.size(), - nullptr, - pers.data()); + &b2b_st(hash_st_data), uc(seed.data()), 32, nonce_key.size(), nullptr, pers.data()); } void Encryptor::update_key(std::span data) { @@ -862,33 +861,52 @@ std::span Encryptor::next() { return {out_buf.data(), out_size}; } -std::pair Encryptor::from_file( - std::span seed, - Domain domain, +cleared_b32 Encryptor::load_key_from_file( const std::filesystem::path& file, - bool allow_large) { - Encryptor enc{seed, domain}; - + bool allow_large, + std::function progress) { auto in = std::make_shared(); in->exceptions(std::ios::badbit); - in->open(file, std::ios::binary); + in->open(file, std::ios::binary | std::ios::ate); + int64_t total = in->tellg(); + in->seekg(0, std::ios::beg); - std::array chunk; - while (in->read(reinterpret_cast(chunk.data()), chunk.size())) - enc.update_key(chunk); - if (in->gcount() > 0) - enc.update_key(std::span{chunk}.first(in->gcount())); + // Phase 1: hash the file + constexpr size_t READ_SIZE = 65536; + std::vector chunk(READ_SIZE); + int64_t read_so_far = 0; + while (in->read(reinterpret_cast(chunk.data()), chunk.size())) { + update_key(chunk); + read_so_far += chunk.size(); + if (progress) + progress(read_so_far, total); + } + if (in->gcount() > 0) { + update_key(std::span{chunk}.first(in->gcount())); + read_so_far += in->gcount(); + if (progress) + progress(read_so_far, total); + } + // Seek back for phase 2 in->clear(); in->seekg(0, std::ios::beg); - auto key = enc.start_encryption( + return start_encryption( [in](std::span buffer) -> size_t { in->read(reinterpret_cast(buffer.data()), buffer.size()); return in->gcount(); }, allow_large); +} +std::pair Encryptor::from_file( + std::span seed, + Domain domain, + const std::filesystem::path& file, + bool allow_large) { + Encryptor enc{seed, domain}; + auto key = enc.load_key_from_file(file, allow_large); return {std::move(enc), std::move(key)}; } diff --git a/src/network/backends/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp index 2e22a2152..5f7e5a589 100644 --- a/src/network/backends/quic_file_client.cpp +++ b/src/network/backends/quic_file_client.cpp @@ -7,12 +7,15 @@ #include #include +#include +#include #include #include #include #include #include +#include "session/clock.hpp" #include "session/ed25519.hpp" using namespace oxen; @@ -47,9 +50,8 @@ QuicFileClient::QuicFileClient( // Set up TLS credentials auto key_pair = ed25519::ed25519_key_pair(); - _creds = quic::GNUTLSCreds::make_from_ed_seckey( - std::string_view{reinterpret_cast(key_pair.second.data()), - key_pair.second.size()}); + _creds = quic::GNUTLSCreds::make_from_ed_seckey(std::string_view{ + reinterpret_cast(key_pair.second.data()), key_pair.second.size()}); // Enable 0RTT if callbacks are provided if (_ticket_store && _ticket_extract) { @@ -60,8 +62,8 @@ QuicFileClient::QuicFileClient( std::chrono::sys_seconds expiry) { store(oxenc::to_hex(remote.view_remote_key()), std::move(data), expiry); }, - [extract = _ticket_extract]( - const quic::RemoteAddress& remote) -> std::optional> { + [extract = _ticket_extract](const quic::RemoteAddress& remote) + -> std::optional> { return extract(oxenc::to_hex(remote.view_remote_key())); }); } @@ -115,10 +117,7 @@ std::shared_ptr QuicFileClient::_ensure_connection() { if (_conn) return _conn; - auto remote = quic::RemoteAddress{ - oxenc::from_hex(_ed_pubkey.hex()), - _address, - _port}; + auto remote = quic::RemoteAddress{oxenc::from_hex(_ed_pubkey.hex()), _address, _port}; log::info(cat, "Connecting to QUIC file server at {}:{}", _address, _port); @@ -128,9 +127,7 @@ std::shared_ptr QuicFileClient::_ensure_connection() { quic::opt::outbound_alpn(QUIC_FILES_ALPN), quic::opt::handshake_timeout{10s}, quic::opt::keep_alive{10s}, - [this](quic::Connection&) { - log::info(cat, "Connected to QUIC file server."); - }, + [this](quic::Connection&) { log::info(cat, "Connected to QUIC file server."); }, [this](quic::Connection&, uint64_t ec) { if (ec) log::warning(cat, "Connection to QUIC file server failed (error {}).", ec); @@ -306,8 +303,7 @@ void QuicFileClient::download( // Phase 2: accumulate metadata bytes if (!state->metadata_parsed) { try { - if (!quic::data_accumulator( - state->meta_buf, data, state->meta_size)) + if (!quic::data_accumulator(state->meta_buf, data, state->meta_size)) return; } catch (const std::exception& e) { log::error(cat, "Download metadata accumulation error: {}", e.what()); @@ -350,10 +346,7 @@ void QuicFileClient::download( try { state->on_data(state->metadata, data); } catch (const std::exception& e) { - log::warning( - cat, - "Download aborted by on_data callback: {}", - e.what()); + log::warning(cat, "Download aborted by on_data callback: {}", e.what()); s.close(QUIC_FILES_CLIENT_ABORT); return; } @@ -391,10 +384,7 @@ void QuicFileClient::download( } log::info( - cat, - "Download complete: {} ({} bytes).", - state->file_id, - state->received); + cat, "Download complete: {} ({} bytes).", state->file_id, state->received); state->on_complete(state->metadata); }; @@ -419,4 +409,177 @@ void QuicFileClient::download( }); } +void streaming_file_upload( + std::shared_ptr loop, + attachment::Encryptor enc, + FileUploadRequest request, + std::function get_client) { + + struct upload_state { + std::mutex mutex; + std::condition_variable cv; + bool paused = false; + bool done = false; + QuicFileClient* client = nullptr; + std::shared_ptr stream; + std::string response_data; + std::optional> result; + }; + auto state = std::make_shared(); + + auto fail = [&](int16_t err) { + if (request.on_complete) + loop->call([request, err] { request.on_complete(err, false); }); + }; + + loop->call([state, get_client = std::move(get_client)] { + auto* client = get_client(); + std::lock_guard lock{state->mutex}; + if (client) + state->client = client; + else + state->done = true; + state->cv.notify_one(); + }); + + auto key = enc.load_key_from_file(request.file, request.allow_large); + auto upload_size = attachment::encrypted_size(enc.data_size()); + auto enc_ptr = std::make_shared(std::move(enc)); + + { + std::unique_lock lock{state->mutex}; + state->cv.wait( + lock, [&] { return state->client || state->done || request.is_cancelled(); }); + if (request.is_cancelled()) + return fail(ERROR_REQUEST_CANCELLED); + if (state->done) + return fail(ERROR_FILE_SERVER_UNAVAILABLE); + } + + loop->call_get([&] { + auto conn = state->client->_ensure_connection(); + if (!conn) { + std::lock_guard lock{state->mutex}; + state->done = true; + return; + } + + auto str = conn->open_stream( + [state](quic::Stream&, std::span incoming) { + state->response_data += std::string_view{ + reinterpret_cast(incoming.data()), incoming.size()}; + }, + [state, upload_size](quic::Stream&, uint64_t error_code) { + std::lock_guard lock{state->mutex}; + if (error_code != 0) { + state->result = static_cast(error_code); + } else if (state->response_data.empty()) { + state->result = static_cast(ERROR_UNKNOWN); + } else { + try { + oxenc::bt_dict_consumer resp{state->response_data}; + file_metadata meta{}; + meta.id = resp.require("#"); + meta.size = upload_size; + meta.uploaded = from_epoch_s(resp.require("u")); + meta.expiry = from_epoch_s(resp.require("x")); + resp.finish(); + state->result = std::move(meta); + } catch (const std::exception& e) { + log::warning( + cat, "Failed to parse streaming upload response: {}", e.what()); + state->result = static_cast(ERROR_UNKNOWN); + } + } + state->done = true; + state->cv.notify_one(); + }); + + constexpr size_t WATERMARK_ALARM = 512 * 1024; + constexpr size_t WATERMARK_CLEAR = 128 * 1024; + str->enable_watermarks( + WATERMARK_ALARM, + [state](quic::Stream&) { + std::lock_guard lock{state->mutex}; + state->paused = true; + }, + WATERMARK_CLEAR, + [state](quic::Stream&) { + { + std::lock_guard lock{state->mutex}; + state->paused = false; + } + state->cv.notify_one(); + }); + + oxenc::bt_dict_producer cmd; + cmd.append("!", "PUT"); + cmd.append("s", static_cast(upload_size)); + if (request.ttl) + cmd.append("t", static_cast(request.ttl->count())); + auto cmd_view = cmd.view(); + str->send(fmt::format("{}:{}", cmd_view.size(), cmd_view)); + + state->stream = std::move(str); + }); + + { + std::lock_guard lock{state->mutex}; + if (state->done) + return fail(ERROR_FILE_SERVER_UNAVAILABLE); + } + + auto check_cancelled = [&]() -> bool { + if (!request.is_cancelled()) + return false; + log::debug(cat, "Streaming file upload cancelled"); + loop->call([state, request] { + if (state->stream) + state->stream->close(QUIC_FILES_CLIENT_ABORT); + if (request.on_complete) + request.on_complete(ERROR_REQUEST_CANCELLED, false); + }); + return true; + }; + + for (auto chunk = enc_ptr->next(); !chunk.empty(); chunk = enc_ptr->next()) { + if (check_cancelled()) + return; + + { + std::unique_lock lock{state->mutex}; + state->cv.wait( + lock, [&] { return !state->paused || state->done || request.is_cancelled(); }); + if (check_cancelled()) + return; + if (state->done) + break; + } + + auto data = std::make_shared>(chunk.begin(), chunk.end()); + loop->call([state, data] { + if (state->stream) + state->stream->send(*data, data); + }); + } + + loop->call([state] { + if (state->stream) + state->stream->send_fin(); + }); + + { + std::unique_lock lock{state->mutex}; + state->cv.wait(lock, [&] { return state->done; }); + } + + if (request.on_complete && state->result) { + loop->call([request, result = std::move(*state->result), key] { + if (auto* meta = std::get_if(&result)) + request.on_complete(std::make_pair(std::move(*meta), key), false); + else + request.on_complete(std::get(result), false); + }); + } +} } // namespace session::network diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index 21140c2b8..7f2ae937f 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -130,12 +130,10 @@ static constexpr auto TESTNET_QUIC_FS_SESH_ADDRESS = std::optional default_quic_target( const config::FileServer& http_config, opt::netid::Target netid) { // Map known HTTP file server pubkeys to their QUIC file server .sesh addresses. - if (http_config.pubkey_hex == DEFAULT_CONFIG.pubkey_hex && - netid == opt::netid::Target::mainnet) + if (http_config.pubkey_hex == DEFAULT_CONFIG.pubkey_hex && netid == opt::netid::Target::mainnet) return SRouterTarget{std::string{MAINNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; - if (http_config.pubkey_hex == TESTNET_CONFIG.pubkey_hex && - netid == opt::netid::Target::testnet) + if (http_config.pubkey_hex == TESTNET_CONFIG.pubkey_hex && netid == opt::netid::Target::testnet) return SRouterTarget{std::string{TESTNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; return std::nullopt; diff --git a/src/network/network_config.cpp b/src/network/network_config.cpp index 3ea275e6d..d557ebf96 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -174,10 +174,7 @@ void Config::handle_config_opt(opt::quic_file_server_ed_pubkey qfep) { void Config::handle_config_opt(opt::quic_file_server_address qfa) { quic_file_server_address = std::move(qfa.address); - log::debug( - cat, - "Network config QUIC file server address set to {}", - *quic_file_server_address); + log::debug(cat, "Network config QUIC file server address set to {}", *quic_file_server_address); } void Config::handle_config_opt(opt::quic_file_server_port qfp) { diff --git a/src/network/routing/direct_router.cpp b/src/network/routing/direct_router.cpp index c7293eadb..8c12bef00 100644 --- a/src/network/routing/direct_router.cpp +++ b/src/network/routing/direct_router.cpp @@ -92,6 +92,49 @@ void DirectRouter::upload(UploadRequest request) { }); } +void DirectRouter::upload_file(FileUploadRequest request, std::span seed) { + if (!_config.quic_file_server_address || !_config.quic_file_server_ed_pubkey) { + if (request.on_complete) + request.on_complete(ERROR_FILE_SERVER_UNAVAILABLE, false); + return; + } + + attachment::Encryptor enc{seed, request.domain}; + auto address = *_config.quic_file_server_address; + auto pubkey_hex = *_config.quic_file_server_ed_pubkey; + auto port = _config.quic_file_server_port; + const auto upload_id = random::unique_id("UPL"); + + auto& upload_thread = + _active_uploads.emplace(upload_id, std::make_pair(UploadRequest{}, std::thread{})) + .first->second.second; + + upload_thread = std::thread([weak_self = weak_from_this(), + this, + enc = std::move(enc), + request = std::move(request), + address, + pubkey_hex, + port, + upload_id]() mutable { + streaming_file_upload( + _loop, + std::move(enc), + std::move(request), + [weak_self, this, address, pubkey_hex, port]() -> QuicFileClient* { + auto self = weak_self.lock(); + if (!self) + return nullptr; + return &_get_file_client(ed25519_pubkey::from_hex(pubkey_hex), address, port); + }); + + _loop->call([weak_self = weak_from_this(), this, upload_id] { + if (auto self = weak_self.lock()) + _cleanup_upload(upload_id); + }); + }); +} + void DirectRouter::download(DownloadRequest request) { _loop->call([weak_self = weak_from_this(), req = std::move(request)] { if (auto self = weak_self.lock()) @@ -178,8 +221,7 @@ QuicFileClient& DirectRouter::_get_file_client( const ed25519_pubkey& pubkey, std::string_view address, uint16_t port) { auto [it, inserted] = _file_clients.try_emplace(pubkey, nullptr); if (inserted) - it->second = std::make_unique( - _loop, pubkey, std::string{address}, port); + it->second = std::make_unique(_loop, pubkey, std::string{address}, port); else it->second->set_target(pubkey, std::string{address}, port); return *it->second; @@ -197,9 +239,8 @@ void DirectRouter::_upload_internal(UploadRequest request) { return; } - auto& upload_thread = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second.second; + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; auto address = *_config.quic_file_server_address; auto pubkey_hex = *_config.quic_file_server_ed_pubkey; @@ -306,9 +347,8 @@ void DirectRouter::_upload_internal(UploadRequest request) { } void DirectRouter::_upload_internal_legacy(UploadRequest request, std::string upload_id) { - auto& upload_thread = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second.second; + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; upload_thread = std::thread([weak_self = weak_from_this(), this, @@ -445,7 +485,8 @@ void DirectRouter::_download_internal(DownloadRequest request) { auto& client = _get_file_client(pubkey, address, port); - log::debug(cat, "[Download {}]: Downloading {} from {}:{}.", download_id, file_id, address, port); + log::debug( + cat, "[Download {}]: Downloading {} from {}:{}.", download_id, file_id, address, port); client.download( std::move(file_id), diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index 5e4c0706b..c7db1f631 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -562,6 +562,79 @@ void OnionRequestRouter::upload(UploadRequest request) { }); } +void OnionRequestRouter::upload_file(FileUploadRequest request, std::span seed) { + attachment::Encryptor enc{seed, request.domain}; + const auto upload_id = random::unique_id("UPL"); + + auto& upload_thread = + _active_uploads.emplace(upload_id, std::make_pair(UploadRequest{}, std::thread{})) + .first->second.second; + + upload_thread = std::thread([weak_self = weak_from_this(), + this, + enc = std::move(enc), + request = std::move(request), + upload_id, + file_server_config = _config.file_server_config]() mutable { + try { + auto key = enc.load_key_from_file(request.file, request.allow_large); + auto enc_size = attachment::encrypted_size(enc.data_size()); + + // Accumulate all encrypted output into a buffer (onion requests require the + // full payload upfront). + std::vector all_data; + all_data.reserve(enc_size); + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) { + auto* p = reinterpret_cast(chunk.data()); + all_data.insert(all_data.end(), p, p + chunk.size()); + } + + // Build the one-shot Request via to_request (needs an UploadRequest with next_data) + UploadRequest legacy_req; + legacy_req.request_timeout = request.request_timeout; + legacy_req.overall_timeout = request.overall_timeout; + legacy_req.stall_timeout = request.stall_timeout; + legacy_req.ttl = request.ttl; + auto data_ptr = std::make_shared>(std::move(all_data)); + bool consumed = false; + legacy_req.next_data = [data_ptr, + consumed]() mutable -> std::vector { + if (consumed) + return {}; + consumed = true; + return std::move(*data_ptr); + }; + + auto req = file_server::to_request(upload_id, file_server_config, legacy_req); + + // Wrap the FileUploadRequest callback to inject the key on success + _dispatch_upload( + upload_id, + std::move(req), + [request] { return request.is_cancelled(); }, + [request, key](std::variant result, bool timeout) { + if (!request.on_complete) + return; + if (auto* meta = std::get_if(&result)) + request.on_complete( + std::make_pair(std::move(*meta), key), timeout); + else + request.on_complete(std::get(result), timeout); + }); + } catch (const std::exception& e) { + log::error(cat, "[Upload {}]: File upload failed: {}", upload_id, e.what()); + _loop->call([weak_self = weak_from_this(), this, request, upload_id] { + auto self = weak_self.lock(); + if (!self) + return; + _cleanup_upload(upload_id); + if (request.on_complete) + request.on_complete(ERROR_UNKNOWN, false); + }); + } + }); +} + void OnionRequestRouter::download(DownloadRequest request) { _loop->call([weak_self = weak_from_this(), req = std::move(request)] { if (auto self = weak_self.lock()) @@ -850,6 +923,106 @@ void OnionRequestRouter::_send_request_internal( } } +void OnionRequestRouter::_cleanup_upload(const std::string& upload_id) { + auto node = _active_uploads.extract(upload_id); + if (!node.empty()) { + auto& thread = node.mapped().second; + if (thread.joinable()) + thread.join(); + } +} + +void OnionRequestRouter::_dispatch_upload( + std::string upload_id, + Request req, + std::function is_cancelled, + std::function, bool)> on_result) { + _loop->call([weak_self = weak_from_this(), + this, + upload_id, + req = std::move(req), + is_cancelled = std::move(is_cancelled), + on_result = std::move(on_result)]() mutable { + auto self = weak_self.lock(); + if (!self) + return; + + if (is_cancelled() || !req.body) { + log::debug(cat, "[Upload {}]: Cancelled before sending request.", upload_id); + on_result(ERROR_REQUEST_CANCELLED, false); + _cleanup_upload(upload_id); + return; + } + + const auto upload_size = req.body->size(); + log::debug( + cat, + "[Upload {}]: Accumulated {} bytes, sending request.", + upload_id, + upload_size); + + _send_request_internal( + std::move(req), + [weak_self, this, upload_id, is_cancelled, on_result, upload_size]( + bool success, + bool timeout, + int16_t status_code, + std::vector> headers, + std::optional body) { + auto self = weak_self.lock(); + if (!self) + return; + + _cleanup_upload(upload_id); + + try { + if (is_cancelled()) + throw cancellation_exception{"Cancelled during request."}; + + if (!success || timeout) + throw status_code_exception{ + status_code, + headers, + fmt::format( + "Request failed with status {}, timeout={}.", + status_code, + timeout)}; + + if (!body) + throw std::runtime_error{"No response body."}; + + auto metadata = + file_server::parse_upload_response(*body, upload_size); + log::info( + cat, + "[Upload {}]: Successfully uploaded {} bytes as file ID: {}", + upload_id, + metadata.size, + metadata.id); + + on_result(std::move(metadata), false); + } catch (const cancellation_exception&) { + log::error(cat, "[Upload {}]: Cancelled", upload_id); + on_result(ERROR_REQUEST_CANCELLED, false); + } catch (const status_code_exception& e) { + log::error( + cat, + "[Upload {}]: Failure with error: {}", + upload_id, + e.what()); + on_result(e.status_code, false); + } catch (const std::exception& e) { + log::error( + cat, + "[Upload {}]: Failure with error: {}", + upload_id, + e.what()); + on_result(ERROR_UNKNOWN, false); + } + }); + }); +} + void OnionRequestRouter::_upload_internal(UploadRequest request) { const std::string upload_id = random::unique_id("UP"); log::info(cat, "[Upload {}]: Starting upload.", upload_id); @@ -874,100 +1047,14 @@ void OnionRequestRouter::_upload_internal(UploadRequest request) { if (!self) return; - // Onion requests don't support streaming data so we need to load all the data from the - // streaming source into memory try { - Request request = - file_server::to_request(upload_id, file_server_config, upload_request); - - _loop->call([weak_self, this, upload_request, req = std::move(request), upload_id] { - auto self = weak_self.lock(); - if (!self) - return; - - if (upload_request.is_cancelled() || !req.body) { - log::debug(cat, "[Upload {}]: Cancelled before sending request.", upload_id); - upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); - - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); - return; - } + auto req = file_server::to_request(upload_id, file_server_config, upload_request); - const auto upload_size = req.body->size(); - log::debug( - cat, - "[Upload {}]: Accumulated {} bytes, building request.", - upload_id, - upload_size); - - _send_request_internal( - std::move(req), - [weak_self, this, upload_id, upload_request, upload_size]( - bool success, - bool timeout, - int16_t status_code, - std::vector> headers, - std::optional body) { - auto self = weak_self.lock(); - if (!self) - return; - - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && - active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); - - try { - if (upload_request.is_cancelled()) - throw cancellation_exception{"Cancelled during request."}; - - if (!success || timeout) - throw status_code_exception{ - status_code, - headers, - fmt::format( - "Request failed with status {}, timeout={}.", - status_code, - timeout)}; - - if (!body) - throw std::runtime_error{"No response body."}; - - auto metadata = - file_server::parse_upload_response(*body, upload_size); - log::info( - cat, - "[Upload {}]: Successfully uploaded {} bytes as file ID: " - "{}", - upload_id, - metadata.size, - metadata.id); - - upload_request.on_complete(std::move(metadata), false); - } catch (const cancellation_exception&) { - log::error(cat, "[Upload {}]: Cancelled", upload_id); - upload_request.on_complete(ERROR_REQUEST_CANCELLED, false); - } catch (const status_code_exception& e) { - log::error( - cat, - "[Upload {}]: Failure with error: {}", - upload_id, - e.what()); - upload_request.on_complete(e.status_code, false); - } catch (const std::exception& e) { - log::error( - cat, - "[Upload {}]: Failure with error: {}", - upload_id, - e.what()); - upload_request.on_complete(ERROR_UNKNOWN, false); - } - }); - }); + _dispatch_upload( + upload_id, + std::move(req), + [upload_request] { return upload_request.is_cancelled(); }, + upload_request.on_complete); } catch (const std::exception& e) { log::error(cat, "[Upload {}]: Exception during upload: {}", upload_id, e.what()); @@ -975,12 +1062,7 @@ void OnionRequestRouter::_upload_internal(UploadRequest request) { auto self = weak_self.lock(); if (!self) return; - - // Join the thread to keep it alive during callback handling - auto active_upload_node = _active_uploads.extract(upload_id); - if (!active_upload_node.empty() && active_upload_node.mapped().second.joinable()) - active_upload_node.mapped().second.join(); - + _cleanup_upload(upload_id); upload_request.on_complete(ERROR_UNKNOWN, false); }); } diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index 76c32772b..bcedb13c2 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -22,6 +22,8 @@ using namespace oxen::log::literals; namespace session::network { +static std::optional pubkey_from_srouter_address(std::string_view address); + namespace { auto cat = oxen::log::Cat("session-router"); @@ -222,6 +224,61 @@ void SessionRouter::upload(UploadRequest request) { }); } +void SessionRouter::upload_file(FileUploadRequest request, std::span seed) { + auto quic_target = file_server::default_quic_target(_config.file_server_config, _config.netid); + if (!quic_target) { + // TODO: legacy file upload fallback + if (request.on_complete) + request.on_complete(ERROR_FILE_SERVER_UNAVAILABLE, false); + return; + } + + // Construct the Encryptor now, consuming the seed. + attachment::Encryptor enc{seed, request.domain}; + auto target = std::move(*quic_target); + + const std::string upload_id = random::unique_id("UPL"); + auto& upload_thread = + _active_uploads.emplace(upload_id, std::make_pair(UploadRequest{}, std::thread{})) + .first->second.second; + + upload_thread = std::thread([weak_self = weak_from_this(), + this, + enc = std::move(enc), + request = std::move(request), + target = std::move(target), + upload_id]() mutable { + // The get_client callback runs on the loop thread: it establishes the tunnel + // and returns the QuicFileClient. This blocks (via promise/future) until the + // tunnel is established. + auto client_promise = std::make_shared>(); + auto client_future = client_promise->get_future(); + + streaming_file_upload( + _loop, + std::move(enc), + std::move(request), + [weak_self, this, target, client_promise]() -> QuicFileClient* { + auto self = weak_self.lock(); + if (!self) + return nullptr; + + // Establish tunnel synchronously (we're on the loop thread here) + auto info = srouter->establish_udp(target.address, target.port); + auto pubkey = pubkey_from_srouter_address(info.remote); + if (!pubkey) + return nullptr; + + return &_get_file_client(*pubkey, "::1", info.local_port); + }); + + _loop->call([weak_self = weak_from_this(), this, upload_id] { + if (auto self = weak_self.lock()) + _cleanup_upload(upload_id); + }); + }); +} + void SessionRouter::download(DownloadRequest request) { _loop->call([weak_self = weak_from_this(), req = std::move(request)] { if (auto self = weak_self.lock()) @@ -580,8 +637,7 @@ QuicFileClient& SessionRouter::_get_file_client( const ed25519_pubkey& pubkey, std::string_view address, uint16_t port) { auto [it, inserted] = _file_clients.try_emplace(pubkey, nullptr); if (inserted) - it->second = std::make_unique( - _loop, pubkey, std::string{address}, port); + it->second = std::make_unique(_loop, pubkey, std::string{address}, port); else it->second->set_target(pubkey, std::string{address}, port); return *it->second; @@ -605,8 +661,7 @@ void SessionRouter::_quic_upload_via_tunnel( } _get_file_client(*pubkey, "::1", info.local_port) - .upload( - std::move(data), + .upload(std::move(data), upload_request.ttl, [weak_self = weak_from_this(), this, upload_request, upload_id]( std::variant result) { @@ -616,10 +671,7 @@ void SessionRouter::_quic_upload_via_tunnel( if (auto* meta = std::get_if(&result)) log::info( - cat, - "[Upload {}]: Success, file ID: {}", - upload_id, - meta->id); + cat, "[Upload {}]: Success, file ID: {}", upload_id, meta->id); else log::error( cat, @@ -692,9 +744,8 @@ void SessionRouter::_upload_internal(UploadRequest request) { } // QUIC upload: accumulate data on background thread, then tunnel and upload - auto& upload_thread = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second.second; + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; upload_thread = std::thread([weak_self = weak_from_this(), this, @@ -787,9 +838,8 @@ void SessionRouter::_upload_internal(UploadRequest request) { // Legacy HTTP-based upload path, used when no QUIC file server target is available. void SessionRouter::_upload_internal_legacy(UploadRequest request, std::string upload_id) { - auto& upload_thread = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->second.second; + auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) + .first->second.second; upload_thread = std::thread([weak_self = weak_from_this(), this, @@ -910,8 +960,7 @@ void SessionRouter::_download_internal(DownloadRequest request) { if (download_info && download_info->srouter_target) quic_target = std::move(download_info->srouter_target); else - quic_target = - file_server::default_quic_target(_config.file_server_config, _config.netid); + quic_target = file_server::default_quic_target(_config.file_server_config, _config.netid); if (!quic_target || !download_info) { _download_internal_legacy(std::move(request), std::move(download_id)); @@ -933,10 +982,7 @@ void SessionRouter::_download_internal(DownloadRequest request) { }, [weak_self = weak_from_this(), this, request, download_id]() { if (auto self = weak_self.lock()) { - log::error( - cat, - "[Download {}]: Tunnel establishment timed out.", - download_id); + log::error(cat, "[Download {}]: Tunnel establishment timed out.", download_id); _active_downloads.erase(download_id); request.on_complete(ERROR_BUILD_TIMEOUT, true); } diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 0e878ac13..71313ab55 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -42,11 +42,19 @@ namespace { "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"sv; constexpr auto clock_out_of_sync_error = "Clock out of sync"; + // Checks a precondition and, if true, fires the request's on_complete with the given error. + // Returns true if the condition was met (i.e. the caller should return). + template + bool fail_if(Req& req, bool cond, int16_t err) { + if (cond && req.on_complete) + req.on_complete(err, false); + return cond; + } + config::FileServer build_file_server_config(const config::Config& main_config) { - config::FileServer file_server_config = - main_config.netid == opt::netid::Target::testnet - ? file_server::TESTNET_CONFIG - : file_server::DEFAULT_CONFIG; + config::FileServer file_server_config = main_config.netid == opt::netid::Target::testnet + ? file_server::TESTNET_CONFIG + : file_server::DEFAULT_CONFIG; file_server_config.use_stream_encryption = main_config.file_server_use_stream_encryption; if (main_config.custom_file_server_scheme) @@ -92,8 +100,7 @@ namespace { main_config.netid, main_config.quic_file_server_address, main_config.quic_file_server_ed_pubkey, - main_config.quic_file_server_port.value_or( - file_server::QUIC_DEFAULT_PORT)}; + main_config.quic_file_server_port.value_or(file_server::QUIC_DEFAULT_PORT)}; } config::SessionRouter build_session_router_config( @@ -516,21 +523,12 @@ void Network::send_request(Request request, network_response_callback_t callback } void Network::upload(UploadRequest request) { - if (_suspended) { - if (request.on_complete) - request.on_complete(ERROR_NETWORK_SUSPENDED, false); + if (fail_if(request, _suspended, ERROR_NETWORK_SUSPENDED)) return; - } - if (!_transport) { - if (request.on_complete) - request.on_complete(ERROR_NO_TRANSPORT_LAYER, false); + if (fail_if(request, !_transport, ERROR_NO_TRANSPORT_LAYER)) return; - } - if (!_router) { - if (request.on_complete) - request.on_complete(ERROR_NO_ROUTING_LAYER, false); + if (fail_if(request, !_router, ERROR_NO_ROUTING_LAYER)) return; - } auto user_callback = request.on_complete; request.on_complete = [weak_self = weak_from_this(), this, user_callback]( @@ -561,25 +559,54 @@ void Network::upload(UploadRequest request) { user_callback(std::move(result), timeout); }; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" _router->upload(std::move(request)); +#pragma GCC diagnostic pop +} + +void Network::upload_file(FileUploadRequest request, std::span seed) { + if (fail_if(request, _suspended, ERROR_NETWORK_SUSPENDED)) + return; + if (fail_if(request, !_transport, ERROR_NO_TRANSPORT_LAYER)) + return; + if (fail_if(request, !_router, ERROR_NO_ROUTING_LAYER)) + return; + + auto user_callback = request.on_complete; + request.on_complete = + [weak_self = weak_from_this(), this, user_callback]( + std::variant, int16_t> result, + bool timeout) { + auto self = weak_self.lock(); + if (!self) + return; + + if (auto* status_code = std::get_if(&result)) { + if (*status_code == ERROR_TOO_EARLY) { + log::info(cat, "File upload received 425, triggering clock resync."); + _resync_clock(std::nullopt, std::nullopt); + + if (user_callback) + user_callback(*status_code, timeout); + return; + } + } + + if (user_callback) + user_callback(std::move(result), timeout); + }; + + _router->upload_file(std::move(request), seed); } void Network::download(DownloadRequest request) { - if (_suspended) { - if (request.on_complete) - request.on_complete(ERROR_NETWORK_SUSPENDED, false); + if (fail_if(request, _suspended, ERROR_NETWORK_SUSPENDED)) return; - } - if (!_transport) { - if (request.on_complete) - request.on_complete(ERROR_NO_TRANSPORT_LAYER, false); + if (fail_if(request, !_transport, ERROR_NO_TRANSPORT_LAYER)) return; - } - if (!_router) { - if (request.on_complete) - request.on_complete(ERROR_NO_ROUTING_LAYER, false); + if (fail_if(request, !_router, ERROR_NO_ROUTING_LAYER)) return; - } auto user_callback = request.on_complete; request.on_complete = [weak_self = weak_from_this(), this, user_callback, req = request]( @@ -1858,8 +1885,11 @@ LIBSESSION_C_API session_upload_handle_t* session_network_upload( result); }; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" handle->cancelled = cpp_request.cancelled; unbox(network)->upload(std::move(cpp_request)); +#pragma GCC diagnostic pop return handle.release(); } catch (...) { diff --git a/tests/dns_utils.hpp b/tests/dns_utils.hpp index 377961081..be94abdc6 100644 --- a/tests/dns_utils.hpp +++ b/tests/dns_utils.hpp @@ -13,15 +13,14 @@ namespace session::test { // Resolves a hostname to an IP address string via getaddrinfo. This is needed because libquic // does not perform DNS resolution. inline std::string resolve_host(const std::string& host) { - struct addrinfo hints {}; + struct addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; struct addrinfo* res = nullptr; if (int rc = getaddrinfo(host.c_str(), nullptr, &hints, &res); rc != 0 || !res) throw std::runtime_error{ - "Failed to resolve '" + host + "': " + - (rc ? gai_strerror(rc) : "no results")}; + "Failed to resolve '" + host + "': " + (rc ? gai_strerror(rc) : "no results")}; char buf[INET6_ADDRSTRLEN]{}; if (res->ai_family == AF_INET6) @@ -32,10 +31,7 @@ inline std::string resolve_host(const std::string& host) { sizeof(buf)); else inet_ntop( - AF_INET, - &reinterpret_cast(res->ai_addr)->sin_addr, - buf, - sizeof(buf)); + AF_INET, &reinterpret_cast(res->ai_addr)->sin_addr, buf, sizeof(buf)); freeaddrinfo(res); return buf; diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp index 167f66222..a5225374d 100644 --- a/tests/live/live_utils.hpp +++ b/tests/live/live_utils.hpp @@ -48,8 +48,8 @@ inline std::shared_ptr make_testnet_network( // because libquic does not do DNS resolution. if (live_router_mode.type == opt::router::Type::direct) { net_opts.push_back(opt::quic_file_server_ed_pubkey{TESTNET_QUIC_FS_ED_PUBKEY}); - net_opts.push_back(opt::quic_file_server_address{ - session::test::resolve_host(TESTNET_QUIC_FS_HOST)}); + net_opts.push_back( + opt::quic_file_server_address{session::test::resolve_host(TESTNET_QUIC_FS_HOST)}); } return std::make_shared(net_opts); diff --git a/tests/live/test_file_transfer.cpp b/tests/live/test_file_transfer.cpp index b7966dc84..b80f1b5ca 100644 --- a/tests/live/test_file_transfer.cpp +++ b/tests/live/test_file_transfer.cpp @@ -1,9 +1,11 @@ -#include #include +#include +#include #include #include #include +#include #include #include "live_utils.hpp" @@ -69,6 +71,53 @@ TEST_CASE("Live: file upload via QUIC", "[live][file]") { CHECK(meta.size > 0); } +TEST_CASE("Live: streaming file upload via upload_file", "[live][file]") { + auto core = make_live_core(); + auto net = core->network(); + REQUIRE(net); + + // Write test data to a temp file + auto tmp = std::filesystem::temp_directory_path() / "upload_file_test.dat"; + { + std::vector plaintext(8192); + randombytes_buf(plaintext.data(), plaintext.size()); + std::ofstream f{tmp, std::ios::binary}; + REQUIRE(f); + f.write(reinterpret_cast(plaintext.data()), plaintext.size()); + } + + std::array seed; + randombytes_buf(seed.data(), seed.size()); + + std::promise, int16_t>> + promise; + auto future = promise.get_future(); + + network::FileUploadRequest req; + req.file = tmp; + req.domain = attachment::Domain::ATTACHMENT; + req.allow_large = true; + req.ttl = 1min; + req.request_timeout = 30s; + req.overall_timeout = LIVE_TIMEOUT; + req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; + + net->upload_file(std::move(req), seed); + + REQUIRE(future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); + auto result = future.get(); + + std::filesystem::remove(tmp); + + using pair_t = std::pair; + REQUIRE(std::holds_alternative(result)); + + auto& [meta, key] = std::get(result); + CHECK(!meta.id.empty()); + CHECK(meta.size > 0); + CHECK(!key.empty()); +} + TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") { // Works under all routing modes: --srouter and --direct use the QUIC file server protocol, // --onionreq falls back to the legacy HTTP proxy path. @@ -107,7 +156,9 @@ TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") reinterpret_cast(encrypted.data() + encrypted.size())}; }; upload_req.ttl = 1min; - upload_req.on_complete = [&](auto result, bool) { upload_promise.set_value(std::move(result)); }; + upload_req.on_complete = [&](auto result, bool) { + upload_promise.set_value(std::move(result)); + }; net->upload(std::move(upload_req)); @@ -129,8 +180,7 @@ TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") download_req.request_timeout = 30s; download_req.overall_timeout = LIVE_TIMEOUT; download_req.on_data = [&](auto&, std::span data) { - downloaded_data.insert( - downloaded_data.end(), data.begin(), data.end()); + downloaded_data.insert(downloaded_data.end(), data.begin(), data.end()); }; download_req.on_complete = [&](auto result, bool) { download_promise.set_value(std::move(result)); @@ -143,8 +193,7 @@ TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") REQUIRE(std::holds_alternative(download_result)); // Decrypt and verify - auto decrypted = attachment::decrypt( - std::span{downloaded_data}, key); + auto decrypted = attachment::decrypt(std::span{downloaded_data}, key); REQUIRE(decrypted.size() == plaintext.size()); CHECK(decrypted == plaintext); } diff --git a/tests/quic-files.cpp b/tests/quic-files.cpp index 1c05e5bf8..56452cac7 100644 --- a/tests/quic-files.cpp +++ b/tests/quic-files.cpp @@ -36,21 +36,9 @@ namespace net = session::network; namespace fs = net::file_server; namespace attachment = session::attachment; -using transfer_result = std::variant; -using on_complete_t = std::function; -using on_data_t = - std::function)>; - -std::vector read_file(const std::filesystem::path& path) { - std::ifstream f{path, std::ios::binary | std::ios::ate}; - if (!f) - throw std::runtime_error{fmt::format("Failed to open {}", path.string())}; - auto size = f.tellg(); - f.seekg(0); - std::vector data(size); - f.read(reinterpret_cast(data.data()), size); - return data; -} +using upload_result = std::variant, int16_t>; +using download_result = std::variant; +using on_data_t = std::function)>; using session::test::resolve_host; @@ -60,41 +48,41 @@ int do_upload( const std::string& filename, attachment::Domain domain, std::optional ttl, - std::function, std::optional, on_complete_t)> - initiate, + std::shared_ptr network, std::string download_cmd_hint) { auto path = std::filesystem::path{filename}; - log::info(logcat, "Reading {}...", path.string()); - auto plaintext = read_file(path); - log::info(logcat, "Plaintext size: {}", human_size{static_cast(plaintext.size())}); + if (!std::filesystem::exists(path)) { + log::error(logcat, "File not found: {}", path.string()); + return 1; + } + auto file_size = static_cast(std::filesystem::file_size(path)); + log::info(logcat, "Uploading {} ({})...", path.string(), human_size{file_size}); std::array seed; randombytes_buf(seed.data(), seed.size()); - log::info( - logcat, - "Encrypting (domain: {})...", - domain == attachment::Domain::PROFILE_PIC ? "profile-pic" : "attachment"); - auto [encrypted, key] = attachment::encrypt(seed, plaintext, domain, true); - auto enc_size = static_cast(encrypted.size()); - log::info(logcat, "Encrypted size: {}", human_size{enc_size}); - auto start = clock::now(); - std::promise promise; + std::promise promise; auto future = promise.get_future(); - log::info(logcat, "Uploading {}...", human_size{enc_size}); - initiate( - std::move(encrypted), - ttl, - [&](transfer_result r) { promise.set_value(std::move(r)); }); + net::FileUploadRequest req; + req.file = path; + req.domain = domain; + req.allow_large = true; + req.ttl = ttl; + req.request_timeout = 60s; + req.overall_timeout = 300s; + req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; + + network->upload_file(std::move(req), seed); auto result = future.get(); auto elapsed_s = std::chrono::duration(clock::now() - start).count(); - if (auto* meta = std::get_if(&result)) { + if (auto* pair = std::get_if>(&result)) { + auto& [meta, key] = *pair; auto key_hex = oxenc::to_hex(key.begin(), key.end()); - auto speed = human_size{static_cast(enc_size / std::max(elapsed_s, 0.001))}; + auto speed = human_size{static_cast(meta.size / std::max(elapsed_s, 0.001))}; log::info( logcat, @@ -107,13 +95,13 @@ int do_upload( "\n" "To download:\n" "{} {} {}", - meta->id, + meta.id, key_hex, - human_size{meta->size}, + human_size{meta.size}, elapsed_s, speed, download_cmd_hint, - meta->id, + meta.id, key_hex); return 0; } @@ -125,7 +113,7 @@ int do_upload( int do_download( const std::string& key_hex, const std::string& output, - std::function initiate) { + std::function)> initiate) { if (key_hex.size() != 64 || !oxenc::is_hex(key_hex)) { log::error(logcat, "Invalid key: expected 64 hex characters"); return 1; @@ -145,12 +133,14 @@ int do_download( int64_t decrypted_bytes = 0; attachment::Decryptor decryptor{key, [&](std::span decrypted) { - out_stream->write(reinterpret_cast(decrypted.data()), decrypted.size()); - decrypted_bytes += decrypted.size(); - }}; + out_stream->write( + reinterpret_cast(decrypted.data()), + decrypted.size()); + decrypted_bytes += decrypted.size(); + }}; auto start = clock::now(); - std::promise promise; + std::promise promise; auto future = promise.get_future(); int64_t received_bytes = 0; bool first_data = true; @@ -193,7 +183,7 @@ int do_download( last_progress_bytes = received_bytes; } }, - [&](transfer_result r) { promise.set_value(std::move(r)); }); + [&](download_result r) { promise.set_value(std::move(r)); }); auto result = future.get(); auto elapsed_s = std::chrono::duration(clock::now() - start).count(); @@ -208,8 +198,7 @@ int do_download( return 1; } - auto speed = human_size{ - static_cast(received_bytes / std::max(elapsed_s, 0.001))}; + auto speed = human_size{static_cast(received_bytes / std::max(elapsed_s, 0.001))}; log::info( logcat, "Download complete: {} encrypted, {} decrypted in {:.1f}s ({}/s)", @@ -272,7 +261,8 @@ int run(const CliArgs& args, bool is_upload) { if (!args.srouter) { if (args.server_pubkey_hex.empty() || args.server_pubkey_hex.size() != 64 || !oxenc::is_hex(args.server_pubkey_hex)) { - fmt::print(stderr, "Error: --server (64 hex Ed25519 pubkey) is required for --direct\n"); + fmt::print( + stderr, "Error: --server (64 hex Ed25519 pubkey) is required for --direct\n"); return 1; } if (args.server_address.empty()) { @@ -297,36 +287,22 @@ int run(const CliArgs& args, bool is_upload) { auto network = std::make_shared(net_opts); - std::string mode_hint = args.srouter - ? fmt::format("{} --srouter{}", args.argv0, args.testnet ? " --testnet" : "") - : fmt::format("{} --direct --server {} --address {} --port {}", - args.argv0, args.server_pubkey_hex, args.server_address, args.server_port); + std::string mode_hint = + args.srouter + ? fmt::format("{} --srouter{}", args.argv0, args.testnet ? " --testnet" : "") + : fmt::format( + "{} --direct --server {} --address {} --port {}", + args.argv0, + args.server_pubkey_hex, + args.server_address, + args.server_port); if (is_upload) { return do_upload( args.upload_filename, args.domain, args.ttl, - [&](auto data, auto ttl, auto cb) { - auto uc = std::make_shared>( - reinterpret_cast(data.data()), - reinterpret_cast(data.data() + data.size())); - bool consumed = false; - net::UploadRequest req; - req.request_timeout = 60s; - req.overall_timeout = 300s; - req.ttl = ttl; - req.next_data = [uc, consumed]() mutable -> std::vector { - if (consumed) - return {}; - consumed = true; - return std::move(*uc); - }; - req.on_complete = [cb = std::move(cb)](auto r, bool) { - cb(std::move(r)); - }; - network->upload(std::move(req)); - }, + network, fmt::format("{} download", mode_hint)); } @@ -338,7 +314,7 @@ int run(const CliArgs& args, bool is_upload) { download_url = fs::generate_download_url(args.dl_source, network->file_server_config); log::info(logcat, "Downloading: {}", download_url); - return do_download(args.dl_key_hex, args.dl_output, [&](auto on_data, auto cb) { + return do_download(args.dl_key_hex, args.dl_output, [&](on_data_t on_data, auto cb) { net::DownloadRequest req; req.download_url = download_url; req.request_timeout = 60s; @@ -387,7 +363,8 @@ int main(int argc, char* argv[]) { upload_cmd->add_option("filename", args.upload_filename, "File to upload")->required(); upload_cmd->add_flag("--profile-pic", profile_pic, "Use PROFILE_PIC encryption domain"); upload_cmd->add_flag("--max-ttl", max_ttl, "Use server's maximum TTL instead of default 1h"); - upload_cmd->add_option("--ttl", ttl_seconds, "TTL in seconds (default: 3600; ignored if --max-ttl)"); + upload_cmd->add_option( + "--ttl", ttl_seconds, "TTL in seconds (default: 3600; ignored if --max-ttl)"); auto* download_cmd = app.add_subcommand("download", "Download and decrypt a file"); download_cmd @@ -421,8 +398,8 @@ int main(int argc, char* argv[]) { namespace log = oxen::log; auto log_type = std::count(print_vals.begin(), print_vals.end(), log_file) ? log::Type::Print - : log_file == "syslog" ? log::Type::System - : log::Type::File; + : log_file == "syslog" ? log::Type::System + : log::Type::File; log::add_sink(log_type, log_file); auto cats = log::extract_categories(log_level); diff --git a/tests/test_attachment_encrypt.cpp b/tests/test_attachment_encrypt.cpp index 2de3e46bd..3d5f82eae 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -464,16 +464,7 @@ TEST_CASE( TEST_CASE("Streaming Encryptor", "[attachments][encryptor]") { - auto DATA_SIZE = GENERATE( - 0, - 1, - 100, - 1000, - 4053, - 8150, - 32768, - 65536, - 100000); + auto DATA_SIZE = GENERATE(0, 1, 100, 1000, 4053, 8150, 32768, 65536, 100000); auto seed = "9123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; const auto data = make_data(DATA_SIZE); @@ -492,13 +483,12 @@ TEST_CASE("Streaming Encryptor", "[attachments][encryptor]") { // Phase 2: start encryption with a pull source size_t src_pos = 0; - auto key = enc.start_encryption( - [&](std::span buf) -> size_t { - size_t avail = std::min(buf.size(), data.size() - src_pos); - std::memcpy(buf.data(), data.data() + src_pos, avail); - src_pos += avail; - return avail; - }); + auto key = enc.start_encryption([&](std::span buf) -> size_t { + size_t avail = std::min(buf.size(), data.size() - src_pos); + std::memcpy(buf.data(), data.data() + src_pos, avail); + src_pos += avail; + return avail; + }); // Collect all encrypted output std::vector encrypted; @@ -514,8 +504,8 @@ TEST_CASE("Streaming Encryptor", "[attachments][encryptor]") { // Decrypt with the streaming Decryptor and verify round-trip std::vector decrypted; attachment::Decryptor dec{key, [&](std::span d) { - decrypted.insert(decrypted.end(), d.begin(), d.end()); - }}; + decrypted.insert(decrypted.end(), d.begin(), d.end()); + }}; REQUIRE(dec.update(encrypted)); REQUIRE(dec.finalize()); REQUIRE(decrypted.size() == data.size()); From 211be88b0760237925447533c2d7d36243911aa2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 1 Apr 2026 20:30:31 -0300 Subject: [PATCH 073/193] update session-router/libquic to fix send_fin bug --- external/session-router | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/session-router b/external/session-router index 07f70d4e4..a920d2a8c 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit 07f70d4e401705b6d70c1ef5f77724d2904dd2f6 +Subproject commit a920d2a8c33de60ef7d462f7c85be08e8aed5f9b From 25f29912e2048b70544a1584888f646f8f1275dc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 00:55:54 -0300 Subject: [PATCH 074/193] Add -Werror=missing-declarations; fix issues it caught. This allows catching C++ free functions that don't have declarations earlier, such as frequently happens during a refactor that changes arguments (and can leave the old function behind in the .cpp, unnoticed until linking fails to find the one declared in the header). Fixes caught by the new warning: - Add missing `static` qualifiers to internal-only functions across multiple source files - Add missing `#include ` in session_protocol headers - Add schema_migrations.hpp.in template for generated migration headers Other changes bundled in this commit: - Refactor pro_features_for_utf8/utf16: replace raw char*/size parameters with std::string_view and std::span overloads; split the combined utf8-or-16 helper into separate validate-then-check functions - Add file_server::extend_ttl() request builder for extending file TTLs - Move compress_message declaration into base.hpp --- CMakeLists.txt | 1 + include/session/config/base.hpp | 4 ++ include/session/config/contacts.h | 2 +- .../network/backends/session_file_server.hpp | 22 +++++++ include/session/session_protocol.hpp | 19 +++---- src/CMakeLists.txt | 9 +++ src/blinding.cpp | 1 + src/config/base.cpp | 4 ++ src/config/contacts.cpp | 1 + src/config/encrypt.cpp | 1 + src/core/devices.cpp | 4 ++ src/core/schema/CMakeLists.txt | 3 +- src/core/schema/apply_schema.cpp.in | 4 +- src/core/schema/schema_migrations.hpp.in | 12 ++++ src/core/schema/schema_registry.cpp.in | 3 +- src/curve25519.cpp | 1 + src/ed25519.cpp | 1 + src/hash.cpp | 1 + src/logging.cpp | 1 + src/multi_encrypt.cpp | 12 ++-- src/network/session_network.cpp | 10 +--- src/onionreq/response_parser.cpp | 1 + src/random.cpp | 1 + src/session_protocol.cpp | 57 ++++++++++--------- src/xed25519-tweetnacl.cpp | 2 + src/xed25519.cpp | 1 + tests/test_configdata.cpp | 6 +- tests/test_logging.cpp | 2 +- tests/test_multi_encrypt.cpp | 2 +- tests/test_network_swarm.cpp | 2 +- tests/test_proto.cpp | 2 +- 31 files changed, 133 insertions(+), 59 deletions(-) create mode 100644 src/core/schema/schema_migrations.hpp.in diff --git a/CMakeLists.txt b/CMakeLists.txt index a4aef1aa6..fcc3cc57e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ else() endif() option(WARNINGS_AS_ERRORS "Treat all compiler warnings as errors" OFF) +option(FATAL_MISSING_DECLARATIONS "Developer/CI option: fatal error on non-static definitions without prior declarations (-Werror=missing-declarations)" OFF) option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd}) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index ed6d57d89..1a44c5ecb 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1562,4 +1562,8 @@ Ret wrap_exceptions(config_object* conf, Call&& f, Ret error_return) { return error_return; } +// Internal helper: attempts zstd compression of `msg` in-place (with a 'z' prefix byte); leaves +// `msg` unchanged if compression does not reduce the size. `level` of 0 disables compression. +void compress_message(std::vector& msg, int level); + } // namespace session::config diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 7fd175c48..87bd35784 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -371,7 +371,7 @@ LIBSESSION_EXPORT bool contacts_set_blinded( /// /// Outputs: /// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool contacts_erase_blinded_contact( +LIBSESSION_EXPORT bool contacts_erase_blinded( config_object* conf, const char* community_base_url, const char* blinded_id); typedef struct contacts_iterator { diff --git a/include/session/network/backends/session_file_server.hpp b/include/session/network/backends/session_file_server.hpp index 61dc0b1a4..7d9178c13 100644 --- a/include/session/network/backends/session_file_server.hpp +++ b/include/session/network/backends/session_file_server.hpp @@ -147,6 +147,28 @@ std::pair> parse_download_response( const std::vector>& headers, const std::string& body); +/// API: file_server/extend_ttl +/// +/// Constructs a request to extend the TTL of an existing file on the file server. +/// +/// Inputs: +/// - `file_id` -- [in] the file ID whose TTL should be extended. +/// - `ttl` -- [in] the new TTL duration to request. +/// - `config` -- [in] file server configuration to use for the request. +/// - `request_timeout` -- [in] timeout in milliseconds to use for the request. This won't take any +/// pre-flight operations into account so the request will never timeout if pre-flight operations +/// never complete. +/// - `overall_timeout` -- [in] timeout in milliseconds to use for the request and any pre-flight +/// operations that may need to occur (eg. path building). This value takes presedence over +/// `request_timeout` if provided, the request itself will be given a timeout of this value +/// subtracting however long the pre-flight operations took. +Request extend_ttl( + std::string_view file_id, + std::chrono::seconds ttl, + const config::FileServer& config, + std::chrono::milliseconds request_timeout, + std::optional overall_timeout = std::nullopt); + /// API: file_server/get_client_version /// /// Constructs a request to retrieve the version information for the given platform. diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 6f2a23711..c451f71b2 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -11,6 +11,7 @@ #include #include #include +#include /// A complimentary file to session encrypt (which has the low level encryption function for Session /// protocol types). This file contains high-level helper functions for decoding payloads on the @@ -317,10 +318,8 @@ struct DecodeEnvelopeKey { /// Determine the Pro features that are used in a given conversation message. /// /// Inputs: -/// - `utf` -- the UTF8 string to count the number of codepoints in to determine if it needs the -/// higher character limit available in Session Pro -/// - `utf_size` -- the size of the message in UTF8 code units to determine if the message requires -/// access to the higher character limit available in Session Pro +/// - `msg` -- the UTF-8 string view to count the number of codepoints in to determine if it needs +/// the higher character limit available in Session Pro /// /// Outputs: /// - `success` -- True if the message was evaluated successfully for PRO features false otherwise. @@ -330,17 +329,17 @@ struct DecodeEnvelopeKey { /// - `features` -- Feature flags suitable for writing directly into the protobuf /// `ProMessage.messageFeatures` /// - `codepoint_count` -- Counts the number of unicode codepoints that were in the message. -ProFeaturesForMsg pro_features_for_utf8(const char* utf, size_t utf_size); +ProFeaturesForMsg pro_features_for_utf8(std::u8string_view msg); +ProFeaturesForMsg pro_features_for_utf8(std::string_view msg); +ProFeaturesForMsg pro_features_for_utf8(std::span msg); /// API: session_protocol/pro_features_for_utf16 /// /// Determine the Pro features that are used in a given conversation message. /// /// Inputs: -/// - `utf` -- the UTF16 string to count the number of codepoints in to determine if it needs the -/// higher character limit available in Session Pro -/// - `utf_size` -- the size of the message in UTF16 code units to determine if the message requires -/// access to the higher character limit available in Session Pro +/// - `msg` -- the UTF-16 string view to count the number of codepoints in to determine if it needs +/// the higher character limit available in Session Pro /// /// Outputs: /// - `success` -- True if the message was evaluated successfully for PRO features false otherwise. @@ -350,7 +349,7 @@ ProFeaturesForMsg pro_features_for_utf8(const char* utf, size_t utf_size); /// - `bitset` -- Feature flags suitable for writing directly into the protobuf /// `ProMessage.messageFeatures` /// - `codepoint_count` -- Counts the number of unicode codepoints that were in the message. -ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size); +ProFeaturesForMsg pro_features_for_utf16(std::u16string_view msg); /// API: session_protocol/pad_message /// diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a9b12358f..0b6f3012f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,15 @@ if(WARNINGS_AS_ERRORS) endif() endif() +if(FATAL_MISSING_DECLARATIONS) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(common INTERFACE -Werror=missing-declarations) + message(STATUS "Compiling with -Werror=missing-declarations") + else() + message(WARNING "FATAL_MISSING_DECLARATIONS is not supported by this compiler (${CMAKE_CXX_COMPILER_ID}); ignoring") + endif() +endif() + set(export_targets) macro(add_libsession_util_library name) diff --git a/src/blinding.cpp b/src/blinding.cpp index d8ddae8a9..762f347a4 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -1,4 +1,5 @@ #include "session/blinding.hpp" +#include "session/blinding.h" #include #include diff --git a/src/config/base.cpp b/src/config/base.cpp index e1229ba91..84146fdbb 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -1122,12 +1122,16 @@ std::array ConfigSig::seed_hash(std::string_view key) const { return hash::blake2b_key<32>(key, std::span{_sign_sk.data(), 32}); } +namespace { + void set_error(config_object* conf, std::string e) { auto& error = unbox(conf).error; error = std::move(e); conf->last_error = error.c_str(); } +} // namespace + } // namespace session::config extern "C" { diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index b55c36aa8..9df541d2b 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,4 +1,5 @@ #include "session/config/contacts.hpp" +#include "session/config/contacts.h" #include #include diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 12dfee9a5..a0bb34508 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -1,4 +1,5 @@ #include "session/config/encrypt.hpp" +#include "session/config/encrypt.h" #include #include diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 16c78ce10..8050fc49d 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -88,6 +88,8 @@ static Keys keys_from_seed(std::span seed) { return keys; } +namespace { + // Lightweight formattable wrapper for logging a brief "aabb…xxyy" hex summary of a key. The // hex computation is deferred to when the formatter is invoked, so it is skipped entirely if // the log level is disabled. @@ -105,6 +107,8 @@ std::string format_as(const key_summary& ks) { oxenc::to_hex(ks.key.end() - 2, ks.key.end())); } +} // namespace + // format_as for XWingKeys-derived types (DeviceKeys, AccountKeys), defined in session::core so // that fmtlib's ADL-based lookup can find it when logging these types. template Keys> diff --git a/src/core/schema/CMakeLists.txt b/src/core/schema/CMakeLists.txt index 45539511a..41b3e3d7f 100644 --- a/src/core/schema/CMakeLists.txt +++ b/src/core/schema/CMakeLists.txt @@ -38,7 +38,8 @@ foreach(f IN LISTS SCHEMA_FILES) string(APPEND SCHEMA_ENTRIES " Migration{\"${basename}\", &${FUNC_NAME}},\n") endforeach() -# Write all of the migration names, declarations, and function pointers: +# Write the generated header with all migration function declarations, then the registry .cpp: +configure_file("schema_migrations.hpp.in" "${CMAKE_CURRENT_BINARY_DIR}/schema_migrations.hpp" @ONLY) configure_file("schema_registry.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/schema_registry.cpp" @ONLY) list(APPEND SCHEMA_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/schema_registry.cpp") diff --git a/src/core/schema/apply_schema.cpp.in b/src/core/schema/apply_schema.cpp.in index 048b0b913..8f1c47b50 100644 --- a/src/core/schema/apply_schema.cpp.in +++ b/src/core/schema/apply_schema.cpp.in @@ -1,7 +1,5 @@ // Auto-generated by CMake from @SCHEMA_FULL_FILENAME@. Do not edit. -#include - -namespace session::core { class Core; } +#include "schema_migrations.hpp" namespace session::core::schema { diff --git a/src/core/schema/schema_migrations.hpp.in b/src/core/schema/schema_migrations.hpp.in new file mode 100644 index 000000000..10befb2bb --- /dev/null +++ b/src/core/schema/schema_migrations.hpp.in @@ -0,0 +1,12 @@ +// Auto-generated by CMake from src/core/schema/schema_migrations.hpp.in. Do not edit. +#pragma once + +#include + +namespace session::core { class Core; } + +namespace session::core::schema { + +@DECLARATIONS@ + +} // namespace session::core::schema diff --git a/src/core/schema/schema_registry.cpp.in b/src/core/schema/schema_registry.cpp.in index d7ef7a0e6..f90c4869b 100644 --- a/src/core/schema/schema_registry.cpp.in +++ b/src/core/schema/schema_registry.cpp.in @@ -1,11 +1,10 @@ // Auto-generated by CMake from src/core/schema/schema_registry.cpp.in. Do not edit. #include +#include "schema_migrations.hpp" namespace session::core::schema { -@DECLARATIONS@ - static std::array migrations = { @SCHEMA_ENTRIES@ }; diff --git a/src/curve25519.cpp b/src/curve25519.cpp index a9daea6cf..8cea81800 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,4 +1,5 @@ #include "session/curve25519.hpp" +#include "session/curve25519.h" #include #include diff --git a/src/ed25519.cpp b/src/ed25519.cpp index b03dce825..bcef80fc2 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -1,4 +1,5 @@ #include "session/ed25519.hpp" +#include "session/ed25519.h" #include #include diff --git a/src/hash.cpp b/src/hash.cpp index 2eb885ac6..162558a0b 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -1,4 +1,5 @@ #include "session/hash.hpp" +#include "session/hash.h" #include diff --git a/src/logging.cpp b/src/logging.cpp index 779b1f669..c0fa70d03 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -1,4 +1,5 @@ #include "session/logging.hpp" +#include "session/logging.h" #include diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index ae4b69f2c..a7e7e2bfd 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -78,6 +78,10 @@ namespace detail { key); } +} // namespace detail + +namespace { + std::pair>, std::array> x_keys( std::span ed25519_secret_key) { if (ed25519_secret_key.size() != 64) @@ -93,7 +97,7 @@ namespace detail { return ret; } -} // namespace detail +} // namespace std::optional> decrypt_for_multiple( const std::vector>& ciphertexts, @@ -174,7 +178,7 @@ std::vector encrypt_for_multiple_simple( std::span nonce, int pad) { - auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); return encrypt_for_multiple_simple( messages, recipients, to_span(x_privkey), to_span(x_pubkey), domain, nonce, pad); @@ -215,7 +219,7 @@ std::optional> decrypt_for_multiple_simple( std::span sender_pubkey, std::string_view domain) { - auto [x_privkey, x_pubkey] = detail::x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); return decrypt_for_multiple_simple( encoded, to_span(x_privkey), to_span(x_pubkey), sender_pubkey, domain); @@ -300,7 +304,7 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( try { auto [priv, pub] = - session::detail::x_keys(std::span{ed25519_secret_key, 64}); + session::x_keys(std::span{ed25519_secret_key, 64}); return session_encrypt_for_multiple_simple( out_len, messages, diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 71313ab55..f0400fb20 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -136,10 +136,6 @@ namespace { main_config.onionreq_min_path_counts}; } -} // namespace - -namespace detail { - std::vector convert_service_nodes( std::vector nodes) { std::vector converted_nodes; @@ -152,7 +148,7 @@ namespace detail { return converted_nodes; } -} // namespace detail +} // namespace Network::Network(config::Config _conf) : config{std::move(_conf)}, file_server_config{std::move(build_file_server_config(config))} { @@ -1687,7 +1683,7 @@ LIBSESSION_C_API void session_network_get_swarm( x25519_pubkey::from_hex({swarm_pubkey_hex, 64}), ignore_strike_count, [cb = std::move(callback), ctx](swarm_id_t, std::vector nodes) { - auto c_nodes = network::detail::convert_service_nodes(nodes); + auto c_nodes = convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); } @@ -1700,7 +1696,7 @@ LIBSESSION_C_API void session_network_get_random_nodes( assert(callback); unbox(network)->get_random_nodes( count, [cb = std::move(callback), ctx](std::vector nodes) { - auto c_nodes = network::detail::convert_service_nodes(nodes); + auto c_nodes = convert_service_nodes(nodes); cb(c_nodes.data(), c_nodes.size(), ctx); }); } diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 89230aee4..1e2ff54cd 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -1,4 +1,5 @@ #include "session/onionreq/response_parser.hpp" +#include "session/onionreq/response_parser.h" #include #include diff --git a/src/random.cpp b/src/random.cpp index e42b815af..4ac7ed4f4 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -1,4 +1,5 @@ #include "session/random.hpp" +#include "session/random.h" #include #include diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index f1d49bd3f..be404b12e 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" @@ -223,18 +224,16 @@ bool ProMessageBitset::is_set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) co return result; } -session::ProFeaturesForMsg pro_features_for_utf8_or_16( - const void* utf, size_t utf_size, bool is_utf8) { +}; // namespace session + +namespace { + +session::ProFeaturesForMsg pro_features_check( + const simdutf::result& validation, size_t codepoints) { session::ProFeaturesForMsg result = {}; - simdutf::result validate = is_utf8 ? simdutf::validate_utf8_with_errors( - reinterpret_cast(utf), utf_size) - : simdutf::validate_utf16_with_errors( - reinterpret_cast(utf), utf_size); - if (validate.is_ok()) { + if (validation.is_ok()) { result.status = session::ProFeaturesForMsgStatus::Success; - result.codepoint_count = - is_utf8 ? simdutf::count_utf8(reinterpret_cast(utf), utf_size) - : simdutf::count_utf16(reinterpret_cast(utf), utf_size); + result.codepoint_count = codepoints; if (result.codepoint_count > SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT) { if (result.codepoint_count <= SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT) { @@ -246,22 +245,29 @@ session::ProFeaturesForMsg pro_features_for_utf8_or_16( } } else { result.status = session::ProFeaturesForMsgStatus::UTFDecodingError; - result.error = simdutf::error_to_string(validate.error); + result.error = simdutf::error_to_string(validation.error); } return result; } -}; // namespace session + +} // namespace namespace session { -ProFeaturesForMsg pro_features_for_utf8(const char* utf, size_t utf_size) { - ProFeaturesForMsg result = pro_features_for_utf8_or_16(utf, utf_size, /*is_utf8*/ true); - return result; +ProFeaturesForMsg pro_features_for_utf8(std::span msg) { + auto v = simdutf::validate_utf8_with_errors(msg); + return pro_features_check(v, v.is_ok() ? simdutf::count_utf8(msg) : 0); +} +ProFeaturesForMsg pro_features_for_utf8(std::u8string_view msg) { + return pro_features_for_utf8({reinterpret_cast(msg.data()), msg.size()}); +} +ProFeaturesForMsg pro_features_for_utf8(std::string_view msg) { + return pro_features_for_utf8({reinterpret_cast(msg.data()), msg.size()}); } -ProFeaturesForMsg pro_features_for_utf16(const char16_t* utf, size_t utf_size) { - ProFeaturesForMsg result = pro_features_for_utf8_or_16(utf, utf_size, /*is_utf8*/ false); - return result; +ProFeaturesForMsg pro_features_for_utf16(std::u16string_view msg) { + auto v = simdutf::validate_utf16_with_errors(msg); + return pro_features_check(v, v.is_ok() ? simdutf::count_utf16(msg) : 0); } constexpr char PADDING_TERMINATING_BYTE = 0x80; @@ -1037,28 +1043,27 @@ LIBSESSION_C_API SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( LIBSESSION_C_API session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf8( - const char* utf, size_t utf_size) { - ProFeaturesForMsg result_cpp = pro_features_for_utf8_or_16(utf, utf_size, /*is_utf8*/ true); - session_protocol_pro_features_for_msg result = { + const char* msg, size_t msg_size) { + auto result_cpp = pro_features_for_utf8({msg, msg_size}); + return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, .bitset = {result_cpp.bitset.data}, .codepoint_count = result_cpp.codepoint_count, }; - return result; } LIBSESSION_C_API session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( - const uint16_t* utf, size_t utf_size) { - ProFeaturesForMsg result_cpp = pro_features_for_utf8_or_16(utf, utf_size, /*is_utf8*/ false); - session_protocol_pro_features_for_msg result = { + const uint16_t* msg, size_t msg_size) { + auto result_cpp = pro_features_for_utf16( + {std::launder(reinterpret_cast(msg)), msg_size}); + return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, .bitset = {result_cpp.bitset.data}, .codepoint_count = result_cpp.codepoint_count, }; - return result; } // Shared try/catch wrapper for all C encode functions. diff --git a/src/xed25519-tweetnacl.cpp b/src/xed25519-tweetnacl.cpp index 2891dcb00..a540a0c7f 100644 --- a/src/xed25519-tweetnacl.cpp +++ b/src/xed25519-tweetnacl.cpp @@ -4,6 +4,8 @@ // this subset of the portable TweetNaCl for that single function, and libsodium for everything // else. +#include "session/xed25519.hpp" + #include #include #include diff --git a/src/xed25519.cpp b/src/xed25519.cpp index db35fce87..940dc3690 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -1,4 +1,5 @@ #include "session/xed25519.hpp" +#include "session/xed25519.h" #include #include diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 748a2458c..05ba68f6b 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -98,6 +98,8 @@ TEST_CASE("config pruning", "[config][prune]") { }); } +namespace { + // shortcut to access a nested dict auto& d(config::dict_value& v) { return std::get(v); @@ -107,6 +109,8 @@ auto& s(config::dict_value& v) { return std::get(v); } +} // namespace + TEST_CASE("config diff", "[config][diff]") { MutableConfigMessage m; m.data()["foo"] = 123; @@ -690,7 +694,7 @@ TEST_CASE("config message empty set/list deserialization", "[config][deserializa Message("Failed to parse config file: Data contains an unpruned, empty dict")); } -void updates_124(MutableConfigMessage& m) { +static void updates_124(MutableConfigMessage& m) { m.data()["dictA"] = config::dict{ {"hello", 123}, {"goodbye", config::set{{123, 456}}}, diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index 061874056..f27a59b58 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -18,7 +18,7 @@ using namespace oxen::log::literals; std::regex timestamp_re{R"(\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[\+[\d.hms]+\])"}; // Clears timestamps out of a log statement for testing reproducibility -std::string fixup_log(std::string_view log) { +static std::string fixup_log(std::string_view log) { std::string fixed; std::regex_replace( std::back_inserter(fixed), diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 9eed94ed6..8507575da 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -11,7 +11,7 @@ using x_pair = std::pair, std::array>; // Returns X25519 privkey, pubkey from an Ed25519 seed -x_pair to_x_keys(std::span ed_seed) { +static x_pair to_x_keys(std::span ed_seed) { std::array ed_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed_seed.data()); diff --git a/tests/test_network_swarm.cpp b/tests/test_network_swarm.cpp index 887ea581e..d4f41e48d 100644 --- a/tests/test_network_swarm.cpp +++ b/tests/test_network_swarm.cpp @@ -10,7 +10,7 @@ using namespace session; using namespace session::network; using namespace session::network::swarm; -swarm_id_t get_swarm_id( +static swarm_id_t get_swarm_id( std::string swarm_pubkey_hex, std::vector>> swarms) { if (swarm_pubkey_hex.size() == 66) diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp index f79706fb9..1b5701a60 100644 --- a/tests/test_proto.cpp +++ b/tests/test_proto.cpp @@ -19,7 +19,7 @@ const std::vector groups{ const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk_raw; std::array ed_sk_raw; -std::span load_seed() { +static std::span load_seed() { crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); return {ed_sk_raw.data(), ed_sk_raw.size()}; } From b46b88f7e416ee24f3d71590f97b9978701d159b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 01:03:11 -0300 Subject: [PATCH 075/193] QUIC file transfer improvements: progress, ready-gating; MTU config Upload/download improvements: - Add on_progress callback to FileTransferRequest (upload and download), fired at most once per progress_interval (default 1s) and only when progress has been made; uploads report acked bytes, downloads report received bytes - Add stall_timeout (default 25s) to FileTransferRequest: kills the upload if no ack progress is seen for that duration - Disable the idle timer during streaming uploads; restart it afterward for connection reuse - Fix streaming upload to not send the preamble twice (was causing 453 "too_much_data" from the server) - Call on_progress with 100% on successful upload completion if the final acked-bytes tick hadn't reached total yet Session-router ready-state gating: - Add _pending_operations queue to SessionRouter (parallel to _pending_requests) for non-HTTP operations - Gate _upload_internal, _download_internal, and upload_file on _ready, queuing them for replay in _finish_setup if the router is not yet ready - Split upload_file into dispatch-to-loop + _start_file_upload() to enable safe queueing MTU cap (opt::quic_max_udp_payload): - Replace opt::quic_disable_mtu_discovery with opt::quic_max_udp_payload (keeps PMTUD active but caps it); deprecate old option as a subclass - Thread max_udp_payload through NetworkConfig, C API (session_network_config), QuicFileClient, and _get_file_client - Wire tunnel_info::suggested_mtu (from session-router) into all _get_file_client calls so the per-tunnel MTU cap is applied automatically quic-files CLI: - Default to testnet; add --mainnet flag - Default server pubkey/address from QUIC_FS_ED_PUBKEY_TESTNET / QUIC_FS_SESH_ADDRESS_TESTNET constants (no longer required for --direct) - Add --max-udp-payload flag - Route all user-facing output through fmt::print (stderr for status/progress, stdout for results); remove logcat - Use separate nodedb cache dirs for mainnet vs testnet - Add upload progress callback at 250ms interval showing speed/percentage - Add app.fallthrough() so unknown args fall through to subcommands --- external/session-router | 2 +- .../network/backends/quic_file_client.hpp | 2 + .../network/backends/session_file_server.hpp | 13 ++ include/session/network/network_config.hpp | 3 +- include/session/network/network_opt.hpp | 14 +- .../network/routing/session_router_router.hpp | 11 +- include/session/network/session_network.h | 5 +- .../session/network/session_network_types.hpp | 9 +- .../network/transport/quic_transport.hpp | 2 +- src/network/backends/quic_file_client.cpp | 97 ++++++++++++- src/network/backends/session_file_server.cpp | 16 +- src/network/network_config.cpp | 11 +- src/network/routing/session_router_router.cpp | 92 ++++++++++-- src/network/session_network.cpp | 11 +- src/network/transport/quic_transport.cpp | 5 +- tests/quic-files.cpp | 137 ++++++++++-------- 16 files changed, 323 insertions(+), 107 deletions(-) diff --git a/external/session-router b/external/session-router index a920d2a8c..08550d47e 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit a920d2a8c33de60ef7d462f7c85be08e8aed5f9b +Subproject commit 08550d47e5f7ae69c37eb1b86bfb611761f944d3 diff --git a/include/session/network/backends/quic_file_client.hpp b/include/session/network/backends/quic_file_client.hpp index 93a0b5ea2..fee844698 100644 --- a/include/session/network/backends/quic_file_client.hpp +++ b/include/session/network/backends/quic_file_client.hpp @@ -63,6 +63,7 @@ class QuicFileClient { ed25519_pubkey ed_pubkey, std::string address, uint16_t port, + std::optional max_udp_payload = std::nullopt, ticket_store_cb ticket_store = nullptr, ticket_extract_cb ticket_extract = nullptr); @@ -102,6 +103,7 @@ class QuicFileClient { ed25519_pubkey _ed_pubkey; std::string _address; uint16_t _port; + std::optional _max_udp_payload; // 0RTT ticket callbacks (optional; if not provided, 0RTT is not used) ticket_store_cb _ticket_store; diff --git a/include/session/network/backends/session_file_server.hpp b/include/session/network/backends/session_file_server.hpp index 7d9178c13..d1db4a1a5 100644 --- a/include/session/network/backends/session_file_server.hpp +++ b/include/session/network/backends/session_file_server.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "session/network/key_types.hpp" #include "session/network/network_opt.hpp" #include "session/network/session_network_types.hpp" @@ -25,6 +27,17 @@ constexpr uint16_t QUIC_DEFAULT_PORT = 11235; extern const config::FileServer DEFAULT_CONFIG; extern const config::FileServer TESTNET_CONFIG; +/// Ed25519 pubkeys of the QUIC file servers. +using namespace oxenc::literals; +constexpr auto QUIC_FS_ED_PUBKEY_MAINNET = + "b8eef9821445ae16e2e97ef8aa6fe782fd11ad5253cd6723b281341dba22e371"_hex_b; +constexpr auto QUIC_FS_ED_PUBKEY_TESTNET = + "929e33ded05e653fec04b49645117f51851f102a947e04806791be416ed76602"_hex_b; + +/// Session-router .sesh addresses of the QUIC file servers (derived from Ed25519 pubkeys). +extern const std::string QUIC_FS_SESH_ADDRESS_MAINNET; +extern const std::string QUIC_FS_SESH_ADDRESS_TESTNET; + /// Parsed session-router address from an `sr=` URL fragment, e.g. `sr=abcdef.sesh:11235`. struct SRouterTarget { std::string address; // e.g. "abcdef...xyz.sesh" or "name.loki" diff --git a/include/session/network/network_config.hpp b/include/session/network/network_config.hpp index 339aa78e5..dca416a8c 100644 --- a/include/session/network/network_config.hpp +++ b/include/session/network/network_config.hpp @@ -68,7 +68,7 @@ struct Config { // Quic Transport Options std::chrono::milliseconds quic_handshake_timeout{3s}; std::chrono::seconds quic_keep_alive{10s}; - bool quic_disable_mtu_discovery = false; + std::optional quic_max_udp_payload; template requires( @@ -131,6 +131,7 @@ struct Config { // Quic transport options void handle_config_opt(opt::quic_handshake_timeout qht); void handle_config_opt(opt::quic_keep_alive qka); + void handle_config_opt(opt::quic_max_udp_payload qmup); void handle_config_opt(opt::quic_disable_mtu_discovery qdmd); // Onion request router options diff --git a/include/session/network/network_opt.hpp b/include/session/network/network_opt.hpp index af6d0b753..b751ab6dd 100644 --- a/include/session/network/network_opt.hpp +++ b/include/session/network/network_opt.hpp @@ -396,8 +396,18 @@ namespace opt { quic_keep_alive(std::chrono::seconds duration) : duration{duration} {} }; - /// Can be used to disable Quic MTU discovery. - struct quic_disable_mtu_discovery : base {}; + /// Caps the maximum QUIC UDP payload size for path MTU discovery. PMTUD will still + /// probe upward from 1200, but will not exceed this value. Must be at least 1200. + struct quic_max_udp_payload : base { + size_t size; + explicit quic_max_udp_payload(size_t s) : size{s} {} + }; + + /// Deprecated: use quic_max_udp_payload{1200} instead. + struct [[deprecated("use quic_max_udp_payload{1200} instead")]] quic_disable_mtu_discovery + : quic_max_udp_payload { + quic_disable_mtu_discovery() : quic_max_udp_payload{1200} {} + }; // MARK: Onion Request Router Options diff --git a/include/session/network/routing/session_router_router.hpp b/include/session/network/routing/session_router_router.hpp index c7fd06b60..24c90048d 100644 --- a/include/session/network/routing/session_router_router.hpp +++ b/include/session/network/routing/session_router_router.hpp @@ -48,6 +48,7 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this _active_tunnels; std::unordered_map>> _pending_requests; + std::vector> _pending_operations; std::unordered_map> _active_uploads; std::unordered_map _active_downloads; @@ -89,12 +90,20 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this enc, + FileUploadRequest request, + file_server::SRouterTarget target); void _upload_internal_legacy(UploadRequest request, std::string upload_id); void _download_internal(DownloadRequest request); void _download_internal_legacy(DownloadRequest request, std::string download_id); void _cleanup_upload(const std::string& upload_id); QuicFileClient& _get_file_client( - const ed25519_pubkey& pubkey, std::string_view address, uint16_t port); + const ed25519_pubkey& pubkey, + std::string_view address, + uint16_t port, + std::optional max_udp_payload = std::nullopt); + void _quic_upload_via_tunnel( UploadRequest upload_request, std::string upload_id, diff --git a/include/session/network/session_network.h b/include/session/network/session_network.h index 43451f18d..fdc51334f 100644 --- a/include/session/network/session_network.h +++ b/include/session/network/session_network.h @@ -94,7 +94,10 @@ typedef struct session_network_config { // Quic transport options (for transport == SESSION_NETWORK_TRANSPORT_QUIC) uint32_t quic_handshake_timeout_seconds; uint32_t quic_keep_alive_seconds; - bool quic_disable_mtu_discovery; + bool quic_disable_mtu_discovery; // deprecated: use quic_max_udp_payload instead + /// Maximum QUIC UDP payload size for PMTUD; 0 for default (no cap). + /// If quic_disable_mtu_discovery is true and this is 0, acts as if set to 1200. + size_t quic_max_udp_payload; } session_network_config; diff --git a/include/session/network/session_network_types.hpp b/include/session/network/session_network_types.hpp index f36815935..90e41e74e 100644 --- a/include/session/network/session_network_types.hpp +++ b/include/session/network/session_network_types.hpp @@ -217,9 +217,10 @@ struct file_metadata { }; struct FileTransferRequest { - std::chrono::milliseconds stall_timeout; + std::chrono::milliseconds stall_timeout = 25s; std::chrono::milliseconds request_timeout; std::optional overall_timeout; + std::chrono::milliseconds progress_interval = 1s; std::optional desired_path_index; // This shared ptr is designed to be held by the caller (without the rest of the request object) @@ -234,6 +235,12 @@ struct FileTransferRequest { // Called when transfer completes (file_metadata) or fails (int16_t error code) std::function result, bool timeout)> on_complete; + + // Called periodically during a transfer with progress information, at most once per + // progress_interval, and only when progress has been made since the last call. + // For uploads, progress_bytes is total bytes acked by the remote; for downloads, it is + // total bytes received. + std::function on_progress; }; struct UploadRequest : FileTransferRequest { diff --git a/include/session/network/transport/quic_transport.hpp b/include/session/network/transport/quic_transport.hpp index fbccb7687..fa039c3bb 100644 --- a/include/session/network/transport/quic_transport.hpp +++ b/include/session/network/transport/quic_transport.hpp @@ -24,7 +24,7 @@ namespace config { std::chrono::milliseconds handshake_timeout; std::chrono::seconds keep_alive; - bool disable_mtu_discovery; + std::optional max_udp_payload; }; } // namespace config diff --git a/src/network/backends/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp index 5f7e5a589..c5e2ed638 100644 --- a/src/network/backends/quic_file_client.cpp +++ b/src/network/backends/quic_file_client.cpp @@ -35,18 +35,25 @@ QuicFileClient::QuicFileClient( ed25519_pubkey ed_pubkey, std::string address, uint16_t port, + std::optional max_udp_payload, ticket_store_cb ticket_store, ticket_extract_cb ticket_extract) : _loop{std::move(loop)}, _ed_pubkey{std::move(ed_pubkey)}, _address{std::move(address)}, _port{port}, + _max_udp_payload{max_udp_payload}, _ticket_store{std::move(ticket_store)}, _ticket_extract{std::move(ticket_extract)}, _last_activity{std::chrono::steady_clock::now()} { // Create a dedicated endpoint for file server connections - _ep = quic::Endpoint::endpoint(*_loop, quic::Address{}); + _ep = quic::Endpoint::endpoint( + *_loop, + quic::Address{}, + (_max_udp_payload + ? std::make_optional(*_max_udp_payload) + : std::nullopt)); // Set up TLS credentials auto key_pair = ed25519::ed25519_key_pair(); @@ -424,12 +431,19 @@ void streaming_file_upload( std::shared_ptr stream; std::string response_data; std::optional> result; + // Tracked on the loop thread by the progress timer + int64_t preamble_size = 0; + int64_t last_acked = 0; // file-relative (preamble subtracted) + std::chrono::steady_clock::time_point last_ack_time; + std::chrono::steady_clock::time_point start_time; }; auto state = std::make_shared(); + state->last_ack_time = std::chrono::steady_clock::now(); + state->start_time = state->last_ack_time; - auto fail = [&](int16_t err) { + auto fail = [&](int16_t err, bool timeout = false) { if (request.on_complete) - loop->call([request, err] { request.on_complete(err, false); }); + loop->call([request, err, timeout] { request.on_complete(err, timeout); }); }; loop->call([state, get_client = std::move(get_client)] { @@ -518,9 +532,14 @@ void streaming_file_upload( if (request.ttl) cmd.append("t", static_cast(request.ttl->count())); auto cmd_view = cmd.view(); - str->send(fmt::format("{}:{}", cmd_view.size(), cmd_view)); + auto preamble = fmt::format("{}:{}", cmd_view.size(), cmd_view); + state->preamble_size = static_cast(preamble.size()); + str->send(std::move(preamble)); state->stream = std::move(str); + + // Disable the idle timer during the upload; stall detection replaces it. + state->client->_idle_timer.reset(); }); { @@ -529,6 +548,62 @@ void streaming_file_upload( return fail(ERROR_FILE_SERVER_UNAVAILABLE); } + // Periodic timer for progress reporting and stall/overall timeout detection. + // Runs on the loop thread where get_stats() is a direct member access (no queuing). + std::shared_ptr progress_timer; + if (request.progress_interval > 0ms) { + progress_timer = loop->call_every( + request.progress_interval, + [state, &request, upload_size] { + if (state->done || !state->stream) + return; + + auto now = std::chrono::steady_clock::now(); + auto [acked, unacked, unsent] = state->stream->get_stats(); + auto file_acked = + std::max(0, static_cast(acked) - state->preamble_size); + + if (file_acked > state->last_acked) { + state->last_acked = file_acked; + state->last_ack_time = now; + + if (request.on_progress) + request.on_progress(file_acked, upload_size); + } + + // Stall detection: no ack progress for stall_timeout + if (request.stall_timeout > 0ms && + now - state->last_ack_time >= request.stall_timeout) { + log::warning( + cat, + "Streaming upload stalled: no ack progress for {}", + request.stall_timeout); + state->stream->close(QUIC_FILES_CLIENT_ABORT); + { + std::lock_guard lock{state->mutex}; + state->result = static_cast(ERROR_REQUEST_TIMEOUT); + state->done = true; + } + state->cv.notify_one(); + return; + } + + // Overall timeout + if (request.overall_timeout && + now - state->start_time >= *request.overall_timeout) { + log::warning(cat, "Streaming upload exceeded overall timeout"); + state->stream->close(QUIC_FILES_CLIENT_ABORT); + { + std::lock_guard lock{state->mutex}; + state->result = static_cast(ERROR_REQUEST_TIMEOUT); + state->done = true; + } + state->cv.notify_one(); + return; + } + }); + } + auto check_cancelled = [&]() -> bool { if (!request.is_cancelled()) return false; @@ -573,12 +648,20 @@ void streaming_file_upload( state->cv.wait(lock, [&] { return state->done; }); } + // Stop the progress timer and restart the idle timer for connection reuse + progress_timer.reset(); + if (state->client) + loop->call([state] { state->client->_start_idle_timer(); }); + if (request.on_complete && state->result) { - loop->call([request, result = std::move(*state->result), key] { - if (auto* meta = std::get_if(&result)) + loop->call([state, request, result = std::move(*state->result), key, upload_size] { + if (auto* meta = std::get_if(&result)) { + if (request.on_progress && state->last_acked < upload_size) + request.on_progress(upload_size, upload_size); request.on_complete(std::make_pair(std::move(*meta), key), false); - else + } else { request.on_complete(std::get(result), false); + } }); } } diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index 7f2ae937f..caf4c32ee 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -1,6 +1,7 @@ #include "session/network/backends/session_file_server.hpp" #include +#include #include #include @@ -119,22 +120,19 @@ std::optional parse_download_url(std::string_view url) { return info; } -// Default QUIC file server session-router addresses for known networks. -// Mainnet: Ed25519 pubkey b8eef9821445ae16e2e97ef8aa6fe782fd11ad5253cd6723b281341dba22e371 -static constexpr auto MAINNET_QUIC_FS_SESH_ADDRESS = - "zdzxuyoweszbpazjx5hkw598om6tdmk1kxgsqe71or4b5qtnhpao.sesh"sv; -// Testnet: Ed25519 pubkey 929e33ded05e653fec04b49645117f51851f102a947e04806791be416ed76602 -static constexpr auto TESTNET_QUIC_FS_SESH_ADDRESS = - "1kxd8zsom31u95yrs1mrkrm9kgnt6rbk1t9yjyd81g9rn5szcaby.sesh"sv; +const std::string QUIC_FS_SESH_ADDRESS_MAINNET = + oxenc::to_base32z(QUIC_FS_ED_PUBKEY_MAINNET) + ".sesh"; +const std::string QUIC_FS_SESH_ADDRESS_TESTNET = + oxenc::to_base32z(QUIC_FS_ED_PUBKEY_TESTNET) + ".sesh"; std::optional default_quic_target( const config::FileServer& http_config, opt::netid::Target netid) { // Map known HTTP file server pubkeys to their QUIC file server .sesh addresses. if (http_config.pubkey_hex == DEFAULT_CONFIG.pubkey_hex && netid == opt::netid::Target::mainnet) - return SRouterTarget{std::string{MAINNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; + return SRouterTarget{QUIC_FS_SESH_ADDRESS_MAINNET, QUIC_DEFAULT_PORT}; if (http_config.pubkey_hex == TESTNET_CONFIG.pubkey_hex && netid == opt::netid::Target::testnet) - return SRouterTarget{std::string{TESTNET_QUIC_FS_SESH_ADDRESS}, QUIC_DEFAULT_PORT}; + return SRouterTarget{QUIC_FS_SESH_ADDRESS_TESTNET, QUIC_DEFAULT_PORT}; return std::nullopt; } diff --git a/src/network/network_config.cpp b/src/network/network_config.cpp index d557ebf96..9320418fb 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -312,10 +312,17 @@ void Config::handle_config_opt(opt::quic_keep_alive qka) { log::debug(cat, "Network config quic keep alive set to {}s", qka.duration.count()); } +void Config::handle_config_opt(opt::quic_max_udp_payload qmup) { + quic_max_udp_payload = qmup.size; + log::debug(cat, "Network config max QUIC UDP payload set to {} bytes", qmup.size); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" void Config::handle_config_opt(opt::quic_disable_mtu_discovery qdmd) { - quic_disable_mtu_discovery = true; - log::debug(cat, "Network config disabled MTU discovery for Quic"); + handle_config_opt(static_cast(qdmd)); } +#pragma GCC diagnostic pop // MARK: Onion Request Router Options diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index bcedb13c2..da0de8be6 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -233,10 +233,44 @@ void SessionRouter::upload_file(FileUploadRequest request, std::span(seed, request.domain); auto target = std::move(*quic_target); + // Dispatch to the loop thread so we wait for _ready before spawning the upload thread. + _loop->call([weak_self = weak_from_this(), + this, + enc = std::move(enc), + request = std::move(request), + target = std::move(target)]() mutable { + auto self = weak_self.lock(); + if (!self) + return; + + if (!_ready) { + log::debug(cat, "Router not ready, queueing upload_file."); + _pending_operations.emplace_back( + [weak_self, + this, + enc = std::move(enc), + request = std::move(request), + target = std::move(target)]() mutable { + auto self = weak_self.lock(); + if (!self) + return; + _start_file_upload(std::move(enc), std::move(request), std::move(target)); + }); + return; + } + + _start_file_upload(std::move(enc), std::move(request), std::move(target)); + }); +} + +void SessionRouter::_start_file_upload( + std::shared_ptr enc, + FileUploadRequest request, + file_server::SRouterTarget target) { const std::string upload_id = random::unique_id("UPL"); auto& upload_thread = _active_uploads.emplace(upload_id, std::make_pair(UploadRequest{}, std::thread{})) @@ -256,7 +290,7 @@ void SessionRouter::upload_file(FileUploadRequest request, std::span QuicFileClient* { auto self = weak_self.lock(); @@ -269,7 +303,8 @@ void SessionRouter::upload_file(FileUploadRequest request, std::spancall([weak_self = weak_from_this(), this, upload_id] { @@ -294,14 +329,16 @@ void SessionRouter::_finish_setup() { log::debug(cat, "Finishing setup, router is now ready."); auto requests_to_process = std::move(_pending_requests); - if (requests_to_process.empty()) + auto ops_to_process = std::move(_pending_operations); + + size_t pending_count = ops_to_process.size(); + for (auto& [_, reqs] : requests_to_process) + pending_count += reqs.size(); + + if (pending_count == 0) return; - // Process any requests that were queued before we were ready - log::debug( - cat, - "Processing {} requests queued during initialization.", - requests_to_process.size()); + log::debug(cat, "Processing {} operations queued during initialization.", pending_count); for (auto& [address, requests] : requests_to_process) { if (!requests.empty()) { @@ -312,6 +349,9 @@ void SessionRouter::_finish_setup() { _send_request_internal(std::move(req), std::move(cb)); } } + + for (auto& op : ops_to_process) + op(); } void SessionRouter::_close_connections() { @@ -634,10 +674,14 @@ void SessionRouter::_cleanup_upload(const std::string& upload_id) { } QuicFileClient& SessionRouter::_get_file_client( - const ed25519_pubkey& pubkey, std::string_view address, uint16_t port) { + const ed25519_pubkey& pubkey, + std::string_view address, + uint16_t port, + std::optional max_udp_payload) { auto [it, inserted] = _file_clients.try_emplace(pubkey, nullptr); if (inserted) - it->second = std::make_unique(_loop, pubkey, std::string{address}, port); + it->second = std::make_unique( + _loop, pubkey, std::string{address}, port, max_udp_payload); else it->second->set_target(pubkey, std::string{address}, port); return *it->second; @@ -660,7 +704,7 @@ void SessionRouter::_quic_upload_via_tunnel( return; } - _get_file_client(*pubkey, "::1", info.local_port) + _get_file_client(*pubkey, "::1", info.local_port, info.suggested_mtu) .upload(std::move(data), upload_request.ttl, [weak_self = weak_from_this(), this, upload_request, upload_id]( @@ -701,7 +745,7 @@ void SessionRouter::_quic_download_via_tunnel( return; } - _get_file_client(*pubkey, "::1", info.local_port) + _get_file_client(*pubkey, "::1", info.local_port, info.suggested_mtu) .download( std::move(file_id), request.on_data, @@ -732,6 +776,16 @@ void SessionRouter::_quic_download_via_tunnel( } void SessionRouter::_upload_internal(UploadRequest request) { + if (!_ready) { + log::debug(cat, "Router not ready, queueing upload."); + _pending_operations.emplace_back( + [weak_self = weak_from_this(), req = std::move(request)]() mutable { + if (auto self = weak_self.lock()) + self->_upload_internal(std::move(req)); + }); + return; + } + const std::string upload_id = random::unique_id("UP"); log::info(cat, "[Upload {}]: Starting upload.", upload_id); @@ -949,6 +1003,16 @@ void SessionRouter::_upload_internal_legacy(UploadRequest request, std::string u } void SessionRouter::_download_internal(DownloadRequest request) { + if (!_ready) { + log::debug(cat, "Router not ready, queueing download."); + _pending_operations.emplace_back( + [weak_self = weak_from_this(), req = std::move(request)]() mutable { + if (auto self = weak_self.lock()) + self->_download_internal(std::move(req)); + }); + return; + } + const std::string download_id = random::unique_id("DL"); log::info(cat, "[Download {}]: Starting download.", download_id); diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index f0400fb20..876b918dd 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -91,7 +91,7 @@ namespace { config::QuicTransport build_quic_transport_config(const config::Config& main_config) { return {main_config.quic_handshake_timeout, main_config.quic_keep_alive, - main_config.quic_disable_mtu_discovery}; + main_config.quic_max_udp_payload}; } config::DirectRouter build_direct_router_config( @@ -1253,7 +1253,8 @@ LIBSESSION_C_API session_network_config session_network_config_default() { .count(); config.quic_keep_alive_seconds = std::chrono::duration_cast(cpp_defaults.quic_keep_alive).count(); - config.quic_disable_mtu_discovery = cpp_defaults.quic_disable_mtu_discovery; + config.quic_disable_mtu_discovery = cpp_defaults.quic_max_udp_payload.has_value(); + config.quic_max_udp_payload = cpp_defaults.quic_max_udp_payload.value_or(0); return config; } @@ -1442,8 +1443,10 @@ LIBSESSION_C_API bool session_network_init( cpp_opts.emplace_back(opt::quic_keep_alive{ std::chrono::seconds{config->quic_keep_alive_seconds}}); - if (config->quic_disable_mtu_discovery) - cpp_opts.emplace_back(opt::quic_disable_mtu_discovery{}); + if (config->quic_max_udp_payload > 0) + cpp_opts.emplace_back(opt::quic_max_udp_payload{config->quic_max_udp_payload}); + else if (config->quic_disable_mtu_discovery) + cpp_opts.emplace_back(opt::quic_max_udp_payload{1200}); break; } diff --git a/src/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index 02c360302..9f22c8e78 100644 --- a/src/network/transport/quic_transport.cpp +++ b/src/network/transport/quic_transport.cpp @@ -148,8 +148,9 @@ void QuicTransport::_recreate_endpoint() { _endpoint = quic::Endpoint::endpoint( *_loop, quic::Address{}, - (_config.disable_mtu_discovery ? std::optional{} - : std::nullopt)); + (_config.max_udp_payload + ? std::make_optional(*_config.max_udp_payload) + : std::nullopt)); } void QuicTransport::_close_connections() { diff --git a/tests/quic-files.cpp b/tests/quic-files.cpp index 56452cac7..674f03a42 100644 --- a/tests/quic-files.cpp +++ b/tests/quic-files.cpp @@ -22,13 +22,8 @@ using namespace std::literals; -static constexpr auto STATUS_CAT = "file"; - namespace { -namespace log = oxen::log; -static auto logcat = log::Cat(STATUS_CAT); - using session::human_size; using clock = std::chrono::steady_clock; @@ -52,11 +47,11 @@ int do_upload( std::string download_cmd_hint) { auto path = std::filesystem::path{filename}; if (!std::filesystem::exists(path)) { - log::error(logcat, "File not found: {}", path.string()); + fmt::print(stderr, "File not found: {}\n", path.string()); return 1; } auto file_size = static_cast(std::filesystem::file_size(path)); - log::info(logcat, "Uploading {} ({})...", path.string(), human_size{file_size}); + fmt::print(stderr, "Uploading {} ({})...\n", path.string(), human_size{file_size}); std::array seed; randombytes_buf(seed.data(), seed.size()); @@ -72,8 +67,24 @@ int do_upload( req.ttl = ttl; req.request_timeout = 60s; req.overall_timeout = 300s; + req.progress_interval = 250ms; req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; + auto last_progress = start; + int64_t last_progress_bytes = 0; + req.on_progress = [&](int64_t acked, int64_t total) { + auto now = clock::now(); + auto since_last = std::chrono::duration(now - last_progress).count(); + auto recent_speed = + since_last > 0 ? human_size{static_cast((acked - last_progress_bytes) / + since_last)} + : human_size{0}; + auto pct = total > 0 ? 100.0 * acked / total : 0.0; + fmt::print(stderr, "[{}/{}] {:.1f}% {}/s\n", human_size{acked}, human_size{total}, pct, recent_speed); + last_progress = now; + last_progress_bytes = acked; + }; + network->upload_file(std::move(req), seed); auto result = future.get(); @@ -84,8 +95,7 @@ int do_upload( auto key_hex = oxenc::to_hex(key.begin(), key.end()); auto speed = human_size{static_cast(meta.size / std::max(elapsed_s, 0.001))}; - log::info( - logcat, + fmt::print( "\nUpload complete!\n" " File ID: {}\n" " Key: {}\n" @@ -94,7 +104,7 @@ int do_upload( " Speed: {}/s\n" "\n" "To download:\n" - "{} {} {}", + "{} {} {}\n", meta.id, key_hex, human_size{meta.size}, @@ -106,7 +116,7 @@ int do_upload( return 0; } - log::error(logcat, "Upload failed with error {}", std::get(result)); + fmt::print(stderr, "Upload failed with error {}\n", std::get(result)); return 1; } @@ -115,7 +125,7 @@ int do_download( const std::string& output, std::function)> initiate) { if (key_hex.size() != 64 || !oxenc::is_hex(key_hex)) { - log::error(logcat, "Invalid key: expected 64 hex characters"); + fmt::print(stderr, "Invalid key: expected 64 hex characters\n"); return 1; } @@ -154,9 +164,9 @@ int do_download( if (first_data) { first_data = false; auto latency = std::chrono::duration(now - start); - log::info( - logcat, - "Transfer started after {:.0f}ms (file size: {})", + fmt::print( + stderr, + "Transfer started after {:.0f}ms (file size: {})\n", latency.count(), human_size{info.size}); last_progress = now; @@ -173,9 +183,9 @@ int do_download( auto since_last_s = std::chrono::duration(since_last).count(); auto recent_speed = human_size{static_cast( (received_bytes - last_progress_bytes) / since_last_s)}; - log::info( - logcat, - "[{}/{}] {}/s", + fmt::print( + stderr, + "[{}/{}] {}/s\n", human_size{received_bytes}, human_size{info.size}, recent_speed); @@ -194,21 +204,20 @@ int do_download( out_file.close(); std::filesystem::remove(output); } - log::error(logcat, "Download succeeded but decryption finalization failed"); + fmt::print(stderr, "Download succeeded but decryption finalization failed\n"); return 1; } auto speed = human_size{static_cast(received_bytes / std::max(elapsed_s, 0.001))}; - log::info( - logcat, - "Download complete: {} encrypted, {} decrypted in {:.1f}s ({}/s)", + fmt::print( + "Download complete: {} encrypted, {} decrypted in {:.1f}s ({}/s)\n", human_size{received_bytes}, human_size{decrypted_bytes}, elapsed_s, speed); if (!output.empty()) - log::info(logcat, "Written to {}", output); + fmt::print("Written to {}\n", output); return 0; } @@ -216,7 +225,7 @@ int do_download( out_file.close(); std::filesystem::remove(output); } - log::error(logcat, "Download failed with error {}", std::get(result)); + fmt::print(stderr, "Download failed with error {}\n", std::get(result)); return 1; } @@ -225,12 +234,13 @@ int do_download( struct CliArgs { // Mode bool srouter = false; - bool testnet = false; + bool testnet = true; // Direct mode std::string server_pubkey_hex; std::string server_address = "::1"; uint16_t server_port = fs::QUIC_DEFAULT_PORT; + size_t max_udp_payload = 0; // Upload std::string upload_filename; @@ -248,7 +258,8 @@ struct CliArgs { int run(const CliArgs& args, bool is_upload) { auto netid = args.testnet ? net::opt::netid::testnet() : net::opt::netid::mainnet(); auto router = args.srouter ? net::opt::router::session_router() : net::opt::router::direct(); - auto cache_dir = std::filesystem::temp_directory_path() / "quic_files_cache"; + auto cache_dir = std::filesystem::temp_directory_path() / + (args.testnet ? "quic_files_cache_testnet" : "quic_files_cache"); std::filesystem::create_directories(cache_dir); std::vector net_opts; @@ -259,43 +270,31 @@ int run(const CliArgs& args, bool is_upload) { // For direct mode, pass the QUIC file server address/pubkey/port so that DirectRouter // uses the QUIC protocol instead of the legacy HTTP path. if (!args.srouter) { - if (args.server_pubkey_hex.empty() || args.server_pubkey_hex.size() != 64 || - !oxenc::is_hex(args.server_pubkey_hex)) { - fmt::print( - stderr, "Error: --server (64 hex Ed25519 pubkey) is required for --direct\n"); - return 1; - } - if (args.server_address.empty()) { - fmt::print(stderr, "Error: --address is required for --direct\n"); - return 1; - } - auto resolved = resolve_host(args.server_address); if (resolved != args.server_address) - log::info(logcat, "Resolved {} -> {}", args.server_address, resolved); + fmt::print(stderr, "Resolved {} -> {}\n", args.server_address, resolved); net_opts.push_back(net::opt::quic_file_server_ed_pubkey{args.server_pubkey_hex}); net_opts.push_back(net::opt::quic_file_server_address{resolved}); net_opts.push_back(net::opt::quic_file_server_port{args.server_port}); } - log::info( - logcat, - "Starting network ({}, {})...", + if (args.max_udp_payload > 0) + net_opts.push_back(net::opt::quic_max_udp_payload{args.max_udp_payload}); + + fmt::print( + stderr, + "Starting network ({}, {})...\n", args.testnet ? "testnet" : "mainnet", args.srouter ? "session-router" : "direct"); auto network = std::make_shared(net_opts); - std::string mode_hint = - args.srouter - ? fmt::format("{} --srouter{}", args.argv0, args.testnet ? " --testnet" : "") - : fmt::format( - "{} --direct --server {} --address {} --port {}", - args.argv0, - args.server_pubkey_hex, - args.server_address, - args.server_port); + std::string mode_hint = fmt::format( + "{}{}{}", + args.argv0, + args.srouter ? " --srouter" : "", + args.testnet ? "" : " --mainnet"); if (is_upload) { return do_upload( @@ -313,7 +312,7 @@ int run(const CliArgs& args, bool is_upload) { else download_url = fs::generate_download_url(args.dl_source, network->file_server_config); - log::info(logcat, "Downloading: {}", download_url); + fmt::print(stderr, "Downloading: {}\n", download_url); return do_download(args.dl_key_hex, args.dl_output, [&](on_data_t on_data, auto cb) { net::DownloadRequest req; req.download_url = download_url; @@ -330,25 +329,28 @@ int run(const CliArgs& args, bool is_upload) { int main(int argc, char* argv[]) { CLI::App app{"QUIC file server upload/download tool"}; app.require_subcommand(1); + app.fallthrough(); // Allow global options after subcommand CliArgs args; args.argv0 = argv[0]; - bool use_direct = false; - app.add_flag("--srouter", args.srouter, "Route via session-router (onion-routed)"); - app.add_flag("--direct", use_direct, "Connect directly to the file server (no routing)"); - app.add_flag("--testnet", args.testnet, "Use testnet (default: mainnet; only for --srouter)"); + bool use_direct = false, use_mainnet = false; + app.add_flag("--srouter", args.srouter, "Route via session-router (default: direct)"); + app.add_flag("--direct", use_direct, "Connect directly to the file server"); + app.add_flag("--mainnet", use_mainnet, "Use mainnet (default: testnet)"); app.add_option("--server", args.server_pubkey_hex, "Ed25519 pubkey of the file server (hex)"); - app.add_option( - "--address", - args.server_address, - "Server address; hostnames are resolved via DNS (default: ::1)"); + app.add_option("--address", args.server_address, "Server address (hostname or IP)"); app.add_option( "--port", args.server_port, fmt::format("Server port (default: {})", fs::QUIC_DEFAULT_PORT)); + app.add_option( + "--max-udp-payload", + args.max_udp_payload, + "Cap network-level QUIC UDP payload size (limits path MTU discovery; minimum 1200)"); + std::string log_level = "warning"; std::string log_file = "stderr"; app.add_option( @@ -384,6 +386,21 @@ int main(int argc, char* argv[]) { } if (!args.srouter && !use_direct) use_direct = true; + if (use_mainnet) + args.testnet = false; + + // For direct mode, default the server pubkey and address from the known file server configs. + if (!args.srouter) { + if (args.server_pubkey_hex.empty()) { + auto& pk = args.testnet ? fs::QUIC_FS_ED_PUBKEY_TESTNET + : fs::QUIC_FS_ED_PUBKEY_MAINNET; + args.server_pubkey_hex = oxenc::to_hex(pk.begin(), pk.end()); + } + if (args.server_address == "::1") { + args.server_address = + args.testnet ? "superduperfiles.oxen.io" : "anna.session.foundation"; + } + } if (profile_pic) args.domain = attachment::Domain::PROFILE_PIC; @@ -403,8 +420,6 @@ int main(int argc, char* argv[]) { log::add_sink(log_type, log_file); auto cats = log::extract_categories(log_level); - if (!cats.cat_levels.count(STATUS_CAT)) - cats.cat_levels[STATUS_CAT] = log::Level::info; cats.apply(); } From ba90d7ccfc694651928fde691a009c9aa038c1ef Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 15:49:03 -0300 Subject: [PATCH 076/193] unsigned char -> std::byte refactor, part 2: b32 This adds a b32 typedef for an std::array, intended to replace uc32. It also renames bytes32 to cbytes32 to avoid cognitive confusion as to what the two types are doing. --- include/session/config/convo_info_volatile.h | 4 +- include/session/config/pro.h | 2 +- include/session/pro_backend.h | 32 +++++++-------- include/session/session_protocol.h | 30 +++++++------- include/session/types.h | 12 +++--- include/session/util.hpp | 4 ++ src/session_protocol.cpp | 14 +++---- tests/test_config_pro.cpp | 2 +- tests/test_pro_backend.cpp | 16 ++++---- tests/test_session_protocol.cpp | 42 ++++++++++---------- 10 files changed, 81 insertions(+), 77 deletions(-) diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 8d8d667d1..f211ea6e1 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -15,7 +15,7 @@ typedef struct convo_info_volatile_1to1 { bool unread; // true if the conversation is explicitly marked unread bool has_pro_gen_index_hash; // Flag indicating if hash is set - bytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to // Session Pro features is valid to } convo_info_volatile_1to1; @@ -52,7 +52,7 @@ typedef struct convo_info_volatile_blinded_1to1 { bool unread; // true if the conversation is explicitly marked unread bool has_pro_gen_index_hash; // Flag indicating if hash is set - bytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to // Session Pro features is valid to } convo_info_volatile_blinded_1to1; diff --git a/include/session/config/pro.h b/include/session/config/pro.h index 05420f57e..4928f0729 100644 --- a/include/session/config/pro.h +++ b/include/session/config/pro.h @@ -13,7 +13,7 @@ extern "C" { typedef struct pro_pro_config pro_pro_config; struct pro_pro_config { - bytes64 rotating_privkey; + cbytes64 rotating_privkey; session_protocol_pro_proof proof; }; diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index ae0256141..c84d73bd1 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -132,8 +132,8 @@ struct session_pro_backend_master_rotating_signatures { bool success; char error[256]; size_t error_count; - bytes64 master_sig; - bytes64 rotating_sig; + cbytes64 master_sig; + cbytes64 rotating_sig; }; typedef struct session_pro_backend_signature session_pro_backend_signature; @@ -141,7 +141,7 @@ struct session_pro_backend_signature { bool success; char error[256]; size_t error_count; - bytes64 sig; + cbytes64 sig; }; typedef struct session_pro_backend_add_pro_payment_user_transaction @@ -158,22 +158,22 @@ typedef struct session_pro_backend_add_pro_payment_request session_pro_backend_add_pro_payment_request; struct session_pro_backend_add_pro_payment_request { uint8_t version; - bytes32 master_pkey; - bytes32 rotating_pkey; + cbytes32 master_pkey; + cbytes32 rotating_pkey; session_pro_backend_add_pro_payment_user_transaction payment_tx; - bytes64 master_sig; - bytes64 rotating_sig; + cbytes64 master_sig; + cbytes64 rotating_sig; }; typedef struct session_pro_backend_generate_pro_proof_request session_pro_backend_generate_pro_proof_request; struct session_pro_backend_generate_pro_proof_request { uint8_t version; - bytes32 master_pkey; - bytes32 rotating_pkey; + cbytes32 master_pkey; + cbytes32 rotating_pkey; uint64_t unix_ts_ms; - bytes64 master_sig; - bytes64 rotating_sig; + cbytes64 master_sig; + cbytes64 rotating_sig; }; typedef struct session_pro_backend_add_pro_payment_or_generate_pro_proof_response @@ -192,7 +192,7 @@ struct session_pro_backend_get_pro_revocations_request { typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { - bytes32 gen_index_hash; + cbytes32 gen_index_hash; uint64_t expiry_unix_ts_ms; }; @@ -210,8 +210,8 @@ typedef struct session_pro_backend_get_pro_details_request session_pro_backend_get_pro_details_request; struct session_pro_backend_get_pro_details_request { uint8_t version; - bytes32 master_pkey; - bytes64 master_sig; + cbytes32 master_pkey; + cbytes64 master_sig; uint64_t unix_ts_ms; uint32_t count; }; @@ -268,8 +268,8 @@ typedef struct session_pro_backend_set_payment_refund_requested_request session_pro_backend_set_payment_refund_requested_request; struct session_pro_backend_set_payment_refund_requested_request { uint8_t version; - bytes32 master_pkey; - bytes64 master_sig; + cbytes32 master_pkey; + cbytes64 master_sig; uint64_t unix_ts_ms; uint64_t refund_requested_unix_ts_ms; session_pro_backend_add_pro_payment_user_transaction payment_tx; diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 3ca2ede6e..d68f4f444 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -79,10 +79,10 @@ struct session_protocol_pro_signed_message { typedef struct session_protocol_pro_proof session_protocol_pro_proof; struct session_protocol_pro_proof { uint8_t version; - bytes32 gen_index_hash; - bytes32 rotating_pubkey; + cbytes32 gen_index_hash; + cbytes32 rotating_pubkey; uint64_t expiry_unix_ts_ms; - bytes64 sig; + cbytes64 sig; }; // Feature flags for profile features where each enum value indicates the bit position in the @@ -142,10 +142,10 @@ typedef struct session_protocol_envelope session_protocol_envelope; struct session_protocol_envelope { SESSION_PROTOCOL_ENVELOPE_FLAGS flags; uint64_t timestamp_ms; - bytes33 source; + cbytes33 source; uint32_t source_device; uint64_t server_timestamp; - bytes64 pro_sig; + cbytes64 pro_sig; }; typedef struct session_protocol_decode_envelope_keys session_protocol_decode_envelope_keys; @@ -170,8 +170,8 @@ struct session_protocol_decoded_envelope { bool success; session_protocol_envelope envelope; span_u8 content_plaintext; - bytes32 sender_ed25519_pubkey; - bytes32 sender_x25519_pubkey; + cbytes32 sender_ed25519_pubkey; + cbytes32 sender_x25519_pubkey; session_protocol_decoded_pro pro; size_t error_len_incl_null_terminator; }; @@ -193,7 +193,7 @@ struct session_protocol_decoded_community_message { session_protocol_envelope envelope; span_u8 content_plaintext; bool has_pro; - bytes64 pro_sig; + cbytes64 pro_sig; session_protocol_decoded_pro pro; size_t error_len_incl_null_terminator; }; @@ -244,8 +244,8 @@ LIBSESSION_EXPORT void session_protocol_pro_message_bitset_unset( /// - `proof` -- Proof to calculate the hash from /// /// Outputs: -/// - `bytes32` -- The 32 byte hash calculated from the proof -LIBSESSION_EXPORT bytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) +/// - `cbytes32` -- The 32 byte hash calculated from the proof +LIBSESSION_EXPORT cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) NON_NULL_ARG(1); /// API: session_protocol/session_protocol_pro_proof_verify_signature @@ -438,7 +438,7 @@ session_protocol_encoded_for_destination session_protocol_encode_dm_v1( const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* recipient_pubkey, + const cbytes33* recipient_pubkey, OPTIONAL const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, OPTIONAL char* error, @@ -499,8 +499,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community_i const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* recipient_pubkey, - const bytes32* community_pubkey, + const cbytes33* recipient_pubkey, + const cbytes32* community_pubkey, OPTIONAL const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, OPTIONAL char* error, @@ -613,8 +613,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* group_ed25519_pubkey, - const bytes32* group_enc_key, + const cbytes33* group_ed25519_pubkey, + const cbytes32* group_enc_key, OPTIONAL const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, OPTIONAL char* error, diff --git a/include/session/types.h b/include/session/types.h index 6c2d59c84..573470325 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -28,18 +28,18 @@ struct string8 { #define string8_literal(literal) {(char*)literal, sizeof(literal) - 1} -typedef struct bytes32 bytes32; -struct bytes32 { +typedef struct cbytes32 cbytes32; +struct cbytes32 { unsigned char data[32]; }; -typedef struct bytes33 bytes33; -struct bytes33 { +typedef struct cbytes33 cbytes33; +struct cbytes33 { unsigned char data[33]; }; -typedef struct bytes64 bytes64; -struct bytes64 { +typedef struct cbytes64 cbytes64; +struct cbytes64 { unsigned char data[64]; }; diff --git a/include/session/util.hpp b/include/session/util.hpp index b9066f963..27f681a81 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -150,6 +150,10 @@ inline bool string_iequal(std::string_view s1, std::string_view s2) { }); } +using b32 = std::array; +using b33 = std::array; +using b64 = std::array; + using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index be404b12e..8d72952e7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -969,8 +969,8 @@ LIBSESSION_C_API void session_protocol_pro_message_bitset_unset( value->data &= ~(1ULL << features); } -LIBSESSION_C_API bytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { - bytes32 result = {}; +LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { + cbytes32 result = {}; session::uc32 hash = proof_hash_internal( proof->version, proof->gen_index_hash.data, @@ -1090,7 +1090,7 @@ session_protocol_encoded_for_destination session_protocol_encode_dm_v1( const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* recipient_pubkey, + const cbytes33* recipient_pubkey, const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, char* error, @@ -1113,8 +1113,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community_i const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* recipient_pubkey, - const bytes32* community_pubkey, + const cbytes33* recipient_pubkey, + const cbytes32* community_pubkey, const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, char* error, @@ -1154,8 +1154,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( const void* ed25519_privkey, size_t ed25519_privkey_len, uint64_t sent_timestamp_ms, - const bytes33* group_ed25519_pubkey, - const bytes32* group_enc_key, + const cbytes33* group_ed25519_pubkey, + const cbytes32* group_enc_key, const void* pro_rotating_ed25519_privkey, size_t pro_rotating_ed25519_privkey_len, char* error, diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 93b8a524c..1fe7845c6 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -45,7 +45,7 @@ TEST_CASE("Pro", "[config][pro]") { // Generate the hashes static_assert(crypto_sign_ed25519_BYTES == pro_cpp.proof.sig.max_size()); std::array hash_to_sign_cpp = pro_cpp.proof.hash(); - bytes32 hash_to_sign = session_protocol_pro_proof_hash(&pro.proof); + cbytes32 hash_to_sign = session_protocol_pro_proof_hash(&pro.proof); static_assert(hash_to_sign_cpp.size() == sizeof(hash_to_sign)); CHECK(std::memcmp(hash_to_sign_cpp.data(), hash_to_sign.data, hash_to_sign_cpp.size()) == diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index a8fae2eda..7a802b2ef 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -78,12 +78,12 @@ static bool string8_equals(string8 s8, std::string_view str) { TEST_CASE("Pro Backend C API", "[pro_backend]") { // Setup: Generate keys and payment token hash - bytes32 master_pubkey = {}; - bytes64 master_privkey = {}; + cbytes32 master_pubkey = {}; + cbytes64 master_privkey = {}; crypto_sign_ed25519_keypair(master_pubkey.data, master_privkey.data); - bytes32 rotating_pubkey = {}; - bytes64 rotating_privkey = {}; + cbytes32 rotating_pubkey = {}; + cbytes64 rotating_privkey = {}; crypto_sign_ed25519_keypair(rotating_pubkey.data, rotating_privkey.data); { @@ -922,12 +922,12 @@ std::string curl_do_basic_blocking_post_request( TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Setup: Generate keys and payment token hash - bytes32 master_pubkey = {}; - bytes64 master_privkey = {}; + cbytes32 master_pubkey = {}; + cbytes64 master_privkey = {}; crypto_sign_ed25519_keypair(master_pubkey.data, master_privkey.data); - bytes32 rotating_pubkey = {}; - bytes64 rotating_privkey = {}; + cbytes32 rotating_pubkey = {}; + cbytes64 rotating_privkey = {}; crypto_sign_ed25519_keypair(rotating_pubkey.data, rotating_privkey.data); const auto DEV_BACKEND_PUBKEY = diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 8dabe228e..9bf49c39e 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -20,8 +20,8 @@ struct SerialisedProtobufContentWithProForTesting { uc64 sig_over_plaintext_with_user_pro_key; uc64 sig_over_plaintext_padded_with_user_pro_key; uc32 pro_proof_hash; - bytes64 sig_over_plaintext_with_user_pro_key_c; - bytes32 pro_proof_hash_c; + cbytes64 sig_over_plaintext_with_user_pro_key_c; + cbytes32 pro_proof_hash_c; }; static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_session_pro( @@ -186,7 +186,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encrypt with and w/o pro sig produce same payload size") { // Same payload size because the encrypt function should put in a dummy signature if one // wasn't specific to make pro and non-pro envelopes indistinguishable. - bytes33 recipient_pubkey = {}; + cbytes33 recipient_pubkey = {}; std::memcpy(recipient_pubkey.data, keys.session_pk1.data(), sizeof(recipient_pubkey.data)); // Withhold the pro signature @@ -250,7 +250,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Encrypt session_protocol_encoded_for_destination encrypt_result = {}; { - bytes33 recipient_pubkey = {}; + cbytes33 recipient_pubkey = {}; std::memcpy(recipient_pubkey.data, keys.session_pk1.data(), keys.session_pk1.size()); encrypt_result = session_protocol_encode_dm_v1( plaintext.data(), @@ -287,7 +287,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro ProProof nil_proof = {}; uc32 nil_hash = nil_proof.hash(); - bytes32 decrypt_result_pro_hash = + cbytes32 decrypt_result_pro_hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_NIL); // Pro was not attached @@ -321,14 +321,14 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*profile_bitset*/ {}); // Setup base destination object with the pro signature w/ Session pubkey 1 as the recipient - bytes64 base_pro_sig = {}; + cbytes64 base_pro_sig = {}; std::memcpy( base_pro_sig.data, protobuf_content.sig_over_plaintext_with_user_pro_key.data(), sizeof(base_pro_sig.data)); uint64_t base_sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); - bytes33 base_recipient_pubkey = {}; + cbytes33 base_recipient_pubkey = {}; REQUIRE(sizeof(base_recipient_pubkey.data) == keys.session_pk1.size()); std::memcpy(base_recipient_pubkey.data, keys.session_pk1.data(), keys.session_pk1.size()); @@ -336,10 +336,10 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Community inbox") { auto [blind15_pk, blind15_sk] = session::blind15_key_pair(keys.ed_sk1, keys.ed_pk1, /*blind factor*/ nullptr); - bytes33 blind15_recipient = {}; + cbytes33 blind15_recipient = {}; blind15_recipient.data[0] = 0x15; std::memcpy(blind15_recipient.data + 1, blind15_pk.data(), blind15_pk.size()); - bytes32 community_pubkey = {}; + cbytes32 community_pubkey = {}; session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_community_inbox( @@ -414,7 +414,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - bytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); + cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); REQUIRE(decrypt_result.pro.msg_bitset.data == 0); // No features requested @@ -490,7 +490,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - bytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); + cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); REQUIRE(session_protocol_pro_profile_bitset_is_set( @@ -512,7 +512,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encrypt/decrypt for legacy groups is rejected") { CHECK(base_recipient_pubkey.data[0] == 0x05); - bytes32 group_enc_key = {}; + cbytes32 group_enc_key = {}; session_protocol_encoded_for_destination encrypt_result = session_protocol_encode_for_group( protobuf_content.plaintext.data(), @@ -544,8 +544,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Encrypt session_protocol_encoded_for_destination encrypt_result = {}; { - bytes33 group_v2_session_pk = {}; - bytes32 group_v2_session_sk = {}; + cbytes33 group_v2_session_pk = {}; + cbytes32 group_v2_session_sk = {}; group_v2_session_pk.data[0] = 0x03; std::memcpy(group_v2_session_pk.data + 1, group_v2_pk.data(), group_v2_pk.size()); std::memcpy( @@ -634,7 +634,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - bytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); + cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(std::memcmp( hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); @@ -865,8 +865,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { crypto_sign_ed25519_seed_keypair( community_pk.data(), community_sk.data(), community_seed.data()); - bytes32 session_blind15_sk0 = {}; - bytes33 session_blind15_pk0 = {}; + cbytes32 session_blind15_sk0 = {}; + cbytes33 session_blind15_pk0 = {}; session_blind15_pk0.data[0] = 0x15; session_blind15_key_pair( keys.ed_sk0.data(), @@ -874,8 +874,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_blind15_pk0.data + 1, session_blind15_sk0.data); - bytes32 session_blind15_sk1 = {}; - bytes33 session_blind15_pk1 = {}; + cbytes32 session_blind15_sk1 = {}; + cbytes33 session_blind15_pk1 = {}; session_blind15_pk1.data[0] = 0x15; session_blind15_key_pair( keys.ed_sk1.data(), @@ -883,8 +883,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_blind15_pk1.data + 1, session_blind15_sk1.data); - bytes33 recipient_pubkey = session_blind15_pk1; - bytes32 community_pubkey = {}; + cbytes33 recipient_pubkey = session_blind15_pk1; + cbytes32 community_pubkey = {}; std::memcpy(community_pubkey.data, community_pk.data(), community_pk.size()); session_protocol_encoded_for_destination encoded = From 7662405bc8efcf7ed27bdf02b1966c51ee459ecb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 12:57:56 -0300 Subject: [PATCH 077/193] unsigned char -> std::byte refactor, part 3: crypto API reorganization Reorganize cryptographic APIs into algorithm-specific namespaces under session:: (ed25519, x25519, mlkem768) with headers grouped under include/session/crypto/ and implementations in src/crypto/. Libsodium and mlkem_native headers are now confined to .cpp wrapper files, keeping public headers dependency-free. Key changes: - New include/session/crypto/{ed25519,x25519,mlkem768}.hpp with clean std::byte APIs; implementations in src/crypto/*.cpp isolate all libsodium/mlkem includes. - ed25519::PrivKeySpan: non-copyable span-like type that accepts 32-byte seeds or 64-byte keys, with automatic seed expansion. Uses Ed25519KeySpannable concept (std::convertible_to, not constructible_from) to prevent dynamic-extent span mismatches. - Deleted include/session/crypto.hpp (consolidated into algorithm namespaces) and include/session/curve25519.hpp (replaced by x25519.hpp + ed25519.hpp conversion functions). - Split decode_envelope into decode_dm_envelope (Ed25519 DH for 1-on-1) and decode_group_envelope (symmetric key for groups), eliminating type confusion in the shared DecodeEnvelopeKey struct. - Added cleared_vector (vector with clearing_allocator that zeros on dealloc) and implicit span conversions on sodium_array. - Added named constants for xchacha20 and mlkem768 parameters. - Extended hash::blake2b HashInput concept to accept raw std::byte. --- .gitignore | 1 + CLAUDE.md | 79 ++ external/session-router | 2 +- include/session/attachments.hpp | 2 +- include/session/blinding.hpp | 100 +- include/session/config.hpp | 29 +- include/session/config/base.hpp | 106 +- include/session/config/community.hpp | 30 +- include/session/config/contacts.hpp | 14 +- .../session/config/convo_info_volatile.hpp | 18 +- include/session/config/encrypt.hpp | 31 +- include/session/config/groups/info.hpp | 10 +- include/session/config/groups/keys.hpp | 85 +- include/session/config/groups/members.hpp | 6 +- include/session/config/local.hpp | 10 +- include/session/config/pro.hpp | 2 +- include/session/config/profile_pic.hpp | 14 +- include/session/config/protos.hpp | 13 +- include/session/config/user_groups.hpp | 18 +- include/session/config/user_profile.hpp | 12 +- include/session/core.hpp | 8 +- include/session/core/callbacks.hpp | 10 +- include/session/core/devices.hpp | 14 +- include/session/core/globals.hpp | 21 +- include/session/core/pro.hpp | 2 +- include/session/core/swarm_message.hpp | 3 +- include/session/crypto.hpp | 178 ---- include/session/crypto/ed25519.hpp | 299 ++++++ include/session/crypto/mlkem768.hpp | 37 + include/session/crypto/x25519.hpp | 44 + include/session/curve25519.hpp | 37 - include/session/ed25519.h | 6 +- include/session/ed25519.hpp | 184 ---- include/session/encrypt.hpp | 36 +- include/session/fields.hpp | 6 +- include/session/hash.hpp | 263 +++-- include/session/mlkem768.hpp | 49 - include/session/multi_encrypt.hpp | 181 ++-- .../network/backends/session_file_server.hpp | 2 +- include/session/network/key_types.hpp | 11 +- include/session/network/network_opt.hpp | 4 +- .../network/routing/session_router_router.hpp | 2 +- include/session/network/service_node.hpp | 8 +- .../session/network/session_network_types.hpp | 9 +- include/session/onionreq/builder.hpp | 14 +- include/session/onionreq/hop_encryption.hpp | 24 +- include/session/onionreq/parser.hpp | 18 +- include/session/onionreq/response_parser.hpp | 2 +- include/session/pro_backend.hpp | 85 +- include/session/random.hpp | 9 +- include/session/session_encrypt.h | 2 +- include/session/session_encrypt.hpp | 180 ++-- include/session/session_protocol.hpp | 132 ++- include/session/sodium_array.hpp | 27 + include/session/util.hpp | 145 ++- include/session/xed25519.hpp | 33 +- src/CMakeLists.txt | 4 +- src/attachments.cpp | 17 +- src/blinding.cpp | 538 ++++------ src/config.cpp | 26 +- src/config/base.cpp | 174 ++-- src/config/community.cpp | 16 +- src/config/contacts.cpp | 29 +- src/config/convo_info_volatile.cpp | 31 +- src/config/encrypt.cpp | 64 +- src/config/groups/info.cpp | 16 +- src/config/groups/keys.cpp | 480 ++++----- src/config/groups/members.cpp | 10 +- src/config/internal.cpp | 16 +- src/config/internal.hpp | 26 +- src/config/local.cpp | 4 +- src/config/pro.cpp | 10 +- src/config/protos.cpp | 54 +- src/config/user_groups.cpp | 57 +- src/config/user_profile.cpp | 22 +- src/core.cpp | 107 +- src/core/devices.cpp | 197 ++-- src/core/globals.cpp | 8 +- src/core/link_sas.cpp | 2 +- src/core/pro.cpp | 8 +- src/crypto/ed25519.cpp | 296 ++++++ src/crypto/mlkem768.cpp | 47 + src/crypto/x25519.cpp | 56 + src/curve25519.cpp | 69 +- src/ed25519.cpp | 184 ---- src/hash.cpp | 37 +- src/multi_encrypt.cpp | 206 ++-- src/network/backends/session_file_server.cpp | 13 +- src/network/key_types.cpp | 14 +- src/network/routing/onion_request_router.cpp | 4 +- src/network/routing/session_router_router.cpp | 23 +- src/network/service_node.cpp | 8 +- src/network/session_network.cpp | 16 +- src/network/session_network_types.cpp | 4 +- src/network/transport/quic_transport.cpp | 9 +- src/onionreq/builder.cpp | 36 +- src/onionreq/hop_encryption.cpp | 66 +- src/onionreq/parser.cpp | 14 +- src/onionreq/response_parser.cpp | 21 +- src/pro_backend.cpp | 362 ++----- src/random.cpp | 11 +- src/session_encrypt.cpp | 962 +++++++----------- src/session_protocol.cpp | 589 +++++------ src/util.cpp | 17 +- src/xed25519-tweetnacl.cpp | 8 +- src/xed25519.cpp | 119 +-- tests/live/live_utils.hpp | 15 +- tests/live/test_pubkey_xfer.cpp | 4 +- tests/static_bundle.cpp | 2 +- tests/swarm-auth-test.cpp | 52 +- tests/test_blinding.cpp | 233 ++--- tests/test_bugs.cpp | 53 +- tests/test_compression.cpp | 22 +- tests/test_config_contacts.cpp | 124 +-- tests/test_config_convo_info_volatile.cpp | 122 +-- tests/test_config_local.cpp | 17 +- tests/test_config_pro.cpp | 45 +- tests/test_config_user_groups.cpp | 95 +- tests/test_config_userprofile.cpp | 83 +- tests/test_configdata.cpp | 57 +- tests/test_core_devices.cpp | 5 +- tests/test_curve25519.cpp | 52 +- tests/test_dm_receive.cpp | 71 +- tests/test_ed25519.cpp | 46 +- tests/test_encrypt.cpp | 14 +- tests/test_group_info.cpp | 88 +- tests/test_group_keys.cpp | 239 ++--- tests/test_group_members.cpp | 64 +- tests/test_hash.cpp | 138 ++- tests/test_helper.hpp | 4 +- tests/test_multi_encrypt.cpp | 301 +++--- tests/test_onion_request_router.cpp | 22 +- tests/test_onionreq.cpp | 30 +- tests/test_pfs_key_cache.cpp | 8 +- tests/test_poll.cpp | 16 +- tests/test_pro_backend.cpp | 14 +- tests/test_proto.cpp | 25 +- tests/test_session_encrypt.cpp | 347 +++---- tests/test_session_protocol.cpp | 149 ++- tests/test_snode_pool.cpp | 8 +- tests/test_xed25519.cpp | 263 ++--- tests/utils.hpp | 29 +- 142 files changed, 4968 insertions(+), 5663 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 include/session/crypto.hpp create mode 100644 include/session/crypto/ed25519.hpp create mode 100644 include/session/crypto/mlkem768.hpp create mode 100644 include/session/crypto/x25519.hpp delete mode 100644 include/session/curve25519.hpp delete mode 100644 include/session/ed25519.hpp delete mode 100644 include/session/mlkem768.hpp create mode 100644 src/crypto/ed25519.cpp create mode 100644 src/crypto/mlkem768.cpp create mode 100644 src/crypto/x25519.cpp delete mode 100644 src/ed25519.cpp diff --git a/.gitignore b/.gitignore index 1fe18bd5a..d413e083f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /build*/ /compile_commands.json /.cache/ +/.claude/ /.vscode/ .DS_STORE diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..82af458b5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Configure (out-of-source build required) +cmake -G Ninja -S . -B build-claude + +# Build +cmake --build build-claude --parallel --verbose + +# Run tests +./build-claude/tests/testAll [test-tag-or-name] + +# Regenerate protobuf files +cmake --build build-claude --target regen-protobuf --parallel +``` + +### Notable CMake Options + +- `-DBUILD_STATIC_DEPS=ON` — force all deps to build statically (no system libs) +- `-DENABLE_ONIONREQ=ON/OFF` — include onion request / network functionality (default ON) +- `-DWARNINGS_AS_ERRORS=ON` — treat warnings as errors +- `-DSUBMODULE_CHECK=OFF` — skip submodule freshness checks (useful during dev) + +## Architecture Overview + +This is **libsession-util**, the C++20 utility library for Session clients. It provides: + +1. **Cryptographic primitives** (`libsession::crypto`) — Ed25519/X25519 keys, blinding, hashing, encryption (session protocol, multi-encrypt, attachments), XEd25519 signatures. + +2. **Config sync system** (`libsession::config`) — CRDT-style distributed config that syncs across Session devices via swarm storage. Each config type has a namespace: + - `UserProfile`, `Contacts`, `ConvoInfoVolatile`, `UserGroups` — per-user configs + - `GroupKeys`, `GroupInfo`, `GroupMembers` — shared group configs (closed groups) + - `Local` — device-local config (never pushed to swarm) + - Config messages use bt-encoding (bencode), seqno-based CRDT merge with deterministic tie-breaking. See `docs/api/docs/config_merge_logic.md` for protocol details. + +3. **Core** (`libsession::core`) — Persistent client state backed by SQLite. The `Core` class owns `CoreComponent`-derived members (`Globals`, `Devices`, `Pro`) that share a connection pool. Migrations live in `src/core/schema/` as `NNN_name.sql` or `NNN_name.cpp` files. + +4. **Onion requests** (`libsession::onionreq`, optional) — Builder/parser for onion-routed requests to the Session network. + +### Library Targets and Dependencies + +``` +util ← file, logging, util (uses zstd, simdutf) +crypto ← util + libsodium (blinding, ed25519, session_encrypt, etc.) +config ← crypto + libsodium + protos (all config types) +core ← crypto + SQLite + mlkem768 (PQC key encapsulation) +onionreq ← crypto + quic + nettle (optional) +``` + +All targets are aliased as `libsession::util`, `libsession::crypto`, etc. + +### Header Layout + +Public headers are in `include/session/`: +- `include/session/config/` — config type headers (`.h` = C API, `.hpp` = C++ API) +- `include/session/config/groups/` — closed group configs (keys, info, members) +- `include/session/core/` — Core persistent state components +- `include/session/onionreq/` — onion request types + +### Dependency System + +Dependencies are managed via `cmake/session-deps/` which provides `session_dep()` and `session_dep_or_submodule()` macros. These first try system libraries; if not found they fall back to static builds. External submodules live in `external/` (oxen-logging, nlohmann-json, ios-cmake, protobuf, oxen-libquic). + +### Tests + +Tests use Catch2. Most tests are compiled into `testAll`; logging tests are isolated in `testLogging` because they modify global sink/level state. Filter tests with Catch2 tag syntax, e.g. `./Build/tests/testAll "[config]"`. + +### Dual C/C++ API + +Many headers come in pairs: `foo.h` (C API for FFI use) and `foo.hpp` (C++ API). The C API generally is a wrapper around the primary C++ API. When adding new public functionality, consider whether a C API is needed. + +## Code Style + +- **Prefer DRY code**: when logic is duplicated across two or more call sites, extract a shared helper. Do this proactively when writing new code, not only when asked. +- **Specify the shape upfront**: when asked to implement something that overlaps with existing code, identify and extract the shared piece before writing the new code, so duplication never appears in the first place. diff --git a/external/session-router b/external/session-router index 08550d47e..faa8c0e44 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit 08550d47e5f7ae69c37eb1b86bfb611761f944d3 +Subproject commit faa8c0e44d1e295e05935ebfc1401275a57dc982 diff --git a/include/session/attachments.hpp b/include/session/attachments.hpp index ebacecfac..e10cd361c 100644 --- a/include/session/attachments.hpp +++ b/include/session/attachments.hpp @@ -288,7 +288,7 @@ class Decryptor { bool failed = false; bool finished = false; bool hit_final = false; - cleared_uc32 key; + cleared_b32 key; unsigned char st_data[52]; // crypto_secretstream_xchacha20poly1305_state data void process_header(std::span chunk); diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index fed3d8fff..a42122e76 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -4,6 +4,7 @@ #include #include +#include "crypto/ed25519.hpp" #include "platform.hpp" #include "sodium_array.hpp" @@ -59,13 +60,13 @@ namespace session { /// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes server pk in bytes, not hex. -std::array blind15_factor(std::span server_pk); +b32 blind15_factor(std::span server_pk); /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). -std::array blind25_factor( - std::span session_id, std::span server_pk); +b32 blind25_factor( + std::span session_id, std::span server_pk); /// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted /// and returned are hex-encoded. @@ -76,8 +77,7 @@ std::array blind15_id(std::string_view session_id, std::string_v /// session_id here may be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). Only /// the *positive* possible ID is returned: the alternative can be computed by flipping the highest /// bit of byte 32, i.e.: `result[32] ^= 0x80`. -std::vector blind15_id( - std::span session_id, std::span server_pk); +b33 blind15_id(std::span session_id, std::span server_pk); /// Computes the 25-blinded id from a session id and server pubkey. Values accepted and /// returned are hex-encoded. @@ -86,23 +86,22 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk); /// Same as above, but takes the session id and pubkey as byte values instead of hex, and returns a /// 33-byte value (instead of a 66-digit hex value). Unlike the string version, session_id here may /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). -std::vector blind25_id( - std::span session_id, std::span server_pk); +b33 blind25_id(std::span session_id, std::span server_pk); /// Computes the 15-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 /// pubkey behind a (X25519) Session ID. Unlike blind15_id, knowing the true Ed25519 pubkey allows /// thie method to compute the correct sign and so using this does not require considering that the /// resulting blinded ID might need to have a sign flipped. /// -/// If the `session_id` is a non-null pointer then it must point at an empty string to be populated +/// If the `session_id` is a non-null pointer then it must point at a nullopt to be populated /// with the session_id associated with `ed_pubkey`. This is here for consistency with /// `blinded25_id_from_ed`, but unlike the 25 version, this value is not read if non-empty, and is /// not an optimization (that is: it is purely for convenience and is no more efficient to use this /// than it is to compute it yourself). -std::vector blinded15_id_from_ed( - std::span ed_pubkey, - std::span server_pk, - std::vector* session_id = nullptr); +b33 blinded15_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::optional* session_id = nullptr); /// Computes the 25-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 /// pubkey behind a (X25519) Session ID. This will be the same as blind25_id (if given the X25519 @@ -110,56 +109,55 @@ std::vector blinded15_id_from_ed( /// known. /// /// The session_id argument is provided to optimize input or output of the session ID derived from -/// the Ed25519 pubkey: if already computed, this argument can be a pointer to a 33-byte string +/// the Ed25519 pubkey: if already computed, this argument can be a pointer to an optional b33 /// containing the precomputed value (to avoid needing to compute it again). If unknown but needed -/// then a pointer to an empty string can be given to computed and stored the value here. Otherwise +/// then a pointer to a nullopt can be given to compute and store the value here. Otherwise /// (if omitted or nullptr) then the value will temporarily computed within the function. -std::vector blinded25_id_from_ed( - std::span ed_pubkey, - std::span server_pk, - std::vector* session_id = nullptr); +b33 blinded25_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::optional* session_id = nullptr); /// Computes a 15-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 /// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). /// -/// Can optionally also return the blinding factor, k, by providing a pointer to a uc32 (or -/// cleared_uc32); if non-nullptr then k will be written to it. +/// Can optionally also return the blinding factor, k, by providing a pointer to a b32; if +/// non-nullptr then k will be written to it. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair, cleared_uc32> blind15_key_pair( - std::span ed25519_sk, - std::span server_pk, - std::array* k = nullptr); +std::pair blind15_key_pair( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + b32* k = nullptr); /// Computes a 25-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 /// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). /// -/// Can optionally also return the blinding factor, k', by providing a pointer to a uc32 (or -/// cleared_uc32); if non-nullptr then k' will be written to it, where k' = ±k. Here, `k'` can be -/// negative to cancel out a negative in the true pubkey, which the remote client will always assume -/// is not present when it does a Session ID -> Ed25519 conversion for blinding purposes. +/// Can optionally also return the blinding factor, k', by providing a pointer to a b32; if +/// non-nullptr then k' will be written to it, where k' = ±k. Here, `k'` can be negative to cancel +/// out a negative in the true pubkey, which the remote client will always assume is not present when +/// it does a Session ID -> Ed25519 conversion for blinding purposes. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair, cleared_uc32> blind25_key_pair( - std::span ed25519_sk, - std::span server_pk, - std::array* k_prime = nullptr); +std::pair blind25_key_pair( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + b32* k_prime = nullptr); /// Computes a version-blinded key pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed). Returns the blinded public key and -/// blinded libsodium seed value. +/// blinded libsodium seed value (sensitive; uses cleared memory). /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::pair, cleared_uc64> blind_version_key_pair( - std::span ed25519_sk); +std::pair blind_version_key_pair(const ed25519::PrivKeySpan& ed25519_sk); /// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would /// be returned from blind15_key_pair(). @@ -169,10 +167,15 @@ std::pair, cleared_uc64> blind_version_key_pair( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::vector blind15_sign( - std::span ed25519_sk, +b64 blind15_sign( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + std::span message); +/// String_view overload: accepts hex (64 digits) or raw bytes (32 bytes) as a string_view. +b64 blind15_sign( + const ed25519::PrivKeySpan& ed25519_sk, std::string_view server_pk_in, - std::span message); + std::span message); /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). @@ -182,10 +185,15 @@ std::vector blind15_sign( /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. -std::vector blind25_sign( - std::span ed25519_sk, +b64 blind25_sign( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + std::span message); +/// String_view overload: accepts hex (64 digits) or raw bytes (32 bytes) as a string_view. +b64 blind25_sign( + const ed25519::PrivKeySpan& ed25519_sk, std::string_view server_pk, - std::span message); + std::span message); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. @@ -193,20 +201,20 @@ std::vector blind25_sign( /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), unix timestamp, method, path, and /// optional body. /// Returns the version-blinded signature. -std::vector blind_version_sign_request( - std::span ed25519_sk, +b64 blind_version_sign_request( + const ed25519::PrivKeySpan& ed25519_sk, uint64_t timestamp, std::string_view method, std::string_view path, - std::optional> body); + std::optional> body); /// Computes a verifiable version-blinded signature that validates with the version-blinded pubkey /// that would be returned from blind_version_key_pair. /// /// Takes the Ed25519 secret key (64 bytes, or 32-byte seed), current platform and unix timestamp. /// Returns the version-blinded signature. -std::vector blind_version_sign( - std::span ed25519_sk, Platform platform, uint64_t timestamp); +b64 blind_version_sign( + const ed25519::PrivKeySpan& ed25519_sk, Platform platform, uint64_t timestamp); /// Takes in a standard session_id and returns a flag indicating whether it matches the given /// blinded_id for a given server_pk. diff --git a/include/session/config.hpp b/include/session/config.hpp index 1be5f2834..29a744913 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -12,6 +12,7 @@ #include #include "types.hpp" +#include "util.hpp" namespace session::config { @@ -50,7 +51,7 @@ constexpr inline const dict_variant& unwrap(const dict_value& v) { return static_cast(v); } -using hash_t = std::array; +using hash_t = std::array; using seqno_hash_t = std::pair; class MutableConfigMessage; @@ -102,9 +103,9 @@ class ConfigMessage { /// Seqno and hash of the message; we calculate this when loading. Subclasses put the hash here /// (so that they can return a reference to it). - seqno_hash_t seqno_hash_{0, {0}}; + seqno_hash_t seqno_hash_{0, {}}; - std::optional> verified_signature_; + std::optional verified_signature_; // This will be set during construction from configs based on the merge result: // nullopt means we had to merge one or more configs together into a new merged config @@ -121,11 +122,11 @@ class ConfigMessage { /// the message when loading multiple messages, but can still continue with other messages; /// throwing aborts the entire construction). using verify_callable = std::function data, std::span signature)>; + std::span data, std::span signature)>; /// Signing function: this is passed the data to be signed and returns the 64-byte signature. using sign_callable = - std::function(std::span data)>; + std::function(std::span data)>; ConfigMessage(); ConfigMessage(const ConfigMessage&) = default; @@ -138,7 +139,7 @@ class ConfigMessage { /// Initializes a config message by parsing a serialized message. Throws on any error. See the /// vector version below for argument descriptions. explicit ConfigMessage( - std::span serialized, + std::span serialized, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -174,7 +175,7 @@ class ConfigMessage { /// `[](size_t, const auto& e) { throw e; }` can be used to make any parse error of any message /// fatal. explicit ConfigMessage( - const std::vector>& configs, + const std::vector>& configs, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -229,7 +230,7 @@ class ConfigMessage { /// verified signature when it was parsed. Returns nullopt otherwise (e.g. not loaded from /// verification at all; loaded without a verification function; or had no signature and a /// signature wasn't required). - const std::optional>& verified_signature() { + const std::optional& verified_signature() { return verified_signature_; } @@ -245,10 +246,10 @@ class ConfigMessage { /// typically for a local serialization value that isn't being pushed to the server). Note that /// signing is always disabled if there is no signing callback set, regardless of the value of /// this argument. - virtual std::vector serialize(bool enable_signing = true); + virtual std::vector serialize(bool enable_signing = true); protected: - std::vector serialize_impl( + std::vector serialize_impl( const oxenc::bt_dict& diff, bool enable_signing = true); }; @@ -297,7 +298,7 @@ class MutableConfigMessage : public ConfigMessage { /// constructor only increments seqno once while the indirect version would increment twice in /// the case of a required merge conflict resolution. explicit MutableConfigMessage( - const std::vector>& configs, + const std::vector>& configs, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS, @@ -307,7 +308,7 @@ class MutableConfigMessage : public ConfigMessage { /// take an error handler and instead always throws on parse errors (the above also throws for /// an erroneous single message, but with a less specific "no valid config messages" error). explicit MutableConfigMessage( - std::span config, + std::span config, verify_callable verifier = nullptr, sign_callable signer = nullptr, int lag = DEFAULT_DIFF_LAGS); @@ -355,7 +356,7 @@ class MutableConfigMessage : public ConfigMessage { protected: /// Internal version of hash() that takes the already-serialized value, to avoid needing a call /// to `serialize()` when such a call has already been done for other reasons. - const hash_t& hash(std::span serialized); + const hash_t& hash(std::span serialized); void increment_impl(); }; @@ -396,7 +397,7 @@ class MutableConfigMessage : public ConfigMessage { void verify_config_sig( oxenc::bt_dict_consumer dict, const ConfigMessage::verify_callable& verifier, - std::optional>* verified_signature = nullptr, + std::optional* verified_signature = nullptr, bool trust_signature = false); } // namespace session::config diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 1a44c5ecb..cb1a29335 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -16,6 +16,7 @@ #include #include +#include "../crypto/ed25519.hpp" #include "../hash.hpp" #include "../logging.hpp" #include "../sodium_array.hpp" @@ -55,8 +56,8 @@ enum class ConfigState : int { Waiting = 2, }; -using Ed25519PubKey = std::array; -using Ed25519Secret = sodium_array; +using Ed25519PubKey = b32; +using Ed25519Secret = sodium_array; // Helper base class for holding a config signing keypair class ConfigSig { @@ -74,7 +75,7 @@ class ConfigSig { // be 64 bytes or less, and should generally be unique for each key use case. // // Throws if a secret key hasn't been set via `set_sig_keys`. - std::array seed_hash(std::string_view key) const; + cleared_b32 seed_hash(std::string_view key) const; virtual void set_verifier(ConfigMessage::verify_callable v) = 0; virtual void set_signer(ConfigMessage::sign_callable v) = 0; @@ -84,8 +85,8 @@ class ConfigSig { // // Throws if given invalid data (i.e. wrong key size, or mismatched pubkey/secretkey). void init_sig_keys( - std::optional> ed25519_pubkey, - std::optional> ed25519_secretkey); + std::optional> ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey); public: virtual ~ConfigSig() = default; @@ -113,7 +114,7 @@ class ConfigSig { /// Inputs: /// - `secret` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey /// concatenated together) that sets both the secret key and public key. - void set_sig_keys(std::span secret); + void set_sig_keys(const ed25519::PrivKeySpan& secret); /// API: base/ConfigSig::set_sig_pubkey /// @@ -123,7 +124,7 @@ class ConfigSig { /// /// Inputs: /// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages - void set_sig_pubkey(std::span pubkey); + void set_sig_pubkey(std::span pubkey); /// API: base/ConfigSig::get_sig_pubkey /// @@ -133,7 +134,7 @@ class ConfigSig { /// /// Outputs: /// - reference to the 32-byte pubkey, or `std::nullopt` if not set. - const std::optional>& get_sig_pubkey() const { return _sign_pk; } + const std::optional& get_sig_pubkey() const { return _sign_pk; } /// API: base/ConfigSig::clear_sig_keys /// @@ -160,7 +161,7 @@ class ConfigBase : public ConfigSig { // Contains the base key(s) we use to encrypt/decrypt messages. If non-empty, the .front() // element will be used when encrypting a new message to push. When decrypting, we attempt each // of them, starting with .front(), until decryption succeeds. - using Key = std::array; + using Key = std::array; sodium_vector _keys; // Contains the current active message hash(es), as fed into us in `confirm_pushed()`. @@ -176,10 +177,10 @@ class ConfigBase : public ConfigSig { struct PartialMessage { int index; // 0-based index of this part std::string message_id; // storage server message hash of this part - std::vector data; // Data chunk + std::vector data; // Data chunk PartialMessage( - int index, std::string_view message_id, std::span data) : + int index, std::string_view message_id, std::span data) : index{index}, message_id{message_id}, data{data.begin(), data.end()} {} }; struct PartialMessages { @@ -231,8 +232,8 @@ class ConfigBase : public ConfigSig { // // For new parts that don't complete a set, errors, and already seen messages the optional // value will be nullopt. - std::pair, std::vector>>> - _handle_multipart(std::string_view msg_id, std::span message); + std::pair, std::vector>>> + _handle_multipart(std::string_view msg_id, std::span message); // Writes multipart data into the sub-dict of the dump data. void _dump_multiparts(oxenc::bt_dict_producer&& multi) const; @@ -253,9 +254,9 @@ class ConfigBase : public ConfigSig { // verification of incoming messages using the associated pubkey, and will be signed using the // secretkey (if a secret key is given). explicit ConfigBase( - std::optional> dump = std::nullopt, - std::optional> ed25519_pubkey = std::nullopt, - std::optional> ed25519_secretkey = std::nullopt); + std::optional> dump = std::nullopt, + std::optional> ed25519_pubkey = std::nullopt, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey = std::nullopt); // Initializes the base config object with dump data and keys; this is typically invoked by the // constructor, but is exposed to subclasses so that they can delay initial processing by @@ -266,9 +267,9 @@ class ConfigBase : public ConfigSig { // // This method must not be called outside derived class construction! void init( - std::optional> dump = std::nullopt, - std::optional> ed25519_pubkey = std::nullopt, - std::optional> ed25519_secretkey = std::nullopt); + std::optional> dump = std::nullopt, + std::optional> ed25519_pubkey = std::nullopt, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey = std::nullopt); // Tracks whether we need to dump again; most mutating methods should set this to true (unless // calling set_state, which sets to to true implicitly). @@ -552,20 +553,19 @@ class ConfigBase : public ConfigSig { /// - `std::string*` -- Returns a pointer to the string if one exists const std::string* string() const { return get_clean(); } - /// API: base/ConfigBase::DictFieldProxy::uview + /// API: base/ConfigBase::DictFieldProxy::bview /// - /// Returns the value as a std::span, if it exists and is a string; + /// Returns the value as a std::span, if it exists and is a string; /// nullopt otherwise. /// /// Inputs: None /// /// Outputs: - /// - `std::optional>` -- Returns a value as a view if it - /// exists - std::optional> uview() const { + /// - `std::optional>` -- Returns a value as a view if it exists + std::optional> bview() const { if (auto* s = get_clean()) - return std::span{ - reinterpret_cast(s->data()), s->size()}; + return std::span{ + reinterpret_cast(s->data()), s->size()}; return std::nullopt; } @@ -704,15 +704,13 @@ class ConfigBase : public ConfigSig { /// API: base/ConfigBase::DictFieldProxy::operator=(std::span) /// - /// Replaces the current value with the given std::span. This also + /// Replaces the current value with the given std::span. This also /// auto-vivifies any intermediate dicts needed to reach the given key, including replacing /// non-dict values if they currently exist along the path (this makes a copy). /// /// Inputs: - /// - `value` -- replaces current value with given std::span - /// - /// Same as above, but takes a std::span - void operator=(std::span value) { + /// - `value` -- replaces current value with given std::span + void operator=(std::span value) { *this = std::string{reinterpret_cast(value.data()), value.size()}; } @@ -933,7 +931,7 @@ class ConfigBase : public ConfigSig { /// and processed as a config message, even if it was too old to be useful (or was already /// known to be included). std::unordered_set _merge( - std::span>> configs); + std::span>> configs); /// API: base/ConfigBase::extra_data /// @@ -975,7 +973,7 @@ class ConfigBase : public ConfigSig { /// /// Inputs: /// - `ed25519_secret_key` -- key is loaded for encryption - void load_key(std::span ed25519_secretkey); + void load_key(const ed25519::PrivKeySpan& ed25519_secretkey); public: virtual ~ConfigBase() = default; @@ -1065,9 +1063,9 @@ class ConfigBase : public ConfigSig { /// Declaration: /// ```cpp /// std::unordered_set merge( - /// const std::vector>>& configs); + /// const std::vector>>& configs); /// std::unordered_set merge( - /// const std::vector>>& configs); + /// const std::vector>>& configs); /// ``` /// /// Inputs: @@ -1084,12 +1082,12 @@ class ConfigBase : public ConfigSig { /// parts that do not complete a message set, inclusion in the return value is based only on /// whether the multipart part itself looked valid. std::unordered_set merge( - const std::vector>>& configs); + const std::vector>>& configs); - // Same as above, but takes values as std::spans (because sometimes that is + // Same as above, but takes values as std::spans (because sometimes that is // more convenient). std::unordered_set merge( - const std::vector>>& configs); + const std::vector>>& configs); /// API: base/ConfigBase::is_dirty /// @@ -1249,12 +1247,12 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::tuple, std::vector>` - Returns a + /// - `std::tuple, std::vector>` - Returns a /// tuple containing /// - `seqno_t` -- sequence number - /// - `std::vector` -- data message to push to the server + /// - `std::vector` -- data message to push to the server /// - `std::vector` -- list of known message hashes - virtual std::tuple>, std::vector> + virtual std::tuple>, std::vector> push(); /// API: base/ConfigBase::confirm_pushed @@ -1291,8 +1289,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::vector` -- Returns binary data of the state dump - std::vector dump(); + /// - `std::vector` -- Returns binary data of the state dump + std::vector dump(); /// API: base/ConfigBase::make_dump /// @@ -1303,8 +1301,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::vector` -- Returns binary data of the state dump - std::vector make_dump() const; + /// - `std::vector` -- Returns binary data of the state dump + std::vector make_dump() const; /// API: base/ConfigBase::needs_dump /// @@ -1367,14 +1365,14 @@ class ConfigBase : public ConfigSig { /// Will throw a std::invalid_argument if the key is not 32 bytes. /// /// Inputs: - /// - `std::span key` -- 32 byte binary key + /// - `std::span key` -- 32 byte binary key /// - `high_priority` -- Whether to add to front or back of key list. If true then key is added /// to beginning and replace highest-priority key for encryption /// - `dirty_config` -- if true then mark the config as dirty (incrementing seqno and needing a /// push) if the first key (i.e. the key used for encryption) is changed as a result of this /// call. Ignored if the config is not modifiable. void add_key( - std::span key, + std::span key, bool high_priority = true, bool dirty_config = false); @@ -1408,7 +1406,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if found and removed - bool remove_key(std::span key, size_t from = 0, bool dirty_config = false); + bool remove_key(std::span key, size_t from = 0, bool dirty_config = false); /// API: base/ConfigBase::replace_keys /// @@ -1422,7 +1420,7 @@ class ConfigBase : public ConfigSig { /// requiring a repush) if the old and new first key are not the same. Ignored if the config /// is not modifiable. void replace_keys( - const std::vector>& new_keys, bool dirty_config = false); + const std::vector>& new_keys, bool dirty_config = false); /// API: base/ConfigBase::get_keys /// @@ -1436,8 +1434,8 @@ class ConfigBase : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::vector>` -- Returns vector of encryption keys - std::vector> get_keys() const; + /// - `std::vector>` -- Returns vector of encryption keys + std::vector> get_keys() const; /// API: base/ConfigBase::key_count /// @@ -1458,7 +1456,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if it does exist - bool has_key(std::span key) const; + bool has_key(std::span key) const; /// API: base/ConfigBase::key /// @@ -1470,8 +1468,8 @@ class ConfigBase : public ConfigSig { /// - `i` -- keys position in key list /// /// Outputs: - /// - `std::span` -- binary data of the key - std::span key(size_t i = 0) const { + /// - `std::span` -- binary data of the key + std::span key(size_t i = 0) const { assert(i < _keys.size()); return {_keys[i].data(), _keys[i].size()}; } diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index 232e53e37..6803ff670 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -30,7 +30,7 @@ struct community { community( std::string_view base_url, std::string_view room, - std::span pubkey); + std::span pubkey); // Same as above, but takes pubkey as an encoded (hex or base32z or base64) string. community(std::string_view base_url, std::string_view room, std::string_view pubkey_encoded); @@ -92,13 +92,13 @@ struct community { /// /// Declaration: /// ```cpp - /// void set_pubkey(std::span pubkey); + /// void set_pubkey(std::span pubkey); /// void set_pubkey(std::string_view pubkey); /// ``` /// /// Inputs: /// - `pubkey` -- Pubkey to be stored - void set_pubkey(std::span pubkey); + void set_pubkey(std::span pubkey); void set_pubkey(std::string_view pubkey); /// API: community/community::base_url @@ -140,8 +140,8 @@ struct community { /// Inputs: None /// /// Outputs: - /// - `const std::vector&` -- Returns the pubkey - const std::vector& pubkey() const { return pubkey_; } + /// - `const std::vector&` -- Returns the pubkey + const std::vector& pubkey() const { return pubkey_; } /// API: community/community::pubkey_hex /// @@ -201,7 +201,7 @@ struct community { static std::string full_url( std::string_view base_url, std::string_view room, - std::span pubkey); + std::span pubkey); /// API: community/community::canonical_url /// @@ -269,8 +269,8 @@ struct community { /// - `std::tuple` -- Tuple of 3 components of the url /// - `std::string` -- canonical url, normalized /// - `std::string` -- room name, *not* normalized - /// - `std::vector` -- binary of the server pubkey - static std::tuple> parse_full_url( + /// - `std::vector` -- binary of the server pubkey + static std::tuple> parse_full_url( std::string_view full_url); /// API: community/community::parse_partial_url @@ -285,9 +285,9 @@ struct community { /// - `std::tuple` -- Tuple of 3 components of the url /// - `std::string` -- canonical url, normalized /// - `std::string` -- room name, *not* normalized - /// - `std::optional>` -- optional binary of the server pubkey if + /// - `std::optional>` -- optional binary of the server pubkey if /// present - static std::tuple>> + static std::tuple>> parse_partial_url(std::string_view url); protected: @@ -297,7 +297,7 @@ struct community { // `someroom` and this could `SomeRoom`). Omitted if not available. std::optional localized_room_; // server pubkey - std::vector pubkey_; + std::vector pubkey_; // Construction without a pubkey for when pubkey isn't known yet but will be set shortly // after constructing (or when isn't needed, such as when deleting). @@ -349,8 +349,12 @@ struct comm_iterator_helper { continue; } - std::span pubkey{ - reinterpret_cast(pubkey_raw->data()), pubkey_raw->size()}; + if (pubkey_raw->size() != 32) { + next_server(); + continue; + } + auto pubkey = std::span{ + reinterpret_cast(pubkey_raw->data()), 32}; if (!it_room) { if (auto rit = server_info_dict->find("R"); diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 7490c2021..b777c3bc9 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -146,7 +146,7 @@ struct blinded_contact_info { blinded_contact_info() = default; explicit blinded_contact_info( std::string_view community_base_url, - std::span community_pubkey, + std::span community_pubkey, std::string_view blinded_id); // Internal ctor/method for C API implementations: @@ -187,8 +187,8 @@ struct blinded_contact_info { /// Inputs: None /// /// Outputs: - /// - `const std::vector&` -- Returns the pubkey - const std::vector& community_pubkey() const { return comm.pubkey(); } + /// - `const std::vector&` -- Returns the pubkey + const std::vector& community_pubkey() const { return comm.pubkey(); } /// API: contacts/blinded_contact_info::community_pubkey_hex /// @@ -212,7 +212,7 @@ struct blinded_contact_info { /// into this struct void set_base_url(std::string_view base_url); void set_room(std::string_view room); - void set_pubkey(std::span pubkey); + void set_pubkey(std::span pubkey); void set_pubkey(std::string_view pubkey); }; @@ -239,8 +239,8 @@ class Contacts : public ConfigBase { /// Outputs: /// - `Contact` - Constructor Contacts( - std::span ed25519_secretkey, - std::optional> dumped); + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: contacts/Contacts::storage_namespace /// @@ -483,7 +483,7 @@ class Contacts : public ConfigBase { // Drills into the nested dicts to access community details DictFieldProxy blinded_contact_field( const blinded_contact_info& bc, - std::span* get_pubkey = nullptr) const; + std::span* get_pubkey = nullptr) const; public: /// API: contacts/Contacts::blinded diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 8147ca829..c2e657415 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -88,7 +88,7 @@ namespace convo { struct pro_base : base { /// Hash of the generation index set by the Session Pro Backend - std::optional pro_gen_index_hash; + std::optional pro_gen_index_hash; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid /// to @@ -231,8 +231,8 @@ class ConvoInfoVolatile : public ConfigBase { /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. ConvoInfoVolatile( - std::span ed25519_secretkey, - std::optional> dumped); + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: convo_info_volatile/ConvoInfoVolatile::storage_namespace /// @@ -285,12 +285,12 @@ class ConvoInfoVolatile : public ConfigBase { /// Inputs: None /// /// Outputs: - /// - `std::tuple, std::vector>` - Returns a + /// - `std::tuple, std::vector>` - Returns a /// tuple containing /// - `seqno_t` -- sequence number - /// - `std::vector>` -- data message(s) to push to the server + /// - `std::vector>` -- data message(s) to push to the server /// - `std::vector` -- list of known message hashes - std::tuple>, std::vector> push() + std::tuple>, std::vector> push() override; /// API: convo_info_volatile/ConvoInfoVolatile::get_1to1 @@ -428,7 +428,7 @@ class ConvoInfoVolatile : public ConfigBase { /// std::string_view base_url, std::string_view room, std::string_view pubkey_hex) /// const; /// convo::community get_or_construct_community( - /// std::string_view base_url, std::string_view room, std::span + /// std::string_view base_url, std::string_view room, std::span /// pubkey) const; /// ``` /// @@ -444,7 +444,7 @@ class ConvoInfoVolatile : public ConfigBase { convo::community get_or_construct_community( std::string_view base_url, std::string_view room, - std::span pubkey) const; + std::span pubkey) const; /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_community(full_url) /// @@ -508,7 +508,7 @@ class ConvoInfoVolatile : public ConfigBase { // Drills into the nested dicts to access community details; if the second argument is // non-nullptr then it will be set to the community's pubkey, if it exists. DictFieldProxy community_field( - const convo::community& og, std::span* get_pubkey = nullptr) const; + const convo::community& og, std::span* get_pubkey = nullptr) const; public: /// API: convo_info_volatile/ConvoInfoVolatile::erase_1to1 diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index 406de6020..4f54ca0ea 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -34,10 +35,10 @@ namespace session::config { /// - `domain` -- short string for the keyed hash /// /// Outputs: -/// - `std::vector` -- Returns the encrypted message bytes -std::vector encrypt( - std::span message, - std::span key_base, +/// - `std::vector` -- Returns the encrypted message bytes +std::vector encrypt( + std::span message, + std::span key_base, std::string_view domain); /// API: encrypt/encrypt_inplace @@ -50,8 +51,8 @@ std::vector encrypt( /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash void encrypt_inplace( - std::vector& message, - std::span key_base, + std::vector& message, + std::span key_base, std::string_view domain); /// API: encrypt/encrypt_prealloced @@ -66,8 +67,8 @@ void encrypt_inplace( /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash void encrypt_prealloced( - std::span message, - std::span key_base, + std::span message, + std::span key_base, std::string_view domain); /// API: encrypt/ENCRYPT_DATA_OVERHEAD @@ -98,10 +99,10 @@ struct decrypt_error : std::runtime_error { /// - `domain` -- short string for the keyed hash /// /// Outputs: -/// - `std::vector` -- Returns the decrypt message bytes -std::vector decrypt( - std::span ciphertext, - std::span key_base, +/// - `std::vector` -- Returns the decrypted message bytes +std::vector decrypt( + std::span ciphertext, + std::span key_base, std::string_view domain); /// API: encrypt/decrypt_inplace @@ -114,8 +115,8 @@ std::vector decrypt( /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash void decrypt_inplace( - std::vector& ciphertext, - std::span key_base, + std::vector& ciphertext, + std::span key_base, std::string_view domain); /// Returns the target size of the message with padding, assuming an additional `overhead` bytes of @@ -142,6 +143,6 @@ inline constexpr size_t padded_size(size_t s, size_t overhead = ENCRYPT_DATA_OVE /// - `data` -- the data; this is modified in place /// - `overhead` -- encryption overhead to account for to reach the desired padded size. The /// default, if omitted, is the space used by the `encrypt()` function defined above. -void pad_message(std::vector& data, size_t overhead = ENCRYPT_DATA_OVERHEAD); +void pad_message(std::vector& data, size_t overhead = ENCRYPT_DATA_OVERHEAD); } // namespace session::config diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 66c0149ca..33a43c69a 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -55,9 +55,9 @@ class Info : public ConfigBase { /// push config changes. /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. - Info(std::span ed25519_pubkey, - std::optional> ed25519_secretkey, - std::optional> dumped); + Info(std::span ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: groups/Info::storage_namespace /// @@ -174,7 +174,7 @@ class Info : public ConfigBase { /// /// Declaration: /// ```cpp - /// void set_profile_pic(std::string_view url, std::span key); + /// void set_profile_pic(std::string_view url, std::span key); /// void set_profile_pic(profile_pic pic); /// ``` /// @@ -184,7 +184,7 @@ class Info : public ConfigBase { /// - `key` -- Decryption key /// - Second function: /// - `pic` -- Profile pic object - void set_profile_pic(std::string_view url, std::span key); + void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); /// API: groups/Info::set_expiry_timer diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 5eaa27bb4..e3d36707d 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -88,7 +88,7 @@ class Keys : public ConfigSig { Ed25519Secret user_ed25519_sk; struct key_info { - std::array key; + std::array key; std::chrono::system_clock::time_point timestamp; // millisecond precision int64_t generation; @@ -108,8 +108,8 @@ class Keys : public ConfigSig { /// Hashes of messages we have successfully parsed; used for deciding what needs to be renewed. std::map> active_msgs_; - sodium_cleared> pending_key_; - sodium_vector pending_key_config_; + cleared_b32 pending_key_; + sodium_vector pending_key_config_; int64_t pending_gen_ = -1; bool needs_dump_ = false; @@ -120,21 +120,20 @@ class Keys : public ConfigSig { void set_verifier(ConfigMessage::verify_callable v) override { verifier_ = std::move(v); } void set_signer(ConfigMessage::sign_callable s) override { signer_ = std::move(s); } - std::vector sign(std::span data) const; + std::vector sign(std::span data) const; // Checks for and drops expired keys. void remove_expired(); // Loads existing state from a previous dump of keys data - void load_dump(std::span dump); + void load_dump(std::span dump); // Inserts a key into the correct place in `keys_`. void insert_key(std::string_view message_hash, key_info&& key); // Returned the blinding factor for a given session X25519 pubkey. This depends on the group's // seed and thus is only obtainable by an admin account. - std::array subaccount_blind_factor( - const std::array& session_xpk) const; + b32 subaccount_blind_factor(std::span session_xpk) const; public: /// The multiple of members keys we include in the message; we add junk entries to the key list @@ -192,10 +191,10 @@ class Keys : public ConfigSig { /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. /// - `info` and `members` -- will be loaded with the group keys, if present in the dump. - Keys(std::span user_ed25519_secretkey, - std::span group_ed25519_pubkey, - std::optional> group_ed25519_secretkey, - std::optional> dumped, + Keys(const ed25519::PrivKeySpan& user_ed25519_secretkey, + std::span group_ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& group_ed25519_secretkey, + std::optional> dumped, Info& info, Members& members); @@ -232,8 +231,8 @@ class Keys : public ConfigSig { /// Inputs: none. /// /// Outputs: - /// - `std::vector>` - vector of encryption keys. - std::vector> group_keys() const; + /// - `std::vector>` - vector of encryption keys. + std::vector> group_keys() const; /// API: groups/Keys::size /// @@ -258,8 +257,8 @@ class Keys : public ConfigSig { /// Inputs: none. /// /// Outputs: - /// - `std::span` of the most current group encryption key. - std::span group_enc_key() const; + /// - `std::span` of the most current group encryption key. + std::span group_enc_key() const; /// API: groups/Keys::is_admin /// @@ -292,7 +291,7 @@ class Keys : public ConfigSig { /// /// Outputs: nothing. After a successful call, `admin()` will return true. Throws if the given /// secret key does not match the group's pubkey. - void load_admin_key(std::span secret, Info& info, Members& members); + void load_admin_key(const ed25519::PrivKeySpan& secret, Info& info, Members& members); /// API: groups/Keys::rekey /// @@ -325,12 +324,12 @@ class Keys : public ConfigSig { /// config will be dirtied after the rekey and will require a push. /// /// Outputs: - /// - `std::span` containing the data that needs to be pushed to the config + /// - `std::span` containing the data that needs to be pushed to the config /// keys namespace /// for the group. (This can be re-obtained from `pending_config()` if needed until it has /// been confirmed or superceded). This data must be consumed or copied from the returned /// string_view immediately: it will not be valid past other calls on the Keys config object. - std::span rekey(Info& info, Members& members); + std::span rekey(Info& info, Members& members); /// API: groups/Keys::key_supplement /// @@ -352,11 +351,11 @@ class Keys : public ConfigSig { /// Session IDs are specified in hex. /// /// Outputs: - /// - `std::vector` containing the message that should be pushed to the swarm + /// - `std::vector` containing the message that should be pushed to the swarm /// containing encrypted /// keys for the given user(s). - std::vector key_supplement(const std::vector& sids) const; - std::vector key_supplement(std::string sid) const { + std::vector key_supplement(const std::vector& sids) const; + std::vector key_supplement(std::string sid) const { return key_supplement(std::vector{{std::move(sid)}}); } @@ -386,7 +385,7 @@ class Keys : public ConfigSig { /// delete messages without having the full admin group keys. /// /// Outputs: - /// - `std::vector` -- contains a subaccount swarm signing value; this can be + /// - `std::vector` -- contains a subaccount swarm signing value; this can be /// passed (by the user) /// into `swarm_subaccount_sign` to sign a value suitable for swarm authentication. /// (Internally this packs the flags, blinding factor, and group admin signature together and @@ -398,7 +397,7 @@ class Keys : public ConfigSig { /// /// The signing value produced will be the same (for a given `session_id`/`write`/`del` /// values) when constructed by any admin of the group. - std::vector swarm_make_subaccount( + std::vector swarm_make_subaccount( std::string_view session_id, bool write = true, bool del = false) const; /// API: groups/Keys::swarm_verify_subaccount @@ -432,12 +431,12 @@ class Keys : public ConfigSig { /// not validate or does not meet the requirements. static bool swarm_verify_subaccount( std::string group_id, - std::span session_ed25519_secretkey, - std::span signing_value, + const ed25519::PrivKeySpan& session_ed25519_secretkey, + std::span signing_value, bool write = false, bool del = false); bool swarm_verify_subaccount( - std::span signing_value, + std::span signing_value, bool write = false, bool del = false) const; @@ -487,8 +486,8 @@ class Keys : public ConfigSig { /// - struct containing three binary values enabling swarm authentication (see description /// above). swarm_auth swarm_subaccount_sign( - std::span msg, - std::span signing_value, + std::span msg, + std::span signing_value, bool binary = false) const; /// API: groups/Keys::swarm_subaccount_token @@ -510,23 +509,23 @@ class Keys : public ConfigSig { /// /// Outputs: /// - 36 byte token that can be used for swarm token revocation. - std::vector swarm_subaccount_token( + std::vector swarm_subaccount_token( std::string_view session_id, bool write = true, bool del = false) const; /// API: groups/Keys::pending_config /// /// If a rekey has been performed but not yet confirmed then this will contain the config /// message to be pushed to the swarm. If there is no push current pending then this returns - /// nullopt. The value should be used immediately (i.e. the std::span may + /// nullopt. The value should be used immediately (i.e. the std::span may /// not remain valid if other calls to the config object are made). /// /// Inputs: None /// /// Outputs: - /// - `std::optional>` -- returns a populated config message that + /// - `std::optional>` -- returns a populated config message that /// should be pushed, /// if not yet confirmed, otherwise when no pending update is present this returns nullopt. - std::optional> pending_config() const; + std::optional> pending_config() const; /// API: groups/Keys::pending_key /// @@ -540,11 +539,11 @@ class Keys : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::optional>` the encryption key generated by the last + /// - `std::optional>` the encryption key generated by the last /// `rekey()` call. /// This is set to a new key when `rekey()` is called, and is cleared when any config message /// is successfully loaded by `load_key`. - std::optional> pending_key() const; + std::optional> pending_key() const; /// API: groups/Keys::load_key /// @@ -579,7 +578,7 @@ class Keys : public ConfigSig { /// it could mean we decrypted one for us, but already had it. bool load_key_message( std::string_view hash, - std::span data, + std::span data, int64_t timestamp_ms, Info& info, Members& members); @@ -648,7 +647,7 @@ class Keys : public ConfigSig { /// Outputs: /// - opaque binary data containing the group keys and other Keys config data that can be passed /// to the `Keys` constructor to reinitialize a Keys object with the current state. - std::vector dump(); + std::vector dump(); /// API: groups/Keys::make_dump /// @@ -659,8 +658,8 @@ class Keys : public ConfigSig { /// Inputs: None /// /// Outputs: - /// - `std::vector` -- Returns binary data of the state dump - std::vector make_dump() const; + /// - `std::vector` -- Returns binary data of the state dump + std::vector make_dump() const; /// API: groups/Keys::encrypt_message /// @@ -687,8 +686,8 @@ class Keys : public ConfigSig { /// /// Outputs: /// - `ciphertext` -- the encrypted, etc. value to send to the swarm - std::vector encrypt_message( - std::span plaintext, + std::vector encrypt_message( + std::span plaintext, bool compress = true, size_t padding = 256) const; @@ -707,7 +706,7 @@ class Keys : public ConfigSig { /// by `encrypt_message()`. /// /// Outputs: - /// - `std::pair>` -- the session ID (in hex) and the + /// - `std::pair>` -- the session ID (in hex) and the /// plaintext binary /// data that was encrypted. /// @@ -715,8 +714,8 @@ class Keys : public ConfigSig { /// some diagnostic info on what part failed. Typically a production session client would catch /// (and possibly log) but otherwise ignore such exceptions and just not process the message if /// it throws. - std::pair> decrypt_message( - std::span ciphertext) const; + std::pair> decrypt_message( + std::span ciphertext) const; }; } // namespace session::config::groups diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 0ea32b001..1d0a8c742 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -310,9 +310,9 @@ class Members : public ConfigBase { /// push config changes. /// - `dumped` -- either `std::nullopt` to construct a new, empty object; or binary state data /// that was previously dumped from an instance of this class by calling `dump()`. - Members(std::span ed25519_pubkey, - std::optional> ed25519_secretkey, - std::optional> dumped); + Members(std::span ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: groups/Members::storage_namespace /// diff --git a/include/session/config/local.hpp b/include/session/config/local.hpp index 7c8e11598..febd1d6cc 100644 --- a/include/session/config/local.hpp +++ b/include/session/config/local.hpp @@ -44,8 +44,8 @@ class Local : public ConfigBase { /// /// Outputs: /// - `Local` - Constructor - Local(std::span ed25519_secretkey, - std::optional> dumped); + Local(const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: local/Local::storage_namespace /// @@ -86,12 +86,12 @@ class Local : public ConfigBase { /// Inputs: None /// /// Outputs: - /// - `std::tuple, std::vector>` - Returns a + /// - `std::tuple, std::vector>` - Returns a /// tuple containing /// - `seqno_t` -- sequence number of 0 - /// - `std::vector` -- empty data vector + /// - `std::vector` -- empty data vector /// - `std::vector` -- empty list of message hashes - std::tuple>, std::vector> push() + std::tuple>, std::vector> push() override { return {0, {}, {}}; }; diff --git a/include/session/config/pro.hpp b/include/session/config/pro.hpp index d8523be7c..01b0cffd0 100644 --- a/include/session/config/pro.hpp +++ b/include/session/config/pro.hpp @@ -22,7 +22,7 @@ class ProConfig { public: /// Rotating private key for the public key specified in the proof. On the wire we store the /// seed. At runtime we derive the full key for convenience. - cleared_uc64 rotating_privkey; + cleared_b64 rotating_privkey; /// A cryptographic proof for entitling an Ed25519 key to Session Pro ProProof proof; diff --git a/include/session/config/profile_pic.hpp b/include/session/config/profile_pic.hpp index 7c82b4573..2f73f51b6 100644 --- a/include/session/config/profile_pic.hpp +++ b/include/session/config/profile_pic.hpp @@ -11,9 +11,9 @@ struct profile_pic { static constexpr size_t MAX_URL_LENGTH = 223; std::string url; - std::vector key; + std::vector key; - static void check_key(std::span key) { + static void check_key(std::span key) { if (!(key.empty() || key.size() == 32)) throw std::invalid_argument{"Invalid profile pic key: 32 bytes required"}; } @@ -22,13 +22,13 @@ struct profile_pic { profile_pic() = default; // Constructs from a URL and key. Key must be empty or 32 bytes. - profile_pic(std::string_view url, std::span key) : - url{url}, key{to_vector(key)} { + profile_pic(std::string_view url, std::span key) : + url{url}, key{to_vector(key)} { check_key(this->key); } - // Constructs from a string/std::vector pair moved into the constructor - profile_pic(std::string&& url, std::vector&& key) : + // Constructs from a string/std::vector pair moved into the constructor + profile_pic(std::string&& url, std::vector&& key) : url{std::move(url)}, key{std::move(key)} { check_key(this->key); } @@ -66,7 +66,7 @@ struct profile_pic { /// /// Inputs: /// - `new_key` -- binary data of a new key to be set. Must be 32 bytes - void set_key(std::vector new_key) { + void set_key(std::vector new_key) { check_key(new_key); key = std::move(new_key); } diff --git a/include/session/config/protos.hpp b/include/session/config/protos.hpp index 2880aed38..e8f59c1ae 100644 --- a/include/session/config/protos.hpp +++ b/include/session/config/protos.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "namespaces.hpp" @@ -21,9 +22,9 @@ namespace session::config::protos { /// Outputs: /// Returns the wrapped config. Will throw on serious errors (e.g. `ed25519_sk` or `ns` are /// invalid). -std::vector wrap_config( - std::span ed25519_sk, - std::span data, +std::vector wrap_config( + const ed25519::PrivKeySpan& ed25519_sk, + std::span data, int64_t seqno, config::Namespace ns); @@ -44,9 +45,9 @@ std::vector wrap_config( /// Throws a std::invalid_argument if the given ed25519_sk is invalid. (It is recommended that only /// the std::runtime_error is caught for detecting non-wrapped input as the invalid secret key is /// more serious). -std::vector unwrap_config( - std::span ed25519_sk, - std::span data, +std::vector unwrap_config( + const ed25519::PrivKeySpan& ed25519_sk, + std::span data, config::Namespace ns); } // namespace session::config::protos diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 8a60913bb..8b354cc1a 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -100,8 +100,8 @@ struct base_group_info { /// Struct containing legacy group info (aka "groups"). struct legacy_group_info : base_group_info { std::string session_id; // The legacy group "session id" (33 bytes). - std::vector enc_pubkey; // bytes (32 or empty) - std::vector enc_seckey; // bytes (32 or empty) + std::vector enc_pubkey; // bytes (32 or empty) + std::vector enc_seckey; // bytes (32 or empty) std::chrono::seconds disappearing_timer{0}; // 0 == disabled. /// Constructs a new legacy group info from an id (which must look like a session_id). Throws @@ -191,7 +191,7 @@ struct group_info : base_group_info { // (to distinguish it from a 05 x25519 pubkey session id). /// Group secret key (64 bytes); this is only possessed by admins. - std::vector secretkey; + std::vector secretkey; /// Group authentication signing value (100 bytes); this is used by non-admins to authenticate /// (using the swarm key generation functions in config::groups::Keys). This value will be @@ -199,7 +199,7 @@ struct group_info : base_group_info { /// is an admin), and so does not need to be explicitly cleared when being promoted to admin. /// /// Producing and using this value is done with the groups::Keys `swarm` methods. - std::vector auth_data; + std::vector auth_data; /// Tracks why we were removed from the group. Values are: /// - NOT_REMOVED: that we haven't been removed, @@ -282,8 +282,8 @@ class UserGroups : public ConfigBase { /// Outputs: /// - `UserGroups` - Constructor UserGroups( - std::span ed25519_secretkey, - std::optional> dumped); + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: user_groups/UserGroups::storage_namespace /// @@ -368,7 +368,7 @@ class UserGroups : public ConfigBase { /// std::string_view room, /// std::string_view pubkey_encoded) const; /// community_info get_or_construct_community( - /// std::string_view base_url, std::string_view room, std::span + /// std::string_view base_url, std::string_view room, std::span /// pubkey) const; /// ``` /// @@ -394,7 +394,7 @@ class UserGroups : public ConfigBase { community_info get_or_construct_community( std::string_view base_url, std::string_view room, - std::span pubkey) const; + std::span pubkey) const; /// API: user_groups/UserGroups::get_or_construct_community(string_view) /// @@ -470,7 +470,7 @@ class UserGroups : public ConfigBase { protected: // Drills into the nested dicts to access open group details DictFieldProxy community_field( - const community_info& og, std::span* get_pubkey = nullptr) const; + const community_info& og, std::span* get_pubkey = nullptr) const; void set_base(const base_group_info& bg, DictFieldProxy& info) const; diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 56242f985..d994da399 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -59,8 +59,8 @@ class UserProfile : public ConfigBase { /// Outputs: /// - `UserProfile` - Constructor UserProfile( - std::span ed25519_secretkey, - std::optional> dumped); + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped); /// API: user_profile/UserProfile::storage_namespace /// @@ -128,7 +128,7 @@ class UserProfile : public ConfigBase { /// /// Declaration: /// ```cpp - /// void set_profile_pic(std::string_view url, std::span key); + /// void set_profile_pic(std::string_view url, std::span key); /// void set_profile_pic(profile_pic pic); /// ``` /// @@ -138,7 +138,7 @@ class UserProfile : public ConfigBase { /// - `key` -- Decryption key /// - Second function: /// - `pic` -- Profile pic object - void set_profile_pic(std::string_view url, std::span key); + void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); /// API: user_profile/UserProfile::set_reupload_profile_pic @@ -147,7 +147,7 @@ class UserProfile : public ConfigBase { /// /// Declaration: /// ```cpp - /// void set_reupload_profile_pic(std::string_view url, std::span key); + /// void set_reupload_profile_pic(std::string_view url, std::span key); /// void set_reupload_profile_pic(profile_pic pic); /// ``` /// @@ -157,7 +157,7 @@ class UserProfile : public ConfigBase { /// - `key` -- Decryption key /// - Second function: /// - `pic` -- Profile pic object - void set_reupload_profile_pic(std::string_view url, std::span key); + void set_reupload_profile_pic(std::string_view url, std::span key); void set_reupload_profile_pic(profile_pic pic); /// API: user_profile/UserProfile::get_nts_priority diff --git a/include/session/core.hpp b/include/session/core.hpp index 0d1b9b3d3..cb5e58776 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -239,7 +239,7 @@ class Core { std::array recipient; std::vector content; sys_ms sent_timestamp; - std::optional pro_privkey; + std::optional pro_privkey; std::chrono::milliseconds ttl; bool force_v2; }; @@ -256,7 +256,7 @@ class Core { std::span recipient, std::span content, sys_ms sent_timestamp, - const OptionalEd25519PrivKeySpan& pro_privkey, + const ed25519::OptionalPrivKeySpan& pro_privkey, std::chrono::milliseconds ttl, bool force_v2); @@ -342,7 +342,7 @@ class Core { /// pfs_keys_fetched callback will fire when it completes. /// - nak -- a recent fetch returned no keys; re-fetching is suppressed for /// PFS_KEY_NAK_DURATION (1h). - PfsKeyStatus prefetch_pfs_keys(std::span session_id); + PfsKeyStatus prefetch_pfs_keys(std::span session_id); /// Sets the polling interval used when a network object is attached. The default is 20s. /// Takes effect by replacing the active ticker (if any); safe to call at any time. diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 8a60b8582..3a1dc58e3 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -47,11 +48,10 @@ struct ReceivedMessage { std::string hash; ///< Swarm-assigned message hash sys_ms timestamp; ///< Server-reported upload timestamp sys_ms expiry; ///< Server-reported expiry timestamp - std::array sender_session_id; ///< 0x05-prefixed sender session ID - int version; ///< Protocol version: 1 or 2 - std::vector content; ///< Decrypted protobuf-encoded payload - std::optional> - pro_signature; ///< Session Pro signature, if present + b33 sender_session_id; ///< 0x05-prefixed sender session ID + int version; ///< Protocol version: 1 or 2 + std::vector content; ///< Decrypted protobuf-encoded payload + std::optional pro_signature; ///< Session Pro signature, if present bool pfs_encrypted = false; ///< True iff decrypted via PFS+PQ (X-Wing) key derivation; ///< false for v1 messages and v2 non-PFS fallback messages. }; diff --git a/include/session/core/devices.hpp b/include/session/core/devices.hpp index 687613738..fca59b0b2 100644 --- a/include/session/core/devices.hpp +++ b/include/session/core/devices.hpp @@ -143,8 +143,8 @@ class Devices final : detail::CoreComponent { // Processes a single incoming device group ("D") or link request ("L") message. `data` is the // full raw message bytes including the outer bt-dict wrapper with the "" type key. - void receive_device_group_message(std::span data); - void receive_link_request(std::span data); + void receive_device_group_message(std::span data); + void receive_link_request(std::span data); // Handlers for incoming swarm messages by namespace, called from Core::receive_messages. void parse_device_messages(std::span messages, bool is_final); @@ -193,10 +193,10 @@ class Devices final : detail::CoreComponent { // Stores the X25519 + MLKEM768 keys that make up an "X-Wing" key struct XWingKeys { - cleared_uc32 x25519_sec; - std::array x25519_pub; - cleared_array mlkem768_sec; - std::array mlkem768_pub; + cleared_b32 x25519_sec; + std::array x25519_pub; + cleared_array mlkem768_sec; + std::array mlkem768_pub; }; struct DeviceKeys : XWingKeys { @@ -249,7 +249,7 @@ class Devices final : detail::CoreComponent { // returned (using the indexed key_indicator virtual column); otherwise all retained keys are // returned and a new key is auto-generated if none is currently active. std::vector active_account_keys( - std::optional> key_indicator = std::nullopt); + std::optional> key_indicator = std::nullopt); // Returns the time when this device's unique device key is due to be rotated. Returns nullopt // if this device is not currently part of the device group. diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index bd97c2548..605e9d9ed 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -42,7 +42,7 @@ class Globals final : detail::CoreComponent { session::secure_buffer _account_seed; network::ed25519_pubkey _pubkey_ed25519; network::x25519_pubkey _pubkey_x25519; - std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix + std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix void init() override; @@ -87,22 +87,19 @@ class Globals final : detail::CoreComponent { explicit AccountSeedAccess(const session::secure_buffer::r_accessor& acc) : _acc{acc} {} session::secure_buffer::r_accessor _acc; - auto ubuf() const { - return std::span{ - reinterpret_cast(_acc.buf.data()), 96}; - } + auto buf() const { return _acc.buf.first<96>(); } public: /// The raw 32-byte account seed (identical to ed25519_secret().first<32>()). - std::span seed() const& { return ubuf().first<32>(); } - std::span seed() const&& = delete; + std::span seed() const& { return buf().first<32>(); } + std::span seed() const&& = delete; /// The 64-byte Ed25519 secret key in libsodium format (seed || pubkey). - std::span ed25519_secret() const& { return ubuf().first<64>(); } - std::span ed25519_secret() const&& = delete; + std::span ed25519_secret() const& { return buf().first<64>(); } + std::span ed25519_secret() const&& = delete; /// The 32-byte X25519 secret key derived from the account seed. This is also the clamped /// private scalar of the Ed25519 key, usable for advanced scalar-multiplication operations. - std::span x25519_key() const& { return ubuf().last<32>(); } - std::span x25519_key() const&& = delete; + std::span x25519_key() const& { return buf().last<32>(); } + std::span x25519_key() const&& = delete; }; AccountSeedAccess account_seed() { @@ -110,7 +107,7 @@ class Globals final : detail::CoreComponent { return AccountSeedAccess{acc}; } // These are computed from the account_seed during construction: - std::span session_id() { return _session_id; } + std::span session_id() { return _session_id; } const network::ed25519_pubkey& pubkey_ed25519() const { return _pubkey_ed25519; } const network::x25519_pubkey& pubkey_x25519() const { return _pubkey_x25519; } diff --git a/include/session/core/pro.hpp b/include/session/core/pro.hpp index be8f862dc..bd559695f 100644 --- a/include/session/core/pro.hpp +++ b/include/session/core/pro.hpp @@ -23,7 +23,7 @@ class Pro final : detail::CoreComponent { /// Outputs: /// - `bool` -- True if the proof was revoked, false otherwise. bool proof_is_revoked( - std::span gen_index_hash, + std::span gen_index_hash, std::chrono::sys_time unix_ts); /// API: core/Pro::pro_update_revocations diff --git a/include/session/core/swarm_message.hpp b/include/session/core/swarm_message.hpp index 60e73a130..679989f55 100644 --- a/include/session/core/swarm_message.hpp +++ b/include/session/core/swarm_message.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -10,7 +11,7 @@ namespace session::core { /// hash, timestamp, and expiry fields are exactly the four values the server returns per /// message; data is owned externally and must remain valid for the lifetime of this struct. struct SwarmMessage { - std::span data; + std::span data; std::string hash; sys_ms timestamp; sys_ms expiry; diff --git a/include/session/crypto.hpp b/include/session/crypto.hpp deleted file mode 100644 index 0b3de7e4c..000000000 --- a/include/session/crypto.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "util.hpp" - -namespace session::crypto { - -// ─── Ed25519 ───────────────────────────────────────────────────────────────── - -/// Generates a random Ed25519 keypair. -inline void ed25519_keypair( - std::span pk, - std::span sk) { - crypto_sign_ed25519_keypair(ucdata(pk), ucdata(sk)); -} - -/// Generates a deterministic Ed25519 keypair from a 32-byte seed. -inline void ed25519_seed_keypair( - std::span pk, - std::span sk, - std::span seed) { - crypto_sign_ed25519_seed_keypair(ucdata(pk), ucdata(sk), ucdata(seed)); -} - -/// Signs `msg` with `sk`, writing the 64-byte detached signature into `sig`. -inline void ed25519_sign( - std::span sig, - std::span msg, - std::span sk) { - crypto_sign_ed25519_detached(ucdata(sig), nullptr, ucdata(msg), msg.size(), ucdata(sk)); -} - -/// Verifies a detached Ed25519 signature. Returns true if valid. -inline bool ed25519_verify( - std::span sig, - std::span msg, - std::span pk) { - return 0 == - crypto_sign_ed25519_verify_detached(ucdata(sig), ucdata(msg), msg.size(), ucdata(pk)); -} - -/// Extracts the Ed25519 public key from a secret key. -inline void ed25519_sk_to_pk( - std::span pk, - std::span sk) { - crypto_sign_ed25519_sk_to_pk(ucdata(pk), ucdata(sk)); -} - -/// Converts an Ed25519 public key to an X25519 public key. Returns false on failure. -inline bool ed25519_pk_to_x25519( - std::span x25519_pk, - std::span ed25519_pk) { - return 0 == crypto_sign_ed25519_pk_to_curve25519(ucdata(x25519_pk), ucdata(ed25519_pk)); -} - -/// Converts an Ed25519 secret key to an X25519 secret key. Returns false on failure. -inline bool ed25519_sk_to_x25519( - std::span x25519_sk, - std::span ed25519_sk) { - return 0 == crypto_sign_ed25519_sk_to_curve25519(ucdata(x25519_sk), ucdata(ed25519_sk)); -} - -// ─── Ed25519 group / scalar operations ─────────────────────────────────────── - -/// Reduces a 64-byte value modulo the Ed25519 group order, writing 32 bytes into `out`. -inline void ed25519_scalar_reduce(std::span out, std::span in) { - crypto_core_ed25519_scalar_reduce(ucdata(out), ucdata(in)); -} - -/// Negates a scalar modulo the Ed25519 group order. -inline void ed25519_scalar_negate(std::span out, std::span in) { - crypto_core_ed25519_scalar_negate(ucdata(out), ucdata(in)); -} - -/// Multiplies two scalars modulo the Ed25519 group order. -inline void ed25519_scalar_mul( - std::span out, - std::span x, - std::span y) { - crypto_core_ed25519_scalar_mul(ucdata(out), ucdata(x), ucdata(y)); -} - -/// Adds two scalars modulo the Ed25519 group order. -inline void ed25519_scalar_add( - std::span out, - std::span x, - std::span y) { - crypto_core_ed25519_scalar_add(ucdata(out), ucdata(x), ucdata(y)); -} - -/// Computes `scalar * B` (clamped) where B is the Ed25519 base point. -/// Returns false if the scalar is zero. -inline bool ed25519_scalarmult_base( - std::span out, std::span scalar) { - return 0 == crypto_scalarmult_ed25519_base(ucdata(out), ucdata(scalar)); -} - -/// Computes `scalar * B` (unclamped) where B is the Ed25519 base point. -/// Returns false if the scalar is zero. -inline bool ed25519_scalarmult_base_noclamp( - std::span out, std::span scalar) { - return 0 == crypto_scalarmult_ed25519_base_noclamp(ucdata(out), ucdata(scalar)); -} - -/// Computes `scalar * point` (unclamped) on the Ed25519 curve. -/// Returns false if the result is the identity element. -inline bool ed25519_scalarmult_noclamp( - std::span out, - std::span scalar, - std::span point) { - return 0 == crypto_scalarmult_ed25519_noclamp(ucdata(out), ucdata(scalar), ucdata(point)); -} - -// ─── X25519 ────────────────────────────────────────────────────────────────── - -/// Generates a random X25519 keypair. -inline void x25519_keypair( - std::span pk, - std::span sk) { - crypto_box_keypair(ucdata(pk), ucdata(sk)); -} - -/// Generates a deterministic X25519 keypair from a 32-byte seed. -inline void x25519_seed_keypair( - std::span pk, - std::span sk, - std::span seed) { - crypto_box_seed_keypair(ucdata(pk), ucdata(sk), ucdata(seed)); -} - -/// Computes the X25519 public key corresponding to `sk`. -inline void x25519_scalarmult_base( - std::span pk, - std::span sk) { - crypto_scalarmult_curve25519_base(ucdata(pk), ucdata(sk)); -} - -/// Computes the X25519 shared secret from a secret key and a remote public key. -/// Returns false if the result is the all-zeros point (degenerate case). -inline bool x25519_scalarmult( - std::span shared, - std::span sk, - std::span pk) { - return 0 == crypto_scalarmult_curve25519(ucdata(shared), ucdata(sk), ucdata(pk)); -} - -// ─── Password hashing ──────────────────────────────────────────────────────── - -/// Derives a key from a password using Argon2. Returns false on failure (e.g. out of memory). -inline bool pwhash( - std::span out, - std::string_view passwd, - std::span salt, - unsigned long long opslimit, - size_t memlimit, - int alg) { - return 0 == crypto_pwhash( - ucdata(out), - out.size(), - passwd.data(), - passwd.size(), - ucdata(salt), - opslimit, - memlimit, - alg); -} - -} // namespace session::crypto diff --git a/include/session/crypto/ed25519.hpp b/include/session/crypto/ed25519.hpp new file mode 100644 index 000000000..03fb00c7f --- /dev/null +++ b/include/session/crypto/ed25519.hpp @@ -0,0 +1,299 @@ +#pragma once + +#include +#include +#include +#include + +#include "session/sodium_array.hpp" +#include "session/util.hpp" + +namespace session::ed25519 { + +/// A span-like type representing a fully-expanded Ed25519 private key (always 64 bytes). +/// Implicitly constructible from any fixed-extent 32- or 64-byte byte/unsigned-char spannable: +/// - 32-byte input (seed): the 64-byte key is computed via libsodium and stored internally. +/// - 64-byte input: holds a non-owning span into the caller's data — no copy or allocation. +/// +/// Non-copyable and non-moveable to avoid dangling references to internal storage. + +/// Concept for types that implicitly convert to a 32- or 64-byte byte/unsigned-char span. +/// Used by PrivKeySpan and OptionalPrivKeySpan to accept Ed25519 seeds and full keys. +template +concept Ed25519KeySpannable = + std::convertible_to> || + std::convertible_to> || + std::convertible_to> || + std::convertible_to>; + +struct PrivKeySpan { + template + PrivKeySpan(const T& src) { + if constexpr (std::convertible_to>) + data_ = std::span{src}.data(); + else if constexpr (std::convertible_to>) + data_ = reinterpret_cast( + std::span{src}.data()); + else if constexpr (std::convertible_to>) { + expand_seed(std::span{src}); + data_ = storage_->data(); + } else { + expand_seed(std::span{src}); + data_ = storage_->data(); + } + } + + // Constructor for runtime-known sizes (e.g. at C API boundaries). + // Throws std::invalid_argument if size is not 32 or 64. + PrivKeySpan(const std::byte* data, size_t size) { + if (size == 64) + data_ = data; + else if (size == 32) { + expand_seed(std::span{data, 32}); + data_ = storage_->data(); + } else + throw std::invalid_argument{ + "Ed25519 private key must be 32 or 64 bytes (got " + + std::to_string(size) + ")"}; + } + PrivKeySpan(const unsigned char* data, size_t size) : + PrivKeySpan{reinterpret_cast(data), size} {} + + // Named factory for dynamic-span input (runtime size check, throws if not 32 or 64). + static PrivKeySpan from(std::span key) { + return {key.data(), key.size()}; + } + static PrivKeySpan from(std::span key) { + return {key.data(), key.size()}; + } + + PrivKeySpan(const PrivKeySpan&) = delete; + PrivKeySpan& operator=(const PrivKeySpan&) = delete; + PrivKeySpan(PrivKeySpan&&) = delete; + PrivKeySpan& operator=(PrivKeySpan&&) = delete; + + std::span span() const { + return std::span(data_, 64); + } + operator std::span() const { return span(); } + operator std::span() const { return span(); } + const std::byte* data() const { return data_; } + auto begin() const { return data_; } + auto end() const { return data_ + 64; } + static constexpr size_t size() { return 64; } + // Returns the 32-byte seed (first half of the libsodium key). + std::span seed() const { return span().first<32>(); } + // Returns the 32-byte Ed25519 public key (second half of the libsodium key). + std::span pubkey() const { return span().last<32>(); } + + private: + void expand_seed(std::span seed); + void expand_seed(std::span seed); + + const std::byte* data_ = nullptr; + std::optional storage_; +}; + +/// Like PrivKeySpan but with an optional (nullable) state. Use this when a private key parameter +/// is optional; PrivKeySpan retains its always-has-value guarantee. +/// +/// Implicitly constructible from the same 32- or 64-byte sources as PrivKeySpan, plus from +/// default/nullopt for the empty state. Non-copyable and non-moveable for the same reason as +/// PrivKeySpan. +struct OptionalPrivKeySpan { + /// Constructs a null (no-key) state. + OptionalPrivKeySpan() = default; + OptionalPrivKeySpan(std::nullopt_t) {} + + template + OptionalPrivKeySpan(const T& src) : key_{std::in_place, src} {} + + // Constructor for runtime-known sizes (e.g. at C API boundaries). + // size == 0 produces the null state; size == 32 or 64 constructs the key. + // Throws std::invalid_argument if size is not 0, 32, or 64. + OptionalPrivKeySpan(const unsigned char* data, size_t size) { + if (size) + key_.emplace(data, size); + } + + OptionalPrivKeySpan(const OptionalPrivKeySpan&) = delete; + OptionalPrivKeySpan& operator=(const OptionalPrivKeySpan&) = delete; + OptionalPrivKeySpan(OptionalPrivKeySpan&&) = delete; + OptionalPrivKeySpan& operator=(OptionalPrivKeySpan&&) = delete; + + bool has_value() const { return key_.has_value(); } + explicit operator bool() const { return has_value(); } + + const PrivKeySpan& value() const { return key_.value(); } + const PrivKeySpan& operator*() const { return *key_; } + const PrivKeySpan* operator->() const { return &*key_; } + + private: + std::optional key_; +}; + +/// Generates a random Ed25519 key pair. +/// Write-to-output form. +void keypair(std::span pk, std::span sk); +/// Return-value form: returns {pubkey, seckey} where seckey uses cleared memory. +std::pair keypair(); + +/// Generates a deterministic Ed25519 key pair from a 32-byte seed. +/// Write-to-output form. +void seed_keypair( + std::span pk, + std::span sk, + std::span seed); +/// Return-value form: returns {pubkey, seckey} where seckey uses cleared memory. +std::pair keypair(std::span ed25519_seed); + +/// Returns the seed portion of an Ed25519 key as a non-owning span into the caller's data. +/// The overload accepting a 64-byte (libsodium-style) key returns the first 32 bytes (the seed). +/// The overload accepting a 32-byte value returns that span unchanged (it is already a seed). +inline std::span extract_seed( + std::span ed25519_privkey) noexcept { + return ed25519_privkey.first<32>(); +} +inline std::span extract_seed( + std::span ed25519_seed) noexcept { + return ed25519_seed; +} + +/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- the Ed25519 private key; accepts a 32-byte seed or 64-byte libsodium key. +/// - `msg` -- the data to generate a signature for. +/// +/// Outputs: +/// - The 64-byte ed25519 signature +/// +/// Write-to-output form. +void sign(std::span sig, const PrivKeySpan& ed25519_privkey, std::span msg); +/// Return-value form. +b64 sign(const PrivKeySpan& ed25519_privkey, std::span msg); + +/// Verify a message and signature for a given pubkey. +/// +/// Inputs: +/// - `sig` -- the signature to verify, 64 bytes. +/// - `pubkey` -- the pubkey for the secret key that was used to generate the signature, 32 bytes. +/// - `msg` -- the data to verify the signature for. +/// +/// Outputs: +/// - A flag indicating whether the signature is valid +bool verify( + std::span sig, + std::span pubkey, + std::span msg); + +/// Derives a deterministic Ed25519 keypair from a seed and a domain string. +/// +/// The derived seed is: Blake2b32(data=ed25519_seed, hash_key=domain) +/// +/// This is a general subkey derivation primitive; use a distinct domain string per use case +/// to produce independent derived keys from the same root seed. +/// +/// Returns the (pubkey, seckey) pair; the secret key uses cleared memory. +std::pair derive_subkey( + std::span ed25519_seed, std::span domain); + +/// Extracts the 32-byte public key from a 64-byte libsodium Ed25519 secret key. +/// Write-to-output form. +void sk_to_pk(std::span pk, const PrivKeySpan& sk); +/// Return-value form. +b32 sk_to_pk(const PrivKeySpan& sk); + +/// Converts an Ed25519 public key to an X25519 public key. +/// Throws std::runtime_error if the key is invalid. +/// Write-to-output form: result written into `out`. +void pk_to_x25519(std::span out, std::span pk); +/// Return-value form. +b32 pk_to_x25519(std::span pk); + +/// Converts an Ed25519 public key to a 33-byte 0x05-prefixed Session ID by converting the +/// Ed25519 pubkey to X25519 and prefixing 0x05. +/// Write-to-output form: result written into `out`. +void pk_to_session_id(std::span out, std::span pk); +/// Return-value form. +b33 pk_to_session_id(std::span pk); + +/// Converts an Ed25519 secret key to an X25519 secret key (using cleared memory). +/// Overload taking the 32-byte seed directly (the libsodium implementation only reads the +/// first 32 bytes anyway, so this is both correct and avoids expanding a seed unnecessarily). +cleared_b32 sk_to_x25519(std::span seed); +/// Overload for a full 64-byte Ed25519 secret key (seed || pubkey); only the seed (first 32 +/// bytes) is used. +inline cleared_b32 sk_to_x25519(std::span full_key) { + return sk_to_x25519(full_key.first<32>()); +} +/// Overload for PrivKeySpan (deduced exactly, suppressing implicit conversions). +template T> +inline cleared_b32 sk_to_x25519(const T& sk) { + return sk_to_x25519(sk.seed()); +} + +/// Returns the private Ed25519 scalar `a` from a seed or PrivKeySpan (using cleared memory). +/// +/// Ed25519 and X25519 share the same private scalar: the Ed25519-to-X25519 conversion is +/// defined by using that same scalar on X25519's base point instead of Ed25519's. Use this +/// alias wherever the goal is to obtain the private scalar `a` rather than an X25519 key. +template + requires requires(T&& t) { sk_to_x25519(std::forward(t)); } +inline cleared_b32 sk_to_private(T&& arg) { + return sk_to_x25519(std::forward(arg)); +} + +/// Computes the Ed25519 group element from a scalar (clamped). +/// Write-to-output form: result written into `out`. +void scalarmult_base(std::span out, std::span scalar); +/// Return-value form. +b32 scalarmult_base(std::span scalar); + +/// Computes the Ed25519 group element from a scalar (no clamping). +/// Write-to-output form: result written into `out`. +void scalarmult_base_noclamp(std::span out, std::span scalar); +/// Return-value form. +b32 scalarmult_base_noclamp(std::span scalar); + +/// Multiplies an Ed25519 point by a scalar (no clamping). +/// Write-to-output form: result written into `out`. +void scalarmult_noclamp( + std::span out, + std::span scalar, + std::span point); +/// Return-value form. +b32 scalarmult_noclamp( + std::span scalar, std::span point); + +/// Reduces a 64-byte scalar modulo the Ed25519 group order to 32 bytes. +/// Write-to-output form: result written into `out`. +void scalar_reduce(std::span out, std::span in); +/// Return-value form. +b32 scalar_reduce(std::span in); + +/// Negates a 32-byte Ed25519 scalar. +/// Write-to-output form: result written into `out` (safe to alias `in`). +void scalar_negate(std::span out, std::span in); +/// Return-value form. +b32 scalar_negate(std::span in); + +/// Multiplies two 32-byte Ed25519 scalars. +/// Write-to-output form: result written into `out` (safe to alias `x` or `y`). +void scalar_mul( + std::span out, + std::span x, + std::span y); +/// Return-value form. +b32 scalar_mul(std::span x, std::span y); + +/// Adds two 32-byte Ed25519 scalars. +/// Write-to-output form: result written into `out` (safe to alias `x` or `y`). +void scalar_add( + std::span out, + std::span x, + std::span y); +/// Return-value form. +b32 scalar_add(std::span x, std::span y); + +} // namespace session::ed25519 diff --git a/include/session/crypto/mlkem768.hpp b/include/session/crypto/mlkem768.hpp new file mode 100644 index 000000000..967812f3a --- /dev/null +++ b/include/session/crypto/mlkem768.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include "session/util.hpp" + +namespace session::mlkem768 { + +inline constexpr size_t PUBLICKEYBYTES = 1184; +inline constexpr size_t SECRETKEYBYTES = 2400; +inline constexpr size_t CIPHERTEXTBYTES = 1088; +inline constexpr size_t SHAREDSECRETBYTES = 32; +inline constexpr size_t SEEDBYTES = 64; // 2 * MLKEM_SYMBYTES + +/// Generates a keypair deterministically from a 64-byte seed. Throws on failure. +void keygen( + std::span pk, + std::span sk, + std::span seed); + +/// Encapsulates a shared secret to `pk` using a 32-byte random seed, writing the ciphertext and +/// shared secret into the provided spans. Throws on failure. +void encapsulate( + std::span ciphertext, + std::span shared_secret, + std::span pk, + std::span seed); + +/// Decapsulates a shared secret from `ciphertext` using `sk`. Returns false on failure. +bool decapsulate( + std::span shared_secret, + std::span ciphertext, + std::span sk); + +} // namespace session::mlkem768 diff --git a/include/session/crypto/x25519.hpp b/include/session/crypto/x25519.hpp new file mode 100644 index 000000000..748cf0a6e --- /dev/null +++ b/include/session/crypto/x25519.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#include "session/sodium_array.hpp" +#include "session/util.hpp" + +namespace session::x25519 { + +/// Generates a random X25519 keypair. +/// Write-to-output form. +void keypair(std::span pk, std::span sk); +/// Return-value form: returns {pubkey, seckey}. +std::pair keypair(); + +/// Generates a deterministic X25519 keypair from a 32-byte seed. +/// Write-to-output form. +void seed_keypair( + std::span pk, + std::span sk, + std::span seed); +/// Return-value form: returns {pubkey, seckey}. +std::pair seed_keypair(std::span seed); + +/// Computes the X25519 public key corresponding to `sk`: out = sk * G. +/// Write-to-output form. +void scalarmult_base(std::span out, std::span scalar); +/// Return-value form. +b32 scalarmult_base(std::span scalar); + +/// Computes X25519 scalar multiplication: out = scalar * point. +/// Returns false if the result is the all-zeros point (degenerate case). +/// Write-to-output form. +bool scalarmult( + std::span out, + std::span scalar, + std::span point); +/// Return-value form. Throws on degenerate case. +b32 scalarmult(std::span scalar, std::span point); + +} // namespace session::x25519 diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp deleted file mode 100644 index b476f5ac1..000000000 --- a/include/session/curve25519.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "types.hpp" - -namespace session::curve25519 { - -/// Generates a random curve25519 key pair -std::pair, std::array> curve25519_key_pair(); - -/// API: curve25519/to_curve25519_pubkey -/// -/// Generates a curve25519 public key for an ed25519 public key. -/// -/// Inputs: -/// - `ed25519_pubkey` -- the ed25519 public key. -/// -/// Outputs: -/// - The curve25519 public key -std::array to_curve25519_pubkey(std::span ed25519_pubkey); - -/// API: curve25519/to_curve25519_seckey -/// -/// Generates a curve25519 secret key given given a libsodium-style secret key, 64 -/// bytes. -/// -/// Inputs: -/// - `ed25519_seckey` -- the libsodium-style secret key, 64 bytes. -/// -/// Outputs: -/// - The curve25519 secret key -std::array to_curve25519_seckey(std::span ed25519_seckey); - -} // namespace session::curve25519 diff --git a/include/session/ed25519.h b/include/session/ed25519.h index 573a66fed..a85adc101 100644 --- a/include/session/ed25519.h +++ b/include/session/ed25519.h @@ -20,7 +20,7 @@ extern "C" { /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. -LIBSESSION_EXPORT bool session_ed25519_key_pair( +LIBSESSION_EXPORT bool session_keypair( unsigned char* ed25519_pk_out, /* 32 byte output buffer */ unsigned char* ed25519_sk_out /* 64 byte output buffer */); @@ -53,7 +53,7 @@ LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( /// /// Outputs: /// - `bool` -- True if the seed was successfully retrieved, false if failed. -LIBSESSION_EXPORT bool session_seed_for_ed_privkey( +LIBSESSION_EXPORT bool session_extract_seed( const unsigned char* ed25519_privkey, /* 64 bytes */ unsigned char* ed25519_seed_out /* 32 byte output buffer */); @@ -108,7 +108,7 @@ LIBSESSION_EXPORT bool session_ed25519_verify( /// /// Outputs: /// - `bool` -- True if the key pair was successfully derived, false if failed. -LIBSESSION_EXPORT bool session_ed25519_pro_privkey_for_ed25519_seed( +LIBSESSION_EXPORT bool session_derive_subkey( const unsigned char* ed25519_seed, /* 32 bytes */ unsigned char* ed25519_sk_out /*64 byte output buffer*/); diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp deleted file mode 100644 index 99901b9d2..000000000 --- a/include/session/ed25519.hpp +++ /dev/null @@ -1,184 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "session/sodium_array.hpp" - -namespace session { - -/// A span-like type representing a fully-expanded Ed25519 private key (always 64 bytes). -/// Implicitly constructible from any fixed-extent 32- or 64-byte unsigned char spannable: -/// - 32-byte input (seed): the 64-byte key is computed via libsodium and stored internally. -/// - 64-byte input: holds a non-owning span into the caller's data — no copy or allocation. -/// -/// Non-copyable and non-moveable to avoid dangling references to internal storage. -struct Ed25519PrivKeySpan { - template - requires( - std::constructible_from, const T&> || - std::constructible_from, const T&>) - Ed25519PrivKeySpan(const T& src) { - if constexpr (std::constructible_from, const T&>) - data_ = std::span{src}.data(); - else { - expand_seed(std::span{src}); - data_ = storage_->data(); - } - } - - // Constructor for runtime-known sizes (e.g. at C API boundaries). - // Throws std::invalid_argument if size is not 32 or 64. - Ed25519PrivKeySpan(const unsigned char* data, size_t size) { - if (size == 64) - data_ = data; - else if (size == 32) { - expand_seed(std::span{data, 32}); - data_ = storage_->data(); - } else - throw std::invalid_argument{"Ed25519 private key must be 32 or 64 bytes"}; - } - - // Named factory alias for dynamic-span input. - static Ed25519PrivKeySpan from(std::span key) { - return {key.data(), key.size()}; - } - - Ed25519PrivKeySpan(const Ed25519PrivKeySpan&) = delete; - Ed25519PrivKeySpan& operator=(const Ed25519PrivKeySpan&) = delete; - Ed25519PrivKeySpan(Ed25519PrivKeySpan&&) = delete; - Ed25519PrivKeySpan& operator=(Ed25519PrivKeySpan&&) = delete; - - std::span span() const { - return std::span(data_, 64); - } - operator std::span() const { return span(); } - operator std::span() const { return span(); } - const unsigned char* data() const { return data_; } - auto begin() const { return data_; } - auto end() const { return data_ + 64; } - static constexpr size_t size() { return 64; } - // Returns the 32-byte seed (first half of the libsodium key). - std::span seed() const { return span().first<32>(); } - // Returns the 32-byte Ed25519 public key (second half of the libsodium key). - std::span pubkey() const { return span().last<32>(); } - - private: - void expand_seed(std::span seed); - - const unsigned char* data_ = nullptr; - std::optional storage_; -}; - -/// Like Ed25519PrivKeySpan but with an optional (nullable) state. Use this when a private key -/// parameter is optional; Ed25519PrivKeySpan retains its always-has-value guarantee. -/// -/// Implicitly constructible from the same 32- or 64-byte sources as Ed25519PrivKeySpan, plus from -/// default/nullopt for the empty state. Non-copyable and non-moveable for the same reason as -/// Ed25519PrivKeySpan. -struct OptionalEd25519PrivKeySpan { - /// Constructs a null (no-key) state. - OptionalEd25519PrivKeySpan() = default; - OptionalEd25519PrivKeySpan(std::nullopt_t) {} - - template - requires( - std::constructible_from, const T&> || - std::constructible_from, const T&>) - OptionalEd25519PrivKeySpan(const T& src) : key_{std::in_place, src} {} - - // Constructor for runtime-known sizes (e.g. at C API boundaries). - // size == 0 produces the null state; size == 32 or 64 constructs the key. - // Throws std::invalid_argument if size is not 0, 32, or 64. - OptionalEd25519PrivKeySpan(const unsigned char* data, size_t size) { - if (size) - key_.emplace(data, size); - } - - OptionalEd25519PrivKeySpan(const OptionalEd25519PrivKeySpan&) = delete; - OptionalEd25519PrivKeySpan& operator=(const OptionalEd25519PrivKeySpan&) = delete; - OptionalEd25519PrivKeySpan(OptionalEd25519PrivKeySpan&&) = delete; - OptionalEd25519PrivKeySpan& operator=(OptionalEd25519PrivKeySpan&&) = delete; - - bool has_value() const { return key_.has_value(); } - explicit operator bool() const { return has_value(); } - - const Ed25519PrivKeySpan& value() const { return key_.value(); } - const Ed25519PrivKeySpan& operator*() const { return *key_; } - const Ed25519PrivKeySpan* operator->() const { return &*key_; } - - private: - std::optional key_; -}; - -} // namespace session - -namespace session::ed25519 { - -/// Generates a random Ed25519 key pair -std::pair, std::array> ed25519_key_pair(); - -/// Given an Ed25519 seed this returns the associated Ed25519 key pair -std::pair, std::array> ed25519_key_pair( - std::span ed25519_seed); - -/// API: ed25519/seed_for_ed_privkey -/// -/// Returns the seed for an ed25519 key pair given either the libsodium-style secret key, 64 -/// bytes. If a 32-byte value is provided it is assumed to be the seed and the value will just -/// be returned directly. -/// -/// Inputs: -/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be -/// passed as a 32-byte seed. -/// -/// Outputs: -/// - The ed25519 seed -std::array seed_for_ed_privkey(std::span ed25519_privkey); - -/// API: ed25519/sign -/// -/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. -/// -/// Inputs: -/// - `ed25519_privkey` -- the Ed25519 private key; accepts a 32-byte seed or 64-byte libsodium key. -/// - `msg` -- the data to generate a signature for. -/// -/// Outputs: -/// - The ed25519 signature -std::vector sign( - const Ed25519PrivKeySpan& ed25519_privkey, std::span msg); - -/// API: ed25519/verify -/// -/// Verify a message and signature for a given pubkey. -/// -/// Inputs: -/// - `sig` -- the signature to verify, 64 bytes. -/// - `pubkey` -- the pubkey for the secret key that was used to generate the signature, 32 bytes. -/// - `msg` -- the data to verify the signature for. -/// -/// Outputs: -/// - A flag indicating whether the signature is valid -bool verify( - std::span sig, - std::span pubkey, - std::span msg); - -/// API: ed25519/ed25519_pro_privkey_for_ed25519_seed -/// -/// Generate the deterministic Master Session Pro key for signing requests to interact with the -/// Session Pro features of the protocol. -/// -/// Inputs: -/// - `ed25519_seed` -- the seed to the long-term key for the Session account to derive the -/// deterministic key from. -/// -/// Outputs: -/// - The libsodium-style Master Session Pro Ed25519 secret key, 64 bytes. -std::array ed25519_pro_privkey_for_ed25519_seed( - std::span ed25519_seed); - -} // namespace session::ed25519 diff --git a/include/session/encrypt.hpp b/include/session/encrypt.hpp index 2bbbbb60d..00721f8c5 100644 --- a/include/session/encrypt.hpp +++ b/include/session/encrypt.hpp @@ -16,47 +16,37 @@ namespace session::encrypt { +// ─── Constants ─────────────────────────────────────────────────────────────── + +inline constexpr size_t XCHACHA20_KEYBYTES = 32; +inline constexpr size_t XCHACHA20_NONCEBYTES = 24; +inline constexpr size_t XCHACHA20_ABYTES = 16; // authentication tag size + // ─── XChaCha20-Poly1305 AEAD ───────────────────────────────────────────────── /// Encrypts `msg` with `key` and `nonce`, writing ciphertext (msg.size() + ABYTES bytes) into -/// `out`. Additional data `ad` is authenticated but not encrypted; pass an empty span for none. +/// `out`. inline void xchacha20poly1305_encrypt( std::span out, std::span msg, - std::span ad, std::span nonce, std::span key) { crypto_aead_xchacha20poly1305_ietf_encrypt( - ucdata(out), - nullptr, - ucdata(msg), - msg.size(), - ad.empty() ? nullptr : ucdata(ad), - ad.size(), - nullptr, - ucdata(nonce), - ucdata(key)); + ucdata(out), nullptr, ucdata(msg), msg.size(), nullptr, 0, nullptr, + ucdata(nonce), ucdata(key)); } /// Decrypts `ciphertext` with `key` and `nonce`, writing plaintext (ciphertext.size() - ABYTES -/// bytes) into `out`. Additional data `ad` must match what was passed at encryption time. -/// Returns false if authentication fails. +/// bytes) into `out`. Returns false if authentication fails. inline bool xchacha20poly1305_decrypt( std::span out, std::span ciphertext, - std::span ad, std::span nonce, std::span key) { return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - ucdata(out), - nullptr, - nullptr, - ucdata(ciphertext), - ciphertext.size(), - ad.empty() ? nullptr : ucdata(ad), - ad.size(), - ucdata(nonce), - ucdata(key)); + ucdata(out), nullptr, nullptr, + ucdata(ciphertext), ciphertext.size(), nullptr, 0, + ucdata(nonce), ucdata(key)); } // ─── XChaCha20 stream ──────────────────────────────────────────────────────── diff --git a/include/session/fields.hpp b/include/session/fields.hpp index b70980d11..7376328e7 100644 --- a/include/session/fields.hpp +++ b/include/session/fields.hpp @@ -6,6 +6,8 @@ #include #include +#include "util.hpp" + namespace session { using namespace std::literals; @@ -32,10 +34,10 @@ struct Disappearing { /// 32-byte pubkey value (i.e. not hex, without the prefix). struct SessionID { /// The fixed session netid, 0x05 - static constexpr unsigned char netid = 0x05; + static constexpr std::byte netid{0x05}; /// The raw x25519 pubkey, as bytes - std::array pubkey; + b32 pubkey; /// Returns the full pubkey in hex, including the netid prefix. std::string hex() const; diff --git a/include/session/hash.hpp b/include/session/hash.hpp index f2dddc908..27169b86c 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -5,16 +5,20 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include #include +#include "session/util.hpp" + namespace session::hash { /// API: hash/hash @@ -32,9 +36,9 @@ namespace session::hash { /// Deprecated: prefer hash::blake2b (unkeyed) or hash::blake2b_key (keyed) instead. [[deprecated("Use hash::blake2b or hash::blake2b_key instead")]] void hash( - std::span hash, - std::span msg, - std::optional> key = std::nullopt); + std::span hash, + std::span msg, + std::optional> key = std::nullopt); /// API: hash/hash /// @@ -52,16 +56,17 @@ void hash( /// /// Deprecated: prefer hash::blake2b (unkeyed) or hash::blake2b_key (keyed) instead. [[deprecated("Use hash::blake2b or hash::blake2b_key instead")]] -std::vector hash( +std::vector hash( const size_t size, - std::span msg, - std::optional> key = std::nullopt); + std::span msg, + std::optional> key = std::nullopt); template concept ByteContainer = std::ranges::contiguous_range && oxenc::basic_char>; template -concept HashInput = ByteContainer || oxenc::endian_swappable_integer; +concept HashInput = + ByteContainer || oxenc::endian_swappable_integer || std::same_as; namespace detail { @@ -137,8 +142,97 @@ concept Blake2BKey = (detail::container_extent_v == std::dynamic_extent || detail::container_extent_v <= 64); -/// Helper value to pass a null `key` to blake2b or blake2b_pers. -inline constexpr std::span nullkey{}; +/// Helper value to pass a null key to blake2b_key, blake2b_key_pers, or blake2b_hasher (e.g. when +/// only a personalisation string is wanted). +inline constexpr std::span nullkey{}; + +/// API: hash/blake2b_hasher +/// +/// Streaming (piecewise) BLAKE2b hasher with a compile-time fixed output size N (in [1, 64]). +/// Construct with an optional key and/or personalisation string, call update() with data pieces +/// in any order, then call finalize() to produce the result. +/// +/// The output size N is a template parameter and is fixed at construction, so init and finalize +/// always agree on the size. +/// +/// Like shake256, non-copyable and non-moveable; the internal state is zeroed on destruction. +/// +/// Constructors: +/// blake2b_hasher{} — no key, no pers +/// blake2b_hasher{key, nullopt} — key only +/// blake2b_hasher{nullkey, pers} — pers only +/// blake2b_hasher{key, pers} — key + pers +/// +/// The two-argument constructor has no default for pers to force explicit intent: if you want +/// only a key, you must write `std::nullopt`; if you want only a pers, you must write `nullkey`. +/// This prevents accidentally passing a `_b2b_pers` value as a key. +/// +/// Example: +/// +/// hash::blake2b_hasher<32> h{my_key, std::nullopt}; +/// h.update(part1, part2); // update with multiple args at once +/// h.update(part3); // or call update multiple times +/// auto result = h.finalize(); +/// +template + requires(N >= 1 && N <= 64) +struct blake2b_hasher { + crypto_generichash_blake2b_state st; + + /// No-key, no-pers constructor. + blake2b_hasher() { + crypto_generichash_blake2b_init_salt_personal(&st, nullptr, 0, N, nullptr, nullptr); + } + + /// Key + optional personalisation constructor. Pass `nullkey` as key for pers-only; + /// pass `std::nullopt` as pers for key-only. + /// + /// Dynamic-extent keys are silently truncated to 64 bytes (the blake2b key size limit); + /// static-extent keys are guaranteed ≤ 64 at compile time by the Blake2BKey concept. + template + blake2b_hasher(const Key& key, std::optional> pers) { + crypto_generichash_blake2b_init_salt_personal( + &st, + reinterpret_cast(std::ranges::data(key)), + std::min(std::ranges::size(key), 64), + N, + /*salt=*/nullptr, + pers ? reinterpret_cast(pers->data()) : nullptr); + } + + ~blake2b_hasher() { sodium_memzero(&st, sizeof(st)); } + + blake2b_hasher(const blake2b_hasher&) = delete; + blake2b_hasher& operator=(const blake2b_hasher&) = delete; + blake2b_hasher(blake2b_hasher&&) = delete; + blake2b_hasher& operator=(blake2b_hasher&&) = delete; + + /// Feeds one or more contiguous byte containers or integer values into the hash state, in + /// argument order. Integer values are written as raw bytes in little-endian encoding (i.e. + /// they will be byte-swapped on big-endian platforms if necessary). May be called multiple + /// times; each call appends to the state from previous calls. + template + requires(sizeof...(T) > 0) + blake2b_hasher& update(const T&... args) { + update_all(st, args...); + return *this; + } + + /// Write-to-output finalize: writes the N-byte hash into `out`. + template + requires(detail::container_extent_v == N) + void finalize(Out& out) { + crypto_generichash_blake2b_final( + &st, reinterpret_cast(std::ranges::data(out)), N); + } + + /// Return-value finalize: returns a `std::array`. + std::array finalize() { + std::array result; + finalize(result); + return result; + } +}; /// API: hash/blake2b_key /// @@ -151,22 +245,12 @@ inline constexpr std::span nullkey{}; template requires(sizeof...(T) > 0) void blake2b_key(Out& out, const Key& key, const T&... args) { - crypto_generichash_blake2b_state st; - // Dynamic-extent keys are silently truncated to 64 bytes (the blake2b key size limit); static- - // extent keys are guaranteed ≤ 64 at compile time by the Blake2BKey concept. - crypto_generichash_blake2b_init( - &st, - reinterpret_cast(std::ranges::data(key)), - std::min(std::ranges::size(key), 64), - std::ranges::size(out)); - update_all(st, args...); - crypto_generichash_blake2b_final( - &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); + blake2b_hasher>{key, std::nullopt}.update(args...).finalize(out); } template requires(sizeof...(T) > 0 && N >= 1 && N <= 64) -std::array blake2b_key(const Key& key, const T&... args) { - std::array result; +std::array blake2b_key(const Key& key, const T&... args) { + std::array result; blake2b_key(result, key, args...); return result; } @@ -197,8 +281,8 @@ void blake2b(Out& out, const T&... args) { } template requires(sizeof...(T) > 0 && N >= 1 && N <= 64) -std::array blake2b(const T&... args) { - std::array result; +std::array blake2b(const T&... args) { + std::array result; blake2b(result, args...); return result; } @@ -207,8 +291,8 @@ std::array blake2b(const T&... args) { /// /// This version of blake2b() takes a both a key and a 16-byte personalisation string as the second /// and third arguments and computes a keyed hash with a personalisation string. The -/// personalisation string must be exact 16 bytes, and is typically constructed with "..."_b2b_pers -/// for compile-time validation. The key must be between 0 and 64 bytes long. +/// personalisation string must be exactly 16 bytes, and is typically constructed with +/// "..."_b2b_pers for compile-time validation. The key must be between 0 and 64 bytes long. /// /// Two overloads are provided: /// - write-to-output: `blake2b_key_pers(out, key, pers, args...)` writes the hash into `out` @@ -217,24 +301,14 @@ std::array blake2b(const T&... args) { template requires(sizeof...(T) > 0) void blake2b_key_pers( - Out& out, const Key& key, std::span pers, const T&... args) { - crypto_generichash_blake2b_state st; - crypto_generichash_blake2b_init_salt_personal( - &st, - reinterpret_cast(std::ranges::data(key)), - std::min(std::ranges::size(key), 64), - std::ranges::size(out), - /*salt=*/nullptr, - pers.data()); - update_all(st, args...); - crypto_generichash_blake2b_final( - &st, reinterpret_cast(std::ranges::data(out)), std::ranges::size(out)); + Out& out, const Key& key, std::span pers, const T&... args) { + blake2b_hasher>{key, pers}.update(args...).finalize(out); } template requires(sizeof...(T) > 0 && N >= 1 && N <= 64) -std::array blake2b_key_pers( - const Key& key, std::span pers, const T&... args) { - std::array result; +std::array blake2b_key_pers( + const Key& key, std::span pers, const T&... args) { + std::array result; blake2b_key_pers(result, key, pers, args...); return result; } @@ -250,14 +324,13 @@ std::array blake2b_key_pers( /// - return-value: `blake2b_pers(pers, args...)` returns a `std::array` template requires(sizeof...(T) > 0) -void blake2b_pers(Out& out, std::span pers, const T&... args) { +void blake2b_pers(Out& out, std::span pers, const T&... args) { return blake2b_key_pers(out, nullkey, pers, args...); } template requires(sizeof...(T) > 0 && N >= 1 && N <= 64) -std::array blake2b_pers( - std::span pers, const T&... args) { - std::array result; +std::array blake2b_pers(std::span pers, const T&... args) { + std::array result; blake2b_pers(result, pers, args...); return result; } @@ -277,10 +350,10 @@ std::array blake2b_pers( /// Example: /// /// // Squeeze two outputs in one call: -/// hash::shake256("SessionMyKey"_uc, seed)(out_a, out_b); +/// hash::shake256("SessionMyKey"_bytes, seed)(out_a, out_b); /// /// // Or squeeze incrementally: -/// hash::shake256 sq{"SessionMyKey"_uc, seed}; +/// hash::shake256 sq{"SessionMyKey"_bytes, seed}; /// sq(out_a); /// sq(out_b); /// @@ -311,11 +384,11 @@ struct [[nodiscard]] shake256 { return *this; } - /// Squeezes N bytes from the state and returns them as a `std::array`. + /// Squeezes N bytes from the state and returns them as a `std::array`. template requires(N >= 1) - std::array squeeze() { - std::array result; + std::array squeeze() { + std::array result; (*this)(result); return result; } @@ -344,7 +417,7 @@ struct [[nodiscard]] shake256 { /// /// Two overloads are provided: /// - write-to-output: `sha3_256(out, args...)` writes the hash into `out` -/// - return-value: `sha3_256<32>(args...)` returns a `std::array` +/// - return-value: `sha3_256<32>(args...)` returns a `std::array` template requires(detail::container_extent_v == 32 && sizeof...(T) > 0) void sha3_256(Out& out, const T&... args) { @@ -355,8 +428,8 @@ void sha3_256(Out& out, const T&... args) { } template requires(N == 32 && sizeof...(T) > 0) -std::array sha3_256(const T&... args) { - std::array result; +std::array sha3_256(const T&... args) { + std::array result; sha3_256(result, args...); return result; } @@ -412,53 +485,55 @@ void hmac_sha256(Out& out, const Key& key, const T&... args) { sodium_memzero(&st, sizeof(st)); } -} // namespace session::hash +// ─── Argon2id (password hashing / KDF) ─────────────────────────────────────── -namespace session { +/// Derives a key from a password using Argon2id (libsodium crypto_pwhash). +/// Throws std::runtime_error if the derivation fails (e.g. out of memory). +/// +/// Inputs: +/// - `out` -- writable byte container to receive the derived key (between 16 and 4294967295 +/// bytes). +/// - `password` -- the password/input data. +/// - `salt` -- the 16-byte Argon2 salt (`crypto_pwhash_SALTBYTES`). +/// - `opslimit` -- CPU cost parameter (e.g. `crypto_pwhash_OPSLIMIT_MODERATE`). +/// - `memlimit` -- memory cost parameter (e.g. `crypto_pwhash_MEMLIMIT_MODERATE`). +/// - `alg` -- algorithm selector (e.g. `crypto_pwhash_ALG_ARGON2ID13`). +template + requires(detail::container_extent_v >= crypto_pwhash_BYTES_MIN) +void argon2( + Out& out, + std::span password, + std::span salt, + unsigned long long opslimit, + size_t memlimit, + int alg) { + if (0 != crypto_pwhash( + reinterpret_cast(std::ranges::data(out)), + std::ranges::size(out), + password.data(), + password.size(), + reinterpret_cast(salt.data()), + opslimit, + memlimit, + alg)) + throw std::runtime_error{"crypto_pwhash failed (out of memory?)"}; +} -template -struct StringLiteral { - std::array chars; - constexpr StringLiteral(const char (&s)[N]) { - for (size_t i = 0; i < N - 1; ++i) - chars[i] = s[i]; - } - static constexpr size_t size() { return N; } -}; +} // namespace session::hash -inline namespace literals { +namespace session { inline namespace literals { - /// User-defined literal for a 16-byte, unsigned char array intended for use as a BLAKE2b - /// personality value. Example: + /// User-defined literal for a 16-byte personalization value for use with BLAKE2b. + /// Enforces the 16-byte length at compile time via the requires clause. Returns a + /// fixed-extent span so it passes directly to blake2b_pers / blake2b_key_pers. Example: /// - /// using namespace session::literals; + /// using namespace session::literals; // or `using namespace session;` /// constexpr auto PERS_XYZ = "XYZ-XYZ-XYZ-WXYZ"_b2b_pers; /// - template - constexpr auto operator""_b2b_pers() { - static_assert( - Str.chars.size() == 16, - "BLAKE2b personalization strings must be exactly 16 bytes long"); - std::array pers; - for (size_t i = 0; i < pers.size(); i++) - pers[i] = static_cast(Str.chars[i]); - return pers; - } - - /// User-defined literal for an arbitrary-length, unsigned char array; this is primarily - /// intended for fixed keys with BLAKE2b hash. Example: - /// - /// using namespace session::literals; - /// constexpr auto HASH_KEY_42 = "forty-two"_uc; - /// - template - constexpr auto operator""_uc() { - std::array pers; - for (size_t i = 0; i < pers.size(); i++) - pers[i] = static_cast(Str.chars[i]); - return pers; + template + requires(Lit.size == 16) + consteval auto operator""_b2b_pers() { + return operator""_bytes(); } -} // namespace literals - -} // namespace session +} } // namespace session::literals diff --git a/include/session/mlkem768.hpp b/include/session/mlkem768.hpp deleted file mode 100644 index e9dc0c508..000000000 --- a/include/session/mlkem768.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace session::mlkem768 { - -/// Generates a keypair deterministically from a 64-byte seed. Throws on failure. -inline void keygen( - std::span pk, - std::span sk, - std::span seed) { - if (0 != sr_mlkem768_keypair_derand( - reinterpret_cast(pk.data()), - reinterpret_cast(sk.data()), - reinterpret_cast(seed.data()))) - throw std::runtime_error{"ML-KEM-768 keygen failed"}; -} - -/// Encapsulates a shared secret to `pk` using a 32-byte random seed, writing the ciphertext and -/// shared secret into the provided spans. Throws on failure. -inline void encapsulate( - std::span ciphertext, - std::span shared_secret, - std::span pk, - std::span seed) { - if (0 != sr_mlkem768_enc_derand( - reinterpret_cast(ciphertext.data()), - reinterpret_cast(shared_secret.data()), - reinterpret_cast(pk.data()), - reinterpret_cast(seed.data()))) - throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; -} - -/// Decapsulates a shared secret from `ciphertext` using `sk`. Returns false on failure. -inline bool decapsulate( - std::span shared_secret, - std::span ciphertext, - std::span sk) { - return 0 == sr_mlkem768_dec( - reinterpret_cast(shared_secret.data()), - reinterpret_cast(ciphertext.data()), - reinterpret_cast(sk.data())); -} - -} // namespace session::mlkem768 diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 89a95b733..cf92b11ee 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -8,6 +8,7 @@ #include #include +#include "crypto/ed25519.hpp" #include "sodium_array.hpp" #include "types.hpp" @@ -33,29 +34,29 @@ namespace session { namespace detail { void encrypt_multi_key( - std::array& key_out, - std::span a, - std::span A, - std::span B, + std::span key_out, + std::span a, + std::span A, + std::span B, bool encrypting, std::string_view domain); void encrypt_multi_impl( - std::vector& out, - std::span message, - const unsigned char* key, - const unsigned char* nonce); + std::vector& out, + std::span message, + std::span key, + std::span nonce); bool decrypt_multi_impl( - std::vector& out, - std::span ciphertext, - const unsigned char* key, - const unsigned char* nonce); + std::vector& out, + std::span ciphertext, + std::span key, + std::span nonce); inline void validate_multi_fields( - std::span nonce, - std::span privkey, - std::span pubkey) { + std::span nonce, + std::span privkey, + std::span pubkey) { if (nonce.size() < 24) throw std::logic_error{"nonce must be 24 bytes"}; if (privkey.size() != 32) @@ -76,7 +77,7 @@ extern const size_t encrypt_multiple_message_overhead; /// API: crypto/encrypt_for_multiple /// /// Encrypts a message multiple times for multiple recipients. `callable` is invoked once per -/// encrypted (or junk) value, passed as a `std::span`. +/// encrypted (or junk) value, passed as a `std::span`. /// /// Inputs: /// - `messages` -- a vector of message bodies to encrypt. Must be either size 1, or of the same @@ -97,20 +98,19 @@ extern const size_t encrypt_multiple_message_overhead; /// used to generate individual keys for domain separation, and so should ideally have a different /// value in different contexts (i.e. group keys uses one value, kicked messages use another, /// etc.). *Can* be empty, but should be set to something. -/// - `call` -- this is invoked for each different encrypted value with a std::span; the caller -/// must copy as needed as the std::span doesn't remain valid past the call. +/// - `call` -- this is invoked for each different encrypted value with a std::span; +/// the caller must copy as needed as the span doesn't remain valid past the call. /// - `ignore_invalid_recipient` -- if given and true then any recipients that appear to have /// invalid public keys (i.e. the shared key multiplication fails) will be silently ignored (the /// callback will not be called). If not given (or false) then such a failure for any recipient /// will raise an exception. template void encrypt_for_multiple( - const std::vector> messages, - const std::vector> recipients, - std::span nonce, - std::span privkey, - std::span pubkey, + const std::vector> messages, + const std::vector> recipients, + std::span nonce, + std::span privkey, + std::span pubkey, std::string_view domain, F&& call, bool ignore_invalid_recipient = false) { @@ -129,10 +129,10 @@ void encrypt_for_multiple( if (auto sz = m.size(); sz > max_msg_size) max_msg_size = sz; - std::vector encrypted; + std::vector encrypted; encrypted.reserve(max_msg_size + encrypt_multiple_message_overhead); - sodium_cleared> key; + cleared_b32 key; auto msg_it = messages.begin(); for (const auto& r : recipients) { const auto& m = *msg_it; @@ -147,7 +147,7 @@ void encrypt_for_multiple( else throw; } - detail::encrypt_multi_impl(encrypted, m, key.data(), nonce.data()); + detail::encrypt_multi_impl(encrypted, m, key, nonce.first<24>()); call(to_span(encrypted)); } } @@ -155,7 +155,7 @@ void encrypt_for_multiple( /// Wrapper for passing a single message for all recipients; all arguments other than the first are /// identical. template -void encrypt_for_multiple(std::span message, Args&&... args) { +void encrypt_for_multiple(std::span message, Args&&... args) { return encrypt_for_multiple( to_view_vector(&message, &message + 1), std::forward(args)...); } @@ -163,21 +163,17 @@ template void encrypt_for_multiple(std::string_view message, Args&&... args) { return encrypt_for_multiple(to_span(message), std::forward(args)...); } -template -void encrypt_for_multiple(std::span message, Args&&... args) { - return encrypt_for_multiple(to_span(message), std::forward(args)...); -} /// API: crypto/decrypt_for_multiple /// /// Decryption via a lambda: we call the lambda (which must return a std::optional>) repeatedly until we get back a nullopt, and attempt to decrypt each returned +/// std::byte>>) repeatedly until we get back a nullopt, and attempt to decrypt each returned /// value. When decryption succeeds, we return the plaintext to the caller. If none of the fed-in /// values can be decrypt, we return std::nullopt. /// /// Inputs: -/// - `ciphertext` -- callback that returns a std::optional> or -/// std::optional> +/// - `ciphertext` -- callback that returns a std::optional> or +/// std::optional> /// when called, containing the next ciphertext; should return std::nullopt when finished. /// - `nonce` -- the nonce used for encryption/decryption (which must have been provided by the /// sender alongside the encrypted messages, and is the same as the `nonce` value given to @@ -193,33 +189,33 @@ template < typename NextCiphertext, typename = std::enable_if_t< std::is_invocable_r_v< - std::optional>, + std::optional>, NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || // legacy + std::is_invocable_r_v>, NextCiphertext> || // legacy std::is_invocable_r_v, NextCiphertext> || - std::is_invocable_r_v, NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext>>> -std::optional> decrypt_for_multiple( + std::is_invocable_r_v, NextCiphertext>>> +std::optional> decrypt_for_multiple( NextCiphertext next_ciphertext, - std::span nonce, - std::span privkey, - std::span pubkey, - std::span sender_pubkey, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { detail::validate_multi_fields(nonce, privkey, pubkey); if (sender_pubkey.size() != 32) throw std::logic_error{"pubkey requires a 32-byte pubkey"}; - sodium_cleared> key; + cleared_b32 key; detail::encrypt_multi_key( key, privkey.first<32>(), pubkey.first<32>(), sender_pubkey.first<32>(), false, domain); - auto decrypted = std::make_optional>(); + auto decrypted = std::make_optional>(); for (auto ciphertext = next_ciphertext(); ciphertext; ciphertext = next_ciphertext()) - if (detail::decrypt_multi_impl(*decrypted, *ciphertext, key.data(), nonce.data())) + if (detail::decrypt_multi_impl(*decrypted, *ciphertext, key, nonce.first<24>())) return decrypted; decrypted.reset(); @@ -244,12 +240,12 @@ std::optional> decrypt_for_multiple( /// - `domain` -- the encryption domain; this is typically a hard-coded string, and must be the same /// as the one used for encryption. /// -std::optional> decrypt_for_multiple( - const std::vector>& ciphertexts, - std::span nonce, - std::span privkey, - std::span pubkey, - std::span sender_pubkey, +std::optional> decrypt_for_multiple( + const std::vector>& ciphertexts, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain); /// API: crypto/encrypt_for_multiple_simple @@ -293,28 +289,28 @@ std::optional> decrypt_for_multiple( /// entries will be somewhat identifiable. /// /// Outputs: -/// std::vector containing bytes that contains the nonce and encoded encrypted -/// messages, suitable for decryption by the recipients with `decrypt_for_multiple_simple`. -std::vector encrypt_for_multiple_simple( - const std::vector>& messages, - const std::vector>& recipients, - std::span privkey, - std::span pubkey, +/// std::vector containing the nonce and encoded encrypted messages, suitable for +/// decryption by the recipients with `decrypt_for_multiple_simple`. +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span privkey, + std::span pubkey, std::string_view domain, - std::optional> nonce = std::nullopt, + std::optional> nonce = std::nullopt, int pad = 0); /// API: crypto/encrypt_for_multiple_simple /// /// This function is the same as the above, except that instead of taking the sender private and -/// public X25519 keys, it takes the single, 64-byte libsodium Ed25519 secret key (which is then -/// converted into the required X25519 keys). -std::vector encrypt_for_multiple_simple( - const std::vector>& messages, - const std::vector>& recipients, - std::span ed25519_secret_key, +/// public X25519 keys, it takes the Ed25519 private key (32-byte seed or 64-byte libsodium key, +/// which is then converted into the required X25519 keys). +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + const ed25519::PrivKeySpan& ed25519_secret_key, std::string_view domain, - std::span nonce = {}, + std::optional> nonce = std::nullopt, int pad = 0); /// API: crypto/encrypt_for_multiple_simple @@ -324,19 +320,14 @@ std::vector encrypt_for_multiple_simple( /// the first are identical. /// template -std::vector encrypt_for_multiple_simple( - std::span message, Args&&... args) { +std::vector encrypt_for_multiple_simple( + std::span message, Args&&... args) { return encrypt_for_multiple_simple( to_view_vector(&message, &message + 1), std::forward(args)...); } template -std::vector encrypt_for_multiple_simple(std::string_view message, Args&&... args) { - return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); -} -template -std::vector encrypt_for_multiple_simple( - std::span message, Args&&... args) { - return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); +std::vector encrypt_for_multiple_simple(std::string_view message, Args&&... args) { + return encrypt_for_multiple_simple(to_span(message), std::forward(args)...); } /// API: crypto/decrypt_for_multiple_simple @@ -357,36 +348,36 @@ std::vector encrypt_for_multiple_simple( /// `encrypt_for_multiple_simple`. /// /// Outputs: -/// If decryption succeeds, returns a std::vector containing the decrypted message, -/// in bytes. If parsing or decryption fails, returns std::nullopt. -std::optional> decrypt_for_multiple_simple( - std::span encoded, - std::span privkey, - std::span pubkey, - std::span sender_pubkey, +/// If decryption succeeds, returns a std::vector containing the decrypted message. +/// If parsing or decryption fails, returns std::nullopt. +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain); /// API: crypto/decrypt_for_multiple_simple /// /// This is the same as the above, except that instead of taking an X25519 private and public key -/// arguments, it takes a single, 64-byte Ed25519 secret key and converts it to X25519 to perform -/// the decryption. +/// arguments, it takes the Ed25519 private key (32-byte seed or 64-byte libsodium key) and +/// converts it to X25519 to perform the decryption. /// /// Note that `sender_pubkey` is still an X25519 pubkey for this version of the function. -std::optional> decrypt_for_multiple_simple( - std::span encoded, - std::span ed25519_secret_key, - std::span sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + const ed25519::PrivKeySpan& ed25519_secret_key, + std::span sender_pubkey, std::string_view domain); /// API: crypto/decrypt_for_multiple_simple_ed25519 /// /// This is the same as the above, except that it takes both the sender and recipient as Ed25519 /// keys, converting them on the fly to attempt the decryption. -std::optional> decrypt_for_multiple_simple_ed25519( - std::span encoded, - std::span ed25519_secret_key, - std::span sender_ed25519_pubkey, +std::optional> decrypt_for_multiple_simple_ed25519( + std::span encoded, + const ed25519::PrivKeySpan& ed25519_secret_key, + std::span sender_ed25519_pubkey, std::string_view domain); } // namespace session diff --git a/include/session/network/backends/session_file_server.hpp b/include/session/network/backends/session_file_server.hpp index d1db4a1a5..62433d4b5 100644 --- a/include/session/network/backends/session_file_server.hpp +++ b/include/session/network/backends/session_file_server.hpp @@ -155,7 +155,7 @@ file_metadata parse_upload_response(const std::string& body, size_t upload_size) /// Outputs: /// - returns a pair of the parsed `file_metadata` and the raw file data. /// - throws `invalid_url_exception` if the URL cannot be parsed. -std::pair> parse_download_response( +std::pair> parse_download_response( std::string_view download_url, const std::vector>& headers, const std::string& body); diff --git a/include/session/network/key_types.hpp b/include/session/network/key_types.hpp index d3d2b1ef9..98861e439 100644 --- a/include/session/network/key_types.hpp +++ b/include/session/network/key_types.hpp @@ -20,7 +20,7 @@ using namespace std::literals; namespace detail { template - inline constexpr std::array null_bytes = {0}; + inline constexpr std::array null_bytes = {}; void load_from_hex(void* buffer, size_t length, std::string_view hex); void load_from_bytes(void* buffer, size_t length, std::string_view bytes); @@ -28,7 +28,7 @@ namespace detail { } // namespace detail template -struct alignas(size_t) key_base : std::array { +struct alignas(size_t) key_base : std::array { std::string_view view() const { return {reinterpret_cast(this->data()), KeyLength}; } @@ -55,10 +55,7 @@ struct alignas(size_t) key_base : std::array { detail::load_from_bytes(d.data(), d.size(), bytes); return d; } - static Derived from_bytes(std::vector bytes) { - return from_bytes(to_string(bytes)); - } - static Derived from_bytes(std::span bytes) { + static Derived from_bytes(std::span bytes) { return from_bytes(to_string(bytes)); } }; @@ -98,7 +95,7 @@ using x25519_keypair = std::pair; legacy_pubkey parse_legacy_pubkey(std::string_view pubkey_in); ed25519_pubkey parse_ed25519_pubkey(std::string_view pubkey_in); x25519_pubkey parse_x25519_pubkey(std::string_view pubkey_in); -x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk); +x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk); } // namespace session::network diff --git a/include/session/network/network_opt.hpp b/include/session/network/network_opt.hpp index b751ab6dd..fb3ed8503 100644 --- a/include/session/network/network_opt.hpp +++ b/include/session/network/network_opt.hpp @@ -15,8 +15,8 @@ namespace opt { using namespace std::chrono_literals; namespace { - inline std::vector from_hex(std::string_view s) { - std::vector out; + inline std::vector from_hex(std::string_view s) { + std::vector out; out.reserve(s.size() / 2); oxenc::from_hex(s.begin(), s.end(), std::back_inserter(out)); diff --git a/include/session/network/routing/session_router_router.hpp b/include/session/network/routing/session_router_router.hpp index 24c90048d..9e8550f46 100644 --- a/include/session/network/routing/session_router_router.hpp +++ b/include/session/network/routing/session_router_router.hpp @@ -115,7 +115,7 @@ class SessionRouter : public IRouter, public std::enable_shared_from_this& remote_pubkey, + std::span remote_pubkey, const uint16_t remote_port, const std::string& initiating_req_id); void _send_via_tunnel( diff --git a/include/session/network/service_node.hpp b/include/session/network/service_node.hpp index 8adc1ef42..c9813b373 100644 --- a/include/session/network/service_node.hpp +++ b/include/session/network/service_node.hpp @@ -44,14 +44,14 @@ struct service_node { uint64_t requested_unlock_height; oxen::quic::RemoteAddress to_https_address() const { - return oxen::quic::RemoteAddress{remote_pubkey, ip, https_port}; + return oxen::quic::RemoteAddress{remote_pubkey.view(), ip, https_port}; } - oxen::quic::RemoteAddress to_omq_address() const { - return oxen::quic::RemoteAddress{remote_pubkey, ip, omq_port}; + oxen::quic::RemoteAddress to_quic_address() const { + return oxen::quic::RemoteAddress{remote_pubkey.view(), ip, omq_port}; } - std::span view_remote_key() const { return remote_pubkey; } + std::span view_remote_key() const { return remote_pubkey; } std::string host() const { return ip.to_string(); } session::network::x25519_pubkey swarm_pubkey() const; diff --git a/include/session/network/session_network_types.hpp b/include/session/network/session_network_types.hpp index 90e41e74e..a1112e1c3 100644 --- a/include/session/network/session_network_types.hpp +++ b/include/session/network/session_network_types.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -150,7 +151,7 @@ struct Request { std::string request_id; network_destination destination; std::string endpoint; - std::optional> body; + std::optional> body; RequestCategory category; /// Timeout for an in-flight request after it has been sent via the transport mechanism. @@ -179,7 +180,7 @@ struct Request { Request(std::string request_id, network_destination destination, std::string endpoint, - std::optional> body, + std::optional> body, RequestCategory category, std::chrono::milliseconds request_timeout, std::optional overall_timeout = std::nullopt, @@ -188,7 +189,7 @@ struct Request { Request(network_destination destination, std::string endpoint, - std::optional> body, + std::optional> body, RequestCategory category, std::chrono::milliseconds request_timeout, std::optional overall_timeout = std::nullopt, @@ -244,7 +245,7 @@ struct FileTransferRequest { }; struct UploadRequest : FileTransferRequest { - std::function()> next_data; + std::function()> next_data; std::optional file_name; std::optional ttl; }; diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 99890cbe6..71a4486d1 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include @@ -64,14 +66,14 @@ class Builder { }; void set_destination(network::network_destination destination); - void add_hop(std::span remote_key); + void add_hop(std::span remote_key); void add_hop(std::pair keys) { hops_.push_back(keys); } - std::vector build(std::vector payload); - std::vector generate_onion_blob( - const std::optional>& plaintext_body); + std::vector build(std::vector payload); + std::vector generate_onion_blob( + const std::optional>& plaintext_body); private: std::vector> hops_ = {}; @@ -91,8 +93,8 @@ class Builder { std::optional>> headers_ = std::nullopt; std::optional>> query_params_ = std::nullopt; - std::vector _generate_payload( - std::optional> body) const; + std::vector _generate_payload( + std::optional> body) const; }; } // namespace session::onionreq diff --git a/include/session/onionreq/hop_encryption.hpp b/include/session/onionreq/hop_encryption.hpp index 47bb1f28b..45b92a53d 100644 --- a/include/session/onionreq/hop_encryption.hpp +++ b/include/session/onionreq/hop_encryption.hpp @@ -25,20 +25,20 @@ class HopEncryption { // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. // `reply` should be false for a client-to-snode message, and true on a returning // snode-to-client message. - std::vector encrypt( + std::vector encrypt( EncryptType type, - std::vector plaintext, + std::vector plaintext, const network::x25519_pubkey& pubkey) const; - std::vector decrypt( + std::vector decrypt( EncryptType type, - std::vector ciphertext, + std::vector ciphertext, const network::x25519_pubkey& pubkey) const; // AES-GCM encryption. - std::vector encrypt_aesgcm( - std::vector plainText, const network::x25519_pubkey& pubKey) const; - std::vector decrypt_aesgcm( - std::vector cipherText, const network::x25519_pubkey& pubKey) const; + std::vector encrypt_aesgcm( + std::vector plainText, const network::x25519_pubkey& pubKey) const; + std::vector decrypt_aesgcm( + std::span cipherText, const network::x25519_pubkey& pubKey) const; // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of @@ -48,10 +48,10 @@ class HopEncryption { // When Bob (the server) encrypts a method for Alice (the client), he uses shared key // H(bA || A || B) (note that this is *different* that what would result if Bob was a client // sending to Alice the client). - std::vector encrypt_xchacha20( - std::vector plaintext, const network::x25519_pubkey& pubKey) const; - std::vector decrypt_xchacha20( - std::vector ciphertext, const network::x25519_pubkey& pubKey) const; + std::vector encrypt_xchacha20( + std::vector plaintext, const network::x25519_pubkey& pubKey) const; + std::vector decrypt_xchacha20( + std::span ciphertext, const network::x25519_pubkey& pubKey) const; private: const network::x25519_seckey private_key_; diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 91857904a..bffd8b94f 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -15,33 +15,33 @@ class OnionReqParser { HopEncryption enc; EncryptType enc_type = EncryptType::aes_gcm; network::x25519_pubkey remote_pk; - std::vector payload_; + std::vector payload_; public: /// Constructs a parser, parsing the given request sent to us. Throws if parsing or decryption /// fails. OnionReqParser( - std::span x25519_pubkey, - std::span x25519_privkey, - std::span req, + std::span x25519_pubkey, + std::span x25519_privkey, + std::span req, size_t max_size = DEFAULT_MAX_SIZE); /// plaintext payload, decrypted from the incoming request during construction. - std::span payload() const { return to_span(payload_); } + std::span payload() const { return payload_; } /// Extracts payload from this object (via a std::move); after the call the object's payload /// will be empty. - std::vector move_payload() { - std::vector ret{std::move(payload_)}; + std::vector move_payload() { + std::vector ret{std::move(payload_)}; payload_.clear(); // Guarantee empty, even if SSO active return ret; } - std::span remote_pubkey() const { return to_span(remote_pk.view()); } + std::span remote_pubkey() const { return remote_pk; } /// Encrypts a reply using the appropriate encryption as determined when parsing the /// request. - std::vector encrypt_reply(std::span reply) const; + std::vector encrypt_reply(std::span reply) const; }; } // namespace session::onionreq diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp index 6a1d7c0a4..6638dbcea 100644 --- a/include/session/onionreq/response_parser.hpp +++ b/include/session/onionreq/response_parser.hpp @@ -34,7 +34,7 @@ class ResponseParser { static bool response_long_enough(EncryptType enc_type, size_t response_size); - std::vector decrypt(std::vector ciphertext) const; + std::vector decrypt(std::vector ciphertext) const; DecryptedResponse decrypted_response(const std::string& encrypted_response); private: diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 629a0f1f7..af917509b 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -63,7 +64,11 @@ namespace session::pro_backend { /// TODO: Assign the Session Pro backend public key for verifying proofs to allow users of the /// library to have the pubkey available for verifying proofs. -constexpr auto PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000"_hex_u; +constexpr auto PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000"_hex_b; + +/// Domain used with ed25519::derive_subkey to derive the Session Pro signing keypair from the +/// account's root Ed25519 seed. +constexpr auto pro_subkey_domain = "SessionProRandom"_bytes; enum struct AddProPaymentResponseStatus { /// Payment was claimed and the pro proof was successfully generated @@ -100,8 +105,8 @@ struct ResponseHeader { }; struct MasterRotatingSignatures { - uc64 master_sig; - uc64 rotating_sig; + b64 master_sig; + b64 rotating_sig; }; struct AddProPaymentUserTransaction { @@ -131,20 +136,20 @@ struct AddProPaymentRequest { /// 32-byte Ed25519 Session Pro master public key derived from the Session account seed to /// register a Session Pro payment under. - uc32 master_pkey; + b32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key to authorise to use the generated Session /// Pro proof - uc32 rotating_pkey; + b32 rotating_pkey; /// Transaction containing the payment details to register on the Session Pro backend AddProPaymentUserTransaction payment_tx; /// 64-byte signature proving knowledge of the master key's secret component - uc64 master_sig; + b64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - uc64 rotating_sig; + b64 rotating_sig; /// API: pro/AddProPaymentRequest::to_json /// @@ -175,11 +180,11 @@ struct AddProPaymentRequest { /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. static MasterRotatingSignatures build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/AddProPaymentRequest::build_to_json /// @@ -201,11 +206,11 @@ struct AddProPaymentRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); }; /// The generated proof from the Session Pro backend that has been parsed from JSON. This structure @@ -238,19 +243,19 @@ struct GenerateProProofRequest { /// 32-byte Ed25519 Session Pro master public key to generate a Session Pro proof from. This key /// must have had a prior, and still active payment registered under it for a new proof to be /// generated successfully. - uc32 master_pkey; + b32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key authorized to use the generated proof - uc32 rotating_pkey; + b32 rotating_pkey; /// Unix timestamp of the request sys_ms unix_ts; /// 64-byte signature proving knowledge of the master key's secret component - uc64 master_sig; + b64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - uc64 rotating_sig; + b64 rotating_sig; /// API: pro/GenerateProProofRequest::build_sigs /// @@ -268,8 +273,8 @@ struct GenerateProProofRequest { /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. static MasterRotatingSignatures build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, sys_ms unix_ts); /// API: pro/GenerateProProofRequest::build_to_json @@ -287,8 +292,8 @@ struct GenerateProProofRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, sys_ms unix_ts); /// API: pro/GenerateProProofRequest::to_json @@ -323,7 +328,7 @@ struct GetProRevocationsRequest { struct ProRevocationItem { /// 32-byte hash of the generation index, identifying a proof - uc32 gen_index_hash; + b32 gen_index_hash; /// Unix timestamp when the proof expires sys_ms expiry_unix_ts; @@ -355,10 +360,10 @@ struct GetProDetailsRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - uc32 master_pkey; + b32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - uc64 master_sig; + b64 master_sig; /// Unix timestamp of the request sys_ms unix_ts; @@ -379,10 +384,10 @@ struct GetProDetailsRequest { /// - `count` -- Amount of historical payments to request /// /// Outputs: - /// - `uc64` - the 64-byte signature - static uc64 build_sig( + /// - `b64` - the 64-byte signature + static b64 build_sig( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, sys_ms unix_ts, uint32_t count); @@ -401,7 +406,7 @@ struct GetProDetailsRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, sys_ms unix_ts, uint32_t count); @@ -566,10 +571,10 @@ struct SetPaymentRefundRequestedRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - uc32 master_pkey; + b32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - uc64 master_sig; + b64 master_sig; /// Unix timestamp of the current time sys_ms unix_ts; @@ -602,15 +607,15 @@ struct SetPaymentRefundRequestedRequest { /// `AddProPaymentUserTransaction` /// /// Outputs: - /// - `uc64` - the 64-byte signature - static uc64 build_sig( + /// - `b64` - the 64-byte signature + static b64 build_sig( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, sys_ms unix_ts, sys_ms refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/SetPaymentRefundRequested::build_to_json /// @@ -635,12 +640,12 @@ struct SetPaymentRefundRequestedRequest { /// - `std::string` -- Request serialised to JSON static std::string build_to_json( std::uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, sys_ms unix_ts, sys_ms refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::span payment_tx_payment_id, + std::span payment_tx_order_id); /// API: pro/SetPaymentRefundRequested::to_json /// diff --git a/include/session/random.hpp b/include/session/random.hpp index 8f1b3b2ac..1acfac821 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -45,9 +45,14 @@ namespace session::random { /// /// Outputs: None. void fill(std::span buf); -void fill(std::span buf); void fill(std::span buf); +/// API: random/random_fill_deterministic +/// +/// Wrapper around randombytes_buf_deterministic: fills `buf` with deterministic pseudorandom +/// bytes derived from the given 32-byte seed. +void fill_deterministic(std::span buf, std::span seed); + /// API: random/random /// /// Wrapper around the randombytes_buf function. @@ -57,7 +62,7 @@ void fill(std::span buf); /// /// Outputs: /// - random bytes of the specified length. -std::vector random(size_t size); +std::vector random(size_t size); /// API: random/random_base32 /// diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h index f19e80993..673f26d3c 100644 --- a/include/session/session_encrypt.h +++ b/include/session/session_encrypt.h @@ -240,7 +240,7 @@ typedef struct session_decrypt_group_message_result { size_t index; // Index of the key that successfully decrypted the message char session_id[66]; // In hex span_u8 plaintext; // Decrypted message on success. Must be freed by calling the CRT's `free` - char error_len_incl_null_terminator; + size_t error_len_incl_null_terminator; } session_decrypt_group_message_result; /// API: crypto/session_decrypt_group_message diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 0fd37becf..fde18fa2a 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -9,7 +9,7 @@ #include #include -#include "ed25519.hpp" +#include "crypto/ed25519.hpp" // Helper functions for the "Session Protocol" encryption mechanism. This is the encryption used // for DMs sent from one Session user to another. @@ -66,10 +66,10 @@ namespace session { /// Outputs: /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) -std::vector encrypt_for_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message); +std::vector encrypt_for_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/encrypt_for_recipient_deterministic /// @@ -87,10 +87,10 @@ std::vector encrypt_for_recipient( /// /// Outputs: /// Identical to `encrypt_for_recipient`. -std::vector encrypt_for_recipient_deterministic( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message); +std::vector encrypt_for_recipient_deterministic( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/session_encrypt_for_blinded_recipient /// @@ -107,11 +107,11 @@ std::vector encrypt_for_recipient_deterministic( /// Outputs: /// - The encrypted ciphertext to send. /// - Throw if encryption fails or (which typically means invalid keys provided) -std::vector encrypt_for_blinded_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span recipient_blinded_id, - std::span message); +std::vector encrypt_for_blinded_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span server_pk, + std::span recipient_blinded_id, + std::span message); /// API: crypto/encrypt_for_recipient_v2 /// @@ -149,13 +149,13 @@ std::vector encrypt_for_blinded_recipient( /// Outputs: /// - The encrypted v2 ciphertext to send to the swarm. /// - Throws on invalid keys or encryption failure. -std::vector encrypt_for_recipient_v2( - const Ed25519PrivKeySpan& sender_ed25519_privkey, - std::span recipient_session_id, - std::span recipient_account_x25519, - std::span recipient_account_mlkem768, - std::span content, - const OptionalEd25519PrivKeySpan& pro_ed25519_privkey = std::nullopt); +std::vector encrypt_for_recipient_v2( + const ed25519::PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span recipient_account_x25519, + std::span recipient_account_mlkem768, + std::span content, + const ed25519::OptionalPrivKeySpan& pro_ed25519_privkey = std::nullopt); /// Exception thrown when a v2 message could not be decrypted with a given account key. The /// caller should catch this and try the next candidate key. Other exceptions (e.g., @@ -167,9 +167,9 @@ struct DecryptV2Error : std::runtime_error { /// Result of decrypt_incoming_v2. struct DecryptV2Result { - std::vector content; ///< Decrypted message content. - std::array sender_session_id; ///< 05-prefixed Session ID of the sender. - std::optional> pro_signature; ///< Pro sig, if present. + std::vector content; ///< Decrypted message content. + b33 sender_session_id; ///< 05-prefixed Session ID of the sender. + std::optional pro_signature; ///< Pro sig, if present. }; /// API: crypto/decrypt_incoming_v2_prefix @@ -190,10 +190,10 @@ struct DecryptV2Result { /// Outputs: /// - The recovered 2-byte ML-KEM-768 public key prefix. /// - Throws `std::runtime_error` if the ciphertext is too short or has the wrong prefix bytes. -std::array decrypt_incoming_v2_prefix( - std::span x25519_sec, - std::span x25519_pub, - std::span ciphertext); +std::array decrypt_incoming_v2_prefix( + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext); /// API: crypto/decrypt_incoming_v2 /// @@ -229,11 +229,11 @@ std::array decrypt_incoming_v2_prefix( /// - Throws `DecryptV2Error` if the key did not decrypt the message (try the next candidate). /// - Throws `std::runtime_error` for unrecoverable errors (invalid format, signature failure). DecryptV2Result decrypt_incoming_v2( - std::span recipient_session_id, - std::span account_pfs_x25519_sec, - std::span account_pfs_x25519_pub, - std::span account_pfs_mlkem768_sec, - std::span ciphertext); + std::span recipient_session_id, + std::span account_pfs_x25519_sec, + std::span account_pfs_x25519_pub, + std::span account_pfs_mlkem768_sec, + std::span ciphertext); /// API: crypto/encrypt_for_recipient_v2_nopfs /// @@ -259,11 +259,11 @@ DecryptV2Result decrypt_incoming_v2( /// Outputs: /// - Wire-format v2 ciphertext (non-PFS). /// - Throws on key or encryption failure. -std::vector encrypt_for_recipient_v2_nopfs( - const Ed25519PrivKeySpan& sender_ed25519_privkey, - std::span recipient_session_id, - std::span content, - const OptionalEd25519PrivKeySpan& pro_ed25519_privkey = std::nullopt); +std::vector encrypt_for_recipient_v2_nopfs( + const ed25519::PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span content, + const ed25519::OptionalPrivKeySpan& pro_ed25519_privkey = std::nullopt); /// API: crypto/decrypt_incoming_v2_nopfs /// @@ -285,10 +285,10 @@ std::vector encrypt_for_recipient_v2_nopfs( /// - Throws `DecryptV2Error` if AEAD authentication fails (wrong key — try PFS path instead). /// - Throws `std::runtime_error` for unrecoverable errors (invalid format, signature failure). DecryptV2Result decrypt_incoming_v2_nopfs( - std::span recipient_session_id, - std::span x25519_sec, - std::span x25519_pub, - std::span ciphertext); + std::span recipient_session_id, + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext); static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; @@ -357,11 +357,11 @@ static constexpr size_t GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE = 1'000'000; /// /// Outputs: /// - `ciphertext` -- the encrypted, etc. value to send to the swarm -std::vector encrypt_for_group( - const Ed25519PrivKeySpan& user_ed25519_privkey, - std::span group_ed25519_pubkey, - std::span group_enc_key, - std::span plaintext, +std::vector encrypt_for_group( + const ed25519::PrivKeySpan& user_ed25519_privkey, + std::span group_ed25519_pubkey, + std::span group_enc_key, + std::span plaintext, bool compress, size_t padding); @@ -389,10 +389,10 @@ std::vector encrypt_for_group( /// - `recipient_pubkey` -- the recipient X25519 pubkey, which may or may not be prefixed with the /// 0x05 session id prefix (33 bytes if prefixed, 32 if not prefixed). /// - `message` -- the message to embed and sign. -std::vector sign_for_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message); +std::vector sign_for_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message); /// API: crypto/decrypt_incoming /// @@ -405,12 +405,11 @@ std::vector sign_for_recipient( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair, std::vector>` -- the plaintext binary -/// data that was encrypted and the -/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on -/// error. -std::pair, std::vector> decrypt_incoming( - const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext); +/// - `std::pair, b32>` -- the plaintext binary data that was encrypted +/// and the sender's Ed25519 pubkey, *if* the message decrypted and validated successfully. +/// Throws on error. +std::pair, b32> decrypt_incoming( + const ed25519::PrivKeySpan& ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -426,14 +425,13 @@ std::pair, std::vector> decrypt_incomi /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair, std::vector>` -- the plaintext binary -/// data that was encrypted and the -/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on -/// error. -std::pair, std::vector> decrypt_incoming( - std::span x25519_pubkey, - std::span x25519_seckey, - std::span ciphertext); +/// - `std::pair, b32>` -- the plaintext binary data that was encrypted +/// and the sender's Ed25519 pubkey, *if* the message decrypted and validated successfully. +/// Throws on error. +std::pair, b32> decrypt_incoming( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -449,8 +447,8 @@ std::pair, std::vector> decrypt_incomi /// - `std::pair, std::string>` -- the plaintext binary data that was /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair, std::string> decrypt_incoming_session_id( - const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext); +std::pair, std::string> decrypt_incoming_session_id( + const ed25519::PrivKeySpan& ed25519_privkey, std::span ciphertext); /// API: crypto/decrypt_incoming /// @@ -465,13 +463,13 @@ std::pair, std::string> decrypt_incoming_session_id( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - `std::pair, std::string>` -- the plaintext binary data that was +/// - `std::pair, std::string>` -- the plaintext binary data that was /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair, std::string> decrypt_incoming_session_id( - std::span x25519_pubkey, - std::span x25519_seckey, - std::span ciphertext); +std::pair, std::string> decrypt_incoming_session_id( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext); /// API: crypto/decrypt_from_blinded_recipient /// @@ -494,17 +492,17 @@ std::pair, std::string> decrypt_incoming_session_id( /// - `std::pair, std::string>` -- the plaintext binary data that was /// encrypted and the /// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. -std::pair, std::string> decrypt_from_blinded_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span sender_id, - std::span recipient_id, - std::span ciphertext); +std::pair, std::string> decrypt_from_blinded_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span server_pk, + std::span sender_id, + std::span recipient_id, + std::span ciphertext); struct DecryptGroupMessage { size_t index; // Index of the key that successfully decrypted the message std::string session_id; // In hex - std::vector plaintext; + std::vector plaintext; }; /// API: crypto/decrypt_group_message @@ -534,9 +532,9 @@ struct DecryptGroupMessage { /// (and possibly log) but otherwise ignore such exceptions and just not process the message if /// it throws. DecryptGroupMessage decrypt_group_message( - std::span> decrypt_ed25519_privkey_list, - std::span group_ed25519_pubkey, - std::span ciphertext); + std::span> decrypt_ed25519_privkey_list, + std::span group_ed25519_pubkey, + std::span ciphertext); /// API: crypto/decrypt_ons_response /// @@ -552,8 +550,8 @@ DecryptGroupMessage decrypt_group_message( /// a session ID. Throws on error/failure. std::string decrypt_ons_response( std::string_view lowercase_name, - std::span ciphertext, - std::optional> nonce); + std::span ciphertext, + std::optional> nonce); /// API: crypto/decrypt_push_notification /// @@ -568,8 +566,8 @@ std::string decrypt_ons_response( /// - `std::vector` -- the decrypted push notification payload, *if* the decryption /// was /// successful. Throws on error/failure. -std::vector decrypt_push_notification( - std::span payload, std::span enc_key); +std::vector decrypt_push_notification( + std::span payload, std::span enc_key); /// API: crypto/encrypt_xchacha20 /// @@ -581,8 +579,8 @@ std::vector decrypt_push_notification( /// /// Outputs: /// - `std::vector` -- the resulting ciphertext. -std::vector encrypt_xchacha20( - std::span plaintext, std::span key); +std::vector encrypt_xchacha20( + std::span plaintext, std::span key); /// API: crypto/decrypt_xchacha20 /// @@ -593,8 +591,8 @@ std::vector encrypt_xchacha20( /// - `key` -- the 32-byte symmetric key. /// /// Outputs: -/// - `std::vector` -- the resulting plaintext. -std::vector decrypt_xchacha20( - std::span ciphertext, std::span key); +/// - `std::vector` -- the resulting plaintext. +std::vector decrypt_xchacha20( + std::span ciphertext, std::span key); } // namespace session diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index c451f71b2..dc26b81b7 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include @@ -79,8 +79,8 @@ enum class ProStatus { }; struct ProSignedMessage { - std::span sig; - std::span msg; + std::span sig; + std::span msg; }; class ProProof { @@ -89,12 +89,12 @@ class ProProof { std::uint8_t version; /// Hash of the generation index set by the Session Pro Backend - uc32 gen_index_hash; + b32 gen_index_hash; /// The public key that the Session client registers their Session Pro entitlement under. /// Session clients must sign messages with this key along side the sending of this proof for /// the network to authenticate their usage of the proof - uc32 rotating_pubkey; + b32 rotating_pubkey; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid to sys_ms expiry_unix_ts; @@ -102,7 +102,7 @@ class ProProof { /// Signature over the contents of the proof. It is signed by the Session Pro Backend key which /// is the entity responsible for issueing tamper-proof Sesison Pro certificates for Session /// clients. - uc64 sig; + b64 sig; /// API: pro/Proof::verify_signature /// @@ -118,7 +118,7 @@ class ProProof { /// /// Outputs: /// - `bool` - True if the given key was the signatory of the proof, false otherwise - bool verify_signature(const std::span& verify_pubkey) const; + bool verify_signature(std::span verify_pubkey) const; /// API: pro/Proof::verify_message /// @@ -134,7 +134,7 @@ class ProProof { /// Outputs: /// - `bool` - True if the message was signed by the embedded `rotating_pubkey` false otherwise. bool verify_message( - std::span sig, const std::span msg) const; + std::span sig, std::span msg) const; /// API: pro/Proof::is_active /// @@ -172,14 +172,14 @@ class ProProof { /// not set then this function can never return `ProStatus::InvalidUserSig` from the set of /// possible enum values. Otherwise this funtion can return all possible values. ProStatus status( - std::span verify_pubkey, + std::span verify_pubkey, sys_ms unix_ts, const std::optional& signed_msg); /// API: pro/Proof::hash /// /// Create a 32-byte hash from the proof. This hash is the payload that is signed in the proof. - uc32 hash() const; + b32 hash() const; bool operator==(const ProProof& other) const { return version == other.version && gen_index_hash == other.gen_index_hash && @@ -226,12 +226,12 @@ struct Envelope { // Optional fields. These fields are set if the appropriate flag has been set in `flags` // otherwise the corresponding values are to be ignored and those fields will be // zero-initialised. - uc33 source; + b33 source; uint32_t source_device; uint64_t server_timestamp; // Signature by the sending client's rotating key - uc64 pro_sig; + b64 pro_sig; }; struct DecodedPro { @@ -248,16 +248,16 @@ struct DecodedEnvelope { Envelope envelope; // Decoded envelope content into plaintext with padding stripped - std::vector content_plaintext; + std::vector content_plaintext; // Sender public key extracted from the encrypted content payload. This is not set if the // envelope was a groups v2 envelope where the envelope was encrypted and only the x25519 pubkey // was available. - uc32 sender_ed25519_pubkey; + b32 sender_ed25519_pubkey; // The x25519 pubkey, always populated on successful parse. Either it's present from decrypting // a Groups v2 envelope or it's re-derived from the Ed25519 pubkey. - uc32 sender_x25519_pubkey; + b32 sender_x25519_pubkey; // Set if the envelope included a pro payload. The caller must check the status to determine if // the embedded pro data/proof was valid, invalid or whether or not the proof has expired. @@ -272,46 +272,20 @@ struct DecodedCommunityMessage { std::optional envelope; // The protobuf encoded `Content` with padding stripped - std::vector content_plaintext; + std::vector content_plaintext; // The signature if it was present in the payload. If the envelope is set and the envelope has // the pro signature flag set, then this signature was extracted from the envelope. When the // signature is sourced from the envelope, the envelope's `pro_sig` field is also set to the // same signature as this instance for consistency. Otherwise the signature, if set was // extracted from the community-exclusive pro signature field in the content message. - std::optional pro_sig; + std::optional pro_sig; // Set if the envelope included a pro payload. The caller must check the status to determine if // the embedded pro data/proof was valid, invalid or whether or not the proof has expired. std::optional pro; }; -struct DecodeEnvelopeKey { - // Set the key to decrypt the envelope. If this key is set then it's assumed that the envelope - // payload is encrypted (e.g. groups v2) and that the contents are unencrypted. If this key is - // not set the it's assumed the envelope is not encrypted but the contents are encrypted (e.g.: - // 1o1 or legacy group). - std::optional> - group_ed25519_pubkey; - - // List of libsodium-style secret key to decrypt the envelope from. Can also be passed as a 32 - // byte secret key. The public key component is not used. - // - // If the `group_ed25519_pubkey` is set then a list of keys is accepted to attempt to decrypt - // the envelope. For envelopes generated by a group message, we assume that the envelope is - // encrypted and must be decrypted by the group keys associated with it (of which there may be - // many candidate keys depending on how many times the group has been rekeyed). It's recommended - // to pass `Keys::group_keys()` or in the C API use the `groups_keys_size` and - // `group_keys_get_keys` combo to retrieve the keys to attempt to use to decrypt this message. - // - // If `group_ed25519_pubkey` is _not_ set then this function assumes the envelope is unencrypted - // but the content is encrypted (e.g.: 1o1 and legacy group messages). The function will attempt - // to decrypt the envelope's contents with the given keys. Typically in these cases you will - // pass exactly 1 ed25519 private key for decryption but this function makes no pre - // existing assumptions on the number of keys and will attempt all given keys specified - // regardless until it finds one that successfully decrypts the envelope contents. - std::span> decrypt_keys; -}; /// API: session_protocol/pro_features_for_utf8 /// @@ -355,7 +329,7 @@ ProFeaturesForMsg pro_features_for_utf16(std::u16string_view msg); /// /// Pad a message to the required alignment for 1o1/community messages (160 bytes) including space /// for the padding-terminating byte. -std::vector pad_message(std::span payload); +std::vector pad_message(std::span payload); /// API: session_protocol/encode_dm_v1 /// @@ -379,12 +353,12 @@ std::vector pad_message(std::span payload); /// /// Outputs: /// - Encrypted, encoded payload, with all required protobuf encoding and wrapping. -std::vector encode_dm_v1( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_dm_v1( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, sys_ms sent_timestamp, - std::span recipient_pubkey, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey = std::nullopt); + std::span recipient_pubkey, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey = std::nullopt); /// API: session_protocol/encode_for_community_inbox /// @@ -411,13 +385,13 @@ std::vector encode_dm_v1( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_community_inbox( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_for_community_inbox( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - std::span recipient_pubkey, - std::span community_pubkey, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); + std::span recipient_pubkey, + std::span community_pubkey, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/encode_community_message /// @@ -441,9 +415,9 @@ std::vector encode_for_community_inbox( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_community( - std::span plaintext, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); +std::vector encode_for_community( + std::span plaintext, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/encode_for_group /// @@ -471,13 +445,13 @@ std::vector encode_for_community( /// Outputs: /// - Encryption result for the plaintext. The retured payload is suitable for sending on the wire /// (i.e: it has been protobuf encoded/wrapped if necessary). -std::vector encode_for_group( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_for_group( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - std::span group_ed25519_pubkey, - std::span group_enc_key, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey); + std::span group_ed25519_pubkey, + std::span group_enc_key, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/decode_envelope /// @@ -535,10 +509,28 @@ std::vector encode_for_group( /// /// If the `status` is set to valid the the caller can proceed with entitling the envelope with /// access to pro features if it's using any. -DecodedEnvelope decode_envelope( - const DecodeEnvelopeKey& keys, - std::span envelope_payload, - std::span pro_backend_pubkey); +/// Decodes a 1-on-1 or legacy group envelope. The envelope payload is a WebSocket-wrapped +/// protobuf whose inner content is encrypted with the Session protocol (Ed25519 DH). +/// +/// Throws on parse or decryption failure. +DecodedEnvelope decode_dm_envelope( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span envelope_payload, + std::span pro_backend_pubkey); + +/// Decodes a groups v2 envelope. The envelope payload is encrypted with a group symmetric key +/// and decrypted via `decrypt_group_message`. The inner content is plaintext. +/// +/// `group_keys` is a list of recent symmetric group encryption keys to try; multiple keys are +/// needed because the key rotates periodically, and retrieved messages may still be encrypted +/// with a pre-rotation key if they were sent before the rotation occurred. +/// +/// Throws on parse or decryption failure. +DecodedEnvelope decode_group_envelope( + std::span> group_keys, + std::span group_ed25519_pubkey, + std::span envelope_payload, + std::span pro_backend_pubkey); /// API: session_protocol/decode_for_community /// @@ -570,8 +562,8 @@ DecodedEnvelope decode_envelope( /// If the `status` is set to valid the the caller can proceed with entitling the envelope with /// access to pro features if it's using any. DecodedCommunityMessage decode_for_community( - std::span content_or_envelope_payload, + std::span content_or_envelope_payload, sys_ms unix_ts, - std::span pro_backend_pubkey); + std::span pro_backend_pubkey); } // namespace session diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp index c14ed7f5b..0a071287c 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -204,6 +204,9 @@ struct sodium_array { bool empty() const { return len == 0; } explicit operator bool() const { return !empty(); } + operator std::span() { return {buf, len}; } + operator std::span() const { return {buf, len}; } + T* begin() { return buf; } const T* begin() const { return buf; } T* end() { return buf + len; } @@ -243,4 +246,28 @@ struct sodium_allocator { template using sodium_vector = std::vector>; +// Like std::allocator but zeros memory before freeing. Lighter weight than sodium_allocator +// (uses regular heap allocation) but still ensures sensitive data is wiped on deallocation. +template +struct clearing_allocator { + using value_type = T; + + [[nodiscard]] static T* allocate(std::size_t n) { + return std::allocator{}.allocate(n); + } + + static void deallocate(T* p, std::size_t n) { + sodium_zero_buffer(p, n * sizeof(T)); + std::allocator{}.deallocate(p, n); + } + + template + bool operator==(const clearing_allocator&) const noexcept { return true; } +}; + +/// Vector that zeros its buffer on deallocation (including when resizing). Lighter weight +/// than sodium_vector but still suitable for short-lived sensitive data. +template +using cleared_vector = std::vector>; + } // namespace session diff --git a/include/session/util.hpp b/include/session/util.hpp index 27f681a81..f2be5def8 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -29,26 +29,26 @@ namespace session { using namespace oxenc; // Helper functions to convert to/from spans -template +template inline std::span as_span(std::span sp) { return std::span{reinterpret_cast(sp.data()), sp.size()}; } -template +template inline std::span as_span(std::span sp) { return std::span{reinterpret_cast(sp.data()), sp.size()}; } -template +template inline std::span to_span(const T& c) { return {reinterpret_cast(c.data()), c.size()}; } -template +template inline std::span to_span(const char (&literal)[N]) { return {reinterpret_cast(literal), N - 1}; } -template +template requires( std::convertible_to< const Container&, @@ -67,32 +67,32 @@ inline OutContainer convert(const InContainer& in) { return OutContainer(begin, begin + in.size()); } -template +template inline std::vector to_vector(std::span sp) { return convert>(sp); } -template +template inline std::vector to_vector(const T& c) { return convert>(to_span(c)); } -template +template inline std::vector to_vector(const std::array& arr) { return convert>(arr); } -template +template requires(!oxenc::bt_input_string) inline std::vector to_vector(const Container& c) { return convert>(to_span(c)); } template -inline std::array to_array(std::span sp) { - std::array result{}; +inline std::array to_array(std::span sp) { + std::array result{}; std::copy_n( - reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()), std::min(N, sp.size()), result.begin()); return result; @@ -108,7 +108,52 @@ inline std::string_view to_string_view(const Container& c) { return {reinterpret_cast(c.data()), c.size()}; } -// Helper function to go to/from char pointers to unsigned char pointers: +/// Returns a fixed-extent std::byte span viewing N bytes starting at p. Primarily useful for +/// wrapping C API pointers (e.g. `unsigned char*`) into typed spans without a reinterpret_cast at +/// the call site. A dynamic-size overload taking a length is also provided. A third overload +/// accepts a C array directly and deduces N. +template +inline std::span to_byte_span(const Char* p) { + return std::span(reinterpret_cast(p), N); +} +template +inline std::span to_byte_span(Char* p) { + return std::span(reinterpret_cast(p), N); +} +template +inline std::span to_byte_span(const std::byte* p) { + return std::span(p, N); +} +template +inline std::span to_byte_span(std::byte* p) { + return std::span(p, N); +} +template +inline std::span to_byte_span(const Char* p, size_t n) { + return {reinterpret_cast(p), n}; +} +template +inline std::span to_byte_span(Char* p, size_t n) { + return {reinterpret_cast(p), n}; +} +inline std::span to_byte_span(const std::byte* p, size_t n) { + return {p, n}; +} +inline std::span to_byte_span(std::byte* p, size_t n) { + return {p, n}; +} +// Array overloads: deduce N from a C array, returning a fixed-extent span. +template +inline std::span to_byte_span(const Char (&arr)[N]) { + return std::span(reinterpret_cast(arr), N); +} +template +inline std::span to_byte_span(Char (&arr)[N]) { + return std::span(reinterpret_cast(arr), N); +} + +// Helper functions to reinterpret char-type pointers as unsigned char* or std::byte*. +// These are primarily for passing binary data to/from C APIs. template inline const unsigned char* to_unsigned(const Char* x) { return reinterpret_cast(x); @@ -131,6 +176,22 @@ inline unsigned char* to_unsigned(unsigned char* x) { return x; } +template +inline const std::byte* to_bytes(const Char* x) { + return reinterpret_cast(x); +} +template +inline std::byte* to_bytes(Char* x) { + return reinterpret_cast(x); +} +// These do nothing, but having them makes template metaprogramming easier: +inline const std::byte* to_bytes(const std::byte* x) { + return x; +} +inline std::byte* to_bytes(std::byte* x) { + return x; +} + /// Returns the data pointer of a std::byte span as an `unsigned char*`, for passing to C APIs /// that expect `unsigned char*`. The const overload returns `const unsigned char*`. template @@ -158,11 +219,11 @@ using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; -/// Takes a container of string-like binary values and returns a vector of unsigned char spans -/// viewing those values. This can be used on a container of any type with a `.data()` and a -/// `.size()` where `.data()` is a one-byte value pointer; std::string, std::string_view, -/// std::vector, std::span, etc. apply, as does std::array -/// of 1-byte char types. +/// Takes a container of string-like binary values and returns a vector of std::byte spans viewing +/// those values. This can be used on a container of any type with a `.data()` and a `.size()` +/// where `.data()` is a one-byte value pointer; std::string, std::string_view, +/// std::vector, std::span, etc. apply, as does std::array of 1-byte +/// char types. /// /// This is useful in various libsession functions that require such a vector. Note that the /// returned vector's views are valid only as the original container remains alive; this is @@ -173,25 +234,26 @@ using uc64 = std::array; /// There are two versions of this: the first takes a generic iterator pair; the second takes a /// single container. template -std::vector> to_view_vector(It begin, It end) { - std::vector> vec; +std::vector> to_view_vector(It begin, It end) { + std::vector> vec; vec.reserve(std::distance(begin, end)); for (; begin != end; ++begin) { if constexpr (std::is_same_v, char*>) // C strings - vec.emplace_back(*begin); + vec.emplace_back(reinterpret_cast(*begin), + std::strlen(*begin)); else { static_assert( sizeof(*begin->data()) == 1, "to_view_vector can only be used with containers of string-like types of " "1-byte characters"); - vec.emplace_back(reinterpret_cast(begin->data()), begin->size()); + vec.emplace_back(reinterpret_cast(begin->data()), begin->size()); } } return vec; } template -std::vector> to_view_vector(const Container& c) { +std::vector> to_view_vector(const Container& c) { return to_view_vector(c.begin(), c.end()); } @@ -318,15 +380,16 @@ static_assert(std::is_same_v< /// ZSTD-compresses a value. `prefix` can be prepended on the returned value, if needed. Throws on /// serious error. -std::vector zstd_compress( - std::span data, +std::vector zstd_compress( + std::span data, int level = 1, - std::span prefix = {}); + std::span prefix = {}); /// ZSTD-decompresses a value. Returns nullopt if decompression fails. If max_size is non-zero /// then this returns nullopt if the decompressed size would exceed that limit. -std::optional> zstd_decompress( - std::span data, size_t max_size = 0); +std::optional> zstd_decompress( + std::span data, size_t max_size = 0); + /// Wrapper for formatting byte sizes with SI prefixes via fmt/oxen-logging. Provides a /// `format_as` friend function discoverable via ADL, so no fmt headers are needed here. /// Usage: `log::info(cat, "Size: {}", human_size{12345});` => "Size: 12.3 kB" @@ -335,6 +398,32 @@ struct human_size { friend std::string format_as(human_size s); }; +/// NTTP helper struct for the `_bytes` user-defined literal. +template +struct BytesLiteral { + std::byte data[N - 1]; + static constexpr size_t size = N - 1; + consteval BytesLiteral(const char (&s)[N]) { + for (size_t i = 0; i < N - 1; ++i) + data[i] = static_cast(static_cast(s[i])); + } +}; + +inline namespace literals { + + /// User-defined literal that returns a compile-time `std::span` view over the + /// string literal's bytes (excluding the null terminator). Example: + /// + /// using namespace session::literals; // or `using namespace session;` + /// constexpr auto domain = "MyDomainKey"_bytes; + /// + template + consteval std::span operator""_bytes() { + return std::span(Lit.data, Lit.size); + } + +} // namespace literals + } // namespace session #ifndef _WIN32 diff --git a/include/session/xed25519.hpp b/include/session/xed25519.hpp index e0e314c2f..7ffbbc3d3 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -5,33 +5,21 @@ #include #include #include -#include + +#include "util.hpp" namespace session::xed25519 { /// XEd25519-signs a message given the curve25519 privkey and message. -std::array sign( - std::span curve25519_privkey /* 32 bytes */, - std::span msg); - -/// std::byte overload; returns a std::byte array. -std::array sign( - std::span curve25519_privkey /* 32 bytes */, - std::span msg); +b64 sign(std::span curve25519_privkey, std::span msg); /// "Softer" version that takes and returns strings of regular chars std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg); /// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey [[nodiscard]] bool verify( - std::span signature /* 64 bytes */, - std::span curve25519_pubkey /* 32 bytes */, - std::span msg); - -/// std::byte overload -[[nodiscard]] bool verify( - std::span signature /* 64 bytes */, - std::span curve25519_pubkey /* 32 bytes */, + std::span signature, + std::span curve25519_pubkey, std::span msg); /// "Softer" version that takes strings of regular chars @@ -44,10 +32,7 @@ std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string /// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519 /// pubkey: this always returns the positive value. You can get the other possibility (the /// negative) by setting the sign bit, i.e. `returned_pubkey[31] |= 0x80`. -std::array pubkey(std::span curve25519_pubkey) noexcept; - -/// std::byte overload; returns a std::byte array. -std::array pubkey(std::span curve25519_pubkey) noexcept; +b32 pubkey(std::span curve25519_pubkey) noexcept; /// "Softer" version that takes/returns strings of regular chars. Throws invalid_argument if the /// input is not 32 bytes. @@ -56,11 +41,11 @@ std::string pubkey(std::string_view curve25519_pubkey); /// Utility function that provides a constant-time `if (b) f = g;` implementation for byte arrays. template void constant_time_conditional_assign( - std::array& f, const std::array& g, bool b) { - std::array x; + std::array& f, const std::array& g, bool b) { + std::array x; for (size_t i = 0; i < x.size(); i++) x[i] = f[i] ^ g[i]; - unsigned char mask = (unsigned char)(-(signed char)b); + auto mask = static_cast(-(signed char)b); for (size_t i = 0; i < x.size(); i++) x[i] &= mask; for (size_t i = 0; i < x.size(); i++) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0b6f3012f..58e382aa1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,8 +54,10 @@ add_libsession_util_library(util add_libsession_util_library(crypto attachments.cpp blinding.cpp + crypto/ed25519.cpp + crypto/mlkem768.cpp + crypto/x25519.cpp curve25519.cpp - ed25519.cpp hash.cpp multi_encrypt.cpp random.cpp diff --git a/src/attachments.cpp b/src/attachments.cpp index 55df70488..65b6efe29 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -103,15 +103,15 @@ class decryption_failure : public std::runtime_error { // the randomness. static crypto_secretstream_xchacha20poly1305_state secretstream_xchacha20poly1305_init_push_with_nonce( - std::span header, - std::span key, - std::span nonce) { + std::span header, + std::span key, + std::span nonce) { crypto_secretstream_xchacha20poly1305_state st; std::memcpy(header.data(), nonce.data(), ENCRYPT_HEADER); crypto_core_hchacha20( - st.k, header.data(), reinterpret_cast(key.data()), nullptr); + st.k, to_unsigned(header.data()), to_unsigned(key.data()), nullptr); static_assert(sizeof(st) == 52); std::memset(st.nonce, 0, 4 /*crypto_secretstream_xchacha20poly1305_COUNTERBYTES*/); st.nonce[0] = 1; @@ -267,12 +267,11 @@ size_t decrypt( throw std::logic_error{ "Attachment decryption failed: output buffer too small to decrypt contents"}; - std::span uenc{ - reinterpret_cast(encrypted.data()), encrypted.size()}; + auto uenc = encrypted; crypto_secretstream_xchacha20poly1305_state st; crypto_secretstream_xchacha20poly1305_init_pull( - &st, uenc.data() + 1, reinterpret_cast(key.data())); + &st, to_unsigned(uenc.data() + 1), to_unsigned(key.data())); auto* inpos = uenc.data() + 1 + ENCRYPT_HEADER; auto* const inend = uenc.data() + uenc.size(); @@ -302,7 +301,7 @@ size_t decrypt( reinterpret_cast(padbuf.data()), nullptr, &tag, - inpos, + to_unsigned(inpos), chunk_size + ENCRYPT_CHUNK_OVERHEAD, nullptr, 0) != 0) @@ -361,7 +360,7 @@ size_t decrypt( reinterpret_cast(decrypted), nullptr, &tag, - inpos, + to_unsigned(inpos), chunk_size + ENCRYPT_CHUNK_OVERHEAD, nullptr, 0) != 0) diff --git a/src/blinding.cpp b/src/blinding.cpp index 762f347a4..b9384ed43 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -1,15 +1,15 @@ #include "session/blinding.hpp" #include "session/blinding.h" +#include #include -#include -#include +#include #include #include #include -#include "session/ed25519.hpp" +#include "session/crypto/ed25519.hpp" #include "session/export.h" #include "session/hash.hpp" #include "session/platform.h" @@ -19,82 +19,111 @@ namespace session { using namespace std::literals; +using namespace oxen::log::literals; -std::array blind15_factor(std::span server_pk) { - assert(server_pk.size() == 32); - +b32 blind15_factor(std::span server_pk) { auto blind_hash = hash::blake2b<64>(server_pk); - uc32 k; - crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); + b32 k; + ed25519::scalar_reduce(k, blind_hash); return k; } -std::array blind25_factor( - std::span session_id, std::span server_pk) { - assert(session_id.size() == 32 || session_id.size() == 33); - assert(server_pk.size() == 32); +b32 blind25_factor( + std::span session_id, std::span server_pk) { - uc64 blind_hash; + b64 blind_hash; if (session_id.size() == 32) hash::blake2b(blind_hash, "05"_hex_b, session_id, server_pk); else hash::blake2b(blind_hash, session_id, server_pk); - uc32 k; - crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); + b32 k; + ed25519::scalar_reduce(k, blind_hash); return k; } namespace { - void blind15_id_impl( - std::span session_id, - std::span server_pk, - unsigned char* out) { - auto k = blind15_factor(server_pk); + void blind_id_impl( + std::span session_id, + std::span server_pk, + std::span blind_factor, + std::span out, + std::byte prefix) { if (session_id.size() == 33) session_id = session_id.subspan(1); if (session_id.size() != 32) throw std::invalid_argument{"Invalid session id"}; - auto ed_pk = xed25519::pubkey(session_id.first<32>()); - if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - out[0] = 0x15; + + ed25519::scalarmult_noclamp(out.last<32>(), blind_factor, xed25519::pubkey(session_id.first<32>())); + out[0] = prefix; + } + + void blind15_id_impl( + std::span session_id, + std::span server_pk, + std::span out) { + blind_id_impl(session_id, server_pk, blind15_factor(server_pk), out, std::byte{0x15}); } void blind25_id_impl( - std::span session_id, - std::span server_pk, - unsigned char* out) { - auto k = blind25_factor(session_id, server_pk); - if (session_id.size() == 33) - session_id = session_id.subspan(1); - if (session_id.size() != 32) - throw std::invalid_argument{"Invalid session id"}; - auto ed_pk = xed25519::pubkey(session_id.first<32>()); - if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - out[0] = 0x25; + std::span session_id, + std::span server_pk, + std::span out) { + blind_id_impl(session_id, server_pk, blind25_factor(session_id, server_pk), out, std::byte{0x25}); + } + + // Parses server_pk from either 32 raw bytes or 64 hex digits. + b32 parse_server_pk(std::string_view server_pk_in, std::string_view func_name) { + b32 server_pk; + if (server_pk_in.size() == 32) + std::memcpy(server_pk.data(), server_pk_in.data(), 32); + else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) + oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); + else + throw std::invalid_argument{ + "{}: Invalid server_pk: expected 32 bytes or 64 hex"_format(func_name)}; + return server_pk; + } + + // Common final portion of blind15/blind25 signing: given blinded pubkey A, blinded scalar a, + // nonce r, and message, computes and returns the 64-byte signature. + b64 blinded_sign_finish( + std::span A, + std::span a, + std::span r, + std::span message) { + b64 result; + auto sig_R = std::span{result}.first<32>(); + auto sig_S = std::span{result}.last<32>(); + + ed25519::scalarmult_base_noclamp(sig_R, r); + + b64 hram; + hash::sha512(hram, sig_R, A, message); + + ed25519::scalar_reduce(sig_S, hram); // S = H(R||A||M) + ed25519::scalar_mul(sig_S, sig_S, a); // S = H(R||A||M) a + ed25519::scalar_add(sig_S, sig_S, r); // S = r + H(R||A||M) a + + return result; } } // namespace -std::vector blind15_id( - std::span session_id, std::span server_pk) { +b33 blind15_id( + std::span session_id, std::span server_pk) { if (session_id.size() == 33) { - if (session_id[0] != 0x05) + if (session_id[0] != std::byte{0x05}) throw std::invalid_argument{"blind15_id: session_id must start with 0x05"}; session_id = session_id.subspan(1); } else if (session_id.size() != 32) { throw std::invalid_argument{"blind15_id: session_id must be 32 or 33 bytes"}; } - if (server_pk.size() != 32) - throw std::invalid_argument{"blind15_id: server_pk must be 32 bytes"}; - std::vector result; - result.resize(33); - blind15_id_impl(session_id, server_pk, result.data()); + b33 result; + blind15_id_impl(session_id, server_pk, result); return result; } @@ -106,34 +135,31 @@ std::array blind15_id(std::string_view session_id, std::string_v if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) throw std::invalid_argument{"blind15_id: server_pk must be hex (64 digits)"}; - uc33 raw_sid; + b33 raw_sid; oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); - uc32 raw_server_pk; + b32 raw_server_pk; oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); - uc33 blinded; - blind15_id_impl(to_span(raw_sid), to_span(raw_server_pk), blinded.data()); + b33 blinded; + blind15_id_impl(raw_sid, raw_server_pk, blinded); std::array result; result[0] = oxenc::to_hex(blinded.begin(), blinded.end()); - blinded.back() ^= 0x80; + blinded.back() ^= std::byte{0x80}; result[1] = oxenc::to_hex(blinded.begin(), blinded.end()); return result; } -std::vector blind25_id( - std::span session_id, std::span server_pk) { +b33 blind25_id( + std::span session_id, std::span server_pk) { if (session_id.size() == 33) { - if (session_id[0] != 0x05) + if (session_id[0] != std::byte{0x05}) throw std::invalid_argument{"blind25_id: session_id must start with 0x05"}; } else if (session_id.size() != 32) { throw std::invalid_argument{"blind25_id: session_id must be 32 or 33 bytes"}; } - if (server_pk.size() != 32) - throw std::invalid_argument{"blind25_id: server_pk must be 32 bytes"}; - std::vector result; - result.resize(33); - blind25_id_impl(session_id, server_pk, result.data()); + b33 result; + blind25_id_impl(session_id, server_pk, result); return result; } @@ -145,361 +171,217 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) throw std::invalid_argument{"blind25_id: server_pk must be hex (64 digits)"}; - uc33 raw_sid; + b33 raw_sid; oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); - uc32 raw_server_pk; + b32 raw_server_pk; oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); - uc33 blinded; - blind25_id_impl(to_span(raw_sid), to_span(raw_server_pk), blinded.data()); + b33 blinded; + blind25_id_impl(raw_sid, raw_server_pk, blinded); return oxenc::to_hex(blinded.begin(), blinded.end()); } -std::vector blinded15_id_from_ed( - std::span ed_pubkey, - std::span server_pk, - std::vector* session_id) { - if (ed_pubkey.size() != 32) - throw std::invalid_argument{"blind15_id_from_ed: ed_pubkey must be 32 bytes"}; - if (server_pk.size() != 32) - throw std::invalid_argument{"blind15_id_from_ed: server_pk must be 32 bytes"}; - if (session_id && !session_id->empty()) - throw std::invalid_argument{ - "blind15_id_from_ed: session_id pointer must be an empty string"}; - - if (session_id) { - session_id->resize(33); - session_id->front() = 0x05; - if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) - throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; - } +b33 blinded15_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::optional* session_id) { + if (session_id && !session_id->has_value()) + session_id->emplace(ed25519::pk_to_session_id(ed_pubkey)); - std::vector result; - result.resize(33); + b33 result; auto k = blind15_factor(server_pk); - if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pubkey.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - result[0] = 0x15; + ed25519::scalarmult_noclamp( + std::span{result.data() + 1, 32}, k, ed_pubkey); + result[0] = std::byte{0x15}; return result; } -std::vector blinded25_id_from_ed( - std::span ed_pubkey, - std::span server_pk, - std::vector* session_id) { - if (ed_pubkey.size() != 32) - throw std::invalid_argument{"blind25_id_from_ed: ed_pubkey must be 32 bytes"}; - if (server_pk.size() != 32) - throw std::invalid_argument{"blind25_id_from_ed: server_pk must be 32 bytes"}; - if (session_id && session_id->size() != 0 && session_id->size() != 33) - throw std::invalid_argument{"blind25_id_from_ed: session_id pointer must be 0 or 33 bytes"}; - - std::vector tmp_session_id; +b33 blinded25_id_from_ed( + std::span ed_pubkey, + std::span server_pk, + std::optional* session_id) { + std::optional tmp_session_id; if (!session_id) session_id = &tmp_session_id; - if (session_id->size() == 0) { - session_id->resize(33); - session_id->front() = 0x05; - if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) - throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; - } + if (!session_id->has_value()) + session_id->emplace(ed25519::pk_to_session_id(ed_pubkey)); - auto k = blind25_factor(*session_id, server_pk); + auto k = blind25_factor(**session_id, server_pk); - std::vector result; - result.resize(33); + b33 result; // Blinded25 ids are always constructed using the absolute value of the ed pubkey, so if // negative we need to clear the sign bit to make it positive before computing the blinded // pubkey. - uc32 pos_ed_pubkey; - std::memcpy(pos_ed_pubkey.data(), ed_pubkey.data(), 32); - pos_ed_pubkey[31] &= 0x7f; + b32 pos_ed_pubkey; + std::ranges::copy(ed_pubkey, pos_ed_pubkey.begin()); + pos_ed_pubkey[31] &= std::byte{0x7f}; - if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), pos_ed_pubkey.data())) - throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; - result[0] = 0x25; + ed25519::scalarmult_noclamp( + std::span{result.data() + 1, 32}, k, pos_ed_pubkey); + result[0] = std::byte{0x25}; return result; } -std::pair blind15_key_pair( - std::span ed25519_sk, - std::span server_pk, - uc32* k) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; - - if (server_pk.size() != 32) - throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; - - std::pair result; +std::pair blind15_key_pair( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + b32* k) { + std::pair result; auto& [A, a] = result; /// Generate the blinding factor (storing into `*k`, if a pointer was provided) - uc32 k_tmp; + b32 k_tmp; if (!k) k = &k_tmp; *k = blind15_factor(server_pk); /// Generate a scalar for the private key - if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) + if (0 != crypto_sign_ed25519_sk_to_curve25519( + to_unsigned(a.data()), to_unsigned(ed25519_sk.data()))) throw std::runtime_error{ "blind15_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; // Turn a, A into their blinded versions - crypto_core_ed25519_scalar_mul(a.data(), k->data(), a.data()); - crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + ed25519::scalar_mul(a, *k, a); + ed25519::scalarmult_base_noclamp(A, a); return result; } -std::pair blind25_key_pair( - std::span ed25519_sk, - std::span server_pk, - uc32* k_prime) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; - - if (server_pk.size() != 32) - throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; - - uc33 session_id; - session_id[0] = 0x05; - if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) - throw std::runtime_error{ - "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; +std::pair blind25_key_pair( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + b32* k_prime) { + b33 session_id; + session_id[0] = std::byte{0x05}; + ed25519::pk_to_x25519(std::span{session_id}.last<32>(), ed25519_sk.pubkey()); - std::span X{session_id.data() + 1, 32}; + auto X = std::span{session_id}.last<32>(); /// Generate the blinding factor (storing into `*k`, if a pointer was provided) - uc32 k_tmp; + b32 k_tmp; if (!k_prime) k_prime = &k_tmp; - *k_prime = blind25_factor(X, {server_pk.data(), server_pk.size()}); + *k_prime = blind25_factor(X, server_pk); // For a negative pubkey we use k' = -k so that k'A == kA when A is positive, and k'A = -kA = // k|A| when A is negative. - if (*(ed25519_sk.data() + 63) & 0x80) - crypto_core_ed25519_scalar_negate(k_prime->data(), k_prime->data()); + if ((ed25519_sk.pubkey()[31] & std::byte{0x80}) != std::byte{}) + ed25519::scalar_negate(*k_prime, *k_prime); - std::pair result; + std::pair result; auto& [A, a] = result; // Generate the private key (scalar), a; (the sodium function naming here is misleading; this // call actually has nothing to do with conversion to X25519, it just so happens that the // conversion method is the easiest way to get `a` out of libsodium). - if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) - throw std::runtime_error{ - "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; + a = ed25519::sk_to_x25519(ed25519_sk); // Turn a, A into their blinded versions - crypto_core_ed25519_scalar_mul(a.data(), k_prime->data(), a.data()); - crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + ed25519::scalar_mul(a, *k_prime, a); + ed25519::scalarmult_base_noclamp(A, a); return result; } -static constexpr auto version_blinding_hash_key_sig = "VersionCheckKey_sig"_uc; +static constexpr auto version_blinding_hash_key_sig = "VersionCheckKey_sig"_bytes; -std::pair blind_version_key_pair(std::span ed25519_sk) { - if (ed25519_sk.size() != 32 && ed25519_sk.size() != 64) - throw std::invalid_argument{ - "blind_version_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte " - "value"}; - - std::pair result; - cleared_uc32 blind_seed; - auto& [pk, sk] = result; - hash::blake2b_key(blind_seed, version_blinding_hash_key_sig, ed25519_sk.first(32)); - - // Reuse `sk` to avoid needing extra secure erasing: - if (0 != crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), blind_seed.data())) - throw std::runtime_error{"blind_version_key_pair: ed25519 generation from seed failed"}; - - return result; +std::pair blind_version_key_pair(const ed25519::PrivKeySpan& ed25519_sk) { + cleared_b32 blind_seed; + hash::blake2b_key(blind_seed, version_blinding_hash_key_sig, ed25519_sk.seed()); + return ed25519::keypair(blind_seed); } -static constexpr auto hash_key_seed = "SessCommBlind25_seed"_uc; -static constexpr auto hash_key_sig = "SessCommBlind25_sig"_uc; - -std::vector blind25_sign( - std::span ed25519_sk, - std::string_view server_pk_in, - std::span message) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "blind25_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; - uc32 server_pk; - if (server_pk_in.size() == 32) - std::memcpy(server_pk.data(), server_pk_in.data(), 32); - else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) - oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); - else - throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; +static constexpr auto hash_key_seed = "SessCommBlind25_seed"_bytes; +static constexpr auto hash_key_sig = "SessCommBlind25_sig"_bytes; - auto [A, a] = blind25_key_pair(ed25519_sk, to_span(server_pk)); +b64 blind25_sign( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + std::span message) { + auto [A, a] = blind25_key_pair(ed25519_sk, server_pk); - uc32 seedhash; - hash::blake2b_key(seedhash, hash_key_seed, ed25519_sk.first(32)); + b32 seedhash; + hash::blake2b_key(seedhash, hash_key_seed, ed25519_sk.seed()); - uc64 r_hash; + b64 r_hash; hash::blake2b_key(r_hash, hash_key_sig, seedhash, A, message); - uc32 r; - crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); - - std::vector result; - result.resize(64); - auto* sig_R = result.data(); - auto* sig_S = result.data() + 32; - crypto_scalarmult_ed25519_base_noclamp(sig_R, r.data()); - - crypto_hash_sha512_state st2; - crypto_hash_sha512_init(&st2); - crypto_hash_sha512_update(&st2, sig_R, 32); - crypto_hash_sha512_update(&st2, A.data(), A.size()); - crypto_hash_sha512_update(&st2, message.data(), message.size()); - uc64 hram; - crypto_hash_sha512_final(&st2, hram.data()); + b32 r; + ed25519::scalar_reduce(r, r_hash); - crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) - - crypto_core_ed25519_scalar_mul(sig_S, sig_S, a.data()); // S = H(R||A||M) a - crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a - - return result; + return blinded_sign_finish(A, a, r, message); } -std::vector blind15_sign( - std::span ed25519_sk, +b64 blind25_sign( + const ed25519::PrivKeySpan& ed25519_sk, std::string_view server_pk_in, - std::span message) { - std::array ed_sk_tmp; - if (ed25519_sk.size() == 32) { - std::array pk_ignore; - crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); - ed25519_sk = {ed_sk_tmp.data(), 64}; - } - if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "blind15_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; - - uc32 server_pk; - if (server_pk_in.size() == 32) - std::memcpy(server_pk.data(), server_pk_in.data(), 32); - else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) - oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); - else - throw std::invalid_argument{"blind15_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + std::span message) { + return blind25_sign(ed25519_sk, parse_server_pk(server_pk_in, "blind25_sign"), message); +} - auto [blind_15_pk, blind_15_sk] = blind15_key_pair(ed25519_sk, {server_pk.data(), 32}); +b64 blind15_sign( + const ed25519::PrivKeySpan& ed25519_sk, + std::span server_pk, + std::span message) { + auto [blind_15_pk, blind_15_sk] = blind15_key_pair(ed25519_sk, server_pk); // H_rh = sha512(s.encode()).digest()[32:] - uc64 hrh; - crypto_hash_sha512_state st1; - crypto_hash_sha512_init(&st1); - crypto_hash_sha512_update(&st1, ed25519_sk.data(), 64); - crypto_hash_sha512_final(&st1, hrh.data()); + b64 hrh; + hash::sha512(hrh, ed25519_sk); // r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts)) - auto hrh_suffix = hrh.data() + 32; - uc32 r; - uc64 r_hash; - crypto_hash_sha512_state st2; - crypto_hash_sha512_init(&st2); - crypto_hash_sha512_update(&st2, hrh_suffix, 32); - crypto_hash_sha512_update(&st2, blind_15_pk.data(), blind_15_pk.size()); - crypto_hash_sha512_update(&st2, message.data(), message.size()); - crypto_hash_sha512_final(&st2, r_hash.data()); - crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); - - // sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) - std::vector result; - result.resize(64); - auto* sig_R = result.data(); - auto* sig_S = result.data() + 32; - crypto_scalarmult_ed25519_base_noclamp(sig_R, r.data()); - - // HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts)) - uc64 hram; - crypto_hash_sha512_state st3; - crypto_hash_sha512_init(&st3); - crypto_hash_sha512_update(&st3, sig_R, 32); - crypto_hash_sha512_update(&st3, blind_15_pk.data(), blind_15_pk.size()); - crypto_hash_sha512_update(&st3, message.data(), message.size()); - crypto_hash_sha512_final(&st3, hram.data()); - - // sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka)) - crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) - crypto_core_ed25519_scalar_mul(sig_S, sig_S, blind_15_sk.data()); // S = H(R||A||M) a - crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a + b64 r_hash; + hash::sha512(r_hash, std::span{hrh}.last<32>(), blind_15_pk, message); - return result; + b32 r; + ed25519::scalar_reduce(r, r_hash); + + return blinded_sign_finish(blind_15_pk, blind_15_sk, r, message); } -std::vector blind_version_sign_request( - std::span ed25519_sk, +b64 blind15_sign( + const ed25519::PrivKeySpan& ed25519_sk, + std::string_view server_pk_in, + std::span message) { + return blind15_sign(ed25519_sk, parse_server_pk(server_pk_in, "blind15_sign"), message); +} + +b64 blind_version_sign_request( + const ed25519::PrivKeySpan& ed25519_sk, uint64_t timestamp, std::string_view method, std::string_view path, - std::optional> body) { + std::optional> body) { auto [pk, sk] = blind_version_key_pair(ed25519_sk); // Signature should be on `TIMESTAMP || METHOD || PATH || BODY` - std::vector ts = to_vector(std::to_string(timestamp)); - std::vector buf; - buf.reserve(10 /* timestamp */ + method.size() + path.size() + (body ? body->size() : 0)); - buf.insert(buf.end(), ts.begin(), ts.end()); - buf.insert(buf.end(), method.begin(), method.end()); - buf.insert(buf.end(), path.begin(), path.end()); - + auto ts = "{}"_format(timestamp); + std::vector buf; + buf.reserve(ts.size() + method.size() + path.size() + (body ? body->size() : 0)); + auto app = [&](std::string_view sv) { + auto s = to_span(sv); + buf.insert(buf.end(), s.begin(), s.end()); + }; + app(ts); + app(method); + app(path); if (body) buf.insert(buf.end(), body->begin(), body->end()); return ed25519::sign(sk, buf); } -std::vector blind_version_sign( - std::span ed25519_sk, Platform platform, uint64_t timestamp) { - auto [pk, sk] = blind_version_key_pair(ed25519_sk); - - // Signature should be on `TIMESTAMP || METHOD || PATH` - std::vector ts = to_vector(std::to_string(timestamp)); - std::vector method = to_vector("GET"); - std::vector buf; - buf.reserve(10 + 6 + 33); - buf.insert(buf.end(), ts.begin(), ts.end()); - buf.insert(buf.end(), method.begin(), method.end()); - - std::vector url; +b64 blind_version_sign( + const ed25519::PrivKeySpan& ed25519_sk, Platform platform, uint64_t timestamp) { + std::string_view url; switch (platform) { - case Platform::android: url = to_vector("/session_version?platform=android"); break; - case Platform::desktop: url = to_vector("/session_version?platform=desktop"); break; - case Platform::ios: url = to_vector("/session_version?platform=ios"); break; - default: url = to_vector("/session_version?platform=desktop"); break; + case Platform::android: url = "/session_version?platform=android"; break; + case Platform::ios: url = "/session_version?platform=ios"; break; + case Platform::desktop: + default: url = "/session_version?platform=desktop"; break; } - buf.insert(buf.end(), url.begin(), url.end()); - - return ed25519::sign(sk, buf); + return blind_version_sign_request(ed25519_sk, timestamp, "GET", url, std::nullopt); } bool session_id_matches_blinded_id( @@ -517,7 +399,7 @@ bool session_id_matches_blinded_id( "session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; std::string converted_blind_id1, converted_blind_id2; - std::vector converted_blind_id1_raw; + std::vector converted_blind_id1_raw; switch (blinded_id[0]) { case '1': { @@ -541,7 +423,8 @@ LIBSESSION_C_API bool session_blind15_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto [b_pk, b_sk] = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = session::blind15_key_pair( + {ed25519_seckey, 64}, to_byte_span<32>(server_pk)); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -556,7 +439,8 @@ LIBSESSION_C_API bool session_blind25_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto [b_pk, b_sk] = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = session::blind25_key_pair( + {ed25519_seckey, 64}, to_byte_span<32>(server_pk)); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -589,7 +473,7 @@ LIBSESSION_C_API bool session_blind15_sign( auto sig = session::blind15_sign( {ed25519_seckey, 64}, {reinterpret_cast(server_pk), 32}, - {msg, msg_len}); + to_byte_span(msg, msg_len)); std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -607,7 +491,7 @@ LIBSESSION_C_API bool session_blind25_sign( auto sig = session::blind25_sign( {ed25519_seckey, 64}, {reinterpret_cast(server_pk), 32}, - {msg, msg_len}); + to_byte_span(msg, msg_len)); std::memcpy(blinded_sig_out, sig.data(), sig.size()); return true; } catch (...) { @@ -626,9 +510,9 @@ LIBSESSION_C_API bool session_blind_version_sign_request( std::string_view method_sv{method}; std::string_view path_sv{path}; - std::optional> body_sv{std::nullopt}; + std::optional> body_sv{std::nullopt}; if (body) - body_sv = std::span{body, body_len}; + body_sv = to_byte_span(body, body_len); try { auto sig = session::blind_version_sign_request( diff --git a/src/config.cpp b/src/config.cpp index c1df4f693..fd056172f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -345,7 +345,7 @@ namespace { return std::string_view{reinterpret_cast(hash.data()), hash.size()}; } - hash_t& hash_msg(hash_t& into, std::span serialized) { + hash_t& hash_msg(hash_t& into, std::span serialized) { hash::blake2b(into, serialized); return into; } @@ -428,11 +428,11 @@ namespace { void verify_config_sig( oxenc::bt_dict_consumer dict, const ConfigMessage::verify_callable& verifier, - std::optional>* verified_signature, + std::optional* verified_signature, bool trust_signature) { if (dict.skip_until("~")) { dict.consume_signature( - [&](std::span to_verify, std::span sig) { + [&](std::span to_verify, std::span sig) { if (sig.size() != 64) throw signature_error{"Config signature is invalid (not 64B)"}; if (verifier && !verifier(to_verify, sig)) @@ -481,7 +481,7 @@ void MutableConfigMessage::increment_impl() { // Append the source config's diff to the new object lagged_diffs_.emplace_hint(lagged_diffs_.end(), seqno_hash_, std::move(diff_)); seqno_hash_.first++; - seqno_hash_.second.fill(0); // Not strictly necessary, but makes it obvious if used + seqno_hash_.second.fill(std::byte{0}); // Not strictly necessary, but makes it obvious if used diff_.clear(); } @@ -522,7 +522,7 @@ ConfigMessage::ConfigMessage() { } ConfigMessage::ConfigMessage( - std::span serialized, + std::span serialized, verify_callable verifier_, sign_callable signer_, int lag, @@ -560,7 +560,7 @@ ConfigMessage::ConfigMessage( } ConfigMessage::ConfigMessage( - const std::vector>& serialized_confs, + const std::vector>& serialized_confs, verify_callable verifier_, sign_callable signer_, int lag, @@ -690,7 +690,7 @@ ConfigMessage::ConfigMessage( } MutableConfigMessage::MutableConfigMessage( - const std::vector>& serialized_confs, + const std::vector>& serialized_confs, verify_callable verifier, sign_callable signer, int lag, @@ -706,7 +706,7 @@ MutableConfigMessage::MutableConfigMessage( } MutableConfigMessage::MutableConfigMessage( - std::span config, + std::span config, verify_callable verifier, sign_callable signer, int lag) : @@ -728,13 +728,13 @@ const oxenc::bt_dict& MutableConfigMessage::diff() { return diff_; } -std::vector ConfigMessage::serialize(bool enable_signing) { +std::vector ConfigMessage::serialize(bool enable_signing) { return serialize_impl( diff(), // implicitly prunes (if actually a mutable instance) enable_signing); } -std::vector ConfigMessage::serialize_impl( +std::vector ConfigMessage::serialize_impl( const oxenc::bt_dict& curr_diff, bool enable_signing) { oxenc::bt_dict_producer outer{}; @@ -775,7 +775,7 @@ std::vector ConfigMessage::serialize_impl( reinterpret_cast(verified_signature_->data()), verified_signature_->size()}); } else if (signer && enable_signing) { - outer.append_signature("~", [this](std::span to_sign) { + outer.append_signature("~", [this](std::span to_sign) { auto sig = signer(to_sign); if (sig.size() != 64) throw std::logic_error{ @@ -783,13 +783,13 @@ std::vector ConfigMessage::serialize_impl( return sig; }); } - return to_vector(outer.view()); + return to_vector(outer.view()); } const hash_t& MutableConfigMessage::hash() { return hash(serialize()); } -const hash_t& MutableConfigMessage::hash(std::span serialized) { +const hash_t& MutableConfigMessage::hash(std::span serialized) { return hash_msg(seqno_hash_.second, serialized); } diff --git a/src/config/base.cpp b/src/config/base.cpp index 84146fdbb..96037e4fc 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -71,8 +71,8 @@ std::unique_ptr make_config_message(bool from_dirty, Args&&... ar } std::unordered_set ConfigBase::merge( - const std::vector>>& configs) { - std::vector>> config_views; + const std::vector>>& configs) { + std::vector>> config_views; config_views.reserve(configs.size()); for (auto& [hash, data] : configs) config_views.emplace_back(hash, data); @@ -80,16 +80,16 @@ std::unordered_set ConfigBase::merge( } std::unordered_set ConfigBase::merge( - const std::vector>>& configs) { + const std::vector>>& configs) { if (accepts_protobuf() && !_keys.empty()) { - std::list> keep_alive; - std::vector>> parsed; + std::list> keep_alive; + std::vector>> parsed; parsed.reserve(configs.size()); for (auto& [h, c] : configs) { try { auto unwrapped = protos::unwrap_config( - std::span{_keys.front().data(), _keys.front().size()}, + _keys.front(), c, storage_namespace()); @@ -98,8 +98,7 @@ std::unordered_set ConfigBase::merge( // support multi-device for users running those old versions try { auto unwrapped2 = protos::unwrap_config( - std::span{ - _keys.front().data(), _keys.front().size()}, + _keys.front(), unwrapped, storage_namespace()); log::warning( @@ -121,9 +120,9 @@ std::unordered_set ConfigBase::merge( return _merge(configs); } -std::pair, std::vector>>> -ConfigBase::_handle_multipart(std::string_view msg_id, std::span message) { - assert(!message.empty() && message[0] == 'm'); +std::pair, std::vector>>> +ConfigBase::_handle_multipart(std::string_view msg_id, std::span message) { + assert(!message.empty() && message[0] == std::byte{'m'}); // Handle multipart messages. Each part of a multipart message starts with `m` and then is // immediately followed by a bt_list where: @@ -140,7 +139,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span()}; - auto h = c.consume>(); + auto h = c.consume>(); hash_t final_hash; if (h.size() != final_hash.size()) throw std::runtime_error{"Invalid multi-part final message hash"}; @@ -156,7 +155,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span(); + auto data = c.consume_span(); if (data.empty()) throw std::runtime_error{"Invalid multi-part message with empty data"}; @@ -201,7 +200,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span, std::vector> result{}; + std::pair, std::vector> result{}; auto& [msgids, recombined] = result; size_t final_size = 0; @@ -237,15 +236,15 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span{ + if (recombined[0] == std::byte{'z'}) { + if (auto decompressed = zstd_decompress(std::span{ recombined.data() + 1, recombined.size() - 1}); decompressed && !decompressed->empty()) { log::debug( @@ -264,7 +263,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span(recombined[0]))}; @@ -355,7 +354,7 @@ void ConfigBase::_load_multiparts(oxenc::bt_dict_consumer&& multi) { auto pd = parts_list.consume_dict_consumer(); auto index = pd.consume_integer(); auto msgid = pd.consume_string_view(); - auto chunk = pd.consume_span(); + auto chunk = pd.consume_span(); pm.parts.emplace_back(index, msgid, chunk); } } @@ -364,14 +363,14 @@ void ConfigBase::_load_multiparts(oxenc::bt_dict_consumer&& multi) { } std::unordered_set ConfigBase::_merge( - std::span>> configs) { + std::span>> configs) { if (_keys.empty()) throw std::logic_error{"Cannot merge configs without any decryption keys"}; const auto old_seqno = _config->seqno(); std::vector> all_hashes; // >1 hashes for multipart configs - std::vector> all_confs; + std::vector> all_confs; all_hashes.reserve(configs.size() + 1); all_confs.reserve(configs.size() + 1); @@ -396,7 +395,7 @@ std::unordered_set ConfigBase::_merge( // at the end (rather than the beginning) so that it is identical to one of the incoming // messages, *that* one becomes the config superset rather than our current, hash-unknown value. - std::vector mine; + std::vector mine; bool mine_last = false; if (old_seqno != 0 || is_dirty()) { mine = _config->serialize(); @@ -409,7 +408,7 @@ std::unordered_set ConfigBase::_merge( } } - std::vector>> plaintexts; + std::vector>> plaintexts; std::unordered_set good_hashes; @@ -440,7 +439,7 @@ std::unordered_set ConfigBase::_merge( for (auto& [hash, plain] : plaintexts) { // Remove prefix padding: if (auto it = std::find_if( - plain.begin(), plain.end(), [](unsigned char c) { return c != 0; }); + plain.begin(), plain.end(), [](std::byte c) { return c != std::byte{0}; }); it != plain.begin() && it != plain.end()) { auto p = std::distance(plain.begin(), it); std::memmove(plain.data(), plain.data() + p, plain.size() - p); @@ -451,7 +450,7 @@ std::unordered_set ConfigBase::_merge( continue; } - bool was_multipart = plain[0] == 'm'; + bool was_multipart = plain[0] == std::byte{'m'}; if (was_multipart) { // Multipart message @@ -471,11 +470,11 @@ std::unordered_set ConfigBase::_merge( } // Single-part message - bool was_compressed = plain[0] == 'z'; + bool was_compressed = plain[0] == std::byte{'z'}; if (was_compressed) { // zstd-compressed data if (auto decompressed = zstd_decompress( - std::span{plain.data() + 1, plain.size() - 1}); + std::span{plain.data() + 1, plain.size() - 1}); decompressed && !decompressed->empty()) plain = std::move(*decompressed); else { @@ -484,7 +483,7 @@ std::unordered_set ConfigBase::_merge( } } - if (plain[0] != 'd') { + if (plain[0] != std::byte{'d'}) { log::error( cat, "invalid/unsupported config message with type {:?}", @@ -685,23 +684,23 @@ bool ConfigBase::needs_push() const { // smaller than the source message then we modify `msg` to contain the 'z'-prefixed compressed // message, otherwise we leave it as-is. Returns true if compression was beneficial and `msg` has // been compressed; false if compression did not reduce the size and msg was left as-is. -void compress_message(std::vector& msg, int level) { +void compress_message(std::vector& msg, int level) { if (!level) return; // "z" is our zstd compression marker prefix byte - std::vector compressed = zstd_compress(msg, level, to_span("z")); + std::vector compressed = zstd_compress(msg, level, to_span("z")); if (compressed.size() < msg.size()) msg = std::move(compressed); } -std::tuple>, std::vector> +std::tuple>, std::vector> ConfigBase::push() { if (_keys.empty()) throw std::logic_error{"Cannot push data without an encryption key!"}; auto s = _config->seqno(); - std::tuple>, std::vector> ret{ + std::tuple>, std::vector> ret{ s, {}, {}}; auto& [seqno, msgs, obs] = ret; @@ -756,14 +755,14 @@ ConfigBase::push() { oxenc::to_hex(final_hash.begin(), final_hash.end()), num_parts); - std::span remaining{msg}; + std::span remaining{msg}; for (uint8_t index = 0; !remaining.empty(); ++index) { auto& out = msgs.emplace_back(); auto chunk = remaining.subspan(0, std::min(MAX_CHUNK_SIZE, remaining.size())); remaining = remaining.subspan(chunk.size()); out.reserve(chunk.size() + ENCODE_OVERHEAD + ENCRYPT_DATA_OVERHEAD); out.resize(chunk.size() + ENCODE_OVERHEAD); - out[0] = 'm'; + out[0] = std::byte{'m'}; { oxenc::bt_list_producer lp{reinterpret_cast(out.data() + 1), out.size() - 1}; lp.append(std::span{final_hash}); @@ -790,7 +789,7 @@ ConfigBase::push() { if (accepts_protobuf() && !_keys.empty()) { auto pbwrapped = protos::wrap_config( - {_keys.front().data(), _keys.front().size()}, msg, s, storage_namespace()); + _keys.front(), msg, s, storage_namespace()); // If protobuf wrapping would push us *over* the max message size then we just skip the // protobuf wrapping because older clients (that need protobuf) also don't support // multipart anyway, so we can't produce a message they will accept no matter what. @@ -824,7 +823,7 @@ void ConfigBase::confirm_pushed(seqno_t seqno, std::unordered_set m } } -std::vector ConfigBase::dump() { +std::vector ConfigBase::dump() { if (is_readonly()) _old_hashes.clear(); @@ -835,7 +834,7 @@ std::vector ConfigBase::dump() { return d; } -std::vector ConfigBase::make_dump() const { +std::vector ConfigBase::make_dump() const { auto data = _config->serialize(false /* disable signing for local storage */); auto data_sv = to_string_view(data); oxenc::bt_list old_hashes; @@ -855,9 +854,9 @@ std::vector ConfigBase::make_dump() const { } ConfigBase::ConfigBase( - std::optional> dump, - std::optional> ed25519_pubkey, - std::optional> ed25519_secretkey) { + std::optional> dump, + std::optional> ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; @@ -866,11 +865,11 @@ ConfigBase::ConfigBase( } void ConfigSig::init_sig_keys( - std::optional> ed25519_pubkey, - std::optional> ed25519_secretkey) { + std::optional> ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey) { if (ed25519_secretkey) { if (ed25519_pubkey && - to_string_view(*ed25519_pubkey) != to_string_view(ed25519_secretkey->subspan(32))) + !std::ranges::equal(*ed25519_pubkey, ed25519_secretkey->pubkey())) throw std::invalid_argument{"Invalid signing keys: secret key and pubkey do not match"}; set_sig_keys(*ed25519_secretkey); } else if (ed25519_pubkey) { @@ -881,9 +880,9 @@ void ConfigSig::init_sig_keys( } void ConfigBase::init( - std::optional> dump, - std::optional> ed25519_pubkey, - std::optional> ed25519_secretkey) { + std::optional> dump, + std::optional> ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey) { if (!dump) { _state = ConfigState::Clean; _config = std::make_unique(); @@ -951,7 +950,7 @@ int ConfigBase::key_count() const { return _keys.size(); } -bool ConfigBase::has_key(std::span key) const { +bool ConfigBase::has_key(std::span key) const { if (key.size() != 32) throw std::invalid_argument{"invalid key given to has_key(): not 32-bytes"}; @@ -962,8 +961,8 @@ bool ConfigBase::has_key(std::span key) const { return false; } -std::vector> ConfigBase::get_keys() const { - std::vector> ret; +std::vector> ConfigBase::get_keys() const { + std::vector> ret; ret.reserve(_keys.size()); for (const auto& key : _keys) ret.emplace_back(key.data(), key.size()); @@ -971,7 +970,7 @@ std::vector> ConfigBase::get_keys() const { } void ConfigBase::add_key( - std::span key, bool high_priority, bool dirty_config) { + std::span key, bool high_priority, bool dirty_config) { static_assert( sizeof(Key) == KEY_SIZE, "std::array appears to have some overhead which seems bad"); @@ -1010,7 +1009,7 @@ int ConfigBase::clear_keys(bool dirty_config) { } void ConfigBase::replace_keys( - const std::vector>& new_keys, bool dirty_config) { + const std::vector>& new_keys, bool dirty_config) { if (new_keys.empty()) { if (_keys.empty()) return; @@ -1035,7 +1034,7 @@ void ConfigBase::replace_keys( dirty(); } -bool ConfigBase::remove_key(std::span key, size_t from, bool dirty_config) { +bool ConfigBase::remove_key(std::span key, size_t from, bool dirty_config) { auto starting_size = _keys.size(); if (from >= starting_size) return false; @@ -1058,46 +1057,31 @@ bool ConfigBase::remove_key(std::span key, size_t from, boo return _keys.size() < starting_size; } -void ConfigBase::load_key(std::span ed25519_secretkey) { - if (!(ed25519_secretkey.size() == 64 || ed25519_secretkey.size() == 32)) - throw std::invalid_argument{ - encryption_domain() + " requires an Ed25519 64-byte secret key or 32-byte seed"s}; - - add_key(ed25519_secretkey.subspan(0, 32)); +void ConfigBase::load_key(const ed25519::PrivKeySpan& ed25519_secretkey) { + add_key(ed25519_secretkey.seed()); } -void ConfigSig::set_sig_keys(std::span secret) { - if (secret.size() != 64) - throw std::invalid_argument{"Invalid sodium secret: expected 64 bytes"}; +void ConfigSig::set_sig_keys(const ed25519::PrivKeySpan& secret) { clear_sig_keys(); _sign_sk.reset(64); - std::memcpy(_sign_sk.data(), secret.data(), secret.size()); - _sign_pk.emplace(); - crypto_sign_ed25519_sk_to_pk(_sign_pk->data(), _sign_sk.data()); + std::memcpy(_sign_sk.data(), secret.data(), 64); + ed25519::sk_to_pk(_sign_pk.emplace(), secret); - set_verifier([this](std::span data, std::span sig) { - return 0 == crypto_sign_ed25519_verify_detached( - sig.data(), data.data(), data.size(), _sign_pk->data()); + set_verifier([this](std::span data, std::span sig) { + return sig.size() == 64 && ed25519::verify(sig.first<64>(), *_sign_pk, data); }); - set_signer([this](std::span data) { - std::vector sig; - sig.resize(64); - if (0 != crypto_sign_ed25519_detached( - sig.data(), nullptr, data.data(), data.size(), _sign_sk.data())) - throw std::runtime_error{"Internal error: config signing failed!"}; - return sig; + set_signer([this](std::span data) { + ed25519::PrivKeySpan sk{std::span{_sign_sk.data(), 64}}; + auto sig = ed25519::sign(sk, data); + return std::vector{sig.begin(), sig.end()}; }); } -void ConfigSig::set_sig_pubkey(std::span pubkey) { - if (pubkey.size() != 32) - throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; - _sign_pk.emplace(); - std::memcpy(_sign_pk->data(), pubkey.data(), 32); +void ConfigSig::set_sig_pubkey(std::span pubkey) { + std::ranges::copy(pubkey, _sign_pk.emplace().begin()); - set_verifier([this](std::span data, std::span sig) { - return 0 == crypto_sign_ed25519_verify_detached( - sig.data(), data.data(), data.size(), _sign_pk->data()); + set_verifier([this](std::span data, std::span sig) { + return sig.size() == 64 && ed25519::verify(sig.first<64>(), *_sign_pk, data); }); } @@ -1116,10 +1100,12 @@ void ConfigBase::set_signer(ConfigMessage::sign_callable s) { _config->signer = std::move(s); } -std::array ConfigSig::seed_hash(std::string_view key) const { +cleared_b32 ConfigSig::seed_hash(std::string_view key) const { if (!_sign_sk) throw std::runtime_error{"Cannot make a seed hash without a signing secret key"}; - return hash::blake2b_key<32>(key, std::span{_sign_sk.data(), 32}); + cleared_b32 result; + hash::blake2b_key(result, key, std::span{_sign_sk.data(), 32}); + return result; } namespace { @@ -1156,11 +1142,11 @@ LIBSESSION_EXPORT config_string_list* config_merge( size_t count) { return wrap_exceptions(conf, [&] { auto& config = *unbox(conf); - std::vector>> confs; + std::vector>> confs; confs.reserve(count); for (size_t i = 0; i < count; i++) confs.emplace_back( - msg_hashes[i], std::span{configs[i], lengths[i]}); + msg_hashes[i], to_byte_span(configs[i], lengths[i])); return make_string_list(config.merge(confs)); }); @@ -1308,7 +1294,7 @@ LIBSESSION_EXPORT bool config_add_key(config_object* conf, const unsigned char* return wrap_exceptions( conf, [&] { - unbox(conf)->add_key({key, 32}); + unbox(conf)->add_key(to_byte_span<32>(key)); return true; }, false); @@ -1318,7 +1304,7 @@ LIBSESSION_EXPORT bool config_add_key_low_prio(config_object* conf, const unsign return wrap_exceptions( conf, [&] { - unbox(conf)->add_key({key, 32}, /*high_priority=*/false); + unbox(conf)->add_key(to_byte_span<32>(key), /*high_priority=*/false); return true; }, false); @@ -1327,20 +1313,20 @@ LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { return unbox(conf)->clear_keys(); } LIBSESSION_EXPORT bool config_remove_key(config_object* conf, const unsigned char* key) { - return unbox(conf)->remove_key({key, 32}); + return unbox(conf)->remove_key(to_byte_span<32>(key)); } LIBSESSION_EXPORT int config_key_count(const config_object* conf) { return unbox(conf)->key_count(); } LIBSESSION_EXPORT bool config_has_key(const config_object* conf, const unsigned char* key) { try { - return unbox(conf)->has_key({key, 32}); + return unbox(conf)->has_key(to_byte_span<32>(key)); } catch (...) { return false; } } LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i) { - return unbox(conf)->key(i).data(); + return to_unsigned(unbox(conf)->key(i).data()); } LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf) { @@ -1351,7 +1337,7 @@ LIBSESSION_EXPORT bool config_set_sig_keys(config_object* conf, const unsigned c return wrap_exceptions( conf, [&] { - unbox(conf)->set_sig_keys({secret, 64}); + unbox(conf)->set_sig_keys(ed25519::PrivKeySpan{secret, 64}); return true; }, false); @@ -1361,7 +1347,7 @@ LIBSESSION_EXPORT bool config_set_sig_pubkey(config_object* conf, const unsigned return wrap_exceptions( conf, [&] { - unbox(conf)->set_sig_pubkey({pubkey, 32}); + unbox(conf)->set_sig_pubkey(to_byte_span<32>(pubkey)); return true; }, false); @@ -1370,7 +1356,7 @@ LIBSESSION_EXPORT bool config_set_sig_pubkey(config_object* conf, const unsigned LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { const auto& pk = unbox(conf)->get_sig_pubkey(); if (pk) - return pk->data(); + return to_unsigned(pk->data()); return nullptr; } diff --git a/src/config/community.cpp b/src/config/community.cpp index 1832f6f6a..0b89b5204 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -24,7 +24,7 @@ community::community(std::string_view base_url_, std::string_view room_) { } community::community( - std::string_view base_url, std::string_view room, std::span pubkey_) : + std::string_view base_url, std::string_view room, std::span pubkey_) : community{base_url, room} { set_pubkey(pubkey_); } @@ -46,9 +46,7 @@ void community::set_base_url(std::string_view new_url) { base_url_ = canonical_url(new_url); } -void community::set_pubkey(std::span pubkey) { - if (pubkey.size() != 32) - throw std::invalid_argument{"Invalid pubkey: expected a 32-byte pubkey"}; +void community::set_pubkey(std::span pubkey) { pubkey_.assign(pubkey.begin(), pubkey.end()); } void community::set_pubkey(std::string_view pubkey) { @@ -80,7 +78,7 @@ std::string community::full_url() const { } std::string community::full_url( - std::string_view base_url, std::string_view room, std::span pubkey) { + std::string_view base_url, std::string_view room, std::span pubkey) { std::string url{base_url}; url += '/'; url += room; @@ -130,9 +128,9 @@ std::string community::canonical_room(std::string_view room) { return r; } -std::tuple>> +std::tuple>> community::parse_partial_url(std::string_view url) { - std::tuple>> result; + std::tuple>> result; auto& [base_url, room_token, maybe_pubkey] = result; // Consume the URL from back to front; first the public key: @@ -156,7 +154,7 @@ community::parse_partial_url(std::string_view url) { return result; } -std::tuple> community::parse_full_url( +std::tuple> community::parse_full_url( std::string_view full_url) { auto [base, rm, maybe_pk] = parse_partial_url(full_url); if (!maybe_pk) @@ -216,7 +214,7 @@ LIBSESSION_C_API bool community_parse_partial_url( LIBSESSION_C_API void community_make_full_url( const char* base_url, const char* room, const unsigned char* pubkey, char* full_url) { auto full = session::config::community::full_url( - base_url, room, std::span{pubkey, 32}); + base_url, room, std::span{reinterpret_cast(pubkey), 32}); assert(full.size() <= COMMUNITY_FULL_URL_MAX_LENGTH); std::memcpy(full_url, full.data(), full.size() + 1); } diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 9df541d2b..db511741d 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -59,8 +59,8 @@ void contact_info::set_nickname_truncated(std::string n) { } Contacts::Contacts( - std::span ed25519_secretkey, - std::optional> dumped) { + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, std::nullopt, std::nullopt); load_key(ed25519_secretkey); } @@ -154,7 +154,9 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); if (std::strlen(c.profile_pic.url)) { profile_picture.url = c.profile_pic.url; - profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); + profile_picture.key.assign( + reinterpret_cast(c.profile_pic.key), + reinterpret_cast(c.profile_pic.key) + 32); } profile_updated = to_sys_seconds(c.profile_updated); approved = c.approved; @@ -321,7 +323,7 @@ size_t Contacts::size() const { blinded_contact_info::blinded_contact_info( std::string_view community_base_url, - std::span community_pubkey, + std::span community_pubkey, std::string_view blinded_id) : comm{community( std::move(community_base_url), blinded_id.substr(2), std::move(community_pubkey))} { @@ -378,13 +380,15 @@ void blinded_contact_info::into(contacts_blinded_contact& c) const { } blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { - comm = community(c.base_url, {c.session_id + 2, 64}, c.pubkey); + comm = community(c.base_url, {c.session_id + 2, 64}, std::as_bytes(std::span{c.pubkey})); assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); name = c.name; assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); if (std::strlen(c.profile_pic.url)) { profile_picture.url = c.profile_pic.url; - profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); + profile_picture.key.assign( + reinterpret_cast(c.profile_pic.key), + reinterpret_cast(c.profile_pic.key) + 32); } profile_updated = to_sys_seconds(c.profile_updated); priority = c.priority; @@ -416,7 +420,7 @@ void blinded_contact_info::set_room(std::string_view room) { comm.set_room(room); } -void blinded_contact_info::set_pubkey(std::span pubkey) { +void blinded_contact_info::set_pubkey(std::span pubkey) { comm.set_pubkey(pubkey); } @@ -425,13 +429,13 @@ void blinded_contact_info::set_pubkey(std::string_view pubkey) { } ConfigBase::DictFieldProxy Contacts::blinded_contact_field( - const blinded_contact_info& bc, std::span* get_pubkey) const { + const blinded_contact_info& bc, std::span* get_pubkey) const { auto record = data["b"][bc.comm.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) - *get_pubkey = std::span{ - reinterpret_cast(pk.data()), pk.size()}; + *get_pubkey = std::span{ + reinterpret_cast(pk.data()), pk.size()}; } return record["R"][bc.comm.room()]; // The `room` value is the blinded id without the prefix } @@ -464,8 +468,11 @@ blinded_contact_info Contacts::get_or_construct_blinded( if (auto maybe = get_blinded(blinded_id_hex)) return *std::move(maybe); + auto pk = oxenc::from_hex(community_pubkey_hex); return blinded_contact_info{ - community_base_url, to_span(oxenc::from_hex(community_pubkey_hex)), blinded_id_hex}; + community_base_url, + std::span{reinterpret_cast(pk.data()), 32}, + blinded_id_hex}; } std::vector Contacts::blinded() const { diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index bee7de48b..7c2878fb7 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -62,7 +62,7 @@ namespace convo { } community::community(const convo_info_volatile_community& c) : - config::community{c.base_url, c.room, std::span{c.pubkey, 32}}, + config::community{c.base_url, c.room, std::as_bytes(std::span{c.pubkey})}, base(c.last_read, c.unread) {} void community::into(convo_info_volatile_community& c) const { @@ -161,7 +161,7 @@ namespace convo { base::load(info_dict); auto pro_expiry = int_or_0(info_dict, "e"); - std::optional> maybe_pro_gen_index_hash = + std::optional> maybe_pro_gen_index_hash = maybe_vector(info_dict, "g"); if (pro_expiry > 0 && maybe_pro_gen_index_hash && maybe_pro_gen_index_hash->size() == 32) { pro_expiry_unix_ts = std::chrono::sys_time( @@ -182,8 +182,8 @@ namespace convo { } // namespace convo ConvoInfoVolatile::ConvoInfoVolatile( - std::span ed25519_secretkey, - std::optional> dumped) { + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, std::nullopt, std::nullopt); load_key(ed25519_secretkey); } @@ -208,12 +208,12 @@ convo::one_to_one ConvoInfoVolatile::get_or_construct_1to1(std::string_view pubk } ConfigBase::DictFieldProxy ConvoInfoVolatile::community_field( - const convo::community& comm, std::span* get_pubkey) const { + const convo::community& comm, std::span* get_pubkey) const { auto record = data["o"][comm.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) - *get_pubkey = to_span(pk); + *get_pubkey = to_span(pk); } return record["R"][comm.room_norm()]; } @@ -222,11 +222,11 @@ std::optional ConvoInfoVolatile::get_community( std::string_view base_url, std::string_view room) const { convo::community og{base_url, community::canonical_room(room)}; - std::span pubkey; + std::span pubkey; if (auto* info_dict = community_field(og, &pubkey).dict()) { og.load(*info_dict); if (!pubkey.empty()) - og.set_pubkey(pubkey); + og.set_pubkey(pubkey.first<32>()); return og; } return std::nullopt; @@ -241,8 +241,8 @@ std::optional ConvoInfoVolatile::get_community( convo::community ConvoInfoVolatile::get_or_construct_community( std::string_view base_url, std::string_view room, - std::span pubkey) const { - convo::community result{base_url, community::canonical_room(room), pubkey}; + std::span pubkey) const { + convo::community result{base_url, community::canonical_room(room), pubkey.first<32>()}; if (auto* info_dict = community_field(result).dict()) result.load(*info_dict); @@ -340,7 +340,7 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { auto pro_expiry = epoch_ms(c.pro_expiry_unix_ts); if (pro_expiry > 0 && c.pro_gen_index_hash) { set_nonzero_int(info["e"], pro_expiry); - info["g"] = *c.pro_gen_index_hash; + info["g"] = to_span(*c.pro_gen_index_hash); } } @@ -410,7 +410,7 @@ void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { erase_community(base, room); } -std::tuple>, std::vector> +std::tuple>, std::vector> ConvoInfoVolatile::push() { // Prune off any conversations with last_read timestamps more than PRUNE_HIGH ago (unless they // also have a `unread` flag set, in which case we keep them indefinitely). @@ -446,7 +446,7 @@ void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { auto pro_expiry = epoch_ms(c.pro_expiry_unix_ts); if (pro_expiry > 0 && c.pro_gen_index_hash) { set_nonzero_int(info["e"], pro_expiry); - info["g"] = *c.pro_gen_index_hash; + info["g"] = to_span(*c.pro_gen_index_hash); } } @@ -742,7 +742,10 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( [&] { unbox(conf) ->get_or_construct_community( - base_url, room, std::span{pubkey, 32}) + base_url, + room, + std::span{ + reinterpret_cast(pubkey), 32}) .into(*convo); return true; }, diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index a0bb34508..3072044ef 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -29,7 +29,7 @@ static constexpr auto NONCE_KEY_PREFIX = "libsessionutil-config-encrypted-"sv; static_assert(NONCE_KEY_PREFIX.size() + DOMAIN_MAX_SIZE < crypto_generichash_blake2b_KEYBYTES_MAX); static std::array make_encrypt_key( - std::span key_base, uint64_t message_size, std::string_view domain) { + std::span key_base, uint64_t message_size, std::string_view domain) { if (key_base.size() != 32) throw std::invalid_argument{"encrypt called with key_base != 32 bytes"}; if (domain.size() < 1 || domain.size() > DOMAIN_MAX_SIZE) @@ -51,8 +51,8 @@ static std::array ma } void encrypt_prealloced( - std::span message, - std::span key_base, + std::span message, + std::span key_base, std::string_view domain) { if (message.size() < ENCRYPT_DATA_OVERHEAD) throw std::invalid_argument{ @@ -68,33 +68,33 @@ void encrypt_prealloced( unsigned long long outlen = 0; crypto_aead_xchacha20poly1305_ietf_encrypt( - message.data(), + to_unsigned(message.data()), &outlen, - plaintext.data(), + to_unsigned(plaintext.data()), plaintext.size(), nullptr, 0, nullptr, - nonce.data(), + to_unsigned(nonce.data()), key.data()); assert(outlen == plaintext.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); - std::memcpy(message.data() + outlen, nonce.data(), nonce.size()); + std::memcpy(to_unsigned(message.data()) + outlen, to_unsigned(nonce.data()), nonce.size()); } -std::vector encrypt( - std::span message, - std::span key_base, +std::vector encrypt( + std::span message, + std::span key_base, std::string_view domain) { - std::vector out(message.size() + ENCRYPT_DATA_OVERHEAD); + std::vector out(message.size() + ENCRYPT_DATA_OVERHEAD); std::memcpy(out.data(), message.data(), message.size()); encrypt_prealloced(out, key_base, domain); return out; } void encrypt_inplace( - std::vector& message, - std::span key_base, + std::vector& message, + std::span key_base, std::string_view domain) { message.resize(message.size() + ENCRYPT_DATA_OVERHEAD); encrypt_prealloced(message, key_base, domain); @@ -104,37 +104,37 @@ static_assert( ENCRYPT_DATA_OVERHEAD == crypto_aead_xchacha20poly1305_IETF_ABYTES + crypto_aead_xchacha20poly1305_IETF_NPUBBYTES); -std::vector decrypt( - std::span ciphertext, - std::span key_base, +std::vector decrypt( + std::span ciphertext, + std::span key_base, std::string_view domain) { - std::vector x = session::to_vector(ciphertext); + auto x = session::to_vector(ciphertext); decrypt_inplace(x, key_base, domain); return x; } void decrypt_inplace( - std::vector& ciphertext, - std::span key_base, + std::vector& ciphertext, + std::span key_base, std::string_view domain) { size_t message_len = ciphertext.size() - ENCRYPT_DATA_OVERHEAD; if (message_len > ciphertext.size()) // overflow throw decrypt_error{"Decryption failed: ciphertext is too short"}; - std::span nonce = std::span{ciphertext}.subspan( + std::span nonce = std::span{ciphertext}.subspan( ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); auto key = make_encrypt_key(key_base, message_len, domain); unsigned long long mlen_wrote = 0; if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - ciphertext.data(), + to_unsigned(ciphertext.data()), &mlen_wrote, nullptr, - ciphertext.data(), + to_unsigned(ciphertext.data()), ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, nullptr, 0, - nonce.data(), + to_unsigned(nonce.data()), key.data())) throw decrypt_error{"Message decryption failed"}; @@ -142,10 +142,10 @@ void decrypt_inplace( ciphertext.resize(mlen_wrote); } -void pad_message(std::vector& data, size_t overhead) { +void pad_message(std::vector& data, size_t overhead) { size_t target_size = padded_size(data.size(), overhead); if (target_size > data.size()) - data.insert(data.begin(), target_size - data.size(), 0); + data.insert(data.begin(), target_size - data.size(), std::byte{0}); } } // namespace session::config @@ -159,9 +159,12 @@ LIBSESSION_EXPORT unsigned char* config_encrypt( const char* domain, size_t* ciphertext_size) { - std::vector ciphertext; + std::vector ciphertext; try { - ciphertext = session::config::encrypt({plaintext, len}, {key_base, 32}, domain); + ciphertext = session::config::encrypt( + std::span{reinterpret_cast(plaintext), len}, + std::span{reinterpret_cast(key_base), 32}, + domain); } catch (...) { return nullptr; } @@ -179,9 +182,12 @@ LIBSESSION_EXPORT unsigned char* config_decrypt( const char* domain, size_t* plaintext_size) { - std::vector plaintext; + std::vector plaintext; try { - plaintext = session::config::decrypt({ciphertext, clen}, {key_base, 32}, domain); + plaintext = session::config::decrypt( + std::span{reinterpret_cast(ciphertext), clen}, + std::span{reinterpret_cast(key_base), 32}, + domain); } catch (const std::exception& e) { return nullptr; } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index f526cacc3..1ede3a399 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -16,9 +16,9 @@ using namespace std::literals; namespace session::config::groups { Info::Info( - std::span ed25519_pubkey, - std::optional> ed25519_secretkey, - std::optional> dumped) : + std::span ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey, + std::optional> dumped) : id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} { init(dumped, ed25519_pubkey, ed25519_secretkey); } @@ -61,12 +61,12 @@ profile_pic Info::get_profile_pic() const { pic.url = *url; if (auto* key = data["q"].string(); key && key->size() == 32) pic.key.assign( - reinterpret_cast(key->data()), - reinterpret_cast(key->data()) + 32); + reinterpret_cast(key->data()), + reinterpret_cast(key->data()) + 32); return pic; } -void Info::set_profile_pic(std::string_view url, std::span key) { +void Info::set_profile_pic(std::string_view url, std::span key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); } @@ -256,9 +256,9 @@ LIBSESSION_C_API user_profile_pic groups_info_get_pic(const config_object* conf) /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic pic) { std::string_view url{pic.url}; - std::span key; + std::span key; if (!url.empty()) - key = {pic.key, 32}; + key = {reinterpret_cast(pic.key), 32}; return wrap_exceptions( conf, diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index 84857fca3..d7ee8dfd7 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -16,13 +16,18 @@ #include #include +#include + #include "../internal.hpp" #include "session/clock.hpp" +#include "session/crypto/ed25519.hpp" +#include "session/encrypt.hpp" #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" #include "session/hash.hpp" #include "session/multi_encrypt.hpp" +#include "session/random.hpp" #include "session/session_encrypt.hpp" #include "session/xed25519.hpp" @@ -35,23 +40,16 @@ static auto sys_time_from_ms(int64_t milliseconds_since_epoch) { } Keys::Keys( - std::span user_ed25519_secretkey, - std::span group_ed25519_pubkey, - std::optional> group_ed25519_secretkey, - std::optional> dumped, + const ed25519::PrivKeySpan& user_ed25519_secretkey, + std::span group_ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& group_ed25519_secretkey, + std::optional> dumped, Info& info, Members& members) { if (sodium_init() == -1) throw std::runtime_error{"libsodium initialization failed!"}; - if (user_ed25519_secretkey.size() != 64) - throw std::invalid_argument{"Invalid Keys construction: invalid user ed25519 secret key"}; - if (group_ed25519_pubkey.size() != 32) - throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 public key"}; - if (group_ed25519_secretkey && group_ed25519_secretkey->size() != 64) - throw std::invalid_argument{"Invalid Keys construction: invalid group ed25519 secret key"}; - init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); @@ -68,14 +66,14 @@ bool Keys::needs_dump() const { return needs_dump_; } -std::vector Keys::dump() { +std::vector Keys::dump() { auto dumped = make_dump(); needs_dump_ = false; return dumped; } -std::vector Keys::make_dump() const { +std::vector Keys::make_dump() const { oxenc::bt_dict_producer d; { auto active = d.append_list("A"); @@ -109,7 +107,7 @@ std::vector Keys::make_dump() const { return to_vector(d.view()); } -void Keys::load_dump(std::span dump) { +void Keys::load_dump(std::span dump) { oxenc::bt_dict_consumer d{dump}; if (d.skip_until("A")) { @@ -183,8 +181,8 @@ size_t Keys::size() const { return keys_.size() + !pending_key_config_.empty(); } -std::vector> Keys::group_keys() const { - std::vector> ret; +std::vector> Keys::group_keys() const { + std::vector> ret; ret.reserve(size()); if (!pending_key_config_.empty()) @@ -196,7 +194,7 @@ std::vector> Keys::group_keys() const { return ret; } -std::span Keys::group_enc_key() const { +std::span Keys::group_enc_key() const { if (!pending_key_config_.empty()) return {pending_key_.data(), 32}; if (keys_.empty()) @@ -206,50 +204,30 @@ std::span Keys::group_enc_key() const { return {key.data(), key.size()}; } -void Keys::load_admin_key(std::span seed, Info& info, Members& members) { +void Keys::load_admin_key(const ed25519::PrivKeySpan& secret, Info& info, Members& members) { if (admin()) return; - if (seed.size() == 64) - seed = seed.subspan(0, seed.size() - 32); - else if (seed.size() != 32) - throw std::invalid_argument{ - "Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"}; - - std::array pk; - sodium_cleared> sk; - crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data()); - - if (_sign_pk.has_value() && *_sign_pk != pk) + if (_sign_pk && !std::ranges::equal(*_sign_pk, secret.pubkey())) throw std::runtime_error{ "Failed to load admin key: given secret key does not match group pubkey"}; - auto seckey = to_span(sk); - set_sig_keys(seckey); - info.set_sig_keys(seckey); - members.set_sig_keys(seckey); + set_sig_keys(secret); + info.set_sig_keys(secret); + members.set_sig_keys(secret); } namespace { - std::array compute_xpk(const unsigned char* ed25519_pk) { - std::array xpk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk)) - throw std::runtime_error{ - "An error occured while attempting to convert Ed25519 pubkey to X25519; " - "is the pubkey valid?"}; - return xpk; - } - constexpr auto seed_hash_key = "SessionGroupKeySeed"sv; - const std::span enc_key_hash_key = to_span("SessionGroupKeyGen"); + const std::span enc_key_hash_key = to_span("SessionGroupKeyGen"); constexpr auto enc_key_admin_hash_key = "SessionGroupKeyAdminKey"sv; constexpr auto enc_key_member_hash_key = "SessionGroupKeyMemberKey"sv; - constexpr auto junk_seed_hash_key = "SessionGroupJunkMembers"_uc; + constexpr auto junk_seed_hash_key = "SessionGroupJunkMembers"_bytes; } // namespace -std::span Keys::rekey(Info& info, Members& members) { +std::span Keys::rekey(Info& info, Members& members) { if (!admin()) throw std::logic_error{ "Unable to issue a new group encryption key without the main group keys"}; @@ -257,10 +235,10 @@ std::span Keys::rekey(Info& info, Members& members) { // For members we calculate the outer encryption key as H(aB || A || B). But because we only // have `B` (the session id) as an x25519 pubkey, we do this in x25519 space, which means we // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. - auto group_xpk = compute_xpk(_sign_pk->data()); + auto group_xpk = ed25519::pk_to_x25519(*_sign_pk); - sodium_cleared> group_xsk; - crypto_sign_ed25519_sk_to_curve25519(group_xsk.data(), _sign_sk.data()); + auto group_xsk = ed25519::sk_to_x25519( + std::span{_sign_sk.data(), 64}); // We need quasi-randomness: full secure random would be great, except that different admins // encrypting for the same update would always create different keys, but we want it @@ -288,51 +266,34 @@ std::span Keys::rekey(Info& info, Members& members) { // member. For admins we encrypt using a 32-byte blake2b keyed hash of the group secret key // seed, just like H2, but with key "SessionGroupKeyAdminKey". - std::array h2 = seed_hash(seed_hash_key); - - std::array h1; - - crypto_generichash_blake2b_state st; + auto h2 = seed_hash(seed_hash_key); - crypto_generichash_blake2b_init( - &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); + hash::blake2b_hasher hasher{ + enc_key_hash_key, std::nullopt}; for (const auto& m : members) - hash::update_all(st, m.session_id); + hasher.update(m.session_id); auto gen = keys_.empty() ? 0 : keys_.back().generation + 1; auto gen_str = std::to_string(gen); - hash::update_all(st, gen_str, h2); + hasher.update(gen_str, h2); - crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); + auto h1 = hasher.finalize(); - std::span enc_key{h1.data(), 32}; - std::span nonce{h1.data() + 32, 24}; + std::span enc_key = + std::span{h1}.first(); + std::span nonce = + std::span{h1}.last(); oxenc::bt_dict_producer d{}; d.append("#", to_string_view(nonce)); - static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); - static_assert(crypto_aead_xchacha20poly1305_ietf_ABYTES == 16); - std::array< - unsigned char, - crypto_aead_xchacha20poly1305_ietf_KEYBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES> - encrypted; + std::array encrypted; std::string_view enc_sv = to_string_view(encrypted); // Shared key for admins auto member_k = seed_hash(enc_key_admin_hash_key); - static_assert(member_k.size() == crypto_aead_xchacha20poly1305_ietf_KEYBYTES); - crypto_aead_xchacha20poly1305_ietf_encrypt( - encrypted.data(), - nullptr, - enc_key.data(), - enc_key.size(), - nullptr, - 0, - nullptr, - nonce.data(), - member_k.data()); + encrypt::xchacha20poly1305_encrypt(encrypted, enc_key, nonce, member_k); d.append("G", gen); d.append("K", enc_sv); @@ -340,8 +301,8 @@ std::span Keys::rekey(Info& info, Members& members) { { auto member_keys = d.append_list("k"); int member_count = 0; - std::vector> member_xpk_raw; - std::vector> member_xpks; + std::vector member_xpk_raw; + std::vector> member_xpks; member_xpk_raw.reserve(members.size()); member_xpks.reserve(members.size()); for (const auto& m : members) { @@ -356,7 +317,7 @@ std::span Keys::rekey(Info& info, Members& members) { to_span(group_xsk), to_span(group_xpk), enc_key_member_hash_key, - [&](std::span enc_sv) { + [&](std::span enc_sv) { member_keys.append(enc_sv); member_count++; }, @@ -366,13 +327,13 @@ std::span Keys::rekey(Info& info, Members& members) { // Pad it out with junk entries to the next MESSAGE_KEY_MULTIPLE if (member_count % MESSAGE_KEY_MULTIPLE) { int n_junk = MESSAGE_KEY_MULTIPLE - (member_count % MESSAGE_KEY_MULTIPLE); - std::vector junk_data; + std::vector junk_data; junk_data.resize(encrypted.size() * n_junk); auto rng_seed = - hash::blake2b_key(junk_seed_hash_key, h1, _sign_sk); + hash::blake2b_key<32>(junk_seed_hash_key, h1, _sign_sk); - randombytes_buf_deterministic(junk_data.data(), junk_data.size(), rng_seed.data()); + random::fill_deterministic(junk_data, rng_seed); std::string_view junk_view = to_string_view(junk_data); while (!junk_view.empty()) { member_keys.append(junk_view.substr(0, encrypted.size())); @@ -384,7 +345,7 @@ std::span Keys::rekey(Info& info, Members& members) { // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). d.append_signature( - "~", [this](std::span to_sign) { return sign(to_sign); }); + "~", [this](std::span to_sign) { return sign(to_sign); }); // Load this key/config/gen into our pending variables pending_gen_ = gen; @@ -402,17 +363,17 @@ std::span Keys::rekey(Info& info, Members& members) { needs_dump_ = true; - return std::span{pending_key_config_.data(), pending_key_config_.size()}; + return std::span{pending_key_config_.data(), pending_key_config_.size()}; } -std::vector Keys::sign(std::span data) const { +std::vector Keys::sign(std::span data) const { auto sig = signer_(data); if (sig.size() != 64) throw std::logic_error{"Invalid signature: signing function did not return 64 bytes"}; return sig; } -std::vector Keys::key_supplement(const std::vector& sids) const { +std::vector Keys::key_supplement(const std::vector& sids) const { if (!admin()) throw std::logic_error{ "Unable to issue supplemental group encryption keys without the main group keys"}; @@ -424,10 +385,9 @@ std::vector Keys::key_supplement(const std::vector& // For members we calculate the outer encryption key as H(aB || A || B). But because we only // have `B` (the session id) as an x25519 pubkey, we do this in x25519 space, which means we // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. - auto group_xpk = compute_xpk(_sign_pk->data()); - - sodium_cleared> group_xsk; - crypto_sign_ed25519_sk_to_curve25519(group_xsk.data(), _sign_sk.data()); + auto group_xpk = ed25519::pk_to_x25519(*_sign_pk); + auto group_xsk = ed25519::sk_to_x25519( + std::span{_sign_sk.data(), 64}); // We need quasi-randomness here for the nonce: full secure random would be great, except that // different admins encrypting for the same update would always create different keys, but we @@ -457,22 +417,15 @@ std::vector Keys::key_supplement(const std::vector& supp_keys = std::move(supp).str(); } - std::array h1; - - crypto_generichash_blake2b_state st; - - crypto_generichash_blake2b_init( - &st, enc_key_hash_key.data(), enc_key_hash_key.size(), h1.size()); - + hash::blake2b_hasher nonce_hasher{ + enc_key_hash_key, std::nullopt}; for (const auto& sid : sids) - hash::update_all(st, sid); - - std::array h2 = seed_hash(seed_hash_key); - hash::update_all(st, supp_keys, h2); + nonce_hasher.update(sid); - crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); + auto h2 = seed_hash(seed_hash_key); + nonce_hasher.update(supp_keys, h2); - std::span nonce{h1.data(), h1.size()}; + auto nonce = nonce_hasher.finalize(); oxenc::bt_dict_producer d{}; @@ -480,13 +433,13 @@ std::vector Keys::key_supplement(const std::vector& { auto list = d.append_list("+"); - std::vector encrypted; + std::vector encrypted; encrypted.resize(supp_keys.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); size_t member_count = 0; - std::vector> member_xpk_raw; - std::vector> member_xpks; + std::vector member_xpk_raw; + std::vector> member_xpks; member_xpk_raw.reserve(sids.size()); member_xpks.reserve(sids.size()); for (const auto& sid : sids) { @@ -501,7 +454,7 @@ std::vector Keys::key_supplement(const std::vector& to_span(group_xsk), to_span(group_xpk), enc_key_member_hash_key, - [&](std::span encrypted) { + [&](std::span encrypted) { list.append(encrypted); member_count++; }, @@ -518,46 +471,44 @@ std::vector Keys::key_supplement(const std::vector& // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). d.append_signature( - "~", [this](std::span to_sign) { return sign(to_sign); }); + "~", [this](std::span to_sign) { return sign(to_sign); }); return to_vector(d.view()); } // Blinding factor for subaccounts: H(sessionid || groupid) mod L, where H is 64-byte blake2b, using // a hash key derived from the group's seed. -std::array Keys::subaccount_blind_factor( - const std::array& session_xpk) const { +b32 Keys::subaccount_blind_factor( + std::span session_xpk) const { auto mask = seed_hash("SessionGroupSubaccountMask"); - static_assert(mask.size() == crypto_generichash_blake2b_KEYBYTES); auto h = hash::blake2b_key<64>( mask, - std::array{'\x05'}, + std::byte{0x05}, session_xpk, - std::array{'\x03'}, + std::byte{0x03}, *_sign_pk); - std::array out; - crypto_core_ed25519_scalar_reduce(out.data(), h.data()); - return out; + return ed25519::scalar_reduce(h); } namespace { // These constants are defined and explains in more detail in oxen-storage-server - constexpr unsigned char SUBACC_FLAG_READ = 0b0001; - constexpr unsigned char SUBACC_FLAG_WRITE = 0b0010; - constexpr unsigned char SUBACC_FLAG_DEL = 0b0100; - constexpr unsigned char SUBACC_FLAG_ANY_PREFIX = 0b1000; - - constexpr unsigned char subacc_flags(bool write, bool del) { - return SUBACC_FLAG_READ | (write ? SUBACC_FLAG_WRITE : 0) | (del ? SUBACC_FLAG_DEL : 0); + constexpr std::byte SUBACC_FLAG_READ{0b0001}; + constexpr std::byte SUBACC_FLAG_WRITE{0b0010}; + constexpr std::byte SUBACC_FLAG_DEL{0b0100}; + constexpr std::byte SUBACC_FLAG_ANY_PREFIX{0b1000}; + + constexpr std::byte subacc_flags(bool write, bool del) { + return SUBACC_FLAG_READ | (write ? SUBACC_FLAG_WRITE : std::byte{0}) | + (del ? SUBACC_FLAG_DEL : std::byte{0}); } } // namespace -std::vector Keys::swarm_make_subaccount( +std::vector Keys::swarm_make_subaccount( std::string_view session_id, bool write, bool del) const { if (!admin()) throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; @@ -589,30 +540,30 @@ std::vector Keys::swarm_make_subaccount( auto T = xed25519::pubkey(X); // kT is the user's Ed25519 blinded pubkey: - std::array kT; - - if (0 != crypto_scalarmult_ed25519_noclamp(kT.data(), k.data(), T.data())) - throw std::runtime_error{"scalarmult failed: perhaps an invalid session id?"}; + auto kT = ed25519::scalarmult_noclamp(k, T); - std::vector out; + std::vector out; out.resize(4 + 32 + 64); - out[0] = 0x03; // network prefix + out[0] = std::byte{0x03}; // network prefix out[1] = subacc_flags(write, del); // permission flags - out[2] = 0; // reserved 1 - out[3] = 0; // reserved 2 + out[2] = std::byte{0}; // reserved 1 + out[3] = std::byte{0}; // reserved 2 // The next 32 bytes are k (NOT kT; the user can go make kT themselves): std::memcpy(&out[4], k.data(), k.size()); // And then finally, we append a group signature of: p || f || 0 || 0 || kT - std::array to_sign; + std::array to_sign; std::memcpy(&to_sign[0], out.data(), 4); // first 4 bytes are the same as out std::memcpy(&to_sign[4], kT.data(), 32); // but then we have kT instead of k - crypto_sign_ed25519_detached(&out[36], nullptr, to_sign.data(), to_sign.size(), c.data()); + ed25519::sign( + std::span{out.data() + 36, 64}, + ed25519::PrivKeySpan{std::span{c.data(), 64}}, + to_sign); return out; } -std::vector Keys::swarm_subaccount_token( +std::vector Keys::swarm_subaccount_token( std::string_view session_id, bool write, bool del) const { if (!admin()) throw std::logic_error{"Cannot make subaccount signature: admin keys required"}; @@ -625,20 +576,21 @@ std::vector Keys::swarm_subaccount_token( // T = |S| auto T = xed25519::pubkey(X); - std::vector out; + auto kT = ed25519::scalarmult_noclamp(k, T); + + std::vector out; out.resize(4 + 32); - out[0] = 0x03; // network prefix + out[0] = std::byte{0x03}; // network prefix out[1] = subacc_flags(write, del); // permission flags - out[2] = 0; // reserved 1 - out[3] = 0; // reserved 2 - if (0 != crypto_scalarmult_ed25519_noclamp(&out[4], k.data(), T.data())) - throw std::runtime_error{"scalarmult failed: perhaps an invalid session id?"}; + out[2] = std::byte{0}; // reserved 1 + out[3] = std::byte{0}; // reserved 2 + std::memcpy(&out[4], kT.data(), 32); return out; } Keys::swarm_auth Keys::swarm_subaccount_sign( - std::span msg, - std::span sign_val, + std::span msg, + std::span sign_val, bool binary) const { if (sign_val.size() != 100) throw std::logic_error{"Invalid signing value: size is wrong"}; @@ -651,7 +603,7 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // (see above for variable/crypto notation) - std::span k = sign_val.subspan(4, 32); + auto k = sign_val.subspan<4, 32>(); // our token is the first 4 bytes of `sign_val` (flags, etc.), followed by kT which we have to // compute: @@ -659,30 +611,27 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( std::memcpy(token.data(), sign_val.data(), 4); // T = |S|, i.e. we have to clear the sign bit from our pubkey - std::array T; - crypto_sign_ed25519_sk_to_pk(T.data(), user_ed25519_sk.data()); - bool neg = T[31] & 0x80; - T[31] &= 0x7f; - if (0 != crypto_scalarmult_ed25519_noclamp(to_unsigned(token.data() + 4), k.data(), T.data())) - throw std::runtime_error{"scalarmult failed: perhaps an invalid session id or seed?"}; + ed25519::PrivKeySpan user_sk{std::span{user_ed25519_sk.data(), 64}}; + b32 T; + std::ranges::copy(user_sk.pubkey(), T.begin()); + bool neg = (T[31] & std::byte{0x80}) != std::byte{0}; + T[31] &= std::byte{0x7f}; - // token is now set: flags || kT - std::span kT{to_unsigned(token.data() + 4), 32}; + auto kT = ed25519::scalarmult_noclamp(k, T); + std::memcpy(token.data() + 4, kT.data(), 32); // sub_sig is just the admin's signature, sitting at the end of sign_val (after 4f || k): sub_sig = to_string_view(sign_val.subspan(36)); // Our signing private scalar is kt, where t = ±s according to whether we had to negate S to // make T - std::array s, s_neg; - crypto_sign_ed25519_sk_to_curve25519(s.data(), user_ed25519_sk.data()); - crypto_core_ed25519_scalar_negate(s_neg.data(), s.data()); + auto s = ed25519::sk_to_x25519(user_sk); + auto s_neg = ed25519::scalar_negate(s); xed25519::constant_time_conditional_assign(s, s_neg, neg); auto& t = s; - std::array kt; - crypto_core_ed25519_scalar_mul(kt.data(), k.data(), t.data()); + auto kt = ed25519::scalar_mul(k, t); // We now have kt, kT, our privkey/public. (Note that kt is a scalar, not a seed). @@ -706,37 +655,28 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // // (using the standard Ed25519 SHA-512 here for H) - constexpr std::array seed_hash_key = { - 'S', 'u', 'b', 'a', 'c', 'c', 'o', 'u', 'n', 't', 'S', 'e', 'e', 'd'}; - constexpr std::array r_hash_key = { - 'S', 'u', 'b', 'a', 'c', 'c', 'o', 'u', 'n', 't', 'S', 'i', 'g'}; - std::array hseed; - hash::blake2b_key( - hseed, seed_hash_key, std::span{user_ed25519_sk.data(), 32}); + constexpr auto subacc_seed_key = "SubaccountSeed"_bytes; + constexpr auto subacc_sig_key = "SubaccountSig"_bytes; + b32 hseed; + hash::blake2b_key(hseed, subacc_seed_key, user_sk.seed()); - std::array tmp; - hash::blake2b_key(tmp, r_hash_key, hseed, kT, msg); + b64 tmp; + hash::blake2b_key(tmp, subacc_sig_key, hseed, kT, msg); - std::array r; - crypto_core_ed25519_scalar_reduce(r.data(), tmp.data()); + auto r = ed25519::scalar_reduce(tmp); sig.resize(64); - unsigned char* R = to_unsigned(sig.data()); - unsigned char* S = to_unsigned(sig.data() + 32); + auto R = std::span{to_bytes(sig.data()), 32}; + auto S = std::span{to_bytes(sig.data()) + 32, 32}; // R = rB - crypto_scalarmult_ed25519_base_noclamp(R, r.data()); + ed25519::scalarmult_base_noclamp(R, r); // Compute S = r + H(R || A || M) a mod L: (with A = kT, a = kt) - crypto_hash_sha512_state shast; - crypto_hash_sha512_init(&shast); - crypto_hash_sha512_update(&shast, R, 32); - crypto_hash_sha512_update(&shast, kT.data(), kT.size()); // A = pubkey, that is, kT - crypto_hash_sha512_update(&shast, msg.data(), msg.size()); - std::array hram; - crypto_hash_sha512_final(&shast, hram.data()); // S = H(R||A||M) - crypto_core_ed25519_scalar_reduce(S, hram.data()); // S %= L - crypto_core_ed25519_scalar_mul(S, S, kt.data()); // S *= a - crypto_core_ed25519_scalar_add(S, S, r.data()); // S += r + b64 hram; + hash::sha512(hram, R, kT, msg); + ed25519::scalar_reduce(S, hram); // S = H(R||A||M) % L + ed25519::scalar_mul(S, S, kt); // S *= a + ed25519::scalar_add(S, S, r); // S += r // sig is now set to the desired R || S, with S = r + H(R || A || M)a (all mod L) @@ -750,12 +690,12 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( } bool Keys::swarm_verify_subaccount( - std::span sign_val, bool write, bool del) const { + std::span sign_val, bool write, bool del) const { if (!_sign_pk) return false; return swarm_verify_subaccount( "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()), - std::span{user_ed25519_sk.data(), user_ed25519_sk.size()}, + ed25519::PrivKeySpan::from(user_ed25519_sk), sign_val, write, del); @@ -763,8 +703,8 @@ bool Keys::swarm_verify_subaccount( bool Keys::swarm_verify_subaccount( std::string group_id, - std::span user_ed_sk, - std::span sign_val, + const ed25519::PrivKeySpan& session_ed25519_secretkey, + std::span sign_val, bool write, bool del) { auto group_pk = session_id_pk(group_id, "03"); @@ -772,46 +712,44 @@ bool Keys::swarm_verify_subaccount( if (sign_val.size() != 100) return false; - std::span prefix = sign_val.subspan(0, 4); - if (prefix[0] != 0x03 && !(prefix[1] & SUBACC_FLAG_ANY_PREFIX)) + auto prefix = sign_val.subspan<0, 4>(); + if (prefix[0] != std::byte{0x03} && + (prefix[1] & SUBACC_FLAG_ANY_PREFIX) == std::byte{0}) return false; // require either 03 prefix match, or the "any prefix" flag - if (!(prefix[1] & SUBACC_FLAG_READ)) + if ((prefix[1] & SUBACC_FLAG_READ) == std::byte{0}) return false; // missing the read flag - if (write && !(prefix[1] & SUBACC_FLAG_WRITE)) + if (write && (prefix[1] & SUBACC_FLAG_WRITE) == std::byte{0}) return false; // we require write, but it isn't set - // - if (del && !(prefix[1] & SUBACC_FLAG_DEL)) + + if (del && (prefix[1] & SUBACC_FLAG_DEL) == std::byte{0}) return false; // we require delete, but it isn't set - std::span k = sign_val.subspan(4, 32); - std::span sig = sign_val.subspan(36); + auto k = sign_val.subspan<4, 32>(); + auto sig = sign_val.subspan<36, 64>(); // T = |S|, i.e. we have to clear the sign bit from our pubkey - std::array T; - crypto_sign_ed25519_sk_to_pk(T.data(), user_ed_sk.data()); - T[31] &= 0x7f; + b32 T; + std::ranges::copy(session_ed25519_secretkey.pubkey(), T.begin()); + T[31] &= std::byte{0x7f}; // Compute kT, then reconstruct the `flags || kT` value the admin should have provided a // signature for - std::array kT; - if (0 != crypto_scalarmult_ed25519_noclamp(kT.data(), k.data(), T.data())) - throw std::runtime_error{"scalarmult failed: perhaps an invalid session id or seed?"}; + auto kT = ed25519::scalarmult_noclamp(k, T); - std::array to_verify; + std::array to_verify; std::memcpy(&to_verify[0], sign_val.data(), 4); // prefix, flags, 2x future use bytes std::memcpy(&to_verify[4], kT.data(), 32); // Verify it! - return 0 == crypto_sign_ed25519_verify_detached( - sig.data(), to_verify.data(), to_verify.size(), group_pk.data()); + return ed25519::verify(sig, group_pk, to_verify); } -std::optional> Keys::pending_config() const { +std::optional> Keys::pending_config() const { if (pending_key_config_.empty()) return std::nullopt; - return std::span{pending_key_config_.data(), pending_key_config_.size()}; + return std::span{pending_key_config_.data(), pending_key_config_.size()}; } void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { @@ -856,39 +794,17 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { // Returns true (after writing to `out`) if decryption succeeds, false if it fails. namespace { bool try_decrypting( - unsigned char* out, - std::span encrypted, - std::span nonce, - std::span key) { - assert(encrypted.size() >= crypto_aead_xchacha20poly1305_ietf_ABYTES); - assert(nonce.size() == crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - assert(key.size() == crypto_aead_xchacha20poly1305_ietf_KEYBYTES); - - return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - out, - nullptr, - nullptr, - encrypted.data(), - encrypted.size(), - nullptr, - 0, - nonce.data(), - key.data()); - } - bool try_decrypting( - unsigned char* out, - std::span encrypted, - std::span nonce, - - const std::array& key) { - return try_decrypting( - out, encrypted, nonce, std::span{key.data(), key.size()}); + std::span out, + std::span encrypted, + std::span nonce, + std::span key) { + return encrypt::xchacha20poly1305_decrypt(out, encrypted, nonce, key); } } // namespace bool Keys::load_key_message( std::string_view hash, - std::span data, + std::span data, int64_t timestamp_ms, Info& info, Members& members) { @@ -898,22 +814,26 @@ bool Keys::load_key_message( if (!_sign_pk || !verifier_) throw std::logic_error{"Group pubkey is not set; unable to load config message"}; - auto group_xpk = compute_xpk(_sign_pk->data()); + auto group_xpk = ed25519::pk_to_x25519(*_sign_pk); if (!d.skip_until("#")) throw config_value_error{"Key message has no nonce"}; - auto nonce = to_span(d.consume_string_view()); + auto nonce_dyn = d.consume_span(); + if (nonce_dyn.size() != encrypt::XCHACHA20_NONCEBYTES) + throw config_value_error{"Key message has invalid nonce size"}; + auto nonce = nonce_dyn.first(); sodium_vector new_keys; std::optional max_gen; // If set then associate the message with this generation // value, even if we didn't find a key for us. - sodium_cleared> member_dec_key; - sodium_cleared> member_xsk; - std::array member_xpk; + sodium_cleared member_dec_key; + sodium_cleared member_xsk; + b32 member_xpk; if (!admin()) { - crypto_sign_ed25519_sk_to_curve25519(member_xsk.data(), user_ed25519_sk.data()); - member_xpk = compute_xpk(user_ed25519_sk.data() + 32); + ed25519::PrivKeySpan user_sk{std::span{user_ed25519_sk.data(), 64}}; + member_xsk = ed25519::sk_to_x25519(user_sk); + member_xpk = ed25519::pk_to_x25519(user_sk.pubkey()); } if (d.skip_until("+")) { @@ -922,7 +842,7 @@ bool Keys::load_key_message( int member_key_pos = -1; - auto next_ciphertext = [&]() -> std::optional> { + auto next_ciphertext = [&]() -> std::optional> { while (!supp.is_finished()) { member_key_pos++; auto encrypted = to_span(supp.consume_string_view()); @@ -1013,7 +933,7 @@ bool Keys::load_key_message( if (admin()) { auto k = seed_hash(enc_key_admin_hash_key); - if (!try_decrypting(new_key.key.data(), admin_key, nonce, k)) + if (!try_decrypting(new_key.key, admin_key, nonce, k)) throw config_value_error{"Failed to decrypt admin key from key message"}; found_key = true; @@ -1027,7 +947,7 @@ bool Keys::load_key_message( auto key_list = d.consume_list_consumer(); int member_key_pos = -1; - auto next_ciphertext = [&]() -> std::optional> { + auto next_ciphertext = [&]() -> std::optional> { while (!key_list.is_finished()) { member_key_pos++; auto member_key = to_span(key_list.consume_string_view()); @@ -1164,25 +1084,25 @@ bool Keys::needs_rekey() const { return last_it->generation == second_it->generation; } -std::optional> Keys::pending_key() const { +std::optional> Keys::pending_key() const { if (!pending_key_config_.empty()) - return std::span{pending_key_.data(), pending_key_.size()}; + return std::span{pending_key_.data(), pending_key_.size()}; return std::nullopt; } static constexpr size_t ENCRYPT_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; -std::vector Keys::encrypt_message( - std::span plaintext, bool compress, size_t padding) const { +std::vector Keys::encrypt_message( + std::span plaintext, bool compress, size_t padding) const { assert(_sign_pk); - std::vector ciphertext = encrypt_for_group( - user_ed25519_sk, *_sign_pk, group_enc_key(), plaintext, compress, padding); + std::vector ciphertext = encrypt_for_group( + ed25519::PrivKeySpan::from(user_ed25519_sk), *_sign_pk, group_enc_key(), plaintext, compress, padding); return ciphertext; } -std::pair> Keys::decrypt_message( - std::span ciphertext) const { +std::pair> Keys::decrypt_message( + std::span ciphertext) const { assert(_sign_pk); // @@ -1192,7 +1112,7 @@ std::pair> Keys::decrypt_message( bool decrypt_success = false; if (auto pending = pending_key(); pending) { try { - std::span> key_list = {&(*pending), 1}; + std::span> key_list = {&(*pending), 1}; decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); decrypt_success = true; } catch (const std::exception&) { @@ -1202,8 +1122,8 @@ std::pair> Keys::decrypt_message( if (!decrypt_success) { for (auto& k : keys_) { try { - std::span key = {k.key.data(), k.key.size()}; - std::span> key_list = {&key, 1}; + std::span key = {k.key.data(), k.key.size()}; + std::span> key_list = {&key, 1}; decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); decrypt_success = true; break; @@ -1217,7 +1137,7 @@ std::pair> Keys::decrypt_message( "unable to decrypt ciphertext with any current group keys; tried {}", keys_.size() + (pending_key() ? 1 : 0))}; - std::pair> result; + std::pair> result; result.first = std::move(decrypt.session_id); result.second = std::move(decrypt.plaintext); return result; @@ -1287,14 +1207,12 @@ LIBSESSION_C_API int groups_keys_init( assert(user_ed25519_secretkey && group_ed25519_pubkey && cinfo && cmembers); - std::span user_sk{user_ed25519_secretkey, 64}; - std::span group_pk{group_ed25519_pubkey, 32}; - std::optional> group_sk; - if (group_ed25519_secretkey) - group_sk.emplace(group_ed25519_secretkey, 64); - std::optional> dumped; + ed25519::PrivKeySpan user_sk{user_ed25519_secretkey, 64}; + auto group_pk = to_byte_span<32>(group_ed25519_pubkey); + ed25519::OptionalPrivKeySpan group_sk{group_ed25519_secretkey, group_ed25519_secretkey ? 64u : 0u}; + std::optional> dumped; if (dump && dumplen) - dumped.emplace(dump, dumplen); + dumped.emplace(to_byte_span(dump, dumplen)); auto& info = *unbox(cinfo); auto& members = *unbox(cmembers); @@ -1334,7 +1252,7 @@ LIBSESSION_C_API const unsigned char* groups_keys_get_key(const config_group_key auto keys = unbox(conf).group_keys(); if (N >= keys.size()) return nullptr; - return keys[N].data(); + return to_unsigned(keys[N].data()); } LIBSESSION_C_API size_t groups_keys_get_keys( @@ -1346,7 +1264,7 @@ LIBSESSION_C_API size_t groups_keys_get_keys( for (size_t index = clamped_offset; index < keys.size() && result < dest_size; index++) { const auto& src_key = keys[index]; span_u8* dest_key = dest + result++; - dest_key->data = const_cast(src_key.data()); + dest_key->data = const_cast(to_unsigned(src_key.data())); dest_key->size = src_key.size(); } } @@ -1357,7 +1275,7 @@ LIBSESSION_C_API const span_u8 groups_keys_group_enc_key(const config_group_keys span_u8 result = {}; try { auto key = unbox(conf).group_enc_key(); - result.data = const_cast(key.data()); + result.data = const_cast(to_unsigned(key.data())); result.size = key.size(); assert(result.size == 32); } catch (const std::exception& e) { @@ -1378,7 +1296,7 @@ LIBSESSION_C_API bool groups_keys_load_admin_key( conf, [&] { unbox(conf).load_admin_key( - std::span{secret, 32}, + ed25519::PrivKeySpan{secret, 32}, *unbox(info), *unbox(members)); return true; @@ -1394,14 +1312,14 @@ LIBSESSION_C_API bool groups_keys_rekey( size_t* outlen) { assert(info && members); auto& keys = unbox(conf); - std::span to_push; + std::span to_push; return wrap_exceptions( conf, [&] { to_push = keys.rekey(*unbox(info), *unbox(members)); if (out && outlen) { - *out = to_push.data(); + *out = to_unsigned(to_push.data()); *outlen = to_push.size(); } return true; @@ -1413,7 +1331,7 @@ LIBSESSION_C_API bool groups_keys_pending_config( const config_group_keys* conf, const unsigned char** out, size_t* outlen) { assert(out && outlen); if (auto pending = unbox(conf).pending_config()) { - *out = pending->data(); + *out = to_unsigned(pending->data()); *outlen = pending->size(); return true; } @@ -1434,7 +1352,7 @@ LIBSESSION_C_API bool groups_keys_load_message( [&] { unbox(conf).load_key_message( msg_hash, - std::span{data, datalen}, + to_byte_span(data, datalen), timestamp_ms, *unbox(info), *unbox(members)); @@ -1472,10 +1390,10 @@ LIBSESSION_C_API void groups_keys_encrypt_message( size_t* ciphertext_len) { assert(plaintext_in && ciphertext_out && ciphertext_len); - std::vector ciphertext; + std::vector ciphertext; try { ciphertext = unbox(conf).encrypt_message( - std::span{plaintext_in, plaintext_len}); + to_byte_span(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(std::malloc(ciphertext.size())); std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); *ciphertext_len = ciphertext.size(); @@ -1498,7 +1416,7 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( conf, [&] { auto [sid, plaintext] = unbox(conf).decrypt_message( - std::span{ciphertext_in, ciphertext_len}); + to_byte_span(ciphertext_in, ciphertext_len)); std::memcpy(session_id, sid.c_str(), sid.size() + 1); *plaintext_out = static_cast(std::malloc(plaintext.size())); std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); @@ -1568,8 +1486,8 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( try { return groups::Keys::swarm_verify_subaccount( group_id, - std::span{session_ed25519_secretkey, 64}, - std::span{signing_value, 100}, + ed25519::PrivKeySpan{session_ed25519_secretkey, 64}, + to_byte_span(signing_value, 100), write, del); } catch (...) { @@ -1584,8 +1502,8 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( try { return groups::Keys::swarm_verify_subaccount( group_id, - std::span{session_ed25519_secretkey, 64}, - std::span{signing_value, 100}); + ed25519::PrivKeySpan{session_ed25519_secretkey, 64}, + to_byte_span(signing_value, 100)); } catch (...) { return false; } @@ -1605,8 +1523,8 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - std::span{msg, msg_len}, - std::span{signing_value, 100}); + to_byte_span(msg, msg_len), + to_byte_span(signing_value, 100)); assert(auth.subaccount.size() == 48); assert(auth.subaccount_sig.size() == 88); assert(auth.signature.size() == 88); @@ -1635,8 +1553,8 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - std::span{msg, msg_len}, - std::span{signing_value, 100}, + to_byte_span(msg, msg_len), + to_byte_span(signing_value, 100), true); assert(auth.subaccount.size() == 36); assert(auth.subaccount_sig.size() == 64); diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 5d40d5fea..bd1c086b5 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -8,9 +8,9 @@ namespace session::config::groups { Members::Members( - std::span ed25519_pubkey, - std::optional> ed25519_secretkey, - std::optional> dumped) { + std::span ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, ed25519_pubkey, ed25519_secretkey); } @@ -186,7 +186,9 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { assert(std::strlen(m.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); if (std::strlen(m.profile_pic.url)) { profile_picture.url = m.profile_pic.url; - profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32); + profile_picture.key.assign( + reinterpret_cast(m.profile_pic.key), + reinterpret_cast(m.profile_pic.key) + 32); } profile_updated = to_sys_seconds(m.profile_updated); admin = m.admin; diff --git a/src/config/internal.cpp b/src/config/internal.cpp index d386dad31..f7c539356 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -57,9 +57,9 @@ std::string session_id_to_bytes(std::string_view session_id, std::string_view pr return oxenc::from_hex(session_id); } -std::array session_id_pk(std::string_view session_id, std::string_view prefix) { +b32 session_id_pk(std::string_view session_id, std::string_view prefix) { check_session_id(session_id, prefix); - std::array pk; + b32 pk; session_id.remove_prefix(2); oxenc::from_hex(session_id.begin(), session_id.end(), pk.begin()); return pk; @@ -72,8 +72,8 @@ void check_encoded_pubkey(std::string_view pk) { throw std::invalid_argument{"Invalid encoded pubkey: expected hex, base32z or base64"}; } -std::vector decode_pubkey(std::string_view pk) { - std::vector pubkey; +std::vector decode_pubkey(std::string_view pk) { + std::vector pubkey; pubkey.reserve(32); if (pk.size() == 64 && oxenc::is_hex(pk)) oxenc::from_hex(pk.begin(), pk.end(), std::back_inserter(pubkey)); @@ -187,13 +187,11 @@ std::string_view sv_or_empty(const session::config::dict& d, const char* key) { return ""sv; } -std::optional> maybe_vector( +std::optional> maybe_vector( const session::config::dict& d, const char* key) { - std::optional> result; + std::optional> result; if (auto* s = maybe_scalar(d, key)) - result.emplace( - reinterpret_cast(s->data()), - reinterpret_cast(s->data()) + s->size()); + result = to_vector(*s); return result; } diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 33c503a6e..828ffdf63 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -64,10 +64,10 @@ template size_t dumplen, char* error) { assert(ed25519_secretkey_bytes); - std::span ed25519_secretkey{ed25519_secretkey_bytes, 64}; - std::optional> dump; + ed25519::PrivKeySpan ed25519_secretkey{ed25519_secretkey_bytes, 64}; + std::optional> dump; if (dumpstr && dumplen) - dump.emplace(dumpstr, dumplen); + dump.emplace(reinterpret_cast(dumpstr), dumplen); return c_wrapper_init_generic(conf, error, ed25519_secretkey, dump); } @@ -82,13 +82,13 @@ template assert(ed25519_pubkey_bytes); - std::span ed25519_pubkey{ed25519_pubkey_bytes, 32}; - std::optional> ed25519_secretkey; - if (ed25519_secretkey_bytes) - ed25519_secretkey.emplace(ed25519_secretkey_bytes, 64); - std::optional> dump; + std::span ed25519_pubkey{ + reinterpret_cast(ed25519_pubkey_bytes), 32}; + ed25519::OptionalPrivKeySpan ed25519_secretkey{ + ed25519_secretkey_bytes, ed25519_secretkey_bytes ? 64u : 0u}; + std::optional> dump; if (dump_bytes && dumplen) - dump.emplace(dump_bytes, dumplen); + dump.emplace(reinterpret_cast(dump_bytes), dumplen); return c_wrapper_init_generic(conf, error, ed25519_pubkey, ed25519_secretkey, dump); } @@ -149,7 +149,7 @@ std::string session_id_to_bytes(std::string_view session_id, std::string_view pr // Checks the session_id (throwing if invalid) then returns it as bytes, omitting the 05 (or // whatever) prefix, which is a pubkey (x25519 for 05 session_ids, ed25519 for other prefixes). -std::array session_id_pk( +std::array session_id_pk( std::string_view session_id, std::string_view prefix = "05"); // Validates a community pubkey; we accept it in hex, base32z, or base64 (padded or unpadded). @@ -158,7 +158,7 @@ void check_encoded_pubkey(std::string_view pk); // Takes a 32-byte pubkey value encoded as hex, base32z, or base64 and returns the decoded 32 bytes. // Throws if invalid. -std::vector decode_pubkey(std::string_view pk); +std::vector decode_pubkey(std::string_view pk); // Modifies a string to be (ascii) lowercase. void make_lc(std::string& s); @@ -208,9 +208,9 @@ std::optional maybe_sv(const session::config::dict& d, const c // string view is only valid as long as the dict stays unchanged. std::string_view sv_or_empty(const session::config::dict& d, const char* key); -// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not +// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not // string) -std::optional> maybe_vector( +std::optional> maybe_vector( const session::config::dict& d, const char* key); /// Sets a value to 1 if true, removes it if false. diff --git a/src/config/local.cpp b/src/config/local.cpp index f95c2bb69..4f8c79edf 100644 --- a/src/config/local.cpp +++ b/src/config/local.cpp @@ -9,8 +9,8 @@ using namespace session::config; Local::Local( - std::span ed25519_secretkey, - std::optional> dumped) { + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, std::nullopt, std::nullopt); load_key(ed25519_secretkey); } diff --git a/src/config/pro.cpp b/src/config/pro.cpp index 4a53871e4..9c32b8bf8 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -20,17 +20,17 @@ bool ProConfig::load(const dict& root) { if (!p) return false; - std::optional> maybe_rotating_seed = maybe_vector(root, "r"); + std::optional> maybe_rotating_seed = maybe_vector(root, "r"); if (!maybe_rotating_seed || maybe_rotating_seed->size() != crypto_sign_ed25519_SEEDBYTES) return false; // NOTE: Load into the proof object { std::optional version = maybe_int(*p, "@"); - std::optional> maybe_gen_index_hash = maybe_vector(*p, "g"); + std::optional> maybe_gen_index_hash = maybe_vector(*p, "g"); std::optional> maybe_expiry_unix_ts_ms = maybe_ts_ms(*p, "e"); - std::optional> maybe_sig = maybe_vector(*p, "s"); + std::optional> maybe_sig = maybe_vector(*p, "s"); if (!version) return false; @@ -53,7 +53,9 @@ bool ProConfig::load(const dict& root) { // Derive the rotating public key from the seed and populate the proof's pubkey and the outer // private key crypto_sign_ed25519_seed_keypair( - proof.rotating_pubkey.data(), rotating_privkey.data(), maybe_rotating_seed->data()); + to_unsigned(proof.rotating_pubkey.data()), + to_unsigned(rotating_privkey.data()), + to_unsigned(maybe_rotating_seed->data())); return true; } diff --git a/src/config/protos.cpp b/src/config/protos.cpp index bfd5d7af1..311dd0e1c 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include + #include #include #include @@ -10,6 +13,7 @@ #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" #include "session/session_encrypt.hpp" +#include "session/util.hpp" namespace session::config::protos { @@ -33,24 +37,12 @@ namespace { } // namespace -std::vector wrap_config( - std::span ed25519_sk, - std::span data, +std::vector wrap_config( + const ed25519::PrivKeySpan& ed25519_sk, + std::span data, int64_t seqno, config::Namespace t) { - std::array tmp_sk; - if (ed25519_sk.size() == 32) { - std::array ignore_pk; - crypto_sign_ed25519_seed_keypair(ignore_pk.data(), tmp_sk.data(), ed25519_sk.data()); - ed25519_sk = {tmp_sk.data(), 64}; - } else if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "Error: ed25519_sk is not the expected 64-byte Ed25519 secret key"}; - - std::array my_xpk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(my_xpk.data(), ed25519_sk.data() + 32)) - throw std::invalid_argument{ - "Failed to convert Ed25519 pubkey to X25519; invalid secret key?"}; + auto my_xpk = ed25519::pk_to_x25519(ed25519_sk.pubkey()); if (static_cast(t) > 5) throw std::invalid_argument{"Error: received invalid outgoing SharedConfigMessage type"}; @@ -91,7 +83,7 @@ std::vector wrap_config( // derived from our private key, but old Session clients expect this. // NOTE: This is dumb. auto enc_shared_conf = encrypt_for_recipient_deterministic( - ed25519_sk, {my_xpk.data(), my_xpk.size()}, to_span(shared_conf)); + ed25519_sk, my_xpk, to_span(shared_conf)); // This is the point in session client code where this value got base64-encoded, passed to // another function, which then base64-decoded that value to put into the envelope. We're going @@ -121,24 +113,16 @@ std::vector wrap_config( msg.set_type(WebSocketProtos::WebSocketMessage_Type_REQUEST); *msg.mutable_request() = webreq; - return to_vector(msg.SerializeAsString()); + return to_vector(msg.SerializeAsString()); } -std::vector unwrap_config( - std::span ed25519_sk, - std::span data, +std::vector unwrap_config( + const ed25519::PrivKeySpan& ed25519_sk, + std::span data, config::Namespace ns) { // Hurray, we get to undo everything from the above! - std::array tmp_sk; - if (ed25519_sk.size() == 32) { - std::array ignore_pk; - crypto_sign_ed25519_seed_keypair(ignore_pk.data(), tmp_sk.data(), ed25519_sk.data()); - ed25519_sk = {tmp_sk.data(), 64}; - } else if (ed25519_sk.size() != 64) - throw std::invalid_argument{ - "Error: ed25519_sk is not the expected 64-byte Ed25519 secret key"}; - auto ed25519_pk = ed25519_sk.subspan(32); + auto ed25519_pk = ed25519_sk.pubkey(); WebSocketProtos::WebSocketMessage req{}; @@ -152,19 +136,19 @@ std::vector unwrap_config( if (!envelope.ParseFromString(req.request().body())) throw std::runtime_error{"Failed to parse Envelope"}; - auto [content, sender] = decrypt_incoming(ed25519_sk, to_span(envelope.content())); + auto [content, sender] = decrypt_incoming(ed25519_sk, to_span(envelope.content())); if (to_string_view(sender) != to_string_view(ed25519_pk)) throw std::runtime_error{"Incoming config data was not from us; ignoring"}; if (content.empty()) throw std::runtime_error{"Incoming config data decrypted to empty string"}; - if (!(content.back() == 0x00 || content.back() == 0x80)) + if (!(content.back() == std::byte{0x00} || content.back() == std::byte{0x80})) throw std::runtime_error{"Incoming config data doesn't have required padding"}; if (auto it = std::find_if( - content.rbegin(), content.rend(), [](unsigned char c) { return c != 0; }); - it != content.rend() && *it == 0x80) + content.rbegin(), content.rend(), [](std::byte c) { return c != std::byte{0}; }); + it != content.rend() && *it == std::byte{0x80}) content.resize(content.size() - std::distance(content.rbegin(), it) - 1); else throw std::runtime_error{"Incoming config data has invalid padding"}; @@ -180,7 +164,7 @@ std::vector unwrap_config( throw std::runtime_error{"SharedConfig has wrong kind for config namespace"}; // if ParseFromString fails, we have a raw (not protobuf encoded) message - return to_vector(shconf.data()); + return to_vector(shconf.data()); } } // namespace session::config::protos diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 26c5281c1..93ac2be37 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -57,7 +57,7 @@ legacy_group_info::legacy_group_info(std::string sid) : session_id{std::move(sid } community_info::community_info(const ugroups_community_info& c) : - community_info{c.base_url, c.room, std::span{c.pubkey, 32}} { + community_info{c.base_url, c.room, std::as_bytes(std::span{c.pubkey})} { base_from(*this, c); } @@ -79,8 +79,8 @@ legacy_group_info::legacy_group_info(const ugroups_legacy_group_info& c, impl_t) assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up base_from(*this, c); if (c.have_enc_keys) { - enc_pubkey.assign(c.enc_pubkey, c.enc_pubkey + 32); - enc_seckey.assign(c.enc_seckey, c.enc_seckey + 32); + enc_pubkey = to_vector(to_byte_span(c.enc_pubkey)); + enc_seckey = to_vector(to_byte_span(c.enc_seckey)); } } @@ -204,9 +204,9 @@ group_info::group_info(const ugroups_group_info& c) : id{c.id, 66} { assert(name.size() <= NAME_MAX_LENGTH); // Otherwise the caller messed up if (c.have_secretkey) - secretkey.assign(c.secretkey, c.secretkey + 64); + secretkey = to_vector(to_byte_span(c.secretkey)); if (c.have_auth_data) - auth_data.assign(c.auth_data, c.auth_data + sizeof(c.auth_data)); + auth_data = to_vector(to_byte_span(c.auth_data)); } void group_info::into(ugroups_group_info& c) const { @@ -230,10 +230,12 @@ void group_info::load(const dict& info_dict) { name.clear(); if (auto seed = maybe_vector(info_dict, "K"); seed && seed->size() == 32) { - std::array pk; - pk[0] = 0x03; + b33 pk; + pk[0] = std::byte{0x03}; secretkey.resize(64); - crypto_sign_seed_keypair(pk.data() + 1, secretkey.data(), seed->data()); + crypto_sign_seed_keypair( + to_unsigned(pk.data() + 1), to_unsigned(secretkey.data()), + to_unsigned(seed->data())); if (id != oxenc::to_hex(pk.begin(), pk.end())) secretkey.clear(); } @@ -279,20 +281,20 @@ void community_info::load(const dict& info_dict) { } UserGroups::UserGroups( - std::span ed25519_secretkey, - std::optional> dumped) { + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, std::nullopt, std::nullopt); load_key(ed25519_secretkey); } ConfigBase::DictFieldProxy UserGroups::community_field( - const community_info& og, std::span* get_pubkey) const { + const community_info& og, std::span* get_pubkey) const { auto record = data["o"][og.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) - *get_pubkey = std::span{ - reinterpret_cast(pk.data()), pk.size()}; + *get_pubkey = std::span{ + reinterpret_cast(pk.data()), pk.size()}; } return record["R"][og.room_norm()]; } @@ -301,11 +303,11 @@ std::optional UserGroups::get_community( std::string_view base_url, std::string_view room) const { community_info og{base_url, room}; - std::span pubkey; + std::span pubkey; if (auto* info_dict = community_field(og, &pubkey).dict()) { og.load(*info_dict); if (!pubkey.empty()) - og.set_pubkey(pubkey); + og.set_pubkey(pubkey.first<32>()); return og; } return std::nullopt; @@ -319,8 +321,8 @@ std::optional UserGroups::get_community(std::string_view partial community_info UserGroups::get_or_construct_community( std::string_view base_url, std::string_view room, - std::span pubkey) const { - community_info result{base_url, room, pubkey}; + std::span pubkey) const { + community_info result{base_url, room, pubkey.first<32>()}; if (auto* info_dict = community_field(result).dict()) result.load(*info_dict); @@ -382,10 +384,10 @@ group_info UserGroups::get_or_construct_group(std::string_view pubkey_hex) const } group_info UserGroups::create_group() const { - std::array pk; - std::vector sk; + b32 pk; + std::vector sk; sk.resize(64); - crypto_sign_keypair(pk.data(), sk.data()); + crypto_sign_keypair(to_unsigned(pk.data()), to_unsigned(sk.data())); std::string pk_hex; pk_hex.reserve(66); pk_hex += "03"; @@ -445,13 +447,13 @@ void UserGroups::set(const group_info& g) { if (g.secretkey.size() == 64 && // Make sure the secretkey's embedded pubkey matches the group id: - to_string_view(std::span{g.secretkey.data() + 32, 32}) == - to_string_view(std::span{ - reinterpret_cast(pk_bytes.data() + 1), + to_string_view(std::span{g.secretkey.data() + 32, 32}) == + to_string_view(std::span{ + reinterpret_cast(pk_bytes.data() + 1), pk_bytes.size() - 1})) - info["K"] = std::span{g.secretkey.data(), 32}; + info["K"] = std::span{g.secretkey.data(), 32}; else { - info["K"] = std::span{}; + info["K"] = std::span{}; if (g.auth_data.size() == 100) info["s"] = g.auth_data; else @@ -667,7 +669,10 @@ LIBSESSION_C_API bool user_groups_get_or_construct_community( [&] { unbox(conf) ->get_or_construct_community( - base_url, room, std::span{pubkey, 32}) + base_url, + room, + std::span{ + reinterpret_cast(pubkey), 32}) .into(*comm); return true; }, diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 3de898a83..a3bbceab2 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -14,8 +14,8 @@ using namespace session::config; UserProfile::UserProfile( - std::span ed25519_secretkey, - std::optional> dumped) { + const ed25519::PrivKeySpan& ed25519_secretkey, + std::optional> dumped) { init(dumped, std::nullopt, std::nullopt); load_key(ed25519_secretkey); } @@ -54,12 +54,12 @@ profile_pic UserProfile::get_profile_pic() const { pic.url = *url; if (auto* key = data[key_key].string(); key && key->size() == 32) pic.key.assign( - reinterpret_cast(key->data()), - reinterpret_cast(key->data()) + 32); + reinterpret_cast(key->data()), + reinterpret_cast(key->data()) + 32); return pic; } -void UserProfile::set_profile_pic(std::string_view url, std::span key) { +void UserProfile::set_profile_pic(std::string_view url, std::span key) { auto current_url = data["p"].string_view_or(""); auto current_key_str = data["q"].string_view_or(""); std::string_view new_key_str{reinterpret_cast(key.data()), key.size()}; @@ -82,7 +82,7 @@ void UserProfile::set_profile_pic(profile_pic pic) { } void UserProfile::set_reupload_profile_pic( - std::string_view url, std::span key) { + std::string_view url, std::span key) { auto current_url = data["P"].string_view_or(""); auto current_key_str = data["Q"].string_view_or(""); std::string_view new_key_str{reinterpret_cast(key.data()), key.size()}; @@ -163,7 +163,7 @@ void UserProfile::set_pro_config(const ProConfig& pro) { std::optional curr = get_pro_config(); if (!curr || *curr != pro) { auto root = data["s"]; - root["r"] = std::span( + root["r"] = std::span( pro.rotating_privkey.data(), crypto_sign_ed25519_SEEDBYTES); auto proof_dict = root["p"]; @@ -271,9 +271,9 @@ LIBSESSION_C_API user_profile_pic user_profile_get_pic(const config_object* conf LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic pic) { std::string_view url{pic.url}; - std::span key; + std::span key; if (!url.empty()) - key = {pic.key, 32}; + key = {reinterpret_cast(pic.key), 32}; return wrap_exceptions( conf, @@ -286,9 +286,9 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic LIBSESSION_C_API int user_profile_set_reupload_pic(config_object* conf, user_profile_pic pic) { std::string_view url{pic.url}; - std::span key; + std::span key; if (!url.empty()) - key = {pic.key, 32}; + key = {reinterpret_cast(pic.key), 32}; return wrap_exceptions( conf, diff --git a/src/core.cpp b/src/core.cpp index 116aaa5eb..1c8a941a6 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include @@ -139,13 +139,7 @@ void Core::_poll() { if (!ns_requires_auth(ns_val)) continue; std::string to_sign = "retrieve{}{}"_format(ns_val, now_ms); - std::array sig; - crypto_sign_ed25519_detached( - sig.data(), - nullptr, - reinterpret_cast(to_sign.data()), - to_sign.size(), - seed.ed25519_secret().data()); + auto sig = ed25519::sign(seed.ed25519_secret(), to_span(to_sign)); ns_sig[i] = oxenc::to_base64(sig); } } @@ -200,7 +194,7 @@ void Core::_poll() { network::Request{ node, "batch", - to_vector(body_str), + to_vector(body_str), network::RequestCategory::standard_small, 20s}, [this, sn_pubkey = node.remote_pubkey, namespaces]( @@ -254,7 +248,7 @@ void Core::_handle_poll_response( // Decode each message; keep the decoded bytes alive until after // receive_messages() returns, since SwarmMessage::data spans // into them. - std::vector> messages_data; + std::vector> messages_data; std::vector swarm_messages; std::string newest_hash; @@ -302,14 +296,14 @@ ON CONFLICT(namespace, sn_pubkey) DO UPDATE SET last_hash = excluded.last_hash } } -PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) { +PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) { auto net = _network; if (!net) throw std::logic_error{"prefetch_pfs_keys called without a network object"}; // One copy of session_id for async use; subsequently moved into lambdas. - std::array sid; - std::memcpy(sid.data(), session_id.data(), 33); + b33 sid; + std::ranges::copy(session_id, sid.begin()); // Skip the fetch if the cached entry is still fresh, or a recent NAK suppresses retrying. // Otherwise determine whether we have a stale (but usable) key or no key at all. @@ -385,7 +379,7 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_ network::Request{ swarm.front(), "retrieve", - to_vector(body_str), + to_vector(body_str), network::RequestCategory::standard_small, 20s}, [this, sid = std::move(sid)]( @@ -500,8 +494,10 @@ void Core::_handle_pfs_response(std::span sid, std::string in.require_signature( "~", [&x25519_pub]( - std::span b, std::span sig) { - if (!xed25519::verify(sig, x25519_pub, b)) + std::span b, + std::span sig) { + if (sig.size() != 64 || + !xed25519::verify(sig.first<64>(), x25519_pub, b)) throw std::runtime_error{"signature verification failed"}; }); std::ranges::copy(X, pk_x25519.emplace().begin()); @@ -557,15 +553,11 @@ void Core::_send_to_swarm( auto now_ms = epoch_ms(clock_now_ms()); std::string to_sign = "store{}{}"_format(ns_val, now_ms); - std::array sig; + b64 sig; { auto seed = globals.account_seed(); - crypto_sign_ed25519_detached( - sig.data(), - nullptr, - reinterpret_cast(to_sign.data()), - to_sign.size(), - seed.ed25519_secret().data()); + auto to_sign_bytes = std::as_bytes(std::span{to_sign}); + ed25519::sign(sig, seed.ed25519_secret(), to_sign_bytes); } nlohmann::json params = { @@ -579,7 +571,7 @@ void Core::_send_to_swarm( {"ttl", ttl.count()}, }; - auto body = to_vector(params.dump()); + auto body = to_vector(params.dump()); // Resolve the recipient's swarm and send. network::x25519_pubkey x25519_pub; @@ -615,7 +607,7 @@ void Core::_do_send_dm( std::span recipient, std::span content, sys_ms sent_timestamp, - const OptionalEd25519PrivKeySpan& pro_privkey, + const ed25519::OptionalPrivKeySpan& pro_privkey, std::chrono::milliseconds ttl, bool force_v2) { auto fire_status = [&](MessageSendStatus status) { @@ -628,15 +620,9 @@ void Core::_do_send_dm( } }; - // TODO: remove these casts once the encrypt/encode APIs are migrated to std::byte. - std::span recipient_uc{ - reinterpret_cast(recipient.data()), 33}; - std::span content_uc{ - reinterpret_cast(content.data()), content.size()}; - // Look up cached PFS keys for the recipient. - using X = sqlite::blob_guts>; - using M = sqlite::blob_guts>; + using X = sqlite::blob_guts; + using M = sqlite::blob_guts>; auto row = db.conn() .prepared_maybe_get< std::optional, @@ -645,43 +631,39 @@ void Core::_do_send_dm( std::optional>( "SELECT fetched_at, nak_at, pubkey_x25519, pubkey_mlkem768" " FROM pfs_key_cache WHERE session_id = ?", - recipient_uc); + recipient); - const std::array* pfs_x25519 = nullptr; - const std::array* pfs_mlkem768 = nullptr; + const b32* pfs_x25519 = nullptr; + const std::array* pfs_mlkem768 = nullptr; if (row) { auto& [fetched_at, nak_at, pk_x, pk_m] = *row; if (fetched_at && pk_x && pk_m) { - pfs_x25519 = &static_cast&>(*pk_x); - pfs_mlkem768 = &static_cast&>(*pk_m); + pfs_x25519 = &static_cast(*pk_x); + pfs_mlkem768 = &static_cast&>(*pk_m); } } // Encrypt the message. v2 (PFS or nopfs) produces the complete wire format directly // (0x00 0x02 | ki | E | mlkem_ct | encrypted_inner) — no protobuf wrapping. v1 uses // encode_dm_v1 which wraps in Envelope + WebSocketMessage protobufs. - std::vector encrypted; + std::vector payload; try { auto seed = globals.account_seed(); auto ed_sec = seed.ed25519_secret(); if (pfs_x25519) - encrypted = encrypt_for_recipient_v2( - ed_sec, recipient_uc, *pfs_x25519, *pfs_mlkem768, content_uc, pro_privkey); + payload = encrypt_for_recipient_v2( + ed_sec, recipient, *pfs_x25519, *pfs_mlkem768, content, pro_privkey); else if (force_v2) - encrypted = - encrypt_for_recipient_v2_nopfs(ed_sec, recipient_uc, content_uc, pro_privkey); + payload = encrypt_for_recipient_v2_nopfs(ed_sec, recipient, content, pro_privkey); else - encrypted = encode_dm_v1(content_uc, ed_sec, sent_timestamp, recipient_uc, pro_privkey); + payload = encode_dm_v1(content, ed_sec, sent_timestamp, recipient, pro_privkey); } catch (const std::exception& e) { log::warning(cat, "send_dm: encryption failed for message {}: {}", message_id, e.what()); fire_status(MessageSendStatus::encrypt_failed); return; } - std::vector payload(encrypted.size()); - std::memcpy(payload.data(), encrypted.data(), encrypted.size()); - // Dispatch to swarm. fire_status(MessageSendStatus::sending); try { @@ -718,8 +700,8 @@ void Core::_flush_pending_sends(std::span session_id) { pending.recipient, pending.content, pending.sent_timestamp, - pending.pro_privkey ? OptionalEd25519PrivKeySpan{*pending.pro_privkey} - : OptionalEd25519PrivKeySpan{}, + pending.pro_privkey ? ed25519::OptionalPrivKeySpan{*pending.pro_privkey} + : ed25519::OptionalPrivKeySpan{}, pending.ttl, pending.force_v2); } else { @@ -732,19 +714,16 @@ int64_t Core::send_dm( std::span recipient_session_id, std::span content, sys_ms sent_timestamp, - const OptionalEd25519PrivKeySpan& pro_privkey, + const ed25519::OptionalPrivKeySpan& pro_privkey, std::chrono::milliseconds ttl, bool force_v2) { auto id = _next_message_id++; - // Reinterpret for the DB query which still takes unsigned char. - std::span recipient_uc{ - reinterpret_cast(recipient_session_id.data()), 33}; - // Check cache state to decide whether we can send immediately or must queue. auto conn = db.conn(); auto row = conn.prepared_maybe_get, std::optional>( - "SELECT fetched_at, nak_at FROM pfs_key_cache WHERE session_id = ?", recipient_uc); + "SELECT fetched_at, nak_at FROM pfs_key_cache WHERE session_id = ?", + recipient_session_id); bool have_cached_key = false; bool is_nak = false; @@ -789,7 +768,7 @@ int64_t Core::send_dm( _flush_pending_sends(sid); }; - prefetch_pfs_keys(recipient_uc); + prefetch_pfs_keys(recipient_session_id); } else { // No cache and no network: fire immediate failure. if (callbacks.message_send_status) @@ -806,15 +785,11 @@ void Core::_handle_direct_messages(std::span messages) { auto seed = globals.account_seed(); auto session_id = globals.session_id(); // Long-term X25519 pub/sec used for v2 key-indicator prefix decryption. - std::span x25519_pub{session_id.data() + 1, 32}; + std::span x25519_pub{session_id.data() + 1, 32}; auto x25519_sec = seed.x25519_key(); // Ed25519 secret key used for v1 envelope decryption. - // TODO: DecodeEnvelopeKey is an ugly API; refactor it (see project memory). auto ed_sec = seed.ed25519_secret(); - std::array, 1> ed_sec_arr{ed_sec}; - DecodeEnvelopeKey v1_keys; - v1_keys.decrypt_keys = ed_sec_arr; auto fire_received = [&](ReceivedMessage out) { if (!callbacks.message_received) @@ -843,15 +818,15 @@ void Core::_handle_direct_messages(std::span messages) { continue; } - if (data[0] == 0x00) { + if (data[0] == std::byte{0x00}) { // Version 2 (PFS+PQ) or an unrecognised future version. - if (data.size() < 2 || data[1] != 0x02) { + if (data.size() < 2 || data[1] != std::byte{0x02}) { fire_fail(msg, MessageDecryptFailure::unknown_version); continue; } // Extract the 2-byte ML-KEM key indicator, then look up matching account keys. - std::array ki; + std::array ki; try { ki = decrypt_incoming_v2_prefix(x25519_sec, x25519_pub, data); } catch (const std::exception&) { @@ -916,7 +891,7 @@ void Core::_handle_direct_messages(std::span messages) { } else { // Version 1: protobuf WebSocketMessage → Envelope wire format. try { - auto decoded = decode_envelope(v1_keys, data, pro_backend::PUBKEY); + auto decoded = decode_dm_envelope(ed_sec, data, pro_backend::PUBKEY); ReceivedMessage out; out.hash = msg.hash; @@ -924,7 +899,7 @@ void Core::_handle_direct_messages(std::span messages) { out.expiry = msg.expiry; out.version = 1; // Reconstruct the 33-byte (0x05-prefixed) session ID from the x25519 pubkey. - out.sender_session_id[0] = 0x05; + out.sender_session_id[0] = std::byte{0x05}; std::ranges::copy(decoded.sender_x25519_pubkey, out.sender_session_id.begin() + 1); out.content = std::move(decoded.content_plaintext); if (decoded.envelope.flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG) diff --git a/src/core/devices.cpp b/src/core/devices.cpp index 8050fc49d..df80c1150 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -1,15 +1,13 @@ #include -#include #include #include #include #include -#include -#include -#include -#include -#include -#include + +#include +#include +#include +#include #include #include @@ -61,11 +59,11 @@ template consteval auto KEY_DOMAIN() = delete; template <> consteval auto KEY_DOMAIN() { - return "SessionDeviceKeys"_uc; + return "SessionDeviceKeys"_bytes; } template <> consteval auto KEY_DOMAIN() { - return "SessionAccountKeys"_uc; + return "SessionAccountKeys"_bytes; } template Keys> @@ -73,17 +71,16 @@ static Keys keys_from_seed(std::span seed) { Keys keys; auto& [x_sec, x_pub, ml_sec, ml_pub] = static_cast(keys); - static_assert(MLKEM768_PUBLICKEYBYTES == sizeof(ml_pub)); - static_assert(MLKEM768_SECRETKEYBYTES == sizeof(ml_sec)); + static_assert(mlkem768::PUBLICKEYBYTES == sizeof(ml_pub)); + static_assert(mlkem768::SECRETKEYBYTES == sizeof(ml_sec)); // Use SHAKE256 to expand the seed into separate X25519 and MLKEM-768 seeds. Domain // separation is achieved by prepending the domain string before the seed. - cleared_array ml_seed; + cleared_array ml_seed; hash::shake256(KEY_DOMAIN(), seed)(x_sec, ml_seed); - crypto_scalarmult_curve25519_base(x_pub.data(), x_sec.data()); + x25519::scalarmult_base(x_pub, x_sec); - if (0 != sr_mlkem768_keypair_derand(ml_pub.data(), ml_sec.data(), ml_seed.data())) - throw std::runtime_error{"ML-KEM-768 keygen failed!"}; + mlkem768::keygen(ml_pub, ml_sec, ml_seed); return keys; } @@ -191,7 +188,7 @@ std::vector Devices::active_device_keys() { } std::vector Devices::active_account_keys( - std::optional> key_indicator) { + std::optional> key_indicator) { auto c = conn(); SQLite::Transaction tx{c.sql}; @@ -217,7 +214,7 @@ std::vector Devices::active_account_keys( int64_t, std::optional, sqlite::blobn<32>, - sqlite::blobn, + sqlite::blobn, sqlite::blobn<32>>; for (auto [id, created, rotated, seed, pk_ml, pk_x] : @@ -263,7 +260,7 @@ namespace { std::string type, std::string desc, int64_t ver, - const sqlite::blobn& pk_ml, + const sqlite::blobn& pk_ml, const sqlite::blobn<32>& pk_x) { device::Info info; std::memcpy(info.id.data(), devid.data(), info.id.size()); @@ -391,7 +388,7 @@ device::map Devices::devices( std::string, std::string, int64_t, - sqlite::blobn, + sqlite::blobn, sqlite::blobn<32>>{std::move(st)}) { auto& info = devs[devid]; info = fill_device_info( @@ -624,7 +621,7 @@ namespace { info.timestamp = std::chrono::sys_seconds{std::chrono::seconds{dev.require("@")}}; read_extras(dev, "M", info.extra); - auto M = dev.require_span("M"); + auto M = dev.require_span("M"); std::memcpy(info.pk_mlkem768.data(), M.data(), M.size()); read_extras(dev, "X", info.extra); @@ -752,11 +749,10 @@ namespace { } // namespace std::vector Devices::encrypt_device_data(const device::map& devices) { - cleared_uc32 a; + cleared_b32 a; random::fill(a); - std::array A; - crypto_scalarmult_curve25519_base(A.data(), a.data()); + auto A = x25519::scalarmult_base(a); int padded_count = devices.size(); padded_count = (padded_count + 3) / 4 * 4; @@ -769,61 +765,56 @@ std::vector Devices::encrypt_device_data(const device::map& devices) std::ranges::shuffle(pos_map, csrng); // Holds MLKEM ciphertexts: - std::vector ciphertext_raw; - ciphertext_raw.resize(MLKEM768_CIPHERTEXTBYTES * padded_count); + std::vector ciphertext_raw; + ciphertext_raw.resize(mlkem768::CIPHERTEXTBYTES * padded_count); // Holds per-device-encrypted copies of the base key, each prefixed with a 2-byte key indicator // hash: - std::vector enc_key_raw; + std::vector enc_key_raw; enc_key_raw.resize((2 + 32) * padded_count); // Accessor for the relevant, position-mapped subspan of ciphertext_raw/enc_key_raw containing // the location of index i as a subspan of the raw vector: auto ciphertext = indices | std::views::transform([&](int i) { - return std::span{ - ciphertext_raw.data() + pos_map[i] * MLKEM768_CIPHERTEXTBYTES, - MLKEM768_CIPHERTEXTBYTES}; + return std::span{ + ciphertext_raw.data() + pos_map[i] * mlkem768::CIPHERTEXTBYTES, + mlkem768::CIPHERTEXTBYTES}; }); auto enc_indicator = indices | std::views::transform([&](int i) { - return std::span{enc_key_raw.data() + pos_map[i] * (2 + 32), 2}; + return std::span{enc_key_raw.data() + pos_map[i] * (2 + 32), 2}; }); auto enc_key = indices | std::views::transform([&](int i) { - return std::span{ + return std::span{ enc_key_raw.data() + pos_map[i] * (2 + 32) + 2, 32}; }); - sodium_array ml_ss_raw{MLKEM768_BYTES * devices.size()}; + cleared_vector ml_ss_raw(mlkem768::SHAREDSECRETBYTES * devices.size()); // Dynamic ss subspan accessor of ml_ss_raw, but *doesn't* go through the pos_map (unlike the // above constructs), and only goes up to the actual number of devices, not the padded number // (because this is never transmitted, and so not shuffled or padded). auto ml_ss = std::views::iota(size_t{0}, devices.size()) | std::views::transform([&](size_t i) { - return std::span{ - ml_ss_raw.data() + i * MLKEM768_BYTES, MLKEM768_BYTES}; + return std::span{ + ml_ss_raw.data() + i * mlkem768::SHAREDSECRETBYTES, mlkem768::SHAREDSECRETBYTES}; }); - cleared_uc32 rnd; + cleared_b32 rnd; int i = -1; for (auto& [devid, info] : devices) { ++i; random::fill(rnd); - if (0 != sr_mlkem768_enc_derand( - ciphertext[i].data(), - ml_ss[i].data(), - reinterpret_cast(info.pk_mlkem768.data()), - rnd.data())) - throw std::runtime_error{"ML-KEM-768 encapsulation failed!"}; + mlkem768::encapsulate(ciphertext[i], ml_ss[i], info.pk_mlkem768, rnd); } // Fill padding entries with randomness: for (; i < padded_count; i++) random::fill(ciphertext[i]); - std::array nonce; + std::array nonce; hash::blake2b_key_pers(nonce, A, PERS_DEV_NONCE, ciphertext_raw); - cleared_uc32 key_base; + cleared_b32 key_base; random::fill(key_base); // Fetch account key seeds for inclusion in the payload. @@ -839,21 +830,12 @@ std::vector Devices::encrypt_device_data(const device::map& devices) } auto plaintext_devices = encode_group_payload(devices, acc_keys); - std::vector enc_devices; + std::vector enc_devices; enc_devices.resize(plaintext_devices.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); - crypto_aead_xchacha20poly1305_ietf_encrypt( - enc_devices.data(), - nullptr, - reinterpret_cast(plaintext_devices.data()), - plaintext_devices.size(), - nullptr, - 0, - nullptr, - nonce.data(), - key_base.data()); - - cleared_uc32 ki; - cleared_uc32 aB; + encrypt::xchacha20poly1305_encrypt(enc_devices, to_span(plaintext_devices), nonce, key_base); + + cleared_b32 ki; + cleared_b32 aB; i = -1; for (auto& [devid, info] : devices) { ++i; @@ -861,8 +843,8 @@ std::vector Devices::encrypt_device_data(const device::map& devices) auto ekey = enc_key[i]; auto ct = ciphertext[i]; - auto B = to_span(info.pk_x25519); - if (0 != crypto_scalarmult_curve25519(aB.data(), a.data(), B.data())) { + auto& B = info.pk_x25519; + if (!x25519::scalarmult(aB, a, B)) { // This really shouldn't happen: we shouldn't have accepted an invalid pubkey in the // first place. log::error( @@ -882,8 +864,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) hash::blake2b_pers(ki, PERS_KEY_KEY, aB, A, B, ml_ss[i], info.pk_mlkem768); static_assert(decltype(ekey)::extent == key_base.size()); - crypto_stream_xchacha20_xor( - ekey.data(), key_base.data(), key_base.size(), nonce.data(), ki.data()); + encrypt::xchacha20_xor(ekey, key_base, nonce, ki); // Hash a bunch of stuff together as a checksum to let decryption skip most not-for-me // values. @@ -915,15 +896,8 @@ std::vector Devices::encrypt_device_data(const device::map& devices) o.append("K", enc_key_raw); o.append("d", enc_devices); o.append_signature( - "~", [seed = core.globals.account_seed()](std::span body) { - std::array sig; - crypto_sign_ed25519_detached( - sig.data(), - nullptr, - body.data(), - body.size(), - seed.ed25519_secret().data()); - return sig; + "~", [seed = core.globals.account_seed()](std::span body) { + return ed25519::sign(seed.ed25519_secret(), body); }); assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above @@ -946,7 +920,7 @@ static const std::string REGISTER_DEVICE_SQL = "UPDATE devices SET processing = {}, broadcast_needed = 1 WHERE id = ?"_format( static_cast(Processing::Registered)); -void Devices::receive_device_group_message(std::span data) { +void Devices::receive_device_group_message(std::span data) { GroupPayload payload; try { auto raw = decrypt_device_data(std::as_bytes(data)); @@ -1064,11 +1038,10 @@ Devices::LinkRequestResult Devices::build_link_request() { auto sas = link_request_sas(to_span(plaintext)); // Encrypt the plaintext - std::vector encrypted(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); + std::vector encrypted(plaintext.size() + config::ENCRYPT_DATA_OVERHEAD); std::memcpy(encrypted.data(), plaintext.data(), plaintext.size()); auto seed = core.globals.account_seed(); - config::encrypt_prealloced( - as_span(std::span{encrypted}), seed.seed(), "link-request"); + config::encrypt_prealloced(encrypted, seed.seed(), "link-request"); // Wrap in outer bt-dict: {"": "L", "L": } std::vector out( @@ -1078,7 +1051,7 @@ Devices::LinkRequestResult Devices::build_link_request() { ); oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; o.append("", "L"); - o.append("L", std::span{encrypted}); + o.append("L", std::span{encrypted}); assert(o.view().size() == out.size()); return {std::move(out), sas}; @@ -1088,29 +1061,26 @@ std::vector Devices::decrypt_device_data(std::span e oxenc::bt_dict_consumer in{enc_data}; in.require(""); // skip the "" type key added by the outer wrapper - auto A = in.require_span("A"); - auto ciphertext_raw = in.require_span("C"); - auto enc_key_raw = in.require_span("K"); + auto A = in.require_span("A"); + auto ciphertext_raw = in.require_span("C"); + auto enc_key_raw = in.require_span("K"); auto enc_devices = in.require_span("d"); in.require_signature( - "~", [this](std::span body, std::span sig) { - if (0 != crypto_sign_ed25519_verify_detached( - sig.data(), - body.data(), - body.size(), - core.globals.pubkey_ed25519().data())) + "~", [this](std::span body, std::span sig) { + if (sig.size() != 64 || + !ed25519::verify(sig.first<64>(), core.globals.pubkey_ed25519(), body)) throw std::runtime_error{ "Invalid encrypted device message: signature verification failed"}; }); in.finish(); - if (ciphertext_raw.size() % MLKEM768_CIPHERTEXTBYTES != 0) + if (ciphertext_raw.size() % mlkem768::CIPHERTEXTBYTES != 0) throw std::runtime_error{ "Invalid encrypted device group data: invalid ciphertext size ({} is not N*{})"_format( - ciphertext_raw.size(), MLKEM768_CIPHERTEXTBYTES)}; - const int count = ciphertext_raw.size() / MLKEM768_CIPHERTEXTBYTES; + ciphertext_raw.size(), mlkem768::CIPHERTEXTBYTES)}; + const int count = ciphertext_raw.size() / mlkem768::CIPHERTEXTBYTES; if (enc_key_raw.size() % (32 + 2) != 0) throw std::runtime_error{ "Invalid encrypted device group data: invalid encrypted keys size ({} is not N*34)"_format( @@ -1128,16 +1098,16 @@ std::vector Devices::decrypt_device_data(std::span e // Accessors for chunk-by-chunk access to the ciphertext_raw/enc_key_raw spans: auto ciphertext = indices | std::views::transform([&](int i) { - return std::span{ - ciphertext_raw.data() + i * MLKEM768_CIPHERTEXTBYTES, - MLKEM768_CIPHERTEXTBYTES}; + return std::span{ + ciphertext_raw.data() + i * mlkem768::CIPHERTEXTBYTES, + mlkem768::CIPHERTEXTBYTES}; }); auto enc_indicator = indices | std::views::transform([&](int i) { - return std::span{enc_key_raw.data() + i * (2 + 32), 2}; + return std::span{enc_key_raw.data() + i * (2 + 32), 2}; }); auto enc_key = indices | std::views::transform([&](int i) { - return std::span{ + return std::span{ enc_key_raw.data() + i * (2 + 32) + 2, 32}; }); @@ -1145,7 +1115,7 @@ std::vector Devices::decrypt_device_data(std::span e auto devices_nonce = hash::blake2b_pers<24>(PERS_DEV_NONCE, ciphertext_raw); - cleared_uc32 ml_ss, aB, ki, key_base; + cleared_b32 ml_ss, aB, ki, key_base; std::vector plaintext_devices; plaintext_devices.resize(enc_devices.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); @@ -1165,6 +1135,7 @@ std::vector Devices::decrypt_device_data(std::span e for (int active_i = 0; active_i < active_keys.size(); active_i++) { const auto& k = active_keys[active_i]; + const auto& b = k.x25519_sec; const auto& B = k.x25519_pub; const auto& M = k.mlkem768_pub; @@ -1175,12 +1146,12 @@ std::vector Devices::decrypt_device_data(std::span e hash::blake2b_pers<2>(PERS_KEY_KEY_IND, A, B, M, ct, ekey), eind)) continue; - if (0 != crypto_scalarmult_curve25519(aB.data(), k.x25519_sec.data(), A.data())) { + if (!x25519::scalarmult(aB, b, A)) { log::warning(cat, "X25519 multiplication failed; ignoring encrypted entry"); continue; } - if (0 != sr_mlkem768_dec(ml_ss.data(), ct.data(), k.mlkem768_sec.data())) { + if (!mlkem768::decapsulate(ml_ss, ct, k.mlkem768_sec)) { log::warning(cat, "MLKEM768 decapsulation failed; skipping device entry"); continue; } @@ -1191,20 +1162,11 @@ std::vector Devices::decrypt_device_data(std::span e // and then use it to recover the key_base: static_assert(decltype(ekey)::extent == key_base.size()); - crypto_stream_xchacha20_xor( - key_base.data(), ekey.data(), ekey.size(), knonce.data(), ki.data()); + encrypt::xchacha20_xor(key_base, ekey, knonce, ki); // Now we can decrypt the encrypted payload: - if (0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - reinterpret_cast(plaintext_devices.data()), - nullptr, - nullptr, - reinterpret_cast(enc_devices.data()), - enc_devices.size(), - nullptr, - 0, - devices_nonce.data(), - key_base.data())) { + if (encrypt::xchacha20poly1305_decrypt( + plaintext_devices, enc_devices, devices_nonce, key_base)) { found = true; break; } @@ -1228,14 +1190,14 @@ std::vector Devices::decrypt_device_data(std::span e return plaintext_devices; } -void Devices::receive_link_request(std::span data) { +void Devices::receive_link_request(std::span data) { // Parse outer bt-dict: {"": "L", "L": } oxenc::bt_dict_consumer outer{data}; outer.require(""); // skip type indicator - auto encrypted = outer.require_span("L"); + auto encrypted = outer.require_span("L"); // Decrypt using the account seed - std::vector plaintext; + std::vector plaintext; try { auto seed = core.globals.account_seed(); plaintext = config::decrypt(encrypted, seed.seed(), "link-request"); @@ -1247,7 +1209,7 @@ void Devices::receive_link_request(std::span data) { // Parse plaintext: {"I": <32-byte device id>, "i": {device info dict}} device::Info info; try { - oxenc::bt_dict_consumer pt{std::span{plaintext}}; + oxenc::bt_dict_consumer pt{std::span{plaintext}}; auto in_id = pt.require_span("I"); std::memcpy(info.id.data(), in_id.data(), info.id.size()); @@ -1363,7 +1325,7 @@ void Devices::parse_device_messages(std::span messages, bool std::string, std::string, int64_t, - sqlite::blobn, + sqlite::blobn, sqlite::blobn<32>, std::optional>( "SELECT id, unique_id, processing, state, seqno, timestamp, device_type," @@ -1459,14 +1421,15 @@ void Devices::parse_account_pubkeys(std::span messages, bool for (const auto& msg : messages) { try { oxenc::bt_dict_consumer in{msg.data}; - auto M = in.require_span("M"); + auto M = in.require_span("M"); auto X = in.require_span("X"); in.require_signature( "~", [&x25519_pub]( - std::span body, - std::span sig) { - if (!xed25519::verify(sig, x25519_pub, body)) + std::span body, + std::span sig) { + if (sig.size() != 64 || + !xed25519::verify(sig.first<64>(), x25519_pub, body)) throw std::runtime_error{ "Invalid account pubkey message: signature verification " "failed"}; @@ -1581,7 +1544,7 @@ std::vector Devices::build_account_pubkey_message() { o.append("M", k.mlkem768_pub); o.append("X", k.x25519_pub); o.append_signature( - "~", [seed = core.globals.account_seed()](std::span body) { + "~", [seed = core.globals.account_seed()](std::span body) { return xed25519::sign(seed.x25519_key(), body); }); diff --git a/src/core/globals.cpp b/src/core/globals.cpp index 67a51f77f..4119c0041 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -139,18 +139,20 @@ void Globals::init() { auto* rw_uc = reinterpret_cast(rw.buf.data()); crypto_sign_ed25519_seed_keypair( - _pubkey_ed25519.data(), + to_unsigned(_pubkey_ed25519.data()), rw_uc, reinterpret_cast(seed_to_use->data())); crypto_sign_ed25519_sk_to_curve25519(rw_uc + 64, rw_uc); _predefined_seed.reset(); // Clear now that it has been consumed - if (0 != crypto_sign_ed25519_pk_to_curve25519(_pubkey_x25519.data(), _pubkey_ed25519.data())) + if (0 != crypto_sign_ed25519_pk_to_curve25519( + to_unsigned(_pubkey_x25519.data()), + to_unsigned(_pubkey_ed25519.data()))) // This *should* be impossible when starting from a seed because that would mean the seed // generation produced an invalid Ed pubkey! log::critical(cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); - _session_id[0] = 0x05; + _session_id[0] = std::byte{0x05}; std::copy(_pubkey_x25519.begin(), _pubkey_x25519.end(), _session_id.data() + 1); if (!have_seed) { diff --git a/src/core/link_sas.cpp b/src/core/link_sas.cpp index fa5a39c55..85e1703bf 100644 --- a/src/core/link_sas.cpp +++ b/src/core/link_sas.cpp @@ -18,7 +18,7 @@ std::array derive_sas_seed(std::span plaintext) seed.size(), reinterpret_cast(plaintext.data()), plaintext.size(), - salt.data(), + reinterpret_cast(salt.data()), /*opslimit=*/2, /*memlimit=*/16ULL * 1024 * 1024, crypto_pwhash_ALG_ARGON2ID13)) diff --git a/src/core/pro.cpp b/src/core/pro.cpp index ad7d1d7fa..ecdc3466d 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -10,7 +10,7 @@ namespace session::core { bool Pro::proof_is_revoked( - std::span gen_index_hash, + std::span gen_index_hash, std::chrono::sys_time unix_ts) { return conn().prepared_get( "SELECT EXISTS (SELECT 1 FROM pro_revocations" @@ -35,7 +35,7 @@ void Pro::update_revocations( if (revocations_ticket_ && ticket == *revocations_ticket_) return; - auto already_hashed = [](const uc32& a) { + auto already_hashed = [](const b32& a) { size_t h; std::memcpy(&h, a.data(), sizeof(h)); return h; @@ -45,9 +45,9 @@ void Pro::update_revocations( SQLite::Transaction tx{c.sql}; - std::unordered_set to_remove; + std::unordered_set to_remove; for (auto id : - c.prepared_results>("SELECT gen_index_hash FROM pro_revocations")) + c.prepared_results>("SELECT gen_index_hash FROM pro_revocations")) to_remove.insert(id); for (auto st = c.prepared_st( diff --git a/src/crypto/ed25519.cpp b/src/crypto/ed25519.cpp new file mode 100644 index 000000000..28c521ce5 --- /dev/null +++ b/src/crypto/ed25519.cpp @@ -0,0 +1,296 @@ +#include "session/crypto/ed25519.hpp" + +#include +#include +#include +#include + +#include + +#include "session/export.h" +#include "session/hash.hpp" +#include "session/pro_backend.hpp" +#include "session/sodium_array.hpp" +#include "session/util.hpp" + +namespace session::ed25519 { + +void PrivKeySpan::expand_seed(std::span seed) { + auto& buf = storage_.emplace(); + b32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + to_unsigned(ignore_pk.data()), to_unsigned(buf.data()), to_unsigned(seed.data())); +} + +void PrivKeySpan::expand_seed(std::span seed) { + auto& buf = storage_.emplace(); + b32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + to_unsigned(ignore_pk.data()), to_unsigned(buf.data()), seed.data()); +} + +void keypair(std::span pk, std::span sk) { + crypto_sign_ed25519_keypair(to_unsigned(pk.data()), to_unsigned(sk.data())); +} + +std::pair keypair() { + std::pair kp; + keypair(kp.first, kp.second); + return kp; +} + +void seed_keypair( + std::span pk, + std::span sk, + std::span seed) { + crypto_sign_ed25519_seed_keypair( + to_unsigned(pk.data()), to_unsigned(sk.data()), to_unsigned(seed.data())); +} + +std::pair keypair(std::span ed25519_seed) { + std::pair kp; + seed_keypair(kp.first, kp.second, ed25519_seed); + return kp; +} + +void sk_to_pk(std::span pk, const PrivKeySpan& sk) { + crypto_sign_ed25519_sk_to_pk(to_unsigned(pk.data()), to_unsigned(sk.data())); +} + +b32 sk_to_pk(const PrivKeySpan& sk) { + b32 pk; + sk_to_pk(pk, sk); + return pk; +} + +void pk_to_x25519(std::span out, std::span pk) { + if (0 != crypto_sign_ed25519_pk_to_curve25519(to_unsigned(out.data()), to_unsigned(pk.data()))) + throw std::runtime_error{"Failed to convert Ed25519 pubkey to X25519: invalid key"}; +} + +b32 pk_to_x25519(std::span pk) { + b32 xpk; + pk_to_x25519(xpk, pk); + return xpk; +} + +void pk_to_session_id(std::span out, std::span pk) { + out[0] = std::byte{0x05}; + pk_to_x25519(out.last<32>(), pk); +} + +b33 pk_to_session_id(std::span pk) { + b33 sid; + pk_to_session_id(sid, pk); + return sid; +} + +cleared_b32 sk_to_x25519(std::span seed) { + cleared_b32 xsk; + crypto_sign_ed25519_sk_to_curve25519(to_unsigned(xsk.data()), to_unsigned(seed.data())); + return xsk; +} + +void scalarmult_base(std::span out, std::span scalar) { + if (0 != crypto_scalarmult_ed25519_base(to_unsigned(out.data()), to_unsigned(scalar.data()))) + throw std::runtime_error{"crypto_scalarmult_ed25519_base failed"}; +} + +b32 scalarmult_base(std::span scalar) { + b32 out; + scalarmult_base(out, scalar); + return out; +} + +void scalarmult_base_noclamp(std::span out, std::span scalar) { + if (0 != crypto_scalarmult_ed25519_base_noclamp( + to_unsigned(out.data()), to_unsigned(scalar.data()))) + throw std::runtime_error{"crypto_scalarmult_ed25519_base_noclamp failed"}; +} + +b32 scalarmult_base_noclamp(std::span scalar) { + b32 out; + scalarmult_base_noclamp(out, scalar); + return out; +} + +void scalarmult_noclamp( + std::span out, + std::span scalar, + std::span point) { + if (0 != crypto_scalarmult_ed25519_noclamp( + to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data()))) + throw std::runtime_error{"crypto_scalarmult_ed25519_noclamp failed"}; +} + +b32 scalarmult_noclamp( + std::span scalar, std::span point) { + b32 out; + scalarmult_noclamp(out, scalar, point); + return out; +} + +void scalar_reduce(std::span out, std::span in) { + crypto_core_ed25519_scalar_reduce(to_unsigned(out.data()), to_unsigned(in.data())); +} + +b32 scalar_reduce(std::span in) { + b32 out; + scalar_reduce(out, in); + return out; +} + +void scalar_negate(std::span out, std::span in) { + crypto_core_ed25519_scalar_negate(to_unsigned(out.data()), to_unsigned(in.data())); +} + +b32 scalar_negate(std::span in) { + b32 out; + scalar_negate(out, in); + return out; +} + +void scalar_mul( + std::span out, + std::span x, + std::span y) { + crypto_core_ed25519_scalar_mul( + to_unsigned(out.data()), to_unsigned(x.data()), to_unsigned(y.data())); +} + +b32 scalar_mul(std::span x, std::span y) { + b32 out; + scalar_mul(out, x, y); + return out; +} + +void scalar_add( + std::span out, + std::span x, + std::span y) { + crypto_core_ed25519_scalar_add( + to_unsigned(out.data()), to_unsigned(x.data()), to_unsigned(y.data())); +} + +b32 scalar_add(std::span x, std::span y) { + b32 out; + scalar_add(out, x, y); + return out; +} + +void sign( + std::span sig, + const PrivKeySpan& ed25519_privkey, + std::span msg) { + if (0 != crypto_sign_ed25519_detached( + to_unsigned(sig.data()), + nullptr, + to_unsigned(msg.data()), + msg.size(), + to_unsigned(ed25519_privkey.data()))) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; +} + +b64 sign(const PrivKeySpan& ed25519_privkey, std::span msg) { + b64 sig; + sign(sig, ed25519_privkey, msg); + return sig; +} + +bool verify( + std::span sig, + std::span pubkey, + std::span msg) { + return (0 == crypto_sign_ed25519_verify_detached( + to_unsigned(sig.data()), + to_unsigned(msg.data()), + msg.size(), + to_unsigned(pubkey.data()))); +} + +std::pair derive_subkey( + std::span ed25519_seed, std::span domain) { + // Construct seed for derived key: + // new_seed = Blake2b32(ed25519_seed, key=domain) + cleared_b32 derived_seed; + hash::blake2b_key(derived_seed, domain, ed25519_seed); + return keypair(derived_seed); +} + +} // namespace session::ed25519 + +using namespace session; + +LIBSESSION_C_API bool session_ed25519_key_pair( + unsigned char* ed25519_pk_out, unsigned char* ed25519_sk_out) { + try { + auto [ed_pk, ed_sk] = session::ed25519::keypair(); + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_key_pair_seed( + const unsigned char* ed25519_seed, + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out) { + try { + auto [ed_pk, ed_sk] = session::ed25519::keypair(to_byte_span<32>(ed25519_seed)); + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_seed_for_ed_privkey( + const unsigned char* ed25519_privkey, unsigned char* ed25519_seed_out) { + try { + auto result = session::ed25519::extract_seed(to_byte_span<64>(ed25519_privkey)); + std::memcpy(ed25519_seed_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_sign( + const unsigned char* ed25519_privkey, + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out) { + try { + auto result = session::ed25519::sign( + to_byte_span<64>(ed25519_privkey), to_byte_span(msg, msg_len)); + std::memcpy(ed25519_sig_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_verify( + const unsigned char* sig, + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len) { + return session::ed25519::verify( + to_byte_span<64>(sig), to_byte_span<32>(pubkey), to_byte_span(msg, msg_len)); +} + +LIBSESSION_C_API bool session_ed25519_pro_privkey_for_ed25519_seed( + const unsigned char* ed25519_seed, unsigned char* ed25519_sk_out) { + try { + auto [pub, sk] = session::ed25519::derive_subkey( + to_byte_span<32>(ed25519_seed), + session::pro_backend::pro_subkey_domain); + std::memcpy(ed25519_sk_out, sk.data(), sk.size()); + return true; + } catch (...) { + return false; + } +} diff --git a/src/crypto/mlkem768.cpp b/src/crypto/mlkem768.cpp new file mode 100644 index 000000000..f49d4fba2 --- /dev/null +++ b/src/crypto/mlkem768.cpp @@ -0,0 +1,47 @@ +#include "session/crypto/mlkem768.hpp" + +#include + +#include + +namespace session::mlkem768 { + +static_assert(PUBLICKEYBYTES == MLKEM768_PUBLICKEYBYTES); +static_assert(SECRETKEYBYTES == MLKEM768_SECRETKEYBYTES); +static_assert(CIPHERTEXTBYTES == MLKEM768_CIPHERTEXTBYTES); +static_assert(SHAREDSECRETBYTES == MLKEM_SYMBYTES); +static_assert(SEEDBYTES == 2 * MLKEM_SYMBYTES); + +void keygen( + std::span pk, + std::span sk, + std::span seed) { + if (0 != sr_mlkem768_keypair_derand( + to_unsigned(pk.data()), to_unsigned(sk.data()), to_unsigned(seed.data()))) + throw std::runtime_error{"ML-KEM-768 keygen failed"}; +} + +void encapsulate( + std::span ciphertext, + std::span shared_secret, + std::span pk, + std::span seed) { + if (0 != sr_mlkem768_enc_derand( + to_unsigned(ciphertext.data()), + to_unsigned(shared_secret.data()), + to_unsigned(pk.data()), + to_unsigned(seed.data()))) + throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; +} + +bool decapsulate( + std::span shared_secret, + std::span ciphertext, + std::span sk) { + return 0 == sr_mlkem768_dec( + to_unsigned(shared_secret.data()), + to_unsigned(ciphertext.data()), + to_unsigned(sk.data())); +} + +} // namespace session::mlkem768 diff --git a/src/crypto/x25519.cpp b/src/crypto/x25519.cpp new file mode 100644 index 000000000..1b5c926bc --- /dev/null +++ b/src/crypto/x25519.cpp @@ -0,0 +1,56 @@ +#include "session/crypto/x25519.hpp" + +#include +#include + +namespace session::x25519 { + +void keypair(std::span pk, std::span sk) { + crypto_box_keypair(to_unsigned(pk.data()), to_unsigned(sk.data())); +} + +std::pair keypair() { + std::pair kp; + keypair(kp.first, kp.second); + return kp; +} + +void seed_keypair( + std::span pk, + std::span sk, + std::span seed) { + crypto_box_seed_keypair(to_unsigned(pk.data()), to_unsigned(sk.data()), to_unsigned(seed.data())); +} + +std::pair seed_keypair(std::span seed) { + std::pair kp; + seed_keypair(kp.first, kp.second, seed); + return kp; +} + +void scalarmult_base(std::span out, std::span scalar) { + crypto_scalarmult_curve25519_base(to_unsigned(out.data()), to_unsigned(scalar.data())); +} + +b32 scalarmult_base(std::span scalar) { + b32 out; + scalarmult_base(out, scalar); + return out; +} + +bool scalarmult( + std::span out, + std::span scalar, + std::span point) { + return 0 == crypto_scalarmult_curve25519( + to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data())); +} + +b32 scalarmult(std::span scalar, std::span point) { + b32 out; + if (!scalarmult(out, scalar, point)) + throw std::runtime_error{"x25519 scalarmult failed (degenerate point)"}; + return out; +} + +} // namespace session::x25519 diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 8cea81800..f34d39f22 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,64 +1,23 @@ -#include "session/curve25519.hpp" -#include "session/curve25519.h" +#include "session/crypto/ed25519.hpp" +#include "session/crypto/x25519.hpp" -#include -#include - -#include +#include #include "session/export.h" #include "session/util.hpp" -namespace session::curve25519 { - -std::pair, std::array> curve25519_key_pair() { - std::array curve_pk; - std::array curve_sk; - crypto_box_keypair(curve_pk.data(), curve_sk.data()); - - return {curve_pk, curve_sk}; -} - -std::array to_curve25519_pubkey(std::span ed25519_pubkey) { - if (ed25519_pubkey.size() != 32) { - throw std::invalid_argument{"Invalid ed25519_pubkey: expected 32 bytes"}; - } - - std::array curve_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed25519_pubkey.data())) - throw std::runtime_error{ - "An error occured while attempting to convert Ed25519 pubkey to curve25519; " - "is the pubkey valid?"}; - - return curve_pk; -} - -std::array to_curve25519_seckey(std::span ed25519_seckey) { - if (ed25519_seckey.size() != 64) { - throw std::invalid_argument{"Invalid ed25519_seckey: expected 64 bytes"}; - } - - std::array curve_sk; - if (0 != crypto_sign_ed25519_sk_to_curve25519(curve_sk.data(), ed25519_seckey.data())) - throw std::runtime_error{ - "An error occured while attempting to convert Ed25519 pubkey to curve25519; " - "is the seckey valid?"}; - - return curve_sk; -} - -} // namespace session::curve25519 +// This file provides the C API wrappers for curve25519/x25519 operations. The C++ functions +// these previously wrapped (session::curve25519::*) have been replaced by session::x25519::* and +// session::ed25519::*. using namespace session; LIBSESSION_C_API bool session_curve25519_key_pair( unsigned char* curve25519_pk_out, unsigned char* curve25519_sk_out) { try { - auto result = session::curve25519::curve25519_key_pair(); - auto [curve_pk, curve_sk] = result; - std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); - std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + auto [pk, sk] = x25519::keypair(); + std::memcpy(curve25519_pk_out, pk.data(), pk.size()); + std::memcpy(curve25519_sk_out, sk.data(), sk.size()); return true; } catch (...) { return false; @@ -68,9 +27,8 @@ LIBSESSION_C_API bool session_curve25519_key_pair( LIBSESSION_C_API bool session_to_curve25519_pubkey( const unsigned char* ed25519_pubkey, unsigned char* curve25519_pk_out) { try { - auto curve_pk = session::curve25519::to_curve25519_pubkey( - std::span{ed25519_pubkey, 32}); - std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); + auto xpk = ed25519::pk_to_x25519(to_byte_span<32>(ed25519_pubkey)); + std::memcpy(curve25519_pk_out, xpk.data(), xpk.size()); return true; } catch (...) { return false; @@ -80,9 +38,8 @@ LIBSESSION_C_API bool session_to_curve25519_pubkey( LIBSESSION_C_API bool session_to_curve25519_seckey( const unsigned char* ed25519_seckey, unsigned char* curve25519_sk_out) { try { - auto curve_sk = session::curve25519::to_curve25519_seckey( - std::span{ed25519_seckey, 64}); - std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + auto xsk = ed25519::sk_to_x25519(to_byte_span<64>(ed25519_seckey)); + std::memcpy(curve25519_sk_out, xsk.data(), xsk.size()); return true; } catch (...) { return false; diff --git a/src/ed25519.cpp b/src/ed25519.cpp deleted file mode 100644 index bcef80fc2..000000000 --- a/src/ed25519.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "session/ed25519.hpp" -#include "session/ed25519.h" - -#include -#include - -#include - -#include "session/export.h" -#include "session/hash.hpp" -#include "session/sodium_array.hpp" - -namespace session { - -void Ed25519PrivKeySpan::expand_seed(std::span seed) { - auto& buf = storage_.emplace(); - uc32 ignore_pk; - crypto_sign_ed25519_seed_keypair(ignore_pk.data(), buf.data(), seed.data()); -} - -} // namespace session - -namespace session::ed25519 { - -std::pair, std::array> ed25519_key_pair() { - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_keypair(ed_pk.data(), ed_sk.data()); - - return {ed_pk, ed_sk}; -} - -std::pair, std::array> ed25519_key_pair( - std::span ed25519_seed) { - if (ed25519_seed.size() != 32) { - throw std::invalid_argument{"Invalid ed25519_seed: expected 32 bytes"}; - } - - std::array ed_pk; - std::array ed_sk; - - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed25519_seed.data()); - - return {ed_pk, ed_sk}; -} - -std::array seed_for_ed_privkey(std::span ed25519_privkey) { - std::array seed; - - if (ed25519_privkey.size() == 32 || ed25519_privkey.size() == 64) - // The first 32 bytes of a 64 byte ed25519 private key are the seed, otherwise - // if the provided value is 32 bytes we just assume we were given a seed - std::memcpy(seed.data(), ed25519_privkey.data(), 32); - else - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - - return seed; -} - -std::vector sign( - const Ed25519PrivKeySpan& ed25519_privkey, std::span msg) { - std::vector sig; - sig.resize(64); - - if (0 != crypto_sign_ed25519_detached( - sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) - throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; - - return sig; -} - -bool verify( - std::span sig, - std::span pubkey, - std::span msg) { - if (sig.size() != 64) - throw std::invalid_argument{"Invalid sig: expected 64 bytes"}; - if (pubkey.size() != 32) - throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; - - return (0 == - crypto_sign_ed25519_verify_detached(sig.data(), msg.data(), msg.size(), pubkey.data())); -} - -std::array ed25519_pro_privkey_for_ed25519_seed( - std::span ed25519_seed) { - - if (ed25519_seed.size() != 32 && ed25519_seed.size() != 64) - throw std::invalid_argument{ - "Invalid ed25519_seed: expected 32 bytes or libsodium style 64 bytes seed"}; - - // Construct seed for derived key - // new_seed = Blake2b32(ed25519_seed, key=) - // b/B = Ed25519FromSeed(new_seed) - session::cleared_uc32 s2; - session::hash::blake2b_key(s2, "SessionProRandom"_uc, ed25519_seed.first<32>()); - - auto [pubkey, privkey] = session::ed25519::ed25519_key_pair(s2); - return privkey; -} - -} // namespace session::ed25519 - -using namespace session; - -LIBSESSION_C_API bool session_ed25519_key_pair( - unsigned char* ed25519_pk_out, unsigned char* ed25519_sk_out) { - try { - auto result = session::ed25519::ed25519_key_pair(); - auto [ed_pk, ed_sk] = result; - std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); - std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); - return true; - } catch (...) { - return false; - } -} - -LIBSESSION_C_API bool session_ed25519_key_pair_seed( - const unsigned char* ed25519_seed, - unsigned char* ed25519_pk_out, - unsigned char* ed25519_sk_out) { - try { - auto result = session::ed25519::ed25519_key_pair( - std::span{ed25519_seed, 32}); - auto [ed_pk, ed_sk] = result; - std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); - std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); - return true; - } catch (...) { - return false; - } -} - -LIBSESSION_C_API bool session_seed_for_ed_privkey( - const unsigned char* ed25519_privkey, unsigned char* ed25519_seed_out) { - try { - auto result = session::ed25519::seed_for_ed_privkey( - std::span{ed25519_privkey, 64}); - std::memcpy(ed25519_seed_out, result.data(), result.size()); - return true; - } catch (...) { - return false; - } -} - -LIBSESSION_C_API bool session_ed25519_sign( - const unsigned char* ed25519_privkey, - const unsigned char* msg, - size_t msg_len, - unsigned char* ed25519_sig_out) { - try { - auto result = session::ed25519::sign( - std::span{ed25519_privkey, 64}, - std::span{msg, msg_len}); - std::memcpy(ed25519_sig_out, result.data(), result.size()); - return true; - } catch (...) { - return false; - } -} - -LIBSESSION_C_API bool session_ed25519_verify( - const unsigned char* sig, - const unsigned char* pubkey, - const unsigned char* msg, - size_t msg_len) { - return session::ed25519::verify( - std::span{sig, 64}, - std::span{pubkey, 32}, - std::span{msg, msg_len}); -} - -LIBSESSION_C_API bool session_ed25519_pro_privkey_for_ed25519_seed( - const unsigned char* ed25519_seed, unsigned char* ed25519_sk_out) { - try { - auto seed = std::span(ed25519_seed, 32); - uc64 sk = session::ed25519::ed25519_pro_privkey_for_ed25519_seed(seed); - std::memcpy(ed25519_sk_out, sk.data(), sk.size()); - return true; - } catch (...) { - return false; - } -} diff --git a/src/hash.cpp b/src/hash.cpp index 162558a0b..453359e7c 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -8,10 +8,12 @@ namespace { +using namespace session; + void hash_impl( - std::span hash, - std::span msg, - std::optional> key) { + std::span hash, + std::span msg, + std::optional> key) { const auto size = hash.size(); if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; @@ -20,11 +22,11 @@ void hash_impl( throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; crypto_generichash_blake2b( - hash.data(), + to_unsigned(hash.data()), size, - msg.data(), + to_unsigned(msg.data()), msg.size(), - key ? key->data() : nullptr, + key ? to_unsigned(key->data()) : nullptr, key ? key->size() : 0); } @@ -33,17 +35,17 @@ void hash_impl( namespace session::hash { void hash( - std::span hash, - std::span msg, - std::optional> key) { + std::span hash, + std::span msg, + std::optional> key) { hash_impl(hash, msg, key); } -std::vector hash( +std::vector hash( const size_t size, - std::span msg, - std::optional> key) { - std::vector result(size); + std::span msg, + std::optional> key) { + std::vector result(size); hash_impl(result, msg, key); return result; } @@ -60,12 +62,15 @@ LIBSESSION_C_API bool session_hash( size_t key_len, unsigned char* hash_out) { try { - std::optional> key; + std::optional> key; if (key_in && key_len) - key = {key_in, key_len}; + key = std::span{reinterpret_cast(key_in), key_len}; - hash_impl({hash_out, size}, {msg_in, msg_len}, key); + hash_impl( + std::span{reinterpret_cast(hash_out), size}, + std::span{reinterpret_cast(msg_in), msg_len}, + key); return true; } catch (...) { return false; diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index a7e7e2bfd..8b9b32052 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -7,10 +7,12 @@ #include #include +#include #include #include #include "session/hash.hpp" +#include "session/util.hpp" namespace session { @@ -19,15 +21,15 @@ const size_t encrypt_multiple_message_overhead = crypto_aead_xchacha20poly1305_i namespace detail { void encrypt_multi_key( - std::array& key, - std::span a, - std::span A, - std::span B, + std::span key, + std::span a, + std::span A, + std::span B, bool encrypting, std::string_view domain) { - std::array buf; - if (0 != crypto_scalarmult_curve25519(buf.data(), a.data(), B.data())) + b32 buf; + if (0 != crypto_scalarmult_curve25519(to_unsigned(buf.data()), to_unsigned(a.data()), to_unsigned(B.data()))) throw std::invalid_argument{"Unable to compute shared encrypted key: invalid pubkey?"}; static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); @@ -42,74 +44,47 @@ namespace detail { } void encrypt_multi_impl( - std::vector& out, - std::span msg, - const unsigned char* key, - const unsigned char* nonce) { - - // auto key = encrypt_multi_key(a, A, B, true, domain); - - out.resize(msg.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); - if (0 != - crypto_aead_xchacha20poly1305_ietf_encrypt( - out.data(), nullptr, msg.data(), msg.size(), nullptr, 0, nullptr, nonce, key)) - throw std::runtime_error{"XChaCha20 encryption failed!"}; + std::vector& out, + std::span msg, + std::span key, + std::span nonce) { + + out.resize(msg.size() + encrypt::XCHACHA20_ABYTES); + encrypt::xchacha20poly1305_encrypt(out, msg, nonce, key); } bool decrypt_multi_impl( - std::vector& out, - std::span ciphertext, - const unsigned char* key, - const unsigned char* nonce) { + std::vector& out, + std::span ciphertext, + std::span key, + std::span nonce) { - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (ciphertext.size() < encrypt::XCHACHA20_ABYTES) return false; - out.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - out.data(), - nullptr, - nullptr, - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, - nonce, - key); + out.resize(ciphertext.size() - encrypt::XCHACHA20_ABYTES); + return encrypt::xchacha20poly1305_decrypt(out, ciphertext, nonce, key); + } + + std::pair x_keys(const ed25519::PrivKeySpan& sk) { + return {ed25519::sk_to_x25519(sk), ed25519::pk_to_x25519(sk.pubkey())}; } } // namespace detail namespace { - std::pair>, std::array> x_keys( - std::span ed25519_secret_key) { - if (ed25519_secret_key.size() != 64) - throw std::invalid_argument{"Ed25519 secret key is not the expected 64 bytes"}; - - std::pair>, std::array> ret; - auto& [x_priv, x_pub] = ret; - - crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), ed25519_secret_key.data()); - if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), ed25519_secret_key.data() + 32)) - throw std::runtime_error{"Failed to convert Ed25519 key to X25519: invalid secret key"}; - - return ret; - } - -} // namespace - -std::optional> decrypt_for_multiple( - const std::vector>& ciphertexts, - std::span nonce, - std::span privkey, - std::span pubkey, - std::span sender_pubkey, +std::optional> decrypt_for_multiple( + const std::vector>& ciphertexts, + std::span nonce, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { auto it = ciphertexts.begin(); return decrypt_for_multiple( - [&]() -> std::optional> { + [&]() -> std::optional> { if (it == ciphertexts.end()) return std::nullopt; return *it++; @@ -121,13 +96,13 @@ std::optional> decrypt_for_multiple( domain); } -std::vector encrypt_for_multiple_simple( - const std::vector>& messages, - const std::vector>& recipients, - std::span privkey, - std::span pubkey, +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + std::span privkey, + std::span pubkey, std::string_view domain, - std::optional> nonce, + std::optional> nonce, int pad) { oxenc::bt_dict_producer d; @@ -135,7 +110,7 @@ std::vector encrypt_for_multiple_simple( std::array random_nonce; if (!nonce) { randombytes_buf(random_nonce.data(), random_nonce.size()); - nonce.emplace(random_nonce.data(), random_nonce.size()); + nonce.emplace(to_byte_span<24>(random_nonce.data())); } else if (nonce->size() != 24) { throw std::invalid_argument{"Invalid nonce: nonce must be 24 bytes"}; } @@ -152,13 +127,13 @@ std::vector encrypt_for_multiple_simple( privkey, pubkey, domain, - [&](std::span encrypted) { + [&](std::span encrypted) { enc_list.append(encrypted); msg_count++; }); if (int pad_size = pad > 1 && !messages.empty() ? messages.front().size() : 0) { - std::vector junk; + std::vector junk; junk.resize(pad_size); for (; msg_count % pad != 0; msg_count++) { randombytes_buf(junk.data(), pad_size); @@ -170,38 +145,38 @@ std::vector encrypt_for_multiple_simple( return to_vector(d.span()); } -std::vector encrypt_for_multiple_simple( - const std::vector>& messages, - const std::vector>& recipients, - std::span ed25519_secret_key, +std::vector encrypt_for_multiple_simple( + const std::vector>& messages, + const std::vector>& recipients, + const ed25519::PrivKeySpan& ed25519_secret_key, std::string_view domain, - std::span nonce, + std::optional> nonce, int pad) { auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); return encrypt_for_multiple_simple( - messages, recipients, to_span(x_privkey), to_span(x_pubkey), domain, nonce, pad); + messages, recipients, x_privkey, x_pubkey, domain, nonce, pad); } -std::optional> decrypt_for_multiple_simple( - std::span encoded, - std::span privkey, - std::span pubkey, - std::span sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + std::span privkey, + std::span pubkey, + std::span sender_pubkey, std::string_view domain) { try { oxenc::bt_dict_consumer d{encoded}; - auto nonce = d.require>("#"); + auto nonce = d.require>("#"); if (nonce.size() != 24) return std::nullopt; auto enc_list = d.require("e"); return decrypt_for_multiple( - [&]() -> std::optional> { + [&]() -> std::optional> { if (enc_list.is_finished()) return std::nullopt; - return enc_list.consume>(); + return enc_list.consume>(); }, nonce, privkey, @@ -213,38 +188,33 @@ std::optional> decrypt_for_multiple_simple( } } -std::optional> decrypt_for_multiple_simple( - std::span encoded, - std::span ed25519_secret_key, - std::span sender_pubkey, +std::optional> decrypt_for_multiple_simple( + std::span encoded, + const ed25519::PrivKeySpan& ed25519_secret_key, + std::span sender_pubkey, std::string_view domain) { auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); - return decrypt_for_multiple_simple( - encoded, to_span(x_privkey), to_span(x_pubkey), sender_pubkey, domain); + return decrypt_for_multiple_simple(encoded, x_privkey, x_pubkey, sender_pubkey, domain); } -std::optional> decrypt_for_multiple_simple_ed25519( - std::span encoded, - std::span ed25519_secret_key, - std::span sender_ed25519_pubkey, +std::optional> decrypt_for_multiple_simple_ed25519( + std::span encoded, + const ed25519::PrivKeySpan& ed25519_secret_key, + std::span sender_ed25519_pubkey, std::string_view domain) { - std::array sender_pub; - if (sender_ed25519_pubkey.size() != 32) - throw std::invalid_argument{"Invalid sender Ed25519 pubkey: expected 32 bytes"}; - if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_pub.data(), sender_ed25519_pubkey.data())) - throw std::runtime_error{"Failed to convert Ed25519 key to X25519: invalid secret key"}; + auto sender_pub = ed25519::pk_to_x25519(sender_ed25519_pubkey); - return decrypt_for_multiple_simple(encoded, ed25519_secret_key, to_span(sender_pub), domain); + return decrypt_for_multiple_simple(encoded, ed25519_secret_key, sender_pub, domain); } } // namespace session using namespace session; -static unsigned char* to_c_buffer(std::span x, size_t* out_len) { +static unsigned char* to_c_buffer(std::span x, size_t* out_len) { auto* ret = static_cast(malloc(x.size())); *out_len = x.size(); std::memcpy(ret, x.data(), x.size()); @@ -264,23 +234,23 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple( const unsigned char* nonce, int pad) { - std::vector> msgs, recips; + std::vector> msgs, recips; msgs.reserve(n_messages); recips.reserve(n_recipients); for (size_t i = 0; i < n_messages; i++) - msgs.emplace_back(messages[i], message_lengths[i]); + msgs.emplace_back(to_byte_span(messages[i], message_lengths[i])); for (size_t i = 0; i < n_recipients; i++) - recips.emplace_back(recipients[i], 32); - std::optional> maybe_nonce; + recips.emplace_back(to_byte_span<32>(recipients[i])); + std::optional> maybe_nonce; if (nonce) - maybe_nonce.emplace(nonce, 24); + maybe_nonce.emplace(to_byte_span<24>(nonce)); try { auto encoded = session::encrypt_for_multiple_simple( msgs, recips, - std::span{x25519_privkey, 32}, - std::span{x25519_pubkey, 32}, + to_byte_span<32>(x25519_privkey), + to_byte_span<32>(x25519_pubkey), domain, std::move(maybe_nonce), pad); @@ -304,7 +274,7 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( try { auto [priv, pub] = - session::x_keys(std::span{ed25519_secret_key, 64}); + session::detail::x_keys(to_byte_span<64>(ed25519_secret_key)); return session_encrypt_for_multiple_simple( out_len, messages, @@ -312,8 +282,8 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( n_messages, recipients, n_recipients, - priv.data(), - pub.data(), + to_unsigned(priv.data()), + to_unsigned(pub.data()), domain, nonce, pad); @@ -333,10 +303,10 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple( try { if (auto decrypted = session::decrypt_for_multiple_simple( - std::span{encoded, encoded_len}, - std::span{x25519_privkey, 32}, - std::span{x25519_pubkey, 32}, - std::span{sender_x25519_pubkey, 32}, + to_byte_span(encoded, encoded_len), + to_byte_span<32>(x25519_privkey), + to_byte_span<32>(x25519_pubkey), + to_byte_span<32>(sender_x25519_pubkey), domain)) { return to_c_buffer(*decrypted, out_len); } @@ -356,9 +326,9 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519_from try { if (auto decrypted = session::decrypt_for_multiple_simple( - std::span{encoded, encoded_len}, - std::span{ed25519_secret, 64}, - std::span{sender_x25519_pubkey, 32}, + to_byte_span(encoded, encoded_len), + to_byte_span<64>(ed25519_secret), + to_byte_span<32>(sender_x25519_pubkey), domain)) { return to_c_buffer(*decrypted, out_len); } @@ -378,9 +348,9 @@ LIBSESSION_C_API unsigned char* session_decrypt_for_multiple_simple_ed25519( try { if (auto decrypted = session::decrypt_for_multiple_simple_ed25519( - std::span{encoded, encoded_len}, - std::span{ed25519_secret, 64}, - std::span{sender_ed25519_pubkey, 32}, + to_byte_span(encoded, encoded_len), + to_byte_span<64>(ed25519_secret), + to_byte_span<32>(sender_ed25519_pubkey), domain)) { return to_c_buffer(*decrypted, out_len); } diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index caf4c32ee..b8927d037 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -175,7 +175,7 @@ Request to_request( const std::string& upload_id, const config::FileServer& config, UploadRequest upload_request) { - std::vector all_data; + std::vector all_data; while (true) { if (upload_request.is_cancelled()) @@ -284,7 +284,7 @@ file_metadata parse_upload_response(const std::string& body, size_t upload_size) return metadata; } -std::pair> parse_download_response( +std::pair> parse_download_response( std::string_view download_url, const std::vector>& headers, const std::string& body) { @@ -307,7 +307,7 @@ std::pair> parse_download_response( } } - std::vector data(body.begin(), body.end()); + auto data = to_vector(body); if (metadata.size == 0) metadata.size = data.size(); @@ -354,9 +354,10 @@ Request get_client_version( } // Generate the auth signature - auto blinded_keys = blind_version_key_pair(to_span(seckey.view())); + auto sk = ed25519::PrivKeySpan::from(to_span(seckey.view())); + auto blinded_keys = blind_version_key_pair(sk); auto timestamp = epoch_seconds(clock_now_s()); - auto signature = blind_version_sign(to_span(seckey.view()), platform, timestamp); + auto signature = blind_version_sign(sk, platform, timestamp); auto pubkey = x25519_pubkey::from_hex(DEFAULT_CONFIG.pubkey_hex); std::string blinded_pk_hex; blinded_pk_hex.reserve(66); @@ -450,7 +451,7 @@ LIBSESSION_C_API session_request_params* session_file_server_get_client_version( try { auto req = file_server::get_client_version( static_cast(platform), - network::ed25519_seckey::from_bytes({ed25519_secret, 64}), + network::ed25519_seckey::from_bytes(to_byte_span<64>(ed25519_secret)), std::chrono::milliseconds{request_timeout_ms}, (overall_timeout_ms > 0 ? std::optional{std::chrono::milliseconds{overall_timeout_ms}} diff --git a/src/network/key_types.cpp b/src/network/key_types.cpp index 1e65a0482..fcfa76644 100644 --- a/src/network/key_types.cpp +++ b/src/network/key_types.cpp @@ -40,17 +40,17 @@ std::string ed25519_pubkey::snode_address() const { legacy_pubkey legacy_seckey::pubkey() const { legacy_pubkey pk; - crypto_scalarmult_ed25519_base_noclamp(pk.data(), data()); + crypto_scalarmult_ed25519_base_noclamp(to_unsigned(pk.data()), to_unsigned(data())); return pk; }; ed25519_pubkey ed25519_seckey::pubkey() const { ed25519_pubkey pk; - crypto_sign_ed25519_sk_to_pk(pk.data(), data()); + crypto_sign_ed25519_sk_to_pk(to_unsigned(pk.data()), to_unsigned(data())); return pk; }; x25519_pubkey x25519_seckey::pubkey() const { x25519_pubkey pk; - crypto_scalarmult_curve25519_base(pk.data(), data()); + crypto_scalarmult_curve25519_base(to_unsigned(pk.data()), to_unsigned(data())); return pk; }; @@ -81,13 +81,13 @@ ed25519_pubkey parse_ed25519_pubkey(std::string_view pubkey_in) { x25519_pubkey parse_x25519_pubkey(std::string_view pubkey_in) { return parse_pubkey(pubkey_in); } -x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk) { - std::array xpk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk.data())) +x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk) { + b32 xpk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(to_unsigned(xpk.data()), to_unsigned(ed25519_pk.data()))) throw std::runtime_error{ "An error occured while attempting to convert Ed25519 pubkey to X25519; " "is the pubkey valid?"}; - return x25519_pubkey::from_bytes({xpk.data(), 32}); + return x25519_pubkey::from_bytes(xpk); } } // namespace session::network diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index c7db1f631..8043524bc 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -149,7 +149,7 @@ namespace { std::optional>> parse_error_response( uint16_t status_code, const std::optional& error_body, - std::optional> destination_pubkey) { + std::optional> destination_pubkey) { for (const auto& pattern : error_patterns) { if (pattern.code != status_code) continue; @@ -1604,7 +1604,7 @@ void OnionRequestRouter::_send_on_path( OnionPath& path, Request request, network_response_callback_t callback) { log::trace(cat, "[Request {}]: Sending on path {}", request.request_id, path.id); - std::vector encrypted_blob; + std::vector encrypted_blob; std::shared_ptr parser; try { diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index da0de8be6..5aa67cf6f 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -51,22 +51,24 @@ namespace { return *key; } - std::pair, uint16_t> remote_info_for_destination( + std::pair, uint16_t> remote_info_for_destination( const network_destination& dest, const std::string& request_id) { - std::optional, uint16_t>> result; + std::optional, uint16_t>> result; std::visit( [&result, &request_id](const T& arg) { if constexpr (std::is_same_v) { log::trace( cat, "[Request {}]: Using pre-resolved RemoteAddress.", request_id); - result.emplace(arg.view_remote_key(), arg.port()); + result.emplace( + as_span(arg.view_remote_key()).template first<32>(), + arg.port()); } else if constexpr (std::is_same_v) { log::trace( cat, "[Request {}]: Resolving service_node to RemoteAddress.", request_id); - result.emplace(arg.remote_pubkey, arg.omq_port); + result.emplace(arg.view_remote_key(), arg.omq_port); } }, dest); @@ -74,9 +76,6 @@ namespace { if (!result) throw std::runtime_error{"Invalid destination"}; - if (result->first.size() != 32) - throw std::runtime_error{"Invalid remote key"}; - return *result; } @@ -584,7 +583,7 @@ void SessionRouter::_send_proxy_request(Request request, network_response_callba } service_node proxy_node = proxy_nodes[0]; - std::vector encrypted_blob; + std::vector encrypted_blob; std::shared_ptr parser; log::debug( cat, "[Request {}]: Selected {} as proxy.", request.request_id, proxy_node.to_string()); @@ -1134,7 +1133,7 @@ void SessionRouter::_download_internal_legacy(DownloadRequest request, std::stri } void SessionRouter::_establish_tunnel( - std::span& remote_pubkey, + std::span remote_pubkey, const uint16_t remote_port, const std::string& initiating_req_id) { auto address_pubkey_hex = oxenc::to_hex(remote_pubkey); @@ -1174,9 +1173,9 @@ void SessionRouter::_establish_tunnel( // } std::string srouter_address; - srouter_address.reserve(oxenc::to_base32z_size(32UL) + ".snode"sv.size()); + srouter_address.reserve(oxenc::to_base32z_size(remote_pubkey.size()) + ".snode"sv.size()); oxenc::to_base32z( - remote_pubkey.begin(), remote_pubkey.begin() + 32, std::back_inserter(srouter_address)); + remote_pubkey.begin(), remote_pubkey.end(), std::back_inserter(srouter_address)); srouter_address += ".snode"sv; // srouter::RouterID router_id{remote_pubkey.first<32>()}; @@ -1291,7 +1290,7 @@ void SessionRouter::_send_via_tunnel( // auto test_key = // oxenc::from_base64("1n+DAM9hKyJhtXSPR5L/HdemIKPiHs8dZsPn2kEQuMs="); auto test_key // = oxenc::from_base32z("55fxd8stjrt9g6rsbftx7eesy47pj4751xjghinr3k9ffxh4ieyo"); - auto router_target = oxen::quic::RemoteAddress{test_key, "::1", tunnel.local_port}; + auto router_target = oxen::quic::RemoteAddress{as_span(test_key), "::1", tunnel.local_port}; // Construct the actual request to send std::optional remaining_overall_timeout = diff --git a/src/network/service_node.cpp b/src/network/service_node.cpp index d9d50d493..d0e5a3d03 100644 --- a/src/network/service_node.cpp +++ b/src/network/service_node.cpp @@ -58,7 +58,7 @@ service_node service_node::from_json(nlohmann::json json) { throw std::invalid_argument{ "Invalid service node json: pubkey_ed25519 is not a valid, hex pubkey"}; - std::vector pubkey; + std::vector pubkey; pubkey.reserve(32); oxenc::from_hex(pk_ed.begin(), pk_ed.end(), std::back_inserter(pubkey)); @@ -209,10 +209,8 @@ std::pair, int> service_node::process_snode_cache_bin( try { // Pubkey - std::vector pubkey; - pubkey.assign( - reinterpret_cast(current_ptr), - reinterpret_cast(current_ptr) + PK_SIZE); + std::vector pubkey; + pubkey.assign(current_ptr, current_ptr + PK_SIZE); note_ptr += PK_SIZE; // Swarm ID diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 876b918dd..39415bab2 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -1760,9 +1760,9 @@ LIBSESSION_C_API void session_network_send_request( throw std::invalid_argument( "Invalid request: Must have either 'snode_dest' or 'server_dest' set."); - std::optional> body; + std::optional> body; if (params->body && params->body_size > 0) - body.emplace(params->body, params->body + params->body_size); + body = to_vector(to_byte_span(params->body, params->body_size)); std::optional request_id; if (params->request_id) @@ -1851,9 +1851,9 @@ LIBSESSION_C_API session_upload_handle_t* session_network_upload( const auto on_complete_fn = callbacks->on_complete; const auto ctx = callbacks->ctx; - cpp_request.next_data = [next_data_fn, ctx]() -> std::vector { - std::vector buffer(64 * 1024); // 64KB chunks - size_t bytes = next_data_fn(buffer.data(), buffer.size(), ctx); + cpp_request.next_data = [next_data_fn, ctx]() -> std::vector { + std::vector buffer(64 * 1024); // 64KB chunks + size_t bytes = next_data_fn(to_unsigned(buffer.data()), buffer.size(), ctx); if (bytes == 0 || bytes == static_cast(-1)) return {}; @@ -1939,11 +1939,7 @@ LIBSESSION_C_API session_download_handle_t* session_network_download( c_meta.uploaded_timestamp = epoch_seconds(metadata.uploaded); c_meta.expiry_timestamp = epoch_seconds(metadata.expiry); - on_data_fn( - &c_meta, - reinterpret_cast(data.data()), - data.size(), - ctx); + on_data_fn(&c_meta, to_unsigned(data.data()), data.size(), ctx); }; cpp_request.on_complete = [on_complete_fn, diff --git a/src/network/session_network_types.cpp b/src/network/session_network_types.cpp index 4b40e1325..b49c8deec 100644 --- a/src/network/session_network_types.cpp +++ b/src/network/session_network_types.cpp @@ -14,7 +14,7 @@ Request::Request( std::string request_id, network_destination destination, std::string endpoint, - std::optional> body, + std::optional> body, RequestCategory category, std::chrono::milliseconds request_timeout, std::optional overall_timeout, @@ -33,7 +33,7 @@ Request::Request( Request::Request( network_destination destination, std::string endpoint, - std::optional> body, + std::optional> body, RequestCategory category, std::chrono::milliseconds request_timeout, std::optional overall_timeout, diff --git a/src/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index 9f22c8e78..f1a8e3907 100644 --- a/src/network/transport/quic_transport.cpp +++ b/src/network/transport/quic_transport.cpp @@ -4,7 +4,7 @@ #include #include -#include "session/ed25519.hpp" +#include "session/crypto/ed25519.hpp" #include "session/network/session_network_types.hpp" using namespace oxen; @@ -112,8 +112,7 @@ void QuicTransport::verify_connectivity( // Only try to establish a connection if we are the first to ask for one if (_pending_requests.count(pubkey_hex) == 0 && _pending_verification_callbacks.at(pubkey_hex).size() == 1) - _establish_connection( - {node.remote_pubkey, node.host(), node.omq_port}, request_id, category); + _establish_connection(node.to_quic_address(), request_id, category); }); } @@ -235,7 +234,7 @@ void QuicTransport::_send_request_internal(Request request, network_response_cal cat, "[Request {}]: Resolving service_node to RemoteAddress.", request_id); - remote.emplace(arg.remote_pubkey, arg.host(), arg.omq_port); + remote = arg.to_quic_address(); } }, request.destination); @@ -295,7 +294,7 @@ void QuicTransport::_establish_connection( if (!_endpoint) throw std::runtime_error{"Network is invalid"}; - auto conn_key_pair = ed25519::ed25519_key_pair(); + auto conn_key_pair = ed25519::keypair(); auto creds = quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(conn_key_pair.second)); // If we are starting a connection attempt then transition to the "connecting" state diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index dcb8b2c06..648fa3156 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -50,8 +50,8 @@ namespace detail { namespace { - std::vector encode_size(uint32_t s) { - std::vector result; + std::vector encode_size(uint32_t s) { + std::vector result; result.resize(4); oxenc::write_host_as_little(s, result.data()); return result; @@ -88,7 +88,7 @@ Builder::Builder( add_hop(n.remote_pubkey); } -void Builder::add_hop(std::span remote_key) { +void Builder::add_hop(std::span remote_key) { hops_.push_back( {network::ed25519_pubkey::from_bytes(remote_key), network::compute_x25519_pubkey(remote_key)}); @@ -121,13 +121,13 @@ void Builder::set_destination(network_destination destination) { throw std::invalid_argument{"Invalid destination type."}; } -std::vector Builder::generate_onion_blob( - const std::optional>& plaintext_body) { +std::vector Builder::generate_onion_blob( + const std::optional>& plaintext_body) { return build(_generate_payload(plaintext_body)); } -std::vector Builder::_generate_payload( - std::optional> body) const { +std::vector Builder::_generate_payload( + std::optional> body) const { // If we don't have the data required for a server request, then assume it's targeting a // service node which has a different structure (`method` is the endpoint and the body is // `params`) @@ -142,7 +142,7 @@ std::vector Builder::_generate_payload( nlohmann::json wrapped_payload = {{"method", endpoint_}, {"params", params_json}}; std::string payload_str = wrapped_payload.dump(); - return {payload_str.begin(), payload_str.end()}; + return to_vector(payload_str); } // Otherwise generate the payload for a server request @@ -174,11 +174,11 @@ std::vector Builder::_generate_payload( payload.emplace_back(session::to_string(*body)); auto result = oxenc::bt_serialize(payload); - return to_vector(result); + return to_vector(result); } -std::vector Builder::build(std::vector payload) { - std::vector blob; +std::vector Builder::build(std::vector payload) { + std::vector blob; // First hop: // @@ -223,7 +223,7 @@ std::vector Builder::build(std::vector payload) { nlohmann::json final_route; { - crypto_box_keypair(A.data(), a.data()); + crypto_box_keypair(to_unsigned(A.data()), to_unsigned(a.data())); HopEncryption e{a, A, false}; // The data we send to the destination differs depending on whether the destination is a @@ -251,7 +251,7 @@ std::vector Builder::build(std::vector payload) { }; auto control_dump = control.dump(); - auto control_span = to_span(control_dump); + auto control_span = to_span(control_dump); auto data = encode_size(payload.size()); data.insert(data.end(), payload.begin(), payload.end()); data.insert(data.end(), control_span.begin(), control_span.end()); @@ -293,7 +293,7 @@ std::vector Builder::build(std::vector payload) { data.insert(data.end(), routing_span.begin(), routing_span.end()); // Generate eph key for *this* request and encrypt it: - crypto_box_keypair(A.data(), a.data()); + crypto_box_keypair(to_unsigned(A.data()), to_unsigned(a.data())); HopEncryption e{a, A, false}; blob = e.encrypt(enc_type, data, it->second); } @@ -357,12 +357,8 @@ LIBSESSION_C_API void onion_request_builder_set_snode_destination( const char* ed25519_pubkey) { assert(builder && ip && ed25519_pubkey); - std::vector pubkey; - pubkey.reserve(32); - oxenc::from_hex(ed25519_pubkey, ed25519_pubkey + 64, std::back_inserter(pubkey)); - unbox(builder).set_destination(session::network::service_node{ - session::network::ed25519_pubkey::from_bytes(pubkey), + session::network::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), oxen::quic::ipv4{std::span(ip, 4)}, 0, quic_port, @@ -411,7 +407,7 @@ LIBSESSION_C_API bool onion_request_builder_build( try { auto& unboxed_builder = unbox(builder); - auto payload = unboxed_builder.build({payload_in, payload_in + payload_in_len}); + auto payload = unboxed_builder.build(session::to_vector(std::span{payload_in, payload_in_len})); if (unboxed_builder.final_hop_x25519_keypair) { auto key_pair = unboxed_builder.final_hop_x25519_keypair.value(); diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index f03c39b7c..1fcee7da4 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -30,7 +30,7 @@ namespace { std::array calculate_shared_secret( const network::x25519_seckey& seckey, const network::x25519_pubkey& pubkey) { std::array secret; - if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) + if (crypto_scalarmult(secret.data(), to_unsigned(seckey.data()), to_unsigned(pubkey.data())) != 0) throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); return secret; } @@ -41,7 +41,7 @@ namespace { const network::x25519_seckey& seckey, const network::x25519_pubkey& pubkey) { auto key = calculate_shared_secret(seckey, pubkey); - auto usalt = to_span(salt); + auto usalt = to_span(salt); crypto_auth_hmacsha256_state state; @@ -64,8 +64,8 @@ namespace { static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES >= crypto_scalarmult_BYTES); if (0 != crypto_scalarmult( key.data(), - local_sec.data(), - remote_pub.data())) // Use key as tmp storage for aB + to_unsigned(local_sec.data()), + to_unsigned(remote_pub.data()))) // Use key as tmp storage for aB throw std::runtime_error{"Failed to compute shared key for xchacha20"}; hash::blake2b( key, @@ -86,9 +86,9 @@ bool HopEncryption::response_long_enough(EncryptType type, size_t response_size) return false; } -std::vector HopEncryption::encrypt( +std::vector HopEncryption::encrypt( EncryptType type, - std::vector plaintext, + std::vector plaintext, const network::x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); @@ -97,9 +97,9 @@ std::vector HopEncryption::encrypt( throw std::runtime_error{"Invalid encryption type"}; } -std::vector HopEncryption::decrypt( +std::vector HopEncryption::decrypt( EncryptType type, - std::vector ciphertext, + std::vector ciphertext, const network::x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); @@ -108,8 +108,8 @@ std::vector HopEncryption::decrypt( throw std::runtime_error{"Invalid decryption type"}; } -std::vector HopEncryption::encrypt_aesgcm( - std::vector plaintext, const network::x25519_pubkey& pubKey) const { +std::vector HopEncryption::encrypt_aesgcm( + std::vector plaintext, const network::x25519_pubkey& pubKey) const { auto key = derive_symmetric_key(private_key_, pubKey); // Initialise cipher context with the key @@ -117,21 +117,21 @@ std::vector HopEncryption::encrypt_aesgcm( static_assert(key.size() == AES256_KEY_SIZE); gcm_aes256_set_key(&ctx, key.data()); - std::vector output; + std::vector output; output.resize(GCM_IV_SIZE + plaintext.size() + GCM_DIGEST_SIZE); // Start the output with the random IV, and load it into ctx auto* o = output.data(); randombytes_buf(o, GCM_IV_SIZE); - gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, o); + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, to_unsigned(o)); o += GCM_IV_SIZE; // Append encrypted data - gcm_aes256_encrypt(&ctx, plaintext.size(), o, plaintext.data()); + gcm_aes256_encrypt(&ctx, plaintext.size(), to_unsigned(o), to_unsigned(plaintext.data())); o += plaintext.size(); // Append digest - gcm_aes256_digest(&ctx, GCM_DIGEST_SIZE, o); + gcm_aes256_digest(&ctx, GCM_DIGEST_SIZE, to_unsigned(o)); o += GCM_DIGEST_SIZE; assert(o == output.data() + output.size()); @@ -139,13 +139,12 @@ std::vector HopEncryption::encrypt_aesgcm( return output; } -std::vector HopEncryption::decrypt_aesgcm( - std::vector ciphertext_, const network::x25519_pubkey& pubKey) const { - std::span ciphertext = to_span(ciphertext_); +std::vector HopEncryption::decrypt_aesgcm( + std::span ciphertext, const network::x25519_pubkey& pubKey) const { - if (!response_long_enough(EncryptType::aes_gcm, ciphertext_.size())) + if (!response_long_enough(EncryptType::aes_gcm, ciphertext.size())) throw std::invalid_argument{ - "Ciphertext data is too short: " + session::to_string(ciphertext_)}; + fmt::format("Ciphertext data is too short: {}", ciphertext.size())}; auto key = derive_symmetric_key(private_key_, pubKey); @@ -154,16 +153,17 @@ std::vector HopEncryption::decrypt_aesgcm( static_assert(key.size() == AES256_KEY_SIZE); gcm_aes256_set_key(&ctx, key.data()); - gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, ciphertext.data()); + gcm_aes256_set_iv(&ctx, GCM_IV_SIZE, to_unsigned(ciphertext.data())); ciphertext = ciphertext.subspan(GCM_IV_SIZE); auto digest_in = ciphertext.subspan(ciphertext.size() - GCM_DIGEST_SIZE); ciphertext = ciphertext.subspan(0, ciphertext.size() - GCM_DIGEST_SIZE); - std::vector plaintext; + std::vector plaintext; plaintext.resize(ciphertext.size()); - gcm_aes256_decrypt(&ctx, ciphertext.size(), plaintext.data(), ciphertext.data()); + gcm_aes256_decrypt( + &ctx, ciphertext.size(), to_unsigned(plaintext.data()), to_unsigned(ciphertext.data())); std::array digest_out; gcm_aes256_digest(&ctx, digest_out.size(), digest_out.data()); @@ -174,10 +174,10 @@ std::vector HopEncryption::decrypt_aesgcm( return plaintext; } -std::vector HopEncryption::encrypt_xchacha20( - std::vector plaintext, const network::x25519_pubkey& pubKey) const { +std::vector HopEncryption::encrypt_xchacha20( + std::vector plaintext, const network::x25519_pubkey& pubKey) const { - std::vector ciphertext; + std::vector ciphertext; ciphertext.resize( crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); @@ -194,7 +194,7 @@ std::vector HopEncryption::encrypt_xchacha20( crypto_aead_xchacha20poly1305_ietf_encrypt( c, &clen, - plaintext.data(), + to_unsigned(plaintext.data()), plaintext.size(), nullptr, 0, // additional data @@ -206,22 +206,22 @@ std::vector HopEncryption::encrypt_xchacha20( return ciphertext; } -std::vector HopEncryption::decrypt_xchacha20( - std::vector ciphertext_, const network::x25519_pubkey& pubKey) const { - std::span ciphertext = to_span(ciphertext_); +std::vector HopEncryption::decrypt_xchacha20( + std::span ciphertext_, const network::x25519_pubkey& pubKey) const { + auto ciphertext = ciphertext_; // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); ciphertext = ciphertext.subspan(nonce.size()); - if (!response_long_enough(EncryptType::xchacha20, ciphertext_.size())) + if (!response_long_enough(EncryptType::xchacha20, ciphertext.size())) throw std::invalid_argument{ "Ciphertext data is too short: " + std::string(reinterpret_cast(ciphertext_.data()))}; const auto key = xchacha20_shared_key(public_key_, private_key_, pubKey, !server_); - std::vector plaintext; + std::vector plaintext; plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); auto* m = reinterpret_cast(plaintext.data()); unsigned long long mlen; @@ -229,11 +229,11 @@ std::vector HopEncryption::decrypt_xchacha20( m, &mlen, nullptr, // nsec (always unused) - ciphertext.data(), + to_unsigned(ciphertext.data()), ciphertext.size(), nullptr, 0, // additional data - nonce.data(), + to_unsigned(nonce.data()), key.data())) throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; assert(mlen <= plaintext.size()); diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index e83640d6a..fb8cfd96a 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -9,9 +9,9 @@ namespace session::onionreq { OnionReqParser::OnionReqParser( - std::span x25519_pk, - std::span x25519_sk, - std::span req, + std::span x25519_pk, + std::span x25519_sk, + std::span req, size_t max_size) : keys{network::x25519_pubkey::from_bytes(x25519_pk), network::x25519_seckey::from_bytes(x25519_sk)}, @@ -40,12 +40,12 @@ OnionReqParser::OnionReqParser( else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - payload_ = enc.decrypt(enc_type, to_vector(ciphertext), remote_pk); + payload_ = enc.decrypt(enc_type, to_vector(ciphertext), remote_pk); } -std::vector OnionReqParser::encrypt_reply( - std::span reply) const { - return enc.encrypt(enc_type, to_vector(reply), remote_pk); +std::vector OnionReqParser::encrypt_reply( + std::span reply) const { + return enc.encrypt(enc_type, to_vector(reply), remote_pk); } } // namespace session::onionreq diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 1e2ff54cd..bd049b57c 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -8,10 +8,10 @@ #include #include "session/export.h" -#include "session/network/service_node.hpp" #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/hop_encryption.hpp" +#include "session/util.hpp" using namespace session; @@ -35,7 +35,7 @@ bool ResponseParser::response_long_enough(EncryptType enc_type, size_t response_ return HopEncryption::response_long_enough(enc_type, response_size); } -std::vector ResponseParser::decrypt(std::vector ciphertext) const { +std::vector ResponseParser::decrypt(std::vector ciphertext) const { HopEncryption d{x25519_keypair_.second, x25519_keypair_.first, false}; // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an @@ -86,7 +86,7 @@ DecryptedResponse ResponseParser::_decrypt_v3_response(const std::string& respon if (!oxenc::is_base64(base64_iv_and_ciphertext)) throw std::runtime_error{"Invalid base64 encoded IV and ciphertext."}; - std::vector iv_and_ciphertext; + std::vector iv_and_ciphertext; oxenc::from_base64( base64_iv_and_ciphertext.begin(), base64_iv_and_ciphertext.end(), @@ -185,14 +185,12 @@ LIBSESSION_C_API bool onion_request_decrypt( } session::onionreq::HopEncryption d{ - session::network::x25519_seckey::from_bytes({final_x25519_seckey, 32}), - session::network::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), + session::network::x25519_seckey::from_bytes(to_byte_span<32>(final_x25519_seckey)), + session::network::x25519_pubkey::from_bytes(to_byte_span<32>(final_x25519_pubkey)), false}; - std::vector result; - std::vector ciphertext; - ciphertext.reserve(ciphertext_len); - ciphertext.assign(ciphertext_, ciphertext_ + ciphertext_len); + std::vector result; + std::vector ciphertext{to_vector(to_byte_span(ciphertext_, ciphertext_len))}; // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an // error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this @@ -201,14 +199,15 @@ LIBSESSION_C_API bool onion_request_decrypt( result = d.decrypt( enc_type, ciphertext, - session::network::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + session::network::x25519_pubkey::from_bytes( + to_byte_span<32>(destination_x25519_pubkey))); } catch (...) { if (enc_type == session::onionreq::EncryptType::xchacha20) result = d.decrypt( session::onionreq::EncryptType::aes_gcm, ciphertext, session::network::x25519_pubkey::from_bytes( - {destination_x25519_pubkey, 32})); + to_byte_span<32>(destination_x25519_pubkey))); else return false; } diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index bded938b3..b311950f8 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -136,7 +136,7 @@ bool json_require_fixed_bytes_from_hex( const nlohmann::json& j, std::string_view key, std::vector& errors, - std::span dest) { + std::span dest) { auto hex = json_require(j, key, errors); if (hex.starts_with("0X") || hex.starts_with("0x")) hex = hex.substr(2); @@ -188,31 +188,11 @@ std::string AddProPaymentRequest::to_json() const { MasterRotatingSignatures AddProPaymentRequest::build_sigs( std::uint8_t version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - - cleared_uc64 rotating_from_seed; - if (rotating_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 rotating_pubkey; - crypto_sign_ed25519_seed_keypair( - rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); - rotating_privkey = rotating_from_seed; - } else if (rotating_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid rotating_privkey: expected 32 or 64 bytes"}; - } - + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { if (payment_tx_provider == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE) { if (payment_tx_order_id.empty()) throw std::invalid_argument{ @@ -230,59 +210,26 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( auto hash_to_sign = hash::blake2b_pers<32>( ADD_PRO_PAYMENT_PERS, version, - master_privkey.subspan( - crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), - rotating_privkey.subspan( - crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), - static_cast(payment_tx_provider), + master_privkey.pubkey(), + rotating_privkey.pubkey(), + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); - // Sign the hash with both keys MasterRotatingSignatures result = {}; - crypto_sign_ed25519_detached( - result.master_sig.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - crypto_sign_ed25519_detached( - result.rotating_sig.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - rotating_privkey.data()); + result.master_sig = ed25519::sign(master_privkey, hash_to_sign); + result.rotating_sig = ed25519::sign(rotating_privkey, hash_to_sign); return result; } std::string AddProPaymentRequest::build_to_json( std::uint8_t version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - - cleared_uc64 rotating_from_seed; - if (rotating_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 rotating_pubkey; - crypto_sign_ed25519_seed_keypair( - rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); - rotating_privkey = rotating_from_seed; - } else if (rotating_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid rotating_privkey: expected 32 or 64 bytes"}; - } - - MasterRotatingSignatures sigs = AddProPaymentRequest::build_sigs( + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { + auto sigs = AddProPaymentRequest::build_sigs( version, master_privkey, rotating_privkey, @@ -292,25 +239,15 @@ std::string AddProPaymentRequest::build_to_json( AddProPaymentRequest request = {}; request.version = version; - std::memcpy( - request.master_pkey.data(), - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - std::memcpy( - request.rotating_pkey.data(), - rotating_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); + std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); + std::ranges::copy(rotating_privkey.pubkey(), request.rotating_pkey.begin()); request.payment_tx.provider = payment_tx_provider; - request.payment_tx.payment_id = std::string( - reinterpret_cast(payment_tx_payment_id.data()), - payment_tx_payment_id.size()); - request.payment_tx.order_id = std::string( - reinterpret_cast(payment_tx_order_id.data()), payment_tx_order_id.size()); + request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); + request.payment_tx.order_id = to_string_view(payment_tx_order_id); request.master_sig = sigs.master_sig; request.rotating_sig = sigs.rotating_sig; - std::string result = request.to_json(); - return result; + return request.to_json(); } AddProPaymentOrGenerateProProofResponse AddProPaymentOrGenerateProProofResponse::parse( @@ -361,30 +298,10 @@ std::string GenerateProProofRequest::to_json() const { MasterRotatingSignatures GenerateProProofRequest::build_sigs( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, std::chrono::sys_time unix_ts) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == 32) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != 64) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - - cleared_uc64 rotating_from_seed; - if (rotating_privkey.size() == 32) { - uc32 rotating_pubkey; - crypto_sign_ed25519_seed_keypair( - rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); - rotating_privkey = rotating_from_seed; - } else if (rotating_privkey.size() != 64) { - throw std::invalid_argument{"Invalid rotating_privkey: expected 32 or 64 bytes"}; - } - // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 uint8_t version = 0; @@ -392,72 +309,33 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( auto hash_to_sign = hash::blake2b_pers<32>( GENERATE_PROOF_PERS, version, - master_privkey.last<32>(), - rotating_privkey.last<32>(), + master_privkey.pubkey(), + rotating_privkey.pubkey(), unix_ts_ms); - // Sign the hash with both keys MasterRotatingSignatures result = {}; - crypto_sign_ed25519_detached( - result.master_sig.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - crypto_sign_ed25519_detached( - result.rotating_sig.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - rotating_privkey.data()); + result.master_sig = ed25519::sign(master_privkey, hash_to_sign); + result.rotating_sig = ed25519::sign(rotating_privkey, hash_to_sign); return result; } std::string GenerateProProofRequest::build_to_json( std::uint8_t request_version, - std::span master_privkey, - std::span rotating_privkey, + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, std::chrono::sys_time unix_ts) { - // Rederive keys from 32 byte seed if given - cleared_uc64 master_from_seed; - if (master_privkey.size() == 32) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != 64) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - - cleared_uc64 rotating_from_seed; - if (rotating_privkey.size() == 32) { - uc32 rotating_pubkey; - crypto_sign_ed25519_seed_keypair( - rotating_pubkey.data(), rotating_from_seed.data(), rotating_privkey.data()); - rotating_privkey = rotating_from_seed; - } else if (rotating_privkey.size() != 64) { - throw std::invalid_argument{"Invalid rotating_privkey: expected 32 or 64 bytes"}; - } - - MasterRotatingSignatures sigs = GenerateProProofRequest::build_sigs( + auto sigs = GenerateProProofRequest::build_sigs( request_version, master_privkey, rotating_privkey, unix_ts); GenerateProProofRequest request = {}; request.version = request_version; - std::memcpy( - request.master_pkey.data(), - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); - std::memcpy( - request.rotating_pkey.data(), - rotating_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); + std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); + std::ranges::copy(rotating_privkey.pubkey(), request.rotating_pkey.begin()); request.unix_ts = unix_ts; request.master_sig = sigs.master_sig; request.rotating_sig = sigs.rotating_sig; - std::string result = request.to_json(); - return result; + return request.to_json(); } std::string GetProRevocationsRequest::to_json() const { @@ -531,64 +409,33 @@ std::string GetProDetailsRequest::to_json() const { return result; } -uc64 GetProDetailsRequest::build_sig( +b64 GetProDetailsRequest::build_sig( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_time unix_ts, uint32_t count) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 uint64_t unix_ts_ms = epoch_ms(unix_ts); auto hash_to_sign = hash::blake2b_pers<32>( - GET_PRO_DETAILS_PERS, version, master_privkey.last<32>(), unix_ts_ms, count); - - // Sign the hash - uc64 result = {}; - crypto_sign_ed25519_detached( - result.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - return result; + GET_PRO_DETAILS_PERS, version, master_privkey.pubkey(), unix_ts_ms, count); + + return ed25519::sign(master_privkey, hash_to_sign); } std::string GetProDetailsRequest::build_to_json( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_time unix_ts, uint32_t count) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - GetProDetailsRequest request = {}; request.version = version; - memcpy(request.master_pkey.data(), - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); + std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); request.master_sig = GetProDetailsRequest::build_sig(version, master_privkey, unix_ts, count); request.unix_ts = unix_ts; request.count = count; - std::string result = request.to_json(); - return result; + return request.to_json(); } GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { @@ -756,24 +603,14 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { return result; } -uc64 SetPaymentRefundRequestedRequest::build_sig( +b64 SetPaymentRefundRequestedRequest::build_sig( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_time unix_ts, std::chrono::sys_time refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - cleared_uc64 master_from_seed; - if (master_privkey.size() == crypto_sign_ed25519_SEEDBYTES) { - uc32 master_pubkey; - crypto_sign_ed25519_seed_keypair( - master_pubkey.data(), master_from_seed.data(), master_privkey.data()); - master_privkey = master_from_seed; - } else if (master_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { - throw std::invalid_argument{"Invalid master_privkey: expected 32 or 64 bytes"}; - } - + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 uint64_t unix_ts_ms = epoch_ms(unix_ts); @@ -781,34 +618,25 @@ uc64 SetPaymentRefundRequestedRequest::build_sig( auto hash_to_sign = hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, version, - master_privkey.subspan( - crypto_sign_ed25519_SEEDBYTES, crypto_sign_ed25519_PUBLICKEYBYTES), + master_privkey.pubkey(), unix_ts_ms, refund_requested_unix_ts_ms, - static_cast(payment_tx_provider), + static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); - // Sign the hash - uc64 result = {}; - crypto_sign_ed25519_detached( - result.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - return result; + return ed25519::sign(master_privkey, hash_to_sign); } std::string SetPaymentRefundRequestedRequest::build_to_json( uint8_t version, - std::span master_privkey, + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_time unix_ts, std::chrono::sys_time refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - uc64 sig = SetPaymentRefundRequestedRequest::build_sig( + std::span payment_tx_payment_id, + std::span payment_tx_order_id) { + auto sig = SetPaymentRefundRequestedRequest::build_sig( version, master_privkey, unix_ts, @@ -819,22 +647,15 @@ std::string SetPaymentRefundRequestedRequest::build_to_json( SetPaymentRefundRequestedRequest request = {}; request.version = version; - std::memcpy( - request.master_pkey.data(), - master_privkey.data() + crypto_sign_ed25519_SEEDBYTES, - crypto_sign_ed25519_PUBLICKEYBYTES); + std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); request.payment_tx.provider = payment_tx_provider; - request.payment_tx.payment_id = std::string( - reinterpret_cast(payment_tx_payment_id.data()), - payment_tx_payment_id.size()); - request.payment_tx.order_id = std::string( - reinterpret_cast(payment_tx_order_id.data()), payment_tx_order_id.size()); + request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); + request.payment_tx.order_id = to_string_view(payment_tx_order_id); request.master_sig = sig; request.unix_ts = unix_ts; request.refund_requested_unix_ts = refund_requested_unix_ts; - std::string result = request.to_json(); - return result; + return request.to_json(); } std::string SetPaymentRefundRequestedRequest::to_json() const { @@ -888,6 +709,7 @@ SetPaymentRefundRequestedResponse SetPaymentRefundRequestedResponse::parse(std:: } } // namespace session::pro_backend +using namespace session; using namespace session::pro_backend; /// Define a string8 from a c-string literal. The string should not be modified as it'll live in the @@ -910,16 +732,13 @@ session_pro_backend_add_pro_payment_request_build_sigs( const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { - // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::span payment_tx_payment_id_span( - payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span( - payment_tx_order_id, payment_tx_order_id_len); - session_pro_backend_master_rotating_signatures result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; + auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); + auto sigs = AddProPaymentRequest::build_sigs( request_version, master_span, @@ -950,15 +769,12 @@ session_pro_backend_add_pro_payment_request_build_to_json( size_t payment_tx_order_id_len) { session_pro_backend_to_json result = {}; - // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::span payment_tx_payment_id_span( - payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span( - payment_tx_order_id, payment_tx_order_id_len); - try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; + auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); + std::string json = AddProPaymentRequest::build_to_json( request_version, master_span, @@ -984,13 +800,11 @@ session_pro_backend_generate_pro_proof_request_build_sigs( size_t rotating_privkey_len, uint64_t unix_ts_ms) { - // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::chrono::milliseconds ts{unix_ts_ms}; - session_pro_backend_master_rotating_signatures result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; + std::chrono::milliseconds ts{unix_ts_ms}; auto sigs = GenerateProProofRequest::build_sigs( request_version, master_span, @@ -1013,13 +827,11 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build const unsigned char* rotating_privkey, size_t rotating_privkey_len, uint64_t unix_ts_ms) { - // Convert C inputs to C++ types - std::span master_span(master_privkey, master_privkey_len); - std::span rotating_span(rotating_privkey, rotating_privkey_len); - std::chrono::milliseconds ts{unix_ts_ms}; - session_pro_backend_to_json result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; + std::chrono::milliseconds ts{unix_ts_ms}; auto json = GenerateProProofRequest::build_to_json( request_version, master_span, @@ -1040,12 +852,10 @@ session_pro_backend_get_pro_details_request_build_sig( size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) { - // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; - session_pro_backend_signature result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; auto sig = GetProDetailsRequest::build_sig(request_version, master_span, ts, count); std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; @@ -1062,12 +872,10 @@ session_pro_backend_get_pro_details_request_build_to_json( size_t master_privkey_len, uint64_t unix_ts_ms, uint32_t count) { - // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; - session_pro_backend_to_json result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; auto json = GetProDetailsRequest::build_to_json(request_version, master_span, ts, count); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; @@ -1431,15 +1239,12 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r size_t payment_tx_payment_id_len, const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { - // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; std::chrono::sys_time refund_requested_unix_ts{ std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - std::span payment_tx_payment_id_span( - payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span( - payment_tx_order_id, payment_tx_order_id_len); + auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_signature result = {}; try { @@ -1472,15 +1277,12 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { - // Convert C inputs to C++ types - std::span master_span{master_privkey, master_privkey_len}; + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; std::chrono::sys_time refund_requested_unix_ts{ std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - std::span payment_tx_payment_id_span( - payment_tx_payment_id, payment_tx_payment_id_len); - std::span payment_tx_order_id_span( - payment_tx_order_id, payment_tx_order_id_len); + auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); session_pro_backend_to_json result = {}; try { diff --git a/src/random.cpp b/src/random.cpp index 4ac7ed4f4..d602b2936 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -22,8 +22,13 @@ void fill(std::span buf) { fill(std::span{reinterpret_cast(buf.data()), buf.size()}); } -std::vector random(size_t size) { - std::vector result; +void fill_deterministic(std::span buf, std::span seed) { + static_assert(seed.extent == randombytes_SEEDBYTES); + randombytes_buf_deterministic(to_unsigned(buf.data()), buf.size(), to_unsigned(seed.data())); +} + +std::vector random(size_t size) { + std::vector result; result.resize(size); fill(result); return result; @@ -65,7 +70,7 @@ extern "C" { LIBSESSION_C_API unsigned char* session_random(size_t size) { auto* ret = static_cast(malloc(size)); - session::random::fill(std::span{ret, size}); + session::random::fill(std::span{reinterpret_cast(ret), size}); return ret; } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 10ee85584..38f33e754 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,20 +1,13 @@ #include "session/session_encrypt.hpp" -#include +#include #include #include #include #include #include -#include -#include -#include #include -#include -#include #include -#include -#include #include #include @@ -27,7 +20,11 @@ #include "internal-util.hpp" #include "session/blinding.hpp" #include "session/clock.hpp" +#include "session/crypto/x25519.hpp" +#include "session/crypto/ed25519.hpp" +#include "session/encrypt.hpp" #include "session/hash.hpp" +#include "session/crypto/mlkem768.hpp" #include "session/random.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" @@ -83,23 +80,23 @@ constexpr auto V2_MSG_SIG_PERS = "SessionV2Message"_b2b_pers; // SHA3-256(ssₘ || ssₓ || E || X || V2_XWING_LABEL) produces the combined X-Wing shared secret. constexpr auto V2_XWING_LABEL = // R"(\./)" - R"(/^\)"_uc; + R"(/^\)"_bytes; // SHAKE256 domain prefix for deriving the XChaCha20+Poly1305 key and nonce from the X-Wing SS. -constexpr auto V2_SS_DOMAIN = "SessionV2MessageSS"_uc; +constexpr auto V2_SS_DOMAIN = "SessionV2MessageSS"_bytes; // Shared v2 wire-format layout constants (used in both encrypt and decrypt) static constexpr size_t V2_AEAD_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_ABYTES; static constexpr size_t V2_NONCE_SIZE = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; -static constexpr size_t V2_HEADER_SIZE = 2 + 2 + 32 + MLKEM768_CIPHERTEXTBYTES; +static constexpr size_t V2_HEADER_SIZE = 2 + 2 + 32 + mlkem768::CIPHERTEXTBYTES; static constexpr size_t V2_OUTER_OVERHEAD = V2_HEADER_SIZE + V2_AEAD_OVERHEAD; static constexpr size_t V2_MIN_FINAL_SIZE = ((V2_OUTER_OVERHEAD + 256 + 255) / 256) * 256; // Validates the v2 ciphertext prefix and minimum size; throws std::runtime_error on failure. -static void v2_check_header(std::span ciphertext) { +static void v2_check_header(std::span ciphertext) { if (ciphertext.size() < V2_MIN_FINAL_SIZE) throw std::runtime_error{"v2 ciphertext is too short"}; - if (ciphertext[0] != 0x00 || ciphertext[1] != 0x02) + if (ciphertext[0] != std::byte{0x00} || ciphertext[1] != std::byte{0x02}) throw std::runtime_error{"v2 ciphertext has wrong version prefix"}; } @@ -107,14 +104,14 @@ static void v2_check_header(std::span ciphertext) { // key_buf (overwriting ssm) and n (V2_NONCE_SIZE B) into nonce_out. Callers are responsible for // storing these outputs in cleared buffers. E is the ephemeral X25519 pubkey; X is the PFS pubkey. static void v2_derive_xwing_key_nonce( - std::span key_buf, - std::span nonce_out, - std::span ssx, - std::span E, - std::span X) { - cleared_uc32 ss; - hash::sha3_256(ss, key_buf, ssx, E, X, V2_XWING_LABEL); + std::span key_buf, + std::span nonce_out, + std::span ssx, + std::span E, + std::span X) { + auto ss = hash::sha3_256<32>(key_buf, ssx, E, X, V2_XWING_LABEL); hash::shake256(V2_SS_DOMAIN, ss)(key_buf, nonce_out); + sodium_memzero(ss.data(), ss.size()); } // Computes the 2-byte Key Indicator Shared Secret (KISS): @@ -122,30 +119,28 @@ static void v2_derive_xwing_key_nonce( // On encrypt: sender holds ephemeral secret e, DH partner is long-term S → call with // encrypting=true On decrypt: recipient holds long-term secret s, DH partner is ephemeral E → call // with encrypting=false -static std::array v2_kiss( - std::span sec, - std::span E, - std::span S, +static std::array v2_kiss( + std::span sec, + std::span E, + std::span S, bool encrypting) { - cleared_uc32 dh; - if (0 != crypto_scalarmult(dh.data(), sec.data(), encrypting ? S.data() : E.data())) - throw std::runtime_error{"X25519 DH (KISS) failed"}; + auto dh = x25519::scalarmult(sec, encrypting ? S : E); return hash::blake2b_key_pers<2>(dh, V2_KISS_PERS, E, S); } -std::vector sign_for_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message) { +std::vector sign_for_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message) { // If prefixed, drop it (and do this for the caller, too) so that everything after this // doesn't need to worry about whether it is prefixed or not. - if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == 0x05) + if (recipient_pubkey.size() == 33 && recipient_pubkey.front() == std::byte{0x05}) recipient_pubkey = recipient_pubkey.subspan(1); else if (recipient_pubkey.size() != 32) throw std::invalid_argument{ "Invalid recipient_pubkey: expected 32 bytes (33 with 05 prefix)"}; - std::vector buf; + std::vector buf; buf.reserve(message.size() + 96); // 32+32 now, but 32+64 when we reuse it for the sealed box buf.insert(buf.end(), message.begin(), message.end()); buf.insert( @@ -154,10 +149,7 @@ std::vector sign_for_recipient( ed25519_privkey.end()); // [32:] of a libsodium full seed value is the *pubkey* buf.insert(buf.end(), recipient_pubkey.begin(), recipient_pubkey.end()); - uc64 sig; - if (0 != crypto_sign_ed25519_detached( - sig.data(), nullptr, buf.data(), buf.size(), ed25519_privkey.data())) - throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + auto sig = ed25519::sign(ed25519_privkey, buf); // We have M||A||Y for the sig, but now we want M||A||SIG so drop Y then append SIG: buf.resize(buf.size() - 32); @@ -166,12 +158,12 @@ std::vector sign_for_recipient( return buf; } -static constexpr auto BOX_HASHKEY = "SessionBoxEphemeralHashKey"_uc; +static constexpr auto BOX_HASHKEY = "SessionBoxEphemeralHashKey"_bytes; -std::vector encrypt_for_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message) { +std::vector encrypt_for_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message) { auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); @@ -180,19 +172,17 @@ std::vector encrypt_for_recipient( recipient_pubkey.subspan(1); // sign_for_recipient already checked that this is the // proper 0x05 prefix when present. - std::vector result; + std::vector result; result.resize(signed_msg.size() + crypto_box_SEALBYTES); - if (0 != crypto_box_seal( - result.data(), signed_msg.data(), signed_msg.size(), recipient_pubkey.data())) - throw std::runtime_error{"Sealed box encryption failed"}; + encrypt::box_seal(result, signed_msg, recipient_pubkey.first<32>()); return result; } -std::vector encrypt_for_recipient_deterministic( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span recipient_pubkey, - std::span message) { +std::vector encrypt_for_recipient_deterministic( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span recipient_pubkey, + std::span message) { auto signed_msg = sign_for_recipient(ed25519_privkey, recipient_pubkey, message); @@ -202,36 +192,31 @@ std::vector encrypt_for_recipient_deterministic( // To make our ephemeral seed we're going to hash: SENDER_SEED || RECIPIENT_PK || MESSAGE with a // keyed blake2b hash. - cleared_uchars seed; + cleared_b32 seed; hash::blake2b_key( seed, BOX_HASHKEY, ed25519_privkey.seed(), recipient_pubkey.first(32), message); - cleared_uchars eph_sk; - cleared_uchars eph_pk; - - crypto_box_seed_keypair(eph_pk.data(), eph_sk.data(), seed.data()); + auto [eph_pk, eph_sk] = x25519::seed_keypair(seed); // The nonce for a sealed box is not passed but is implicitly defined as the (unkeyed) blake2b // hash of: // EPH_PUBKEY || RECIPIENT_PUBKEY - cleared_uchars nonce; + std::array nonce; hash::blake2b(nonce, eph_pk, recipient_pubkey); // A sealed box is a regular box (using the ephermal keys and nonce), but with the ephemeral // pubkey prepended: static_assert(crypto_box_SEALBYTES == crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES); - std::vector result; + std::vector result; result.resize(crypto_box_SEALBYTES + signed_msg.size()); - std::memcpy(result.data(), eph_pk.data(), crypto_box_PUBLICKEYBYTES); - if (0 != crypto_box_easy( - result.data() + crypto_box_PUBLICKEYBYTES, - signed_msg.data(), - signed_msg.size(), - nonce.data(), - recipient_pubkey.data(), - eph_sk.data())) - throw std::runtime_error{"Crypto box encryption failed"}; + std::ranges::copy(eph_pk, result.begin()); + encrypt::box_easy( + std::span{result}.subspan(crypto_box_PUBLICKEYBYTES), + signed_msg, + nonce, + recipient_pubkey.first<32>(), + eph_sk); return result; } @@ -239,16 +224,16 @@ std::vector encrypt_for_recipient_deterministic( // Builds and returns a complete v2 DM wire-format ciphertext from already-derived header fields // and encryption key material. Used by both encrypt_for_recipient_v2 (PFS+PQ) and // encrypt_for_recipient_v2_nopfs (non-PFS fallback). -static std::vector v2_encrypt_inner( - std::array ki, - std::span E, - std::span outer_ct, - std::span enc_key, - std::span enc_nonce, - const Ed25519PrivKeySpan& sender_ed25519_privkey, - std::span recipient_session_id, - std::span content, - const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { +static std::vector v2_encrypt_inner( + std::array ki, + std::span E, + std::span outer_ct, + std::span enc_key, + std::span enc_nonce, + const ed25519::PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span content, + const ed25519::OptionalPrivKeySpan& pro_ed25519_privkey) { auto sender_ed_pk = sender_ed25519_privkey.pubkey(); @@ -278,14 +263,14 @@ static std::vector v2_encrypt_inner( // Allocate result (zero-initialized so padding bytes are already 0), write header, // build inner dict directly into result buffer, then encrypt in-place. // (c == m is explicitly supported by libsodium for AEAD functions) - std::vector result(final_size, 0); + std::vector result(final_size, std::byte{0}); - result[0] = 0x00; - result[1] = 0x02; + result[0] = std::byte{0x00}; + result[1] = std::byte{0x02}; result[2] = ki[0]; result[3] = ki[1]; std::memcpy(result.data() + 4, E.data(), 32); - std::memcpy(result.data() + 36, outer_ct.data(), MLKEM768_CIPHERTEXTBYTES); + std::memcpy(result.data() + 36, outer_ct.data(), mlkem768::CIPHERTEXTBYTES); { oxenc::bt_dict_producer dict{ @@ -293,94 +278,68 @@ static std::vector v2_encrypt_inner( dict.append("S", sender_ed_pk); dict.append("c", content); // "~" signs BLAKE2b-64(body-so-far, key=recipient_session_id_33B, pers="SessionV2Message") - dict.append_signature("~", [&](std::span body) { - cleared_uc64 h; + dict.append_signature("~", [&](std::span body) { + cleared_b64 h; hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); - uc64 sig; - if (0 != - crypto_sign_ed25519_detached( - sig.data(), nullptr, h.data(), h.size(), sender_ed25519_privkey.data())) - throw std::runtime_error{"Failed to sign v2 message"}; - return sig; + return ed25519::sign(sender_ed25519_privkey, h); }); if (pro_ed25519_privkey) - dict.append_signature("~P", [&](std::span body) { - uc64 sig; - if (0 != crypto_sign_ed25519_detached( - sig.data(), - nullptr, - body.data(), - body.size(), - pro_ed25519_privkey->data())) - throw std::runtime_error{"Failed to sign v2 pro signature"}; - return sig; + dict.append_signature("~P", [&](std::span body) { + return ed25519::sign(*pro_ed25519_privkey, body); }); assert(dict.view().size() == inner_dict_size); } - if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - result.data() + V2_HEADER_SIZE, // c (output, in-place) - nullptr, - result.data() + V2_HEADER_SIZE, // m (input, same buffer) - padded_inner_size, - nullptr, - 0, - nullptr, - enc_nonce.data(), // 24-byte nonce - enc_key.data())) // key - throw std::runtime_error{"v2 message encryption failed"}; + // In-place AEAD encrypt (libsodium explicitly supports c == m) + encrypt::xchacha20poly1305_encrypt( + std::span{result}.subspan(V2_HEADER_SIZE), + std::span{result}.subspan(V2_HEADER_SIZE, padded_inner_size), + enc_nonce, + enc_key); return result; } -std::vector encrypt_for_recipient_v2( - const Ed25519PrivKeySpan& sender_ed25519_privkey, - std::span recipient_session_id, - std::span recipient_account_x25519, - std::span recipient_account_mlkem768, - std::span content, - const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { +std::vector encrypt_for_recipient_v2( + const ed25519::PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span recipient_account_x25519, + std::span recipient_account_mlkem768, + std::span content, + const ed25519::OptionalPrivKeySpan& pro_ed25519_privkey) { // S = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) - std::span S{recipient_session_id.data() + 1, 32}; + std::span S{recipient_session_id.data() + 1, 32}; // Step 1: Generate ephemeral X25519 keypair e/E - cleared_uc32 e; - uc32 E; - crypto_box_keypair(E.data(), e.data()); + auto [E, e] = x25519::keypair(); // Three cleared buffers for key material: // enc_key_buf: ML-KEM shared secret ssm (step 4) → SHAKE256-derived enc key k (step 6) // ssx_buf: eS DH result (step 2) → ML-KEM coins (step 4) → ssx DH result (step 5) // enc_nonce: SHAKE256-derived enc nonce n (step 6) - cleared_uc32 enc_key_buf; - cleared_uc32 ssx_buf; - cleared_uchars enc_nonce; + cleared_b32 enc_key_buf; + cleared_b32 ssx_buf; // Step 2: KISS = BLAKE2b_2(E || S, key=eS, pers="Session-Msg-KISS") // eS is the X25519 DH with the long-term key, used only for cheap key indicator obfuscation auto kiss = v2_kiss(e, E, S, /*encrypting=*/true); // Step 3: ki = M[0:2] ⊕ kiss (encrypted key indicator; lets recipient quickly identify key) - std::array ki{ - static_cast(recipient_account_mlkem768[0] ^ kiss[0]), - static_cast(recipient_account_mlkem768[1] ^ kiss[1])}; + std::array ki{ + recipient_account_mlkem768[0] ^ kiss[0], + recipient_account_mlkem768[1] ^ kiss[1]}; // Step 4: ML-KEM-768 encapsulate: ssₘ, mlkem_ct = Encapsulate(M) - std::array mlkem_ct; + std::array mlkem_ct; random::fill(ssx_buf); // repurpose ssx_buf as random ML-KEM coins - if (0 != sr_mlkem768_enc_derand( - mlkem_ct.data(), - enc_key_buf.data(), - recipient_account_mlkem768.data(), - ssx_buf.data())) - throw std::runtime_error{"ML-KEM-768 encapsulation failed"}; + mlkem768::encapsulate(mlkem_ct, enc_key_buf, recipient_account_mlkem768, ssx_buf); // Step 5: ssx = eX (X25519 DH with account PFS key X, not long-term key S) - if (0 != crypto_scalarmult(ssx_buf.data(), e.data(), recipient_account_x25519.data())) - throw std::runtime_error{"X25519 DH (account key) failed"}; + x25519::scalarmult(ssx_buf, e, recipient_account_x25519); // Step 6: X-Wing KDF → enc key k (in enc_key_buf) and enc nonce n (in enc_nonce) + std::array enc_nonce; v2_derive_xwing_key_nonce(enc_key_buf, enc_nonce, ssx_buf, E, recipient_account_x25519); return v2_encrypt_inner( @@ -395,65 +354,58 @@ std::vector encrypt_for_recipient_v2( pro_ed25519_privkey); } -std::array decrypt_incoming_v2_prefix( - std::span x25519_sec, - std::span x25519_pub, - std::span ciphertext) { +std::array decrypt_incoming_v2_prefix( + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext) { v2_check_header(ciphertext); auto E = ciphertext.subspan<4, 32>(); auto kiss = v2_kiss(x25519_sec, E, x25519_pub, /*encrypting=*/false); - return {static_cast(ciphertext[2] ^ kiss[0]), - static_cast(ciphertext[3] ^ kiss[1])}; + return {ciphertext[2] ^ kiss[0], ciphertext[3] ^ kiss[1]}; } // Decrypts the v2 AEAD payload and parses the inner bt-encoded dict. Used by both // decrypt_incoming_v2 (PFS+PQ) and decrypt_incoming_v2_nopfs (non-PFS fallback). // Throws DecryptV2Error on AEAD failure; std::runtime_error on structural/format errors. static DecryptV2Result v2_aead_decrypt_and_parse( - std::span recipient_session_id, - std::span key, - std::span nonce, - std::span ciphertext) { + std::span recipient_session_id, + std::span key, + std::span nonce, + std::span ciphertext) { size_t enc_size = ciphertext.size() - V2_HEADER_SIZE; - std::vector plain(enc_size - V2_AEAD_OVERHEAD); - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - plain.data(), - nullptr, - nullptr, - ciphertext.data() + V2_HEADER_SIZE, - enc_size, - nullptr, - 0, - nonce.data(), - key.data())) + std::vector plain(enc_size - V2_AEAD_OVERHEAD); + if (!encrypt::xchacha20poly1305_decrypt( + plain, + ciphertext.subspan(V2_HEADER_SIZE, enc_size), + nonce, + key)) throw DecryptV2Error{"v2 message decryption failed"}; // Strip zero padding from end (the plaintext was padded to a multiple of 256 bytes) - while (!plain.empty() && plain.back() == 0) + while (!plain.empty() && plain.back() == std::byte{0}) plain.pop_back(); // Parse the bencoded inner dict oxenc::bt_dict_consumer dict{plain}; - auto sender_ed_pk = dict.require_span("S"); - auto content_sv = dict.require_span("c"); + auto sender_ed_pk = dict.require_span("S"); + auto content_sv = dict.require_span("c"); // Verify the Ed25519 signature over BLAKE2b(body, key=recipient_session_id, pers=…) dict.require_signature( - "~", [&](std::span body, std::span sig) { + "~", [&](std::span body, std::span sig) { if (sig.size() != 64) throw std::runtime_error{"v2 message signature has wrong size"}; - uc64 h; + b64 h; hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); - if (0 != crypto_sign_ed25519_verify_detached( - sig.data(), h.data(), h.size(), sender_ed_pk.data())) + if (!ed25519::verify(sig.first<64>(), sender_ed_pk, h)) throw std::runtime_error{"v2 message signature verification failed"}; }); // Optional "~P" pro signature. Extracted but not verified here — the Pro public key is // inside the protobuf Content, so verification is deferred to the message parsing layer. - std::optional> pro_sig; + std::optional pro_sig; if (dict.skip_until("~P")) dict.consume_signature([&](std::span, std::span sig) { if (sig.size() != 64) @@ -464,13 +416,11 @@ static DecryptV2Result v2_aead_decrypt_and_parse( dict.finish(); // Convert sender Ed25519 pubkey to X25519 and build the 33-byte session ID - std::array sender_x25519; - if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x25519.data(), sender_ed_pk.data())) - throw std::runtime_error{"sender ed25519 pubkey is invalid"}; + b32 sender_x25519 = ed25519::pk_to_x25519(sender_ed_pk); DecryptV2Result result; result.content.assign(content_sv.begin(), content_sv.end()); - result.sender_session_id[0] = 0x05; + result.sender_session_id[0] = std::byte{0x05}; std::ranges::copy(sender_x25519, result.sender_session_id.begin() + 1); if (pro_sig) std::memcpy(result.pro_signature.emplace().data(), pro_sig->data(), 64); @@ -478,27 +428,26 @@ static DecryptV2Result v2_aead_decrypt_and_parse( } DecryptV2Result decrypt_incoming_v2( - std::span recipient_session_id, - std::span account_pfs_x25519_sec, - std::span account_pfs_x25519_pub, - std::span account_pfs_mlkem768_sec, - std::span ciphertext) { + std::span recipient_session_id, + std::span account_pfs_x25519_sec, + std::span account_pfs_x25519_pub, + std::span account_pfs_mlkem768_sec, + std::span ciphertext) { v2_check_header(ciphertext); auto E = ciphertext.subspan<4, 32>(); - auto mlkem_ct = ciphertext.subspan<36, MLKEM768_CIPHERTEXTBYTES>(); + auto mlkem_ct = ciphertext.subspan<36, mlkem768::CIPHERTEXTBYTES>(); - cleared_uc32 key_buf; // ssm → k - cleared_uc32 ssx_buf; - cleared_uchars nonce; + cleared_b32 key_buf; // ssm → k + cleared_b32 ssx_buf; + std::array nonce; // Step 1: ML-KEM-768 decapsulate → shared secret ssm in key_buf - if (0 != sr_mlkem768_dec(key_buf.data(), mlkem_ct.data(), account_pfs_mlkem768_sec.data())) + if (!mlkem768::decapsulate(key_buf, mlkem_ct, account_pfs_mlkem768_sec)) throw DecryptV2Error{"ML-KEM-768 decapsulation failed"}; // Step 2: X25519 DH with account PFS key → shared secret ssx in ssx_buf - if (0 != crypto_scalarmult(ssx_buf.data(), account_pfs_x25519_sec.data(), E.data())) - throw DecryptV2Error{"X25519 DH (account key) failed"}; + x25519::scalarmult(ssx_buf, account_pfs_x25519_sec, E); // Step 3: X-Wing KDF → enc key k (in key_buf) and enc nonce n (in nonce) v2_derive_xwing_key_nonce(key_buf, nonce, ssx_buf, E, account_pfs_x25519_pub); @@ -508,40 +457,36 @@ DecryptV2Result decrypt_incoming_v2( // Non-PFS fallback key derivation domain labels (private to this translation unit). // The outer wire format is identical to a PFS+PQ v2 message; only the key derivation differs. -constexpr auto V2_NONPFS_KDF_LABEL = "SessionV2NonPFS"_uc; // SHA3-256 input domain -constexpr auto V2_NONPFS_SS_DOMAIN = "SessionV2NonPFSSS"_uc; // SHAKE256 output domain +constexpr auto V2_NONPFS_KDF_LABEL = "SessionV2NonPFS"_bytes; +constexpr auto V2_NONPFS_SS_DOMAIN = "SessionV2NonPFSSS"_bytes; -std::vector encrypt_for_recipient_v2_nopfs( - const Ed25519PrivKeySpan& sender_ed25519_privkey, - std::span recipient_session_id, - std::span content, - const OptionalEd25519PrivKeySpan& pro_ed25519_privkey) { +std::vector encrypt_for_recipient_v2_nopfs( + const ed25519::PrivKeySpan& sender_ed25519_privkey, + std::span recipient_session_id, + std::span content, + const ed25519::OptionalPrivKeySpan& pro_ed25519_privkey) { // R = long-term X25519 pubkey of the recipient (session ID without the 0x05 prefix) auto R = recipient_session_id.last<32>(); // Generate ephemeral X25519 keypair e/E - cleared_uc32 e; - uc32 E; - crypto_box_keypair(E.data(), e.data()); + auto [E, e] = x25519::keypair(); // ki and the outer "mlkem_ct" slot are random: the message is externally indistinguishable // from a PFS+PQ v2 message, but carries no actual ML-KEM ciphertext. - std::array ki; - std::array outer_ct; + std::array ki; + std::array outer_ct; random::fill(ki); random::fill(outer_ct); // ss = eR, then overwritten in-place with SHA3-256(ss || R || E || V2_NONPFS_KDF_LABEL). - // Using the output-buffer overload writes directly into the cleared buffer. - cleared_uc32 ss; - if (0 != crypto_scalarmult(ss.data(), e.data(), R.data())) - throw std::runtime_error{"X25519 DH (non-PFS) failed"}; + cleared_b32 ss; + x25519::scalarmult(ss, e, R); hash::sha3_256(ss, ss, R, E, V2_NONPFS_KDF_LABEL); // k, n = SHAKE256(V2_NONPFS_SS_DOMAIN, ss) → 32-byte key + 24-byte nonce - cleared_uc32 enc_key; - cleared_uchars enc_nonce; + cleared_b32 enc_key; + std::array enc_nonce; hash::shake256(V2_NONPFS_SS_DOMAIN, ss)(enc_key, enc_nonce); return v2_encrypt_inner( @@ -557,25 +502,23 @@ std::vector encrypt_for_recipient_v2_nopfs( } DecryptV2Result decrypt_incoming_v2_nopfs( - std::span recipient_session_id, - std::span x25519_sec, - std::span x25519_pub, - std::span ciphertext) { + std::span recipient_session_id, + std::span x25519_sec, + std::span x25519_pub, + std::span ciphertext) { v2_check_header(ciphertext); // E = ephemeral X25519 pubkey from bytes 4-35; bytes 36-1123 (fake mlkem_ct) are ignored. auto E = ciphertext.subspan<4, 32>(); // ss = rE, then overwritten in-place with SHA3-256(ss || R || E || V2_NONPFS_KDF_LABEL). - // Using the output-buffer overload writes directly into the cleared buffer. - cleared_uc32 ss; - if (0 != crypto_scalarmult(ss.data(), x25519_sec.data(), E.data())) - throw DecryptV2Error{"X25519 DH (non-PFS) failed"}; + cleared_b32 ss; + x25519::scalarmult(ss, x25519_sec, E); hash::sha3_256(ss, ss, x25519_pub, E, V2_NONPFS_KDF_LABEL); // k, n = SHAKE256(V2_NONPFS_SS_DOMAIN, ss) → 32-byte key + 24-byte nonce - cleared_uc32 key; - cleared_uchars nonce; + cleared_b32 key; + std::array nonce; hash::shake256(V2_NONPFS_SS_DOMAIN, ss)(key, nonce); return v2_aead_decrypt_and_parse(recipient_session_id, key, nonce, ciphertext); @@ -609,11 +552,11 @@ DecryptV2Result decrypt_incoming_v2_nopfs( // jB -- A's 33-byte blinded id, beginning with 0x15 or 0x25 (must be the same prefix as kA). // server_pk -- the server's pubkey (needed to compute A's `k` value) // sending -- true if this for a message from A to B, false if this is from B to A. -static cleared_uc32 blinded_shared_secret( - std::span seed, - std::span kA_prefixed, - std::span jB_prefixed, - std::span server_pk, +static cleared_b32 blinded_shared_secret( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span kA_prefixed, + std::span jB_prefixed, + std::span server_pk, bool sending) { // Because we're doing this generically, we use notation a/A/k for ourselves and b/jB for the @@ -621,37 +564,32 @@ static cleared_uc32 blinded_shared_secret( // the BLAKE2b hashed value: there we have to use kA || jB if we are the sender, but reverse the // order to jB || kA if we are the receiver. - std::pair blinded_key_pair; - cleared_uc32 k; + std::pair blinded_key_pair; + cleared_b32 k; - if (seed.size() != 64 && seed.size() != 32) - throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; - if (kA_prefixed[0] == 0x15 && jB_prefixed[0] == 0x15) - blinded_key_pair = blind15_key_pair(seed, server_pk, &k); - else if (kA_prefixed[0] == 0x25 && jB_prefixed[0] == 0x25) - blinded_key_pair = blind25_key_pair(seed, server_pk, &k); + if (kA_prefixed[0] == std::byte{0x15} && jB_prefixed[0] == std::byte{0x15}) + blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk, &k); + else if (kA_prefixed[0] == std::byte{0x25} && jB_prefixed[0] == std::byte{0x25}) + blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk, &k); else throw std::invalid_argument{"Both ids must start with the same 0x15 or 0x25 prefix"}; - bool blind25 = kA_prefixed[0] == 0x25; + bool blind25 = kA_prefixed[0] == std::byte{0x25}; auto kA = kA_prefixed.subspan<1>(); auto jB = jB_prefixed.subspan<1>(); - cleared_uc32 ka; - // Not really switching to x25519 here, this is just an easy way to compute `a` - crypto_sign_ed25519_sk_to_curve25519(ka.data(), seed.data()); + cleared_b32 ka = ed25519::sk_to_private(ed25519_privkey); if (blind25) // Multiply a by k, so that we end up computing kajB = kjaB, which the other side can // compute as jkbA. - crypto_core_ed25519_scalar_mul(ka.data(), ka.data(), k.data()); + ed25519::scalar_mul(ka, ka, k); // Else for 15 blinding we leave "ka" as just a, because j=k and so we don't need the // double-blind. - cleared_uc32 shared_secret; - if (0 != crypto_scalarmult_ed25519_noclamp(shared_secret.data(), ka.data(), jB.data())) - throw std::runtime_error{"Shared secret generation failed"}; + cleared_b32 shared_secret; + ed25519::scalarmult_noclamp(shared_secret, ka, jB); auto& sender = sending ? kA : jB; auto& recipient = sending ? jB : kA; @@ -662,39 +600,30 @@ static cleared_uc32 blinded_shared_secret( return shared_secret; } -std::vector encrypt_for_blinded_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span recipient_blinded_id, - std::span message) { +std::vector encrypt_for_blinded_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span server_pk, + std::span recipient_blinded_id, + std::span message) { // Generate the blinded key pair & shared encryption key - std::pair blinded_key_pair; - switch (recipient_blinded_id[0]) { - case 0x15: blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); break; - - case 0x25: blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); break; + std::pair blinded_key_pair; + if (recipient_blinded_id[0] == std::byte{0x15}) + blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); + else if (recipient_blinded_id[0] == std::byte{0x25}) + blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); + else + throw std::invalid_argument{"Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; - default: - throw std::invalid_argument{ - "Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; - } - std::vector blinded_id; - blinded_id.reserve(33); - blinded_id.insert( - blinded_id.end(), recipient_blinded_id.begin(), recipient_blinded_id.begin() + 1); - blinded_id.insert( - blinded_id.end(), blinded_key_pair.first.begin(), blinded_key_pair.first.end()); + std::array blinded_id; + blinded_id[0] = recipient_blinded_id[0]; + std::ranges::copy(blinded_key_pair.first, blinded_id.begin() + 1); auto enc_key = blinded_shared_secret( - ed25519_privkey, - std::span{blinded_id.data(), 33}, - recipient_blinded_id, - server_pk, - true); + ed25519_privkey, blinded_id, recipient_blinded_id, server_pk, true); // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) - std::vector buf; + std::vector buf; buf.reserve(message.size() + 32); buf.insert(buf.end(), message.begin(), message.end()); @@ -702,36 +631,24 @@ std::vector encrypt_for_blinded_recipient( auto pk = ed25519_privkey.pubkey(); buf.insert(buf.end(), pk.begin(), pk.end()); - // Encrypt using xchacha20-poly1305 - cleared_uchars nonce; - randombytes_buf(nonce.data(), nonce.size()); - - std::vector ciphertext; - unsigned long long outlen = 0; + // Layout: version(1) || ciphertext(buf+ABYTES) || nonce(NPUBBYTES) + std::vector ciphertext; ciphertext.resize( 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); // Prepend with a version byte, so that the recipient can reliably detect if a future version is // no longer encrypting things the way it expects. - ciphertext[0] = BLINDED_ENCRYPT_VERSION; - - if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data() + 1, - &outlen, - buf.data(), - buf.size(), - nullptr, - 0, - nullptr, - nonce.data(), - enc_key.data())) - throw std::runtime_error{"Crypto aead encryption failed"}; + ciphertext[0] = std::byte{BLINDED_ENCRYPT_VERSION}; - assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + auto nonce = std::span{ciphertext}.last(); + random::fill(nonce); - // append the nonce, so that we have: data = b'\x00' + ciphertext + nonce - std::memcpy(ciphertext.data() + (1 + outlen), nonce.data(), nonce.size()); + encrypt::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(1, buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES), + buf, + nonce, + enc_key); return ciphertext; } @@ -739,21 +656,20 @@ std::vector encrypt_for_blinded_recipient( static constexpr size_t GROUPS_ENCRYPT_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; -std::vector encrypt_for_group( - const Ed25519PrivKeySpan& user_ed25519_privkey, - std::span group_ed25519_pubkey, - std::span group_enc_key, - std::span plaintext, +std::vector encrypt_for_group( + const ed25519::PrivKeySpan& user_ed25519_privkey, + std::span group_ed25519_pubkey, + std::span group_enc_key, + std::span plaintext, bool compress, size_t padding) { if (plaintext.size() > GROUPS_MAX_PLAINTEXT_MESSAGE_SIZE) throw std::runtime_error{"Cannot encrypt plaintext: message size is too large"}; - static_assert(decltype(group_ed25519_pubkey)::extent == crypto_sign_ed25519_PUBLICKEYBYTES); if (group_enc_key.size() != 32 && group_enc_key.size() != 64) throw std::invalid_argument{"Invalid group_enc_key: expected 32 or 64 bytes"}; - std::vector _compressed; + std::vector _compressed; if (compress) { _compressed = zstd_compress(plaintext); if (_compressed.size() < plaintext.size()) @@ -775,9 +691,8 @@ std::vector encrypt_for_group( // components to this validation: first the regular signature validation of the "s" signature we // add below, but then also validation that this Ed25519 converts to the Session ID of the // claimed sender of the message inside the encoded message data. - dict.append( - "a", - std::string_view{reinterpret_cast(user_ed25519_privkey.data()) + 32, 32}); + auto sender_pk = user_ed25519_privkey.pubkey(); + dict.append("a", to_string_view(sender_pk)); if (!compress) dict.append("d", to_string_view(plaintext)); @@ -786,16 +701,14 @@ std::vector encrypt_for_group( // encrypted data will not validate if cross-posted to any other group. We don't actually // include the pubkey alongside, because that is implicitly known by the group members that // receive it. - std::vector to_sign(plaintext.size() + group_ed25519_pubkey.size()); + std::vector to_sign(plaintext.size() + group_ed25519_pubkey.size()); std::memcpy(to_sign.data(), plaintext.data(), plaintext.size()); std::memcpy( to_sign.data() + plaintext.size(), group_ed25519_pubkey.data(), group_ed25519_pubkey.size()); - std::array signature; - crypto_sign_ed25519_detached( - signature.data(), nullptr, to_sign.data(), to_sign.size(), user_ed25519_privkey.data()); + auto signature = ed25519::sign(user_ed25519_privkey, to_sign); dict.append("s", to_string_view(signature)); if (compress) @@ -813,35 +726,26 @@ std::vector encrypt_for_group( encoded.resize(encoded.size() + to_append); } - std::vector ciphertext; + std::vector ciphertext; ciphertext.resize(GROUPS_ENCRYPT_OVERHEAD + encoded.size()); - randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::span nonce{ - ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; - if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( - ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - nullptr, - to_unsigned(encoded.data()), - encoded.size(), - nullptr, - 0, - nullptr, - nonce.data(), - group_enc_key.data())) - throw std::runtime_error{"Encryption failed"}; + auto nonce = std::span{ciphertext}.first(); + random::fill(nonce); + + encrypt::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES), + to_span(encoded), + nonce, + group_enc_key.first()); return ciphertext; } -std::pair, std::string> decrypt_incoming_session_id( - const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext) { +std::pair, std::string> decrypt_incoming_session_id( + const ed25519::PrivKeySpan& ed25519_privkey, std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(ed25519_privkey, ciphertext); // Convert the sender_ed_pk to the sender's session ID - std::array sender_x_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) - throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + auto sender_x_pk = ed25519::pk_to_x25519(sender_ed_pk); // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to // the sender session ID @@ -853,17 +757,14 @@ std::pair, std::string> decrypt_incoming_session_id( return {buf, sender_session_id}; } -std::pair, std::string> decrypt_incoming_session_id( - std::span x25519_pubkey, - std::span x25519_seckey, - std::span ciphertext) { +std::pair, std::string> decrypt_incoming_session_id( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext) { auto [buf, sender_ed_pk] = decrypt_incoming(x25519_pubkey, x25519_seckey, ciphertext); // Convert the sender_ed_pk to the sender's session ID - std::array sender_x_pk; - - if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) - throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + auto sender_x_pk = ed25519::pk_to_x25519(sender_ed_pk); // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to // the sender session ID @@ -875,47 +776,38 @@ std::pair, std::string> decrypt_incoming_session_id( return {buf, sender_session_id}; } -std::pair, std::vector> decrypt_incoming( - const Ed25519PrivKeySpan& ed25519_privkey, std::span ciphertext) { - cleared_uc32 x_sec; - uc32 x_pub; - crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); - crypto_scalarmult_base(x_pub.data(), x_sec.data()); - +std::pair, b32> decrypt_incoming( + const ed25519::PrivKeySpan& ed25519_privkey, std::span ciphertext) { + auto x_sec = ed25519::sk_to_x25519(ed25519_privkey); + auto x_pub = x25519::scalarmult_base(x_sec); return decrypt_incoming(x_pub, x_sec, ciphertext); } -std::pair, std::vector> decrypt_incoming( - std::span x25519_pubkey, - std::span x25519_seckey, - std::span ciphertext) { +std::pair, b32> decrypt_incoming( + std::span x25519_pubkey, + std::span x25519_seckey, + std::span ciphertext) { if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; const size_t msg_size = outer_size - 32 - 64; - std::pair, std::vector> result; + std::pair, b32> result; auto& [buf, sender_ed_pk] = result; buf.resize(outer_size); - int opened = crypto_box_seal_open( - buf.data(), - ciphertext.data(), - ciphertext.size(), - x25519_pubkey.data(), - x25519_seckey.data()); - if (opened != 0) + if (!encrypt::box_seal_open(buf, ciphertext, x25519_pubkey, x25519_seckey)) throw std::runtime_error{"Decryption failed"}; - uc64 sig; - sender_ed_pk.assign(buf.begin() + msg_size, buf.begin() + msg_size + 32); - std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); + auto tail = std::span{buf}.subspan(msg_size); // A(32) || SIG(64) + std::ranges::copy(tail.first<32>(), sender_ed_pk.begin()); + b64 sig; + std::ranges::copy(tail.last<64>(), sig.begin()); buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify - buf.insert(buf.end(), x25519_pubkey.begin(), x25519_pubkey.begin() + 32); + buf.insert(buf.end(), x25519_pubkey.begin(), x25519_pubkey.end()); - if (0 != crypto_sign_ed25519_verify_detached( - sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) + if (!ed25519::verify(sig, sender_ed_pk, buf)) throw std::runtime_error{"Signature verification failed"}; // Everything is good, so just drop A and Y off the message @@ -924,36 +816,36 @@ std::pair, std::vector> decrypt_incomi return result; } -std::pair, std::string> decrypt_from_blinded_recipient( - const Ed25519PrivKeySpan& ed25519_privkey, - std::span server_pk, - std::span sender_id, - std::span recipient_id, - std::span ciphertext) { +std::pair, std::string> decrypt_from_blinded_recipient( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span server_pk, + std::span sender_id, + std::span recipient_id, + std::span ciphertext) { auto ed_pk = ed25519_privkey.pubkey(); if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; - cleared_uc32 dec_key; - auto blinded_id = recipient_id[0] == 0x25 ? blinded25_id_from_ed(to_span(ed_pk), server_pk) - : blinded15_id_from_ed(to_span(ed_pk), server_pk); + cleared_b32 dec_key; + auto blinded_id = recipient_id[0] == std::byte{0x25} + ? blinded25_id_from_ed(ed_pk, server_pk) + : blinded15_id_from_ed(ed_pk, server_pk); if (to_string_view(sender_id) == to_string_view(blinded_id)) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); else dec_key = blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); - std::pair, std::string> result; + std::pair, std::string> result; auto& [buf, sender_session_id] = result; // v, ct, nc = data[0], data[1:-24], data[-24:] - if (ciphertext[0] != BLINDED_ENCRYPT_VERSION) + if (ciphertext[0] != std::byte{BLINDED_ENCRYPT_VERSION}) throw std::invalid_argument{ "Invalid ciphertext: version is not " + std::to_string(BLINDED_ENCRYPT_VERSION)}; - std::vector nonce; const size_t msg_size = (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -962,49 +854,31 @@ std::pair, std::string> decrypt_from_blinded_recipien throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; buf.resize(msg_size); - unsigned long long buf_len = 0; - - nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::memcpy( - nonce.data(), - ciphertext.data() + msg_size + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES, - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), - &buf_len, - nullptr, - ciphertext.data() + 1, - msg_size + crypto_aead_xchacha20poly1305_ietf_ABYTES, - nullptr, - 0, - nonce.data(), - dec_key.data())) + auto nonce = ciphertext.last(); + if (!encrypt::xchacha20poly1305_decrypt( + buf, + ciphertext.subspan(1, msg_size + crypto_aead_xchacha20poly1305_ietf_ABYTES), + nonce, + dec_key)) throw std::invalid_argument{"Decryption failed"}; - assert(buf_len == buf.size()); - // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key - uc32 sender_ed_pk; - std::memcpy(sender_ed_pk.data(), buf.data() + (buf.size() - 32), 32); + b32 sender_ed_pk; + std::ranges::copy(std::span{buf}.last<32>(), sender_ed_pk.begin()); // Convert the sender_ed_pk to the sender's session ID - uc32 sender_x_pk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) - throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; - - std::vector session_id; // Gets populated by the following ..._from_ed calls + auto sender_x_pk = ed25519::pk_to_x25519(sender_ed_pk); // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message auto extracted_sender = - recipient_id[0] == 0x25 - ? blinded25_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id) - : blinded15_id_from_ed(to_span(sender_ed_pk), server_pk, &session_id); + recipient_id[0] == std::byte{0x25} + ? blinded25_id_from_ed(sender_ed_pk, server_pk) + : blinded15_id_from_ed(sender_ed_pk, server_pk); bool matched = to_string_view(sender_id) == to_string_view(extracted_sender); - if (!matched && extracted_sender[0] == 0x15) { + if (!matched && extracted_sender[0] == std::byte{0x15}) { // With 15-blinding we might need the negative instead: - extracted_sender[31] ^= 0x80; + extracted_sender[31] ^= std::byte{0x80}; matched = to_string_view(sender_id) == to_string_view(extracted_sender); } if (!matched) @@ -1021,11 +895,11 @@ std::pair, std::string> decrypt_from_blinded_recipien } DecryptGroupMessage decrypt_group_message( - std::span> decrypt_ed25519_privkey_list, - std::span group_ed25519_pubkey, - std::span ciphertext) { - static_assert(decltype(group_ed25519_pubkey)::extent == crypto_sign_ed25519_PUBLICKEYBYTES); + std::span> decrypt_ed25519_privkey_list, + std::span group_ed25519_pubkey, + std::span ciphertext) { DecryptGroupMessage result = {}; + auto& [res_index, session_id, data] = result; if (ciphertext.size() < GROUPS_ENCRYPT_OVERHEAD) throw std::runtime_error{"ciphertext is too small to be encrypted data"}; @@ -1033,9 +907,9 @@ DecryptGroupMessage decrypt_group_message( // generating the pubkey component if the user only passed in a 32 byte libsodium-style secret // key. - std::vector plain; + std::vector plain; - auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + auto nonce = ciphertext.first(); ciphertext = ciphertext.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); plain.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); @@ -1044,18 +918,13 @@ DecryptGroupMessage decrypt_group_message( const auto& decrypt_ed25519_privkey = decrypt_ed25519_privkey_list[index]; if (decrypt_ed25519_privkey.size() != 32 && decrypt_ed25519_privkey.size() != 64) throw std::invalid_argument{"Invalid decrypt_ed25519_privkey: expected 32 or 64 bytes"}; - decrypt_success = 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - plain.data(), - nullptr, - nullptr, - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, - nonce.data(), - decrypt_ed25519_privkey.data()); + decrypt_success = encrypt::xchacha20poly1305_decrypt( + plain, + ciphertext, + nonce, + decrypt_ed25519_privkey.first()); if (decrypt_success) { - result.index = index; + res_index = index; break; } } @@ -1066,15 +935,15 @@ DecryptGroupMessage decrypt_group_message( // // Removing any null padding bytes from the end // - if (auto it = - std::find_if(plain.rbegin(), plain.rend(), [](unsigned char c) { return c != 0; }); + if (auto it = std::find_if( + plain.rbegin(), plain.rend(), [](std::byte b) { return b != std::byte{0}; }); it != plain.rend()) plain.resize(plain.size() - std::distance(plain.rbegin(), it)); // // Now what we have less should be a bt_dict // - if (plain.empty() || plain.front() != 'd' || plain.back() != 'e') + if (plain.empty() || plain.front() != std::byte{'d'} || plain.back() != std::byte{'e'}) throw std::runtime_error{"decrypted data is not a bencoded dict"}; oxenc::bt_dict_consumer dict{to_string_view(plain)}; @@ -1093,17 +962,13 @@ DecryptGroupMessage decrypt_group_message( throw std::runtime_error{ "message author pubkey size (" + std::to_string(ed_pk.size()) + ") is invalid"}; - std::array x_pk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pk.data(), ed_pk.data())) - throw std::runtime_error{ - "author ed25519 pubkey is invalid (unable to convert it to a session id)"}; + auto x_pk = ed25519::pk_to_x25519(ed_pk.first<32>()); - auto& [_, session_id, data] = result; session_id.reserve(66); session_id += "05"; oxenc::to_hex(x_pk.begin(), x_pk.end(), std::back_inserter(session_id)); - std::span raw_data; + std::span raw_data; if (dict.skip_until("d")) { raw_data = to_span(dict.consume_string_view()); if (raw_data.empty()) @@ -1133,14 +998,13 @@ DecryptGroupMessage decrypt_group_message( // The value we verify is the raw data *followed by* the group Ed25519 pubkey. (See the comment // in encrypt_message). - std::vector to_verify(raw_data.size() + group_ed25519_pubkey.size()); + std::vector to_verify(raw_data.size() + group_ed25519_pubkey.size()); std::memcpy(to_verify.data(), raw_data.data(), raw_data.size()); std::memcpy( to_verify.data() + raw_data.size(), group_ed25519_pubkey.data(), group_ed25519_pubkey.size()); - if (0 != crypto_sign_ed25519_verify_detached( - ed_sig.data(), to_verify.data(), to_verify.size(), ed_pk.data())) + if (!ed25519::verify(ed_sig.first<64>(), ed_pk.first<32>(), to_verify)) throw std::runtime_error{"message signature failed validation"}; if (compressed) { @@ -1154,36 +1018,32 @@ DecryptGroupMessage decrypt_group_message( return result; } +// The old Argon2-based ONS encryption always used an all-zero salt and all-zero secretbox nonce. +static constexpr std::array ONS_ARGON2_SALT = {}; +static constexpr std::array ONS_SECRETBOX_NONCE = {}; + std::string decrypt_ons_response( std::string_view lowercase_name, - std::span ciphertext, - std::optional> nonce) { + std::span ciphertext, + std::optional> nonce) { // Handle old Argon2-based encryption used before HF16 if (!nonce) { if (ciphertext.size() < crypto_secretbox_MACBYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; - uc32 key; - std::array salt = {0}; - - if (0 != crypto_pwhash( - key.data(), - key.size(), - lowercase_name.data(), - lowercase_name.size(), - salt.data(), - crypto_pwhash_OPSLIMIT_MODERATE, - crypto_pwhash_MEMLIMIT_MODERATE, - crypto_pwhash_ALG_ARGON2ID13)) - throw std::runtime_error{"Failed to generate key"}; - - std::vector msg; + b32 key; + hash::argon2( + key, + {lowercase_name.data(), lowercase_name.size()}, + ONS_ARGON2_SALT, + crypto_pwhash_OPSLIMIT_MODERATE, + crypto_pwhash_MEMLIMIT_MODERATE, + crypto_pwhash_ALG_ARGON2ID13); + + std::vector msg; msg.resize(ciphertext.size() - crypto_secretbox_MACBYTES); - std::array nonce = {0}; - if (0 != - crypto_secretbox_open_easy( - msg.data(), ciphertext.data(), ciphertext.size(), nonce.data(), key.data())) + if (!encrypt::secretbox_open_easy(msg, ciphertext, ONS_SECRETBOX_NONCE, key)) throw std::runtime_error{"Failed to decrypt"}; std::string session_id = oxenc::to_hex(msg.begin(), msg.end()); @@ -1191,149 +1051,84 @@ std::string decrypt_ons_response( } static_assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES == 24); - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) - throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + if (ciphertext.size() != 33 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid ciphertext: expected exactly 49 bytes"}; // Hash the ONS name using BLAKE2b // // xchacha-based encryption // key = H(name, key=H(name)) - uc32 key; - uc32 name_hash; + b32 name_hash; hash::blake2b(name_hash, lowercase_name); - hash::blake2b_key(key, name_hash, lowercase_name); - - std::vector buf; - unsigned long long buf_len = 0; - buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), - &buf_len, - nullptr, - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, - nonce->data(), - key.data())) - throw std::runtime_error{"Failed to decrypt"}; + auto key = hash::blake2b_key<32>(name_hash, lowercase_name); - if (buf_len != 33) - throw std::runtime_error{"Invalid decrypted value: expected to be 33 bytes"}; + std::array buf; + if (!encrypt::xchacha20poly1305_decrypt(buf, ciphertext, *nonce, key)) + throw std::runtime_error{"Failed to decrypt"}; - std::string session_id = oxenc::to_hex(buf.begin(), buf.end()); - return session_id; + return oxenc::to_hex(buf); } -std::vector decrypt_push_notification( - std::span payload, std::span enc_key) { +std::vector decrypt_push_notification( + std::span payload, std::span enc_key) { if (payload.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; - std::vector buf; - std::vector nonce; - const size_t msg_size = - (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - unsigned long long buf_len = 0; - buf.resize(msg_size); - nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::memcpy(nonce.data(), payload.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - buf.data(), - &buf_len, - nullptr, - payload.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - payload.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, - nullptr, - 0, - nonce.data(), - enc_key.data())) + auto nonce = payload.first(); + auto ct = payload.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + std::vector buf(ct.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + if (!encrypt::xchacha20poly1305_decrypt(buf, ct, nonce, enc_key)) throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; // Removing any null padding bytes from the end - if (auto it = std::find_if(buf.rbegin(), buf.rend(), [](unsigned char c) { return c != 0; }); + if (auto it = std::find_if( + buf.rbegin(), buf.rend(), [](std::byte b) { return b != std::byte{0}; }); it != buf.rend()) buf.resize(buf.size() - std::distance(buf.rbegin(), it)); return buf; } -std::vector encrypt_xchacha20( - std::span plaintext, std::span key) { +std::vector encrypt_xchacha20( + std::span plaintext, std::span key) { - std::vector ciphertext; - ciphertext.resize( + std::vector ciphertext( crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); - // Generate random nonce, and stash it at the beginning of ciphertext: - randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - - auto* c = reinterpret_cast(ciphertext.data()) + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; - unsigned long long clen; - - crypto_aead_xchacha20poly1305_ietf_encrypt( - c, - &clen, - plaintext.data(), - plaintext.size(), - nullptr, - 0, // additional data - nullptr, // nsec (always unused) - reinterpret_cast(ciphertext.data()), - key.data()); - assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); - ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); + auto nonce = std::span{ciphertext}.first(); + random::fill(nonce); + + encrypt::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES), + plaintext, + nonce, + key); return ciphertext; } -std::vector decrypt_xchacha20( - std::span ciphertext, std::span key) { +std::vector decrypt_xchacha20( + std::span ciphertext, std::span key) { if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; // Extract nonce from the beginning of the ciphertext: - auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + auto nonce = ciphertext.first(); ciphertext = ciphertext.subspan(nonce.size()); - std::vector plaintext; - plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - auto* m = reinterpret_cast(plaintext.data()); - unsigned long long mlen; - if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( - m, - &mlen, - nullptr, // nsec (always unused) - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, // additional data - nonce.data(), - key.data())) + std::vector plaintext(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + if (!encrypt::xchacha20poly1305_decrypt(plaintext, ciphertext, nonce, key)) throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; - assert(mlen <= plaintext.size()); - plaintext.resize(mlen); return plaintext; } } // namespace session -// Helpers: construct const unsigned char spans from raw C pointers. -// Used in C API wrappers to avoid verbose std::span{ptr, N} casts. -template -static constexpr std::span cspan(const unsigned char* p) noexcept { - return std::span(p, N); -} -static std::span cspan(const unsigned char* p, size_t n) noexcept { - return {p, n}; -} extern "C" { @@ -1348,9 +1143,9 @@ LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_recipient_deterministic( - cspan<64>(ed25519_privkey), - cspan<32>(recipient_pubkey), - cspan(plaintext_in, plaintext_len)); + to_byte_span<64>(ed25519_privkey), + to_byte_span<32>(recipient_pubkey), + to_byte_span(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1371,10 +1166,10 @@ LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( size_t* ciphertext_len) { try { auto ciphertext = session::encrypt_for_blinded_recipient( - cspan<64>(ed25519_privkey), - cspan<32>(community_pubkey), - cspan<33>(recipient_blinded_id), - cspan(plaintext_in, plaintext_len)); + to_byte_span<64>(ed25519_privkey), + to_byte_span<32>(community_pubkey), + to_byte_span<33>(recipient_blinded_id), + to_byte_span(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1400,11 +1195,11 @@ LIBSESSION_C_API session_encrypt_group_message session_encrypt_for_group( size_t error_len) { session_encrypt_group_message result = {}; try { - std::vector result_cpp = encrypt_for_group( + std::vector result_cpp = encrypt_for_group( {user_ed25519_privkey, user_ed25519_privkey_len}, - cspan<32>(group_ed25519_pubkey), - cspan(group_enc_key, group_enc_key_len), - cspan(plaintext, plaintext_len), + to_byte_span<32>(group_ed25519_pubkey), + to_byte_span(group_enc_key, group_enc_key_len), + to_byte_span(plaintext, plaintext_len), compress, padding); result = { @@ -1426,7 +1221,7 @@ LIBSESSION_C_API bool session_decrypt_incoming( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - cspan<64>(ed25519_privkey), cspan(ciphertext_in, ciphertext_len)); + to_byte_span<64>(ed25519_privkey), to_byte_span(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1449,9 +1244,9 @@ LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( size_t* plaintext_len) { try { auto result = session::decrypt_incoming_session_id( - cspan<32>(x25519_pubkey), - cspan<32>(x25519_seckey), - cspan(ciphertext_in, ciphertext_len)); + to_byte_span<32>(x25519_pubkey), + to_byte_span<32>(x25519_seckey), + to_byte_span(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1476,11 +1271,11 @@ LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( size_t* plaintext_len) { try { auto result = session::decrypt_from_blinded_recipient( - cspan<64>(ed25519_privkey), - cspan<32>(community_pubkey), - cspan<33>(sender_id), - cspan<33>(recipient_id), - cspan(ciphertext_in, ciphertext_len)); + to_byte_span<64>(ed25519_privkey), + to_byte_span<32>(community_pubkey), + to_byte_span<33>(sender_id), + to_byte_span<33>(recipient_id), + to_byte_span(ciphertext_in, ciphertext_len)); auto [plaintext, session_id] = result; std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); @@ -1503,26 +1298,25 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess char* error, size_t error_len) { session_decrypt_group_message_result result = {}; - for (size_t index = 0; index < decrypt_ed25519_privkey_len; index++) { - std::span key = { - decrypt_ed25519_privkey_list[index].data, decrypt_ed25519_privkey_list[index].size}; - - DecryptGroupMessage result_cpp = {}; - try { - result_cpp = decrypt_group_message( - {&key, 1}, cspan<32>(group_ed25519_pubkey), cspan(ciphertext, ciphertext_len)); - result = { - .success = true, - .index = index, - .plaintext = session::span_u8_copy_or_throw( - result.plaintext.data, result.plaintext.size), - }; - assert(result_cpp.session_id.size() == sizeof(result.session_id)); - std::memcpy(result.session_id, result_cpp.session_id.data(), sizeof(result.session_id)); - break; - } catch (const std::exception& e) { - result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); - } + try { + std::vector> keys; + keys.reserve(decrypt_ed25519_privkey_len); + for (size_t i = 0; i < decrypt_ed25519_privkey_len; i++) + keys.push_back(to_byte_span( + decrypt_ed25519_privkey_list[i].data, decrypt_ed25519_privkey_list[i].size)); + auto [index, session_id, plaintext] = decrypt_group_message( + keys, + to_byte_span<32>(group_ed25519_pubkey), + to_byte_span(ciphertext, ciphertext_len)); + result.success = true; + result.index = index; + result.plaintext = session::span_u8_copy_or_throw(plaintext.data(), plaintext.size()); + assert(session_id.size() == sizeof(result.session_id)); + std::memcpy(result.session_id, session_id.data(), sizeof(result.session_id)); + } catch (const std::exception& e) { + auto msg = std::string_view{e.what()}; + result.error_len_incl_null_terminator = + snprintf_clamped(error, error_len, "%.*s", (int)msg.size(), msg.data()) + 1; } return result; } @@ -1534,13 +1328,13 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* nonce_in, char* session_id_out) { try { - std::optional> + std::optional> nonce; if (nonce_in) - nonce = cspan(nonce_in); + nonce = to_byte_span(nonce_in); auto session_id = - session::decrypt_ons_response(name_in, cspan(ciphertext_in, ciphertext_len), nonce); + session::decrypt_ons_response(name_in, to_byte_span(ciphertext_in, ciphertext_len), nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -1557,7 +1351,7 @@ LIBSESSION_C_API bool session_decrypt_push_notification( size_t* plaintext_len) { try { auto plaintext = session::decrypt_push_notification( - cspan(payload_in, payload_len), cspan<32>(enc_key_in)); + to_byte_span(payload_in, payload_len), to_byte_span<32>(enc_key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); @@ -1576,7 +1370,7 @@ LIBSESSION_C_API bool session_encrypt_xchacha20( size_t* ciphertext_len) { try { auto ciphertext = - session::encrypt_xchacha20(cspan(plaintext_in, plaintext_len), cspan<32>(key_in)); + session::encrypt_xchacha20(to_byte_span(plaintext_in, plaintext_len), to_byte_span<32>(key_in)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1595,7 +1389,7 @@ LIBSESSION_C_API bool session_decrypt_xchacha20( size_t* plaintext_len) { try { auto plaintext = - session::decrypt_xchacha20(cspan(ciphertext_in, ciphertext_len), cspan<32>(key_in)); + session::decrypt_xchacha20(to_byte_span(ciphertext_in, ciphertext_len), to_byte_span<32>(key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 8d72952e7..f7b10e0ac 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -50,10 +50,10 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { // clang-format on namespace { -session::uc32 proof_hash_internal( +session::b32 proof_hash_internal( std::uint8_t version, - std::span gen_index_hash, - std::span rotating_pubkey, + std::span gen_index_hash, + std::span rotating_pubkey, std::uint64_t expiry_unix_ts_ms) { // This must match the hashing routine at @@ -62,40 +62,10 @@ session::uc32 proof_hash_internal( session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); } -bool proof_verify_signature_internal( - std::span hash, - std::span sig, - std::span verify_pubkey) { - // The C/C++ interface verifies that the payloads are the correct size using the type system so - // only need asserts here. - assert(hash.size() == 32); - assert(sig.size() == crypto_sign_ed25519_BYTES); - assert(verify_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES); - - int verify_result = crypto_sign_ed25519_verify_detached( - sig.data(), hash.data(), hash.size(), verify_pubkey.data()); - bool result = verify_result == 0; - return result; -} - -bool proof_verify_message_internal( - std::span rotating_pubkey, - std::span sig, - std::span msg) { - // C++ throws on bad size, C uses a fixed sized array - assert(rotating_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES); - if (sig.size() != crypto_sign_ed25519_BYTES) - return false; - - int verify_result = crypto_sign_ed25519_verify_detached( - sig.data(), msg.data(), msg.size(), rotating_pubkey.data()); - bool result = verify_result == 0; - return result; -} struct array_uc32_from_ptr_result { bool success; - session::uc32 data; + session::b32 data; }; static array_uc32_from_ptr_result array_uc32_from_ptr(const void* ptr, size_t len) { @@ -142,28 +112,17 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP namespace session { -static_assert(sizeof(((ProProof*)0)->gen_index_hash) == 32); -static_assert(sizeof(((ProProof*)0)->rotating_pubkey) == crypto_sign_ed25519_PUBLICKEYBYTES); -static_assert(sizeof(((ProProof*)0)->sig) == crypto_sign_ed25519_BYTES); +static_assert(sizeof(std::declval().gen_index_hash) == 32); +static_assert(sizeof(std::declval().rotating_pubkey) == crypto_sign_ed25519_PUBLICKEYBYTES); +static_assert(sizeof(std::declval().sig) == crypto_sign_ed25519_BYTES); -bool ProProof::verify_signature(const std::span& verify_pubkey) const { - if (verify_pubkey.size() != crypto_sign_ed25519_PUBLICKEYBYTES) - throw std::invalid_argument{fmt::format( - "Invalid verify_pubkey: Must be 32 byte Ed25519 public key (was: {})", - verify_pubkey.size())}; - - uc32 hash_to_sign = hash(); - bool result = proof_verify_signature_internal(hash_to_sign, sig, verify_pubkey); - return result; +bool ProProof::verify_signature(std::span verify_pubkey) const { + return ed25519::verify(sig, verify_pubkey, hash()); } bool ProProof::verify_message( - std::span sig, std::span msg) const { - if (sig.size() != crypto_sign_ed25519_BYTES) - throw std::invalid_argument{fmt::format( - "Invalid signed_msg: Signature must be 64 bytes (was: {})", sig.size())}; - bool result = proof_verify_message_internal(rotating_pubkey, sig, msg); - return result; + std::span sig, std::span msg) const { + return ed25519::verify(sig, rotating_pubkey, msg); } bool ProProof::is_active(std::chrono::sys_time unix_ts) const { @@ -171,7 +130,7 @@ bool ProProof::is_active(std::chrono::sys_time unix_t } ProStatus ProProof::status( - std::span verify_pubkey, + std::span verify_pubkey, std::chrono::sys_time unix_ts, const std::optional& signed_msg) { ProStatus result = ProStatus::Valid; @@ -192,8 +151,8 @@ ProStatus ProProof::status( return result; } -uc32 ProProof::hash() const { - uc32 result = proof_hash_internal( +b32 ProProof::hash() const { + b32 result = proof_hash_internal( version, gen_index_hash, rotating_pubkey, expiry_unix_ts.time_since_epoch().count()); return result; } @@ -270,8 +229,8 @@ ProFeaturesForMsg pro_features_for_utf16(std::u16string_view msg) { return pro_features_check(v, v.is_ok() ? simdutf::count_utf16(msg) : 0); } -constexpr char PADDING_TERMINATING_BYTE = 0x80; -std::vector pad_message(std::span payload) { +constexpr std::byte PADDING_TERMINATING_BYTE{0x80}; +std::vector pad_message(std::span payload) { // Calculate amount of padding required size_t padded_content_size = payload.size() + 1 /*padding byte*/; @@ -282,19 +241,19 @@ std::vector pad_message(std::span payload) { assert(padded_content_size % SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING == 0); // Do the padding - std::vector result; + std::vector result; result.resize(padded_content_size); std::memcpy(result.data(), payload.data(), payload.size()); result[payload.size()] = PADDING_TERMINATING_BYTE; return result; } -static std::span unpad_message(std::span payload) { +static std::span unpad_message(std::span payload) { // Strip padding from content size_t size_without_padding = payload.size(); while (size_without_padding) { - char ch = payload[size_without_padding - 1]; - if (ch != 0 && ch != PADDING_TERMINATING_BYTE) { + std::byte ch = payload[size_without_padding - 1]; + if (ch != std::byte{0} && ch != PADDING_TERMINATING_BYTE) { // Non-zero padding encountered, terminate the loop and assume message is not // padded // TODO: We should enforce this but no client enforces it right now. @@ -307,9 +266,7 @@ static std::span unpad_message(std::span(payload.data(), payload.data() + size_without_padding); - return result; + return payload.first(size_without_padding); } // Attaches a Session Pro signature to an envelope. If no pro key is provided, a dummy @@ -317,36 +274,25 @@ static std::span unpad_message(std::span content, - const OptionalEd25519PrivKeySpan& pro_key) { - std::string* pro_sig = envelope.mutable_prosig(); - pro_sig->resize(crypto_sign_ed25519_BYTES); + std::span content, + const ed25519::OptionalPrivKeySpan& pro_key) { + b64 signature; if (!pro_key) { - uc32 ignore_pk; - cleared_uc64 dummy_ed_sk; - crypto_sign_ed25519_keypair(ignore_pk.data(), dummy_ed_sk.data()); - crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), - nullptr, - content.data(), - content.size(), - dummy_ed_sk.data()); + auto [dummy_pk, dummy_sk] = ed25519::keypair(); + signature = ed25519::sign(dummy_sk, content); } else { - crypto_sign_ed25519_detached( - reinterpret_cast(pro_sig->data()), - nullptr, - content.data(), - content.size(), - pro_key->data()); + signature = ed25519::sign(*pro_key, content); } + std::string* pro_sig = envelope.mutable_prosig(); + pro_sig->assign(reinterpret_cast(signature.data()), signature.size()); } // TODO: We don't need to actually pad the community message since that's unencrypted, // there's no need to make the message sizes uniform but we need it for backwards // compat. We can remove this eventually, first step is to unify the clients. -std::vector encode_for_community( - std::span plaintext, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { +std::vector encode_for_community( + std::span plaintext, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey) { if (!pro_rotating_ed25519_privkey) return pad_message(plaintext); @@ -368,48 +314,42 @@ std::vector encode_for_community( "responsible for generating the signature and setting it"}; // We need to sign the padded content, so we pad the `Content` then sign it - std::vector padded = pad_message(plaintext); - uc64 pro_sig; - [[maybe_unused]] bool ok = crypto_sign_ed25519_detached( - pro_sig.data(), - nullptr, - padded.data(), - padded.size(), - pro_rotating_ed25519_privkey->data()) == 0; - assert(ok); + std::vector padded = pad_message(plaintext); + auto pro_sig = ed25519::sign(*pro_rotating_ed25519_privkey, padded); // Now assign the community specific pro signature field, reserialize it and we have // to, yes, pad it again. This is all temporary wasted work whilst transitioning // open groups. - content_w_sig.set_prosigforcommunitymessageonly(pro_sig.data(), pro_sig.size()); - std::vector reserialized(content_w_sig.ByteSizeLong()); - ok = content_w_sig.SerializeToArray(reserialized.data(), reserialized.size()); + content_w_sig.set_prosigforcommunitymessageonly( + reinterpret_cast(pro_sig.data()), pro_sig.size()); + std::vector reserialized(content_w_sig.ByteSizeLong()); + [[maybe_unused]] bool ok = content_w_sig.SerializeToArray(reserialized.data(), reserialized.size()); assert(ok); return pad_message(reserialized); } -std::vector encode_for_community_inbox( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_for_community_inbox( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - std::span recipient_pubkey, - std::span community_pubkey, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { - std::vector content = + std::span recipient_pubkey, + std::span community_pubkey, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey) { + std::vector content = encode_for_community(plaintext, pro_rotating_ed25519_privkey); return encrypt_for_blinded_recipient( ed25519_privkey, community_pubkey, recipient_pubkey, content); } -std::vector encode_dm_v1( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_dm_v1( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, sys_ms sent_timestamp, - std::span recipient_pubkey, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { + std::span recipient_pubkey, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey) { // For 1o1 messages, encrypt the padded payload for the recipient. See: // https://github.com/session-foundation/session-desktop/blob/a04e62427034a6b6fee39dcff7dbabf0d0131b13/ts/session/crypto/BufferPadding.ts#L49 - std::vector encrypted = + std::vector encrypted = encrypt_for_recipient(ed25519_privkey, recipient_pubkey, pad_message(plaintext)); // Create envelope. @@ -431,20 +371,20 @@ std::vector encode_dm_v1( req_msg->set_requestid(0); // Required but unused on iOS req_msg->set_body(envelope.SerializeAsString()); - std::vector result(msg.ByteSizeLong()); + std::vector result(msg.ByteSizeLong()); [[maybe_unused]] bool ok = msg.SerializeToArray(result.data(), result.size()); assert(ok); return result; } -std::vector encode_for_group( - std::span plaintext, - const Ed25519PrivKeySpan& ed25519_privkey, +std::vector encode_for_group( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - std::span group_ed25519_pubkey, - std::span group_enc_key, - const OptionalEd25519PrivKeySpan& pro_rotating_ed25519_privkey) { - if (group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { + std::span group_ed25519_pubkey, + std::span group_enc_key, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey) { + if (group_ed25519_pubkey[0] != std::byte{static_cast(SessionIDPrefix::group)}) { // Legacy groups which have a 05 prefixed key throw std::runtime_error{ "Unsupported configuration, encrypting for a legacy group (0x05 prefix) is " @@ -471,70 +411,15 @@ std::vector encode_for_group( /*padding*/ 256); } -DecodedEnvelope decode_envelope( - const DecodeEnvelopeKey& keys, - std::span envelope_payload, - std::span pro_backend_pubkey) { - DecodedEnvelope result = {}; - SessionProtos::Envelope envelope = {}; - std::span envelope_plaintext = envelope_payload; - - // The caller is indicating that the envelope_payload is encrypted, if the group keys are - // provided. We will decrypt the payload to get the plaintext. In all other cases, the envelope - // is assumed to be websocket wrapped - std::vector envelope_from_decrypted_groups; - std::string envelope_from_websocket_message; - if (keys.group_ed25519_pubkey) { - // Decrypt using the keys - DecryptGroupMessage decrypt = decrypt_group_message( - keys.decrypt_keys, *keys.group_ed25519_pubkey, envelope_plaintext); - - if (decrypt.session_id.size() != ((crypto_sign_ed25519_PUBLICKEYBYTES + 1) * 2)) - throw std::runtime_error{fmt::format( - "Parse encrypted envelope failed, extracted session ID was wrong size: " - "{}", - decrypt.session_id.size())}; - - // Update the plaintext to use the decrypted envelope - envelope_from_decrypted_groups = std::move(decrypt.plaintext); - envelope_plaintext = envelope_from_decrypted_groups; - - // Copy keys out - assert(decrypt.session_id.starts_with("05")); - oxenc::from_hex( - decrypt.session_id.begin() + 2, - decrypt.session_id.end(), - result.sender_x25519_pubkey.begin()); - } else { - // Assumed to be a 1o1/sync message which is wrapped in a websocket message - WebSocketProtos::WebSocketMessage ws_msg; - if (!ws_msg.ParseFromArray(envelope_plaintext.data(), envelope_plaintext.size())) - throw std::runtime_error{fmt::format( - "Parse websocket wrapped envelope from payload failed: {}", - envelope_plaintext.size())}; - - if (!ws_msg.has_request()) - throw std::runtime_error{"Parse websocket wrapped envelope failed, missing request"}; - - if (!ws_msg.request().has_body()) - throw std::runtime_error{ - "Parse websocket wrapped envelope failed, missing request body"}; - - WebSocketProtos::WebSocketRequestMessage* request = ws_msg.mutable_request(); - std::string* body = request->mutable_body(); - envelope_from_websocket_message = std::move(*body); - envelope_plaintext = to_span(envelope_from_websocket_message); - } - - if (!envelope.ParseFromArray(envelope_plaintext.data(), envelope_plaintext.size())) - throw std::runtime_error{"Parse envelope from plaintext failed"}; +// Shared helper 1: parses envelope metadata fields (timestamp, source, etc.) from an +// already-parsed Envelope protobuf. +static void parse_envelope_fields( + DecodedEnvelope& result, const SessionProtos::Envelope& envelope) { - // TODO: We do not parse the envelop type anymore, we infer the type from - // the namespace. Deciding whether or not we decrypt the envelope vs the content depends on - // whether or not the group keys were passed in so we don't care about the type anymore. - // - // When the type is removed, we can remove this TODO. This is just a reminder as to why we skip - // over that field but it's still in the schema and still being set on the sending side. + // TODO: We do not parse the envelope type anymore, we infer the type from the namespace. + // Deciding whether or not we decrypt the envelope vs the content depends on the function + // called (dm vs group) so we don't care about the type anymore. When the type is removed + // from the schema, we can remove this TODO. // Parse timestamp if (envelope.has_timestamp()) { @@ -582,58 +467,14 @@ DecodedEnvelope decode_envelope( result.envelope.server_timestamp = envelope.servertimestamp(); result.envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SERVER_TIMESTAMP; } +} - // Parse content - if (!envelope.has_content()) - throw std::runtime_error{"Parse decrypted message failed, missing content"}; - - // Decrypt content - // The envelope is encrypted in GroupsV2, contents unencrypted. In 1o1 and legacy groups, the - // envelope is encrypted, contents is encrypted. - if (keys.group_ed25519_pubkey) { - result.content_plaintext.resize(envelope.content().size()); - std::memcpy( - result.content_plaintext.data(), - envelope.content().data(), - envelope.content().size()); - } else { - const std::string& content = envelope.content(); - bool decrypt_success = false; - std::vector content_plaintext; - std::vector sender_ed25519_pubkey; - for (const auto& privkey_it : keys.decrypt_keys) { - try { - std::tie(content_plaintext, sender_ed25519_pubkey) = - session::decrypt_incoming(privkey_it, to_span(content)); - assert(result.sender_ed25519_pubkey.size() == crypto_sign_ed25519_PUBLICKEYBYTES); - decrypt_success = true; - break; - } catch (...) { - } - } - - if (!decrypt_success) { - throw std::runtime_error{fmt::format( - "Envelope content decryption failed, tried {} key(s)", - keys.decrypt_keys.size())}; - } - - // Strip padding from content - std::span unpadded_content = unpad_message(content_plaintext); - content_plaintext.resize(unpadded_content.size()); - result.content_plaintext = std::move(content_plaintext); - - std::memcpy( - result.sender_ed25519_pubkey.data(), - sender_ed25519_pubkey.data(), - result.sender_ed25519_pubkey.size()); - - if (crypto_sign_ed25519_pk_to_curve25519( - result.sender_x25519_pubkey.data(), result.sender_ed25519_pubkey.data()) != 0) - throw std::runtime_error( - "Parse content failed, ed25519 public key could not be converted to x25519 " - "key."); - } +// Shared helper 2: parses Content protobuf from result.content_plaintext (which must already be +// set) and extracts pro metadata/verification. +static void parse_content_and_pro( + DecodedEnvelope& result, + const SessionProtos::Envelope& envelope, + std::span pro_backend_pubkey) { // TODO: We parse the content in libsession to extract pro metadata but we return the unparsed // blob back to the caller. This is temporary, eventually we will return a proxy structure for @@ -660,9 +501,9 @@ DecodedEnvelope decode_envelope( if (envelope.has_prosig()) { // Copy (maybe dummy) pro signature into our result struct const std::string& pro_sig = envelope.prosig(); - if (pro_sig.size() != crypto_sign_ed25519_BYTES) + if (pro_sig.size() != 64) throw std::runtime_error("Parse envelope failed, pro signature has wrong size"); - static_assert(sizeof(result.envelope.pro_sig) == crypto_sign_ed25519_BYTES); + static_assert(sizeof(result.envelope.pro_sig) == 64); std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); if (content.has_promessage()) { @@ -716,24 +557,105 @@ DecodedEnvelope decode_envelope( // Evaluate the pro status given the extracted components (was it signed, is it expired, // was the message signed validly?) - ProSignedMessage signed_msg = {}; - signed_msg.sig = to_span(pro_sig); - + // pro_sig.size() validated == 64 above + ProSignedMessage signed_msg = { + .sig = to_byte_span<64>(pro_sig.data()), + .msg = to_span(envelope.content()), + }; // Note that we sign the envelope content wholesale. For 1o1 which are padded to 160 // bytes, this means that we expected the user to have signed the padding as well. auto unix_ts = std::chrono::sys_time( std::chrono::milliseconds(content.sigtimestamp())); - signed_msg.msg = to_span(envelope.content()); pro.status = proof.status(pro_backend_pubkey, unix_ts, signed_msg); } } +} + +DecodedEnvelope decode_dm_envelope( + const ed25519::PrivKeySpan& ed25519_privkey, + std::span envelope_payload, + std::span pro_backend_pubkey) { + DecodedEnvelope result = {}; + + // 1-on-1/sync messages are wrapped in a WebSocket message protobuf + WebSocketProtos::WebSocketMessage ws_msg; + if (!ws_msg.ParseFromArray(envelope_payload.data(), envelope_payload.size())) + throw std::runtime_error{fmt::format( + "Parse websocket wrapped envelope from payload failed: {}", + envelope_payload.size())}; + if (!ws_msg.has_request()) + throw std::runtime_error{"Parse websocket wrapped envelope failed, missing request"}; + if (!ws_msg.request().has_body()) + throw std::runtime_error{"Parse websocket wrapped envelope failed, missing request body"}; + + SessionProtos::Envelope envelope = {}; + if (!envelope.ParseFromArray( + ws_msg.request().body().data(), ws_msg.request().body().size())) + throw std::runtime_error{"Parse envelope from plaintext failed"}; + + parse_envelope_fields(result, envelope); + + if (!envelope.has_content()) + throw std::runtime_error{"Parse decrypted message failed, missing content"}; + + // The inner content is encrypted with Session protocol (Ed25519 DH) + auto [content_plaintext, sender_ed25519_pubkey] = + session::decrypt_incoming(ed25519_privkey, to_span(envelope.content())); + + // Strip padding from content + auto unpadded = unpad_message(content_plaintext); + content_plaintext.resize(unpadded.size()); + result.content_plaintext = std::move(content_plaintext); + + result.sender_ed25519_pubkey = sender_ed25519_pubkey; + result.sender_x25519_pubkey = ed25519::pk_to_x25519(sender_ed25519_pubkey); + + parse_content_and_pro(result, envelope, pro_backend_pubkey); + return result; +} + +DecodedEnvelope decode_group_envelope( + std::span> group_keys, + std::span group_ed25519_pubkey, + std::span envelope_payload, + std::span pro_backend_pubkey) { + DecodedEnvelope result = {}; + + // Groups v2: the entire envelope payload is encrypted with a group symmetric key + DecryptGroupMessage decrypt = + decrypt_group_message(group_keys, group_ed25519_pubkey, envelope_payload); + + if (decrypt.session_id.size() != 66) + throw std::runtime_error{fmt::format( + "Parse encrypted envelope failed, extracted session ID was wrong size: {}", + decrypt.session_id.size())}; + + assert(decrypt.session_id.starts_with("05")); + oxenc::from_hex( + decrypt.session_id.begin() + 2, + decrypt.session_id.end(), + result.sender_x25519_pubkey.begin()); + + SessionProtos::Envelope envelope = {}; + if (!envelope.ParseFromArray(decrypt.plaintext.data(), decrypt.plaintext.size())) + throw std::runtime_error{"Parse envelope from decrypted group data failed"}; + + parse_envelope_fields(result, envelope); + + if (!envelope.has_content()) + throw std::runtime_error{"Parse decrypted message failed, missing content"}; + + // Group content is plaintext (the envelope itself was the encrypted layer) + result.content_plaintext = to_vector(envelope.content()); + + parse_content_and_pro(result, envelope, pro_backend_pubkey); return result; } DecodedCommunityMessage decode_for_community( - std::span content_or_envelope_payload, + std::span content_or_envelope_payload, std::chrono::sys_time unix_ts, - std::span pro_backend_pubkey) { + std::span pro_backend_pubkey) { // TODO: Community message parsing requires a custom code path for now as we are planning to // migrate from sending plain `Content` to `Content` with a pro signature embedded in `Content` // (added exclusively for communities usecase), then, transitioning to sending an `Envelope` to @@ -750,7 +672,7 @@ DecodedCommunityMessage decode_for_community( DecodedCommunityMessage result = {}; // Attempt to parse the blob as an envelope - std::optional> pro_sig; + std::optional> pro_sig; SessionProtos::Envelope pb_envelope = {}; { bool envelope_parsed = pb_envelope.ParseFromArray( @@ -759,8 +681,7 @@ DecodedCommunityMessage decode_for_community( if (envelope_parsed) { // Create the envelope Envelope& envelope = result.envelope.emplace(); - result.content_plaintext = std::vector( - pb_envelope.content().begin(), pb_envelope.content().end()); + result.content_plaintext = to_vector(pb_envelope.content()); // Extract the envelope into our type // Parse source (optional) @@ -795,13 +716,13 @@ DecodedCommunityMessage decode_for_community( } } else { // TODO: Do wasteful copy in the interim whilst transitioning protocol - result.content_plaintext = std::vector( + result.content_plaintext = std::vector( content_or_envelope_payload.begin(), content_or_envelope_payload.end()); } } // Parse the content blob - std::span unpadded_content = unpad_message(result.content_plaintext); + std::span unpadded_content = unpad_message(result.content_plaintext); SessionProtos::Content content = {}; if (!content.ParseFromArray(unpadded_content.data(), unpadded_content.size())) throw std::runtime_error{ @@ -876,9 +797,7 @@ DecodedCommunityMessage decode_for_community( // Evaluate the pro status given the extracted components (was it signed, is it expired, // was the message signed validly?) - ProSignedMessage signed_msg = {}; - signed_msg.sig = to_span(*result.pro_sig); - + // // IMPORTANT: We have to bit-manipulate the content because we're including the signature // inside the payload itself that we had to sign. But we originally signed the payload // without a signature set in it. This is only the case if we're dealing with a `Content` @@ -887,8 +806,10 @@ DecodedCommunityMessage decode_for_community( // Entering the `pro_sig` and `result.envelope` branch means that the envelope must have // a pro signature. assert(result.envelope->flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG); - signed_msg.msg = result.content_plaintext; - pro.status = proof.status(pro_backend_pubkey, unix_ts, signed_msg); + pro.status = proof.status( + pro_backend_pubkey, + unix_ts, + ProSignedMessage{*result.pro_sig, result.content_plaintext}); } else { SessionProtos::Content content_copy_without_sig = content; assert(content_copy_without_sig.has_prosigforcommunitymessageonly()); @@ -898,11 +819,13 @@ DecodedCommunityMessage decode_for_community( assert(!content_copy_without_sig.has_prosigforcommunitymessageonly()); // Reserialise the payload without the signature, repad it then verify the signature - std::vector content_copy_without_sig_payload = + std::vector content_copy_without_sig_payload = pad_message(to_span(content_copy_without_sig.SerializeAsString())); - signed_msg.msg = to_span(content_copy_without_sig_payload); - pro.status = proof.status(pro_backend_pubkey, unix_ts, signed_msg); + pro.status = proof.status( + pro_backend_pubkey, + unix_ts, + ProSignedMessage{*result.pro_sig, to_span(content_copy_without_sig_payload)}); } } @@ -971,10 +894,10 @@ LIBSESSION_C_API void session_protocol_pro_message_bitset_unset( LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { cbytes32 result = {}; - session::uc32 hash = proof_hash_internal( + session::b32 hash = proof_hash_internal( proof->version, - proof->gen_index_hash.data, - proof->rotating_pubkey.data, + to_byte_span(proof->gen_index_hash.data), + to_byte_span(proof->rotating_pubkey.data), proof->expiry_unix_ts_ms); std::memcpy(result.data, hash.data(), hash.size()); return result; @@ -984,16 +907,17 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( session_protocol_pro_proof const* proof, uint8_t const* verify_pubkey, size_t verify_pubkey_len) { - if (verify_pubkey_len != crypto_sign_ed25519_PUBLICKEYBYTES) + if (verify_pubkey_len != 32) return false; - auto verify_pubkey_span = std::span(verify_pubkey, verify_pubkey_len); - session::uc32 hash = proof_hash_internal( + session::b32 hash = proof_hash_internal( proof->version, - proof->gen_index_hash.data, - proof->rotating_pubkey.data, + to_byte_span(proof->gen_index_hash.data), + to_byte_span(proof->rotating_pubkey.data), proof->expiry_unix_ts_ms); - bool result = proof_verify_signature_internal(hash, proof->sig.data, verify_pubkey_span); - return result; + return ed25519::verify( + to_byte_span(proof->sig.data), + to_byte_span<32>(verify_pubkey), + hash); } LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( @@ -1002,10 +926,12 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( size_t sig_len, uint8_t const* msg, size_t msg_len) { - std::span sig_span = {sig, sig_len}; - std::span msg_span = {msg, msg_len}; - bool result = proof_verify_message_internal(proof->rotating_pubkey.data, sig_span, msg_span); - return result; + if (sig_len != 64) + return false; + return ed25519::verify( + to_byte_span<64>(sig), + to_byte_span(proof->rotating_pubkey.data), + to_byte_span(msg, msg_len)); } LIBSESSION_C_API bool session_protocol_pro_proof_is_active( @@ -1097,12 +1023,14 @@ session_protocol_encoded_for_destination session_protocol_encode_dm_v1( size_t error_len) { return c_encode_impl(error, error_len, [&] { return encode_dm_v1( - {static_cast(plaintext), plaintext_len}, - {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::span{static_cast(plaintext), plaintext_len}, + ed25519::PrivKeySpan{static_cast(ed25519_privkey), + ed25519_privkey_len}, from_epoch_ms(sent_timestamp_ms), - recipient_pubkey->data, - {static_cast(pro_rotating_ed25519_privkey), - pro_rotating_ed25519_privkey_len}); + to_byte_span(recipient_pubkey->data), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); }); } @@ -1121,13 +1049,15 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community_i size_t error_len) { return c_encode_impl(error, error_len, [&] { return encode_for_community_inbox( - {static_cast(plaintext), plaintext_len}, - {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::span{static_cast(plaintext), plaintext_len}, + ed25519::PrivKeySpan{static_cast(ed25519_privkey), + ed25519_privkey_len}, std::chrono::milliseconds(sent_timestamp_ms), - recipient_pubkey->data, - community_pubkey->data, - {static_cast(pro_rotating_ed25519_privkey), - pro_rotating_ed25519_privkey_len}); + to_byte_span(recipient_pubkey->data), + to_byte_span(community_pubkey->data), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); }); } @@ -1141,9 +1071,10 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community( size_t error_len) { return c_encode_impl(error, error_len, [&] { return encode_for_community( - {static_cast(plaintext), plaintext_len}, - {static_cast(pro_rotating_ed25519_privkey), - pro_rotating_ed25519_privkey_len}); + std::span{static_cast(plaintext), plaintext_len}, + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); }); } @@ -1162,13 +1093,15 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( size_t error_len) { return c_encode_impl(error, error_len, [&] { return encode_for_group( - {static_cast(plaintext), plaintext_len}, - {static_cast(ed25519_privkey), ed25519_privkey_len}, + std::span{static_cast(plaintext), plaintext_len}, + ed25519::PrivKeySpan{static_cast(ed25519_privkey), + ed25519_privkey_len}, std::chrono::milliseconds(sent_timestamp_ms), - group_ed25519_pubkey->data, - group_enc_key->data, - {static_cast(pro_rotating_ed25519_privkey), - pro_rotating_ed25519_privkey_len}); + to_byte_span(group_ed25519_pubkey->data), + to_byte_span(group_enc_key->data), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); }); } @@ -1203,41 +1136,59 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( return result; } - // Setup decryption keys and decrypt - DecodeEnvelopeKey keys_cpp = {}; - if (keys->group_ed25519_pubkey.size == crypto_sign_ed25519_PUBLICKEYBYTES) { - keys_cpp.group_ed25519_pubkey = - std::span{ - keys->group_ed25519_pubkey.data, crypto_sign_ed25519_PUBLICKEYBYTES}; - } else if (keys->group_ed25519_pubkey.size) { - result.error_len_incl_null_terminator = format_c_str( - error, - error_len, - "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: {}", - keys->group_ed25519_pubkey.size); - return result; - } + std::span payload{ + static_cast(envelope_plaintext), envelope_plaintext_len}; DecodedEnvelope result_cpp = {}; - for (size_t index = 0; index < keys->decrypt_keys_len; index++) { - std::span key = { - keys->decrypt_keys[index].data, keys->decrypt_keys[index].size}; - keys_cpp.decrypt_keys = {&key, 1}; + if (keys->group_ed25519_pubkey.size == 32) { + // Groups v2 path: decrypt with group symmetric keys + auto group_pk = to_byte_span<32>(keys->group_ed25519_pubkey.data); + + std::vector> group_keys; + group_keys.reserve(keys->decrypt_keys_len); + for (size_t i = 0; i < keys->decrypt_keys_len; i++) + group_keys.emplace_back( + to_byte_span(keys->decrypt_keys[i].data, keys->decrypt_keys[i].size)); + try { - result_cpp = decode_envelope( - keys_cpp, - {static_cast(envelope_plaintext), envelope_plaintext_len}, - pro_backend_pubkey_cpp.data); + result_cpp = decode_group_envelope( + group_keys, group_pk, payload, pro_backend_pubkey_cpp.data); result.success = true; - break; } catch (const std::exception& e) { - result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); + std::string error_cpp = e.what(); + result.error_len_incl_null_terminator = snprintf_clamped( + error, error_len, "%.*s", + static_cast(error_cpp.size()), error_cpp.data()) + 1; } - } - - if (keys->decrypt_keys_len == 0) { + } else if (keys->group_ed25519_pubkey.size) { result.error_len_incl_null_terminator = - copy_c_str(error, error_len, "No keys ed25519_privkeys were provided"); + snprintf_clamped( + error, error_len, + "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: %zu", + keys->group_ed25519_pubkey.size) + 1; + return result; + } else { + // DM path: decrypt with Ed25519 private key(s) + for (size_t index = 0; index < keys->decrypt_keys_len; index++) { + try { + ed25519::PrivKeySpan privkey{ + keys->decrypt_keys[index].data, keys->decrypt_keys[index].size}; + result_cpp = decode_dm_envelope( + privkey, payload, pro_backend_pubkey_cpp.data); + result.success = true; + break; + } catch (const std::exception& e) { + std::string error_cpp = e.what(); + result.error_len_incl_null_terminator = snprintf_clamped( + error, error_len, "%.*s", + static_cast(error_cpp.size()), error_cpp.data()) + 1; + } + } + + if (keys->decrypt_keys_len == 0) { + result.error_len_incl_null_terminator = + snprintf_clamped(error, error_len, "No ed25519 private keys were provided") + 1; + } } // Marshall into c type @@ -1290,9 +1241,9 @@ session_protocol_decoded_community_message session_protocol_decode_for_community OPTIONAL char* error, size_t error_len) { session_protocol_decoded_community_message result = {}; - auto content_or_envelope_payload_span = std::span( - reinterpret_cast(content_or_envelope_payload), - content_or_envelope_payload_len); + std::span content_or_envelope_payload_span{ + static_cast(content_or_envelope_payload), + content_or_envelope_payload_len}; auto unix_ts = std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); array_uc32_from_ptr_result pro_backend_pubkey_cpp = diff --git a/src/util.cpp b/src/util.cpp index ab290a9b4..0681f3e70 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -126,9 +126,9 @@ namespace { using zstd_decomp_ptr = std::unique_ptr; } // namespace -std::vector zstd_compress( - std::span data, int level, std::span prefix) { - std::vector compressed; +std::vector zstd_compress( + std::span data, int level, std::span prefix) { + std::vector compressed; if (prefix.empty()) compressed.resize(ZSTD_compressBound(data.size())); else { @@ -148,8 +148,8 @@ std::vector zstd_compress( return compressed; } -std::optional> zstd_decompress( - std::span data, size_t max_size) { +std::optional> zstd_decompress( + std::span data, size_t max_size) { zstd_decomp_ptr z_decompressor{ZSTD_createDStream()}; auto* zds = z_decompressor.get(); @@ -158,7 +158,7 @@ std::optional> zstd_decompress( std::array out_buf; ZSTD_outBuffer output{/*.dst=*/out_buf.data(), /*.size=*/out_buf.size(), /*.pos=*/0}; - std::vector decompressed; + std::vector decompressed; size_t ret; do { @@ -169,7 +169,10 @@ std::optional> zstd_decompress( if (max_size > 0 && decompressed.size() + output.pos > max_size) return std::nullopt; - decompressed.insert(decompressed.end(), out_buf.begin(), out_buf.begin() + output.pos); + decompressed.insert( + decompressed.end(), + reinterpret_cast(out_buf.data()), + reinterpret_cast(out_buf.data()) + output.pos); } while (ret > 0 || input.pos < input.size); return decompressed; diff --git a/src/xed25519-tweetnacl.cpp b/src/xed25519-tweetnacl.cpp index a540a0c7f..4acd3ad72 100644 --- a/src/xed25519-tweetnacl.cpp +++ b/src/xed25519-tweetnacl.cpp @@ -121,9 +121,9 @@ void inv25519(gf o,const gf i) } // namespace -std::array pubkey(std::span x_pk) noexcept { +std::array pubkey(std::span x_pk) noexcept { gf u; - unpack25519(u, x_pk.data()); + unpack25519(u, reinterpret_cast(x_pk.data())); // u - 1 gf u_minus_one; @@ -142,8 +142,8 @@ std::array pubkey(std::span x_pk) no M(y, u_minus_one, u_plus_one_inv); // Encode to 32 bytes (sign bit is naturally 0) - std::array ed_pk; - pack25519(ed_pk.data(), y); + std::array ed_pk; + pack25519(reinterpret_cast(ed_pk.data()), y); return ed_pk; } diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 940dc3690..57e8c515f 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -19,8 +19,9 @@ namespace session::xed25519 { using namespace session::literals; +// Internal unsigned char arrays; kept as unsigned char for direct C API use template -using bytes = std::array; +using uchars = std::array; namespace { @@ -31,16 +32,16 @@ namespace { // This deviates from Signal's XEd25519 specified derivation of r in that we use a personalized // Black2b hash (for better performance and cryptographic properties), rather than a // custom-prefixed SHA-512 hash. - bytes<32> xed25519_compute_r(const bytes<32>& a, std::span msg) { - bytes<64> random; + uchars<32> xed25519_compute_r(const uchars<32>& a, std::span msg) { + uchars<64> random; randombytes_buf(random.data(), random.size()); constexpr static auto personality = "xed25519signatur"_b2b_pers; - bytes<64> h_aMZ; + uchars<64> h_aMZ; hash::blake2b_pers(h_aMZ, personality, a, msg, random); - bytes<32> r; + uchars<32> r; crypto_core_ed25519_scalar_reduce(r.data(), h_aMZ.data()); return r; } @@ -49,14 +50,14 @@ namespace { void ed25519_hram( unsigned char* S, const unsigned char* R, - const bytes<32>& A, - std::span msg) { - bytes<64> hram; + const uchars<32>& A, + std::span msg) { + uchars<64> hram; crypto_hash_sha512_state st; crypto_hash_sha512_init(&st); crypto_hash_sha512_update(&st, R, 32); crypto_hash_sha512_update(&st, A.data(), A.size()); - crypto_hash_sha512_update(&st, msg.data(), msg.size()); + crypto_hash_sha512_update(&st, to_unsigned(msg.data()), msg.size()); crypto_hash_sha512_final(&st, hram.data()); crypto_core_ed25519_scalar_reduce(S, hram.data()); @@ -64,32 +65,32 @@ namespace { } // namespace -bytes<64> sign( - std::span curve25519_privkey, std::span msg) { - - assert(curve25519_privkey.size() == 32); - - bytes<32> A; +b64 sign(std::span curve25519_privkey, std::span msg) { + uchars<32> A; // Convert the x25519 privkey to an ed25519 pubkey: - crypto_scalarmult_ed25519_base(A.data(), curve25519_privkey.data()); + crypto_scalarmult_ed25519_base(A.data(), to_unsigned(curve25519_privkey.data())); // Signal's XEd25519 spec requires that the sign bit be zero, so if it isn't we negate. bool negative = A[31] >> 7; - A[31] &= 0x7f; - bytes<32> a, neg_a; + uchars<32> a, neg_a; std::memcpy(a.data(), curve25519_privkey.data(), a.size()); crypto_core_ed25519_scalar_negate(neg_a.data(), a.data()); - constant_time_conditional_assign(a, neg_a, negative); + + // constant_time_conditional_assign works on std::byte arrays; use bit_cast for uchars + auto ba = std::bit_cast>(a); + auto bna = std::bit_cast>(neg_a); + constant_time_conditional_assign(ba, bna, negative); + a = std::bit_cast>(ba); // We now have our a, A privkey/public. (Note that a is just the private key scalar, *not* the // ed25519 secret key). - bytes<32> r = xed25519_compute_r(a, msg); - bytes<64> signature; // R || S - auto* R = signature.data(); - auto* S = signature.data() + 32; + uchars<32> r = xed25519_compute_r(a, msg); + uchars<64> sig_uc; // R || S + auto* R = sig_uc.data(); + auto* S = sig_uc.data() + 32; crypto_scalarmult_ed25519_base_noclamp(R, r.data()); @@ -98,56 +99,49 @@ bytes<64> sign( crypto_core_ed25519_scalar_mul(S, S, a.data()); // S *= a crypto_core_ed25519_scalar_add(S, S, r.data()); // S += r - return signature; -} - -std::array sign( - std::span curve25519_privkey, std::span msg) { - return std::bit_cast>( - sign(as_span(curve25519_privkey), as_span(msg))); + return std::bit_cast(sig_uc); } std::string sign(std::string_view curve25519_privkey, std::string_view msg) { - auto sig = sign(to_span(curve25519_privkey), to_span(msg)); + if (curve25519_privkey.size() != 32) + throw std::invalid_argument{"curve25519 privkey must be 32 bytes"}; + auto sig = sign( + std::span{ + reinterpret_cast(curve25519_privkey.data()), 32}, + to_span(msg)); return std::string{reinterpret_cast(sig.data()), sig.size()}; } bool verify( - std::span signature, - std::span curve25519_pubkey, - std::span msg) { - assert(signature.size() == crypto_sign_ed25519_BYTES); - assert(curve25519_pubkey.size() == 32); - auto ed_pubkey = pubkey(curve25519_pubkey.first<32>()); - return 0 == crypto_sign_ed25519_verify_detached( - signature.data(), msg.data(), msg.size(), ed_pubkey.data()); -} - -bool verify( - std::span signature, - std::span curve25519_pubkey, + std::span signature, + std::span curve25519_pubkey, std::span msg) { - return verify( - as_span(signature), - as_span(curve25519_pubkey), - as_span(msg)); + auto ed_pubkey = pubkey(curve25519_pubkey); + return 0 == crypto_sign_ed25519_verify_detached( + to_unsigned(signature.data()), + to_unsigned(msg.data()), + msg.size(), + to_unsigned(ed_pubkey.data())); } bool verify(std::string_view signature, std::string_view curve25519_pubkey, std::string_view msg) { - return verify(to_span(signature), to_span(curve25519_pubkey), to_span(msg)); + if (signature.size() != 64 || curve25519_pubkey.size() != 32) + return false; + return verify( + std::span{ + reinterpret_cast(signature.data()), 64}, + std::span{ + reinterpret_cast(curve25519_pubkey.data()), 32}, + to_span(msg)); } // pubkey(...) is in xed25519-tweetnacl.cpp -std::array pubkey(std::span curve25519_pubkey) noexcept { - return std::bit_cast>( - pubkey(as_span(curve25519_pubkey))); -} - std::string pubkey(std::string_view curve25519_pubkey) { if (curve25519_pubkey.size() != 32) throw std::invalid_argument{"Invalid X25519 pubkey"}; - auto ed_pk = pubkey(to_span(curve25519_pubkey).first<32>()); + auto ed_pk = pubkey(std::span{ + reinterpret_cast(curve25519_pubkey.data()), 32}); return std::string{reinterpret_cast(ed_pk.data()), ed_pk.size()}; } @@ -162,7 +156,10 @@ LIBSESSION_C_API bool session_xed25519_sign( size_t msg_len) { assert(signature != NULL); try { - auto sig = session::xed25519::sign({curve25519_privkey, 32}, {msg, msg_len}); + auto sig = session::xed25519::sign( + std::span{ + reinterpret_cast(curve25519_privkey), 32}, + std::span{reinterpret_cast(msg), msg_len}); std::memcpy(signature, sig.data(), sig.size()); return true; } catch (...) { @@ -175,14 +172,18 @@ LIBSESSION_C_API bool session_xed25519_verify( const unsigned char* pubkey, const unsigned char* msg, size_t msg_len) { - return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}); + return session::xed25519::verify( + std::span{reinterpret_cast(signature), 64}, + std::span{reinterpret_cast(pubkey), 32}, + std::span{reinterpret_cast(msg), msg_len}); } LIBSESSION_C_API void session_xed25519_pubkey( unsigned char* ed25519_pubkey, const unsigned char* curve25519_pubkey) { assert(ed25519_pubkey != NULL); - std::span xpk{curve25519_pubkey, 32}; - std::memcpy(ed25519_pubkey, session::xed25519::pubkey(xpk).data(), 32); + auto ed_pk = session::xed25519::pubkey(std::span{ + reinterpret_cast(curve25519_pubkey), 32}); + std::memcpy(ed25519_pubkey, ed_pk.data(), 32); } } // extern "C" diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp index a5225374d..a73fe255a 100644 --- a/tests/live/live_utils.hpp +++ b/tests/live/live_utils.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -11,12 +10,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include "../dns_utils.hpp" @@ -76,21 +77,15 @@ inline session::TempCore make_live_core(Opts&&... opts) { // routing layer adds the wrapper as required by the transport. // // See session-storage-server client_rpc_endpoints.h for the store endpoint spec. -inline std::vector build_account_pubkeys_store_params(session::core::Core& core) { +inline std::vector build_account_pubkeys_store_params(session::core::Core& core) { auto session_id_hex = oxenc::to_hex(core.globals.session_id()); auto now_ms = session::epoch_ms(session::clock_now_ms()); constexpr auto ns = static_cast(session::config::Namespace::AccountPubkeys); // Signature covers: "store" || namespace (decimal) || sig_timestamp (decimal) auto to_sign = fmt::format("store{}{}", ns, now_ms); - std::array sig; auto seed = core.globals.account_seed(); - crypto_sign_ed25519_detached( - sig.data(), - nullptr, - reinterpret_cast(to_sign.data()), - to_sign.size(), - seed.ed25519_secret().data()); + auto sig = session::ed25519::sign(seed.ed25519_secret(), session::to_span(to_sign)); auto msg = core.devices.build_account_pubkey_message(); @@ -106,7 +101,7 @@ inline std::vector build_account_pubkeys_store_params(session::co {"signature", oxenc::to_base64(sig)}, {"ttl", int64_t{2592000000}}, // 30 days in ms }; - return session::to_vector(params.dump()); + return session::to_vector(params.dump()); } // Pushes Core's AccountPubkeys message to its swarm. Resolves the swarm, sends the signed store diff --git a/tests/live/test_pubkey_xfer.cpp b/tests/live/test_pubkey_xfer.cpp index 401b59a20..8391ed335 100644 --- a/tests/live/test_pubkey_xfer.cpp +++ b/tests/live/test_pubkey_xfer.cpp @@ -15,7 +15,7 @@ TEST_CASE("Live: PFS key prefetch returns NAK for account with no published keys auto core_a = make_live_core(); auto core_b = make_live_core(); - std::array sid_a; + b33 sid_a; std::ranges::copy(core_a->globals.session_id(), sid_a.begin()); core_b->prefetch_pfs_keys(sid_a); @@ -37,7 +37,7 @@ TEST_CASE("Live: PFS key prefetch retrieves keys after store to swarm", "[live][ REQUIRE(store_account_pubkeys(*core_a, LIVE_TIMEOUT)); // Now fetch from Core B's perspective. - std::array sid_a; + b33 sid_a; std::ranges::copy(core_a->globals.session_id(), sid_a.begin()); core_b->prefetch_pfs_keys(sid_a); diff --git a/tests/static_bundle.cpp b/tests/static_bundle.cpp index a760a178d..b90084b1c 100644 --- a/tests/static_bundle.cpp +++ b/tests/static_bundle.cpp @@ -7,6 +7,6 @@ int main() { if (std::mt19937_64{}() == 123) { auto& k = *reinterpret_cast(12345); - k.encrypt_message(std::span{}); + k.encrypt_message(std::span{}); } } diff --git a/tests/swarm-auth-test.cpp b/tests/swarm-auth-test.cpp index 98c565f4b..45d9d0139 100644 --- a/tests/swarm-auth-test.cpp +++ b/tests/swarm-auth-test.cpp @@ -26,16 +26,16 @@ static constexpr int64_t created_ts = 1680064059; using namespace session::config; -static std::array sk_from_seed(std::span seed) { - std::array ignore; - std::array sk; +static b64 sk_from_seed(std::span seed) { + b32 ignore; + b64 sk; crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); return sk; } -static std::string session_id_from_ed(std::span ed_pk) { +static std::string session_id_from_ed(std::span ed_pk) { std::string sid; - std::array xpk; + b32 xpk; int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); assert(rc == 0); sid.reserve(66); @@ -45,8 +45,8 @@ static std::string session_id_from_ed(std::span ed_pk) { } struct pseudo_client { - std::array secret_key; - const std::span public_key{secret_key.data() + 32, 32}; + b64 secret_key; + const std::span public_key{secret_key.data() + 32, 32}; std::string session_id{session_id_from_ed(public_key)}; groups::Info info; @@ -54,22 +54,22 @@ struct pseudo_client { groups::Keys keys; pseudo_client( - std::span seed, + std::span seed, bool admin, const unsigned char* gpk, std::optional gsk) : secret_key{sk_from_seed(seed)}, - info{std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) + info{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt}, - members{std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) + members{std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt}, keys{to_usv(secret_key), - std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) + std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt, info, @@ -78,15 +78,15 @@ struct pseudo_client { int main() { - const std::vector group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const std::vector admin_seed = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const std::vector member_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + const std::vector group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hex_b; + const std::vector admin_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + const std::vector member_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hex_b; - std::array group_pk; - std::array group_sk; + b32 group_pk; + b64 group_sk; crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); @@ -104,7 +104,7 @@ int main() { session::config::UserGroups member_gr2{member_seed, std::nullopt}; auto [seqno, push, obs] = member_groups.push(); - std::vector>> gr_conf; + std::vector>> gr_conf; gr_conf.emplace_back("fakehash1", push); member_gr2.merge(gr_conf); @@ -114,8 +114,8 @@ int main() { .count(); auto msg = to_usv("hello world"); - std::array store_sig; - std::vector store_to_sign; + b64 store_sig; + std::vector store_to_sign; auto store_vec = session::str_to_vec("store999{}"_format(now)); store_to_sign.insert(store_to_sign.end(), store_vec.begin(), store_vec.end()); @@ -134,7 +134,7 @@ int main() { std::cout << "STORE:\n\n" << store.dump() << "\n\n"; - std::vector retrieve_to_sign; + std::vector retrieve_to_sign; auto retrieve_vec = session::str_to_vec("retrieve999{}"_format(now)); retrieve_to_sign.insert(retrieve_to_sign.end(), retrieve_vec.begin(), retrieve_vec.end()); auto subauth = member.keys.swarm_subaccount_sign(retrieve_to_sign, auth_data); diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 97a64fb2b..b2214799e 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -3,7 +3,6 @@ #include #include -#include #include "session/blinding.hpp" #include "session/hash.hpp" @@ -14,16 +13,19 @@ using namespace session; constexpr auto seed1 = "fecd9a6034bc9aba273925dee7062b123334587c3c6257341afae2d7fe85e122" - "f4ef873908f6a5377ba3853f0e2fa326eed9e741edf9f7d0311a3ecc66a57b32"_hex_u; + "f4ef873908f6a5377ba3853f0e2fa326eed9e741edf9f7d0311a3ecc66a57b32"_hex_b; constexpr auto seed2 = "8659efdcbe0949e0f81141e6d397e8be75f45d09262f209d5950e97989eb43c7" - "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_hex_u; + "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_hex_b; -constexpr auto xpub1 = "fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_u; -constexpr auto xpub2 = "05c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_u; - -const std::string session_id1 = "05" + oxenc::to_hex(xpub1.begin(), xpub1.end()); -const std::string session_id2 = "05" + oxenc::to_hex(xpub2.begin(), xpub2.end()); +constexpr auto sid1 = + "05fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; +constexpr auto sid2 = + "0505c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; +constexpr auto xpub1 = sid1.last<32>(); +constexpr auto xpub2 = sid2.last<32>(); +const std::string session_id1 = oxenc::to_hex(sid1); +const std::string session_id2 = oxenc::to_hex(sid2); TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") { REQUIRE(sodium_init() >= 0); @@ -41,216 +43,158 @@ TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") == "25a69cc6884530bf8498d22892e563716c4742f2845a7eb608de2aecbe7b6b5996"); - std::vector session_id1_raw; - oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); CHECK(to_hex(blind25_id( - session_id1_raw, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + sid1, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b)) == "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); CHECK(to_hex(blind25_id( - {session_id1_raw.begin() + 1, session_id1_raw.end()}, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + xpub1, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b)) == "253b991dcbba44cfdb45d5b38880d95cff723309e3ece6fd01415ad5fa1dccc7ac"); } TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { - - std::array server_pks = { - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; - auto b25_1 = blind25_id(session_id1, server_pks[0]); - auto b25_2 = blind25_id(session_id1, server_pks[1]); - auto b25_3 = blind25_id(session_id2, server_pks[2]); - auto b25_4 = blind25_id(session_id2, server_pks[3]); - auto b25_5 = blind25_id(session_id2, server_pks[4]); - auto b25_6 = blind25_id(session_id1, server_pks[5]); - - auto sig1 = blind25_sign(to_span(seed1), server_pks[0], to_span("hello")); + constexpr std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b}; + auto b25_1 = blind25_id(sid1, server_pks[0]); + auto b25_2 = blind25_id(sid1, server_pks[1]); + auto b25_3 = blind25_id(sid2, server_pks[2]); + auto b25_4 = blind25_id(sid2, server_pks[3]); + auto b25_5 = blind25_id(sid2, server_pks[4]); + auto b25_6 = blind25_id(sid1, server_pks[5]); + + auto sig1 = blind25_sign(seed1, server_pks[0], "hello"_bytes); CHECK(to_hex(sig1) == "e6c57de4ac0cd278abbeef815bd88b163a037085deae789ecaaf4805884c4c3d3db25f3afa856241366cb341" "a3a4c9bbaa2cda81d028079c956fab16a7fe6206"); - CHECK(0 == crypto_sign_verify_detached( - sig1.data(), - to_unsigned("hello"), - 5, - to_unsigned(oxenc::from_hex(b25_1).data()) + 1)); + CHECK(ed25519::verify(sig1, std::span{b25_1}.last<32>(), "hello"_bytes)); - auto sig2 = blind25_sign(to_span(seed1), server_pks[1], to_span("world")); + auto sig2 = blind25_sign(seed1, server_pks[1], "world"_bytes); CHECK(to_hex(sig2) == "4460b606e9f55a7cba0bbe24207fe2859c3422783373788b6b070b2fa62ceba4f2a50749a6cee68e095747a3" "69927f9f4afa86edaf055cad68110e35e8b06607"); - CHECK(0 == crypto_sign_verify_detached( - sig2.data(), - to_unsigned("world"), - 5, - to_unsigned(oxenc::from_hex(b25_2).data()) + 1)); + CHECK(ed25519::verify(sig2, std::span{b25_2}.last<32>(), "world"_bytes)); - auto sig3 = blind25_sign(to_span(seed2), server_pks[2], to_span("this")); + auto sig3 = blind25_sign(seed2, server_pks[2], "this"_bytes); CHECK(to_hex(sig3) == "57bb2f80c88ce2f677902ee58e02cbd83e4e1ec9e06e1c72a34b4ab76d0f5219cfd141ac5ce7016c73c8382d" "b99df9f317f2bc0af6ca68edac2a9a7670938902"); - CHECK(0 == crypto_sign_verify_detached( - sig3.data(), - to_unsigned("this"), - 4, - to_unsigned(oxenc::from_hex(b25_3).data()) + 1)); + CHECK(ed25519::verify(sig3, std::span{b25_3}.last<32>(), "this"_bytes)); - auto sig4 = blind25_sign(to_span(seed2), server_pks[3], to_span("is")); + auto sig4 = blind25_sign(seed2, server_pks[3], "is"_bytes); CHECK(to_hex(sig4) == "ecce032b27b09d2d3d6df4ebab8cae86656c64fd1e3e70d6f020cd7e1a8058c57e3df7b6b01e90ccd592ac4a" "845dde7a2fdceb1a328a6690686851583133ea0c"); - CHECK(0 == crypto_sign_verify_detached( - sig4.data(), - to_unsigned("is"), - 2, - to_unsigned(oxenc::from_hex(b25_4).data()) + 1)); + CHECK(ed25519::verify(sig4, std::span{b25_4}.last<32>(), "is"_bytes)); - auto sig5 = blind25_sign(to_span(seed2), server_pks[4], to_span("")); + auto sig5 = blind25_sign(seed2, server_pks[4], ""_bytes); CHECK(to_hex(sig5) == "bf2fb9a511adbf5827e2e3bcf09f0a1cff80f85556fb76d8001aa8483b5f22e14539b170eaa0dbfa1489d1b8" "618ce8b48d7512cb5602c7eb8a05ce330a68350b"); - CHECK(0 == - crypto_sign_verify_detached( - sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b25_5).data()) + 1)); + CHECK(ed25519::verify(sig5, std::span{b25_5}.last<32>(), ""_bytes)); - auto sig6 = blind25_sign(to_span(seed1), server_pks[5], to_span("omg!")); + auto sig6 = blind25_sign(seed1, server_pks[5], "omg!"_bytes); CHECK(to_hex(sig6) == "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); - CHECK(0 == crypto_sign_verify_detached( - sig6.data(), - to_unsigned("omg!"), - 4, - to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); + CHECK(ed25519::verify(sig6, std::span{b25_6}.last<32>(), "omg!"_bytes)); // Test that it works when given just the seed instead of the whole sk: - auto sig6b = blind25_sign(to_span(seed1).subspan(0, 32), server_pks[5], to_span("omg!")); + auto sig6b = blind25_sign(seed1.first<32>(), server_pks[5], "omg!"_bytes); CHECK(to_hex(sig6b) == "322e280fbc3547c6b6512dbea4d60563d32acaa2df10d665c40a336c99fc3b8e4b13a7109dfdeadab2ab58b2" "cb314eb0510b947f43e5dfb6e0ce5bf1499d240f"); - CHECK(0 == crypto_sign_verify_detached( - sig6b.data(), - to_unsigned("omg!"), - 4, - to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); + CHECK(ed25519::verify(sig6b, std::span{b25_6}.last<32>(), "omg!"_bytes)); } TEST_CASE("Communities 15xxx-blinded pubkey derivation", "[blinding15][pubkey]") { REQUIRE(sodium_init() >= 0); - std::vector session_id1_raw, session_id2_raw; - oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); - oxenc::from_hex(session_id2.begin(), session_id2.end(), std::back_inserter(session_id2_raw)); CHECK(to_hex(blind15_id( - session_id1_raw, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + sid1, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b)) == "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); CHECK(to_hex(blind15_id( - session_id2_raw, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + sid2, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b)) == "1561e070286ff7a71f167e92b18c709882b148d8238c8872caf414b301ba0564fd"); CHECK(to_hex(blind15_id( - {session_id1_raw.begin() + 1, session_id1_raw.end()}, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + xpub1, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b)) == "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); } TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { REQUIRE(sodium_init() >= 0); - std::array server_pks = { - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, - "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; - auto b15_1 = blind15_id(session_id1, server_pks[0])[0]; - auto b15_2 = blind15_id(session_id1, server_pks[1])[0]; - // session_id2 has a negative pubkey, so these next three need the negative [1] instead: - auto b15_3 = blind15_id(session_id2, server_pks[2])[1]; - auto b15_4 = blind15_id(session_id2, server_pks[3])[1]; - auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; - auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; - - auto sig1 = blind15_sign(to_span(seed1), server_pks[0], to_span("hello")); + constexpr std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b}; + // Use blind15_key_pair for pubkeys: avoids the sign ambiguity of blind15_id + auto [b15_1_pk, _1] = blind15_key_pair(seed1, server_pks[0]); + auto [b15_2_pk, _2] = blind15_key_pair(seed1, server_pks[1]); + auto [b15_3_pk, _3] = blind15_key_pair(seed2, server_pks[2]); + auto [b15_4_pk, _4] = blind15_key_pair(seed2, server_pks[3]); + auto [b15_5_pk, _5] = blind15_key_pair(seed2, server_pks[4]); + auto [b15_6_pk, _6] = blind15_key_pair(seed1, server_pks[5]); + + auto sig1 = blind15_sign(seed1, server_pks[0], "hello"_bytes); CHECK(to_hex(sig1) == "1a5ade20b43af0e16b3e591d6f86303938d7557c0ac54469dd4f5aea759f82d22cafa42587251756e133acdd" "dd8cbec2f707a9ce09a49f2193f46a91502c5006"); - CHECK(0 == crypto_sign_verify_detached( - sig1.data(), - to_unsigned("hello"), - 5, - to_unsigned(oxenc::from_hex(b15_1).data()) + 1)); + CHECK(ed25519::verify(sig1, b15_1_pk, "hello"_bytes)); - auto sig2 = blind15_sign(to_span(seed1), server_pks[1], to_span("world")); + auto sig2 = blind15_sign(seed1, server_pks[1], "world"_bytes); CHECK(to_hex(sig2) == "d357f74c5ec5536840aec575051f71fdb22d70f35ef31db1715f5f694842de3b39aa647c84aa8e28ec56eb76" "2d237c9e030639c83f429826d419ac719cd4df03"); - CHECK(0 == crypto_sign_verify_detached( - sig2.data(), - to_unsigned("world"), - 5, - to_unsigned(oxenc::from_hex(b15_2).data()) + 1)); + CHECK(ed25519::verify(sig2, b15_2_pk, "world"_bytes)); - auto sig3 = blind15_sign(to_span(seed2), server_pks[2], to_span("this")); + auto sig3 = blind15_sign(seed2, server_pks[2], "this"_bytes); CHECK(to_hex(sig3) == "dacf91dfb411e99cd8ef4cb07b195b49289cf1a724fef122c73462818560bc29832a98d870ec4feb79dedca5" "b59aba6a466d3ce8f3e35adf25a1813f6989fd0a"); - CHECK(0 == crypto_sign_verify_detached( - sig3.data(), - to_unsigned("this"), - 4, - to_unsigned(oxenc::from_hex(b15_3).data()) + 1)); + CHECK(ed25519::verify(sig3, b15_3_pk, "this"_bytes)); - auto sig4 = blind15_sign(to_span(seed2), server_pks[3], to_span("is")); + auto sig4 = blind15_sign(seed2, server_pks[3], "is"_bytes); CHECK(to_hex(sig4) == "8339ea9887d3e44131e33403df160539cdc7a0a8107772172c311e95773660a0d39ed0a6c2b2c794dde1fdc6" "40943e403497aa02c4d1a21a7d9030742beabb05"); - CHECK(0 == crypto_sign_verify_detached( - sig4.data(), - to_unsigned("is"), - 2, - to_unsigned(oxenc::from_hex(b15_4).data()) + 1)); + CHECK(ed25519::verify(sig4, b15_4_pk, "is"_bytes)); - auto sig5 = blind15_sign(to_span(seed2), server_pks[4], to_span("")); + auto sig5 = blind15_sign(seed2, server_pks[4], ""_bytes); CHECK(to_hex(sig5) == "8b0d6447decff3a21ec1809141580139c4a51e24977b0605fe7984439993f5377ebc9681e4962593108d03cc" "8b6873c5c5ba8c30287188137d2dee9ab10afd0f"); - CHECK(0 == - crypto_sign_verify_detached( - sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b15_5).data()) + 1)); + CHECK(ed25519::verify(sig5, b15_5_pk, ""_bytes)); - auto sig6 = blind15_sign(to_span(seed1), server_pks[5], to_span("omg!")); + auto sig6 = blind15_sign(seed1, server_pks[5], "omg!"_bytes); CHECK(to_hex(sig6) == "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); - CHECK(0 == crypto_sign_verify_detached( - sig6.data(), - to_unsigned("omg!"), - 4, - to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); + CHECK(ed25519::verify(sig6, b15_6_pk, "omg!"_bytes)); // Test that it works when given just the seed instead of the whole sk: - auto sig6b = blind15_sign(to_span(seed1).subspan(0, 32), server_pks[5], to_span("omg!")); + auto sig6b = blind15_sign(seed1.first<32>(), server_pks[5], "omg!"_bytes); CHECK(to_hex(sig6b) == "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); - CHECK(0 == crypto_sign_verify_detached( - sig6b.data(), - to_unsigned("omg!"), - 4, - to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); + CHECK(ed25519::verify(sig6b, b15_6_pk, "omg!"_bytes)); } TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { REQUIRE(sodium_init() >= 0); - auto [pubkey, seckey] = blind_version_key_pair(to_span(seed1)); + auto [pubkey, seckey] = blind_version_key_pair(seed1); CHECK(oxenc::to_hex(pubkey.begin(), pubkey.end()) == "88e8adb27e7b8ce776fcc25bc1501fb2888fcac0308e52fb10044f789ae1a8fa"); @@ -258,7 +202,7 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { oxenc::to_hex(pubkey.begin(), pubkey.end())); // Hash ourselves just to make sure we get what we expect for the seed part of the secret key: - cleared_uc32 expect_seed; + cleared_b32 expect_seed; hash::blake2b_key(expect_seed, "VersionCheckKey_sig"sv, seed1.first<32>()); CHECK(oxenc::to_hex(seckey.begin(), seckey.begin() + 32) == @@ -272,31 +216,26 @@ TEST_CASE("Version 07xxx-blinded pubkey derivation", "[blinding07][key_pair]") { TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { REQUIRE(sodium_init() >= 0); - auto signature = blind_version_sign(to_span(seed1), Platform::desktop, 1234567890); + auto signature = blind_version_sign(seed1, Platform::desktop, 1234567890); CHECK(oxenc::to_hex(signature.begin(), signature.end()) == "143c2c9828f7680ee81e6247bc7aa4777c4991add87cd724149b00452bed4e92" "0fa57daf4627c68f43fcbddb2d465d5ea11def523f3befb2bbee39c769676305"); - auto [pk, sk] = blind_version_key_pair(to_span(seed1)); + auto [pk, sk] = blind_version_key_pair(seed1); auto method = "GET"sv; - auto method_span = to_span(method); auto path = "/path/to/somewhere"sv; - auto path_span = to_span(path); auto body = to_span("some body (once told me)"); uint64_t timestamp = 1234567890; - std::vector full_message = to_vector("{}{}{}"_format(timestamp, method, path)); + std::vector full_message = to_vector("{}{}{}"_format(timestamp, method, path)); auto req_sig_no_body = - blind_version_sign_request(to_span(seed1), timestamp, method, path, std::nullopt); - CHECK(crypto_sign_verify_detached( - req_sig_no_body.data(), full_message.data(), full_message.size(), pk.data()) == - 0); + blind_version_sign_request(seed1, timestamp, method, path, std::nullopt); + CHECK(ed25519::verify(req_sig_no_body, pk, full_message)); full_message.insert(full_message.end(), body.begin(), body.end()); - auto req_sig = blind_version_sign_request(to_span(seed1), timestamp, method, path, body); - CHECK(crypto_sign_verify_detached( - req_sig.data(), full_message.data(), full_message.size(), pk.data()) == 0); + auto req_sig = blind_version_sign_request(seed1, timestamp, method, path, body); + CHECK(ed25519::verify(req_sig, pk, full_message)); } TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { diff --git a/tests/test_bugs.cpp b/tests/test_bugs.cpp index e8b4a6cc2..03d9b5e87 100644 --- a/tests/test_bugs.cpp +++ b/tests/test_bugs.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -10,13 +9,9 @@ using namespace session::config; TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -25,14 +20,14 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{session::to_span(seed), std::nullopt}; + session::config::Contacts c1{seed, std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); c1.confirm_pushed(seqno, {"fakehash1"}); - session::config::Contacts c2{session::to_span(seed), c1.dump()}; - session::config::Contacts c3{session::to_span(seed), c1.dump()}; + session::config::Contacts c2{seed, c1.dump()}; + session::config::Contacts c3{seed, c1.dump()}; CHECK_FALSE(c2.needs_dump()); CHECK_FALSE(c2.needs_push()); @@ -53,7 +48,7 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { REQUIRE(seqno3 == 2); CHECK(as_set(obs3) == make_set("fakehash1"s)); - auto r = c1.merge(std::vector>>{ + auto r = c1.merge(std::vector>>{ {{"fakehash2", data2[0]}, {"fakehash3", data3[0]}}}); CHECK(r == std::unordered_set{{"fakehash2"s, "fakehash3"s}}); CHECK(c1.needs_dump()); @@ -77,13 +72,9 @@ TEST_CASE("Dirty/Mutable test case", "[config][dirty]") { // included in the old_hashes (which would result in clients deleting the current config from the // swarm) TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -92,7 +83,7 @@ TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{std::span{seed}, std::nullopt}; + session::config::Contacts c1{seed, std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); @@ -101,7 +92,7 @@ TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") CHECK(!c1.needs_dump()); CHECK(!c1.needs_push()); - auto r = c1.merge(std::vector>>{ + auto r = c1.merge(std::vector>>{ {{"fakehash1"s, session::to_span(data[0])}}}); CHECK(as_set(r) == make_set("fakehash1"s)); @@ -114,13 +105,9 @@ TEST_CASE("Merge existing config into clean state", "[config][merge_existing]") // in old_hashes (which ends up being the same hash the dirty config gets after pushing, resulting // in the current config getting deleted from the swarm) TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -129,13 +116,13 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts c1{std::span{seed}, std::nullopt}; + session::config::Contacts c1{seed, std::nullopt}; c1.set_name("050000000000000000000000000000000000000000000000000000000000000000", "alfonso"); auto [seqno, data, obsolete] = c1.push(); CHECK(obsolete == std::vector{}); c1.confirm_pushed(seqno, {"fakehash1"s}); - session::config::Contacts c2{std::span{seed}, c1.dump()}; + session::config::Contacts c2{seed, c1.dump()}; CHECK_FALSE(c2.needs_dump()); CHECK_FALSE(c2.needs_push()); @@ -151,7 +138,7 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c2.confirm_pushed(seqno2, {"fakehash2"s}); CHECK(c1.is_dirty()); // already dirty before the merge - auto r = c1.merge(std::vector>>{ + auto r = c1.merge(std::vector>>{ {{"fakehash2"s, session::to_span(data2[0])}}}); CHECK(r == std::unordered_set{{"fakehash2"s}}); CHECK(c1.needs_dump()); @@ -171,7 +158,7 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c2.confirm_pushed(seqno3, {"fakehash3"s}); CHECK(c1.is_dirty()); // already dirty before the merge - auto r2 = c1.merge(std::vector>>{ + auto r2 = c1.merge(std::vector>>{ {{"fakehash3", session::to_span(data3[0])}}}); CHECK(r2 == std::unordered_set{{"fakehash3"s}}); CHECK(c1.needs_dump()); @@ -203,7 +190,7 @@ TEST_CASE("Merge config matching local changse", "[config][merge_matching_dirty] c1.set_name("051111111111111111111111111111111111111111111111111111111111111140", "barney40"); auto size_before_merge = c1.size(); // retrieve size before trying to merge CHECK(c1.is_dirty()); // already dirty before the merge - auto r4 = c1.merge(std::vector>>{ + auto r4 = c1.merge(std::vector>>{ {{"fakehash21", session::to_span(data4[0])}}}); CHECK(r4 == std::unordered_set{{"fakehash21"s}}); CHECK(c1.needs_dump()); diff --git a/tests/test_compression.cpp b/tests/test_compression.cpp index fa361e334..430f6dfc2 100644 --- a/tests/test_compression.cpp +++ b/tests/test_compression.cpp @@ -10,27 +10,27 @@ #include "utils.hpp" namespace session::config { -void compress_message(std::vector& msg, int level); +void compress_message(std::vector& msg, int level); } TEST_CASE("compression", "[config][compression]") { - auto data = + auto data = session::to_vector( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes; + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b); CHECK(data.size() == 81); auto d = data; session::config::compress_message(d, 1); - CHECK(d[0] == 'z'); + CHECK(d[0] == std::byte{'z'}); CHECK(d.size() == 18); CHECK(to_hex(d) == "7a28b52ffd205145000010aaaa01008c022c"); d = data; session::config::compress_message(d, 5); - CHECK(d[0] == 'z'); + CHECK(d[0] == std::byte{'z'}); CHECK(d.size() == 17); CHECK(to_hex(d) == "7a28b52ffd20513d000008aa01000dea84"); @@ -49,7 +49,7 @@ TEST_CASE("compression", "[config][compression]") { "l" "i0e" "32:" + - session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes) + + session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hex_b) + "de" "e" "e" @@ -73,7 +73,7 @@ TEST_CASE("compression", "[config][compression]") { "l" "i0e" "32:" + - session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes) + + session::to_string("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hex_b) + "de" "e" "e" @@ -91,7 +91,7 @@ TEST_CASE("compression", "[config][compression]") { // Doesn't compress, so shouldn't change: CHECK(d.size() == 142); session::config::compress_message(d, 1); - CHECK(d[0] == 'd'); + CHECK(d[0] == std::byte{'d'}); CHECK(d.size() == 142); CHECK(reinterpret_cast(d.data()) == dptr); @@ -100,7 +100,7 @@ TEST_CASE("compression", "[config][compression]") { // version of zstd). d = data2; session::config::compress_message(d, 1); - CHECK(d[0] == 'z'); + CHECK(d[0] == std::byte{'z'}); CHECK(d.size() == 161); CHECK(d.size() < data2.size()); CHECK(to_hex(d) == @@ -111,7 +111,7 @@ TEST_CASE("compression", "[config][compression]") { d = data2; session::config::compress_message(d, 5); - CHECK(d[0] == 'z'); + CHECK(d[0] == std::byte{'z'}); CHECK(d.size() == 156); CHECK(d.size() < data2.size()); CHECK(to_hex(d) == @@ -122,7 +122,7 @@ TEST_CASE("compression", "[config][compression]") { d = data2; session::config::compress_message(d, 19); - CHECK(d[0] == 'z'); + CHECK(d[0] == std::byte{'z'}); CHECK(d.size() == 156); CHECK(d.size() < data2.size()); CHECK(to_hex(d) == diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index e7bb0ae5a..500e5bd7b 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -2,13 +2,13 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include @@ -16,15 +16,13 @@ static constexpr int64_t created_ts = 1680064059; +using namespace session; + TEST_CASE("Contacts", "[config][contacts]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -33,7 +31,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts contacts{std::span{seed}, std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; constexpr auto definitely_real_id = "050000000000000000000000000000000000000000000000000000000000000000"sv; @@ -133,7 +131,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(seqno == 2); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash2", to_push[0]); contacts.merge(merge_configs); contacts2.confirm_pushed(seqno, {"fakehash2"}); @@ -177,7 +175,8 @@ TEST_CASE("Contacts", "[config][contacts]") { session::config::profile_pic p; { // These don't stay alive, so we use set_key/set_url to make a local copy: - std::vector key = "qwerty78901234567890123456789012"_bytes; + constexpr auto k = "qwerty78901234567890123456789012"_bytes; + std::vector key(k.begin(), k.end()); std::string url = "http://example.com/huge.bmp"; p.set_key(std::move(key)); p.url = std::move(url); @@ -266,13 +265,9 @@ TEST_CASE("Contacts", "[config][contacts]") { } TEST_CASE("Contacts (C API)", "[config][contacts][c]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -282,7 +277,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); config_object* conf; - REQUIRE(0 == contacts_init(&conf, ed_sk.data(), NULL, 0, NULL)); + REQUIRE(0 == contacts_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, NULL)); const char* const definitely_real_id = "050000000000000000000000000000000000000000000000000000000000000000"; @@ -329,7 +324,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(to_push->seqno == 1); config_object* conf2; - REQUIRE(contacts_init(&conf2, ed_sk.data(), NULL, 0, NULL) == 0); + REQUIRE(contacts_init(&conf2, to_unsigned(ed_sk.data()), NULL, 0, NULL) == 0); const char* merge_hash[1]; const unsigned char* merge_data[1]; @@ -442,20 +437,16 @@ TEST_CASE("huge contacts compression", "[config][compression][contacts]") { // Test that we can produce a config message whose *uncompressed* length exceeds the maximum // message length as long as its *compressed* length does not. - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - session::config::Contacts contacts{std::span{seed}, std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; for (uint16_t i = 0; i < 12000; i++) { char buf[2]; @@ -492,20 +483,16 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact // Test that we can produce a config message whose *uncompressed* length exceeds the maximum // message length as long as its *compressed* length does not. - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - session::config::Contacts contacts{session::to_span(seed), std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; std::string friend42; @@ -514,8 +501,8 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact // are randomly generated and thus not usefully compressible, which results in a much larger // (compressed) config. std::mt19937_64 rng{i}; - std::array random_sessionid; - random_sessionid[0] = 0x05; + b33 random_sessionid; + random_sessionid[0] = std::byte{0x05}; for (int i = 1; i < 33; i += 8) oxenc::write_host_as_little(rng(), random_sessionid.data() + i); @@ -575,9 +562,9 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact dump = contacts.dump(); CHECK(dump.size() == base_dump_size + 12 * 13); // 12 x "10:fakehashNN" - auto c2 = std::make_unique(session::to_span(seed), std::nullopt); + auto c2 = std::make_unique(seed, std::nullopt); - std::vector>> merge_configs, merge_more; + std::vector>> merge_configs, merge_more; bool dump_load_in_between = false; std::mt19937_64 rng{12345}; @@ -657,7 +644,7 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact if (dump_load_in_between) { auto c2b = - std::make_unique(session::to_span(seed), c2->dump()); + std::make_unique(seed, c2->dump()); CHECK_FALSE(c2b->needs_dump()); c2 = std::move(c2b); CHECK_FALSE(c2->needs_dump()); @@ -687,37 +674,28 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact TEST_CASE("multipart message expiry", "[config][multipart][contacts][expiry]") { // Tests that stored multipart message expires as expected. - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - session::config::Contacts contacts{session::to_span(seed), std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; std::string friend42; - std::array seedi = {0}; + b32 seedi = {}; for (uint16_t i = 0; i < 2000; i++) { // Unlike the above case where we have nearly identical Session IDs, here our session IDs // are randomly generated from fixed seeds and thus not usefully compressible, which results // in a much larger (compressed) config. - seedi[0] = i % 256; - seedi[1] = i >> 8; - std::array i_ed_pk, i_curve_pk; - std::array i_ed_sk; - crypto_sign_ed25519_seed_keypair( - i_ed_pk.data(), - i_ed_sk.data(), - reinterpret_cast(seedi.data())); - rc = crypto_sign_ed25519_pk_to_curve25519(i_curve_pk.data(), i_ed_pk.data()); + seedi[0] = static_cast(i % 256); + seedi[1] = static_cast(i >> 8); + auto [i_ed_pk, i_ed_sk] = ed25519::keypair(seedi); + auto i_curve_pk = ed25519::pk_to_x25519(i_ed_pk); std::string session_id = "05" + oxenc::to_hex(i_curve_pk.begin(), i_curve_pk.end()); auto c = contacts.get_or_construct(session_id); @@ -742,7 +720,7 @@ TEST_CASE("multipart message expiry", "[config][multipart][contacts][expiry]") { contacts.confirm_pushed(seqno, {"fakehash0", "fakehash1"}); - auto c2 = std::make_unique(session::to_span(seed), std::nullopt); + auto c2 = std::make_unique(seed, std::nullopt); c2->MULTIPART_MAX_WAIT = 200ms; c2->MULTIPART_MAX_REMEMBER = 600ms; @@ -750,7 +728,7 @@ TEST_CASE("multipart message expiry", "[config][multipart][contacts][expiry]") { auto old_seqno = std::get(c2->push()); REQUIRE(old_seqno == 0); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash0", to_push[0]); std::unordered_set accepted; @@ -856,9 +834,9 @@ TEST_CASE("multipart message expiry", "[config][multipart][contacts][expiry]") { TEST_CASE("needs_dump bug", "[config][needs_dump]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; - session::config::Contacts contacts{std::span{seed}, std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; CHECK_FALSE(contacts.needs_dump()); @@ -890,13 +868,9 @@ TEST_CASE("needs_dump bug", "[config][needs_dump]") { TEST_CASE("Contacts", "[config][blinded_contacts]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -905,7 +879,7 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Contacts contacts{std::span{seed}, std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; constexpr auto definitely_real_id = "150000000000000000000000000000000000000000000000000000000000000000"sv; @@ -993,7 +967,7 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { CHECK(seqno == 2); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash2", to_push[0]); contacts.merge(merge_configs); contacts2.confirm_pushed(seqno, {"fakehash2"}); @@ -1039,7 +1013,7 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { session::config::profile_pic p; { // These don't stay alive, so we use set_key/set_url to make a local copy: - std::vector key = "qwerty78901234567890123456789012"_bytes; + auto key = to_vector("qwerty78901234567890123456789012"_bytes); std::string url = "http://example.com/huge.bmp"; p.set_key(std::move(key)); p.url = std::move(url); @@ -1120,9 +1094,9 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { TEST_CASE("Contacts Pro Storage", "[config][contacts][pro]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; - session::config::Contacts contacts{std::span{seed}, std::nullopt}; + session::config::Contacts contacts{seed, std::nullopt}; REQUIRE(contacts.is_clean()); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 74b30dc5d..4a6034c88 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -13,13 +12,9 @@ TEST_CASE("Conversations", "[config][conversations]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -28,7 +23,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::ConvoInfoVolatile convos{std::span{seed}, std::nullopt}; + session::config::ConvoInfoVolatile convos{seed, std::nullopt}; constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"sv; @@ -72,7 +67,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(convos.needs_dump()); const auto community_pubkey = - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; auto og = convos.get_or_construct_community( "http://Example.ORG:5678", "SudokuRoom", community_pubkey); @@ -190,7 +185,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(seqno == 2); REQUIRE(to_push.size() == 1); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("hash2", to_push[0]); convos.merge(merge_configs); convos2.confirm_pushed(seqno, {"hash2"}); @@ -296,13 +291,9 @@ TEST_CASE("Conversations", "[config][conversations]") { } TEST_CASE("Conversations (C API)", "[config][conversations][c]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -312,7 +303,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + REQUIRE(0 == convo_info_volatile_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, NULL)); const char* const definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; @@ -354,7 +345,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(config_needs_dump(conf)); const auto community_pubkey = - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; convo_info_volatile_community og; @@ -363,18 +354,18 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { &og, "bad-url", "room", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); + "0000000000000000000000000000000000000000000000000000000000000000"_hex_u.data())); CHECK(conf->last_error == "Invalid URL: invalid/missing protocol://"sv); CHECK_FALSE(convo_info_volatile_get_or_construct_community( conf, &og, "https://example.com", "bad room name", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); + "0000000000000000000000000000000000000000000000000000000000000000"_hex_u.data())); CHECK(conf->last_error == "Invalid community URL: room token contains invalid characters"sv); CHECK(convo_info_volatile_get_or_construct_community( - conf, &og, "http://Example.ORG:5678", "SudokuRoom", community_pubkey.data())); + conf, &og, "http://Example.ORG:5678", "SudokuRoom", to_unsigned(community_pubkey.data()))); CHECK(conf->last_error == nullptr); CHECK(og.base_url == "http://example.org:5678"sv); // Note: lower-case CHECK(og.room == "sudokuroom"sv); // Note: lower-case @@ -414,7 +405,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { config_dump(conf, &dump, &dumplen); config_object* conf2; - REQUIRE(convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL) == 0); + REQUIRE(convo_info_volatile_init(&conf2, to_unsigned(ed_sk.data()), dump, dumplen, NULL) == 0); free(dump); CHECK_FALSE(config_needs_push(conf2)); @@ -496,19 +487,14 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { } convo_info_volatile_iterator_free(it); - CHECK(seen == std::vector{ - "1-to-1: " - "051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: " - "055000000000000000000000000000000000000000000000000000000000000000", - "comm: http://example.org:5678/r/sudokuroom", - "lgr: " - "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "b: " - "150000000000000000000000000000000000101010111010000110100001210000", - "b: " - "2512345cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - "c"}); + CHECK(seen == + std::vector{ + "1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", + "comm: http://example.org:5678/r/sudokuroom", + "lgr: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "b: 150000000000000000000000000000000000101010111010000110100001210000", + "b: 2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}); } CHECK_FALSE(config_needs_push(conf)); @@ -581,13 +567,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -596,12 +578,12 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::ConvoInfoVolatile convos{std::span{seed}, std::nullopt}; + session::config::ConvoInfoVolatile convos{seed, std::nullopt}; - auto some_pubkey = [](unsigned char x) -> std::vector { - std::vector s = - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes; - s[31] = x; + auto some_pubkey = [](unsigned char x) -> std::vector { + std::vector s; + s.resize(32); + s[31] = static_cast(x); return s; }; auto some_session_id = [&](unsigned char x) -> std::string { @@ -626,9 +608,8 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { c.pro_expiry_unix_ts = std::chrono::sys_time{ std::chrono::milliseconds{unix_timestamp(i)}}; - session::uc32 hash{}; - std::fill(hash.begin(), hash.end(), static_cast(i % 256)); - c.pro_gen_index_hash = hash; + auto& hash = c.pro_gen_index_hash.emplace(); + std::fill(hash.begin(), hash.end(), static_cast(i % 256)); } convos.set(c); @@ -699,13 +680,9 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -715,7 +692,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + REQUIRE(0 == convo_info_volatile_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, NULL)); convo_info_volatile_1to1 c; CHECK(convo_info_volatile_get_or_construct_1to1( @@ -744,7 +721,7 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load // Load the dump: config_object* conf2; - REQUIRE(0 == convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL)); + REQUIRE(0 == convo_info_volatile_init(&conf2, to_unsigned(ed_sk.data()), dump, dumplen, NULL)); free(dump); @@ -810,13 +787,9 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load TEST_CASE("Conversation pro data", "[config][conversations][pro]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -826,7 +799,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + REQUIRE(0 == convo_info_volatile_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, NULL)); convo_info_volatile_1to1 c; CHECK(convo_info_volatile_get_or_construct_1to1( @@ -836,9 +809,10 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { .count(); c.pro_expiry_unix_ts_ms = 10000; - session::uc32 hash{}; - std::fill(hash.begin(), hash.end(), static_cast(3)); - std::memcpy(c.pro_gen_index_hash.data, hash.data(), hash.size()); + std::fill( + c.pro_gen_index_hash.data, + c.pro_gen_index_hash.data + 32, + static_cast(3)); c.has_pro_gen_index_hash = true; convo_info_volatile_set_1to1(conf, &c); @@ -861,7 +835,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { // Load the dump: config_object* conf2; - REQUIRE(0 == convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL)); + REQUIRE(0 == convo_info_volatile_init(&conf2, to_unsigned(ed_sk.data()), dump, dumplen, NULL)); free(dump); @@ -873,4 +847,4 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { CHECK(c.has_pro_gen_index_hash); CHECK(c2.has_pro_gen_index_hash); CHECK(oxenc::to_hex(c2.pro_gen_index_hash.data) == oxenc::to_hex(c.pro_gen_index_hash.data)); -} \ No newline at end of file +} diff --git a/tests/test_config_local.cpp b/tests/test_config_local.cpp index fc3e29392..451456a65 100644 --- a/tests/test_config_local.cpp +++ b/tests/test_config_local.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include @@ -9,21 +8,19 @@ #include #include #include +#include #include #include "utils.hpp" +using namespace session; using namespace std::literals; TEST_CASE("Local", "[config][local]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -32,7 +29,7 @@ TEST_CASE("Local", "[config][local]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::Local local{std::span{seed}, std::nullopt}; + session::config::Local local{seed, std::nullopt}; CHECK(local.get_notification_content() == session::config::notify_content::defaulted); CHECK(local.get_ios_notification_sound() == 0); @@ -60,7 +57,7 @@ TEST_CASE("Local", "[config][local]") { CHECK(local.size_settings() == 1); // Ensure all of these settings were stored in the dump and loaded correctly - session::config::Local local2{std::span{seed}, local.dump()}; + session::config::Local local2{seed, local.dump()}; CHECK_FALSE(local.needs_dump()); CHECK(local2.get_notification_content() == session::config::notify_content::name_no_preview); diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 1fe7845c6..84092cb0d 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -6,16 +6,15 @@ #include #include #include + +#include "session/crypto/ed25519.hpp" +#include "utils.hpp" using namespace oxenc::literals; TEST_CASE("Pro", "[config][pro]") { // Setup keys - std::array rotating_pk, signing_pk; - session::cleared_uc64 rotating_sk, signing_sk; - { - crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); - crypto_sign_ed25519_keypair(signing_pk.data(), signing_sk.data()); - } + auto [rotating_pk, rotating_sk] = ed25519::keypair(); + auto [signing_pk, signing_sk] = ed25519::keypair(); // Setup the Pro data structure session::config::ProConfig pro_cpp = {}; @@ -44,7 +43,7 @@ TEST_CASE("Pro", "[config][pro]") { { // Generate the hashes static_assert(crypto_sign_ed25519_BYTES == pro_cpp.proof.sig.max_size()); - std::array hash_to_sign_cpp = pro_cpp.proof.hash(); + b32 hash_to_sign_cpp = pro_cpp.proof.hash(); cbytes32 hash_to_sign = session_protocol_pro_proof_hash(&pro.proof); static_assert(hash_to_sign_cpp.size() == sizeof(hash_to_sign)); @@ -52,21 +51,8 @@ TEST_CASE("Pro", "[config][pro]") { 0); // Write the signature into the proof - int sig_result = crypto_sign_ed25519_detached( - pro_cpp.proof.sig.data(), - nullptr, - hash_to_sign_cpp.data(), - hash_to_sign_cpp.size(), - signing_sk.data()); - CHECK(sig_result == 0); - - sig_result = crypto_sign_ed25519_detached( - pro.proof.sig.data, - nullptr, - hash_to_sign.data, - sizeof(hash_to_sign.data), - signing_sk.data()); - CHECK(sig_result == 0); + ed25519::sign(pro_cpp.proof.sig, signing_sk, hash_to_sign_cpp); + ed25519::sign(to_byte_span(pro.proof.sig.data), signing_sk, to_byte_span(hash_to_sign.data)); } // Verify expiry @@ -82,18 +68,11 @@ TEST_CASE("Pro", "[config][pro]") { // Verify it can verify messages signed with the rotating public key { std::string_view body = "hello world"; - std::array sig = {}; - int sign_result = crypto_sign_ed25519_detached( - sig.data(), - nullptr, - reinterpret_cast(body.data()), - body.size(), - rotating_sk.data()); - CHECK(sign_result == 0); - CHECK(pro_cpp.proof.verify_message(sig, session::to_span(body))); + auto sig = ed25519::sign(rotating_sk, to_span(body)); + CHECK(pro_cpp.proof.verify_message(sig, to_span(body))); CHECK(session_protocol_pro_proof_verify_message( &pro.proof, - sig.data(), + to_unsigned(sig.data()), sig.size(), reinterpret_cast(body.data()), body.size())); @@ -128,7 +107,7 @@ TEST_CASE("Pro", "[config][pro]") { // Try loading a proof with a bad signature in it from dict { - std::array broken_sig = pro_cpp.proof.sig; + b64 broken_sig = pro_cpp.proof.sig; broken_sig[0] = ~broken_sig[0]; // Break the sig const session::ProProof& proof = pro_cpp.proof; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index dfb5ce339..40ef56d3c 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -79,13 +78,9 @@ TEST_CASE("Open Group URLs", "[config][community_urls]") { TEST_CASE("User Groups", "[config][groups]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -94,7 +89,7 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups groups{std::span{seed}, std::nullopt}; + session::config::UserGroups groups{seed, std::nullopt}; constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"sv; @@ -159,11 +154,8 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(c.members() == expected_members); const auto lgroup_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; - std::array lg_pk; - std::array lg_sk; - crypto_sign_ed25519_seed_keypair( - lg_pk.data(), lg_sk.data(), reinterpret_cast(lgroup_seed.data())); + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hex_b; + auto [lg_pk, lg_sk] = ed25519::keypair(lgroup_seed); // Note: this isn't exactly what Session actually does here for legacy groups (rather it // uses X25519 keys) but for this test the distinction doesn't matter. c.enc_pubkey.assign(lg_pk.data(), lg_pk.data() + lg_pk.size()); @@ -182,7 +174,7 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(groups.needs_dump()); const auto community_pubkey = - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; auto og = groups.get_or_construct_community( "http://Example.ORG:5678", "SudokuRoom", community_pubkey); @@ -310,7 +302,7 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK_FALSE(g2.needs_dump()); REQUIRE(to_push.size() == 1); - std::vector>> to_merge; + std::vector>> to_merge; to_merge.emplace_back("fakehash2", to_push[0]); groups.merge(to_merge); auto x3 = groups.get_community("http://example.org:5678", "SudokuRoom"); @@ -427,13 +419,9 @@ TEST_CASE("User Groups", "[config][groups]") { TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -442,7 +430,7 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups groups{std::span{seed}, std::nullopt}; + session::config::UserGroups groups{seed, std::nullopt}; constexpr auto definitely_real_id = "035000000000000000000000000000000000000000000000000000000000000000"sv; @@ -463,10 +451,10 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { c.secretkey = session::to_vector(ed_sk); // This *isn't* the right secret key for the group, so // won't propagate, and so auth data will: - c.auth_data = + c.auth_data = to_vector( "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000000000"_hexbytes; + "0000000000000000000000000000"_hex_b); groups.set(c); @@ -478,7 +466,7 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { auto d1 = groups.dump(); - session::config::UserGroups g2{std::span{seed}, d1}; + session::config::UserGroups g2{seed, d1}; auto c2 = g2.get_group(definitely_real_id); REQUIRE(c2.has_value()); @@ -504,16 +492,17 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { c2b.secretkey = session::to_vector(ed_sk); // This one does match the group ID, so should propagate c2b.auth_data = // should get ignored, since we have a valid secret key set: + to_vector( "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000000000"_hexbytes; + "0000000000000000000000000000"_hex_b); g2.set(c2b); std::tie(seqno, to_push, obs) = g2.push(); g2.confirm_pushed(seqno, {"fakehash2"}); REQUIRE(to_push.size() == 1); - std::vector>> to_merge; + std::vector>> to_merge; to_merge.emplace_back("fakehash2", to_push[0]); groups.merge(to_merge); @@ -582,13 +571,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { TEST_CASE("User Groups members C API", "[config][groups][c]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -599,7 +584,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { char err[256]; config_object* conf; - rc = user_groups_init(&conf, ed_sk.data(), NULL, 0, err); + auto rc = user_groups_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, err); REQUIRE(rc == 0); constexpr auto definitely_real_id = @@ -719,13 +704,13 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { REQUIRE(keys); REQUIRE(key_len == 1); - session::config::UserGroups c2{std::span{seed}, std::nullopt}; + session::config::UserGroups c2{seed, std::nullopt}; REQUIRE(to_push->n_configs == 1); - std::vector>> to_merge; + std::vector>> to_merge; to_merge.emplace_back( "fakehash1", - std::span{to_push->config[0], to_push->config_lens[0]}); + to_byte_span(to_push->config[0], to_push->config_lens[0])); CHECK(c2.merge(to_merge) == std::unordered_set{{"fakehash1"}}); auto grp = c2.get_legacy_group(definitely_real_id); @@ -739,18 +724,14 @@ TEST_CASE("User groups empty member bug", "[config][groups][bug]") { // the config, even when the current members (or admin) list is empty. (This isn't strictly // specific to user groups, but that's where the bug is easily encountered). - const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups c{std::span{seed}, std::nullopt}; + session::config::UserGroups c{seed, std::nullopt}; CHECK_FALSE(c.needs_push()); @@ -825,18 +806,14 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro // the config, even when the current members (or admin) list is empty. (This isn't strictly // specific to user groups, but that's where the bug is easily encountered). - const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserGroups c{std::span{seed}, std::nullopt}; + session::config::UserGroups c{seed, std::nullopt}; CHECK_FALSE(c.needs_push()); @@ -872,7 +849,7 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro { const auto community_pubkey = - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; const auto url = "http://example.org:5678"; const auto room = "sudoku_room"; auto comm = c.get_or_construct_community(url, room, community_pubkey); @@ -901,8 +878,8 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro "3a03" "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef64313a21303a313a4b" "303a" - "313a6a303a65656565313a28303a313a296c6565"_hexbytes; - session::config::UserGroups c2{std::span{seed}, dump_with_not_seconds}; + "313a6a303a65656565313a28303a313a296c6565"_hex_b; + session::config::UserGroups c2{seed, dump_with_not_seconds}; auto gr = c2.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 630c9d06f..b43e2a310 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -1,17 +1,18 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include "utils.hpp" +using namespace session; using namespace std::literals; namespace { @@ -51,13 +52,9 @@ struct UserProfileTester { TEST_CASE("UserProfile", "[config][user_profile]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -66,7 +63,7 @@ TEST_CASE("UserProfile", "[config][user_profile]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - session::config::UserProfile profile{std::span{seed}, std::nullopt}; + session::config::UserProfile profile{seed, std::nullopt}; CHECK_THROWS( profile.set_name("123456789012345678901234567890123456789012345678901234567890123456789" @@ -93,24 +90,19 @@ TEST_CASE("UserProfile", "[config][user_profile]") { TEST_CASE("user profile C API", "[config][user_profile][c]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); - int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - REQUIRE(rc == 0); + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(seed) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); // Initialize a brand new, empty config because we have no dump data to deal with. char err[256]; config_object* conf; - rc = user_profile_init(&conf, ed_sk.data(), NULL, 0, err); + int rc = user_profile_init(&conf, to_unsigned(ed_sk.data()), NULL, 0, err); REQUIRE(rc == 0); // We don't need to push anything, since this is an empty config @@ -171,10 +163,8 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { pic = user_profile_get_pic(conf); REQUIRE(pic.url != ""s); - REQUIRE(pic.key != session::to_vector("").data()); CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv); - CHECK(session::to_vector(std::span{pic.key, 32}) == - "secret78901234567890123456789012"_bytes); + CHECK(std::ranges::equal(to_byte_span(pic.key), "secret78901234567890123456789012"_bytes)); CHECK(user_profile_get_nts_priority(conf) == 9); @@ -188,7 +178,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // between dumps; even though we changed two fields here). // The hash of a completely empty, initial seqno=0 message: - auto exp_hash0 = "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes; + auto exp_hash0 = "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hex_b; // The data to be actually pushed, expanded like this to make it somewhat human-readable: // clang-format off @@ -221,7 +211,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d" "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656" "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea" - "49bf122762d7bc1d6d9c02f6d54f8384"_hexbytes; + "49bf122762d7bc1d6d9c02f6d54f8384"_hex_b; // Copy this out; we need to hold onto it to do the confirmation later on seqno_t seqno = to_push->seqno; @@ -285,7 +275,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Start with an empty config, as above: config_object* conf2; - REQUIRE(user_profile_init(&conf2, ed_sk.data(), NULL, 0, err) == 0); + REQUIRE(user_profile_init(&conf2, to_unsigned(ed_sk.data()), NULL, 0, err) == 0); CHECK_FALSE(config_needs_dump(conf2)); // Now imagine we just pulled down the encrypted string from the swarm; we merge it into conf2: @@ -293,7 +283,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { const char* merge_hash[1]; size_t merge_size[1]; merge_hash[0] = "fakehash1"; - merge_data[0] = exp_push1_encrypted.data(); + merge_data[0] = to_unsigned(exp_push1_encrypted.data()); merge_size[0] = exp_push1_encrypted.size(); config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); REQUIRE(accepted->len == 1); @@ -415,7 +405,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(oxenc::to_hex(std::span{pic.key, 32}) == + CHECK(oxenc::to_hex(to_byte_span(pic.key)) == "7177657274007975696f31323334353637383930313233343536373839303132"); pic = user_profile_get_pic(conf2); #if defined(__APPLE__) || defined(__clang__) || defined(__llvm__) @@ -429,7 +419,7 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { #else REQUIRE(pic.key != nullptr); #endif - CHECK(oxenc::to_hex(std::span{pic.key, 32}) == + CHECK(oxenc::to_hex(to_byte_span(pic.key)) == "7177657274007975696f31323334353637383930313233343536373839303132"); CHECK(user_profile_get_nts_priority(conf) == 9); @@ -456,10 +446,9 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { // Check the current pic pic = user_profile_get_pic(conf); REQUIRE(pic.url != ""s); - REQUIRE(pic.key != session::to_vector("").data()); + CHECK(pic.url == "http://new.example.com/pic"sv); - CHECK(session::to_vector(std::span{pic.key, 32}) == - "qwert\0yuio1234567890123456789012"_bytes); + CHECK(std::ranges::equal(to_byte_span(pic.key), "qwert\0yuio1234567890123456789012"_bytes)); // Reupload the "current" pic and confirm it gets returned strcpy(p.url, "testUrl"); @@ -468,10 +457,9 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { pic = user_profile_get_pic(conf); REQUIRE(pic.url != ""s); - REQUIRE(pic.key != session::to_vector("").data()); + CHECK(pic.url == "testUrl"sv); - CHECK(session::to_vector(std::span{pic.key, 32}) == - "secret78901234567890123456789000"_bytes); + CHECK(std::ranges::equal(to_byte_span(pic.key), "secret78901234567890123456789000"_bytes)); // Upload a "new" pic and it now gets returned strcpy(p.url, "testNewUrl"); @@ -479,10 +467,9 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { CHECK(0 == user_profile_set_pic(conf, p)); pic = user_profile_get_pic(conf); REQUIRE(pic.url != ""s); - REQUIRE(pic.key != session::to_vector("").data()); + CHECK(pic.url == "testNewUrl"sv); - CHECK(session::to_vector(std::span{pic.key, 32}) == - "secret78901234567890123456789111"_bytes); + CHECK(std::ranges::equal(to_byte_span(pic.key), "secret78901234567890123456789111"_bytes)); // Ensure the timestamp for the last modified pic gets updated correctly when the name gets set UserProfileTester::set_profile_updated(conf, std::chrono::sys_seconds{0s}); @@ -538,13 +525,13 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { TEST_CASE("user profile timestamp update bug", "[config][user_profile]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; - session::config::UserProfile profile{std::span{seed}, std::nullopt}; + session::config::UserProfile profile{seed, std::nullopt}; // Initially the code would update `profile_updated` even if the data hadn't changed, this test // verifies that no longer happens - std::vector key = "qwerty78901234567890123456789012"_bytes; + auto key = to_vector("qwerty78901234567890123456789012"_bytes); std::string url = "http://example.com/huge.bmp"; profile.set_name("Nibbler"); profile.set_blinded_msgreqs(true); @@ -564,9 +551,9 @@ TEST_CASE("user profile timestamp update bug", "[config][user_profile]") { TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; - session::config::UserProfile profile{std::span{seed}, std::nullopt}; + session::config::UserProfile profile{seed, std::nullopt}; // Ensure the bitset is being updated correctly CHECK(profile.get_profile_bitset().data == 0); @@ -596,7 +583,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); { - session::config::UserProfile profile2{std::span{seed}, profile.dump()}; + session::config::UserProfile profile2{seed, profile.dump()}; CHECK(profile2.get_profile_bitset().is_set( SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); CHECK_FALSE(profile2.get_profile_bitset().is_set( @@ -604,12 +591,8 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { } // Ensure the pro config is being stored correctly - std::array rotating_pk, signing_pk; - session::cleared_uc64 rotating_sk, signing_sk; - { - crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); - crypto_sign_ed25519_keypair(signing_pk.data(), signing_sk.data()); - } + auto [rotating_pk, rotating_sk] = ed25519::keypair(); + auto [signing_pk, signing_sk] = ed25519::keypair(); session::config::ProConfig pro_cpp = {}; pro_pro_config pro = {}; @@ -620,7 +603,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { pro_cpp.proof.rotating_pubkey = rotating_pk; pro_cpp.proof.expiry_unix_ts = std::chrono::sys_time(1s); constexpr auto gen_index_hash = - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_u; + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; static_assert(pro_cpp.proof.gen_index_hash.max_size() == gen_index_hash.size()); std::memcpy( pro_cpp.proof.gen_index_hash.data(), gen_index_hash.data(), gen_index_hash.size()); @@ -641,7 +624,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { CHECK(profile.get_profile_updated().time_since_epoch().count() != 123); { - session::config::UserProfile profile2{std::span{seed}, profile.dump()}; + session::config::UserProfile profile2{seed, profile.dump()}; CHECK(profile.get_pro_config() == pro_cpp); } diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 05ba68f6b..9da83a81f 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -1,6 +1,7 @@ #include #include -#include + +#include #include #include @@ -327,20 +328,18 @@ TEST_CASE("config message signature", "[config][signing]") { constexpr auto skey_hex = "79f530dbf3d81aecc04072933c1b3e3edc0b7d91f2dcc2f7756f2611886cca5f" "4384261cdd338f5820ca9cbbe3fc72ac8944ee60d3b795b797fbbf5597b09f17"sv; - std::array secretkey; + b64 secretkey; oxenc::from_hex(skey_hex.begin(), skey_hex.end(), secretkey.begin()); - auto signer = [&secretkey](std::span data) { - std::vector result; - result.resize(64); - crypto_sign_ed25519_detached( - result.data(), nullptr, data.data(), data.size(), secretkey.data()); - return result; + ed25519::PrivKeySpan sk{secretkey}; + auto signer = [&sk](std::span data) { + auto sig = ed25519::sign(sk, data); + return std::vector{sig.begin(), sig.end()}; }; - auto verifier = [&secretkey]( - std::span data, - std::span signature) { - return 0 == crypto_sign_verify_detached( - signature.data(), data.data(), data.size(), secretkey.data() + 32); + auto verifier = [&sk]( + std::span data, + std::span signature) { + return signature.size() == 64 && + ed25519::verify(signature.first<64>(), sk.pubkey(), data); }; m.signer = signer; @@ -374,15 +373,13 @@ TEST_CASE("config message signature", "[config][signing]") { auto expected_sig = "77267f4de7701ae348eba0ef73175281512ba3f1051cfed22dc3e31b9c699330" - "2938863e09bc8b33638161071bd8dc397d5c1d3f674120d08fbb9c64dde2e907"_hexbytes; - std::vector sig(64, '\0'); + "2938863e09bc8b33638161071bd8dc397d5c1d3f674120d08fbb9c64dde2e907"_hex_b; // Sign it ourselves, and check what we get: - crypto_sign_ed25519_detached( - sig.data(), nullptr, m_signing_value.data(), m_signing_value.size(), secretkey.data()); + auto sig = ed25519::sign(sk, m_signing_value); CHECK(to_hex(sig) == to_hex(expected_sig)); auto key_bytes = "1:~64:"_bytes; auto end_bytes = "e"_bytes; - auto m_expected = m_signing_value; + auto m_expected = to_vector(m_signing_value); m_expected.insert(m_expected.end(), key_bytes.begin(), key_bytes.end()); m_expected.insert(m_expected.end(), expected_sig.begin(), expected_sig.end()); m_expected.insert(m_expected.end(), end_bytes.begin(), end_bytes.end()); @@ -395,8 +392,8 @@ TEST_CASE("config message signature", "[config][signing]") { // Deliberately modify the signature to break it: auto m_broken = m_expected; - REQUIRE(m_broken[m_broken.size() - 2] == 0x07); - m_broken[m_broken.size() - 2] = 0x17; + REQUIRE(m_broken[m_broken.size() - 2] == std::byte{0x07}); + m_broken[m_broken.size() - 2] = std::byte{0x17}; using Catch::Matchers::Message; CHECK_THROWS_AS(ConfigMessage(m_broken, verifier), config::signature_error); @@ -423,7 +420,7 @@ TEST_CASE("config message signature", "[config][signing]") { config::config_error, Message("Config signature failed verification")); - auto m_unsigned = m_signing_value; + auto m_unsigned = to_vector(m_signing_value); m_unsigned.insert(m_unsigned.end(), end_bytes.begin(), end_bytes.end()); CHECK_THROWS_MATCHES( ConfigMessage(m_unsigned, verifier), @@ -442,10 +439,10 @@ const config::dict data118{ {"string2", "goodbye"}, }; -const auto h119 = "43094f68c1faa37eff79e1c2f3973ffd5f9d6423b00ccda306fc6e7dac5f0c44"_hexbytes; -const auto h120 = "e3a237f91014d31e4d30569c4a8bfcd72157804f99b8732c611c48bf126432b5"_hexbytes; -const auto h121 = "1a7f602055124deaf21175ef3f32983dee7c9de570e5d9c9a0bbc2db71dcb97f"_hexbytes; -const auto h122 = "46560604fe352101bb869435260d7100ccfe007be5f741c7e96303f02f394e8a"_hexbytes; +const auto h119 = "43094f68c1faa37eff79e1c2f3973ffd5f9d6423b00ccda306fc6e7dac5f0c44"_hex_b; +const auto h120 = "e3a237f91014d31e4d30569c4a8bfcd72157804f99b8732c611c48bf126432b5"_hex_b; +const auto h121 = "1a7f602055124deaf21175ef3f32983dee7c9de570e5d9c9a0bbc2db71dcb97f"_hex_b; +const auto h122 = "46560604fe352101bb869435260d7100ccfe007be5f741c7e96303f02f394e8a"_hex_b; const auto m123_expected = to_vector( // clang-format off "d" @@ -486,7 +483,7 @@ const auto m123_expected = to_vector( "e" "e"); // clang-format on -const auto h123 = "d9398c597b058ac7e28e3febb76ed68eb8c5b6c369610562ab5f2b596775d73c"_hexbytes; +const auto h123 = "d9398c597b058ac7e28e3febb76ed68eb8c5b6c369610562ab5f2b596775d73c"_hex_b; TEST_CASE("config message example 1", "[config][example]") { /// This is the "Ordinary update" example described in docs/api/docs/config-merge-logic.md @@ -716,7 +713,7 @@ static void updates_124(MutableConfigMessage& m) { m.data().erase("great"); } -const auto h124 = "8b73f316178765b9b3b37168e865c84bb5a78610cbb59b84d0fa4d3b4b3c102b"_hexbytes; +const auto h124 = "8b73f316178765b9b3b37168e865c84bb5a78610cbb59b84d0fa4d3b4b3c102b"_hex_b; TEST_CASE("config message example 2", "[config][example]") { /// This is the "Large, but still ordinary, update" example described in @@ -803,8 +800,8 @@ TEST_CASE("config message example 2", "[config][example]") { CHECK(to_hex(m.hash()) == to_hex(h124)); } -const auto h125a = "80f229c3667de6d0fa6f96b53118e097fbda82db3ca1aea221a3db91ea9c45fb"_hexbytes; -const auto h125b = "ab12f0efe9a9ed00db6b17b44ae0ff36b9f49094077fb114f415522f2a0e98de"_hexbytes; +const auto h125a = "80f229c3667de6d0fa6f96b53118e097fbda82db3ca1aea221a3db91ea9c45fb"_hex_b; +const auto h125b = "ab12f0efe9a9ed00db6b17b44ae0ff36b9f49094077fb114f415522f2a0e98de"_hex_b; // clang-format off const auto m126_expected = to_vector( @@ -992,7 +989,7 @@ TEST_CASE("config message example 4 - complex conflict resolution", "[config][ex m120b.serialize()}}; REQUIRE(m124a.hash() < m124b.hash()); - REQUIRE(h125a < h125b); + REQUIRE(to_hex(h125a) < to_hex(h125b)); REQUIRE(m126a.hash() < m126b.hash()); // Now we merge m126a and m126b together and should end up with the final merged result. diff --git a/tests/test_core_devices.cpp b/tests/test_core_devices.cpp index f62c4ec04..6f3c37ffd 100644 --- a/tests/test_core_devices.cpp +++ b/tests/test_core_devices.cpp @@ -364,8 +364,9 @@ TEST_CASE("Devices - build_account_pubkey_message", "[core][devices]") { auto x25519_pub = c->globals.session_id().template subspan<1>(); // skip 0x05 prefix bool sig_valid = false; dict.require_signature( - "~", [&](std::span body, std::span sig) { - sig_valid = xed25519::verify(sig, x25519_pub, body); + "~", [&](std::span body, std::span sig) { + sig_valid = + sig.size() == 64 && xed25519::verify(sig.first<64>(), x25519_pub, body); }); CHECK(sig_valid); } diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp index 14323dd25..ad03537ba 100644 --- a/tests/test_curve25519.cpp +++ b/tests/test_curve25519.cpp @@ -1,51 +1,47 @@ #include #include +#include +#include #include #include "session/curve25519.h" -#include "session/curve25519.hpp" #include "utils.hpp" +using namespace session; +using namespace session::literals; + TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { - auto kp1 = session::curve25519::curve25519_key_pair(); - auto kp2 = session::curve25519::curve25519_key_pair(); + auto [pk1, sk1] = x25519::keypair(); + auto [pk2, sk2] = x25519::keypair(); - CHECK(kp1.first.size() == 32); - CHECK(kp1.second.size() == 32); - CHECK(kp1.first != kp2.first); - CHECK(kp1.second != kp2.second); + CHECK(pk1.size() == 32); + CHECK(sk1.size() == 32); + CHECK(pk1 != pk2); + CHECK(sk1 != sk2); } TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { - using namespace session; - - auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; - auto x_pk1 = curve25519::to_curve25519_pubkey(to_span(ed_pk1)); - auto x_pk2 = curve25519::to_curve25519_pubkey(to_span(ed_pk2)); + auto x_pk1 = ed25519::pk_to_x25519(ed_pk1); + auto x_pk2 = ed25519::pk_to_x25519(ed_pk2); - CHECK(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == - "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == - "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + CHECK(to_hex(x_pk1) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(to_hex(x_pk2) == "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); } TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { - using namespace session; - auto ed_sk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_b; auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" - "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; - auto x_sk1 = curve25519::to_curve25519_seckey(to_span(ed_sk1)); - auto x_sk2 = curve25519::to_curve25519_seckey(to_span(ed_sk2)); - - CHECK(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == - "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); - CHECK(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == - "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hex_b; + auto x_sk1 = ed25519::sk_to_x25519(ed_sk1); + auto x_sk2 = ed25519::sk_to_x25519(ed_sk2); + + CHECK(to_hex(x_sk1) == "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); + CHECK(to_hex(x_sk2) == "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); } diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index d0c2ec4a0..00907b5af 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/crypto/ed25519.hpp" #include "test_helper.hpp" using namespace session; @@ -22,39 +23,30 @@ constexpr auto SENDER_SEED = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_u; struct SenderKeys { - std::array ed_pk; - std::array ed_sk; - std::array session_id; // 0x05-prefixed long-term X25519 pubkey + b32 ed_pk; + b64 ed_sk; + b33 session_id; // 0x05-prefixed long-term X25519 pubkey SenderKeys() { - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), SENDER_SEED.data()); - std::array curve_pk; - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); - session_id[0] = 0x05; - std::ranges::copy(curve_pk, session_id.begin() + 1); + ed25519::keypair(ed_pk, ed_sk); + ed25519::pk_to_session_id(session_id, ed_pk); } }; // Bundles an owned data buffer with a SwarmMessage whose data span points into it, // keeping lifetime correct: the buffer must outlive any SwarmMessage referencing it. struct OwnedMessage { - std::vector data; + std::vector data; SwarmMessage msg; explicit OwnedMessage( - std::span d, + std::span d, std::string hash = "testhash", sys_ms ts = from_epoch_ms(1000), sys_ms exp = from_epoch_ms(9999)) : data{d.begin(), d.end()}, msg{data, std::move(hash), ts, exp} {} }; -// Cast the std::byte pubkeys returned by TestHelper into unsigned-char spans. -template -std::span as_uc(const std::array& a) { - return std::span{reinterpret_cast(a.data()), N}; -} - } // namespace // ── V1 happy path ──────────────────────────────────────────────────────────────────────────────── @@ -72,11 +64,11 @@ TEST_CASE("_handle_direct_messages: v1 receive", "[core][dm]") { TempCore recipient{cbs}; - uc33 recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. - constexpr auto plaintext = "7801"_hex_u; + constexpr auto plaintext = "7801"_hex_b; auto encoded = encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); @@ -113,17 +105,12 @@ TEST_CASE("_handle_direct_messages: v2 receive", "[core][dm]") { recipient->devices.active_account_keys(); auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); - std::array recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); - constexpr auto content = "deadbeef"_hex_u; + constexpr auto content = "deadbeef"_hex_b; auto ct = encrypt_for_recipient_v2( - sender.ed_sk, - recip_session_id, - as_uc(x25519_bytes), - as_uc(mlkem_bytes), - content, - std::nullopt); + sender.ed_sk, recip_session_id, x25519_bytes, mlkem_bytes, content, std::nullopt); OwnedMessage om{std::span{ct}, "hash_v2", from_epoch_ms(5678), from_epoch_ms(8888)}; recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); @@ -156,20 +143,20 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { TempCore recipient{cbs}; - auto deliver = [&](std::span data) { + auto deliver = [&](std::span data) { OwnedMessage om{data}; recipient->receive_messages({&om.msg, 1}, config::Namespace::Default, true); }; SECTION("empty data → bad_format") { - deliver(std::span{}); + deliver(std::span{}); CHECK(received.empty()); REQUIRE(failures.size() == 1); CHECK(failures[0] == MessageDecryptFailure::bad_format); } SECTION("0x00 0x03 → unknown_version") { - deliver("0003010203"_hex_u); + deliver("0003010203"_hex_b); CHECK(received.empty()); REQUIRE(failures.size() == 1); CHECK(failures[0] == MessageDecryptFailure::unknown_version); @@ -177,7 +164,7 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { SECTION("v2 too short for prefix decryption → bad_format") { // Prefix decryption needs at least version(2) + ki(2) + ephemeral_E(32) = 36 bytes. - deliver("00020102030405060708"_hex_u); + deliver("00020102030405060708"_hex_b); CHECK(received.empty()); REQUIRE(failures.size() == 1); CHECK(failures[0] == MessageDecryptFailure::bad_format); @@ -188,15 +175,15 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { TempCore other; other->devices.active_account_keys(); auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*other); - std::array other_session_id; + b33 other_session_id; std::ranges::copy(other->globals.session_id(), other_session_id.begin()); auto ct = encrypt_for_recipient_v2( sender.ed_sk, other_session_id, - as_uc(x25519_bytes), - as_uc(mlkem_bytes), - "01"_hex_u, + x25519_bytes, + mlkem_bytes, + "01"_hex_b, std::nullopt); deliver(std::span{ct}); CHECK(received.empty()); @@ -210,20 +197,20 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { // Both paths throw DecryptV2Error, so no_pfs_key is fired (nothing could decrypt it). recipient->devices.active_account_keys(); auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); - std::array recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); auto ct = encrypt_for_recipient_v2( sender.ed_sk, recip_session_id, - as_uc(x25519_bytes), - as_uc(mlkem_bytes), - "01"_hex_u, + x25519_bytes, + mlkem_bytes, + "01"_hex_b, std::nullopt); // Wire format: [0,1]=version, [2,3]=ki, [4,35]=E, [36,1123]=mlkem_ct, [1124+]=xchacha. // Flip the final byte of the xchacha ciphertext to corrupt the AEAD tag. REQUIRE(ct.size() > 1124 + 16); - ct.back() ^= 0xff; + ct.back() ^= std::byte{0xff}; deliver(std::span{ct}); CHECK(received.empty()); REQUIRE(failures.size() == 1); @@ -231,7 +218,7 @@ TEST_CASE("_handle_direct_messages: failure paths", "[core][dm]") { } SECTION("v1 malformed ciphertext → decrypt_failed") { - deliver("0102030405060708"_hex_u); + deliver("0102030405060708"_hex_b); CHECK(received.empty()); REQUIRE(failures.size() == 1); CHECK(failures[0] == MessageDecryptFailure::decrypt_failed); @@ -427,11 +414,11 @@ TEST_CASE( TempCore recipient{cbs}; - uc33 recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); // Minimal valid SessionProtos::Content: field 15 (sigTimestamp) = 1. - constexpr auto plaintext = "7801"_hex_u; + constexpr auto plaintext = "7801"_hex_b; auto e1 = encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); auto e2 = encode_dm_v1(plaintext, sender.ed_sk, clock_now_ms(), recip_session_id, std::nullopt); diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index 98b715533..8064fe55c 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -3,12 +3,13 @@ #include #include -#include "session/ed25519.hpp" +#include "session/crypto/ed25519.hpp" +#include "session/pro_backend.hpp" TEST_CASE("Ed25519 key pair generation", "[ed25519][keypair]") { // Generate two random key pairs and make sure they don't match - auto [pk1, sk1] = session::ed25519::ed25519_key_pair(); - auto [pk2, sk2] = session::ed25519::ed25519_key_pair(); + auto [pk1, sk1] = session::ed25519::keypair(); + auto [pk2, sk2] = session::ed25519::keypair(); CHECK(pk1.size() == 32); CHECK(sk1.size() == 64); @@ -20,14 +21,12 @@ TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { using namespace session; constexpr auto ed_seed1 = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_u; + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; constexpr auto ed_seed2 = - "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_u; - constexpr auto ed_seed_invalid = "010203040506070809"_hex_u; + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; - auto [pk1, sk1] = session::ed25519::ed25519_key_pair(ed_seed1); - auto [pk2, sk2] = session::ed25519::ed25519_key_pair(ed_seed2); - CHECK_THROWS(session::ed25519::ed25519_key_pair(ed_seed_invalid)); + auto [pk1, sk1] = session::ed25519::keypair(ed_seed1); + auto [pk2, sk2] = session::ed25519::keypair(ed_seed2); CHECK(pk1.size() == 32); CHECK(sk1.size() == 64); @@ -50,15 +49,13 @@ TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { using namespace session; constexpr auto ed_sk1 = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" - "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_u; + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7" + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_b; constexpr auto ed_sk2 = - "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_u; - constexpr auto ed_sk_invalid = "010203040506070809"_hex_u; + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; - auto seed1 = session::ed25519::seed_for_ed_privkey(ed_sk1); - auto seed2 = session::ed25519::seed_for_ed_privkey(ed_sk2); - CHECK_THROWS(session::ed25519::seed_for_ed_privkey(ed_sk_invalid)); + auto seed1 = session::ed25519::extract_seed(ed_sk1); + auto seed2 = session::ed25519::extract_seed(ed_sk2); CHECK(oxenc::to_hex(seed1) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); @@ -95,13 +92,11 @@ TEST_CASE("Ed25519 pro key pair generation seed", "[ed25519][keypair]") { // // clang-format on - constexpr auto seed1 = "e5481635020d6f7b327e94e6d63e33a431fccabc4d2775845c43a8486a9f2884"_hex_u; - constexpr auto seed2 = "743d646706b6b04b97b752036dd6cf5f2adc4b339fcfdfb4b496f0764bb93a84"_hex_u; - constexpr auto seed_invalid = "010203040506070809"_hex_u; + constexpr auto seed1 = "e5481635020d6f7b327e94e6d63e33a431fccabc4d2775845c43a8486a9f2884"_hex_b; + constexpr auto seed2 = "743d646706b6b04b97b752036dd6cf5f2adc4b339fcfdfb4b496f0764bb93a84"_hex_b; - auto sk1 = session::ed25519::ed25519_pro_privkey_for_ed25519_seed(seed1); - auto sk2 = session::ed25519::ed25519_pro_privkey_for_ed25519_seed(seed2); - CHECK_THROWS(session::ed25519::ed25519_pro_privkey_for_ed25519_seed(seed_invalid)); + auto [pk1, sk1] = session::ed25519::derive_subkey(seed1, session::pro_backend::pro_subkey_domain); + auto [pk2, sk2] = session::ed25519::derive_subkey(seed2, session::pro_backend::pro_subkey_domain); CHECK(sk1.size() == 64); CHECK(sk1 != sk2); @@ -121,8 +116,9 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { using namespace session; constexpr auto ed_seed = - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_u; - constexpr auto ed_pk = "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_u; + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; + constexpr auto ed_pk = + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_b; constexpr auto ed_invalid = "010203040506070809"_hex_u; auto sig1 = session::ed25519::sign(ed_seed, to_span("hello")); @@ -134,6 +130,4 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { CHECK(oxenc::to_hex(sig1) == expected_sig_hex); CHECK(session::ed25519::verify(sig1, ed_pk, to_span("hello"))); - CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_span("hello"))); - CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_span("hello"))); } diff --git a/tests/test_encrypt.cpp b/tests/test_encrypt.cpp index 940db8a8a..2c304e581 100644 --- a/tests/test_encrypt.cpp +++ b/tests/test_encrypt.cpp @@ -14,8 +14,8 @@ using namespace session; TEST_CASE("config message encryption", "[config][encrypt]") { auto message1 = "some message 1"_bytes; - auto key1 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes; - auto key2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; + auto key1 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hex_b; + auto key2 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; auto enc1 = config::encrypt(message1, key1, "test-suite1"); CHECK(oxenc::to_hex(enc1.begin(), enc1.end()) == "f14f242a26638f3305707d1035e734577f943cd7d28af58e32637e" @@ -24,19 +24,19 @@ TEST_CASE("config message encryption", "[config][encrypt]") { CHECK(to_hex(enc2) != to_hex(enc1)); auto enc3 = config::encrypt(message1, key2, "test-suite1"); CHECK(to_hex(enc3) != to_hex(enc1)); - auto nonce = std::vector{enc1.begin() + (enc1.size() - 24), enc1.end()}; - auto nonce2 = std::vector{enc2.begin() + (enc2.size() - 24), enc2.end()}; - auto nonce3 = std::vector{enc3.begin() + (enc3.size() - 24), enc3.end()}; + auto nonce = std::vector{enc1.begin() + (enc1.size() - 24), enc1.end()}; + auto nonce2 = std::vector{enc2.begin() + (enc2.size() - 24), enc2.end()}; + auto nonce3 = std::vector{enc3.begin() + (enc3.size() - 24), enc3.end()}; CHECK(to_hex(nonce) == "af2f4860cb4d0f8ba7e09d29e31f5e4a18f65847287a54a0"); CHECK(to_hex(nonce2) == "277e639d36ba46470dfff509a68cb73d9a96386c51739bdd"); CHECK(to_hex(nonce3) == to_hex(nonce)); auto plain = config::decrypt(enc1, key1, "test-suite1"); - CHECK(plain == message1); + CHECK(std::ranges::equal(plain, message1)); CHECK_THROWS_AS(config::decrypt(enc1, key1, "test-suite2"), config::decrypt_error); CHECK_THROWS_AS(config::decrypt(enc1, key2, "test-suite1"), config::decrypt_error); - enc1[3] = '\x42'; + enc1[3] = std::byte{0x42}; CHECK_THROWS_AS(config::decrypt(enc1, key1, "test-suite1"), config::decrypt_error); } diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 4dfea5c11..73d8ed416 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -1,12 +1,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include "utils.hpp" @@ -14,24 +14,23 @@ using namespace std::literals; using namespace oxenc::literals; using namespace session::config; +using namespace session; TEST_CASE("Group Info settings", "[config][groups][info]") { - const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector> enc_keys{ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; + std::vector> enc_keys; + enc_keys.push_back(to_vector( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)); - groups::Info ginfo1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Info ginfo1{ed_pk, ed_sk, std::nullopt}; // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. @@ -40,10 +39,11 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { enc_keys.insert( enc_keys.begin(), - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); - enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); - enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Info ginfo2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + to_vector( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); + enc_keys.push_back(to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); + enc_keys.push_back(to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); + groups::Info ginfo2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. ginfo2.add_key(k, false); @@ -64,7 +64,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.needs_dump()); CHECK_FALSE(ginfo1.needs_push()); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", p1[0]); CHECK(ginfo2.merge(merge_configs) == std::unordered_set{{"fakehash1"s}}); CHECK_FALSE(ginfo2.needs_push()); @@ -73,7 +73,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { ginfo2.set_profile_pic( "http://example.com/12345", - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b); ginfo2.set_expiry_timer(1h); constexpr int64_t create_time{1682529839}; ginfo2.set_created(create_time); @@ -96,9 +96,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { // This fails because ginfo1 doesn't yet have the new key that ginfo2 used (bbb...) CHECK(ginfo1.merge(merge_configs) == std::unordered_set{}); - ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + ginfo1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b); ginfo1.add_key( - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b, /*prepend=*/false); CHECK(ginfo1.merge(merge_configs) == std::unordered_set{{"fakehash2"s}}); @@ -108,8 +108,8 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.get_name() == "Better name!"); CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); - CHECK(ginfo1.get_profile_pic().key == - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(std::ranges::equal(ginfo1.get_profile_pic().key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); CHECK(ginfo1.get_expiry_timer() == 1h); CHECK(ginfo1.get_created() == create_time); CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400); @@ -123,8 +123,8 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.merge(merge_configs) == std::unordered_set{{"fakehash3"s}}); CHECK(ginfo2.get_name() == "Better name!"); CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345"); - CHECK(ginfo2.get_profile_pic().key == - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(std::ranges::equal(ginfo2.get_profile_pic().key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); CHECK(ginfo2.get_expiry_timer() == 1h); CHECK(ginfo2.get_created() == create_time); CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); @@ -144,28 +144,25 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { - const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector> enc_keys1; + std::vector> enc_keys1; enc_keys1.push_back( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); - std::vector> enc_keys2; + to_vector("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)); + std::vector> enc_keys2; enc_keys2.push_back( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + to_vector("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); enc_keys2.push_back( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes); + to_vector("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)); // This Info object has only the public key, not the priv key, and so cannot modify things: - groups::Info ginfo{session::to_span(ed_pk), std::nullopt, std::nullopt}; + groups::Info ginfo{ed_pk, std::nullopt, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo.add_key(k, false); @@ -177,7 +174,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: - groups::Info ginfo_rw{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Info ginfo_rw{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw.add_key(k, false); @@ -195,12 +192,12 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo_rw.needs_dump()); CHECK_FALSE(ginfo_rw.needs_push()); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", to_push.at(0)); CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash1"s}}); CHECK_FALSE(ginfo.needs_push()); - groups::Info ginfo_rw2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Info ginfo_rw2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_rw2.add_key(k, false); @@ -219,21 +216,16 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // Deliberately use the wrong signing key so that what we produce encrypts successfully but // doesn't verify const auto seed_bad1 = - "0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - std::array ed_pk_bad1; - std::array ed_sk_bad1; - crypto_sign_ed25519_seed_keypair( - ed_pk_bad1.data(), - ed_sk_bad1.data(), - reinterpret_cast(seed_bad1.data())); + "0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + auto [ed_pk_bad1, ed_sk_bad1] = ed25519::keypair(seed_bad1); - groups::Info ginfo_bad1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Info ginfo_bad1{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo_bad1.add_key(k, false); ginfo_bad1.merge(merge_configs); - ginfo_bad1.set_sig_keys(session::to_span(ed_sk_bad1)); + ginfo_bad1.set_sig_keys(ed_sk_bad1); ginfo_bad1.set_name("Bad name, BAD!"); auto [s_bad, p_bad, o_bad] = ginfo_bad1.push(); @@ -310,7 +302,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.needs_dump()); auto dump = ginfo.dump(); - groups::Info ginfo2{session::to_span(ed_pk), std::nullopt, dump}; + groups::Info ginfo2{ed_pk, std::nullopt, dump}; for (const auto& k : enc_keys1) // Just for testing, as above. ginfo2.add_key(k, false); @@ -328,7 +320,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(o5.empty()); // This account has a different primary decryption key - groups::Info ginfo_rw3{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Info ginfo_rw3{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys2) // Just for testing, as above. ginfo_rw3.add_key(k, false); @@ -348,7 +340,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { ginfo_rw3.set_profile_pic( "http://example.com/12345", - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b); CHECK(ginfo_rw3.needs_push()); auto [s7, t7, o7] = ginfo_rw3.push(); CHECK(s7 == s6 + 1); @@ -366,5 +358,5 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { auto pic = ginfo.get_profile_pic(); CHECK_FALSE(pic.empty()); CHECK(pic.url == "http://example.com/12345"); - CHECK(pic.key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); + CHECK(std::ranges::equal(pic.key, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); } diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 0af36f311..93212af56 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -19,29 +19,12 @@ #include #include +#include "session/crypto/ed25519.hpp" #include "utils.hpp" using namespace std::literals; using namespace session::config; -static std::array sk_from_seed(std::span seed) { - std::array ignore; - std::array sk; - crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); - return sk; -} - -static std::string session_id_from_ed(std::span ed_pk) { - std::string sid; - std::array xpk; - int rc = crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed_pk.data()); - REQUIRE(rc == 0); - sid.reserve(66); - sid += "05"; - oxenc::to_hex(xpk.begin(), xpk.end(), std::back_inserter(sid)); - return sid; -} - // Hacky little class that implements `[n]` on a std::list. This is inefficient (since it access // has to iterate n times through the list) but we only use it on small lists in this test code so // convenience wins over efficiency. (Why not just use a vector? Because vectors requires `T` to @@ -52,38 +35,26 @@ struct hacky_list : std::list { }; struct pseudo_client { - std::array secret_key; - const std::span public_key{secret_key.data() + 32, 32}; - std::string session_id{session_id_from_ed(public_key)}; + cleared_b64 secret_key; + const std::span public_key{secret_key.data() + 32, 32}; + std::string session_id = oxenc::to_hex(ed25519::pk_to_session_id(public_key)); groups::Info info; groups::Members members; groups::Keys keys; pseudo_client( - std::span seed, + std::span seed, bool admin, - const unsigned char* gpk, - std::optional gsk, - std::optional> info_dump = std::nullopt, - std::optional> members_dump = std::nullopt, - std::optional> keys_dump = std::nullopt) : - secret_key{sk_from_seed(seed)}, - info{std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) - : std::nullopt, - info_dump}, - members{std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) - : std::nullopt, - members_dump}, - keys{session::to_span(secret_key), - std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) - : std::nullopt, - keys_dump, - info, - members} { + std::span gpk, + const ed25519::OptionalPrivKeySpan& gsk, + std::optional> info_dump = std::nullopt, + std::optional> members_dump = std::nullopt, + std::optional> keys_dump = std::nullopt) : + secret_key{ed25519::keypair(seed).second}, + info{gpk, gsk, info_dump}, + members{gpk, gsk, members_dump}, + keys{secret_key, gpk, gsk, keys_dump, info, members} { if (gsk) keys.rekey(info, members); } @@ -91,25 +62,22 @@ struct pseudo_client { TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { - const std::vector group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const std::vector admin1_seed = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const std::vector admin2_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; - const std::array member_seeds = { - "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 - "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 - "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes, // member4 - "3333333333333333333333333333333333333333333333333333333333333333"_hexbytes, // member3b - "4444444444444444444444444444444444444444444444444444444444444444"_hexbytes, // member4b + constexpr auto group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hex_b; + constexpr auto admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + constexpr auto admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hex_b; + constexpr std::array member_seeds = { + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hex_b, // member1 + "00011122435111155566677788811263446552465222efff0123456789abcdef"_hex_b, // member2 + "00011129824754185548239498168169316979583253efff0123456789abcdef"_hex_b, // member3 + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hex_b, // member4 + "3333333333333333333333333333333333333333333333333333333333333333"_hex_b, // member3b + "4444444444444444444444444444444444444444444444444444444444444444"_hex_b, // member4b }; - std::array group_pk; - std::array group_sk; - - crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + auto [group_pk, group_sk] = ed25519::keypair(group_seed); REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); @@ -120,11 +88,11 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { hacky_list members; // Initialize admin and member objects - admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); - admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); + admins.emplace_back(admin1_seed, true, group_pk, group_sk); + admins.emplace_back(admin2_seed, true, group_pk, group_sk); for (int i = 0; i < 4; ++i) - members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + members.emplace_back(member_seeds[i], false, group_pk, std::nullopt); REQUIRE(admins[0].session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); @@ -144,8 +112,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { for (const auto& m : members) REQUIRE(m.members.size() == 0); - std::vector>> info_configs; - std::vector>> mem_configs; + std::vector>> info_configs; + std::vector>> mem_configs; // add admin account, re-key, distribute auto& admin1 = admins[0]; @@ -296,7 +264,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(admin1.members.needs_push()); - std::vector old_key = session::to_vector(admin1.keys.group_enc_key()); + std::vector old_key = session::to_vector(admin1.keys.group_enc_key()); auto new_keys_config4 = admin1.keys.rekey(admin1.info, admin1.members); CHECK(not new_keys_config4.empty()); @@ -363,7 +331,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // Add two new members and send them supplemental keys for (int i = 0; i < 2; ++i) { - auto& m = members.emplace_back(member_seeds[4 + i], false, group_pk.data(), std::nullopt); + auto& m = members.emplace_back(member_seeds[4 + i], false, group_pk, std::nullopt); auto memb = admin1.members.get_or_construct(m.session_id); memb.set_invite_sent(); @@ -431,7 +399,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(m.keys.active_hashes() == std::unordered_set{{"keyhash5"s}}); } - std::pair> decrypted1, decrypted2; + std::pair> decrypted1, decrypted2; REQUIRE_NOTHROW(decrypted1 = members.back().keys.decrypt_message(compressed)); CHECK(decrypted1.first == admin1.session_id); CHECK(session::to_string(decrypted1.second) == msg); @@ -441,7 +409,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { CHECK(session::to_string(decrypted2.second) == msg); auto bad_compressed = compressed; - bad_compressed.back() ^= 0b100; + bad_compressed.back() ^= std::byte{0b100}; CHECK_THROWS_WITH( members.back().keys.decrypt_message(bad_compressed), "unable to decrypt ciphertext with any current group keys; tried 4"); @@ -450,7 +418,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { auto& m1b = members.emplace_back( member_seeds[1], false, - group_pk.data(), + group_pk, std::nullopt, members[1].info.dump(), members[1].members.dump(), @@ -469,7 +437,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { // get dropped as stale. info_configs.clear(); mem_configs.clear(); - std::vector new_keys_config6 = + std::vector new_keys_config6 = session::to_vector(admin1.keys.rekey(admin1.info, admin1.members)); auto [iseq6, ipush6, iobs6] = admin1.info.push(); info_configs.emplace_back("ifakehash6", ipush6[0]); @@ -499,7 +467,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { "keyhash6"s}}); } - std::vector new_keys_config7 = + std::vector new_keys_config7 = session::to_vector(admin1.keys.rekey(admin1.info, admin1.members)); // Make sure we can encrypt & decrypt even if the rekey is still pending: @@ -553,8 +521,8 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { pseudo_client admin1b{ admin1_seed, true, - group_pk.data(), - group_sk.data(), + group_pk, + group_sk, admin1.info.dump(), admin1.members.dump(), admin1.keys.dump()}; @@ -577,21 +545,21 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { } TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { - struct pseudo_client { - std::array secret_key; - const std::span public_key{secret_key.data() + 32, 32}; - std::string session_id{session_id_from_ed(public_key)}; + struct pseudo_client_c { + b64 secret_key; + std::span public_key{secret_key.data() + 32, 32}; + std::string session_id = oxenc::to_hex(ed25519::pk_to_session_id(public_key)); config_group_keys* keys; config_object* info; config_object* members; - pseudo_client( - std::vector seed, + pseudo_client_c( + std::span seed, bool is_admin, unsigned char* gpk, std::optional gsk) : - secret_key{sk_from_seed(seed)} { + secret_key{ed25519::keypair(seed).second} { int rv = groups_members_init(&members, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL); REQUIRE(rv == 0); @@ -600,7 +568,7 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { rv = groups_keys_init( &keys, - secret_key.data(), + to_unsigned(secret_key.data()), gpk, is_admin ? *gsk : NULL, info, @@ -614,44 +582,41 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { REQUIRE(groups_keys_rekey(keys, info, members, nullptr, nullptr)); } - ~pseudo_client() { + ~pseudo_client_c() { config_free(info); config_free(members); } }; - const std::vector group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const std::vector admin1_seed = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const std::vector admin2_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; - const std::array member_seeds = { - "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes, // member1 - "00011122435111155566677788811263446552465222efff0123456789abcdef"_hexbytes, // member2 - "00011129824754185548239498168169316979583253efff0123456789abcdef"_hexbytes, // member3 - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 + constexpr auto group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hex_b; + constexpr auto admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + constexpr auto admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hex_b; + constexpr std::array member_seeds = { + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hex_b, // member1 + "00011122435111155566677788811263446552465222efff0123456789abcdef"_hex_b, // member2 + "00011129824754185548239498168169316979583253efff0123456789abcdef"_hex_b, // member3 + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hex_b // member4 }; - std::array group_pk; - std::array group_sk; + auto [group_pk, group_sk] = ed25519::keypair(group_seed); - crypto_sign_ed25519_seed_keypair( - group_pk.data(), - group_sk.data(), - reinterpret_cast(group_seed.data())); REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - hacky_list admins; - hacky_list members; + hacky_list admins; + hacky_list members; // Initialize admin and member objects - admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); - admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); + auto* gpk = to_unsigned(group_pk.data()); + auto* gsk = to_unsigned(group_sk.data()); + admins.emplace_back(admin1_seed, true, gpk, gsk); + admins.emplace_back(admin2_seed, true, gpk, gsk); for (int i = 0; i < 4; ++i) - members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + members.emplace_back(member_seeds[i], false, gpk, std::nullopt); REQUIRE(admins[0].session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); @@ -851,25 +816,22 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { - const std::vector group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const std::vector admin_seed = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const std::vector member_seed = - "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + constexpr auto group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hex_b; + constexpr auto admin_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + constexpr auto member_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hex_b; - std::array group_pk; - std::array group_sk; - - crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + auto [group_pk, group_sk] = ed25519::keypair(group_seed); REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); CHECK(oxenc::to_hex(group_pk.begin(), group_pk.end()) == "c50cb3ae956947a8de19135b5be2685ff348afc63fc34a837aca12bc5c1f5625"); - pseudo_client admin{admin_seed, true, group_pk.data(), group_sk.data()}; - pseudo_client member{member_seed, false, group_pk.data(), std::nullopt}; + pseudo_client admin{admin_seed, true, group_pk, group_sk}; + pseudo_client member{member_seed, false, group_pk, std::nullopt}; session::config::UserGroups member_groups{member_seed, std::nullopt}; CHECK(admin.session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); @@ -893,7 +855,7 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") REQUIRE(push.size() == 1); - std::vector>> gr_conf; + std::vector>> gr_conf; gr_conf.emplace_back("fakehash1", push[0]); member_gr2.merge(gr_conf); @@ -919,16 +881,14 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") CHECK(oxenc::to_base64(subauth.subaccount_sig) == subauth_b64.subaccount_sig); CHECK(oxenc::to_base64(subauth.signature) == subauth_b64.signature); - CHECK(0 == - crypto_sign_ed25519_verify_detached( - reinterpret_cast(subauth.signature.data()), - to_sign.data(), - to_sign.size(), - reinterpret_cast(subauth.subaccount.substr(4).data()))); + CHECK(ed25519::verify( + to_span(subauth.signature).first<64>(), + to_span(subauth.subaccount.substr(4)).first<32>(), + to_sign)); CHECK(member.keys.swarm_verify_subaccount(auth_data)); CHECK(session::config::groups::Keys::swarm_verify_subaccount( - member.info.id, session::to_span(member.secret_key), auth_data)); + member.info.id, member.secret_key, auth_data)); // Try flipping a bit in each position of the auth data and make sure it fails to validate: for (size_t i = 0; i < auth_data.size(); i++) { @@ -938,33 +898,30 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") // sign bit, so won't actually change anything if it flips. continue; auto auth_data2 = auth_data; - auth_data2[i] ^= 1 << b; + auth_data2[i] ^= static_cast(1 << b); CHECK_FALSE(session::config::groups::Keys::swarm_verify_subaccount( - member.info.id, session::to_span(member.secret_key), auth_data2)); + member.info.id, member.secret_key, auth_data2)); } } } TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { - const std::vector group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; - const std::vector admin1_seed = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - const std::vector member1_seed = - "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes; - - std::array group_pk; - std::array group_sk; + constexpr auto group_seed = + "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hex_b; + constexpr auto admin1_seed = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + constexpr auto member1_seed = + "000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hex_b; - crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data()); + auto [group_pk, group_sk] = ed25519::keypair(group_seed); REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - pseudo_client admin{admin1_seed, true, group_pk.data(), group_sk.data()}; - pseudo_client member{member1_seed, false, group_pk.data(), std::nullopt}; + pseudo_client admin{admin1_seed, true, group_pk, group_sk}; + pseudo_client member{member1_seed, false, group_pk, std::nullopt}; - std::vector>> configs; + std::vector>> configs; { auto m = admin.members.get_or_construct(admin.session_id); m.admin = true; @@ -1018,7 +975,7 @@ TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") { REQUIRE(member.info.is_readonly()); REQUIRE(member.members.is_readonly()); - member.keys.load_admin_key(session::to_span(group_sk), member.info, member.members); + member.keys.load_admin_key(group_sk, member.info, member.members); CHECK(member.keys.admin()); CHECK_FALSE(member.members.is_readonly()); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 017abe75a..874850674 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -1,11 +1,11 @@ #include #include -#include #include #include #include #include +#include #include #include "utils.hpp" @@ -24,21 +24,18 @@ constexpr bool is_prime100(int i) { TEST_CASE("Group Members", "[config][groups][members]") { - const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - std::vector> enc_keys{ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hexbytes}; + std::vector> enc_keys{ + to_vector("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)}; - groups::Members gmem1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Members gmem1{ed_pk, ed_sk, std::nullopt}; // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. @@ -47,20 +44,20 @@ TEST_CASE("Group Members", "[config][groups][members]") { enc_keys.insert( enc_keys.begin(), - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); - enc_keys.push_back("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hexbytes); - enc_keys.push_back("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hexbytes); - groups::Members gmem2{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + to_vector("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); + enc_keys.push_back(to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); + enc_keys.push_back(to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); + groups::Members gmem2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. gmem2.add_key(k, false); std::vector sids; while (sids.size() < 256) { - std::array sid; + b33 sid; for (auto& s : sid) - s = sids.size(); - sid[0] = 0x05; + s = static_cast(sids.size()); + sid[0] = std::byte{0x05}; sids.push_back(oxenc::to_hex(sid.begin(), sid.end())); } @@ -71,7 +68,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.name = "Admin {}"_format(i); m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + to_vector("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b); m.profile_updated = std::chrono::sys_seconds{1s}; gmem1.set(m); } @@ -81,7 +78,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.set_name("Member {}"_format(i)); m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + to_vector("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b); m.profile_updated = session::to_sys_seconds(2); gmem1.set(m); } @@ -102,7 +99,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(gmem1.needs_dump()); CHECK_FALSE(gmem1.needs_push()); - std::vector>> merge_configs; + std::vector>> merge_configs; merge_configs.emplace_back("fakehash1", p1.at(0)); CHECK(gmem2.merge(merge_configs) == std::unordered_set{{"fakehash1"s}}); CHECK_FALSE(gmem2.needs_push()); @@ -205,7 +202,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { gmem2.confirm_pushed(s2, {"fakehash2"}); merge_configs.emplace_back("fakehash2", p2.at(0)); // not clearing it first! CHECK(gmem1.merge(merge_configs) == std::unordered_set{{"fakehash1"s}}); - gmem1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hexbytes); + gmem1.add_key("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b); CHECK(gmem1.merge(merge_configs) == std::unordered_set{{"fakehash1"s, "fakehash2"s}}); CHECK(gmem1.get(sids[23]).value().name == "Member 23"); @@ -218,9 +215,11 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : "{} {}"_format(i < 10 ? "Admin" : "Member", i))); - CHECK(m.profile_picture.key == - (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes - : ""_hexbytes)); + if (i < 20) + CHECK(std::ranges::equal(m.profile_picture.key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); + else + CHECK(m.profile_picture.key.empty()); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) CHECK(m.profile_updated.time_since_epoch() == 1s); @@ -302,9 +301,11 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(m.name == ((i == 20 || i == 21 || i >= 50) ? "" : "{} {}"_format(i < 10 ? "Admin" : "Member", i))); - CHECK(m.profile_picture.key == - (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes - : ""_hexbytes)); + if (i < 20) + CHECK(std::ranges::equal(m.profile_picture.key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); + else + CHECK(m.profile_picture.key.empty()); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) CHECK(m.profile_updated.time_since_epoch() == 1s); @@ -371,18 +372,15 @@ TEST_CASE("Group Members", "[config][groups][members]") { TEST_CASE("Group Members restores extra data", "[config][groups][members]") { - const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair( - ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + const auto seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == "cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"); CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - groups::Members gmem1{session::to_span(ed_pk), session::to_span(ed_sk), std::nullopt}; + groups::Members gmem1{ed_pk, ed_sk, std::nullopt}; auto memberId1 = "050000000000000000000000000000000000000000000000000000000000000000"; auto memberId2 = "051111111111111111111111111111111111111111111111111111111111111111"; @@ -401,7 +399,7 @@ TEST_CASE("Group Members restores extra data", "[config][groups][members]") { auto dumped = gmem1.dump(); - groups::Members gmem2{session::to_span(ed_pk), session::to_span(ed_sk), dumped}; + groups::Members gmem2{ed_pk, ed_sk, dumped}; CHECK(gmem2.get_status(gmem1.get_or_construct(memberId1)) == groups::member::Status::invite_sending); diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index d89786acf..a35be41d2 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -54,6 +54,125 @@ TEST_CASE("Hash generation", "[hash][hash]") { CHECK(to_hex(hash6) == expected_hash6); } +TEST_CASE("blake2b_hasher", "[hash][blake2b]") { + using session::b32; + using session::hash::blake2b_hasher; + using session::hash::nullkey; + + // The deprecated hash::hash calls libsodium directly (no blake2b_hasher involvement) and serves + // as the independent reference for the no-pers cases below. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + + // ── No-key, no-pers ────────────────────────────────────────────────────────────────────── + // KAT value cross-checks against deprecated hash::hash (independent libsodium path). + + { + auto out = blake2b_hasher<32>{}.update("TestMessage"_bytes).finalize(); + auto ref = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); + CHECK(std::ranges::equal(out, ref)); + CHECK(to_hex(out) == "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + } + + { + auto out = blake2b_hasher<64>{}.update("TestMessage"_bytes).finalize(); + auto ref = session::hash::hash(64, session::to_span("TestMessage"), std::nullopt); + CHECK(std::ranges::equal(out, ref)); + CHECK(to_hex(out) == + "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a3" + "9c02db69c4416d5c45acc2e9469b7f274992b2858f3bb2746becb48c8b56ce4b"); + } + + // ── Keyed, no-pers ─────────────────────────────────────────────────────────────────────── + + { + auto out = blake2b_hasher<32>{"TestKey"_bytes, std::nullopt} + .update("TestMessage"_bytes) + .finalize(); + auto ref = session::hash::hash( + 32, session::to_span("TestMessage"), session::to_span("TestKey")); + CHECK(std::ranges::equal(out, ref)); + CHECK(to_hex(out) == "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + } + + { + auto out = blake2b_hasher<64>{"TestKey"_bytes, std::nullopt} + .update("TestMessage"_bytes) + .finalize(); + auto ref = session::hash::hash( + 64, session::to_span("TestMessage"), session::to_span("TestKey")); + CHECK(std::ranges::equal(out, ref)); + CHECK(to_hex(out) == + "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e8778" + "74c9059edf53d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"); + } + +#pragma GCC diagnostic pop + + // ── Multi-update consistency ────────────────────────────────────────────────────────────── + // Splitting the input across calls must yield the same hash. + + { + auto single = blake2b_hasher<32>{}.update("TestMessage"_bytes).finalize(); + auto multi = blake2b_hasher<32>{} + .update("Test"_bytes) // split across two calls + .update("Message"_bytes) + .finalize(); + CHECK(single == multi); + + b32 out_write; + blake2b_hasher<32>{}.update("TestMes"_bytes, "sage"_bytes).finalize(out_write); + CHECK(single == out_write); + } + + // ── Return-value vs write-to-output finalize ────────────────────────────────────────────── + + { + b32 out_write; + blake2b_hasher<32>{}.update("TestMessage"_bytes).finalize(out_write); + auto out_rv = blake2b_hasher<32>{}.update("TestMessage"_bytes).finalize(); + CHECK(out_write == out_rv); + } + + // ── Personalisation string changes output ───────────────────────────────────────────────── + + constexpr auto pers = "TestPers1234567!"_b2b_pers; + + b32 no_pers_out, pers_out; + blake2b_hasher<32>{}.update("TestMessage"_bytes).finalize(no_pers_out); + blake2b_hasher<32>{nullkey, pers}.update("TestMessage"_bytes).finalize(pers_out); + CHECK(no_pers_out != pers_out); + + // Pers is deterministic: same config and input → same output. + b32 pers_out2; + blake2b_hasher<32>{nullkey, pers}.update("TestMessage"_bytes).finalize(pers_out2); + CHECK(pers_out == pers_out2); + + // Pers + multi-update consistency. + b32 pers_multi; + blake2b_hasher<32>{nullkey, pers}.update("Test"_bytes).update("Message"_bytes).finalize( + pers_multi); + CHECK(pers_out == pers_multi); + + // Different pers → different output. + constexpr auto pers2 = "OtherPers123456!"_b2b_pers; + b32 pers2_out; + blake2b_hasher<32>{nullkey, pers2}.update("TestMessage"_bytes).finalize(pers2_out); + CHECK(pers_out != pers2_out); + + // ── Key + pers ──────────────────────────────────────────────────────────────────────────── + + b32 key_pers_out; + blake2b_hasher<32>{"TestKey"_bytes, pers}.update("TestMessage"_bytes).finalize(key_pers_out); + // Distinct from keyed-only, pers-only, and no-key/no-pers outputs. + CHECK(key_pers_out != pers_out); + CHECK(key_pers_out != no_pers_out); + // Consistent across repeated construction. + b32 key_pers_out2; + blake2b_hasher<32>{"TestKey"_bytes, pers}.update("TestMessage"_bytes).finalize(key_pers_out2); + CHECK(key_pers_out == key_pers_out2); +} + TEST_CASE("SHA3-256 and SHAKE-256 known-answer tests", "[hash][sha3_256][shake256]") { // This test case serves two purposes: // 1. Verify SHA3-256 against NIST FIPS 202 known-answer test vectors. @@ -65,25 +184,26 @@ TEST_CASE("SHA3-256 and SHAKE-256 known-answer tests", "[hash][sha3_256][shake25 // https://csrc.nist.gov/csrc/media/projects/cryptographic-algorithm-validation-program/documents/sha3/sha-3bittestvectors.zip // SHAKE-256 KATs: NIST FIPS 202, Appendix A / CAVS test data + using session::b32; using session::hash::sha3_256; using session::hash::shake256; - std::array sha3_out, shake_out; + b32 sha3_out, shake_out; // --- SHA3-256 NIST vectors --- // Empty input - sha3_256(sha3_out, ""_uc); + sha3_256(sha3_out, ""_bytes); CHECK(oxenc::to_hex(sha3_out) == "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"); // "abc" (24 bits) - sha3_256(sha3_out, "abc"_uc); + sha3_256(sha3_out, "abc"_bytes); CHECK(oxenc::to_hex(sha3_out) == "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); // 448-bit message - sha3_256(sha3_out, "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"_uc); + sha3_256(sha3_out, "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"_bytes); CHECK(oxenc::to_hex(sha3_out) == "41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376"); @@ -91,24 +211,24 @@ TEST_CASE("SHA3-256 and SHAKE-256 known-answer tests", "[hash][sha3_256][shake25 sha3_256( sha3_out, "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklm" - "nopqklmnopqrlmnopqrsmnopqrstnopqrstu"_uc); + "nopqklmnopqrlmnopqrsmnopqrstnopqrstu"_bytes); CHECK(oxenc::to_hex(sha3_out) == "916f6061fe879741ca6469b43971dfdb28b1a32dc36cb3254e812be27aad1d18"); // --- SHAKE-256 NIST vectors (32-byte output) --- // Empty input; first 32 bytes from FIPS 202 Appendix B.2 sample output - shake256(""_uc)(shake_out); + shake256(""_bytes)(shake_out); CHECK(oxenc::to_hex(shake_out) == "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f"); // "abc" (24 bits) - shake256("abc"_uc)(shake_out); + shake256("abc"_bytes)(shake_out); CHECK(oxenc::to_hex(shake_out) == "483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739"); // --- Cross-check: same input must produce different output --- - sha3_256(sha3_out, "abc"_uc); - shake256("abc"_uc)(shake_out); + sha3_256(sha3_out, "abc"_bytes); + shake256("abc"_bytes)(shake_out); CHECK(sha3_out != shake_out); } diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index 15cbc420d..b162573c7 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -89,7 +89,7 @@ class TestHelper { // Returns the raw 32-byte seed for the account key identified by the given x25519 public key. static cleared_b32 account_key_seed( - core::Devices& d, std::span x25519_pub) { + core::Devices& d, std::span x25519_pub) { cleared_b32 seed; auto c = d.conn(); auto blob = c.prepared_get>>( @@ -148,7 +148,7 @@ class TestHelper { // Returns the pfs_key_cache entry for the given session_id, or nullopt if absent. static std::optional pfs_cache_entry( - core::Core& core, std::span session_id) { + core::Core& core, std::span session_id) { using X = sqlite::blob_guts>; using M = sqlite::blob_guts>; auto row = core.db.conn() diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 8507575da..fbbcf7db9 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -1,68 +1,63 @@ #include -#include -#include #include +#include +#include #include #include #include "utils.hpp" -using x_pair = std::pair, std::array>; - -// Returns X25519 privkey, pubkey from an Ed25519 seed -static x_pair to_x_keys(std::span ed_seed) { - std::array ed_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed_seed.data()); - x_pair ret; - auto& [x_priv, x_pub] = ret; - [[maybe_unused]] int rc = crypto_sign_ed25519_pk_to_curve25519(x_pub.data(), ed_pk.data()); - assert(rc == 0); - crypto_sign_ed25519_sk_to_curve25519(x_priv.data(), ed_sk.data()); - return ret; +using namespace session; + +using x_pair = std::pair; + +// Returns X25519 {privkey, pubkey} from an Ed25519 seed +x_pair to_x_keys(std::span ed_seed) { + auto [ed_pk, ed_sk] = ed25519::keypair(ed_seed); + return {ed25519::sk_to_x25519(ed_sk), ed25519::pk_to_x25519(ed_pk)}; } TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { const std::array seeds = { - "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes, - "0123456789abcdef000000000000000000000000000000000000000000000000"_hexbytes, - "0123456789abcdef111111111111111100000000000000000000000000000000"_hexbytes, - "0123456789abcdef222222222222222200000000000000000000000000000000"_hexbytes, - "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; + "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b, + "0123456789abcdef000000000000000000000000000000000000000000000000"_hex_b, + "0123456789abcdef111111111111111100000000000000000000000000000000"_hex_b, + "0123456789abcdef222222222222222200000000000000000000000000000000"_hex_b, + "0123456789abcdef333333333333333300000000000000000000000000000000"_hex_b}; std::array x_keys; for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); - CHECK(oxenc::to_hex(session::to_span(x_keys[0].second)) == + CHECK(oxenc::to_hex(x_keys[0].second) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(session::to_span(x_keys[1].second)) == + CHECK(oxenc::to_hex(x_keys[1].second) == "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); - CHECK(oxenc::to_hex(session::to_span(x_keys[2].second)) == + CHECK(oxenc::to_hex(x_keys[2].second) == "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); - CHECK(oxenc::to_hex(session::to_span(x_keys[3].second)) == + CHECK(oxenc::to_hex(x_keys[3].second) == "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); - CHECK(oxenc::to_hex(session::to_span(x_keys[4].second)) == + CHECK(oxenc::to_hex(x_keys[4].second) == "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); - auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; + auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hex_b; - std::vector> recipients; + std::vector> recipients; for (auto& [_, pubkey] : x_keys) recipients.emplace_back(pubkey.data(), pubkey.size()); std::vector msgs{{"hello", "cruel", "world"}}; - std::vector> encrypted; + std::vector> encrypted; session::encrypt_for_multiple( msgs[0], - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + x_keys[0].first, + x_keys[0].second, "test suite", - [&](std::span enc) { + [&](std::span enc) { encrypted.emplace_back(session::to_vector(enc)); }); @@ -72,39 +67,39 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { CHECK(to_hex(encrypted[2]) == "01c4fc2156327735f3fb5063b11ea95f6ebcc5b6cc"); auto m1 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[1].first), - session::to_span(x_keys[1].second), - session::to_span(x_keys[0].second), + x_keys[1].first, + x_keys[1].second, + x_keys[0].second, "test suite"); auto m2 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[2].first), - session::to_span(x_keys[2].second), - session::to_span(x_keys[0].second), + x_keys[2].first, + x_keys[2].second, + x_keys[0].second, "test suite"); auto m3 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "test suite"); auto m3b = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "not test suite"); auto m4 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[4].first), - session::to_span(x_keys[4].second), - session::to_span(x_keys[0].second), + x_keys[4].first, + x_keys[4].second, + x_keys[0].second, "test suite"); REQUIRE(m1); @@ -119,13 +114,13 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { encrypted.clear(); session::encrypt_for_multiple( - session::to_view_vector(msgs.begin(), msgs.end()), - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_view_vector(msgs.begin(), msgs.end()), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + x_keys[0].first, + x_keys[0].second, "test suite", - [&](std::span enc) { + [&](std::span enc) { encrypted.emplace_back(session::to_vector(enc)); }); @@ -135,39 +130,39 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { CHECK(to_hex(encrypted[2]) == "1ecee2215d226817edfdb097f05037eb799309103a"); m1 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[1].first), - session::to_span(x_keys[1].second), - session::to_span(x_keys[0].second), + x_keys[1].first, + x_keys[1].second, + x_keys[0].second, "test suite"); m2 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[2].first), - session::to_span(x_keys[2].second), - session::to_span(x_keys[0].second), + x_keys[2].first, + x_keys[2].second, + x_keys[0].second, "test suite"); m3 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "test suite"); m3b = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "not test suite"); m4 = session::decrypt_for_multiple( - session::to_view_vector(encrypted), + to_view_vector(encrypted), nonce, - session::to_span(x_keys[4].first), - session::to_span(x_keys[4].second), - session::to_span(x_keys[0].second), + x_keys[4].first, + x_keys[4].second, + x_keys[0].second, "test suite"); REQUIRE(m1); @@ -182,13 +177,13 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { // Mismatch messages & recipients size throws: CHECK_THROWS(session::encrypt_for_multiple( - session::to_view_vector(msgs.begin(), std::prev(msgs.end())), - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + to_view_vector(msgs.begin(), std::prev(msgs.end())), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), nonce, - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + x_keys[0].first, + x_keys[0].second, "test suite", - [&](std::span enc) { + [&](std::span enc) { encrypted.emplace_back(session::to_vector(enc)); })); } @@ -196,39 +191,39 @@ TEST_CASE("Multi-recipient encryption", "[encrypt][multi]") { TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][simple]") { const std::array seeds = { - "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes, - "0123456789abcdef000000000000000000000000000000000000000000000000"_hexbytes, - "0123456789abcdef111111111111111100000000000000000000000000000000"_hexbytes, - "0123456789abcdef222222222222222200000000000000000000000000000000"_hexbytes, - "0123456789abcdef333333333333333300000000000000000000000000000000"_hexbytes}; + "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b, + "0123456789abcdef000000000000000000000000000000000000000000000000"_hex_b, + "0123456789abcdef111111111111111100000000000000000000000000000000"_hex_b, + "0123456789abcdef222222222222222200000000000000000000000000000000"_hex_b, + "0123456789abcdef333333333333333300000000000000000000000000000000"_hex_b}; std::array x_keys; for (size_t i = 0; i < seeds.size(); i++) x_keys[i] = to_x_keys(seeds[i]); - CHECK(oxenc::to_hex(session::to_span(x_keys[0].second)) == + CHECK(oxenc::to_hex(x_keys[0].second) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - CHECK(oxenc::to_hex(session::to_span(x_keys[1].second)) == + CHECK(oxenc::to_hex(x_keys[1].second) == "d673a8fb4800d2a252d2fc4e3342a88cdfa9412853934e8993d12d593be13371"); - CHECK(oxenc::to_hex(session::to_span(x_keys[2].second)) == + CHECK(oxenc::to_hex(x_keys[2].second) == "afd9716ea69ab8c7f475e1b250c86a6539e260804faecf2a803e9281a4160738"); - CHECK(oxenc::to_hex(session::to_span(x_keys[3].second)) == + CHECK(oxenc::to_hex(x_keys[3].second) == "03be14feabd59122349614b88bdc90db1d1af4c230e9a73c898beec833d51f11"); - CHECK(oxenc::to_hex(session::to_span(x_keys[4].second)) == + CHECK(oxenc::to_hex(x_keys[4].second) == "27b5c1ea87cef76284c752fa6ee1b9186b1a95e74e8f5b88f8b47e5191ce6f08"); - auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hexbytes; + auto nonce = "32ab4bb45d6df5cc14e1c330fb1a8b68ea3826a8c2213a49"_hex_b; - std::vector> recipients; + std::vector> recipients; for (auto& [_, pubkey] : x_keys) recipients.emplace_back(pubkey.data(), pubkey.size()); std::vector msgs{{"hello", "cruel", "world"}}; - std::vector encrypted = session::encrypt_for_multiple_simple( + std::vector encrypted = encrypt_for_multiple_simple( msgs[0], - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + x_keys[0].first, + x_keys[0].second, "test suite"); REQUIRE(encrypted.size() == @@ -236,46 +231,46 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim /* 1:# 24:...nonce... */ 3 + 27 + /* 1:e le */ 3 + 2 + /* XX: then data with overhead */ 3 * - (3 + 5 + crypto_aead_xchacha20poly1305_ietf_ABYTES)); + (3 + 5 + encrypt::XCHACHA20_ABYTES)); // If we encrypt again the value should be different (because of the default randomized nonce): - CHECK(encrypted != session::encrypt_for_multiple_simple( + CHECK(encrypted != encrypt_for_multiple_simple( msgs[0], - session::to_view_vector( + to_view_vector( std::next(recipients.begin()), std::prev(recipients.end())), - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + x_keys[0].first, + x_keys[0].second, "test suite")); - auto m1 = session::decrypt_for_multiple_simple( + auto m1 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[1].first), - session::to_span(x_keys[1].second), - session::to_span(x_keys[0].second), + x_keys[1].first, + x_keys[1].second, + x_keys[0].second, "test suite"); - auto m2 = session::decrypt_for_multiple_simple( + auto m2 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[2].first), - session::to_span(x_keys[2].second), - session::to_span(x_keys[0].second), + x_keys[2].first, + x_keys[2].second, + x_keys[0].second, "test suite"); - auto m3 = session::decrypt_for_multiple_simple( + auto m3 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "test suite"); - auto m3b = session::decrypt_for_multiple_simple( + auto m3b = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "not test suite"); - auto m4 = session::decrypt_for_multiple_simple( + auto m4 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[4].first), - session::to_span(x_keys[4].second), - session::to_span(x_keys[0].second), + x_keys[4].first, + x_keys[4].second, + x_keys[0].second, "test suite"); REQUIRE(m1); @@ -288,11 +283,11 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim CHECK(session::to_string(*m2) == "hello"); CHECK(session::to_string(*m3) == "hello"); - encrypted = session::encrypt_for_multiple_simple( - session::to_view_vector(msgs), - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + encrypted = encrypt_for_multiple_simple( + to_view_vector(msgs), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + x_keys[0].first, + x_keys[0].second, "test suite", nonce); @@ -303,35 +298,35 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim "bcb642c49c6da03f70cdaab2ed6666721318afd631"_hex, "1ecee2215d226817edfdb097f05037eb799309103a"_hex)); - m1 = session::decrypt_for_multiple_simple( + m1 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[1].first), - session::to_span(x_keys[1].second), - session::to_span(x_keys[0].second), + x_keys[1].first, + x_keys[1].second, + x_keys[0].second, "test suite"); - m2 = session::decrypt_for_multiple_simple( + m2 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[2].first), - session::to_span(x_keys[2].second), - session::to_span(x_keys[0].second), + x_keys[2].first, + x_keys[2].second, + x_keys[0].second, "test suite"); - m3 = session::decrypt_for_multiple_simple( + m3 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "test suite"); - m3b = session::decrypt_for_multiple_simple( + m3b = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[3].first), - session::to_span(x_keys[3].second), - session::to_span(x_keys[0].second), + x_keys[3].first, + x_keys[3].second, + x_keys[0].second, "not test suite"); - m4 = session::decrypt_for_multiple_simple( + m4 = decrypt_for_multiple_simple( encrypted, - session::to_span(x_keys[4].first), - session::to_span(x_keys[4].second), - session::to_span(x_keys[0].second), + x_keys[4].first, + x_keys[4].second, + x_keys[0].second, "test suite"); REQUIRE(m1); @@ -344,10 +339,10 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim CHECK(session::to_string(*m2) == "cruel"); CHECK(session::to_string(*m3) == "world"); - CHECK_THROWS(session::encrypt_for_multiple_simple( - session::to_view_vector(msgs.begin(), std::prev(msgs.end())), - session::to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), - session::to_span(x_keys[0].first), - session::to_span(x_keys[0].second), + CHECK_THROWS(encrypt_for_multiple_simple( + to_view_vector(msgs.begin(), std::prev(msgs.end())), + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + x_keys[0].first, + x_keys[0].second, "test suite")); } diff --git a/tests/test_onion_request_router.cpp b/tests/test_onion_request_router.cpp index e48e31698..a4afbe0b5 100644 --- a/tests/test_onion_request_router.cpp +++ b/tests/test_onion_request_router.cpp @@ -5,8 +5,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -249,10 +249,10 @@ TEST_CASE("Network", "[network][onion_request_router][handle_errors]") { true, true, {{PathCategory::standard, 1}}}; - auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hexbytes; - auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hexbytes; + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; + auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hex_b; + auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hex_b; auto target = service_node{ ed25519_pubkey::from_bytes(ed_pk), oxen::quic::ipv4{"127.0.0.1"}, @@ -611,10 +611,10 @@ TEST_CASE("Network", "[network][onion_request_router][find_valid_path]") { true, false, {{PathCategory::standard, 1}}}; - auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hexbytes; - auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hexbytes; + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; + auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hex_b; + auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hex_b; auto target = service_node{ ed25519_pubkey::from_bytes(ed_pk), oxen::quic::ipv4{"127.0.0.1"}, @@ -721,7 +721,7 @@ TEST_CASE("Network", "[network][onion_request_router][check_request_queue_timeou true, false, {{PathCategory::standard, 1}}}; - auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; auto target = service_node{ ed25519_pubkey::from_bytes(ed_pk), oxen::quic::ipv4{"127.0.0.1"}, diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 79f3bfd78..6509267a0 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -10,28 +10,28 @@ using namespace session::network; TEST_CASE("Onion request encryption", "[encryption][onionreq]") { - auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hexbytes; - auto B = "caea52c5b0c316d85ffb53ea536826618b13dee40685f166f632653114526a78"_hexbytes; - auto b = "8fcd8ad3a15c76f76f1c56dff0c529999f8c59b4acda79e05666e54d5727dca1"_hexbytes; + auto A = "bbdfc83022d0aff084a6f0c529a93d1c4d728bf7e41199afed0e01ae70d20540"_hex_b; + auto B = "caea52c5b0c316d85ffb53ea536826618b13dee40685f166f632653114526a78"_hex_b; + auto b = "8fcd8ad3a15c76f76f1c56dff0c529999f8c59b4acda79e05666e54d5727dca1"_hex_b; auto enc_gcm = "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" - ""_hexbytes; + ""_hex_b; auto enc_gcm_broken1 = "1eb6ae1cd72f60999486365759bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b61" - ""_hexbytes; + ""_hex_b; auto enc_gcm_broken2 = "1eb6ae1cd72f60999486365749bd5dc15cc0b6a2a44d7d063daa5e93722f0c025fd00306403b69" - ""_hexbytes; + ""_hex_b; auto enc_xchacha20 = "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b032"_hexbytes; + "7d1638d765db75de02b032"_hex_b; auto enc_xchacha20_broken1 = "9e1a3abe60eff3ea5c23556cc7e225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b033"_hexbytes; + "7d1638d765db75de02b033"_hex_b; auto enc_xchacha20_broken2 = "9e1a3abe60eff3ea5c23556ccfe225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" - "7d1638d765db75de02b032"_hexbytes; + "7d1638d765db75de02b032"_hex_b; HopEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; @@ -46,21 +46,21 @@ TEST_CASE("Onion request encryption", "[encryption][onionreq]") { TEST_CASE("Onion request parser", "[onionreq][parser]") { - auto A = "8167e97672005c669a48858c69895f395ca235219ac3f7a4210022b1f910e652"_hexbytes; - auto a = "d2ee09e1a557a077d385fcb69a11ffb6909ecdcc8348def3e0e4172c8a1431c1"_hexbytes; - auto B = "8388de69bc0d4b6196133233ad9a46ba0473474bc67718aad96a3a33c257f726"_hexbytes; - auto b = "2f4d1c0d28e137777ec0a316e9f4f763e3e66662a6c51994c6315c9ef34b6deb"_hexbytes; + auto A = "8167e97672005c669a48858c69895f395ca235219ac3f7a4210022b1f910e652"_hex_b; + auto a = "d2ee09e1a557a077d385fcb69a11ffb6909ecdcc8348def3e0e4172c8a1431c1"_hex_b; + auto B = "8388de69bc0d4b6196133233ad9a46ba0473474bc67718aad96a3a33c257f726"_hex_b; + auto b = "2f4d1c0d28e137777ec0a316e9f4f763e3e66662a6c51994c6315c9ef34b6deb"_hex_b; auto enc_gcm = "270000009525d587d188c92a966eef0e7162bef99a6171a124575b998072a8ee7eb265e0b6f0930ed96504" "7b22656e635f74797065223a20226165732d67636d222c2022657068656d6572616c5f6b6579223a202238" "31363765393736373230303563363639613438383538633639383935663339356361323335323139616333" - "6637613432313030323262316639313065363532227d"_hexbytes; + "6637613432313030323262316639313065363532227d"_hex_b; auto enc_xchacha20 = "33000000e440bc244ddcafd947b86fc5a964aa58de54a6d75cc0f0f3840db14b6c1176a8e2e0a04d5fbdf9" "8f23adee1edc8362ab99b10b7b22656e635f74797065223a2022786368616368613230222c202265706865" "6d6572616c5f6b6579223a2022383136376539373637323030356336363961343838353863363938393566" - "33393563613233353231396163336637613432313030323262316639313065363532227d"_hexbytes; + "33393563613233353231396163336637613432313030323262316639313065363532227d"_hex_b; OnionReqParser parser_gcm{B, b, enc_gcm}; CHECK(to_string(parser_gcm.payload()) == "Hello world"); diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp index 11f3a69c3..ec8856b54 100644 --- a/tests/test_pfs_key_cache.cpp +++ b/tests/test_pfs_key_cache.cpp @@ -36,7 +36,7 @@ TEST_CASE("prefetch_pfs_keys throws without network", "[core][pfs]") { TempCore c; TempCore remote; auto session_id = remote->globals.session_id(); - std::array sid; + b33 sid; std::ranges::copy(session_id, sid.begin()); CHECK_THROWS_AS(c->prefetch_pfs_keys(sid), std::logic_error); } @@ -51,7 +51,7 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] auto remote_msg = remote->devices.build_account_pubkey_message(); auto session_id_span = remote->globals.session_id(); - std::array sid; + b33 sid; std::ranges::copy(session_id_span, sid.begin()); SECTION("Fetches and stores pubkeys when cache is absent") { @@ -119,7 +119,7 @@ TEST_CASE("prefetch_pfs_keys NAK handling", "[core][pfs]") { TempCore remote; auto session_id_span = remote->globals.session_id(); - std::array sid; + b33 sid; std::ranges::copy(session_id_span, sid.begin()); // Helper: fire the pending request with an empty-messages response (NAK condition). @@ -219,7 +219,7 @@ TEST_CASE("prefetch_pfs_keys handles malformed responses gracefully", "[core][pf TempCore remote; auto session_id_span = remote->globals.session_id(); - std::array sid; + b33 sid; std::ranges::copy(session_id_span, sid.begin()); SECTION("Garbage bt-dict data: NAK written, no valid pubkeys stored") { diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 23e250506..0a97ca84b 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -16,7 +16,7 @@ using namespace session; // slots are empty. The ns parameter is accepted for readability at call sites but is otherwise // unused. static nlohmann::json make_response( - int16_t /*ns*/, std::vector msg_data, std::string hash) { + int16_t /*ns*/, std::vector msg_data, std::string hash) { nlohmann::json msg_item; msg_item["data"] = oxenc::to_base64(msg_data); msg_item["hash"] = std::move(hash); @@ -44,7 +44,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { TempCore core{cbs}; auto mock_net = std::make_shared(); // Use a fixed non-zero pubkey for the node. - mock_net->current_node.remote_pubkey[0] = 0x01; + mock_net->current_node.remote_pubkey[0] = std::byte{0x01}; core->set_network(mock_net); @@ -84,9 +84,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { std::ranges::copy(std::as_bytes(seed_acc.seed()), seed_bytes.begin()); } TempCore linker{core::predefined_seed{std::span{seed_bytes}}}; - auto link_msg = linker->devices.build_link_request().message; - const auto* p = reinterpret_cast(link_msg.data()); - std::vector outer_msg{p, p + link_msg.size()}; + auto outer_msg = linker->devices.build_link_request().message; sent.callback(true, false, 200, {}, make_response(21, outer_msg, "hash1").dump()); @@ -112,8 +110,8 @@ TEST_CASE( // Two distinct service nodes with different pubkeys. network::service_node node_a, node_b; - node_a.remote_pubkey[0] = 0xAA; - node_b.remote_pubkey[0] = 0xBB; + node_a.remote_pubkey[0] = std::byte{0xAA}; + node_b.remote_pubkey[0] = std::byte{0xBB}; c->set_network(mock_net); @@ -129,7 +127,7 @@ TEST_CASE( } // Respond with hash "xyz" from node A. mock_net->sent_requests[0].callback( - true, false, 200, {}, make_response(21, {0x01}, "xyz").dump()); + true, false, 200, {}, make_response(21, {std::byte{0x01}}, "xyz").dump()); CHECK(TestHelper::namespace_last_hash(*c, 21, node_a.remote_pubkey) == "xyz"); CHECK_FALSE(TestHelper::namespace_last_hash(*c, 21, node_b.remote_pubkey).has_value()); @@ -156,7 +154,7 @@ TEST_CASE( } // Respond with hash "zyx" from node B (the message that B happens to have seen first). mock_net->sent_requests[0].callback( - true, false, 200, {}, make_response(21, {0x02}, "zyx").dump()); + true, false, 200, {}, make_response(21, {std::byte{0x02}}, "zyx").dump()); CHECK(TestHelper::namespace_last_hash(*c, 21, node_b.remote_pubkey) == "zyx"); // A's hash is untouched. CHECK(TestHelper::namespace_last_hash(*c, 21, node_a.remote_pubkey) == "xyz"); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 7a802b2ef..9c43dd5c6 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -133,12 +133,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { master_privkey.data, rotating_privkey.data, payment_tx.provider, - std::span( - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count), - std::span( - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count)); + to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count), + to_byte_span(payment_tx.order_id, payment_tx.order_id_count)); REQUIRE(std::memcmp( result.master_sig.data, @@ -456,7 +452,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } SECTION("session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse") { - std::array fake_gen_index_hash; + b32 fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); nlohmann::json j; @@ -561,7 +557,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { j["result"]["ticket"] = 123; j["result"]["items"] = nlohmann::json::array(); - std::array fake_gen_index_hash; + b32 fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); auto obj = nlohmann::json::object(); @@ -931,7 +927,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { crypto_sign_ed25519_keypair(rotating_pubkey.data, rotating_privkey.data); const auto DEV_BACKEND_PUBKEY = - "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"_hexbytes; + "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"_hex_b; // Setup CURL curl_global_init(CURL_GLOBAL_DEFAULT); diff --git a/tests/test_proto.cpp b/tests/test_proto.cpp index 1b5701a60..bb673c302 100644 --- a/tests/test_proto.cpp +++ b/tests/test_proto.cpp @@ -16,17 +16,11 @@ const std::vector groups{ Namespace::ConvoInfoVolatile, Namespace::UserGroups}; -const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; -std::array ed_pk_raw; -std::array ed_sk_raw; -static std::span load_seed() { - crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); - return {ed_sk_raw.data(), ed_sk_raw.size()}; -} -auto ed_sk = load_seed(); +const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; +auto [ed_pk, ed_sk] = ed25519::keypair(seed); TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { - auto msg = "Hello from the other side"_bytes; + auto msg = to_vector("Hello from the other side"_bytes); SECTION("Wrap/unwrap message types") { for (auto& n : groups) { @@ -60,7 +54,7 @@ TEST_CASE("Protobuf Handling - Wrap, Unwrap", "[config][proto][wrap]") { } TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { - auto msg = "Hello from the other side"_bytes; + auto msg = to_vector("Hello from the other side"_bytes); auto addendum = "jfeejj0ifdoesam"_bytes; const auto user_profile_msg = protos::wrap_config(ed_sk, msg, 1, Namespace::UserProfile); @@ -79,11 +73,8 @@ TEST_CASE("Protobuf Handling - Error Handling", "[config][proto][error]") { TEST_CASE("Protobuf old config loading test", "[config][proto][old]") { - const auto seed = "f887566576de6c16d9ec251d55e24c1400000000000000000000000000000000"_hexbytes; - std::array ed_pk_raw; - std::array ed_sk_raw; - crypto_sign_ed25519_seed_keypair(ed_pk_raw.data(), ed_sk_raw.data(), seed.data()); - std::span ed_sk{ed_sk_raw.data(), ed_sk_raw.size()}; + const auto seed = "f887566576de6c16d9ec251d55e24c1400000000000000000000000000000000"_hex_b; + auto [local_ed_pk, local_ed_sk] = ed25519::keypair(seed); auto old_conf = "080112c2060a03505554120f2f6170692f76312f6d6573736167651a9f060806120028e1c5a0beaf313801" @@ -105,7 +96,7 @@ TEST_CASE("Protobuf old config loading test", "[config][proto][old]") { "51bbd320ba901ff6110dad0c70442286cf6220a53c6f9693636a42d5523eeb1e5fb3453169581384fb8a8f" "3914fb6c01900a4f872f55742b117ddd7bd40c4c5911bb214e28eb9450dbdd0d831a93054c63f9a04bf50c" "db9aac0032c484062d7ba7bbe64e07bcd633eec8378d5d914732693c5e298f015ebde2ae45769ed319e267" - "f0528f5cc6da268343b6647b20bae6e9ee8d92cca702"_hexbytes; + "f0528f5cc6da268343b6647b20bae6e9ee8d92cca702"_hex_b; - CHECK_NOTHROW(protos::unwrap_config(ed_sk, old_conf, Namespace::UserProfile)); + CHECK_NOTHROW(protos::unwrap_config(local_ed_sk, old_conf, Namespace::UserProfile)); } diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index bbe4b524b..d631368bb 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,10 +1,10 @@ -#include #include -#include -#include #include #include +#include +#include +#include #include #include @@ -14,51 +14,46 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { using namespace session; - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); - REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); - REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); + REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - std::vector sid_raw; + auto sid = "05" + oxenc::to_hex(curve_pk); + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - REQUIRE(sid_raw == - "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); - - const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; - std::array ed_pk2, curve_pk2; - std::array ed_sk2; - crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); - REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + REQUIRE(oxenc::to_hex(sid_raw) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hex_b; + auto [ed_pk2, ed_sk2] = ed25519::keypair(seed2); + auto curve_pk2 = ed25519::pk_to_x25519(ed_pk2); + REQUIRE(oxenc::to_hex(ed_pk2) == "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); - REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + REQUIRE(oxenc::to_hex(curve_pk2) == "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + auto sid2 = "05" + oxenc::to_hex(curve_pk2); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - std::vector sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); - REQUIRE(sid_raw2 == - "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + REQUIRE(oxenc::to_hex(sid_raw2) == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); SECTION("full secret, prefixed sid") { - auto enc = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); + auto enc = encrypt_for_recipient(ed_sk, sid_raw2, to_span("hello")); CHECK(to_string(enc) != "hello"); - CHECK_THROWS(decrypt_incoming(to_span(ed_sk), enc)); + CHECK_THROWS(decrypt_incoming(ed_sk, enc)); - auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc); - CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + auto [msg, sender] = decrypt_incoming(ed_sk2, enc); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk)); CHECK(to_string(msg) == "hello"); auto broken = enc; - broken[2] ^= 0x02; - CHECK_THROWS(decrypt_incoming(to_span(ed_sk2), broken)); + broken[2] ^= std::byte{0x02}; + CHECK_THROWS(decrypt_incoming(ed_sk2, broken)); } SECTION("only seed, unprefixed sid") { constexpr auto lorem_ipsum = @@ -69,22 +64,18 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = - encrypt_for_recipient(to_span(ed_sk).first<32>(), sid_raw2, to_span(lorem_ipsum)); - CHECK(std::search( - enc.begin(), - enc.end(), - to_unsigned("dolore magna"), - to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); + encrypt_for_recipient(ed25519::extract_seed(ed_sk), sid_raw2, to_span(lorem_ipsum)); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); - CHECK_THROWS(decrypt_incoming(to_span(ed_sk), enc)); + CHECK_THROWS(decrypt_incoming(ed_sk, enc)); - auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc); - CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + auto [msg, sender] = decrypt_incoming(ed_sk2, enc); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk)); CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; - broken[14] ^= 0x80; - CHECK_THROWS(decrypt_incoming(to_span(ed_sk2), broken)); + broken[14] ^= std::byte{0x80}; + CHECK_THROWS(decrypt_incoming(ed_sk2, broken)); } } @@ -92,43 +83,38 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry using namespace session; - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); - REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); - REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); + REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - std::vector sid_raw; + auto sid = "05" + oxenc::to_hex(curve_pk); + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - REQUIRE(sid_raw == - "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); - - const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; - std::array ed_pk2, curve_pk2; - std::array ed_sk2; - crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); - REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + REQUIRE(oxenc::to_hex(sid_raw) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hex_b; + auto [ed_pk2, ed_sk2] = ed25519::keypair(seed2); + auto curve_pk2 = ed25519::pk_to_x25519(ed_pk2); + REQUIRE(oxenc::to_hex(ed_pk2) == "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); - REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + REQUIRE(oxenc::to_hex(curve_pk2) == "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + auto sid2 = "05" + oxenc::to_hex(curve_pk2); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - std::vector sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); - REQUIRE(sid_raw2 == - "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + REQUIRE(oxenc::to_hex(sid_raw2) == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - auto enc1 = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); - auto enc2 = encrypt_for_recipient(to_span(ed_sk), sid_raw2, to_span("hello")); + auto enc1 = encrypt_for_recipient(ed_sk, sid_raw2, to_span("hello")); + auto enc2 = encrypt_for_recipient(ed_sk, sid_raw2, to_span("hello")); REQUIRE(enc1 != enc2); - auto enc_det = encrypt_for_recipient_deterministic(to_span(ed_sk), sid_raw2, to_span("hello")); + auto enc_det = encrypt_for_recipient_deterministic(ed_sk, sid_raw2, to_span("hello")); CHECK(enc_det != enc1); CHECK(enc_det != enc2); CHECK(enc_det.size() == enc1.size()); @@ -138,13 +124,13 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry "6aa3b7b218bdc6dd7c1adccda8ef4897f0f458492240b39079c27a6c791067ab26a03067a7602b50f0434639" "906f93e548f909d5286edde365ebddc146"); - auto [msg, sender] = decrypt_incoming(to_span(ed_sk2), enc_det); - CHECK(to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); + auto [msg, sender] = decrypt_incoming(ed_sk2, enc_det); + CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk)); CHECK(to_string(msg) == "hello"); } -static std::array prefixed(unsigned char prefix, const session::uc32& pubkey) { - std::array result; +static session::b33 prefixed(std::byte prefix, const session::b32& pubkey) { + session::b33 result; result[0] = prefix; std::memcpy(result.data() + 1, pubkey.data(), 32); return result; @@ -154,62 +140,57 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e using namespace session; - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; constexpr auto server_pk = - "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hex_u; - std::array ed_pk, curve_pk; - std::array ed_sk; - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); - REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == - "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); - REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hex_b; + auto [ed_pk, ed_sk] = ed25519::keypair(seed); + auto curve_pk = ed25519::pk_to_x25519(ed_pk); + REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); - std::vector sid_raw; + auto sid = "05" + oxenc::to_hex(curve_pk); + std::vector sid_raw; oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); - REQUIRE(sid_raw == - "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); - auto [blind15_pk, blind15_sk] = blind15_key_pair(to_span(ed_sk), server_pk); - auto [blind25_pk, blind25_sk] = blind25_key_pair(to_span(ed_sk), server_pk); - auto blind15_pk_prefixed = prefixed(0x15, blind15_pk); - auto blind25_pk_prefixed = prefixed(0x25, blind25_pk); - - const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; - std::array ed_pk2, curve_pk2; - std::array ed_sk2; - crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); - REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + REQUIRE(oxenc::to_hex(sid_raw) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + auto [blind15_pk, blind15_sk] = blind15_key_pair(ed_sk, server_pk); + auto [blind25_pk, blind25_sk] = blind25_key_pair(ed_sk, server_pk); + auto blind15_pk_prefixed = prefixed(std::byte{0x15}, blind15_pk); + auto blind25_pk_prefixed = prefixed(std::byte{0x25}, blind25_pk); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hex_b; + auto [ed_pk2, ed_sk2] = ed25519::keypair(seed2); + auto curve_pk2 = ed25519::pk_to_x25519(ed_pk2); + REQUIRE(oxenc::to_hex(ed_pk2) == "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); - REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + REQUIRE(oxenc::to_hex(curve_pk2) == "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + auto sid2 = "05" + oxenc::to_hex(curve_pk2); REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); - std::vector sid_raw2; + std::vector sid_raw2; oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); - REQUIRE(sid_raw2 == - "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); - auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_span(ed_sk2), server_pk); - auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_span(ed_sk2), server_pk); - auto blind15_pk2_prefixed = prefixed(0x15, blind15_pk2); - auto blind25_pk2_prefixed = prefixed(0x25, blind25_pk2); + REQUIRE(oxenc::to_hex(sid_raw2) == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + auto [blind15_pk2, blind15_sk2] = blind15_key_pair(ed_sk2, server_pk); + auto [blind25_pk2, blind25_sk2] = blind25_key_pair(ed_sk2, server_pk); + auto blind15_pk2_prefixed = prefixed(std::byte{0x15}, blind15_pk2); + auto blind25_pk2_prefixed = prefixed(std::byte{0x25}, blind25_pk2); SECTION("blind15, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), server_pk, blind15_pk2_prefixed, to_span("hello")); + ed_sk, server_pk, blind15_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, enc); + ed_sk2, server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + broken[23] ^= std::byte{0x80}; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, broken)); + ed_sk2, server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, sender decrypt") { constexpr auto lorem_ipsum = @@ -220,15 +201,11 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); - CHECK(std::search( - enc.begin(), - enc.end(), - to_unsigned("dolore magna"), - to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); + ed25519::extract_seed(ed_sk), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk).first<32>(), + ed25519::extract_seed(ed_sk), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, @@ -237,9 +214,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; - broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + broken[463] ^= std::byte{0x80}; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk).first<32>(), + ed25519::extract_seed(ed_sk), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, @@ -254,15 +231,11 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); - CHECK(std::search( - enc.begin(), - enc.end(), - to_unsigned("dolore magna"), - to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); + ed25519::extract_seed(ed_sk), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2).first<32>(), + ed25519::extract_seed(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, @@ -271,9 +244,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; - broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + broken[463] ^= std::byte{0x80}; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2).first<32>(), + ed25519::extract_seed(ed_sk2), server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, @@ -281,33 +254,33 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e } SECTION("blind25, full secret, sender decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), server_pk, blind25_pk2_prefixed, to_span("hello")); + ed_sk, server_pk, blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); + ed_sk, server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + broken[23] ^= std::byte{0x80}; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); + ed_sk, server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); } SECTION("blind25, full secret, recipient decrypt") { auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk), server_pk, blind25_pk2_prefixed, to_span("hello")); + ed_sk, server_pk, blind25_pk2_prefixed, to_span("hello")); CHECK(to_string(enc) != "hello"); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); + ed_sk2, server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, enc); CHECK(sender == sid); CHECK(to_string(msg) == "hello"); auto broken = enc; - broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + broken[23] ^= std::byte{0x80}; // 1 + 5 + 16 = 22 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); + ed_sk2, server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); } SECTION("blind25, only seed, recipient decrypt") { constexpr auto lorem_ipsum = @@ -318,15 +291,11 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - to_span(ed_sk).first<32>(), server_pk, blind25_pk2_prefixed, to_span(lorem_ipsum)); - CHECK(std::search( - enc.begin(), - enc.end(), - to_unsigned("dolore magna"), - to_unsigned("dolore magna") + strlen("dolore magna")) == enc.end()); + ed25519::extract_seed(ed_sk), server_pk, blind25_pk2_prefixed, to_span(lorem_ipsum)); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - to_span(ed_sk2).first<32>(), + ed25519::extract_seed(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, @@ -335,9 +304,9 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e CHECK(to_string(msg) == lorem_ipsum); auto broken = enc; - broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + broken[463] ^= std::byte{0x80}; // 1 + 445 + 16 = 462 is the start of the nonce CHECK_THROWS(decrypt_from_blinded_recipient( - to_span(ed_sk2).first<32>(), + ed25519::extract_seed(ed_sk2), server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, @@ -351,10 +320,10 @@ TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { std::string_view name = "test"; auto ciphertext = "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" - "1580d9a8c9b8a64cacfec97"_hexbytes; + "1580d9a8c9b8a64cacfec97"_hex_b; auto ciphertext_legacy = - "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; - constexpr auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hex_u; + "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hex_b; + constexpr auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hex_b; CHECK(decrypt_ons_response(name, ciphertext, nonce) == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); @@ -369,10 +338,10 @@ TEST_CASE("Session ONS response decryption C API", "[session-ons][session_decryp auto name = "test\0"; auto ciphertext = "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" - "1580d9a8c9b8a64cacfec97"_hexbytes; + "1580d9a8c9b8a64cacfec97"_hex_u; auto ciphertext_legacy = - "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hexbytes; - auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hex_u; + auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hex_u; char ons1[67]; CHECK(session_decrypt_ons_response( @@ -390,12 +359,12 @@ TEST_CASE("Session push notification decryption", "[session-notification][decryp auto payload = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + "ab4aca5f0991e99eb0344ceeafa"_hex_b; auto payload_padded = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + "ab4aca5f0991e99eb0344ceeafa"_hex_b; constexpr auto enc_key = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_u; + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; CHECK(decrypt_push_notification(payload, enc_key) == to_vector("TestMessage")); CHECK(decrypt_push_notification(payload_padded, enc_key) == to_vector("TestMessage")); @@ -406,9 +375,9 @@ TEST_CASE("xchacha20", "[session][xchacha20]") { using namespace session; auto payload = - "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; + "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hex_b; constexpr auto enc_key = - "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_u; + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; CHECK(decrypt_xchacha20(payload, enc_key) == to_vector("TestMessage")); CHECK_THROWS(decrypt_xchacha20(to_span("invalid"), enc_key)); @@ -421,44 +390,35 @@ TEST_CASE("v2 PFS+PQ message encryption", "[session-protocol][encrypt][v2]") { using namespace session; // Sender: existing well-known test keypair 1 - const auto seed1 = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - std::array sender_ed_pk; - std::array sender_ed_sk; - crypto_sign_ed25519_seed_keypair(sender_ed_pk.data(), sender_ed_sk.data(), seed1.data()); + const auto seed1 = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + auto [sender_ed_pk, sender_ed_sk] = ed25519::keypair(seed1); // Recipient: long-term session identity from test keypair 2 - const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; - std::array recip_ed_pk, recip_curve_pk; - std::array recip_ed_sk; - crypto_sign_ed25519_seed_keypair(recip_ed_pk.data(), recip_ed_sk.data(), seed2.data()); - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(recip_curve_pk.data(), recip_ed_pk.data())); - - std::array recip_x25519_sec; - REQUIRE(0 == crypto_sign_ed25519_sk_to_curve25519(recip_x25519_sec.data(), recip_ed_sk.data())); + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hex_b; + auto [recip_ed_pk, recip_ed_sk] = ed25519::keypair(seed2); + auto recip_curve_pk = ed25519::pk_to_x25519(recip_ed_pk); + auto recip_x25519_sec = ed25519::sk_to_x25519(recip_ed_sk); - std::array recip_session_id; - recip_session_id[0] = 0x05; + b33 recip_session_id; + recip_session_id[0] = std::byte{0x05}; std::copy(recip_curve_pk.begin(), recip_curve_pk.end(), recip_session_id.begin() + 1); // Recipient PFS X25519 account key (deterministic) - const auto pfs_x25519_seed = - "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff"_hexbytes; - std::array pfs_x25519_sec, pfs_x25519_pub; - std::copy(pfs_x25519_seed.begin(), pfs_x25519_seed.end(), pfs_x25519_sec.begin()); - crypto_scalarmult_curve25519_base(pfs_x25519_pub.data(), pfs_x25519_sec.data()); + const auto pfs_x25519_sec = + "aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff"_hex_b; + auto pfs_x25519_pub = x25519::scalarmult_base(pfs_x25519_sec); // Recipient PFS ML-KEM-768 account key (deterministic, needs 64-byte seed) const auto pfs_mlkem_seed = "deadbeefcafebabe0123456789abcdef0123456789abcdef0123456789abcdef" - "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"_hexbytes; - std::array pfs_mlkem_pub; - std::array pfs_mlkem_sec; - REQUIRE(0 == sr_mlkem768_keypair_derand( - pfs_mlkem_pub.data(), pfs_mlkem_sec.data(), pfs_mlkem_seed.data())); + "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"_hex_b; + std::array pfs_mlkem_pub; + std::array pfs_mlkem_sec; + mlkem768::keygen(pfs_mlkem_pub, pfs_mlkem_sec, pfs_mlkem_seed); // Encrypt a message from sender to recipient auto ct = encrypt_for_recipient_v2( - to_span(sender_ed_sk), + sender_ed_sk, recip_session_id, pfs_x25519_pub, pfs_mlkem_pub, @@ -478,39 +438,36 @@ TEST_CASE("v2 PFS+PQ message encryption", "[session-protocol][encrypt][v2]") { auto result = decrypt_incoming_v2( recip_session_id, pfs_x25519_sec, pfs_x25519_pub, pfs_mlkem_sec, ct); CHECK(result.content == to_vector("hello world")); - CHECK(result.sender_session_id[0] == 0x05); + CHECK(result.sender_session_id[0] == std::byte{0x05}); CHECK(!result.pro_signature); // The recovered sender session ID matches the sender's X25519 pubkey - std::array sender_curve_pk; - REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(sender_curve_pk.data(), sender_ed_pk.data())); + auto sender_curve_pk = ed25519::pk_to_x25519(sender_ed_pk); CHECK(std::equal( result.sender_session_id.begin() + 1, result.sender_session_id.end(), sender_curve_pk.begin())); // Wrong X25519 key throws DecryptV2Error (wrong-key failure, not a format error) - auto wrong_x25519_sec = pfs_x25519_sec; - wrong_x25519_sec[0] ^= 0xff; - std::array wrong_x25519_pub; - crypto_scalarmult_curve25519_base(wrong_x25519_pub.data(), wrong_x25519_sec.data()); + b32 wrong_x25519_sec; + std::ranges::copy(pfs_x25519_sec, wrong_x25519_sec.begin()); + wrong_x25519_sec[0] ^= std::byte{0xff}; + auto wrong_x25519_pub = x25519::scalarmult_base(wrong_x25519_sec); CHECK_THROWS_AS( decrypt_incoming_v2( recip_session_id, wrong_x25519_sec, wrong_x25519_pub, pfs_mlkem_sec, ct), DecryptV2Error); // Truncated ciphertext throws before key matching (unrecoverable format error) - auto truncated = std::vector(ct.begin(), ct.begin() + 100); + auto truncated = std::vector(ct.begin(), ct.begin() + 100); CHECK_THROWS_AS( decrypt_incoming_v2_prefix(recip_x25519_sec, recip_curve_pk, truncated), std::runtime_error); // Encrypting and decrypting with a pro private key - uc32 pro_pk; - cleared_uc64 pro_sk; - crypto_sign_ed25519_keypair(pro_pk.data(), pro_sk.data()); + auto [pro_pk, pro_sk] = ed25519::keypair(); auto ct_pro = encrypt_for_recipient_v2( - to_span(sender_ed_sk), + sender_ed_sk, recip_session_id, pfs_x25519_pub, pfs_mlkem_pub, diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 9bf49c39e..4c84e448c 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -16,18 +16,18 @@ using namespace session; struct SerialisedProtobufContentWithProForTesting { ProProof proof; std::string plaintext; - std::vector plaintext_padded; - uc64 sig_over_plaintext_with_user_pro_key; - uc64 sig_over_plaintext_padded_with_user_pro_key; - uc32 pro_proof_hash; + std::vector plaintext_padded; + b64 sig_over_plaintext_with_user_pro_key; + b64 sig_over_plaintext_padded_with_user_pro_key; + b32 pro_proof_hash; cbytes64 sig_over_plaintext_with_user_pro_key_c; cbytes32 pro_proof_hash_c; }; static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_session_pro( std::string_view data_body, - const uc64& user_rotating_privkey, - const uc64& pro_backend_privkey, + const ed25519::PrivKeySpan& user_rotating_privkey, + const ed25519::PrivKeySpan& pro_backend_privkey, std::chrono::sys_seconds content_unix_ts, std::chrono::sys_seconds pro_expiry_unix_ts, session_protocol_pro_message_bitset msg_bitset, @@ -44,17 +44,12 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se data->set_body(std::string(data_body)); // Generate a dummy proof - crypto_sign_ed25519_sk_to_pk(result.proof.rotating_pubkey.data(), user_rotating_privkey.data()); + std::ranges::copy(user_rotating_privkey.pubkey(), result.proof.rotating_pubkey.begin()); result.proof.expiry_unix_ts = pro_expiry_unix_ts; // Sign the proof by the dummy "Session Pro Backend" key result.pro_proof_hash = result.proof.hash(); - crypto_sign_ed25519_detached( - result.proof.sig.data(), - nullptr, - result.pro_proof_hash.data(), - result.pro_proof_hash.size(), - pro_backend_privkey.data()); + result.proof.sig = ed25519::sign(pro_backend_privkey, result.pro_proof_hash); // Create protobuf `Content.proMessage` SessionProtos::ProMessage* pro = content.mutable_promessage(); @@ -80,19 +75,10 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se REQUIRE(result.plaintext_padded.size() % SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING == 0); // Sign the plaintext with the user's pro key - crypto_sign_ed25519_detached( - result.sig_over_plaintext_with_user_pro_key.data(), - nullptr, - reinterpret_cast(result.plaintext.data()), - result.plaintext.size(), - user_rotating_privkey.data()); - - crypto_sign_ed25519_detached( - result.sig_over_plaintext_padded_with_user_pro_key.data(), - nullptr, - result.plaintext_padded.data(), - result.plaintext_padded.size(), - user_rotating_privkey.data()); + result.sig_over_plaintext_with_user_pro_key = + ed25519::sign(user_rotating_privkey, to_span(result.plaintext)); + result.sig_over_plaintext_padded_with_user_pro_key = + ed25519::sign(user_rotating_privkey, result.plaintext_padded); // Setup the C versions for convenience std::memcpy( @@ -177,11 +163,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Generate the user's Session Pro rotating key for testing encrypted payloads with Session // Pro metadata const auto user_pro_seed = - "0123456789abcdef0123456789abcdeff00baa00000000000000000000000000"_hexbytes; - uc32 user_pro_ed_pk; - uc64 user_pro_ed_sk; - crypto_sign_ed25519_seed_keypair( - user_pro_ed_pk.data(), user_pro_ed_sk.data(), user_pro_seed.data()); + "0123456789abcdef0123456789abcdeff00baa00000000000000000000000000"_hex_b; + auto [user_pro_ed_pk, user_pro_ed_sk] = ed25519::keypair(user_pro_seed); SECTION("Encrypt with and w/o pro sig produce same payload size") { // Same payload size because the encrypt function should put in a dummy signature if one @@ -230,8 +213,6 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Setup a dummy "Session Pro Backend" key // We reuse test key 1 as the "Session Pro" backend key that signs the proofs as it // doesn't matter what key really, just that we have one available for signing. - const uc64& pro_backend_ed_sk = keys.ed_sk1; - const uc32& pro_backend_ed_pk = keys.ed_pk1; char error[256]; SECTION("Encrypt/decrypt for contact in default namespace w/o pro attached") { @@ -275,8 +256,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); INFO("ERROR: " << error); @@ -286,7 +267,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro ProProof nil_proof = {}; - uc32 nil_hash = nil_proof.hash(); + b32 nil_hash = nil_proof.hash(); cbytes32 decrypt_result_pro_hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(decrypt_result.pro.status == @@ -314,7 +295,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { build_protobuf_content_with_session_pro( /*data_body*/ data_body, /*user_rotating_privkey*/ user_pro_ed_sk, - /*pro_backend_privkey*/ pro_backend_ed_sk, + /*pro_backend_privkey*/ keys.ed_sk1, /*content_unix_ts=*/timestamp_s, /*pro_expiry_unix_ts*/ timestamp_s, /*msg_bitset*/ {}, @@ -335,7 +316,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Check non-encryptable messages produce only plaintext") { SECTION("Community inbox") { auto [blind15_pk, blind15_sk] = - session::blind15_key_pair(keys.ed_sk1, keys.ed_pk1, /*blind factor*/ nullptr); + session::blind15_key_pair(keys.ed_sk1, to_byte_span<32>(keys.ed_pk1.data()), /*blind factor*/ nullptr); cbytes33 blind15_recipient = {}; blind15_recipient.data[0] = 0x15; std::memcpy(blind15_recipient.data + 1, blind15_pk.data(), blind15_pk.size()); @@ -403,8 +384,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); REQUIRE(decrypt_result.success); @@ -447,7 +428,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { build_protobuf_content_with_session_pro( /*data_body*/ large_message, /*user_rotating_privkey*/ user_pro_ed_sk, - /*pro_backend_privkey*/ pro_backend_ed_sk, + /*pro_backend_privkey*/ keys.ed_sk1, /*content_unix_ts*/ timestamp_s, /*pro_expiry_unix_ts*/ timestamp_s, /*msg_bitset*/ pro_msg.bitset, @@ -477,8 +458,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); INFO("ERROR: " << error); @@ -535,11 +516,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encrypt/decrypt for groups v2 (w/ encrypted envelope, plaintext content) with Pro") { // TODO: Finish setting up a fake group const auto group_v2_seed = - "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; - uc64 group_v2_sk = {}; - uc32 group_v2_pk = {}; - crypto_sign_ed25519_seed_keypair( - group_v2_pk.data(), group_v2_sk.data(), group_v2_seed.data()); + "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hex_b; + auto [group_v2_pk, group_v2_sk] = ed25519::keypair(group_v2_seed); // Encrypt session_protocol_encoded_for_destination encrypt_result = {}; @@ -547,9 +525,9 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { cbytes33 group_v2_session_pk = {}; cbytes32 group_v2_session_sk = {}; group_v2_session_pk.data[0] = 0x03; - std::memcpy(group_v2_session_pk.data + 1, group_v2_pk.data(), group_v2_pk.size()); + std::memcpy(group_v2_session_pk.data + 1, to_unsigned(group_v2_pk.data()), group_v2_pk.size()); std::memcpy( - group_v2_session_sk.data, group_v2_sk.data(), sizeof(group_v2_session_sk.data)); + group_v2_session_sk.data, to_unsigned(group_v2_sk.data()), sizeof(group_v2_session_sk.data)); encrypt_result = session_protocol_encode_for_group( protobuf_content.plaintext.data(), @@ -569,9 +547,9 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { } // Decrypt envelope - span_u8 key = {group_v2_sk.data(), group_v2_sk.size()}; + span_u8 key = {to_unsigned(group_v2_sk.data()), group_v2_sk.size()}; session_protocol_decode_envelope_keys decrypt_keys = {}; - decrypt_keys.group_ed25519_pubkey = {group_v2_pk.data(), group_v2_pk.size()}; + decrypt_keys.group_ed25519_pubkey = {to_unsigned(group_v2_pk.data()), group_v2_pk.size()}; decrypt_keys.decrypt_keys = &key; decrypt_keys.decrypt_keys_len = 1; @@ -581,8 +559,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); INFO("Decrypt for group error: " << error); @@ -624,8 +602,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); REQUIRE(decrypt_result.error_len_incl_null_terminator == 0); @@ -664,7 +642,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { build_protobuf_content_with_session_pro( /*data_body*/ data_body, /*user_rotating_privkey*/ user_pro_ed_sk, - /*pro_backend_privkey*/ pro_backend_ed_sk, + /*pro_backend_privkey*/ keys.ed_sk1, /*content_unix_ts=*/ std::chrono::sys_seconds( std::chrono::duration_cast( @@ -691,8 +669,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &decrypt_keys, encrypt_bad_result.ciphertext.data, encrypt_bad_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); REQUIRE(decrypt_result.success); @@ -703,14 +681,14 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Try decrypt with a bad backend key { - uc32 bad_pro_backend_ed_pk = pro_backend_ed_pk; - bad_pro_backend_ed_pk[0] ^= 1; + uc32 bad_pro_ed_pk = keys.ed_pk1; + bad_pro_ed_pk[0] ^= 1; session_protocol_decoded_envelope decrypt_result = session_protocol_decode_envelope( &decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - bad_pro_backend_ed_pk.data(), - bad_pro_backend_ed_pk.size(), + bad_pro_ed_pk.data(), + bad_pro_ed_pk.size(), error, sizeof(error)); REQUIRE(decrypt_result.success); @@ -730,8 +708,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &bad_decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); INFO("Checking error from bad envelope decryption: " << std::string_view( @@ -752,8 +730,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { &multi_decrypt_keys, encrypt_result.ciphertext.data, encrypt_result.ciphertext.size, - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); REQUIRE(decrypt_result.success); @@ -779,8 +757,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { encoded.ciphertext.data, encoded.ciphertext.size, timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; @@ -802,8 +780,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { encoded.ciphertext.data, encoded.ciphertext.size, timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; @@ -823,8 +801,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { envelope_plaintext.data(), envelope_plaintext.size(), timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; @@ -847,8 +825,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { envelope_plaintext.data(), envelope_plaintext.size(), timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; @@ -859,18 +837,15 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encode/decode for community inbox (content message)") { const auto community_seed = - "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hexbytes; - uc64 community_sk = {}; - uc32 community_pk = {}; - crypto_sign_ed25519_seed_keypair( - community_pk.data(), community_sk.data(), community_seed.data()); + "0123456789abcdef0123456789abcdeff00baadeadb33f000000000000000000"_hex_b; + auto [community_pk, community_sk] = ed25519::keypair(community_seed); cbytes32 session_blind15_sk0 = {}; cbytes33 session_blind15_pk0 = {}; session_blind15_pk0.data[0] = 0x15; session_blind15_key_pair( keys.ed_sk0.data(), - community_pk.data(), + to_unsigned(community_pk.data()), session_blind15_pk0.data + 1, session_blind15_sk0.data); @@ -879,7 +854,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_blind15_pk1.data[0] = 0x15; session_blind15_key_pair( keys.ed_sk1.data(), - community_pk.data(), + to_unsigned(community_pk.data()), session_blind15_pk1.data + 1, session_blind15_sk1.data); @@ -905,16 +880,16 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { auto [decrypted_cipher, sender_id] = session::decrypt_from_blinded_recipient( keys.ed_sk1, community_pk, - session_blind15_pk0.data, - session_blind15_pk1.data, - {encoded.ciphertext.data, encoded.ciphertext.size}); + to_byte_span(session_blind15_pk0.data), + to_byte_span(session_blind15_pk1.data), + to_byte_span(encoded.ciphertext.data, encoded.ciphertext.size)); session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( decrypted_cipher.data(), decrypted_cipher.size(), timestamp_ms.time_since_epoch().count(), - pro_backend_ed_pk.data(), - pro_backend_ed_pk.size(), + keys.ed_pk1.data(), + keys.ed_pk1.size(), error, sizeof(error)); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; diff --git a/tests/test_snode_pool.cpp b/tests/test_snode_pool.cpp index 107233ecc..0aa1bc2ba 100644 --- a/tests/test_snode_pool.cpp +++ b/tests/test_snode_pool.cpp @@ -52,10 +52,10 @@ TEST_CASE("Network", "[network][get_unused_nodes]") { 0, 3, // cache_node_strike_threshold false}; - auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; - auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; - auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hexbytes; - auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hexbytes; + auto ed_pk = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hex_b; + auto ed_pk3 = "e17a692033200ae41350df9709754edde7343e2cf2f23e88f993319e0720e5e5"_hex_b; + auto ed_pk4 = "7b633fa6fb462b90db6f0f50384190ce7715e31b7aa93d87dbd7e94e33d4251f"_hex_b; std::vector snode_cache; std::vector unused_nodes; diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index e31f9f9cc..7440ab940 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -1,220 +1,163 @@ #include -#include -#include #include +#include "session/crypto/ed25519.hpp" #include "session/util.hpp" #include "session/xed25519.h" #include "session/xed25519.hpp" -constexpr std::array seed1{ - 0xfe, 0xcd, 0x9a, 0x60, 0x34, 0xbc, 0x9a, 0xba, 0x27, 0x39, 0x25, 0xde, 0xe7, - 0x06, 0x2b, 0x12, 0x33, 0x34, 0x58, 0x7c, 0x3c, 0x62, 0x57, 0x34, 0x1a, 0xfa, - 0xe2, 0xd7, 0xfe, 0x85, 0xe1, 0x22, 0xf4, 0xef, 0x87, 0x39, 0x08, 0xf6, 0xa5, - 0x37, 0x7b, 0xa3, 0x85, 0x3f, 0x0e, 0x2f, 0xa3, 0x26, 0xee, 0xd9, 0xe7, 0x41, - 0xed, 0xf9, 0xf7, 0xd0, 0x31, 0x1a, 0x3e, 0xcc, 0x66, 0xa5, 0x7b, 0x32}; -constexpr std::array seed2{ - 0x86, 0x59, 0xef, 0xdc, 0xbe, 0x09, 0x49, 0xe0, 0xf8, 0x11, 0x41, 0xe6, 0xd3, - 0x97, 0xe8, 0xbe, 0x75, 0xf4, 0x5d, 0x09, 0x26, 0x2f, 0x20, 0x9d, 0x59, 0x50, - 0xe9, 0x79, 0x89, 0xeb, 0x43, 0xc7, 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, - 0x45, 0x44, 0xc1, 0xc5, 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, - 0xe8, 0xaa, 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x81}; - -constexpr std::span pub1{seed1.data() + 32, 32}; -constexpr std::span pub2{seed2.data() + 32, 32}; - -constexpr std::array xpub1{ - 0xfe, 0x94, 0xb7, 0xad, 0x4b, 0x7f, 0x1c, 0xc1, 0xbb, 0x92, 0x67, - 0x1f, 0x1f, 0x0d, 0x24, 0x3f, 0x22, 0x6e, 0x11, 0x5b, 0x33, 0x77, - 0x04, 0x65, 0xe8, 0x2b, 0x50, 0x3f, 0xc3, 0xe9, 0x6e, 0x1f, -}; -constexpr std::array xpub2{ - 0x05, 0xc9, 0xa9, 0xbf, 0x17, 0x8f, 0xa6, 0x44, 0xd4, 0x4b, 0xeb, - 0xf6, 0x28, 0x71, 0x6d, 0xc7, 0xf2, 0xdf, 0x3d, 0x08, 0x42, 0xe9, - 0x78, 0x81, 0x96, 0x2c, 0x72, 0x36, 0x99, 0x15, 0x20, 0x73, -}; - -constexpr std::array pub2_abs{ - 0x35, 0x70, 0xb6, 0x9a, 0x47, 0xdc, 0x09, 0x45, 0x44, 0xc1, 0xc5, - 0x08, 0x9c, 0x40, 0x41, 0x4b, 0xbd, 0xa1, 0xff, 0xdd, 0xe8, 0xaa, - 0xb2, 0x61, 0x7f, 0xe9, 0x37, 0xee, 0x74, 0xa5, 0xee, 0x01, -}; - -template -static std::string view_hex(const std::array& x) { - return oxenc::to_hex(session::to_span(x)); -} +using namespace session; +using namespace session::literals; + +// Full 64-byte libsodium-style Ed25519 keys (32-byte seed || 32-byte pubkey) +constexpr auto seed1 = + "fecd9a6034bc9aba273925dee7062b123334587c3c6257341afae2d7fe85e122" + "f4ef873908f6a5377ba3853f0e2fa326eed9e741edf9f7d0311a3ecc66a57b32"_hex_b; +constexpr auto seed2 = + "8659efdcbe0949e0f81141e6d397e8be75f45d09262f209d5950e97989eb43c7" + "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_hex_b; + +// Ed25519 pubkeys (second half of the seed arrays) +constexpr auto pub1 = seed1.last<32>(); +constexpr auto pub2 = seed2.last<32>(); + +// Expected X25519 pubkeys derived from the Ed25519 pubkeys +constexpr auto xpub1 = + "fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; +constexpr auto xpub2 = + "05c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; + +// The "absolute" (positive) version of pub2's Ed25519 pubkey +constexpr auto pub2_abs = + "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee01"_hex_b; TEST_CASE("XEd25519 pubkey conversion", "[xed25519][pubkey]") { - std::array xpk1; - int rc = crypto_sign_ed25519_pk_to_curve25519(xpk1.data(), pub1.data()); - REQUIRE(rc == 0); - REQUIRE(view_hex(xpk1) == view_hex(xpub1)); + auto xpk1 = ed25519::pk_to_x25519(pub1); + REQUIRE(oxenc::to_hex(xpk1) == oxenc::to_hex(xpub1)); - std::array xpk2; - rc = crypto_sign_ed25519_pk_to_curve25519(xpk2.data(), pub2.data()); - REQUIRE(rc == 0); - REQUIRE(view_hex(xpk2) == view_hex(xpub2)); + auto xpk2 = ed25519::pk_to_x25519(pub2); + REQUIRE(oxenc::to_hex(xpk2) == oxenc::to_hex(xpub2)); - auto xed1 = session::xed25519::pubkey(xpub1); - REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); + auto xed1 = xed25519::pubkey(xpub1); + REQUIRE(oxenc::to_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(xpub2); - REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); + auto xed2 = xed25519::pubkey(xpub2); + REQUIRE(oxenc::to_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: - xed2[31] |= 0x80; - REQUIRE(view_hex(xed2) == oxenc::to_hex(pub2)); + xed2[31] |= std::byte{0x80}; + REQUIRE(oxenc::to_hex(xed2) == oxenc::to_hex(pub2)); } TEST_CASE("XEd25519 signing", "[xed25519][sign]") { - std::array xsk1; - int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); - REQUIRE(rc == 0); - std::array xpk1; - rc = crypto_sign_ed25519_pk_to_curve25519(xpk1.data(), pub1.data()); + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); + auto xsk2 = ed25519::sk_to_x25519(seed2.first<32>()); - std::array xsk2; - rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); - REQUIRE(rc == 0); - std::array xpk2; - rc = crypto_sign_ed25519_pk_to_curve25519(xpk2.data(), pub2.data()); + const auto msg = "hello world"_bytes; - const auto msg = session::to_span("hello world"); + auto xed_sig1 = xed25519::sign(xsk1, msg); - auto xed_sig1 = session::xed25519::sign(session::to_span(xsk1), msg); + REQUIRE(ed25519::verify(xed_sig1, pub1, msg)); - rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); - REQUIRE(rc == 0); - - auto xed_sig2 = session::xed25519::sign(session::to_span(xsk2), msg); + auto xed_sig2 = xed25519::sign(xsk2, msg); // This one will fail, because Xed signing always uses the positive but our actual pub2 is the // negative: - rc = crypto_sign_ed25519_verify_detached(xed_sig2.data(), msg.data(), msg.size(), pub2.data()); - REQUIRE(rc != 0); + REQUIRE_FALSE(ed25519::verify(xed_sig2, pub2, msg)); // Flip it, though, and it should work: - rc = crypto_sign_ed25519_verify_detached( - xed_sig2.data(), msg.data(), msg.size(), pub2_abs.data()); - REQUIRE(rc == 0); + REQUIRE(ed25519::verify(xed_sig2, pub2_abs, msg)); } TEST_CASE("XEd25519 verification", "[xed25519][verify]") { - std::array xsk1; - int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); - REQUIRE(rc == 0); - - std::array xsk2; - rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); - REQUIRE(rc == 0); + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); + auto xsk2 = ed25519::sk_to_x25519(seed2.first<32>()); - const auto msg = session::to_span("hello world"); + const auto msg = "hello world"_bytes; - auto xed_sig1 = session::xed25519::sign(session::to_span(xsk1), msg); - auto xed_sig2 = session::xed25519::sign(session::to_span(xsk2), msg); + auto xed_sig1 = xed25519::sign(xsk1, msg); + auto xed_sig2 = xed25519::sign(xsk2, msg); - REQUIRE(session::xed25519::verify(session::to_span(xed_sig1), session::to_span(xpub1), msg)); - REQUIRE(session::xed25519::verify(session::to_span(xed_sig2), session::to_span(xpub2), msg)); + REQUIRE(xed25519::verify(xed_sig1, xpub1, msg)); + REQUIRE(xed25519::verify(xed_sig2, xpub2, msg)); // Unlike regular Ed25519, XEd25519 uses randomness in the signature, so signing the same value // a second should give us a different signature: - auto xed_sig1b = session::xed25519::sign(session::to_span(xsk1), msg); - REQUIRE(view_hex(xed_sig1b) != view_hex(xed_sig1)); + auto xed_sig1b = xed25519::sign(xsk1, msg); + REQUIRE(oxenc::to_hex(xed_sig1b) != oxenc::to_hex(xed_sig1)); } TEST_CASE("XEd25519 pubkey conversion (C wrapper)", "[xed25519][pubkey][c]") { - auto xed1 = session::xed25519::pubkey(xpub1); - REQUIRE(view_hex(xed1) == oxenc::to_hex(pub1)); + auto xed1 = xed25519::pubkey(xpub1); + REQUIRE(oxenc::to_hex(xed1) == oxenc::to_hex(pub1)); // This one fails because the original Ed pubkey is negative - auto xed2 = session::xed25519::pubkey(xpub2); - REQUIRE(view_hex(xed2) != oxenc::to_hex(pub2)); + auto xed2 = xed25519::pubkey(xpub2); + REQUIRE(oxenc::to_hex(xed2) != oxenc::to_hex(pub2)); // After making the xed negative we should be okay: - xed2[31] |= 0x80; - REQUIRE(view_hex(xed2) == oxenc::to_hex(pub2)); + xed2[31] |= std::byte{0x80}; + REQUIRE(oxenc::to_hex(xed2) == oxenc::to_hex(pub2)); } -TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { - std::array xsk1; - int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); - REQUIRE(rc == 0); - std::array xpk1; - rc = crypto_sign_ed25519_pk_to_curve25519(xpk1.data(), pub1.data()); - - std::array xsk2; - rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); - REQUIRE(rc == 0); - std::array xpk2; - rc = crypto_sign_ed25519_pk_to_curve25519(xpk2.data(), pub2.data()); - - const auto msg = session::to_span("hello world"); - std::array xed_sig1, xed_sig2; - REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); - REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); - - rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); - REQUIRE(rc == 0); - - rc = crypto_sign_ed25519_verify_detached(xed_sig2.data(), msg.data(), msg.size(), pub2.data()); - REQUIRE(rc != 0); // Failure expected (pub2 is negative) - - rc = crypto_sign_ed25519_verify_detached( - xed_sig2.data(), msg.data(), msg.size(), pub2_abs.data()); - REQUIRE(rc == 0); // Flipped sign should work +TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); + auto xsk2 = ed25519::sk_to_x25519(seed2.first<32>()); + + const auto msg = "hello world"_bytes; + + b64 xed_sig1, xed_sig2; + REQUIRE(session_xed25519_sign( + to_unsigned(xed_sig1.data()), to_unsigned(xsk1.data()), to_unsigned(msg.data()), + msg.size())); + REQUIRE(session_xed25519_sign( + to_unsigned(xed_sig2.data()), to_unsigned(xsk2.data()), to_unsigned(msg.data()), + msg.size())); + + REQUIRE(ed25519::verify(xed_sig1, pub1, msg)); + REQUIRE_FALSE(ed25519::verify(xed_sig2, pub2, msg)); // Failure expected (pub2 is negative) + REQUIRE(ed25519::verify(xed_sig2, pub2_abs, msg)); // Flipped sign should work } + TEST_CASE("XEd25519 std::byte overloads", "[xed25519][byte]") { - std::array xsk1; - int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); - REQUIRE(rc == 0); + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); - const auto msg_uc = session::to_span("hello world"); - // Build std::byte versions of privkey, pubkey, and message. - auto xsk1_b = std::bit_cast>(xsk1); - auto xpub1_b = std::bit_cast>(xpub1); - std::array msg_b; - std::memcpy(msg_b.data(), msg_uc.data(), msg_b.size()); + const auto msg = "hello world"_bytes; // sign() byte overload should return a std::byte array. - auto sig_b = session::xed25519::sign( - std::span{xsk1_b}, std::span{msg_b}); + auto sig_b = xed25519::sign(std::span{xsk1}, msg); static_assert(std::same_as>); - // The signature must verify with the unsigned char overload. - auto sig_uc = std::bit_cast>(sig_b); - rc = crypto_sign_ed25519_verify_detached( - sig_uc.data(), msg_uc.data(), msg_uc.size(), pub1.data()); - REQUIRE(rc == 0); + // The signature must verify via the ed25519 helper. + REQUIRE(ed25519::verify(sig_b, pub1, msg)); // verify() byte overload. - REQUIRE(session::xed25519::verify( - std::span{sig_b}, - std::span{xpub1_b}, - std::span{msg_b})); + REQUIRE(xed25519::verify(sig_b, xpub1, msg)); // pubkey() byte overload should return a std::byte array. - auto ed_pk_b = session::xed25519::pubkey(std::span{xpub1_b}); + auto ed_pk_b = xed25519::pubkey(xpub1); static_assert(std::same_as>); - auto ed_pk_uc = std::bit_cast>(ed_pk_b); - REQUIRE(view_hex(ed_pk_uc) == oxenc::to_hex(pub1)); + REQUIRE(oxenc::to_hex(ed_pk_b) == oxenc::to_hex(pub1)); } TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { - std::array xsk1; - int rc = crypto_sign_ed25519_sk_to_curve25519(xsk1.data(), seed1.data()); - REQUIRE(rc == 0); - - std::array xsk2; - rc = crypto_sign_ed25519_sk_to_curve25519(xsk2.data(), seed2.data()); - REQUIRE(rc == 0); - - const auto msg = session::to_span("hello world"); - - std::array xed_sig1, xed_sig2; - REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); - REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); - - REQUIRE(session_xed25519_verify(xed_sig1.data(), xpub1.data(), msg.data(), msg.size())); - REQUIRE(session_xed25519_verify(xed_sig2.data(), xpub2.data(), msg.data(), msg.size())); + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); + auto xsk2 = ed25519::sk_to_x25519(seed2.first<32>()); + + const auto msg = "hello world"_bytes; + + b64 xed_sig1, xed_sig2; + REQUIRE(session_xed25519_sign( + to_unsigned(xed_sig1.data()), to_unsigned(xsk1.data()), to_unsigned(msg.data()), + msg.size())); + REQUIRE(session_xed25519_sign( + to_unsigned(xed_sig2.data()), to_unsigned(xsk2.data()), to_unsigned(msg.data()), + msg.size())); + + REQUIRE(session_xed25519_verify( + to_unsigned(xed_sig1.data()), to_unsigned(xpub1.data()), to_unsigned(msg.data()), + msg.size())); + REQUIRE(session_xed25519_verify( + to_unsigned(xed_sig2.data()), to_unsigned(xpub2.data()), to_unsigned(msg.data()), + msg.size())); } diff --git a/tests/utils.hpp b/tests/utils.hpp index 72515b0ad..4f692a150 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -38,6 +38,7 @@ struct ScopedClockOffset { using namespace std::literals; using namespace oxenc::literals; using namespace oxen::log::literals; +using namespace session; namespace session { @@ -148,17 +149,9 @@ class CallTracker { } // namespace session -inline std::vector operator""_bytes(const char* x, size_t n) { - auto begin = reinterpret_cast(x); - return {begin, begin + n}; -} -inline std::vector operator""_hexbytes(const char* x, size_t n) { - std::vector bytes; - oxenc::from_hex(x, x + n, std::back_inserter(bytes)); - return bytes; -} -inline std::string to_hex(std::vector bytes) { +template +inline std::string to_hex(const Container& bytes) { std::string hex; oxenc::to_hex(bytes.begin(), bytes.end(), std::back_inserter(hex)); return hex; @@ -190,16 +183,20 @@ inline int64_t get_timestamp_us() { .count(); } -inline std::string printable(std::span x) { +inline std::string printable(std::span x) { std::string p; - for (auto c : x) { + for (auto b : x) { + auto c = static_cast(b); if (c >= 0x20 && c <= 0x7e) - p += c; + p += static_cast(c); else p += "\\x" + oxenc::to_hex(&c, &c + 1); } return p; } +inline std::string printable(std::span x) { + return printable(session::as_span(x)); +} inline std::string printable(std::string_view x) { return printable(session::to_span(x)); } @@ -210,7 +207,7 @@ inline std::string printable(fmt::format_string format, T&&... args) { } std::string printable(const unsigned char* x) = delete; inline std::string printable(const unsigned char* x, size_t n) { - return printable({x, n}); + return printable(std::span{x, n}); } template @@ -244,7 +241,7 @@ static inline TestKeys get_deterministic_test_keys() { // Key 0 { // Seed - auto seed0 = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + auto seed0 = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; std::memcpy(result.seed0.data(), seed0.data(), seed0.size()); // Ed25519 @@ -262,7 +259,7 @@ static inline TestKeys get_deterministic_test_keys() { // Key 1 { // Seed - auto seed1 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + auto seed1 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hex_b; std::memcpy(result.seed1.data(), seed1.data(), seed1.size()); // Ed25519 From c9b239447eefcf0c7e7dca8839d4df506eae87b5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 21:24:46 -0300 Subject: [PATCH 078/193] Merge conflicts Update merged branch code with std::byte refactor modifications. --- external/session-router | 2 +- include/session/core.hpp | 2 +- include/session/crypto/ed25519.hpp | 4 +++ src/attachments.cpp | 11 ++++---- src/crypto/ed25519.cpp | 4 +++ src/multi_encrypt.cpp | 12 +++------ src/network/backends/quic_file_client.cpp | 8 +++--- src/network/routing/onion_request_router.cpp | 12 ++++----- src/network/transport/quic_transport.cpp | 11 +++++--- src/session_encrypt.cpp | 4 +-- src/session_protocol.cpp | 24 ++++++++--------- tests/live/test_file_transfer.cpp | 10 +++----- tests/test_dm_receive.cpp | 27 ++++++++++---------- tests/test_dm_send.cpp | 8 +++--- 14 files changed, 66 insertions(+), 73 deletions(-) diff --git a/external/session-router b/external/session-router index faa8c0e44..8d1df2efe 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit faa8c0e44d1e295e05935ebfc1401275a57dc982 +Subproject commit 8d1df2efe612c15eb15f50aab6f6c72fda326d25 diff --git a/include/session/core.hpp b/include/session/core.hpp index cb5e58776..3ac2d1e7d 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -375,7 +375,7 @@ class Core { std::span recipient_session_id, std::span content, sys_ms sent_timestamp, - const OptionalEd25519PrivKeySpan& pro_privkey = std::nullopt, + const ed25519::OptionalPrivKeySpan& pro_privkey = std::nullopt, std::chrono::milliseconds ttl = 14 * 24h, bool force_v2 = false); diff --git a/include/session/crypto/ed25519.hpp b/include/session/crypto/ed25519.hpp index 03fb00c7f..2fcdefb75 100644 --- a/include/session/crypto/ed25519.hpp +++ b/include/session/crypto/ed25519.hpp @@ -233,6 +233,10 @@ inline cleared_b32 sk_to_x25519(const T& sk) { return sk_to_x25519(sk.seed()); } +/// Derives the X25519 {secret, public} key pair from an Ed25519 private key. +/// Equivalent to `{sk_to_x25519(sk), pk_to_x25519(sk.pubkey())}` but as a single call. +std::pair x25519_keypair(const PrivKeySpan& sk); + /// Returns the private Ed25519 scalar `a` from a seed or PrivKeySpan (using cleared memory). /// /// Ed25519 and X25519 share the same private scalar: the Ed25519-to-X25519 conversion is diff --git a/src/attachments.cpp b/src/attachments.cpp index 65b6efe29..55b249a62 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -725,7 +725,7 @@ Encryptor::Encryptor(std::span seed, Domain domain) { const auto& pers = domain_pers(domain); crypto_generichash_blake2b_init_salt_personal( - &b2b_st(hash_st_data), uc(seed.data()), 32, nonce_key.size(), nullptr, pers.data()); + &b2b_st(hash_st_data), uc(seed.data()), 32, nonce_key.size(), nullptr, uc(pers.data())); } void Encryptor::update_key(std::span data) { @@ -760,12 +760,11 @@ cleared_b32 Encryptor::start_encryption( // Write 'S' prefix + header into out_buf; initialize secretstream out_buf[0] = std::byte{'S'}; - auto* header = uc(out_buf.data()) + 1; ss_st(ss_st_data) = secretstream_xchacha20poly1305_init_push_with_nonce( - std::span{header, ENCRYPT_HEADER}, - std::span{ - uc(nonce_key.data()) + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE}, - std::span{uc(nonce_key.data()), ENCRYPT_HEADER}); + std::span{out_buf.data() + 1, ENCRYPT_HEADER}, + std::span{ + nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE}, + std::span{nonce_key.data(), ENCRYPT_HEADER}); out_size = 1 + ENCRYPT_HEADER; plaintext_buf.reserve(ENCRYPT_CHUNK_SIZE); diff --git a/src/crypto/ed25519.cpp b/src/crypto/ed25519.cpp index 28c521ce5..ca13757b3 100644 --- a/src/crypto/ed25519.cpp +++ b/src/crypto/ed25519.cpp @@ -91,6 +91,10 @@ cleared_b32 sk_to_x25519(std::span seed) { return xsk; } +std::pair x25519_keypair(const PrivKeySpan& sk) { + return {sk_to_x25519(sk), pk_to_x25519(sk.pubkey())}; +} + void scalarmult_base(std::span out, std::span scalar) { if (0 != crypto_scalarmult_ed25519_base(to_unsigned(out.data()), to_unsigned(scalar.data()))) throw std::runtime_error{"crypto_scalarmult_ed25519_base failed"}; diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 8b9b32052..1cfd09e1f 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -66,14 +66,8 @@ namespace detail { return encrypt::xchacha20poly1305_decrypt(out, ciphertext, nonce, key); } - std::pair x_keys(const ed25519::PrivKeySpan& sk) { - return {ed25519::sk_to_x25519(sk), ed25519::pk_to_x25519(sk.pubkey())}; - } - } // namespace detail -namespace { - std::optional> decrypt_for_multiple( const std::vector>& ciphertexts, std::span nonce, @@ -153,7 +147,7 @@ std::vector encrypt_for_multiple_simple( std::optional> nonce, int pad) { - auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = ed25519::x25519_keypair(ed25519_secret_key); return encrypt_for_multiple_simple( messages, recipients, x_privkey, x_pubkey, domain, nonce, pad); @@ -194,7 +188,7 @@ std::optional> decrypt_for_multiple_simple( std::span sender_pubkey, std::string_view domain) { - auto [x_privkey, x_pubkey] = x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = ed25519::x25519_keypair(ed25519_secret_key); return decrypt_for_multiple_simple(encoded, x_privkey, x_pubkey, sender_pubkey, domain); } @@ -274,7 +268,7 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( try { auto [priv, pub] = - session::detail::x_keys(to_byte_span<64>(ed25519_secret_key)); + session::ed25519::x25519_keypair(to_byte_span<64>(ed25519_secret_key)); return session_encrypt_for_multiple_simple( out_len, messages, diff --git a/src/network/backends/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp index c5e2ed638..8076e92ac 100644 --- a/src/network/backends/quic_file_client.cpp +++ b/src/network/backends/quic_file_client.cpp @@ -16,7 +16,7 @@ #include #include "session/clock.hpp" -#include "session/ed25519.hpp" +#include "session/crypto/ed25519.hpp" using namespace oxen; using namespace std::literals; @@ -56,9 +56,9 @@ QuicFileClient::QuicFileClient( : std::nullopt)); // Set up TLS credentials - auto key_pair = ed25519::ed25519_key_pair(); - _creds = quic::GNUTLSCreds::make_from_ed_seckey(std::string_view{ - reinterpret_cast(key_pair.second.data()), key_pair.second.size()}); + auto [pk, sk] = ed25519::keypair(); + _creds = quic::GNUTLSCreds::make_from_ed_seckey( + std::string_view{reinterpret_cast(sk.data()), sk.size()}); // Enable 0RTT if callbacks are provided if (_ticket_store && _ticket_extract) { diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index 8043524bc..d4f8da49e 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -582,12 +582,10 @@ void OnionRequestRouter::upload_file(FileUploadRequest request, std::span all_data; + std::vector all_data; all_data.reserve(enc_size); - for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) { - auto* p = reinterpret_cast(chunk.data()); - all_data.insert(all_data.end(), p, p + chunk.size()); - } + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) + all_data.insert(all_data.end(), chunk.begin(), chunk.end()); // Build the one-shot Request via to_request (needs an UploadRequest with next_data) UploadRequest legacy_req; @@ -595,10 +593,10 @@ void OnionRequestRouter::upload_file(FileUploadRequest request, std::span>(std::move(all_data)); + auto data_ptr = std::make_shared>(std::move(all_data)); bool consumed = false; legacy_req.next_data = [data_ptr, - consumed]() mutable -> std::vector { + consumed]() mutable -> std::vector { if (consumed) return {}; consumed = true; diff --git a/src/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index f1a8e3907..d5e4948ba 100644 --- a/src/network/transport/quic_transport.cpp +++ b/src/network/transport/quic_transport.cpp @@ -112,7 +112,10 @@ void QuicTransport::verify_connectivity( // Only try to establish a connection if we are the first to ask for one if (_pending_requests.count(pubkey_hex) == 0 && _pending_verification_callbacks.at(pubkey_hex).size() == 1) - _establish_connection(node.to_quic_address(), request_id, category); + _establish_connection( + {node.remote_pubkey.view(), node.host(), node.omq_port}, + request_id, + category); }); } @@ -234,7 +237,7 @@ void QuicTransport::_send_request_internal(Request request, network_response_cal cat, "[Request {}]: Resolving service_node to RemoteAddress.", request_id); - remote = arg.to_quic_address(); + remote.emplace(arg.remote_pubkey.view(), arg.host(), arg.omq_port); } }, request.destination); @@ -294,8 +297,8 @@ void QuicTransport::_establish_connection( if (!_endpoint) throw std::runtime_error{"Network is invalid"}; - auto conn_key_pair = ed25519::keypair(); - auto creds = quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(conn_key_pair.second)); + auto [conn_pk, conn_sk] = ed25519::keypair(); + auto creds = quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(conn_sk)); // If we are starting a connection attempt then transition to the "connecting" state if (_status.load() == ConnectionStatus::unknown || diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 38f33e754..45f83698f 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1314,9 +1314,7 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess assert(session_id.size() == sizeof(result.session_id)); std::memcpy(result.session_id, session_id.data(), sizeof(result.session_id)); } catch (const std::exception& e) { - auto msg = std::string_view{e.what()}; - result.error_len_incl_null_terminator = - snprintf_clamped(error, error_len, "%.*s", (int)msg.size(), msg.data()) + 1; + result.error_len_incl_null_terminator = format_c_str(error, error_len, "{}", e.what()); } return result; } diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index f7b10e0ac..fa223db9c 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -1155,17 +1155,15 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( group_keys, group_pk, payload, pro_backend_pubkey_cpp.data); result.success = true; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = snprintf_clamped( - error, error_len, "%.*s", - static_cast(error_cpp.size()), error_cpp.data()) + 1; + result.error_len_incl_null_terminator = + format_c_str(error, error_len, "{}", e.what()); } } else if (keys->group_ed25519_pubkey.size) { - result.error_len_incl_null_terminator = - snprintf_clamped( - error, error_len, - "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: %zu", - keys->group_ed25519_pubkey.size) + 1; + result.error_len_incl_null_terminator = format_c_str( + error, + error_len, + "Invalid group_ed25519_pubkey: must be exactly 32 bytes, was: {}", + keys->group_ed25519_pubkey.size); return result; } else { // DM path: decrypt with Ed25519 private key(s) @@ -1178,16 +1176,14 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( result.success = true; break; } catch (const std::exception& e) { - std::string error_cpp = e.what(); - result.error_len_incl_null_terminator = snprintf_clamped( - error, error_len, "%.*s", - static_cast(error_cpp.size()), error_cpp.data()) + 1; + result.error_len_incl_null_terminator = + format_c_str(error, error_len, "{}", e.what()); } } if (keys->decrypt_keys_len == 0) { result.error_len_incl_null_terminator = - snprintf_clamped(error, error_len, "No ed25519 private keys were provided") + 1; + format_c_str(error, error_len, "No ed25519 private keys were provided"); } } diff --git a/tests/live/test_file_transfer.cpp b/tests/live/test_file_transfer.cpp index b80f1b5ca..9883f5c90 100644 --- a/tests/live/test_file_transfer.cpp +++ b/tests/live/test_file_transfer.cpp @@ -50,12 +50,11 @@ TEST_CASE("Live: file upload via QUIC", "[live][file]") { req.overall_timeout = LIVE_TIMEOUT; bool consumed = false; - req.next_data = [&]() -> std::vector { + req.next_data = [&]() -> std::vector { if (consumed) return {}; consumed = true; - return {reinterpret_cast(encrypted.data()), - reinterpret_cast(encrypted.data() + encrypted.size())}; + return {encrypted.begin(), encrypted.end()}; }; req.ttl = 1min; req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; @@ -148,12 +147,11 @@ TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") upload_req.overall_timeout = LIVE_TIMEOUT; bool consumed = false; - upload_req.next_data = [&]() -> std::vector { + upload_req.next_data = [&]() -> std::vector { if (consumed) return {}; consumed = true; - return {reinterpret_cast(encrypted.data()), - reinterpret_cast(encrypted.data() + encrypted.size())}; + return {encrypted.begin(), encrypted.end()}; }; upload_req.ttl = 1min; upload_req.on_complete = [&](auto result, bool) { diff --git a/tests/test_dm_receive.cpp b/tests/test_dm_receive.cpp index 00907b5af..cb48d79b3 100644 --- a/tests/test_dm_receive.cpp +++ b/tests/test_dm_receive.cpp @@ -20,7 +20,7 @@ namespace { // Fixed sender seed, shared across all test cases. constexpr auto SENDER_SEED = - "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_u; + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_b; struct SenderKeys { b32 ed_pk; @@ -241,10 +241,10 @@ TEST_CASE("_handle_direct_messages: v2 non-PFS fallback receive", "[core][dm]") TempCore recipient{cbs}; // Do NOT call active_account_keys() — sender has no PFS keys for this recipient. - std::array recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); - constexpr auto content = "cafebabe"_hex_u; + constexpr auto content = "cafebabe"_hex_b; auto ct = encrypt_for_recipient_v2_nopfs(sender.ed_sk, recip_session_id, content, std::nullopt); OwnedMessage om{std::span{ct}, "hash_nopfs", from_epoch_ms(3333), from_epoch_ms(7777)}; @@ -288,18 +288,17 @@ TEST_CASE( TempCore recipient{cbs}; recipient->devices.active_account_keys(); - std::array recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); auto seed_access = recipient->globals.account_seed(); auto x25519_sec = seed_access.x25519_key(); - std::span x25519_pub{recip_session_id.data() + 1, 32}; + std::span x25519_pub{recip_session_id.data() + 1, 32}; // The target ki is the first 2 bytes of the recipient's active ML-KEM public key. auto [x25519_bytes, mlkem_bytes] = TestHelper::active_account_pubkeys(*recipient); - std::array target_ki{ - static_cast(mlkem_bytes[0]), static_cast(mlkem_bytes[1])}; + std::array target_ki{mlkem_bytes[0], mlkem_bytes[1]}; - constexpr auto content = "deadc0de"_hex_u; + constexpr auto content = "deadc0de"_hex_b; auto ct = encrypt_for_recipient_v2_nopfs(sender.ed_sk, recip_session_id, content, std::nullopt); // Recover the current plaintext ki so we can XOR it out and XOR the target in. @@ -343,20 +342,20 @@ TEST_CASE( TempCore recipient{cbs}; - std::array recip_session_id; + b33 recip_session_id; std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); // Generate the first account key and record (prefix → pubkeys) as we rotate. recipient->devices.active_account_keys(); - using Prefix = std::array; + using Prefix = std::array; using PubkeyPair = std::pair, std::array>; std::map seen; // Record the current active key; returns the earlier key's pubkeys if its prefix collides. auto record_active = [&]() -> std::optional { auto [x, m] = TestHelper::active_account_pubkeys(*recipient); - Prefix pfx{static_cast(m[0]), static_cast(m[1])}; + Prefix pfx{m[0], m[1]}; if (auto it = seen.find(pfx); it != seen.end()) return it->second; seen.emplace(pfx, PubkeyPair{x, m}); @@ -379,12 +378,12 @@ TEST_CASE( // Encrypt with the earlier (now-rotated) key that shares the active key's ki prefix. // active_account_keys(ki) returns [active_key (wrong), rotated_target (right)], so Core // tries the wrong key first (DecryptV2Error), then succeeds with the right one. - constexpr auto content = "feedface"_hex_u; + constexpr auto content = "feedface"_hex_b; auto ct = encrypt_for_recipient_v2( sender.ed_sk, recip_session_id, - as_uc(target_pubkeys.first), - as_uc(target_pubkeys.second), + target_pubkeys.first, + target_pubkeys.second, content, std::nullopt); diff --git a/tests/test_dm_send.cpp b/tests/test_dm_send.cpp index c4e82c621..a200b4f13 100644 --- a/tests/test_dm_send.cpp +++ b/tests/test_dm_send.cpp @@ -22,7 +22,7 @@ std::array sid_bytes(Core& c) { } // Minimal valid SessionProtos::Content protobuf: field 15 (sigTimestamp) = 1. -constexpr auto MINIMAL_CONTENT = "7801"_hex_u; +constexpr auto MINIMAL_CONTENT = "7801"_hex_b; // A valid session ID for tests that don't need actual decryption on the other side. constexpr auto DUMMY_SID = @@ -75,7 +75,7 @@ TEST_CASE("send_dm: v2 PFS round-trip", "[core][send_dm]") { // Feed the captured payload into recipient to verify decryption. REQUIRE(!captured_payload.empty()); SwarmMessage sm; - sm.data = to_span(captured_payload); + sm.data = captured_payload; sm.hash = "send_test_hash"; sm.timestamp = clock_now_ms(); sm.expiry = clock_now_ms() + 24h; @@ -123,7 +123,7 @@ TEST_CASE("send_dm: v1 fallback on NAK", "[core][send_dm]") { REQUIRE(!captured_payload.empty()); SwarmMessage sm; - sm.data = to_span(captured_payload); + sm.data = captured_payload; sm.hash = "v1_hash"; sm.timestamp = clock_now_ms(); sm.expiry = clock_now_ms() + 24h; @@ -171,7 +171,7 @@ TEST_CASE("send_dm: v2 non-PFS with force_v2", "[core][send_dm]") { REQUIRE(!captured_payload.empty()); SwarmMessage sm; - sm.data = to_span(captured_payload); + sm.data = captured_payload; sm.hash = "nopfs_hash"; sm.timestamp = clock_now_ms(); sm.expiry = clock_now_ms() + 24h; From 6e6ddbd978cd9c906c41bde7bf0d6897be6356d1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 9 Apr 2026 16:37:47 -0300 Subject: [PATCH 079/193] Add generic fmt::formatter for std::byte spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add include/session/formattable.hpp with a concept-constrained fmt::formatter that works for any contiguous range of std::byte (std::span, std::array, std::vector, etc.). This enables lazy formatting of binary data in log calls — the encoding only happens if the log message is actually emitted, unlike the previous pattern of eagerly calling oxenc::to_hex() before the log call. The byte_spannable concept is restricted to std::byte element types only (not unsigned char) to avoid conflicts with built-in fmt formatters and to align with the ongoing unsigned char -> std::byte migration. Supported format specs: {} or {:x} — lowercase hex (default) {:z} — hex with leading zero bytes stripped {:a} — base32z {:b} — base64 (padded) {:B} — base64 (unpadded) {:r} — raw bytes {:W.Tx} — ellipsis truncation to W display chars with T trailing The header also re-exports oxen::log::literals _format/_format_to UDLs into session::literals for convenient use alongside the formatter. --- include/session/formattable.hpp | 211 ++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_formattable.cpp | 146 ++++++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 include/session/formattable.hpp create mode 100644 tests/test_formattable.cpp diff --git a/include/session/formattable.hpp b/include/session/formattable.hpp new file mode 100644 index 000000000..7783e32fa --- /dev/null +++ b/include/session/formattable.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace session { + +/// Concept matching contiguous ranges of std::byte. +template +concept byte_spannable = std::ranges::contiguous_range && + std::same_as>, std::byte>; + +/// User-defined literals for convenient fmt::format usage, re-exported from oxen::log::literals. +/// +/// "_format" works like fmt::format but with the format string as a UDL: +/// +/// "xyz {}"_format(42) // returns std::string "xyz 42" +/// +/// "_format_to" appends in-place to an existing string (more efficient than +=): +/// +/// std::string s = "hello"; +/// "xyz {}"_format_to(s, 42) // s is now "helloxyz 42" +/// +/// Available via `using namespace session::literals;` or `using namespace session;`. +inline namespace literals { + using oxen::log::literals::operator""_format; + using oxen::log::literals::operator""_format_to; +} // namespace literals + +} // namespace session + +namespace fmt { + +/// Generic formatter for any byte_spannable type (std::span, std::array, std::vector of std::byte). +/// +/// Format spec: +/// {} or {:x} — full lowercase hex (default) +/// {:z} — hex with leading zero bytes stripped +/// {:a} — base32z encoding +/// {:b} — base64 encoding (padded) +/// {:B} — base64 encoding (unpadded) +/// {:r} — raw bytes +/// +/// Ellipsis truncation: use {:W.T} before any mode letter, where W is the total output width +/// (including the single "…" character) and T is the number of characters shown after the +/// ellipsis. W must be >= 2 and >= T+2. If the encoded value fits within W characters, no +/// truncation occurs. +/// +/// For example, with a 32-byte all-zero value: +/// {:x} → "0000000000000000000000000000000000000000000000000000000000000000" +/// {:z} → "0" +/// {:10.4} → "00000…0000" +/// {:9.4x} → "0000…0000" +template +struct formatter { + private: + enum class mode_t { full_hex, stripped_hex, b32z, b64, b64_unpadded, raw }; + mode_t mode = mode_t::full_hex; + bool do_ellipsis = false; + int ellipsis_width = -1, ellipsis_tail = -1; + + public: + constexpr fmt::format_parse_context::iterator parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + for (; it != ctx.end(); ++it) { + char c = *it; + if (c == '}') + break; + + bool mode_set = false; + switch (c) { + case 'x': + mode = mode_t::full_hex; + mode_set = true; + break; + case 'z': + mode = mode_t::stripped_hex; + mode_set = true; + break; + case 'r': + mode = mode_t::raw; + mode_set = true; + break; + case 'a': + mode = mode_t::b32z; + mode_set = true; + break; + case 'b': + mode = mode_t::b64; + mode_set = true; + break; + case 'B': + mode = mode_t::b64_unpadded; + mode_set = true; + break; + case '0': + // Leading zero before any width digits means zero-fill, which we don't support + if (!do_ellipsis && ellipsis_width == -1) + throw fmt::format_error{ + "invalid format for byte span: 0-fill is not supported"}; + [[fallthrough]]; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + auto& v = do_ellipsis ? ellipsis_tail : ellipsis_width; + v = (v < 0 ? 0 : v) * 10 + (c - '0'); + break; + } + case '.': + if (!do_ellipsis && ellipsis_width >= 2) { + do_ellipsis = true; + break; + } + [[fallthrough]]; + default: throw fmt::format_error{"invalid format spec for byte span"}; + } + + if (mode_set) { + if (++it == ctx.end() || *it != '}') + throw fmt::format_error{ + "invalid format for byte span: trailing characters after mode"}; + break; + } + } + + if (do_ellipsis) { + if (ellipsis_tail < 0) + throw fmt::format_error{ + "invalid ellipsis format for byte span: missing tail length after '.'"}; + if (ellipsis_tail > ellipsis_width - 2) + throw fmt::format_error{ + "invalid ellipsis format for byte span: width must be >= tail+2"}; + } else if (ellipsis_width >= 0) { + throw fmt::format_error{ + "invalid format for byte span: width specified without '.' and tail length"}; + } + + return it; + } + + auto format(const T& v, fmt::format_context& ctx) const { + const auto* data = reinterpret_cast(std::ranges::data(v)); + std::span bytes{data, std::ranges::size(v)}; + + fmt::memory_buffer buf; + auto out = do_ellipsis ? fmt::appender(buf) : ctx.out(); + + switch (mode) { + case mode_t::raw: out = std::copy(bytes.begin(), bytes.end(), out); break; + case mode_t::b64: out = oxenc::to_base64(bytes.begin(), bytes.end(), out); break; + case mode_t::b64_unpadded: + out = oxenc::to_base64(bytes.begin(), bytes.end(), out, false); + break; + case mode_t::b32z: out = oxenc::to_base32z(bytes.begin(), bytes.end(), out); break; + case mode_t::stripped_hex: { + auto it = bytes.begin(); + while (it != bytes.end() && *it == 0) + ++it; + if (it == bytes.end()) { + *out++ = '0'; + break; + } + // If the first remaining byte would produce a leading 0 in hex (e.g. 0x0a → "0a"), + // skip the leading '0' so the output starts with the significant hex digit. + if (*it < 16) { + char pair[2]; + oxenc::to_hex(it, it + 1, pair); + *out++ = pair[1]; + ++it; + } + out = oxenc::to_hex(it, bytes.end(), out); + break; + } + case mode_t::full_hex: + default: out = oxenc::to_hex(bytes.begin(), bytes.end(), out); break; + } + + if (!do_ellipsis) + return out; + + std::string_view full{buf.data(), buf.size()}; + auto final_out = ctx.out(); + if (full.size() <= static_cast(ellipsis_width)) { + final_out = std::copy(full.begin(), full.end(), final_out); + } else { + final_out = std::copy( + full.begin(), full.begin() + (ellipsis_width - 1 - ellipsis_tail), final_out); + constexpr std::string_view ellipsis_char{"…"}; + final_out = std::copy(ellipsis_char.begin(), ellipsis_char.end(), final_out); + final_out = std::copy(full.end() - ellipsis_tail, full.end(), final_out); + } + return final_out; + } +}; + +} // namespace fmt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e8181390c..19535d8b5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ set(LIB_SESSION_UTESTS_SOURCES test_curve25519.cpp test_ed25519.cpp test_encrypt.cpp + test_formattable.cpp test_group_keys.cpp test_group_info.cpp test_group_members.cpp diff --git a/tests/test_formattable.cpp b/tests/test_formattable.cpp new file mode 100644 index 000000000..004598880 --- /dev/null +++ b/tests/test_formattable.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include +#include +#include + +#include "session/formattable.hpp" +#include "utils.hpp" + +TEST_CASE("byte span formatting - default hex", "[formattable]") { + CHECK(fmt::format("{}", "abcd0123"_hex_b) == "abcd0123"); + CHECK(fmt::format("{:x}", "abcd0123"_hex_b) == "abcd0123"); +} + +TEST_CASE("byte span formatting - various types", "[formattable]") { + auto arr = "deadbeef"_hex_b; + + SECTION("std::span with static extent") { + CHECK(fmt::format("{}", arr) == "deadbeef"); + } + + SECTION("std::span with dynamic extent") { + std::span sp{arr}; + CHECK(fmt::format("{}", sp) == "deadbeef"); + } + + SECTION("std::vector") { + std::vector vec{arr.begin(), arr.end()}; + CHECK(fmt::format("{}", vec) == "deadbeef"); + } + + SECTION("std::array") { + std::array a; + std::copy(arr.begin(), arr.end(), a.begin()); + CHECK(fmt::format("{}", a) == "deadbeef"); + } +} + +TEST_CASE("byte span formatting - empty span", "[formattable]") { + std::span empty; + CHECK(fmt::format("{}", empty) == ""); + CHECK(fmt::format("{:x}", empty) == ""); + CHECK(fmt::format("{:z}", empty) == "0"); + // Note: empty base64 is skipped due to an oxenc bug producing "=" for empty input + CHECK(fmt::format("{:a}", empty) == ""); + CHECK(fmt::format("{:r}", empty) == ""); +} + +TEST_CASE("byte span formatting - stripped hex", "[formattable]") { + SECTION("all zeros") { + CHECK(fmt::format("{:z}", "00000000"_hex_b) == "0"); + } + + SECTION("leading zeros stripped") { + CHECK(fmt::format("{:z}", "00001234"_hex_b) == "1234"); + } + + SECTION("leading zero nibble stripped") { + CHECK(fmt::format("{:z}", "000abc"_hex_b) == "abc"); + } + + SECTION("no leading zeros") { + CHECK(fmt::format("{:z}", "ff01"_hex_b) == "ff01"); + } + + SECTION("single non-zero byte with leading nibble zero") { + CHECK(fmt::format("{:z}", "0002"_hex_b) == "2"); + } +} + +TEST_CASE("byte span formatting - base32z", "[formattable]") { + auto val = "0001020304"_hex_b; + auto hex_result = fmt::format("{:x}", val); + auto b32z_result = fmt::format("{:a}", val); + CHECK(hex_result == "0001020304"); + CHECK(!b32z_result.empty()); + CHECK(b32z_result != hex_result); +} + +TEST_CASE("byte span formatting - base64", "[formattable]") { + CHECK(fmt::format("{:b}", "00010203"_hex_b) == "AAECAw=="); + CHECK(fmt::format("{:B}", "00010203"_hex_b) == "AAECAw"); +} + +TEST_CASE("byte span formatting - raw", "[formattable]") { + CHECK(fmt::format("{:r}", "6869"_hex_b) == "hi"); +} + +TEST_CASE("byte span formatting - ellipsis", "[formattable]") { + // 8 bytes = 16 hex chars: "0123456789abcdef" + auto val = "0123456789abcdef"_hex_b; + CHECK(fmt::format("{}", val) == "0123456789abcdef"); + + SECTION("truncation with tail") { + // 10 display chars: 7 leading + ellipsis + 2 trailing + CHECK(fmt::format("{:10.2}", val) == "0123456…ef"); + } + + SECTION("no truncation when value fits") { + CHECK(fmt::format("{:20.4}", val) == "0123456789abcdef"); + } + + SECTION("ellipsis with explicit mode") { + CHECK(fmt::format("{:10.2x}", val) == "0123456…ef"); + } + + SECTION("tail of zero") { + CHECK(fmt::format("{:5.0}", val) == "0123…"); + } + + SECTION("minimum ellipsis") { + CHECK(fmt::format("{:2.0}", val) == "0…"); + } +} + +TEST_CASE("byte span formatting - 32 byte key ellipsis", "[formattable]") { + auto key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_b; + auto full = fmt::format("{}", key); + CHECK(full.size() == 64); + + // Ellipsize to 12 display chars with 4-char tail: + // 7 leading + 3 bytes UTF-8 ellipsis + 4 trailing = 14 bytes + auto ellipsized = fmt::format("{:12.4}", key); + CHECK(ellipsized == "0102030…1f20"); + CHECK(ellipsized.size() == 7 + 3 + 4); +} + +TEST_CASE("byte span formatting - format errors", "[formattable]") { + auto val = "01"_hex_b; + + // Use fmt::runtime() to bypass compile-time format string checking + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:0}"), val), fmt::format_error); + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:5}"), val), fmt::format_error); + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:q}"), val), fmt::format_error); + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:xx}"), val), fmt::format_error); + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:3.3}"), val), fmt::format_error); + CHECK_THROWS_AS(fmt::format(fmt::runtime("{:1.0}"), val), fmt::format_error); +} + +TEST_CASE("byte span formatting - _format UDL", "[formattable]") { + using namespace session::literals; + auto val = "deadbeef"_hex_b; + CHECK("key: {}"_format(val) == "key: deadbeef"); + CHECK("key: {:z}"_format(val) == "key: deadbeef"); +} From 414b5ff1a08c6ed142a90360460ea9258bfe24a9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 Apr 2026 00:33:44 -0300 Subject: [PATCH 080/193] std::byte refactor: cleanup, formatting, and API improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-rebase cleanup and API improvements following the std::byte refactor: Crypto API improvements: - Add ed25519::x25519_keypair() for combined Ed25519→X25519 key pair derivation; remove detail::x_keys helper - Add write-to-output ed25519::sk_to_x25519(out, seed) overload - Make ed25519::sk_to_private variadic to forward to all sk_to_x25519 overloads - Move PrivKeySpan runtime-size constructor body to .cpp (removes fmt dependency from public header) - Add encryption::BOX_*/SECRETBOX_* named constants alongside existing XCHACHA20_* constants - Add hash::ARGON2_* named constants for pwhash parameters Namespace and naming: - Rename session::encrypt namespace to session::encryption - Rename decrypt_group_message parameter from decrypt_ed25519_privkey_list to group_enc_keys (these are symmetric keys, not Ed25519 keys) - Change group_enc_keys type to span> (fixed inner extent); simplify keys.cpp caller from singleton-per-key loop to single call with all keys Formatting and string handling: - Replace all std::to_string usage with fmt _format (locale-independent) - Replace "prefix" + to_hex() concatenation with "prefix{:x}"_format() using new byte span formatter - Rename formattable.hpp to format.hpp; re-export _format/_format_to into session::literals - Add fmt::range_format_kind disable for byte spans (prevents ambiguity with fmt/ranges.h) - Add Globals::session_id_hex() cached hex string - Replace lazy-convertible to_hex calls in log statements with direct byte span formatting - Delete unused/buggy SessionID struct and fields.cpp Code organization: - Move wrap_exceptions, unbox, copy_c_str from public base.hpp to internal config/internal.hpp - Move hash::update_all into detail namespace (no external callers) - Delete sodium_array (replaced by sodium_vector) and sodium_ptr (unused) - Remove cleared_uc32/cleared_uc64 aliases (no longer used) - Audit and fix PrivKeySpan constructions outside try blocks in C API wrappers (pro_backend.cpp) Raw libsodium elimination (non-wrapper files): - Replace crypto_sign_ed25519_* calls with ed25519:: wrappers in user_groups, globals, pro, key_types, blinding, multi_encrypt - Replace crypto_scalarmult/crypto_box calls with x25519::/encryption:: wrappers - Replace all raw libsodium constants with named wrapper constants - Remove vestigial sodium includes from protos.cpp, keys.cpp, user_profile.cpp, session_encrypt.cpp, and others Fixed-extent span conversions: - Config key APIs (add_key, remove_key, replace_keys, get_keys, has_key, key(), group_keys) now use span - Remove runtime key size checks that are now enforced at compile time Minor fixes: - Fix URVO-breaking patterns (unnecessary local + return) --- include/session/config/base.hpp | 68 +------ include/session/config/groups/keys.hpp | 6 +- include/session/core.hpp | 2 +- include/session/core/globals.hpp | 2 + include/session/crypto/ed25519.hpp | 33 ++- include/session/encrypt.hpp | 48 +++-- include/session/fields.hpp | 12 -- .../session/{formattable.hpp => format.hpp} | 6 + include/session/hash.hpp | 33 ++- include/session/session_encrypt.hpp | 2 +- include/session/session_protocol.hpp | 2 +- include/session/sodium_array.hpp | 180 ----------------- src/CMakeLists.txt | 1 - src/blinding.cpp | 15 +- src/config/base.cpp | 45 ++--- src/config/community.cpp | 25 +-- src/config/contacts.cpp | 4 +- src/config/convo_info_volatile.cpp | 3 +- src/config/groups/info.cpp | 2 +- src/config/groups/keys.cpp | 124 +++++------- src/config/internal.cpp | 4 +- src/config/internal.hpp | 42 ++++ src/config/pro.cpp | 13 +- src/config/protos.cpp | 3 - src/config/user_groups.cpp | 27 +-- src/core.cpp | 31 ++- src/core/devices.cpp | 44 ++-- src/core/globals.cpp | 27 +-- src/crypto/ed25519.cpp | 18 +- src/fields.cpp | 17 -- src/internal-util.hpp | 3 + src/multi_encrypt.cpp | 32 ++- src/network/backends/session_file_server.cpp | 17 +- src/network/key_types.cpp | 26 +-- src/onionreq/builder.cpp | 2 +- src/onionreq/response_parser.cpp | 4 +- src/pro_backend.cpp | 32 +-- src/session_encrypt.cpp | 190 ++++++++---------- src/session_protocol.cpp | 38 ++-- src/util.cpp | 3 +- tests/CMakeLists.txt | 2 +- tests/test_config_userprofile.cpp | 1 + .../{test_formattable.cpp => test_format.cpp} | 24 +-- tests/test_group_info.cpp | 18 +- tests/test_group_keys.cpp | 2 +- tests/test_group_members.cpp | 4 +- tests/test_multi_encrypt.cpp | 2 +- tests/test_session_protocol.cpp | 2 +- 48 files changed, 463 insertions(+), 778 deletions(-) rename include/session/{formattable.hpp => format.hpp} (95%) delete mode 100644 src/fields.cpp rename tests/{test_formattable.cpp => test_format.cpp} (85%) diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index cb1a29335..dc18673da 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -57,7 +57,7 @@ enum class ConfigState : int { }; using Ed25519PubKey = b32; -using Ed25519Secret = sodium_array; +using Ed25519Secret = sodium_vector; // Helper base class for holding a config signing keypair class ConfigSig { @@ -1372,7 +1372,7 @@ class ConfigBase : public ConfigSig { /// push) if the first key (i.e. the key used for encryption) is changed as a result of this /// call. Ignored if the config is not modifiable. void add_key( - std::span key, + std::span key, bool high_priority = true, bool dirty_config = false); @@ -1406,7 +1406,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if found and removed - bool remove_key(std::span key, size_t from = 0, bool dirty_config = false); + bool remove_key(std::span key, size_t from = 0, bool dirty_config = false); /// API: base/ConfigBase::replace_keys /// @@ -1420,7 +1420,7 @@ class ConfigBase : public ConfigSig { /// requiring a repush) if the old and new first key are not the same. Ignored if the config /// is not modifiable. void replace_keys( - const std::vector>& new_keys, bool dirty_config = false); + const std::vector>& new_keys, bool dirty_config = false); /// API: base/ConfigBase::get_keys /// @@ -1435,7 +1435,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `std::vector>` -- Returns vector of encryption keys - std::vector> get_keys() const; + std::vector> get_keys() const; /// API: base/ConfigBase::key_count /// @@ -1456,7 +1456,7 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `bool` -- Returns true if it does exist - bool has_key(std::span key) const; + bool has_key(std::span key) const; /// API: base/ConfigBase::key /// @@ -1469,9 +1469,9 @@ class ConfigBase : public ConfigSig { /// /// Outputs: /// - `std::span` -- binary data of the key - std::span key(size_t i = 0) const { + std::span key(size_t i = 0) const { assert(i < _keys.size()); - return {_keys[i].data(), _keys[i].size()}; + return _keys[i]; } }; @@ -1508,58 +1508,6 @@ struct internals final { const ConfigT& operator*() const { return *operator->(); } }; -template , int> = 0> -inline internals& unbox(config_object* conf) { - return *static_cast*>(conf->internals); -} -template , int> = 0> -inline const internals& unbox(const config_object* conf) { - return *static_cast*>(conf->internals); -} - -template -void copy_c_str(char (&dest)[N], std::string_view src) { - if (src.size() >= N) - src.remove_suffix(src.size() - N - 1); - std::memcpy(dest, src.data(), src.size()); - dest[src.size()] = 0; -} - -// Wraps a labmda and, if an exception is thrown, sets an error message in the internals.error -// string and updates the last_error pointer in the outer (C) config_object struct to point at it. -// -// No return value: accepts void and pointer returns; pointer returns will become nullptr on error -template -decltype(auto) wrap_exceptions(config_object* conf, Call&& f) { - using Ret = std::invoke_result_t; - - try { - conf->last_error = nullptr; - return std::invoke(std::forward(f)); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - if constexpr (std::is_pointer_v) - return static_cast(nullptr); - else - static_assert(std::is_void_v, "Don't know how to return an error value!"); -} - -// Same as above but accepts callbacks with value returns on errors: returns `f()` on success, -// `error_return` on exception -template -Ret wrap_exceptions(config_object* conf, Call&& f, Ret error_return) { - try { - conf->last_error = nullptr; - return std::invoke(std::forward(f)); - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return error_return; -} - // Internal helper: attempts zstd compression of `msg` in-place (with a 'z' prefix byte); leaves // `msg` unchanged if compression does not reduce the size. `level` of 0 disables compression. void compress_message(std::vector& msg, int level); diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index e3d36707d..786d5d759 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -232,7 +232,7 @@ class Keys : public ConfigSig { /// /// Outputs: /// - `std::vector>` - vector of encryption keys. - std::vector> group_keys() const; + std::vector> group_keys() const; /// API: groups/Keys::size /// @@ -268,7 +268,7 @@ class Keys : public ConfigSig { /// /// Outputs: /// - `true` if this object knows the group's master key - bool admin() const { return _sign_sk && _sign_pk; } + bool admin() const { return !_sign_sk.empty() && _sign_pk; } /// API: groups/Keys::load_admin_key /// @@ -543,7 +543,7 @@ class Keys : public ConfigSig { /// `rekey()` call. /// This is set to a new key when `rekey()` is called, and is cleared when any config message /// is successfully loaded by `load_key`. - std::optional> pending_key() const; + std::optional> pending_key() const; /// API: groups/Keys::load_key /// diff --git a/include/session/core.hpp b/include/session/core.hpp index 3ac2d1e7d..e4c8a9a19 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -60,7 +60,7 @@ /// password. /// /// (Note that the above makes a secure copy when opening the database. If you cannot avoid making -/// a copy yourself, remember to copy into at least a `session::cleared_uc32` or use similar secure +/// a copy yourself, remember to copy into at least a `session::cleared_b32` or use similar secure /// clearing after use to ensure the key bytes does not remain in unused process memory). /// /// If you want password protection instead of secure raw key protection then replace diff --git a/include/session/core/globals.hpp b/include/session/core/globals.hpp index 605e9d9ed..41692b415 100644 --- a/include/session/core/globals.hpp +++ b/include/session/core/globals.hpp @@ -43,6 +43,7 @@ class Globals final : detail::CoreComponent { network::ed25519_pubkey _pubkey_ed25519; network::x25519_pubkey _pubkey_x25519; std::array _session_id; // AKA pubkey_x25519 with a 0x05 byte prefix + std::string _session_id_hex; // hex encoding of _session_id, computed once in init() void init() override; @@ -108,6 +109,7 @@ class Globals final : detail::CoreComponent { } // These are computed from the account_seed during construction: std::span session_id() { return _session_id; } + const std::string& session_id_hex() const { return _session_id_hex; } const network::ed25519_pubkey& pubkey_ed25519() const { return _pubkey_ed25519; } const network::x25519_pubkey& pubkey_x25519() const { return _pubkey_x25519; } diff --git a/include/session/crypto/ed25519.hpp b/include/session/crypto/ed25519.hpp index 2fcdefb75..a44dcd4d8 100644 --- a/include/session/crypto/ed25519.hpp +++ b/include/session/crypto/ed25519.hpp @@ -45,17 +45,7 @@ struct PrivKeySpan { // Constructor for runtime-known sizes (e.g. at C API boundaries). // Throws std::invalid_argument if size is not 32 or 64. - PrivKeySpan(const std::byte* data, size_t size) { - if (size == 64) - data_ = data; - else if (size == 32) { - expand_seed(std::span{data, 32}); - data_ = storage_->data(); - } else - throw std::invalid_argument{ - "Ed25519 private key must be 32 or 64 bytes (got " + - std::to_string(size) + ")"}; - } + PrivKeySpan(const std::byte* data, size_t size); PrivKeySpan(const unsigned char* data, size_t size) : PrivKeySpan{reinterpret_cast(data), size} {} @@ -218,10 +208,15 @@ void pk_to_session_id(std::span out, std::span pk); -/// Converts an Ed25519 secret key to an X25519 secret key (using cleared memory). -/// Overload taking the 32-byte seed directly (the libsodium implementation only reads the -/// first 32 bytes anyway, so this is both correct and avoids expanding a seed unnecessarily). -cleared_b32 sk_to_x25519(std::span seed); +/// Converts an Ed25519 secret key to an X25519 secret key. +/// Write-to-output form. +void sk_to_x25519(std::span out, std::span seed); +/// Return-value form (using cleared memory). +inline cleared_b32 sk_to_x25519(std::span seed) { + cleared_b32 xsk; + sk_to_x25519(xsk, seed); + return xsk; +} /// Overload for a full 64-byte Ed25519 secret key (seed || pubkey); only the seed (first 32 /// bytes) is used. inline cleared_b32 sk_to_x25519(std::span full_key) { @@ -242,10 +237,10 @@ std::pair x25519_keypair(const PrivKeySpan& sk); /// Ed25519 and X25519 share the same private scalar: the Ed25519-to-X25519 conversion is /// defined by using that same scalar on X25519's base point instead of Ed25519's. Use this /// alias wherever the goal is to obtain the private scalar `a` rather than an X25519 key. -template - requires requires(T&& t) { sk_to_x25519(std::forward(t)); } -inline cleared_b32 sk_to_private(T&& arg) { - return sk_to_x25519(std::forward(arg)); +template + requires requires(Args&&... args) { sk_to_x25519(std::forward(args)...); } +inline decltype(auto) sk_to_private(Args&&... args) { + return sk_to_x25519(std::forward(args)...); } /// Computes the Ed25519 group element from a scalar (clamped). diff --git a/include/session/encrypt.hpp b/include/session/encrypt.hpp index 00721f8c5..2c5bb5800 100644 --- a/include/session/encrypt.hpp +++ b/include/session/encrypt.hpp @@ -14,7 +14,7 @@ #include "util.hpp" -namespace session::encrypt { +namespace session::encryption { // ─── Constants ─────────────────────────────────────────────────────────────── @@ -22,6 +22,16 @@ inline constexpr size_t XCHACHA20_KEYBYTES = 32; inline constexpr size_t XCHACHA20_NONCEBYTES = 24; inline constexpr size_t XCHACHA20_ABYTES = 16; // authentication tag size +inline constexpr size_t BOX_PUBLICKEYBYTES = 32; +inline constexpr size_t BOX_SECRETKEYBYTES = 32; +inline constexpr size_t BOX_MACBYTES = 16; +inline constexpr size_t BOX_NONCEBYTES = 24; +inline constexpr size_t BOX_SEALBYTES = BOX_PUBLICKEYBYTES + BOX_MACBYTES; // 48 + +inline constexpr size_t SECRETBOX_KEYBYTES = 32; +inline constexpr size_t SECRETBOX_NONCEBYTES = 24; +inline constexpr size_t SECRETBOX_MACBYTES = 16; + // ─── XChaCha20-Poly1305 AEAD ───────────────────────────────────────────────── /// Encrypts `msg` with `key` and `nonce`, writing ciphertext (msg.size() + ABYTES bytes) into @@ -29,8 +39,8 @@ inline constexpr size_t XCHACHA20_ABYTES = 16; // authentication tag size inline void xchacha20poly1305_encrypt( std::span out, std::span msg, - std::span nonce, - std::span key) { + std::span nonce, + std::span key) { crypto_aead_xchacha20poly1305_ietf_encrypt( ucdata(out), nullptr, ucdata(msg), msg.size(), nullptr, 0, nullptr, ucdata(nonce), ucdata(key)); @@ -41,8 +51,8 @@ inline void xchacha20poly1305_encrypt( inline bool xchacha20poly1305_decrypt( std::span out, std::span ciphertext, - std::span nonce, - std::span key) { + std::span nonce, + std::span key) { return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( ucdata(out), nullptr, nullptr, ucdata(ciphertext), ciphertext.size(), nullptr, 0, @@ -56,8 +66,8 @@ inline bool xchacha20poly1305_decrypt( inline void xchacha20_xor( std::span out, std::span in, - std::span nonce, - std::span key) { + std::span nonce, + std::span key) { crypto_stream_xchacha20_xor(ucdata(out), ucdata(in), in.size(), ucdata(nonce), ucdata(key)); } @@ -118,13 +128,13 @@ inline std::optional secretstream_pull( // ─── Box (X25519 + XSalsa20-Poly1305) ──────────────────────────────────────── /// Encrypts `msg` for `recipient_pk` from `sender_sk`, writing ciphertext into `out`. -/// `out` must be `msg.size() + crypto_box_MACBYTES` bytes. +/// `out` must be `msg.size() + BOX_MACBYTES` bytes. inline void box_easy( std::span out, std::span msg, - std::span nonce, - std::span recipient_pk, - std::span sender_sk) { + std::span nonce, + std::span recipient_pk, + std::span sender_sk) { if (0 != crypto_box_easy( ucdata(out), ucdata(msg), @@ -136,22 +146,22 @@ inline void box_easy( } /// Seals `msg` for `pk` (anonymous sender), writing ciphertext into `out`. -/// `out` must be `msg.size() + crypto_box_SEALBYTES` bytes. +/// `out` must be `msg.size() + BOX_SEALBYTES` bytes. inline void box_seal( std::span out, std::span msg, - std::span pk) { + std::span pk) { if (0 != crypto_box_seal(ucdata(out), ucdata(msg), msg.size(), ucdata(pk))) throw std::runtime_error{"crypto_box_seal failed (invalid public key?)"}; } -/// Decrypts a sealed box. `out` must be `ciphertext.size() - crypto_box_SEALBYTES` bytes. +/// Decrypts a sealed box. `out` must be `ciphertext.size() - BOX_SEALBYTES` bytes. /// Returns false if authentication fails. inline bool box_seal_open( std::span out, std::span ciphertext, - std::span pk, - std::span sk) { + std::span pk, + std::span sk) { return 0 == crypto_box_seal_open( ucdata(out), ucdata(ciphertext), ciphertext.size(), ucdata(pk), ucdata(sk)); } @@ -163,11 +173,11 @@ inline bool box_seal_open( inline bool secretbox_open_easy( std::span out, std::span ciphertext, - std::span nonce, - std::span key) { + std::span nonce, + std::span key) { return 0 == crypto_secretbox_open_easy( ucdata(out), ucdata(ciphertext), ciphertext.size(), ucdata(nonce), ucdata(key)); } -} // namespace session::encrypt +} // namespace session::encryption diff --git a/include/session/fields.hpp b/include/session/fields.hpp index 7376328e7..46cba9deb 100644 --- a/include/session/fields.hpp +++ b/include/session/fields.hpp @@ -30,17 +30,5 @@ struct Disappearing { std::chrono::seconds timer = 0s; }; -/// A Session ID: an x25519 pubkey, with a 05 identifying prefix. On the wire we send just the -/// 32-byte pubkey value (i.e. not hex, without the prefix). -struct SessionID { - /// The fixed session netid, 0x05 - static constexpr std::byte netid{0x05}; - - /// The raw x25519 pubkey, as bytes - b32 pubkey; - - /// Returns the full pubkey in hex, including the netid prefix. - std::string hex() const; -}; } // namespace session diff --git a/include/session/formattable.hpp b/include/session/format.hpp similarity index 95% rename from include/session/formattable.hpp rename to include/session/format.hpp index 7783e32fa..5b2a99363 100644 --- a/include/session/formattable.hpp +++ b/include/session/format.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -40,6 +41,11 @@ inline namespace literals { namespace fmt { +// Disable fmt's generic range formatter for byte spans so that our byte_spannable formatter takes +// precedence (avoids ambiguity when fmt/ranges.h is also included). +template +struct range_format_kind : std::integral_constant {}; + /// Generic formatter for any byte_spannable type (std::span, std::array, std::vector of std::byte). /// /// Format spec: diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 27169b86c..275809a00 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -109,22 +109,16 @@ namespace detail { }; (update(make_hashable(args)), ...); } -} // namespace detail + template + requires(sizeof...(T) > 0) + void update_all(crypto_generichash_blake2b_state& st, const T&... args) { + auto update_hash = [&st](std::span arg) { + crypto_generichash_blake2b_update(&st, arg.data(), arg.size()); + }; + (update_hash(make_hashable(args)), ...); + } -/// API: hash/update_all -/// -/// Wrapper about crypto_generichash_blake2b_update that takes any number of contiguous byte -/// containers *or* integer values and updates the hash state with them, in argument order. Integer -/// values are always written as raw bytes in little-endian encoding (i.e. they will be byte-swapped -/// if necessary). -template - requires(sizeof...(T) > 0) -void update_all(crypto_generichash_blake2b_state& st, const T&... args) { - auto update_hash = [&st](std::span arg) { - crypto_generichash_blake2b_update(&st, arg.data(), arg.size()); - }; - (update_hash(detail::make_hashable(args)), ...); -} +} // namespace detail /// Concept for a fixed-size, writable byte container — the basic requirement for any hash output. template @@ -214,7 +208,7 @@ struct blake2b_hasher { template requires(sizeof...(T) > 0) blake2b_hasher& update(const T&... args) { - update_all(st, args...); + detail::update_all(st, args...); return *this; } @@ -487,6 +481,11 @@ void hmac_sha256(Out& out, const Key& key, const T&... args) { // ─── Argon2id (password hashing / KDF) ─────────────────────────────────────── +inline constexpr size_t ARGON2_SALTBYTES = crypto_pwhash_SALTBYTES; +inline constexpr unsigned long long ARGON2_OPSLIMIT_MODERATE = crypto_pwhash_OPSLIMIT_MODERATE; +inline constexpr size_t ARGON2_MEMLIMIT_MODERATE = crypto_pwhash_MEMLIMIT_MODERATE; +inline constexpr int ARGON2ID13 = crypto_pwhash_ALG_ARGON2ID13; + /// Derives a key from a password using Argon2id (libsodium crypto_pwhash). /// Throws std::runtime_error if the derivation fails (e.g. out of memory). /// @@ -503,7 +502,7 @@ template void argon2( Out& out, std::span password, - std::span salt, + std::span salt, unsigned long long opslimit, size_t memlimit, int alg) { diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index fde18fa2a..23ab4486f 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -532,7 +532,7 @@ struct DecryptGroupMessage { /// (and possibly log) but otherwise ignore such exceptions and just not process the message if /// it throws. DecryptGroupMessage decrypt_group_message( - std::span> decrypt_ed25519_privkey_list, + std::span> group_enc_keys, std::span group_ed25519_pubkey, std::span ciphertext); diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index dc26b81b7..9efa38b5a 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -527,7 +527,7 @@ DecodedEnvelope decode_dm_envelope( /// /// Throws on parse or decryption failure. DecodedEnvelope decode_group_envelope( - std::span> group_keys, + std::span> group_keys, std::span group_ed25519_pubkey, std::span envelope_payload, std::span pro_backend_pubkey); diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp index 0a071287c..66da66226 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -11,47 +11,6 @@ void sodium_buffer_deallocate(void* p); // Calls sodium_memzero to zero a buffer void sodium_zero_buffer(void* ptr, size_t size); -// Works similarly to a unique_ptr, but allocations and free go via libsodium (which is slower, but -// more secure for sensitive data). -template -struct sodium_ptr { - private: - T* x; - - public: - sodium_ptr() : x{nullptr} {} - sodium_ptr(std::nullptr_t) : sodium_ptr{} {} - ~sodium_ptr() { reset(x); } - - // Allocates and constructs a new `T` in-place, forwarding any given arguments to the `T` - // constructor. If this sodium_ptr already has an object, `reset()` is first called implicitly - // to destruct and deallocate the existing object. - template - T& emplace(Args&&... args) { - if (x) - reset(); - x = static_cast(sodium_buffer_allocate(sizeof(T))); - new (x) T(std::forward(args)...); - return *x; - } - - void reset() { - if (x) { - x->~T(); - sodium_buffer_deallocate(x); - x = nullptr; - } - } - void operator=(std::nullptr_t) { reset(); } - - T& operator*() { return *x; } - const T& operator*() const { return *x; } - - T* operator->() { return x; } - const T* operator->() const { return x; } - - explicit operator bool() const { return x != nullptr; } -}; // Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only // be used with trivially destructible types. @@ -74,150 +33,11 @@ struct cleared_array : sodium_cleared> { } }; -template -using cleared_uchars = cleared_array; -using cleared_uc32 = cleared_uchars<32>; -using cleared_uc64 = cleared_uchars<64>; template using cleared_bytes = cleared_array; using cleared_b32 = cleared_bytes<32>; using cleared_b64 = cleared_bytes<64>; -// This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation -// and freeing via libsodium. It is slower and heavier than a regular allocation type but takes -// extra precautions, intended for storing sensitive values. -template -struct sodium_array { - private: - T* buf; - size_t len; - - public: - // Default constructor: makes an empty object (that is, has no buffer and has `.size()` of 0). - sodium_array() : buf{nullptr}, len{0} {} - - // Constructs an array with a given size, default-constructing the individual elements. - template >> - explicit sodium_array(size_t length) : - buf{length == 0 ? nullptr - : static_cast(sodium_buffer_allocate(length * sizeof(T)))}, - len{0} { - - if (length > 0) { - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else if constexpr (std::is_nothrow_default_constructible_v) { - for (; len < length; len++) - new (buf[len]) T(); - } else { - try { - for (; len < length; len++) - new (buf[len]) T(); - } catch (...) { - reset(); - throw; - } - } - } - } - - ~sodium_array() { reset(); } - - // Moveable: ownership is transferred to the new object and the old object becomes empty. - sodium_array(sodium_array&& other) : buf{other.buf}, len{other.len} { - other.buf = nullptr; - other.len = 0; - } - sodium_array& operator=(sodium_array&& other) { - sodium_buffer_deallocate(buf); - buf = other.buf; - len = other.len; - other.buf = nullptr; - other.len = 0; - } - - // Non-copyable - sodium_array(const sodium_array&) = delete; - sodium_array& operator=(const sodium_array&) = delete; - - // Destroys the held array; after destroying elements the allocated space is overwritten with - // 0s before being deallocated. - void reset() { - if (buf) { - if constexpr (!std::is_trivially_destructible_v) - while (len > 0) - buf[--len].~T(); - - sodium_buffer_deallocate(buf); - } - buf = nullptr; - len = 0; - } - - // Calls reset() to destroy the current value (if any) and then allocates a new - // default-constructed one of the given size. - template >> - void reset(size_t length) { - reset(); - if (length > 0) { - buf = static_cast(sodium_buffer_allocate(length * sizeof(T))); - if constexpr (std::is_trivial_v) { - std::memset(buf, 0, length * sizeof(T)); - len = length; - } else { - for (; len < length; len++) - new (buf[len]) T(); - } - } - } - - // Loads the array from a pointer and size; this first resets a value (if present), allocates a - // new array of the given size, the copies the given value(s) into the new buffer. T must be - // copyable. This is *not* safe to use if `buf` points into the currently allocated data. - template >> - void load(const T* data, size_t length) { - reset(length); - if (length == 0) - return; - - if constexpr (std::is_trivially_copyable_v) - std::memcpy(buf, data, sizeof(T) * length); - else - for (; len < length; len++) - new (buf[len]) T(data[len]); - } - - const T& operator[](size_t i) const { - assert(i < len); - return buf[i]; - } - T& operator[](size_t i) { - assert(i < len); - return buf[i]; - } - - T* data() { return buf; } - const T* data() const { return buf; } - - size_t size() const { return len; } - bool empty() const { return len == 0; } - explicit operator bool() const { return !empty(); } - - operator std::span() { return {buf, len}; } - operator std::span() const { return {buf, len}; } - - T* begin() { return buf; } - const T* begin() const { return buf; } - T* end() { return buf + len; } - const T* end() const { return buf + len; } - - using difference_type = ptrdiff_t; - using value_type = T; - using pointer = value_type*; - using reference = value_type&; - using iterator_category = std::random_access_iterator_tag; -}; // sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with // sensitive data. It is as a result slower and has more overhead than a standard allocator and diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 58e382aa1..ddb5ae50f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -88,7 +88,6 @@ add_libsession_util_library(config config/pro.cpp config/user_groups.cpp config/user_profile.cpp - fields.cpp ) diff --git a/src/blinding.cpp b/src/blinding.cpp index b9384ed43..d4ceb500f 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -143,9 +143,9 @@ std::array blind15_id(std::string_view session_id, std::string_v b33 blinded; blind15_id_impl(raw_sid, raw_server_pk, blinded); std::array result; - result[0] = oxenc::to_hex(blinded.begin(), blinded.end()); + result[0] = oxenc::to_hex(blinded); blinded.back() ^= std::byte{0x80}; - result[1] = oxenc::to_hex(blinded.begin(), blinded.end()); + result[1] = oxenc::to_hex(blinded); return result; } @@ -178,7 +178,7 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) b33 blinded; blind25_id_impl(raw_sid, raw_server_pk, blinded); - return oxenc::to_hex(blinded.begin(), blinded.end()); + return oxenc::to_hex(blinded); } b33 blinded15_id_from_ed( @@ -235,11 +235,8 @@ std::pair blind15_key_pair( k = &k_tmp; *k = blind15_factor(server_pk); - /// Generate a scalar for the private key - if (0 != crypto_sign_ed25519_sk_to_curve25519( - to_unsigned(a.data()), to_unsigned(ed25519_sk.data()))) - throw std::runtime_error{ - "blind15_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; + // Calculate the private scalar `a` + ed25519::sk_to_private(a, ed25519_sk.seed()); // Turn a, A into their blinded versions ed25519::scalar_mul(a, *k, a); diff --git a/src/config/base.cpp b/src/config/base.cpp index 96037e4fc..758de3906 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -217,8 +217,8 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::spansize(), recombined.size()); recombined = std::move(*decompressed); } else throw std::runtime_error{ "Invalid recombined data (hash {}): decompression failed"_format( - oxenc::to_hex(final_hash.begin(), final_hash.end()), msg_id)}; + final_hash, msg_id)}; } if (recombined.empty()) @@ -276,7 +276,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span remaining{msg}; @@ -950,10 +950,7 @@ int ConfigBase::key_count() const { return _keys.size(); } -bool ConfigBase::has_key(std::span key) const { - if (key.size() != 32) - throw std::invalid_argument{"invalid key given to has_key(): not 32-bytes"}; - +bool ConfigBase::has_key(std::span key) const { auto* keyptr = key.data(); for (const auto& key : _keys) if (sodium_memcmp(keyptr, key.data(), KEY_SIZE) == 0) @@ -961,22 +958,19 @@ bool ConfigBase::has_key(std::span key) const { return false; } -std::vector> ConfigBase::get_keys() const { - std::vector> ret; +std::vector> ConfigBase::get_keys() const { + std::vector> ret; ret.reserve(_keys.size()); for (const auto& key : _keys) - ret.emplace_back(key.data(), key.size()); + ret.emplace_back(key); return ret; } void ConfigBase::add_key( - std::span key, bool high_priority, bool dirty_config) { + std::span key, bool high_priority, bool dirty_config) { static_assert( sizeof(Key) == KEY_SIZE, "std::array appears to have some overhead which seems bad"); - if (key.size() != KEY_SIZE) - throw std::invalid_argument{"add_key failed: key size must be 32 bytes"}; - if (!_keys.empty() && sodium_memcmp(_keys.front().data(), key.data(), KEY_SIZE) == 0) return; else if (!high_priority && has_key(key)) @@ -1009,7 +1003,7 @@ int ConfigBase::clear_keys(bool dirty_config) { } void ConfigBase::replace_keys( - const std::vector>& new_keys, bool dirty_config) { + const std::vector>& new_keys, bool dirty_config) { if (new_keys.empty()) { if (_keys.empty()) return; @@ -1017,10 +1011,6 @@ void ConfigBase::replace_keys( return; } - for (auto& k : new_keys) - if (k.size() != KEY_SIZE) - throw std::invalid_argument{"replace_keys failed: keys must be 32 bytes"}; - dirty_config = dirty_config && !is_readonly() && (_keys.empty() || sodium_memcmp(_keys.front().data(), new_keys.front().data(), KEY_SIZE) != 0); @@ -1034,7 +1024,7 @@ void ConfigBase::replace_keys( dirty(); } -bool ConfigBase::remove_key(std::span key, size_t from, bool dirty_config) { +bool ConfigBase::remove_key(std::span key, size_t from, bool dirty_config) { auto starting_size = _keys.size(); if (from >= starting_size) return false; @@ -1063,8 +1053,7 @@ void ConfigBase::load_key(const ed25519::PrivKeySpan& ed25519_secretkey) { void ConfigSig::set_sig_keys(const ed25519::PrivKeySpan& secret) { clear_sig_keys(); - _sign_sk.reset(64); - std::memcpy(_sign_sk.data(), secret.data(), 64); + _sign_sk.assign(secret.begin(), secret.end()); ed25519::sk_to_pk(_sign_pk.emplace(), secret); set_verifier([this](std::span data, std::span sig) { @@ -1087,7 +1076,7 @@ void ConfigSig::set_sig_pubkey(std::span pubkey) { void ConfigSig::clear_sig_keys() { _sign_pk.reset(); - _sign_sk.reset(); + _sign_sk.clear(); set_signer(nullptr); set_verifier(nullptr); } @@ -1101,7 +1090,7 @@ void ConfigBase::set_signer(ConfigMessage::sign_callable s) { } cleared_b32 ConfigSig::seed_hash(std::string_view key) const { - if (!_sign_sk) + if (_sign_sk.empty()) throw std::runtime_error{"Cannot make a seed hash without a signing secret key"}; cleared_b32 result; hash::blake2b_key(result, key, std::span{_sign_sk.data(), 32}); diff --git a/src/config/community.cpp b/src/config/community.cpp index 0b89b5204..6a74ca94a 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -10,8 +11,6 @@ #include #include "internal.hpp" -#include "oxenc/base32z.h" -#include "oxenc/base64.h" #include "session/config/community.h" #include "session/export.h" #include "session/util.hpp" @@ -54,18 +53,15 @@ void community::set_pubkey(std::string_view pubkey) { } std::string community::pubkey_hex() const { - const auto& pk = pubkey(); - return oxenc::to_hex(pk.begin(), pk.end()); + return "{:x}"_format(pubkey()); } std::string community::pubkey_b32z() const { - const auto& pk = pubkey(); - return oxenc::to_base32z(pk.begin(), pk.end()); + return "{:a}"_format(pubkey()); } std::string community::pubkey_b64() const { - const auto& pk = pubkey(); - return oxenc::to_base64(pk.begin(), pk.end()); + return "{:b}"_format(pubkey()); } void community::set_room(std::string_view room) { @@ -79,12 +75,7 @@ std::string community::full_url() const { std::string community::full_url( std::string_view base_url, std::string_view room, std::span pubkey) { - std::string url{base_url}; - url += '/'; - url += room; - url += qs_pubkey; - url += oxenc::to_hex(pubkey); - return url; + return "{}/{}?public_key={:x}"_format(base_url, room, pubkey); } void community::canonicalize_url(std::string& url) { @@ -110,10 +101,8 @@ std::string community::canonical_url(std::string_view url) { std::string result; result += proto; result += host; - if (port) { - result += ':'; - result += std::to_string(*port); - } + if (port) + fmt::format_to(std::back_inserter(result), ":{}", *port); // We don't (currently) allow a /path in a community URL if (path) throw std::invalid_argument{"Invalid community URL: found unexpected trailing value"}; diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index db511741d..70664be3c 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -333,7 +333,7 @@ blinded_contact_info::blinded_contact_info( if (prefix != session::SessionIDPrefix::community_blinded && prefix != session::SessionIDPrefix::community_blinded_legacy) throw std::invalid_argument{ - "Invalid blinded ID: Expected '15' or '25' prefix; got " + std::string{blinded_id}}; + "Invalid blinded ID: Expected '15' or '25' prefix; got {}"_format(blinded_id)}; } void blinded_contact_info::load(const dict& info_dict) { @@ -520,7 +520,7 @@ bool Contacts::erase_blinded(std::string_view base_url_, std::string_view blinde if (prefix != session::SessionIDPrefix::community_blinded && prefix != session::SessionIDPrefix::community_blinded_legacy) throw std::invalid_argument{ - "Invalid blinded ID: Expected '15' or '25' prefix; got " + std::string{blinded_id}}; + "Invalid blinded ID: Expected '15' or '25' prefix; got {}"_format(blinded_id)}; auto base_url = community::canonical_url(base_url_); auto pk = std::string(blinded_id.substr(2)); diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 7c2878fb7..dc91b3851 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -312,7 +313,7 @@ std::optional ConvoInfoVolatile::get_blinded_1to1( if (prefix != session::SessionIDPrefix::community_blinded && prefix != session::SessionIDPrefix::community_blinded_legacy) throw std::invalid_argument{ - "Invalid blinded ID: Expected '15' or '25' prefix; got " + std::string{pubkey_hex}}; + "Invalid blinded ID: Expected '15' or '25' prefix; got {}"_format(pubkey_hex)}; std::string pubkey = session_id_to_bytes(pubkey_hex, to_string(prefix)); diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 1ede3a399..72490eb6e 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -19,7 +19,7 @@ Info::Info( std::span ed25519_pubkey, const ed25519::OptionalPrivKeySpan& ed25519_secretkey, std::optional> dumped) : - id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} { + id{"03{:x}"_format(ed25519_pubkey)} { init(dumped, ed25519_pubkey, ed25519_secretkey); } diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index d7ee8dfd7..b96fb1e1d 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -3,19 +3,14 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include +#include "../../internal-util.hpp" + #include #include "../internal.hpp" @@ -52,7 +47,7 @@ Keys::Keys( init_sig_keys(group_ed25519_pubkey, group_ed25519_secretkey); - user_ed25519_sk.load(user_ed25519_secretkey.data(), 64); + user_ed25519_sk.assign(user_ed25519_secretkey.begin(), user_ed25519_secretkey.end()); if (dumped) { load_dump(*dumped); @@ -137,8 +132,8 @@ void Keys::load_dump(std::span dump) { auto key_bytes = kd.consume_string_view(); if (key_bytes.size() != key.key.size()) throw config_value_error{ - "Invalid Keys dump: found key with invalid size (" + - std::to_string(key_bytes.size()) + ")"}; + "Invalid Keys dump: found key with invalid size ({})"_format( + key_bytes.size())}; std::memcpy(key.key.data(), key_bytes.data(), key.key.size()); if (!kd.skip_until("t")) @@ -171,8 +166,8 @@ void Keys::load_dump(std::span dump) { auto pk = pending.consume_string_view(); if (pk.size() != pending_key_.size()) throw config_value_error{ - "Invalid Keys dump: found pending key (k) with invalid size (" + - std::to_string(pk.size()) + ")"}; + "Invalid Keys dump: found pending key (k) with invalid size ({})"_format( + pk.size())}; std::memcpy(pending_key_.data(), pk.data(), pending_key_.size()); } } @@ -181,15 +176,15 @@ size_t Keys::size() const { return keys_.size() + !pending_key_config_.empty(); } -std::vector> Keys::group_keys() const { - std::vector> ret; +std::vector> Keys::group_keys() const { + std::vector> ret; ret.reserve(size()); if (!pending_key_config_.empty()) - ret.emplace_back(pending_key_.data(), 32); + ret.emplace_back(pending_key_); for (auto it = keys_.rbegin(); it != keys_.rend(); ++it) - ret.emplace_back(it->key.data(), 32); + ret.emplace_back(it->key); return ret; } @@ -268,32 +263,31 @@ std::span Keys::rekey(Info& info, Members& members) { auto h2 = seed_hash(seed_hash_key); - hash::blake2b_hasher hasher{ + hash::blake2b_hasher hasher{ enc_key_hash_key, std::nullopt}; for (const auto& m : members) hasher.update(m.session_id); auto gen = keys_.empty() ? 0 : keys_.back().generation + 1; - auto gen_str = std::to_string(gen); - hasher.update(gen_str, h2); + hasher.update("{}"_format(gen), h2); auto h1 = hasher.finalize(); - std::span enc_key = - std::span{h1}.first(); - std::span nonce = - std::span{h1}.last(); + std::span enc_key = + std::span{h1}.first(); + std::span nonce = + std::span{h1}.last(); oxenc::bt_dict_producer d{}; d.append("#", to_string_view(nonce)); - std::array encrypted; + std::array encrypted; std::string_view enc_sv = to_string_view(encrypted); // Shared key for admins auto member_k = seed_hash(enc_key_admin_hash_key); - encrypt::xchacha20poly1305_encrypt(encrypted, enc_key, nonce, member_k); + encryption::xchacha20poly1305_encrypt(encrypted, enc_key, nonce, member_k); d.append("G", gen); d.append("K", enc_sv); @@ -417,7 +411,7 @@ std::vector Keys::key_supplement(const std::vector& sids supp_keys = std::move(supp).str(); } - hash::blake2b_hasher nonce_hasher{ + hash::blake2b_hasher nonce_hasher{ enc_key_hash_key, std::nullopt}; for (const auto& sid : sids) nonce_hasher.update(sid); @@ -434,7 +428,7 @@ std::vector Keys::key_supplement(const std::vector& sids { auto list = d.append_list("+"); std::vector encrypted; - encrypted.resize(supp_keys.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + encrypted.resize(supp_keys.size() + encryption::XCHACHA20_ABYTES); size_t member_count = 0; @@ -694,7 +688,7 @@ bool Keys::swarm_verify_subaccount( if (!_sign_pk) return false; return swarm_verify_subaccount( - "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()), + "03{:x}"_format(*_sign_pk), ed25519::PrivKeySpan::from(user_ed25519_sk), sign_val, write, @@ -783,10 +777,10 @@ void Keys::insert_key(std::string_view msg_hash, key_info&& new_key) { // Attempts xchacha20 decryption. // // Preconditions: -// - `ciphertext` must be at least 16 [crypto_aead_xchacha20poly1305_ietf_ABYTES] +// - `ciphertext` must be at least 16 [encryption::XCHACHA20_ABYTES] // - `out` must have enough space (ciphertext.size() - 16 -// [crypto_aead_xchacha20poly1305_ietf_ABYTES]) -// - `nonce` must be 24 bytes [crypto_aead_xchacha20poly1305_ietf_NPUBBYTES] +// [encryption::XCHACHA20_ABYTES]) +// - `nonce` must be 24 bytes [encryption::XCHACHA20_NONCEBYTES] // - `key` must be 32 bytes [crypto_aead_xchacha20poly1305_ietf_KEYBYTES] // // The latter two are asserted in a debug build, but not otherwise checked. @@ -796,9 +790,9 @@ namespace { bool try_decrypting( std::span out, std::span encrypted, - std::span nonce, - std::span key) { - return encrypt::xchacha20poly1305_decrypt(out, encrypted, nonce, key); + std::span nonce, + std::span key) { + return encryption::xchacha20poly1305_decrypt(out, encrypted, nonce, key); } } // namespace @@ -819,9 +813,9 @@ bool Keys::load_key_message( if (!d.skip_until("#")) throw config_value_error{"Key message has no nonce"}; auto nonce_dyn = d.consume_span(); - if (nonce_dyn.size() != encrypt::XCHACHA20_NONCEBYTES) + if (nonce_dyn.size() != encryption::XCHACHA20_NONCEBYTES) throw config_value_error{"Key message has invalid nonce size"}; - auto nonce = nonce_dyn.first(); + auto nonce = nonce_dyn.first(); sodium_vector new_keys; std::optional max_gen; // If set then associate the message with this generation @@ -855,11 +849,10 @@ bool Keys::load_key_message( // e + 1 // --- // 52 - if (encrypted.size() < 52 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (encrypted.size() < 52 + encryption::XCHACHA20_ABYTES) throw config_value_error{ - "Supplemental key message has invalid key info size at " - "index " + - std::to_string(member_key_pos)}; + "Supplemental key message has invalid key info size at index {}"_format( + member_key_pos)}; if (!new_keys.empty() || admin()) continue; // Keep parsing, to ensure validity of the whole message @@ -927,7 +920,7 @@ bool Keys::load_key_message( "Non-supplemental key message is missing required admin key (K)"}; auto admin_key = to_span(d.consume_string_view()); - if (admin_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (admin_key.size() != 32 + encryption::XCHACHA20_ABYTES) throw config_value_error{"Key message has invalid admin key length"}; if (admin()) { @@ -951,10 +944,10 @@ bool Keys::load_key_message( while (!key_list.is_finished()) { member_key_pos++; auto member_key = to_span(key_list.consume_string_view()); - if (member_key.size() != 32 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (member_key.size() != 32 + encryption::XCHACHA20_ABYTES) throw config_value_error{ - "Key message has invalid member key length at index " + - std::to_string(member_key_pos)}; + "Key message has invalid member key length at index {}"_format( + member_key_pos)}; if (found_key) continue; @@ -1084,14 +1077,14 @@ bool Keys::needs_rekey() const { return last_it->generation == second_it->generation; } -std::optional> Keys::pending_key() const { +std::optional> Keys::pending_key() const { if (!pending_key_config_.empty()) - return std::span{pending_key_.data(), pending_key_.size()}; + return std::span{pending_key_}; return std::nullopt; } static constexpr size_t ENCRYPT_OVERHEAD = - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; + encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES; std::vector Keys::encrypt_message( std::span plaintext, bool compress, size_t padding) const { @@ -1108,34 +1101,15 @@ std::pair> Keys::decrypt_message( // // Decrypt, using all the possible keys, starting with a pending one (if we have one) // - DecryptGroupMessage decrypt = {}; - bool decrypt_success = false; - if (auto pending = pending_key(); pending) { - try { - std::span> key_list = {&(*pending), 1}; - decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); - decrypt_success = true; - } catch (const std::exception&) { - } - } - - if (!decrypt_success) { - for (auto& k : keys_) { - try { - std::span key = {k.key.data(), k.key.size()}; - std::span> key_list = {&key, 1}; - decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); - decrypt_success = true; - break; - } catch (const std::exception&) { - } - } - } - - if (!decrypt_success) // none of the keys worked - throw std::runtime_error{fmt::format( - "unable to decrypt ciphertext with any current group keys; tried {}", - keys_.size() + (pending_key() ? 1 : 0))}; + // Build the list of candidate keys: pending key (if any) first, then all active keys. + std::vector> key_list; + key_list.reserve(keys_.size() + 1); + if (auto pending = pending_key()) + key_list.push_back(*pending); + for (auto& k : keys_) + key_list.emplace_back(k.key); + + auto decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); std::pair> result; result.first = std::move(decrypt.session_id); diff --git a/src/config/internal.cpp b/src/config/internal.cpp index f7c539356..3233b049a 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -30,8 +30,8 @@ void check_session_id(std::string_view session_id, std::string_view prefix) { if (!(session_id.size() == 64 + prefix.size() && oxenc::is_hex(session_id) && session_id.substr(0, prefix.size()) == prefix)) throw std::invalid_argument{ - "Invalid session ID: expected 66 hex digits starting with " + std::string{prefix} + - "; got " + std::string{session_id}}; + "Invalid session ID: expected 66 hex digits starting with {}; got {}"_format( + prefix, session_id)}; } SessionIDPrefix get_session_id_prefix(std::string_view id) { diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 828ffdf63..2f0366150 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -11,6 +11,7 @@ #include "session/clock.hpp" #include "session/config/base.h" #include "session/config/base.hpp" +#include "../internal-util.hpp" #include "session/config/error.h" #include "session/types.hpp" @@ -261,6 +262,47 @@ void load_unknowns( oxenc::bt_dict_consumer& in, std::string_view previous, std::string_view until); +template , int> = 0> +inline internals& unbox(config_object* conf) { + return *static_cast*>(conf->internals); +} +template , int> = 0> +inline const internals& unbox(const config_object* conf) { + return *static_cast*>(conf->internals); +} + +// Wraps a lambda and, if an exception is thrown, sets an error message in the config_object's +// error buffer and updates the last_error pointer. +template +decltype(auto) wrap_exceptions(config_object* conf, Call&& f) { + using Ret = std::invoke_result_t; + + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + session::copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + if constexpr (std::is_pointer_v) + return static_cast(nullptr); + else + static_assert(std::is_void_v, "Don't know how to return an error value!"); +} + +// Same as above but accepts callbacks with value returns on errors +template +Ret wrap_exceptions(config_object* conf, Call&& f, Ret error_return) { + try { + conf->last_error = nullptr; + return std::invoke(std::forward(f)); + } catch (const std::exception& e) { + session::copy_c_str(conf->_error_buf, e.what()); + conf->last_error = conf->_error_buf; + } + return error_return; +} + } // namespace session::config namespace fmt { diff --git a/src/config/pro.cpp b/src/config/pro.cpp index 9c32b8bf8..2d9315858 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -1,8 +1,7 @@ #include #include -#include - #include +#include #include #include "internal.hpp" @@ -21,7 +20,7 @@ bool ProConfig::load(const dict& root) { return false; std::optional> maybe_rotating_seed = maybe_vector(root, "r"); - if (!maybe_rotating_seed || maybe_rotating_seed->size() != crypto_sign_ed25519_SEEDBYTES) + if (!maybe_rotating_seed || maybe_rotating_seed->size() != 32) return false; // NOTE: Load into the proof object @@ -52,10 +51,10 @@ bool ProConfig::load(const dict& root) { // Derive the rotating public key from the seed and populate the proof's pubkey and the outer // private key - crypto_sign_ed25519_seed_keypair( - to_unsigned(proof.rotating_pubkey.data()), - to_unsigned(rotating_privkey.data()), - to_unsigned(maybe_rotating_seed->data())); + ed25519::seed_keypair( + proof.rotating_pubkey, + rotating_privkey, + std::span{*maybe_rotating_seed}.first<32>()); return true; } diff --git a/src/config/protos.cpp b/src/config/protos.cpp index 311dd0e1c..409d84833 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -1,8 +1,5 @@ #include "session/config/protos.hpp" -#include -#include - #include #include diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 93ac2be37..da19455f3 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -230,14 +229,11 @@ void group_info::load(const dict& info_dict) { name.clear(); if (auto seed = maybe_vector(info_dict, "K"); seed && seed->size() == 32) { - b33 pk; - pk[0] = std::byte{0x03}; - secretkey.resize(64); - crypto_sign_seed_keypair( - to_unsigned(pk.data() + 1), to_unsigned(secretkey.data()), - to_unsigned(seed->data())); - if (id != oxenc::to_hex(pk.begin(), pk.end())) + auto [pk, sk] = ed25519::keypair(std::span{*seed}.first<32>()); + if (id != "03{:x}"_format(pk)) secretkey.clear(); + else + secretkey.assign(sk.begin(), sk.end()); } if (auto sig = maybe_vector(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); @@ -384,17 +380,10 @@ group_info UserGroups::get_or_construct_group(std::string_view pubkey_hex) const } group_info UserGroups::create_group() const { - b32 pk; - std::vector sk; - sk.resize(64); - crypto_sign_keypair(to_unsigned(pk.data()), to_unsigned(sk.data())); - std::string pk_hex; - pk_hex.reserve(66); - pk_hex += "03"; - oxenc::to_hex(pk.begin(), pk.end(), std::back_inserter(pk_hex)); - - group_info gr{std::move(pk_hex)}; - gr.secretkey = std::move(sk); + auto [pk, sk] = ed25519::keypair(); + + group_info gr{"03{:x}"_format(pk)}; + gr.secretkey.assign(sk.begin(), sk.end()); return gr; } diff --git a/src/core.cpp b/src/core.cpp index 1c8a941a6..51b60901e 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -126,7 +127,6 @@ void Core::_poll() { config::Namespace::AccountPubkeys}; auto now_ms = epoch_ms(clock_now_ms()); - auto session_id_hex = oxenc::to_hex(globals.session_id()); auto ed25519_hex = globals.pubkey_ed25519().hex(); // Build per-namespace signatures for namespaces that require authentication; index-aligned with @@ -140,7 +140,7 @@ void Core::_poll() { continue; std::string to_sign = "retrieve{}{}"_format(ns_val, now_ms); auto sig = ed25519::sign(seed.ed25519_secret(), to_span(to_sign)); - ns_sig[i] = oxenc::to_base64(sig); + ns_sig[i] = "{:b}"_format(sig); } } @@ -150,7 +150,6 @@ void Core::_poll() { [this, net, namespaces, - session_id_hex, ed25519_hex, now_ms, ns_sig = std::move(ns_sig)](auto, auto swarm) { @@ -167,7 +166,7 @@ void Core::_poll() { auto ns = namespaces[i]; auto ns_val = static_cast(ns); nlohmann::json params = { - {"pubkey", session_id_hex}, + {"pubkey", globals.session_id_hex()}, {"namespace", ns_val}, }; @@ -310,8 +309,6 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) auto status = PfsKeyStatus::fetching; { auto conn = db.conn(); - auto sid_hex = oxenc::to_hex(session_id.begin(), session_id.end()); - if (auto row = conn.prepared_maybe_get, std::optional>( "SELECT fetched_at, nak_at FROM pfs_key_cache WHERE session_id = ?", sid)) { auto [fetched_at, nak_at] = *row; @@ -322,14 +319,14 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) cat, "prefetch_pfs_keys: cached key for {} is still fresh ({} old), " "skipping", - sid_hex, + session_id, age); return PfsKeyStatus::fresh; } log::debug( cat, "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", - sid_hex, + session_id, age); status = PfsKeyStatus::stale; } else if (nak_at) { @@ -338,18 +335,18 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) log::debug( cat, "prefetch_pfs_keys: recent NAK for {} ({} old), skipping", - sid_hex, + session_id, age); return PfsKeyStatus::nak; } log::debug( cat, "prefetch_pfs_keys: expired NAK for {} ({} old), re-fetching", - sid_hex, + session_id, age); } } else { - log::debug(cat, "prefetch_pfs_keys: no cached key for {}, fetching", sid_hex); + log::debug(cat, "prefetch_pfs_keys: no cached key for {}, fetching", session_id); } } @@ -357,12 +354,11 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) network::x25519_pubkey x25519_pub; std::ranges::copy(session_id.subspan<1>(), x25519_pub.begin()); - auto session_id_hex = oxenc::to_hex(session_id.begin(), session_id.end()); auto now_ms = epoch_ms(clock_now_ms()); // AccountPubkeys (-21) allows unauthenticated retrieve: no signature needed. nlohmann::json params = { - {"pubkey", session_id_hex}, + {"pubkey", oxenc::to_hex(session_id)}, {"namespace", static_cast(config::Namespace::AccountPubkeys)}, }; @@ -392,7 +388,7 @@ PfsKeyStatus Core::prefetch_pfs_keys(std::span session_id) log::warning( cat, "Failed to fetch PFS keys for {}: {}", - oxenc::to_hex(sid), + sid, timeout ? "timed out" : body ? *body : "request failed"); @@ -547,7 +543,6 @@ void Core::_send_to_swarm( throw std::logic_error{"_send_to_swarm: no send_to_swarm callback and no network object"}; // Build signed store request. - auto session_id_hex = oxenc::to_hex(globals.session_id()); auto ed25519_hex = globals.pubkey_ed25519().hex(); auto ns_val = static_cast(ns); auto now_ms = epoch_ms(clock_now_ms()); @@ -561,13 +556,13 @@ void Core::_send_to_swarm( } nlohmann::json params = { - {"pubkey", session_id_hex}, + {"pubkey", globals.session_id_hex()}, {"pubkey_ed25519", ed25519_hex}, {"namespace", ns_val}, - {"data", oxenc::to_base64(payload)}, + {"data", "{:b}"_format(payload)}, {"timestamp", now_ms}, {"sig_timestamp", now_ms}, - {"signature", oxenc::to_base64(sig)}, + {"signature", "{:b}"_format(sig)}, {"ttl", ttl.count()}, }; diff --git a/src/core/devices.cpp b/src/core/devices.cpp index df80c1150..ae16378f0 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -43,16 +43,16 @@ static constexpr auto dev_key = "device_unique_id"sv; void Devices::init() { if (core.globals.get_blob_to(dev_key, self_id)) - log::info(cat, "Loaded existing unique device id: {}", oxenc::to_hex(self_id)); + log::info(cat, "Loaded existing unique device id: {}", self_id); else { random::fill(self_id); core.globals.set(dev_key, self_id); - log::info(cat, "Generated new unique device id: {}", oxenc::to_hex(self_id)); + log::info(cat, "Generated new unique device id: {}", self_id); } } std::string Devices::device_id() const { - return oxenc::to_hex(self_id.begin(), self_id.end()); + return oxenc::to_hex(self_id); } template @@ -87,31 +87,13 @@ static Keys keys_from_seed(std::span seed) { namespace { -// Lightweight formattable wrapper for logging a brief "aabb…xxyy" hex summary of a key. The -// hex computation is deferred to when the formatter is invoked, so it is skipped entirely if -// the log level is disabled. -struct key_summary { - std::span key; - - template - requires oxenc::basic_char> - key_summary(const T& k) : key{std::as_bytes(std::span{k})} {} -}; - -std::string format_as(const key_summary& ks) { - return "{}…{}"_format( - oxenc::to_hex(ks.key.begin(), ks.key.begin() + 2), - oxenc::to_hex(ks.key.end() - 2, ks.key.end())); -} - } // namespace // format_as for XWingKeys-derived types (DeviceKeys, AccountKeys), defined in session::core so // that fmtlib's ADL-based lookup can find it when logging these types. template Keys> std::string format_as(const Keys& k) { - return "X25519[{}], MLKEM768[{}]"_format( - key_summary{k.x25519_pub}, key_summary{k.mlkem768_pub}); + return "X25519[{:9.4}], MLKEM768[{:9.4}]"_format(k.x25519_pub, k.mlkem768_pub); } Devices::DeviceKeys Devices::rotate_device_keys() { @@ -811,7 +793,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) for (; i < padded_count; i++) random::fill(ciphertext[i]); - std::array nonce; + std::array nonce; hash::blake2b_key_pers(nonce, A, PERS_DEV_NONCE, ciphertext_raw); cleared_b32 key_base; @@ -831,8 +813,8 @@ std::vector Devices::encrypt_device_data(const device::map& devices) auto plaintext_devices = encode_group_payload(devices, acc_keys); std::vector enc_devices; - enc_devices.resize(plaintext_devices.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); - encrypt::xchacha20poly1305_encrypt(enc_devices, to_span(plaintext_devices), nonce, key_base); + enc_devices.resize(plaintext_devices.size() + encryption::XCHACHA20_ABYTES); + encryption::xchacha20poly1305_encrypt(enc_devices, to_span(plaintext_devices), nonce, key_base); cleared_b32 ki; cleared_b32 aB; @@ -864,7 +846,7 @@ std::vector Devices::encrypt_device_data(const device::map& devices) hash::blake2b_pers(ki, PERS_KEY_KEY, aB, A, B, ml_ss[i], info.pk_mlkem768); static_assert(decltype(ekey)::extent == key_base.size()); - encrypt::xchacha20_xor(ekey, key_base, nonce, ki); + encryption::xchacha20_xor(ekey, key_base, nonce, ki); // Hash a bunch of stuff together as a checksum to let decryption skip most not-for-me // values. @@ -1089,7 +1071,7 @@ std::vector Devices::decrypt_device_data(std::span e throw std::runtime_error{ "Invalid encrypted device data: ciphertext ({}) vs enc key ({}) size mismatch"_format( count, k_count)}; - if (enc_devices.size() <= crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (enc_devices.size() <= encryption::XCHACHA20_ABYTES) throw std::runtime_error{ "Invalid encrypted device data: encrypted data is too short ({}B)"_format( enc_devices.size())}; @@ -1118,7 +1100,7 @@ std::vector Devices::decrypt_device_data(std::span e cleared_b32 ml_ss, aB, ki, key_base; std::vector plaintext_devices; - plaintext_devices.resize(enc_devices.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + plaintext_devices.resize(enc_devices.size() - encryption::XCHACHA20_ABYTES); // Trial decrypt until we find one that works, except that we can skip most of the heavy // operations for most keys not intended for us. Note that we have to attempt each received key @@ -1162,10 +1144,10 @@ std::vector Devices::decrypt_device_data(std::span e // and then use it to recover the key_base: static_assert(decltype(ekey)::extent == key_base.size()); - encrypt::xchacha20_xor(key_base, ekey, knonce, ki); + encryption::xchacha20_xor(key_base, ekey, knonce, ki); // Now we can decrypt the encrypted payload: - if (encrypt::xchacha20poly1305_decrypt( + if (encryption::xchacha20poly1305_decrypt( plaintext_devices, enc_devices, devices_nonce, key_base)) { found = true; break; diff --git a/src/core/globals.cpp b/src/core/globals.cpp index 4119c0041..eac1816d1 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -1,11 +1,13 @@ #include -#include -#include #include +#include +#include + #include #include #include +#include #include #include @@ -130,37 +132,30 @@ void Globals::init() { // FIXME: we should allow full 32-byte seeds here, but for now this is compatible with // the 16-byte/128-bit seed that Session accounts use which is 16 random bytes followed // by 16 0s: - randombytes_buf(seed.data(), 16); + random::fill(std::span{seed}.first<16>()); std::memset(seed.data() + 16, 0, 16); } } + // Layout: [ed25519_sk(64) | x25519_sk(32)] = 96 bytes auto rw = _account_seed.resize(96); - auto* rw_uc = reinterpret_cast(rw.buf.data()); - crypto_sign_ed25519_seed_keypair( - to_unsigned(_pubkey_ed25519.data()), - rw_uc, - reinterpret_cast(seed_to_use->data())); - crypto_sign_ed25519_sk_to_curve25519(rw_uc + 64, rw_uc); + ed25519::seed_keypair(_pubkey_ed25519, rw.buf.first<64>(), *seed_to_use); + ed25519::sk_to_x25519(rw.buf.last<32>(), *seed_to_use); _predefined_seed.reset(); // Clear now that it has been consumed - if (0 != crypto_sign_ed25519_pk_to_curve25519( - to_unsigned(_pubkey_x25519.data()), - to_unsigned(_pubkey_ed25519.data()))) - // This *should* be impossible when starting from a seed because that would mean the seed - // generation produced an invalid Ed pubkey! - log::critical(cat, "Failed to convert seed-extracted Ed25519 pubkey to X25519 session ID!"); + ed25519::pk_to_x25519(_pubkey_x25519, _pubkey_ed25519); _session_id[0] = std::byte{0x05}; std::copy(_pubkey_x25519.begin(), _pubkey_x25519.end(), _session_id.data() + 1); + _session_id_hex = oxenc::to_hex(_session_id); if (!have_seed) { log::info(cat, "Generated new Session account seed"); set("_seed", rw.buf.first(32)); } - log::info(cat, "Initialized with Session ID: {}", oxenc::to_hex(_session_id)); + log::info(cat, "Initialized with Session ID: {}", _session_id_hex); } mnemonics::secure_mnemonic Globals::seed_mnemonic(const mnemonics::Mnemonics& lang, bool force_24) { diff --git a/src/crypto/ed25519.cpp b/src/crypto/ed25519.cpp index ca13757b3..f24a16970 100644 --- a/src/crypto/ed25519.cpp +++ b/src/crypto/ed25519.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "session/export.h" @@ -15,6 +16,17 @@ namespace session::ed25519 { +PrivKeySpan::PrivKeySpan(const std::byte* data, size_t size) { + if (size == 64) + data_ = data; + else if (size == 32) { + expand_seed(std::span{data, 32}); + data_ = storage_->data(); + } else + throw std::invalid_argument{ + "Ed25519 private key must be 32 or 64 bytes (got {})"_format(size)}; +} + void PrivKeySpan::expand_seed(std::span seed) { auto& buf = storage_.emplace(); b32 ignore_pk; @@ -85,10 +97,8 @@ b33 pk_to_session_id(std::span pk) { return sid; } -cleared_b32 sk_to_x25519(std::span seed) { - cleared_b32 xsk; - crypto_sign_ed25519_sk_to_curve25519(to_unsigned(xsk.data()), to_unsigned(seed.data())); - return xsk; +void sk_to_x25519(std::span out, std::span seed) { + crypto_sign_ed25519_sk_to_curve25519(to_unsigned(out.data()), to_unsigned(seed.data())); } std::pair x25519_keypair(const PrivKeySpan& sk) { diff --git a/src/fields.cpp b/src/fields.cpp deleted file mode 100644 index b9b2515f0..000000000 --- a/src/fields.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "session/fields.hpp" - -#include - -#include - -namespace session { - -std::string SessionID::hex() const { - std::string id; - id.reserve(33); - id.push_back(static_cast(netid)); - oxenc::to_hex(pubkey.begin(), pubkey.end(), std::back_inserter(id)); - return id; -} - -} // namespace session diff --git a/src/internal-util.hpp b/src/internal-util.hpp index 05c857bc2..f76ba4980 100644 --- a/src/internal-util.hpp +++ b/src/internal-util.hpp @@ -3,8 +3,11 @@ #include #include +#include #include +using namespace session::literals; + namespace session { // Copies `msg` into `buf`, truncating if necessary, always null-terminating. Returns the number diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 1cfd09e1f..7fc06d238 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -2,10 +2,8 @@ #include #include #include -#include -#include -#include -#include +#include +#include #include #include @@ -16,7 +14,7 @@ namespace session { -const size_t encrypt_multiple_message_overhead = crypto_aead_xchacha20poly1305_ietf_ABYTES; +const size_t encrypt_multiple_message_overhead = encryption::XCHACHA20_ABYTES; namespace detail { @@ -28,11 +26,7 @@ namespace detail { bool encrypting, std::string_view domain) { - b32 buf; - if (0 != crypto_scalarmult_curve25519(to_unsigned(buf.data()), to_unsigned(a.data()), to_unsigned(B.data()))) - throw std::invalid_argument{"Unable to compute shared encrypted key: invalid pubkey?"}; - - static_assert(crypto_aead_xchacha20poly1305_ietf_KEYBYTES == 32); + auto buf = x25519::scalarmult(a, B); // If we're encrypting then a/A == sender, B = recipient // If we're decrypting then a/A = recipient, B = sender @@ -49,8 +43,8 @@ namespace detail { std::span key, std::span nonce) { - out.resize(msg.size() + encrypt::XCHACHA20_ABYTES); - encrypt::xchacha20poly1305_encrypt(out, msg, nonce, key); + out.resize(msg.size() + encryption::XCHACHA20_ABYTES); + encryption::xchacha20poly1305_encrypt(out, msg, nonce, key); } bool decrypt_multi_impl( @@ -59,11 +53,11 @@ namespace detail { std::span key, std::span nonce) { - if (ciphertext.size() < encrypt::XCHACHA20_ABYTES) + if (ciphertext.size() < encryption::XCHACHA20_ABYTES) return false; - out.resize(ciphertext.size() - encrypt::XCHACHA20_ABYTES); - return encrypt::xchacha20poly1305_decrypt(out, ciphertext, nonce, key); + out.resize(ciphertext.size() - encryption::XCHACHA20_ABYTES); + return encryption::xchacha20poly1305_decrypt(out, ciphertext, nonce, key); } } // namespace detail @@ -101,10 +95,10 @@ std::vector encrypt_for_multiple_simple( oxenc::bt_dict_producer d; - std::array random_nonce; + std::array random_nonce; if (!nonce) { - randombytes_buf(random_nonce.data(), random_nonce.size()); - nonce.emplace(to_byte_span<24>(random_nonce.data())); + random::fill(random_nonce); + nonce.emplace(random_nonce); } else if (nonce->size() != 24) { throw std::invalid_argument{"Invalid nonce: nonce must be 24 bytes"}; } @@ -130,7 +124,7 @@ std::vector encrypt_for_multiple_simple( std::vector junk; junk.resize(pad_size); for (; msg_count % pad != 0; msg_count++) { - randombytes_buf(junk.data(), pad_size); + random::fill(junk); enc_list.append(to_string(junk)); } } diff --git a/src/network/backends/session_file_server.cpp b/src/network/backends/session_file_server.cpp index b8927d037..a76d5ba9c 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "../session_network_internal.hpp" #include "session/blinding.hpp" @@ -120,10 +121,8 @@ std::optional parse_download_url(std::string_view url) { return info; } -const std::string QUIC_FS_SESH_ADDRESS_MAINNET = - oxenc::to_base32z(QUIC_FS_ED_PUBKEY_MAINNET) + ".sesh"; -const std::string QUIC_FS_SESH_ADDRESS_TESTNET = - oxenc::to_base32z(QUIC_FS_ED_PUBKEY_TESTNET) + ".sesh"; +const std::string QUIC_FS_SESH_ADDRESS_MAINNET = "{:a}.sesh"_format(QUIC_FS_ED_PUBKEY_MAINNET); +const std::string QUIC_FS_SESH_ADDRESS_TESTNET = "{:a}.sesh"_format(QUIC_FS_ED_PUBKEY_TESTNET); std::optional default_quic_target( const config::FileServer& http_config, opt::netid::Target netid) { @@ -359,18 +358,12 @@ Request get_client_version( auto timestamp = epoch_seconds(clock_now_s()); auto signature = blind_version_sign(sk, platform, timestamp); auto pubkey = x25519_pubkey::from_hex(DEFAULT_CONFIG.pubkey_hex); - std::string blinded_pk_hex; - blinded_pk_hex.reserve(66); - blinded_pk_hex += "07"; - oxenc::to_hex( - blinded_keys.first.begin(), - blinded_keys.first.end(), - std::back_inserter(blinded_pk_hex)); + auto blinded_pk_hex = "07{:x}"_format(blinded_keys.first); auto headers = std::vector>{}; headers.emplace_back(HEADER_PUBKEY, blinded_pk_hex); headers.emplace_back(HEADER_TIMESTAMP, "{}"_format(timestamp)); - headers.emplace_back(HEADER_SIGNATURE, oxenc::to_base64(signature.begin(), signature.end())); + headers.emplace_back(HEADER_SIGNATURE, oxenc::to_base64(signature)); return Request{ random::unique_id("GCV"), diff --git a/src/network/key_types.cpp b/src/network/key_types.cpp index fcfa76644..120940ac1 100644 --- a/src/network/key_types.cpp +++ b/src/network/key_types.cpp @@ -3,9 +3,10 @@ #include #include #include -#include - #include +#include +#include +#include #include namespace session::network { @@ -17,16 +18,16 @@ namespace detail { throw std::runtime_error{"Hex key data is invalid: data is not hex"}; if (hex.size() != 2 * length) throw std::runtime_error{ - "Hex key data is invalid: expected " + std::to_string(length) + - " hex digits, received " + std::to_string(hex.size())}; + "Hex key data is invalid: expected {} hex digits, received {}"_format( + length, hex.size())}; oxenc::from_hex(hex.begin(), hex.end(), reinterpret_cast(buffer)); } void load_from_bytes(void* buffer, size_t length, std::string_view bytes) { if (bytes.size() != length) throw std::runtime_error{ - "Key data is invalid: expected " + std::to_string(length) + - " bytes, received " + std::to_string(bytes.size())}; + "Key data is invalid: expected {} bytes, received {}"_format( + length, bytes.size())}; std::memmove(buffer, bytes.data(), length); } @@ -40,17 +41,17 @@ std::string ed25519_pubkey::snode_address() const { legacy_pubkey legacy_seckey::pubkey() const { legacy_pubkey pk; - crypto_scalarmult_ed25519_base_noclamp(to_unsigned(pk.data()), to_unsigned(data())); + ed25519::scalarmult_base_noclamp(pk, *this); return pk; }; ed25519_pubkey ed25519_seckey::pubkey() const { ed25519_pubkey pk; - crypto_sign_ed25519_sk_to_pk(to_unsigned(pk.data()), to_unsigned(data())); + ed25519::sk_to_pk(pk, ed25519::PrivKeySpan::from(*this)); return pk; }; x25519_pubkey x25519_seckey::pubkey() const { x25519_pubkey pk; - crypto_scalarmult_curve25519_base(to_unsigned(pk.data()), to_unsigned(data())); + x25519::scalarmult_base(pk, *this); return pk; }; @@ -82,12 +83,7 @@ x25519_pubkey parse_x25519_pubkey(std::string_view pubkey_in) { return parse_pubkey(pubkey_in); } x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk) { - b32 xpk; - if (0 != crypto_sign_ed25519_pk_to_curve25519(to_unsigned(xpk.data()), to_unsigned(ed25519_pk.data()))) - throw std::runtime_error{ - "An error occured while attempting to convert Ed25519 pubkey to X25519; " - "is the pubkey valid?"}; - return x25519_pubkey::from_bytes(xpk); + return x25519_pubkey::from_bytes(ed25519::pk_to_x25519(ed25519_pk)); } } // namespace session::network diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index 648fa3156..f728a188e 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -63,7 +63,7 @@ EncryptType parse_enc_type(std::string_view enc_type) { return EncryptType::xchacha20; if (enc_type == "aes-gcm" || enc_type == "gcm") return EncryptType::aes_gcm; - throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; + throw std::runtime_error{"Invalid encryption type {}"_format(enc_type)}; } Builder Builder::make( diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index bd049b57c..5c6dcc02f 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "session/export.h" @@ -181,7 +182,8 @@ LIBSESSION_C_API bool onion_request_decrypt( break; default: - throw std::runtime_error{"Invalid decryption type " + std::to_string(enc_type_)}; + throw std::runtime_error{ + "Invalid decryption type {}"_format(static_cast(enc_type_))}; } session::onionreq::HopEncryption d{ diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index b311950f8..8119603fb 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -1239,15 +1239,17 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r size_t payment_tx_payment_id_len, const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; - std::chrono::sys_time refund_requested_unix_ts{ - std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); - session_pro_backend_signature result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + std::chrono::sys_time unix_ts{ + std::chrono::milliseconds(unix_ts_ms)}; + std::chrono::sys_time refund_requested_unix_ts{ + std::chrono::milliseconds(refund_requested_unix_ts_ms)}; + auto payment_tx_payment_id_span = + to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = + to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto sig = SetPaymentRefundRequestedRequest::build_sig( request_version, master_span, @@ -1277,15 +1279,17 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( const unsigned char* payment_tx_order_id, size_t payment_tx_order_id_len) { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time unix_ts{std::chrono::milliseconds(unix_ts_ms)}; - std::chrono::sys_time refund_requested_unix_ts{ - std::chrono::milliseconds(refund_requested_unix_ts_ms)}; - auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); - session_pro_backend_to_json result = {}; try { + ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; + std::chrono::sys_time unix_ts{ + std::chrono::milliseconds(unix_ts_ms)}; + std::chrono::sys_time refund_requested_unix_ts{ + std::chrono::milliseconds(refund_requested_unix_ts_ms)}; + auto payment_tx_payment_id_span = + to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_order_id_span = + to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto json = SetPaymentRefundRequestedRequest::build_to_json( request_version, master_span, diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 45f83698f..9e0df3cb9 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -6,8 +6,6 @@ #include #include #include -#include -#include #include #include @@ -86,8 +84,8 @@ constexpr auto V2_XWING_LABEL = // constexpr auto V2_SS_DOMAIN = "SessionV2MessageSS"_bytes; // Shared v2 wire-format layout constants (used in both encrypt and decrypt) -static constexpr size_t V2_AEAD_OVERHEAD = crypto_aead_xchacha20poly1305_ietf_ABYTES; -static constexpr size_t V2_NONCE_SIZE = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; +static constexpr size_t V2_AEAD_OVERHEAD = encryption::XCHACHA20_ABYTES; +static constexpr size_t V2_NONCE_SIZE = encryption::XCHACHA20_NONCEBYTES; static constexpr size_t V2_HEADER_SIZE = 2 + 2 + 32 + mlkem768::CIPHERTEXTBYTES; static constexpr size_t V2_OUTER_OVERHEAD = V2_HEADER_SIZE + V2_AEAD_OVERHEAD; static constexpr size_t V2_MIN_FINAL_SIZE = ((V2_OUTER_OVERHEAD + 256 + 255) / 256) * 256; @@ -109,9 +107,9 @@ static void v2_derive_xwing_key_nonce( std::span ssx, std::span E, std::span X) { - auto ss = hash::sha3_256<32>(key_buf, ssx, E, X, V2_XWING_LABEL); + cleared_b32 ss; + hash::sha3_256(ss, key_buf, ssx, E, X, V2_XWING_LABEL); hash::shake256(V2_SS_DOMAIN, ss)(key_buf, nonce_out); - sodium_memzero(ss.data(), ss.size()); } // Computes the 2-byte Key Indicator Shared Secret (KISS): @@ -173,8 +171,8 @@ std::vector encrypt_for_recipient( // proper 0x05 prefix when present. std::vector result; - result.resize(signed_msg.size() + crypto_box_SEALBYTES); - encrypt::box_seal(result, signed_msg, recipient_pubkey.first<32>()); + result.resize(signed_msg.size() + encryption::BOX_SEALBYTES); + encryption::box_seal(result, signed_msg, recipient_pubkey.first<32>()); return result; } @@ -201,18 +199,18 @@ std::vector encrypt_for_recipient_deterministic( // The nonce for a sealed box is not passed but is implicitly defined as the (unkeyed) blake2b // hash of: // EPH_PUBKEY || RECIPIENT_PUBKEY - std::array nonce; + std::array nonce; hash::blake2b(nonce, eph_pk, recipient_pubkey); // A sealed box is a regular box (using the ephermal keys and nonce), but with the ephemeral // pubkey prepended: - static_assert(crypto_box_SEALBYTES == crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES); + static_assert(encryption::BOX_SEALBYTES == encryption::BOX_PUBLICKEYBYTES + encryption::BOX_MACBYTES); std::vector result; - result.resize(crypto_box_SEALBYTES + signed_msg.size()); + result.resize(encryption::BOX_SEALBYTES + signed_msg.size()); std::ranges::copy(eph_pk, result.begin()); - encrypt::box_easy( - std::span{result}.subspan(crypto_box_PUBLICKEYBYTES), + encryption::box_easy( + std::span{result}.subspan(encryption::BOX_PUBLICKEYBYTES), signed_msg, nonce, recipient_pubkey.first<32>(), @@ -291,7 +289,7 @@ static std::vector v2_encrypt_inner( } // In-place AEAD encrypt (libsodium explicitly supports c == m) - encrypt::xchacha20poly1305_encrypt( + encryption::xchacha20poly1305_encrypt( std::span{result}.subspan(V2_HEADER_SIZE), std::span{result}.subspan(V2_HEADER_SIZE, padded_inner_size), enc_nonce, @@ -375,7 +373,7 @@ static DecryptV2Result v2_aead_decrypt_and_parse( size_t enc_size = ciphertext.size() - V2_HEADER_SIZE; std::vector plain(enc_size - V2_AEAD_OVERHEAD); - if (!encrypt::xchacha20poly1305_decrypt( + if (!encryption::xchacha20poly1305_decrypt( plain, ciphertext.subspan(V2_HEADER_SIZE, enc_size), nonce, @@ -634,18 +632,18 @@ std::vector encrypt_for_blinded_recipient( // Layout: version(1) || ciphertext(buf+ABYTES) || nonce(NPUBBYTES) std::vector ciphertext; ciphertext.resize( - 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + 1 + buf.size() + encryption::XCHACHA20_ABYTES + + encryption::XCHACHA20_NONCEBYTES); // Prepend with a version byte, so that the recipient can reliably detect if a future version is // no longer encrypting things the way it expects. ciphertext[0] = std::byte{BLINDED_ENCRYPT_VERSION}; - auto nonce = std::span{ciphertext}.last(); + auto nonce = std::span{ciphertext}.last(); random::fill(nonce); - encrypt::xchacha20poly1305_encrypt( - std::span{ciphertext}.subspan(1, buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES), + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(1, buf.size() + encryption::XCHACHA20_ABYTES), buf, nonce, enc_key); @@ -654,7 +652,7 @@ std::vector encrypt_for_blinded_recipient( } static constexpr size_t GROUPS_ENCRYPT_OVERHEAD = - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES; + encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES; std::vector encrypt_for_group( const ed25519::PrivKeySpan& user_ed25519_privkey, @@ -728,14 +726,14 @@ std::vector encrypt_for_group( std::vector ciphertext; ciphertext.resize(GROUPS_ENCRYPT_OVERHEAD + encoded.size()); - auto nonce = std::span{ciphertext}.first(); + auto nonce = std::span{ciphertext}.first(); random::fill(nonce); - encrypt::xchacha20poly1305_encrypt( - std::span{ciphertext}.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES), + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), to_span(encoded), nonce, - group_enc_key.first()); + group_enc_key.first()); return ciphertext; } @@ -749,10 +747,7 @@ std::pair, std::string> decrypt_incoming_session_id( // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to // the sender session ID - std::string sender_session_id; - sender_session_id.reserve(66); - sender_session_id += "05"; - oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + auto sender_session_id = "05{:x}"_format(sender_x_pk); return {buf, sender_session_id}; } @@ -768,10 +763,7 @@ std::pair, std::string> decrypt_incoming_session_id( // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to // the sender session ID - std::string sender_session_id; - sender_session_id.reserve(66); - sender_session_id += "05"; - oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + auto sender_session_id = "05{:x}"_format(sender_x_pk); return {buf, sender_session_id}; } @@ -788,16 +780,16 @@ std::pair, b32> decrypt_incoming( std::span x25519_seckey, std::span ciphertext) { - if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) + if (ciphertext.size() < encryption::BOX_SEALBYTES + 32 + 64) throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; - const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; + const size_t outer_size = ciphertext.size() - encryption::BOX_SEALBYTES; const size_t msg_size = outer_size - 32 - 64; std::pair, b32> result; auto& [buf, sender_ed_pk] = result; buf.resize(outer_size); - if (!encrypt::box_seal_open(buf, ciphertext, x25519_pubkey, x25519_seckey)) + if (!encryption::box_seal_open(buf, ciphertext, x25519_pubkey, x25519_seckey)) throw std::runtime_error{"Decryption failed"}; auto tail = std::span{buf}.subspan(msg_size); // A(32) || SIG(64) @@ -823,8 +815,8 @@ std::pair, std::string> decrypt_from_blinded_recipient( std::span recipient_id, std::span ciphertext) { auto ed_pk = ed25519_privkey.pubkey(); - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + - crypto_aead_xchacha20poly1305_ietf_ABYTES) + if (ciphertext.size() < encryption::XCHACHA20_NONCEBYTES + 1 + + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; @@ -844,20 +836,20 @@ std::pair, std::string> decrypt_from_blinded_recipient( // v, ct, nc = data[0], data[1:-24], data[-24:] if (ciphertext[0] != std::byte{BLINDED_ENCRYPT_VERSION}) throw std::invalid_argument{ - "Invalid ciphertext: version is not " + std::to_string(BLINDED_ENCRYPT_VERSION)}; + fmt::format("Invalid ciphertext: version is not {}", BLINDED_ENCRYPT_VERSION)}; const size_t msg_size = - (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + (ciphertext.size() - encryption::XCHACHA20_ABYTES - 1 - + encryption::XCHACHA20_NONCEBYTES); if (msg_size < 32) throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; buf.resize(msg_size); - auto nonce = ciphertext.last(); - if (!encrypt::xchacha20poly1305_decrypt( + auto nonce = ciphertext.last(); + if (!encryption::xchacha20poly1305_decrypt( buf, - ciphertext.subspan(1, msg_size + crypto_aead_xchacha20poly1305_ietf_ABYTES), + ciphertext.subspan(1, msg_size + encryption::XCHACHA20_ABYTES), nonce, dec_key)) throw std::invalid_argument{"Decryption failed"}; @@ -887,15 +879,13 @@ std::pair, std::string> decrypt_from_blinded_recipient( // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix // to the sender session ID buf.resize(buf.size() - 32); - sender_session_id.reserve(66); - sender_session_id += "05"; - oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + sender_session_id = "05{:x}"_format(sender_x_pk); return result; } DecryptGroupMessage decrypt_group_message( - std::span> decrypt_ed25519_privkey_list, + std::span> group_enc_keys, std::span group_ed25519_pubkey, std::span ciphertext) { DecryptGroupMessage result = {}; @@ -903,26 +893,20 @@ DecryptGroupMessage decrypt_group_message( if (ciphertext.size() < GROUPS_ENCRYPT_OVERHEAD) throw std::runtime_error{"ciphertext is too small to be encrypted data"}; - // Note we only use the secret key of the decrypt_ed25519_privkey so we don't care about - // generating the pubkey component if the user only passed in a 32 byte libsodium-style secret - // key. + // Each group encryption key is a 32-byte symmetric XChaCha20-Poly1305 key. Multiple keys + // are tried because the group key rotates and recently-received messages may still be + // encrypted with a pre-rotation key. std::vector plain; - auto nonce = ciphertext.first(); - ciphertext = ciphertext.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - plain.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + auto nonce = ciphertext.first(); + ciphertext = ciphertext.subspan(encryption::XCHACHA20_NONCEBYTES); + plain.resize(ciphertext.size() - encryption::XCHACHA20_ABYTES); bool decrypt_success = false; - for (size_t index = 0; index < decrypt_ed25519_privkey_list.size(); index++) { - const auto& decrypt_ed25519_privkey = decrypt_ed25519_privkey_list[index]; - if (decrypt_ed25519_privkey.size() != 32 && decrypt_ed25519_privkey.size() != 64) - throw std::invalid_argument{"Invalid decrypt_ed25519_privkey: expected 32 or 64 bytes"}; - decrypt_success = encrypt::xchacha20poly1305_decrypt( - plain, - ciphertext, - nonce, - decrypt_ed25519_privkey.first()); + for (size_t index = 0; index < group_enc_keys.size(); index++) { + decrypt_success = encryption::xchacha20poly1305_decrypt( + plain, ciphertext, nonce, group_enc_keys[index]); if (decrypt_success) { res_index = index; break; @@ -952,21 +936,18 @@ DecryptGroupMessage decrypt_group_message( throw std::runtime_error{"group message version tag (\"\") is missing"}; if (auto v = dict.consume_integer(); v != 1) throw std::runtime_error{ - "group message version tag (" + std::to_string(v) + - ") is not compatible (we support v1)"}; + fmt::format("group message version tag ({}) is not compatible (we support v1)", v)}; if (!dict.skip_until("a")) throw std::runtime_error{"missing message author pubkey"}; auto ed_pk = to_span(dict.consume_string_view()); if (ed_pk.size() != 32) throw std::runtime_error{ - "message author pubkey size (" + std::to_string(ed_pk.size()) + ") is invalid"}; + fmt::format("message author pubkey size ({}) is invalid", ed_pk.size())}; auto x_pk = ed25519::pk_to_x25519(ed_pk.first<32>()); - session_id.reserve(66); - session_id += "05"; - oxenc::to_hex(x_pk.begin(), x_pk.end(), std::back_inserter(session_id)); + session_id = "05{:x}"_format(x_pk); std::span raw_data; if (dict.skip_until("d")) { @@ -980,7 +961,7 @@ DecryptGroupMessage decrypt_group_message( auto ed_sig = to_span(dict.consume_string_view()); if (ed_sig.size() != 64) throw std::runtime_error{ - "message signature size (" + std::to_string(ed_sig.size()) + ") is invalid"}; + fmt::format("message signature size ({}) is invalid", ed_sig.size())}; bool compressed = false; if (dict.skip_until("z")) { @@ -1019,8 +1000,8 @@ DecryptGroupMessage decrypt_group_message( } // The old Argon2-based ONS encryption always used an all-zero salt and all-zero secretbox nonce. -static constexpr std::array ONS_ARGON2_SALT = {}; -static constexpr std::array ONS_SECRETBOX_NONCE = {}; +static constexpr std::array ONS_ARGON2_SALT = {}; +static constexpr std::array ONS_SECRETBOX_NONCE = {}; std::string decrypt_ons_response( std::string_view lowercase_name, @@ -1028,7 +1009,7 @@ std::string decrypt_ons_response( std::optional> nonce) { // Handle old Argon2-based encryption used before HF16 if (!nonce) { - if (ciphertext.size() < crypto_secretbox_MACBYTES) + if (ciphertext.size() < encryption::SECRETBOX_MACBYTES) throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; b32 key; @@ -1036,22 +1017,21 @@ std::string decrypt_ons_response( key, {lowercase_name.data(), lowercase_name.size()}, ONS_ARGON2_SALT, - crypto_pwhash_OPSLIMIT_MODERATE, - crypto_pwhash_MEMLIMIT_MODERATE, - crypto_pwhash_ALG_ARGON2ID13); + hash::ARGON2_OPSLIMIT_MODERATE, + hash::ARGON2_MEMLIMIT_MODERATE, + hash::ARGON2ID13); std::vector msg; - msg.resize(ciphertext.size() - crypto_secretbox_MACBYTES); + msg.resize(ciphertext.size() - encryption::SECRETBOX_MACBYTES); - if (!encrypt::secretbox_open_easy(msg, ciphertext, ONS_SECRETBOX_NONCE, key)) + if (!encryption::secretbox_open_easy(msg, ciphertext, ONS_SECRETBOX_NONCE, key)) throw std::runtime_error{"Failed to decrypt"}; - std::string session_id = oxenc::to_hex(msg.begin(), msg.end()); - return session_id; + return oxenc::to_hex(msg); } - static_assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES == 24); - if (ciphertext.size() != 33 + crypto_aead_xchacha20poly1305_ietf_ABYTES) + static_assert(encryption::XCHACHA20_NONCEBYTES == 24); + if (ciphertext.size() != 33 + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{"Invalid ciphertext: expected exactly 49 bytes"}; // Hash the ONS name using BLAKE2b @@ -1063,7 +1043,7 @@ std::string decrypt_ons_response( auto key = hash::blake2b_key<32>(name_hash, lowercase_name); std::array buf; - if (!encrypt::xchacha20poly1305_decrypt(buf, ciphertext, *nonce, key)) + if (!encryption::xchacha20poly1305_decrypt(buf, ciphertext, *nonce, key)) throw std::runtime_error{"Failed to decrypt"}; return oxenc::to_hex(buf); @@ -1072,15 +1052,15 @@ std::string decrypt_ons_response( std::vector decrypt_push_notification( std::span payload, std::span enc_key) { if (payload.size() < - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; - auto nonce = payload.first(); - auto ct = payload.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + auto nonce = payload.first(); + auto ct = payload.subspan(encryption::XCHACHA20_NONCEBYTES); - std::vector buf(ct.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + std::vector buf(ct.size() - encryption::XCHACHA20_ABYTES); - if (!encrypt::xchacha20poly1305_decrypt(buf, ct, nonce, enc_key)) + if (!encryption::xchacha20poly1305_decrypt(buf, ct, nonce, enc_key)) throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; // Removing any null padding bytes from the end @@ -1096,14 +1076,14 @@ std::vector encrypt_xchacha20( std::span plaintext, std::span key) { std::vector ciphertext( - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + plaintext.size() + - crypto_aead_xchacha20poly1305_ietf_ABYTES); + encryption::XCHACHA20_NONCEBYTES + plaintext.size() + + encryption::XCHACHA20_ABYTES); - auto nonce = std::span{ciphertext}.first(); + auto nonce = std::span{ciphertext}.first(); random::fill(nonce); - encrypt::xchacha20poly1305_encrypt( - std::span{ciphertext}.subspan(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES), + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), plaintext, nonce, key); @@ -1113,16 +1093,16 @@ std::vector encrypt_xchacha20( std::vector decrypt_xchacha20( std::span ciphertext, std::span key) { if (ciphertext.size() < - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; // Extract nonce from the beginning of the ciphertext: - auto nonce = ciphertext.first(); + auto nonce = ciphertext.first(); ciphertext = ciphertext.subspan(nonce.size()); - std::vector plaintext(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - if (!encrypt::xchacha20poly1305_decrypt(plaintext, ciphertext, nonce, key)) + std::vector plaintext(ciphertext.size() - encryption::XCHACHA20_ABYTES); + if (!encryption::xchacha20poly1305_decrypt(plaintext, ciphertext, nonce, key)) throw std::runtime_error{"Could not decrypt (XChaCha20-Poly1305)"}; return plaintext; } @@ -1299,11 +1279,15 @@ LIBSESSION_C_API session_decrypt_group_message_result session_decrypt_group_mess size_t error_len) { session_decrypt_group_message_result result = {}; try { - std::vector> keys; + std::vector> keys; keys.reserve(decrypt_ed25519_privkey_len); - for (size_t i = 0; i < decrypt_ed25519_privkey_len; i++) - keys.push_back(to_byte_span( - decrypt_ed25519_privkey_list[i].data, decrypt_ed25519_privkey_list[i].size)); + for (size_t i = 0; i < decrypt_ed25519_privkey_len; i++) { + if (decrypt_ed25519_privkey_list[i].size != 32) + throw std::invalid_argument{fmt::format( + "Invalid group encryption key: expected 32 bytes, got {}", + decrypt_ed25519_privkey_list[i].size)}; + keys.push_back(to_byte_span<32>(decrypt_ed25519_privkey_list[i].data)); + } auto [index, session_id, plaintext] = decrypt_group_message( keys, to_byte_span<32>(group_ed25519_pubkey), @@ -1326,10 +1310,10 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* nonce_in, char* session_id_out) { try { - std::optional> + std::optional> nonce; if (nonce_in) - nonce = to_byte_span(nonce_in); + nonce = to_byte_span(nonce_in); auto session_id = session::decrypt_ons_response(name_in, to_byte_span(ciphertext_in, ciphertext_len), nonce); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index fa223db9c..59a5b3f2c 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -113,8 +113,8 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP namespace session { static_assert(sizeof(std::declval().gen_index_hash) == 32); -static_assert(sizeof(std::declval().rotating_pubkey) == crypto_sign_ed25519_PUBLICKEYBYTES); -static_assert(sizeof(std::declval().sig) == crypto_sign_ed25519_BYTES); +static_assert(sizeof(std::declval().rotating_pubkey) == 32); +static_assert(sizeof(std::declval().sig) == 64); bool ProProof::verify_signature(std::span verify_pubkey) const { return ed25519::verify(sig, verify_pubkey, hash()); @@ -166,8 +166,7 @@ void ProProfileBitset::unset(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { } bool ProProfileBitset::is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) const { - bool result = data & (1ULL << static_cast(features)); - return result; + return data & (1ULL << static_cast(features)); } void ProMessageBitset::set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { @@ -179,8 +178,7 @@ void ProMessageBitset::unset(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { } bool ProMessageBitset::is_set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) const { - bool result = data & (1ULL << static_cast(features)); - return result; + return data & (1ULL << static_cast(features)); } }; // namespace session @@ -615,7 +613,7 @@ DecodedEnvelope decode_dm_envelope( } DecodedEnvelope decode_group_envelope( - std::span> group_keys, + std::span> group_keys, std::span group_ed25519_pubkey, std::span envelope_payload, std::span pro_backend_pubkey) { @@ -744,7 +742,7 @@ DecodedCommunityMessage decode_for_community( // If there was a pro signature in one of the payloads, verify and copy it to our result struct if (pro_sig) { - if (pro_sig->size() != crypto_sign_ed25519_BYTES) + if (pro_sig->size() != 64) throw std::runtime_error( "Decoding community message failed, pro signature has wrong size"); @@ -845,10 +843,8 @@ DecodedCommunityMessage decode_for_community( using namespace session; static_assert((sizeof((session_protocol_pro_proof*)0)->gen_index_hash) == 32); -static_assert( - (sizeof((session_protocol_pro_proof*)0)->rotating_pubkey) == - crypto_sign_ed25519_PUBLICKEYBYTES); -static_assert((sizeof((session_protocol_pro_proof*)0)->sig) == crypto_sign_ed25519_BYTES); +static_assert(sizeof(std::declval().rotating_pubkey) == 32); +static_assert(sizeof(std::declval().sig) == 64); static_assert( SESSION_PROTOCOL_PRO_PROFILE_FEATURES_COUNT <= @@ -858,8 +854,7 @@ static_assert( LIBSESSION_C_API bool session_protocol_pro_profile_bitset_is_set( session_protocol_pro_profile_bitset value, SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - bool result = value.data & (1ULL << features); - return result; + return value.data & (1ULL << features); } LIBSESSION_C_API void session_protocol_pro_profile_bitset_set( @@ -876,8 +871,7 @@ LIBSESSION_C_API void session_protocol_pro_profile_bitset_unset( LIBSESSION_C_API bool session_protocol_pro_message_bitset_is_set( session_protocol_pro_message_bitset value, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - bool result = value.data & (1ULL << features); - return result; + return value.data & (1ULL << features); } LIBSESSION_C_API void session_protocol_pro_message_bitset_set( @@ -1144,11 +1138,15 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( // Groups v2 path: decrypt with group symmetric keys auto group_pk = to_byte_span<32>(keys->group_ed25519_pubkey.data); - std::vector> group_keys; + std::vector> group_keys; group_keys.reserve(keys->decrypt_keys_len); - for (size_t i = 0; i < keys->decrypt_keys_len; i++) - group_keys.emplace_back( - to_byte_span(keys->decrypt_keys[i].data, keys->decrypt_keys[i].size)); + for (size_t i = 0; i < keys->decrypt_keys_len; i++) { + if (keys->decrypt_keys[i].size != 32) + throw std::invalid_argument{fmt::format( + "Invalid group encryption key: expected 32 bytes, got {}", + keys->decrypt_keys[i].size)}; + group_keys.emplace_back(to_byte_span<32>(keys->decrypt_keys[i].data)); + } try { result_cpp = decode_group_envelope( diff --git a/src/util.cpp b/src/util.cpp index 0681f3e70..85e7fb101 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -142,7 +143,7 @@ std::vector zstd_compress( data.size(), level); if (ZSTD_isError(size)) - throw std::runtime_error{"Compression failed: " + std::string{ZSTD_getErrorName(size)}}; + throw std::runtime_error{"Compression failed: {}"_format(ZSTD_getErrorName(size))}; compressed.resize(prefix.size() + size); return compressed; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 19535d8b5..4a354322d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,7 +23,7 @@ set(LIB_SESSION_UTESTS_SOURCES test_curve25519.cpp test_ed25519.cpp test_encrypt.cpp - test_formattable.cpp + test_format.cpp test_group_keys.cpp test_group_info.cpp test_group_members.cpp diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index b43e2a310..6eadb0d1b 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -10,6 +10,7 @@ #include #include +#include "../src/config/internal.hpp" #include "utils.hpp" using namespace session; diff --git a/tests/test_formattable.cpp b/tests/test_format.cpp similarity index 85% rename from tests/test_formattable.cpp rename to tests/test_format.cpp index 004598880..d712c64c2 100644 --- a/tests/test_formattable.cpp +++ b/tests/test_format.cpp @@ -5,15 +5,15 @@ #include #include -#include "session/formattable.hpp" +#include "session/format.hpp" #include "utils.hpp" -TEST_CASE("byte span formatting - default hex", "[formattable]") { +TEST_CASE("byte span formatting - default hex", "[format]") { CHECK(fmt::format("{}", "abcd0123"_hex_b) == "abcd0123"); CHECK(fmt::format("{:x}", "abcd0123"_hex_b) == "abcd0123"); } -TEST_CASE("byte span formatting - various types", "[formattable]") { +TEST_CASE("byte span formatting - various types", "[format]") { auto arr = "deadbeef"_hex_b; SECTION("std::span with static extent") { @@ -37,7 +37,7 @@ TEST_CASE("byte span formatting - various types", "[formattable]") { } } -TEST_CASE("byte span formatting - empty span", "[formattable]") { +TEST_CASE("byte span formatting - empty span", "[format]") { std::span empty; CHECK(fmt::format("{}", empty) == ""); CHECK(fmt::format("{:x}", empty) == ""); @@ -47,7 +47,7 @@ TEST_CASE("byte span formatting - empty span", "[formattable]") { CHECK(fmt::format("{:r}", empty) == ""); } -TEST_CASE("byte span formatting - stripped hex", "[formattable]") { +TEST_CASE("byte span formatting - stripped hex", "[format]") { SECTION("all zeros") { CHECK(fmt::format("{:z}", "00000000"_hex_b) == "0"); } @@ -69,7 +69,7 @@ TEST_CASE("byte span formatting - stripped hex", "[formattable]") { } } -TEST_CASE("byte span formatting - base32z", "[formattable]") { +TEST_CASE("byte span formatting - base32z", "[format]") { auto val = "0001020304"_hex_b; auto hex_result = fmt::format("{:x}", val); auto b32z_result = fmt::format("{:a}", val); @@ -78,16 +78,16 @@ TEST_CASE("byte span formatting - base32z", "[formattable]") { CHECK(b32z_result != hex_result); } -TEST_CASE("byte span formatting - base64", "[formattable]") { +TEST_CASE("byte span formatting - base64", "[format]") { CHECK(fmt::format("{:b}", "00010203"_hex_b) == "AAECAw=="); CHECK(fmt::format("{:B}", "00010203"_hex_b) == "AAECAw"); } -TEST_CASE("byte span formatting - raw", "[formattable]") { +TEST_CASE("byte span formatting - raw", "[format]") { CHECK(fmt::format("{:r}", "6869"_hex_b) == "hi"); } -TEST_CASE("byte span formatting - ellipsis", "[formattable]") { +TEST_CASE("byte span formatting - ellipsis", "[format]") { // 8 bytes = 16 hex chars: "0123456789abcdef" auto val = "0123456789abcdef"_hex_b; CHECK(fmt::format("{}", val) == "0123456789abcdef"); @@ -114,7 +114,7 @@ TEST_CASE("byte span formatting - ellipsis", "[formattable]") { } } -TEST_CASE("byte span formatting - 32 byte key ellipsis", "[formattable]") { +TEST_CASE("byte span formatting - 32 byte key ellipsis", "[format]") { auto key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"_hex_b; auto full = fmt::format("{}", key); CHECK(full.size() == 64); @@ -126,7 +126,7 @@ TEST_CASE("byte span formatting - 32 byte key ellipsis", "[formattable]") { CHECK(ellipsized.size() == 7 + 3 + 4); } -TEST_CASE("byte span formatting - format errors", "[formattable]") { +TEST_CASE("byte span formatting - format errors", "[format]") { auto val = "01"_hex_b; // Use fmt::runtime() to bypass compile-time format string checking @@ -138,7 +138,7 @@ TEST_CASE("byte span formatting - format errors", "[formattable]") { CHECK_THROWS_AS(fmt::format(fmt::runtime("{:1.0}"), val), fmt::format_error); } -TEST_CASE("byte span formatting - _format UDL", "[formattable]") { +TEST_CASE("byte span formatting - _format UDL", "[format]") { using namespace session::literals; auto val = "deadbeef"_hex_b; CHECK("key: {}"_format(val) == "key: deadbeef"); diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 73d8ed416..1d5a06e82 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -35,7 +35,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. for (const auto& k : enc_keys) - ginfo1.add_key(k, false); + ginfo1.add_key(std::span{k}.first<32>(), false); enc_keys.insert( enc_keys.begin(), @@ -46,7 +46,7 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { groups::Info ginfo2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. - ginfo2.add_key(k, false); + ginfo2.add_key(std::span{k}.first<32>(), false); ginfo1.set_name("GROUP Name"); CHECK(ginfo1.is_dirty()); @@ -165,7 +165,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo{ed_pk, std::nullopt, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. - ginfo.add_key(k, false); + ginfo.add_key(std::span{k}.first<32>(), false); REQUIRE_THROWS_WITH( ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); @@ -177,7 +177,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_rw{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. - ginfo_rw.add_key(k, false); + ginfo_rw.add_key(std::span{k}.first<32>(), false); ginfo_rw.set_name("Super Group!!"); CHECK(ginfo_rw.is_dirty()); @@ -200,7 +200,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_rw2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. - ginfo_rw2.add_key(k, false); + ginfo_rw2.add_key(std::span{k}.first<32>(), false); CHECK(ginfo_rw2.merge(merge_configs) == std::unordered_set{{"fakehash1"s}}); CHECK_FALSE(ginfo.needs_push()); @@ -222,7 +222,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_bad1{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys1) // Just for testing, as above. - ginfo_bad1.add_key(k, false); + ginfo_bad1.add_key(std::span{k}.first<32>(), false); ginfo_bad1.merge(merge_configs); ginfo_bad1.set_sig_keys(ed_sk_bad1); @@ -305,7 +305,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo2{ed_pk, std::nullopt, dump}; for (const auto& k : enc_keys1) // Just for testing, as above. - ginfo2.add_key(k, false); + ginfo2.add_key(std::span{k}.first<32>(), false); CHECK(!ginfo.needs_dump()); CHECK(!ginfo2.needs_dump()); @@ -323,7 +323,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { groups::Info ginfo_rw3{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys2) // Just for testing, as above. - ginfo_rw3.add_key(k, false); + ginfo_rw3.add_key(std::span{k}.first<32>(), false); CHECK(ginfo_rw3.merge(merge_configs) == std::unordered_set{{"fakehash23"s}}); CHECK(ginfo_rw3.get_name() == "Super Group 2"); @@ -352,7 +352,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // If we don't have the new "bbb" key loaded yet, this will fail: CHECK(ginfo.merge(merge_configs) == std::unordered_set{}); - ginfo.add_key(enc_keys2.front()); + ginfo.add_key(std::span{enc_keys2.front()}.first<32>()); CHECK(ginfo.merge(merge_configs) == std::unordered_set{{"fakehash7"s}}); auto pic = ginfo.get_profile_pic(); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 93212af56..7dc89009c 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -412,7 +412,7 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { bad_compressed.back() ^= std::byte{0b100}; CHECK_THROWS_WITH( members.back().keys.decrypt_message(bad_compressed), - "unable to decrypt ciphertext with any current group keys; tried 4"); + "unable to decrypt ciphertext with any current group keys"); // Duplicate members[1] from dumps auto& m1b = members.emplace_back( diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 874850674..4745ae99b 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -40,7 +40,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { // This is just for testing: normally you don't load keys manually but just make a groups::Keys // object that loads the keys into the Members object for you. for (const auto& k : enc_keys) - gmem1.add_key(k, false); + gmem1.add_key(std::span{k}.first<32>(), false); enc_keys.insert( enc_keys.begin(), @@ -50,7 +50,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { groups::Members gmem2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. - gmem2.add_key(k, false); + gmem2.add_key(std::span{k}.first<32>(), false); std::vector sids; while (sids.size() < 256) { diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index fbbcf7db9..d0a4d9bcc 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -231,7 +231,7 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim /* 1:# 24:...nonce... */ 3 + 27 + /* 1:e le */ 3 + 2 + /* XX: then data with overhead */ 3 * - (3 + 5 + encrypt::XCHACHA20_ABYTES)); + (3 + 5 + encryption::XCHACHA20_ABYTES)); // If we encrypt again the value should be different (because of the default randomized nonce): CHECK(encrypted != encrypt_for_multiple_simple( diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 4c84e448c..71aff0b45 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -547,7 +547,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { } // Decrypt envelope - span_u8 key = {to_unsigned(group_v2_sk.data()), group_v2_sk.size()}; + span_u8 key = {to_unsigned(group_v2_sk.data()), 32}; session_protocol_decode_envelope_keys decrypt_keys = {}; decrypt_keys.group_ed25519_pubkey = {to_unsigned(group_v2_pk.data()), group_v2_pk.size()}; decrypt_keys.decrypt_keys = &key; From 6559d36656547ad97ae424d1c5f2cc8a2c8a5a05 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 Apr 2026 00:40:11 -0300 Subject: [PATCH 081/193] std::byte refactor: final reformatting --- include/session/blinding.hpp | 7 +- include/session/config.hpp | 10 +-- include/session/config/base.hpp | 7 +- include/session/config/community.hpp | 4 +- include/session/config/contacts.hpp | 3 +- include/session/config/convo_info_volatile.h | 4 +- include/session/config/encrypt.hpp | 4 +- include/session/config/groups/keys.hpp | 8 +- include/session/config/user_groups.hpp | 4 +- include/session/core/callbacks.hpp | 18 ++--- include/session/core/swarm_message.hpp | 2 +- include/session/crypto/ed25519.hpp | 25 +++--- include/session/encrypt.hpp | 23 ++++-- include/session/fields.hpp | 1 - include/session/format.hpp | 3 +- include/session/hash.hpp | 7 +- include/session/multi_encrypt.hpp | 16 ++-- include/session/onionreq/builder.hpp | 5 +- include/session/session_encrypt.hpp | 6 +- include/session/session_protocol.hpp | 4 +- include/session/sodium_array.hpp | 10 +-- include/session/util.hpp | 11 +-- src/attachments.cpp | 3 +- src/blinding.cpp | 42 +++++----- src/config/base.cpp | 33 +++----- src/config/community.cpp | 6 +- src/config/contacts.cpp | 1 - src/config/convo_info_volatile.cpp | 6 +- src/config/encrypt.cpp | 2 +- src/config/groups/keys.cpp | 70 +++++++---------- src/config/internal.hpp | 5 +- src/config/pro.cpp | 5 +- src/config/protos.cpp | 2 +- src/config/user_groups.cpp | 4 +- src/config/user_profile.cpp | 3 +- src/core.cpp | 15 ++-- src/core/devices.cpp | 48 ++++++------ src/core/globals.cpp | 5 +- src/crypto/ed25519.cpp | 12 +-- src/crypto/x25519.cpp | 8 +- src/curve25519.cpp | 5 +- src/hash.cpp | 2 +- src/logging.cpp | 2 +- src/multi_encrypt.cpp | 7 +- src/network/backends/quic_file_client.cpp | 14 ++-- src/network/key_types.cpp | 6 +- src/network/routing/onion_request_router.cpp | 26 ++----- src/network/routing/session_router_router.cpp | 30 ++++---- src/network/transport/quic_transport.cpp | 4 +- src/onionreq/builder.cpp | 3 +- src/onionreq/hop_encryption.cpp | 5 +- src/onionreq/parser.cpp | 3 +- src/onionreq/response_parser.cpp | 2 +- src/pro_backend.cpp | 12 +-- src/random.cpp | 2 +- src/session_encrypt.cpp | 65 ++++++---------- src/session_protocol.cpp | 33 ++++---- src/util.cpp | 2 +- src/xed25519-tweetnacl.cpp | 4 +- src/xed25519.cpp | 2 +- tests/live/live_utils.hpp | 3 +- tests/quic-files.cpp | 20 +++-- tests/swarm-auth-test.cpp | 6 +- tests/test_blinding.cpp | 9 +-- tests/test_config_contacts.cpp | 5 +- tests/test_config_convo_info_volatile.cpp | 6 +- tests/test_config_local.cpp | 2 +- tests/test_config_pro.cpp | 3 +- tests/test_config_user_groups.cpp | 13 ++-- tests/test_configdata.cpp | 10 +-- tests/test_ed25519.cpp | 9 ++- tests/test_group_info.cpp | 31 ++++---- tests/test_group_members.cpp | 12 ++- tests/test_hash.cpp | 6 +- tests/test_multi_encrypt.cpp | 77 +++++-------------- tests/test_onion_request_router.cpp | 2 +- tests/test_session_encrypt.cpp | 26 +++++-- tests/test_session_protocol.cpp | 13 +++- tests/test_xed25519.cpp | 33 +++++--- tests/utils.hpp | 1 - 80 files changed, 431 insertions(+), 537 deletions(-) diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index a42122e76..e4d90de7d 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -65,8 +65,7 @@ b32 blind15_factor(std::span server_pk); /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is /// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session /// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). -b32 blind25_factor( - std::span session_id, std::span server_pk); +b32 blind25_factor(std::span session_id, std::span server_pk); /// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted /// and returned are hex-encoded. @@ -140,8 +139,8 @@ std::pair blind15_key_pair( /// /// Can optionally also return the blinding factor, k', by providing a pointer to a b32; if /// non-nullptr then k' will be written to it, where k' = ±k. Here, `k'` can be negative to cancel -/// out a negative in the true pubkey, which the remote client will always assume is not present when -/// it does a Session ID -> Ed25519 conversion for blinding purposes. +/// out a negative in the true pubkey, which the remote client will always assume is not present +/// when it does a Session ID -> Ed25519 conversion for blinding purposes. /// /// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. diff --git a/include/session/config.hpp b/include/session/config.hpp index 29a744913..f0129ed94 100644 --- a/include/session/config.hpp +++ b/include/session/config.hpp @@ -125,8 +125,7 @@ class ConfigMessage { std::span data, std::span signature)>; /// Signing function: this is passed the data to be signed and returns the 64-byte signature. - using sign_callable = - std::function(std::span data)>; + using sign_callable = std::function(std::span data)>; ConfigMessage(); ConfigMessage(const ConfigMessage&) = default; @@ -230,9 +229,7 @@ class ConfigMessage { /// verified signature when it was parsed. Returns nullopt otherwise (e.g. not loaded from /// verification at all; loaded without a verification function; or had no signature and a /// signature wasn't required). - const std::optional& verified_signature() { - return verified_signature_; - } + const std::optional& verified_signature() { return verified_signature_; } /// Constructs a new MutableConfigMessage from this config message with an incremented seqno. /// The new config message's diff will reflect changes made after this construction. @@ -249,8 +246,7 @@ class ConfigMessage { virtual std::vector serialize(bool enable_signing = true); protected: - std::vector serialize_impl( - const oxenc::bt_dict& diff, bool enable_signing = true); + std::vector serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true); }; // Constructor tag diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index dc18673da..a6991a8e1 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -175,12 +175,11 @@ class ConfigBase : public ConfigSig { std::unordered_set _old_hashes; struct PartialMessage { - int index; // 0-based index of this part - std::string message_id; // storage server message hash of this part + int index; // 0-based index of this part + std::string message_id; // storage server message hash of this part std::vector data; // Data chunk - PartialMessage( - int index, std::string_view message_id, std::span data) : + PartialMessage(int index, std::string_view message_id, std::span data) : index{index}, message_id{message_id}, data{data.begin(), data.end()} {} }; struct PartialMessages { diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index 6803ff670..ee2f3f582 100644 --- a/include/session/config/community.hpp +++ b/include/session/config/community.hpp @@ -199,9 +199,7 @@ struct community { /// Outputs: /// - `std::string` -- Returns the Full URL static std::string full_url( - std::string_view base_url, - std::string_view room, - std::span pubkey); + std::string_view base_url, std::string_view room, std::span pubkey); /// API: community/community::canonical_url /// diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index b777c3bc9..53c8a2eef 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -482,8 +482,7 @@ class Contacts : public ConfigBase { protected: // Drills into the nested dicts to access community details DictFieldProxy blinded_contact_field( - const blinded_contact_info& bc, - std::span* get_pubkey = nullptr) const; + const blinded_contact_info& bc, std::span* get_pubkey = nullptr) const; public: /// API: contacts/Contacts::blinded diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index f211ea6e1..b00c14e85 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -15,7 +15,7 @@ typedef struct convo_info_volatile_1to1 { bool unread; // true if the conversation is explicitly marked unread bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to // Session Pro features is valid to } convo_info_volatile_1to1; @@ -52,7 +52,7 @@ typedef struct convo_info_volatile_blinded_1to1 { bool unread; // true if the conversation is explicitly marked unread bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to // Session Pro features is valid to } convo_info_volatile_blinded_1to1; diff --git a/include/session/config/encrypt.hpp b/include/session/config/encrypt.hpp index 4f54ca0ea..709df747f 100644 --- a/include/session/config/encrypt.hpp +++ b/include/session/config/encrypt.hpp @@ -67,9 +67,7 @@ void encrypt_inplace( /// - `key_base` -- Fixed key that all clients, must be 32 bytes. /// - `domain` -- short string for the keyed hash void encrypt_prealloced( - std::span message, - std::span key_base, - std::string_view domain); + std::span message, std::span key_base, std::string_view domain); /// API: encrypt/ENCRYPT_DATA_OVERHEAD /// diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index 786d5d759..433ae5ed1 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -436,9 +436,7 @@ class Keys : public ConfigSig { bool write = false, bool del = false); bool swarm_verify_subaccount( - std::span signing_value, - bool write = false, - bool del = false) const; + std::span signing_value, bool write = false, bool del = false) const; /// API: groups/Keys::swarm_auth /// @@ -687,9 +685,7 @@ class Keys : public ConfigSig { /// Outputs: /// - `ciphertext` -- the encrypted, etc. value to send to the swarm std::vector encrypt_message( - std::span plaintext, - bool compress = true, - size_t padding = 256) const; + std::span plaintext, bool compress = true, size_t padding = 256) const; /// API: groups/Keys::decrypt_message /// diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 8b354cc1a..183d02076 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -100,8 +100,8 @@ struct base_group_info { /// Struct containing legacy group info (aka "groups"). struct legacy_group_info : base_group_info { std::string session_id; // The legacy group "session id" (33 bytes). - std::vector enc_pubkey; // bytes (32 or empty) - std::vector enc_seckey; // bytes (32 or empty) + std::vector enc_pubkey; // bytes (32 or empty) + std::vector enc_seckey; // bytes (32 or empty) std::chrono::seconds disappearing_timer{0}; // 0 == disabled. /// Constructs a new legacy group info from an id (which must look like a session_id). Throws diff --git a/include/session/core/callbacks.hpp b/include/session/core/callbacks.hpp index 3a1dc58e3..8a2825d00 100644 --- a/include/session/core/callbacks.hpp +++ b/include/session/core/callbacks.hpp @@ -45,15 +45,15 @@ enum class MessageDecryptFailure { /// A successfully decrypted one-to-one message from Namespace::Default. struct ReceivedMessage { - std::string hash; ///< Swarm-assigned message hash - sys_ms timestamp; ///< Server-reported upload timestamp - sys_ms expiry; ///< Server-reported expiry timestamp - b33 sender_session_id; ///< 0x05-prefixed sender session ID - int version; ///< Protocol version: 1 or 2 - std::vector content; ///< Decrypted protobuf-encoded payload - std::optional pro_signature; ///< Session Pro signature, if present - bool pfs_encrypted = false; ///< True iff decrypted via PFS+PQ (X-Wing) key derivation; - ///< false for v1 messages and v2 non-PFS fallback messages. + std::string hash; ///< Swarm-assigned message hash + sys_ms timestamp; ///< Server-reported upload timestamp + sys_ms expiry; ///< Server-reported expiry timestamp + b33 sender_session_id; ///< 0x05-prefixed sender session ID + int version; ///< Protocol version: 1 or 2 + std::vector content; ///< Decrypted protobuf-encoded payload + std::optional pro_signature; ///< Session Pro signature, if present + bool pfs_encrypted = false; ///< True iff decrypted via PFS+PQ (X-Wing) key derivation; + ///< false for v1 messages and v2 non-PFS fallback messages. }; /// Status of a send operation initiated by Core::send_dm(). diff --git a/include/session/core/swarm_message.hpp b/include/session/core/swarm_message.hpp index 679989f55..371ecbe60 100644 --- a/include/session/core/swarm_message.hpp +++ b/include/session/core/swarm_message.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include diff --git a/include/session/crypto/ed25519.hpp b/include/session/crypto/ed25519.hpp index a44dcd4d8..bdb9cf8c3 100644 --- a/include/session/crypto/ed25519.hpp +++ b/include/session/crypto/ed25519.hpp @@ -20,11 +20,10 @@ namespace session::ed25519 { /// Concept for types that implicitly convert to a 32- or 64-byte byte/unsigned-char span. /// Used by PrivKeySpan and OptionalPrivKeySpan to accept Ed25519 seeds and full keys. template -concept Ed25519KeySpannable = - std::convertible_to> || - std::convertible_to> || - std::convertible_to> || - std::convertible_to>; +concept Ed25519KeySpannable = std::convertible_to> || + std::convertible_to> || + std::convertible_to> || + std::convertible_to>; struct PrivKeySpan { template @@ -50,12 +49,8 @@ struct PrivKeySpan { PrivKeySpan{reinterpret_cast(data), size} {} // Named factory for dynamic-span input (runtime size check, throws if not 32 or 64). - static PrivKeySpan from(std::span key) { - return {key.data(), key.size()}; - } - static PrivKeySpan from(std::span key) { - return {key.data(), key.size()}; - } + static PrivKeySpan from(std::span key) { return {key.data(), key.size()}; } + static PrivKeySpan from(std::span key) { return {key.data(), key.size()}; } PrivKeySpan(const PrivKeySpan&) = delete; PrivKeySpan& operator=(const PrivKeySpan&) = delete; @@ -159,7 +154,10 @@ inline std::span extract_seed( /// - The 64-byte ed25519 signature /// /// Write-to-output form. -void sign(std::span sig, const PrivKeySpan& ed25519_privkey, std::span msg); +void sign( + std::span sig, + const PrivKeySpan& ed25519_privkey, + std::span msg); /// Return-value form. b64 sign(const PrivKeySpan& ed25519_privkey, std::span msg); @@ -262,8 +260,7 @@ void scalarmult_noclamp( std::span scalar, std::span point); /// Return-value form. -b32 scalarmult_noclamp( - std::span scalar, std::span point); +b32 scalarmult_noclamp(std::span scalar, std::span point); /// Reduces a 64-byte scalar modulo the Ed25519 group order to 32 bytes. /// Write-to-output form: result written into `out`. diff --git a/include/session/encrypt.hpp b/include/session/encrypt.hpp index 2c5bb5800..d396be799 100644 --- a/include/session/encrypt.hpp +++ b/include/session/encrypt.hpp @@ -42,8 +42,15 @@ inline void xchacha20poly1305_encrypt( std::span nonce, std::span key) { crypto_aead_xchacha20poly1305_ietf_encrypt( - ucdata(out), nullptr, ucdata(msg), msg.size(), nullptr, 0, nullptr, - ucdata(nonce), ucdata(key)); + ucdata(out), + nullptr, + ucdata(msg), + msg.size(), + nullptr, + 0, + nullptr, + ucdata(nonce), + ucdata(key)); } /// Decrypts `ciphertext` with `key` and `nonce`, writing plaintext (ciphertext.size() - ABYTES @@ -54,9 +61,15 @@ inline bool xchacha20poly1305_decrypt( std::span nonce, std::span key) { return 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - ucdata(out), nullptr, nullptr, - ucdata(ciphertext), ciphertext.size(), nullptr, 0, - ucdata(nonce), ucdata(key)); + ucdata(out), + nullptr, + nullptr, + ucdata(ciphertext), + ciphertext.size(), + nullptr, + 0, + ucdata(nonce), + ucdata(key)); } // ─── XChaCha20 stream ──────────────────────────────────────────────────────── diff --git a/include/session/fields.hpp b/include/session/fields.hpp index 46cba9deb..e70c50272 100644 --- a/include/session/fields.hpp +++ b/include/session/fields.hpp @@ -30,5 +30,4 @@ struct Disappearing { std::chrono::seconds timer = 0s; }; - } // namespace session diff --git a/include/session/format.hpp b/include/session/format.hpp index 5b2a99363..24e73ccbb 100644 --- a/include/session/format.hpp +++ b/include/session/format.hpp @@ -44,7 +44,8 @@ namespace fmt { // Disable fmt's generic range formatter for byte spans so that our byte_spannable formatter takes // precedence (avoids ambiguity when fmt/ranges.h is also included). template -struct range_format_kind : std::integral_constant {}; +struct range_format_kind + : std::integral_constant {}; /// Generic formatter for any byte_spannable type (std::span, std::array, std::vector of std::byte). /// diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 275809a00..6c5df3308 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -239,7 +239,8 @@ struct blake2b_hasher { template requires(sizeof...(T) > 0) void blake2b_key(Out& out, const Key& key, const T&... args) { - blake2b_hasher>{key, std::nullopt}.update(args...).finalize(out); + blake2b_hasher>{key, std::nullopt}.update(args...).finalize( + out); } template requires(sizeof...(T) > 0 && N >= 1 && N <= 64) @@ -532,7 +533,7 @@ namespace session { inline namespace literals { template requires(Lit.size == 16) consteval auto operator""_b2b_pers() { - return operator""_bytes(); + return operator""_bytes < Lit>(); } -} } // namespace session::literals +}} // namespace session::literals diff --git a/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index cf92b11ee..5c93a0152 100644 --- a/include/session/multi_encrypt.hpp +++ b/include/session/multi_encrypt.hpp @@ -98,8 +98,8 @@ extern const size_t encrypt_multiple_message_overhead; /// used to generate individual keys for domain separation, and so should ideally have a different /// value in different contexts (i.e. group keys uses one value, kicked messages use another, /// etc.). *Can* be empty, but should be set to something. -/// - `call` -- this is invoked for each different encrypted value with a std::span; -/// the caller must copy as needed as the span doesn't remain valid past the call. +/// - `call` -- this is invoked for each different encrypted value with a std::span; the caller must copy as needed as the span doesn't remain valid past the call. /// - `ignore_invalid_recipient` -- if given and true then any recipients that appear to have /// invalid public keys (i.e. the shared key multiplication fails) will be silently ignored (the /// callback will not be called). If not given (or false) then such a failure for any recipient @@ -188,12 +188,14 @@ void encrypt_for_multiple(std::string_view message, Args&&... args) { template < typename NextCiphertext, typename = std::enable_if_t< - std::is_invocable_r_v< - std::optional>, - NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || std::is_invocable_r_v>, NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext> || // legacy - std::is_invocable_r_v>, NextCiphertext> || // legacy + std::is_invocable_r_v< + std::optional>, + NextCiphertext> || // legacy + std::is_invocable_r_v< + std::optional>, + NextCiphertext> || // legacy std::is_invocable_r_v, NextCiphertext> || std::is_invocable_r_v, NextCiphertext>>> std::optional> decrypt_for_multiple( diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 71a4486d1..3ebda64f8 100644 --- a/include/session/onionreq/builder.hpp +++ b/include/session/onionreq/builder.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include #include @@ -93,8 +93,7 @@ class Builder { std::optional>> headers_ = std::nullopt; std::optional>> query_params_ = std::nullopt; - std::vector _generate_payload( - std::optional> body) const; + std::vector _generate_payload(std::optional> body) const; }; } // namespace session::onionreq diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index 23ab4486f..243039c1e 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -167,9 +167,9 @@ struct DecryptV2Error : std::runtime_error { /// Result of decrypt_incoming_v2. struct DecryptV2Result { - std::vector content; ///< Decrypted message content. - b33 sender_session_id; ///< 05-prefixed Session ID of the sender. - std::optional pro_signature; ///< Pro sig, if present. + std::vector content; ///< Decrypted message content. + b33 sender_session_id; ///< 05-prefixed Session ID of the sender. + std::optional pro_signature; ///< Pro sig, if present. }; /// API: crypto/decrypt_incoming_v2_prefix diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 9efa38b5a..8febafefd 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -133,8 +133,7 @@ class ProProof { /// /// Outputs: /// - `bool` - True if the message was signed by the embedded `rotating_pubkey` false otherwise. - bool verify_message( - std::span sig, std::span msg) const; + bool verify_message(std::span sig, std::span msg) const; /// API: pro/Proof::is_active /// @@ -286,7 +285,6 @@ struct DecodedCommunityMessage { std::optional pro; }; - /// API: session_protocol/pro_features_for_utf8 /// /// Determine the Pro features that are used in a given conversation message. diff --git a/include/session/sodium_array.hpp b/include/session/sodium_array.hpp index 66da66226..841799e8f 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -11,7 +11,6 @@ void sodium_buffer_deallocate(void* p); // Calls sodium_memzero to zero a buffer void sodium_zero_buffer(void* ptr, size_t size); - // Wrapper around a type that uses `sodium_memzero` to zero the container on destruction; may only // be used with trivially destructible types. template >> @@ -38,7 +37,6 @@ using cleared_bytes = cleared_array; using cleared_b32 = cleared_bytes<32>; using cleared_b64 = cleared_bytes<64>; - // sodium Allocator wrapper; this allocates/frees via libsodium, which is designed for dealing with // sensitive data. It is as a result slower and has more overhead than a standard allocator and // intended for use with a container (such as std::vector) when storing keys. @@ -72,9 +70,7 @@ template struct clearing_allocator { using value_type = T; - [[nodiscard]] static T* allocate(std::size_t n) { - return std::allocator{}.allocate(n); - } + [[nodiscard]] static T* allocate(std::size_t n) { return std::allocator{}.allocate(n); } static void deallocate(T* p, std::size_t n) { sodium_zero_buffer(p, n * sizeof(T)); @@ -82,7 +78,9 @@ struct clearing_allocator { } template - bool operator==(const clearing_allocator&) const noexcept { return true; } + bool operator==(const clearing_allocator&) const noexcept { + return true; + } }; /// Vector that zeros its buffer on deallocation (including when resizing). Lighter weight diff --git a/include/session/util.hpp b/include/session/util.hpp index f2be5def8..0ff1d3614 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -92,9 +92,7 @@ template inline std::array to_array(std::span sp) { std::array result{}; std::copy_n( - reinterpret_cast(sp.data()), - std::min(N, sp.size()), - result.begin()); + reinterpret_cast(sp.data()), std::min(N, sp.size()), result.begin()); return result; } @@ -239,8 +237,7 @@ std::vector> to_view_vector(It begin, It end) { vec.reserve(std::distance(begin, end)); for (; begin != end; ++begin) { if constexpr (std::is_same_v, char*>) // C strings - vec.emplace_back(reinterpret_cast(*begin), - std::strlen(*begin)); + vec.emplace_back(reinterpret_cast(*begin), std::strlen(*begin)); else { static_assert( sizeof(*begin->data()) == 1, @@ -381,9 +378,7 @@ static_assert(std::is_same_v< /// ZSTD-compresses a value. `prefix` can be prepended on the returned value, if needed. Throws on /// serious error. std::vector zstd_compress( - std::span data, - int level = 1, - std::span prefix = {}); + std::span data, int level = 1, std::span prefix = {}); /// ZSTD-decompresses a value. Returns nullopt if decompression fails. If max_size is non-zero /// then this returns nullopt if the decompressed size would exceed that limit. diff --git a/src/attachments.cpp b/src/attachments.cpp index 55b249a62..256b1a80b 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -110,8 +110,7 @@ secretstream_xchacha20poly1305_init_push_with_nonce( crypto_secretstream_xchacha20poly1305_state st; std::memcpy(header.data(), nonce.data(), ENCRYPT_HEADER); - crypto_core_hchacha20( - st.k, to_unsigned(header.data()), to_unsigned(key.data()), nullptr); + crypto_core_hchacha20(st.k, to_unsigned(header.data()), to_unsigned(key.data()), nullptr); static_assert(sizeof(st) == 52); std::memset(st.nonce, 0, 4 /*crypto_secretstream_xchacha20poly1305_COUNTERBYTES*/); st.nonce[0] = 1; diff --git a/src/blinding.cpp b/src/blinding.cpp index d4ceb500f..8a272e8c7 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -1,14 +1,14 @@ #include "session/blinding.hpp" -#include "session/blinding.h" #include #include -#include -#include #include +#include +#include #include +#include "session/blinding.h" #include "session/crypto/ed25519.hpp" #include "session/export.h" #include "session/hash.hpp" @@ -56,7 +56,8 @@ namespace { if (session_id.size() != 32) throw std::invalid_argument{"Invalid session id"}; - ed25519::scalarmult_noclamp(out.last<32>(), blind_factor, xed25519::pubkey(session_id.first<32>())); + ed25519::scalarmult_noclamp( + out.last<32>(), blind_factor, xed25519::pubkey(session_id.first<32>())); out[0] = prefix; } @@ -71,7 +72,8 @@ namespace { std::span session_id, std::span server_pk, std::span out) { - blind_id_impl(session_id, server_pk, blind25_factor(session_id, server_pk), out, std::byte{0x25}); + blind_id_impl( + session_id, server_pk, blind25_factor(session_id, server_pk), out, std::byte{0x25}); } // Parses server_pk from either 32 raw bytes or 64 hex digits. @@ -103,17 +105,16 @@ namespace { b64 hram; hash::sha512(hram, sig_R, A, message); - ed25519::scalar_reduce(sig_S, hram); // S = H(R||A||M) - ed25519::scalar_mul(sig_S, sig_S, a); // S = H(R||A||M) a - ed25519::scalar_add(sig_S, sig_S, r); // S = r + H(R||A||M) a + ed25519::scalar_reduce(sig_S, hram); // S = H(R||A||M) + ed25519::scalar_mul(sig_S, sig_S, a); // S = H(R||A||M) a + ed25519::scalar_add(sig_S, sig_S, r); // S = r + H(R||A||M) a return result; } } // namespace -b33 blind15_id( - std::span session_id, std::span server_pk) { +b33 blind15_id(std::span session_id, std::span server_pk) { if (session_id.size() == 33) { if (session_id[0] != std::byte{0x05}) throw std::invalid_argument{"blind15_id: session_id must start with 0x05"}; @@ -149,8 +150,7 @@ std::array blind15_id(std::string_view session_id, std::string_v return result; } -b33 blind25_id( - std::span session_id, std::span server_pk) { +b33 blind25_id(std::span session_id, std::span server_pk) { if (session_id.size() == 33) { if (session_id[0] != std::byte{0x05}) throw std::invalid_argument{"blind25_id: session_id must start with 0x05"}; @@ -190,8 +190,7 @@ b33 blinded15_id_from_ed( b33 result; auto k = blind15_factor(server_pk); - ed25519::scalarmult_noclamp( - std::span{result.data() + 1, 32}, k, ed_pubkey); + ed25519::scalarmult_noclamp(std::span{result.data() + 1, 32}, k, ed_pubkey); result[0] = std::byte{0x15}; return result; } @@ -216,16 +215,13 @@ b33 blinded25_id_from_ed( std::ranges::copy(ed_pubkey, pos_ed_pubkey.begin()); pos_ed_pubkey[31] &= std::byte{0x7f}; - ed25519::scalarmult_noclamp( - std::span{result.data() + 1, 32}, k, pos_ed_pubkey); + ed25519::scalarmult_noclamp(std::span{result.data() + 1, 32}, k, pos_ed_pubkey); result[0] = std::byte{0x25}; return result; } std::pair blind15_key_pair( - const ed25519::PrivKeySpan& ed25519_sk, - std::span server_pk, - b32* k) { + const ed25519::PrivKeySpan& ed25519_sk, std::span server_pk, b32* k) { std::pair result; auto& [A, a] = result; @@ -420,8 +416,8 @@ LIBSESSION_C_API bool session_blind15_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto [b_pk, b_sk] = session::blind15_key_pair( - {ed25519_seckey, 64}, to_byte_span<32>(server_pk)); + auto [b_pk, b_sk] = + session::blind15_key_pair({ed25519_seckey, 64}, to_byte_span<32>(server_pk)); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; @@ -436,8 +432,8 @@ LIBSESSION_C_API bool session_blind25_key_pair( unsigned char* blinded_pk_out, unsigned char* blinded_sk_out) { try { - auto [b_pk, b_sk] = session::blind25_key_pair( - {ed25519_seckey, 64}, to_byte_span<32>(server_pk)); + auto [b_pk, b_sk] = + session::blind25_key_pair({ed25519_seckey, 64}, to_byte_span<32>(server_pk)); std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); return true; diff --git a/src/config/base.cpp b/src/config/base.cpp index 758de3906..3a352ce14 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -88,19 +88,14 @@ std::unordered_set ConfigBase::merge( for (auto& [h, c] : configs) { try { - auto unwrapped = protos::unwrap_config( - _keys.front(), - c, - storage_namespace()); + auto unwrapped = protos::unwrap_config(_keys.front(), c, storage_namespace()); // There was a release of one of the clients which resulted in double-wrapped // config messages so we now need to try to double-unwrap in order to better // support multi-device for users running those old versions try { - auto unwrapped2 = protos::unwrap_config( - _keys.front(), - unwrapped, - storage_namespace()); + auto unwrapped2 = + protos::unwrap_config(_keys.front(), unwrapped, storage_namespace()); log::warning( cat, "Found double wraped message in namespace {}", @@ -217,8 +212,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span> ed25519_pubkey, const ed25519::OptionalPrivKeySpan& ed25519_secretkey) { if (ed25519_secretkey) { - if (ed25519_pubkey && - !std::ranges::equal(*ed25519_pubkey, ed25519_secretkey->pubkey())) + if (ed25519_pubkey && !std::ranges::equal(*ed25519_pubkey, ed25519_secretkey->pubkey())) throw std::invalid_argument{"Invalid signing keys: secret key and pubkey do not match"}; set_sig_keys(*ed25519_secretkey); } else if (ed25519_pubkey) { @@ -1099,11 +1091,11 @@ cleared_b32 ConfigSig::seed_hash(std::string_view key) const { namespace { -void set_error(config_object* conf, std::string e) { - auto& error = unbox(conf).error; - error = std::move(e); - conf->last_error = error.c_str(); -} + void set_error(config_object* conf, std::string e) { + auto& error = unbox(conf).error; + error = std::move(e); + conf->last_error = error.c_str(); + } } // namespace @@ -1134,8 +1126,7 @@ LIBSESSION_EXPORT config_string_list* config_merge( std::vector>> confs; confs.reserve(count); for (size_t i = 0; i < count; i++) - confs.emplace_back( - msg_hashes[i], to_byte_span(configs[i], lengths[i])); + confs.emplace_back(msg_hashes[i], to_byte_span(configs[i], lengths[i])); return make_string_list(config.merge(confs)); }); diff --git a/src/config/community.cpp b/src/config/community.cpp index 6a74ca94a..fb4703a5a 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include #include #include @@ -203,7 +203,9 @@ LIBSESSION_C_API bool community_parse_partial_url( LIBSESSION_C_API void community_make_full_url( const char* base_url, const char* room, const unsigned char* pubkey, char* full_url) { auto full = session::config::community::full_url( - base_url, room, std::span{reinterpret_cast(pubkey), 32}); + base_url, + room, + std::span{reinterpret_cast(pubkey), 32}); assert(full.size() <= COMMUNITY_FULL_URL_MAX_LENGTH); std::memcpy(full_url, full.data(), full.size() + 1); } diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 70664be3c..65ff3110a 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,5 +1,4 @@ #include "session/config/contacts.hpp" -#include "session/config/contacts.h" #include #include diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index dc91b3851..41f49b5a1 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -6,8 +6,8 @@ #include #include -#include #include +#include #include #include @@ -240,9 +240,7 @@ std::optional ConvoInfoVolatile::get_community( } convo::community ConvoInfoVolatile::get_or_construct_community( - std::string_view base_url, - std::string_view room, - std::span pubkey) const { + std::string_view base_url, std::string_view room, std::span pubkey) const { convo::community result{base_url, community::canonical_room(room), pubkey.first<32>()}; if (auto* info_dict = community_field(result).dict()) diff --git a/src/config/encrypt.cpp b/src/config/encrypt.cpp index 3072044ef..d5f029f59 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -1,5 +1,4 @@ #include "session/config/encrypt.hpp" -#include "session/config/encrypt.h" #include #include @@ -7,6 +6,7 @@ #include #include +#include "session/config/encrypt.h" #include "session/export.h" #include "session/hash.hpp" #include "session/util.hpp" diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index b96fb1e1d..161cd3649 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -6,20 +6,18 @@ #include #include +#include #include #include #include "../../internal-util.hpp" - -#include - #include "../internal.hpp" #include "session/clock.hpp" -#include "session/crypto/ed25519.hpp" -#include "session/encrypt.hpp" #include "session/config/groups/info.hpp" #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" +#include "session/crypto/ed25519.hpp" +#include "session/encrypt.hpp" #include "session/hash.hpp" #include "session/multi_encrypt.hpp" #include "session/random.hpp" @@ -232,8 +230,7 @@ std::span Keys::rekey(Info& info, Members& members) { // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. auto group_xpk = ed25519::pk_to_x25519(*_sign_pk); - auto group_xsk = ed25519::sk_to_x25519( - std::span{_sign_sk.data(), 64}); + auto group_xsk = ed25519::sk_to_x25519(std::span{_sign_sk.data(), 64}); // We need quasi-randomness: full secure random would be great, except that different admins // encrypting for the same update would always create different keys, but we want it @@ -324,8 +321,7 @@ std::span Keys::rekey(Info& info, Members& members) { std::vector junk_data; junk_data.resize(encrypted.size() * n_junk); - auto rng_seed = - hash::blake2b_key<32>(junk_seed_hash_key, h1, _sign_sk); + auto rng_seed = hash::blake2b_key<32>(junk_seed_hash_key, h1, _sign_sk); random::fill_deterministic(junk_data, rng_seed); std::string_view junk_view = to_string_view(junk_data); @@ -338,8 +334,7 @@ std::span Keys::rekey(Info& info, Members& members) { // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). - d.append_signature( - "~", [this](std::span to_sign) { return sign(to_sign); }); + d.append_signature("~", [this](std::span to_sign) { return sign(to_sign); }); // Load this key/config/gen into our pending variables pending_gen_ = gen; @@ -380,8 +375,7 @@ std::vector Keys::key_supplement(const std::vector& sids // have `B` (the session id) as an x25519 pubkey, we do this in x25519 space, which means we // have to use the x25519 conversion of a/A rather than the group's ed25519 pubkey. auto group_xpk = ed25519::pk_to_x25519(*_sign_pk); - auto group_xsk = ed25519::sk_to_x25519( - std::span{_sign_sk.data(), 64}); + auto group_xsk = ed25519::sk_to_x25519(std::span{_sign_sk.data(), 64}); // We need quasi-randomness here for the nonce: full secure random would be great, except that // different admins encrypting for the same update would always create different keys, but we @@ -464,25 +458,18 @@ std::vector Keys::key_supplement(const std::vector& sids // Finally we sign the message at put it as the ~ key (which is 0x7e, and thus comes later than // any other printable ascii key). - d.append_signature( - "~", [this](std::span to_sign) { return sign(to_sign); }); + d.append_signature("~", [this](std::span to_sign) { return sign(to_sign); }); return to_vector(d.view()); } // Blinding factor for subaccounts: H(sessionid || groupid) mod L, where H is 64-byte blake2b, using // a hash key derived from the group's seed. -b32 Keys::subaccount_blind_factor( - std::span session_xpk) const { +b32 Keys::subaccount_blind_factor(std::span session_xpk) const { auto mask = seed_hash("SessionGroupSubaccountMask"); - auto h = hash::blake2b_key<64>( - mask, - std::byte{0x05}, - session_xpk, - std::byte{0x03}, - *_sign_pk); + auto h = hash::blake2b_key<64>(mask, std::byte{0x05}, session_xpk, std::byte{0x03}, *_sign_pk); return ed25519::scalar_reduce(h); } @@ -583,9 +570,7 @@ std::vector Keys::swarm_subaccount_token( } Keys::swarm_auth Keys::swarm_subaccount_sign( - std::span msg, - std::span sign_val, - bool binary) const { + std::span msg, std::span sign_val, bool binary) const { if (sign_val.size() != 100) throw std::logic_error{"Invalid signing value: size is wrong"}; @@ -668,9 +653,9 @@ Keys::swarm_auth Keys::swarm_subaccount_sign( // Compute S = r + H(R || A || M) a mod L: (with A = kT, a = kt) b64 hram; hash::sha512(hram, R, kT, msg); - ed25519::scalar_reduce(S, hram); // S = H(R||A||M) % L - ed25519::scalar_mul(S, S, kt); // S *= a - ed25519::scalar_add(S, S, r); // S += r + ed25519::scalar_reduce(S, hram); // S = H(R||A||M) % L + ed25519::scalar_mul(S, S, kt); // S *= a + ed25519::scalar_add(S, S, r); // S += r // sig is now set to the desired R || S, with S = r + H(R || A || M)a (all mod L) @@ -707,8 +692,7 @@ bool Keys::swarm_verify_subaccount( return false; auto prefix = sign_val.subspan<0, 4>(); - if (prefix[0] != std::byte{0x03} && - (prefix[1] & SUBACC_FLAG_ANY_PREFIX) == std::byte{0}) + if (prefix[0] != std::byte{0x03} && (prefix[1] & SUBACC_FLAG_ANY_PREFIX) == std::byte{0}) return false; // require either 03 prefix match, or the "any prefix" flag if ((prefix[1] & SUBACC_FLAG_READ) == std::byte{0}) @@ -1090,7 +1074,12 @@ std::vector Keys::encrypt_message( std::span plaintext, bool compress, size_t padding) const { assert(_sign_pk); std::vector ciphertext = encrypt_for_group( - ed25519::PrivKeySpan::from(user_ed25519_sk), *_sign_pk, group_enc_key(), plaintext, compress, padding); + ed25519::PrivKeySpan::from(user_ed25519_sk), + *_sign_pk, + group_enc_key(), + plaintext, + compress, + padding); return ciphertext; } @@ -1183,7 +1172,8 @@ LIBSESSION_C_API int groups_keys_init( ed25519::PrivKeySpan user_sk{user_ed25519_secretkey, 64}; auto group_pk = to_byte_span<32>(group_ed25519_pubkey); - ed25519::OptionalPrivKeySpan group_sk{group_ed25519_secretkey, group_ed25519_secretkey ? 64u : 0u}; + ed25519::OptionalPrivKeySpan group_sk{ + group_ed25519_secretkey, group_ed25519_secretkey ? 64u : 0u}; std::optional> dumped; if (dump && dumplen) dumped.emplace(to_byte_span(dump, dumplen)); @@ -1366,8 +1356,7 @@ LIBSESSION_C_API void groups_keys_encrypt_message( std::vector ciphertext; try { - ciphertext = unbox(conf).encrypt_message( - to_byte_span(plaintext_in, plaintext_len)); + ciphertext = unbox(conf).encrypt_message(to_byte_span(plaintext_in, plaintext_len)); *ciphertext_out = static_cast(std::malloc(ciphertext.size())); std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); *ciphertext_len = ciphertext.size(); @@ -1389,8 +1378,8 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( return wrap_exceptions( conf, [&] { - auto [sid, plaintext] = unbox(conf).decrypt_message( - to_byte_span(ciphertext_in, ciphertext_len)); + auto [sid, plaintext] = + unbox(conf).decrypt_message(to_byte_span(ciphertext_in, ciphertext_len)); std::memcpy(session_id, sid.c_str(), sid.size() + 1); *plaintext_out = static_cast(std::malloc(plaintext.size())); std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); @@ -1497,8 +1486,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - to_byte_span(msg, msg_len), - to_byte_span(signing_value, 100)); + to_byte_span(msg, msg_len), to_byte_span(signing_value, 100)); assert(auth.subaccount.size() == 48); assert(auth.subaccount_sig.size() == 88); assert(auth.signature.size() == 88); @@ -1527,9 +1515,7 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( conf, [&] { auto auth = unbox(conf).swarm_subaccount_sign( - to_byte_span(msg, msg_len), - to_byte_span(signing_value, 100), - true); + to_byte_span(msg, msg_len), to_byte_span(signing_value, 100), true); assert(auth.subaccount.size() == 36); assert(auth.subaccount_sig.size() == 64); assert(auth.signature.size() == 64); diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 2f0366150..5b2aab0fe 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -8,10 +8,10 @@ #include #include +#include "../internal-util.hpp" #include "session/clock.hpp" #include "session/config/base.h" #include "session/config/base.hpp" -#include "../internal-util.hpp" #include "session/config/error.h" #include "session/types.hpp" @@ -211,8 +211,7 @@ std::string_view sv_or_empty(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out a std::vector; nullopt if not there (or not // string) -std::optional> maybe_vector( - const session::config::dict& d, const char* key); +std::optional> maybe_vector(const session::config::dict& d, const char* key); /// Sets a value to 1 if true, removes it if false. void set_flag(ConfigBase::DictFieldProxy&& field, bool val); diff --git a/src/config/pro.cpp b/src/config/pro.cpp index 2d9315858..eda2bb5a3 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -1,5 +1,6 @@ #include #include + #include #include #include @@ -52,9 +53,7 @@ bool ProConfig::load(const dict& root) { // Derive the rotating public key from the seed and populate the proof's pubkey and the outer // private key ed25519::seed_keypair( - proof.rotating_pubkey, - rotating_privkey, - std::span{*maybe_rotating_seed}.first<32>()); + proof.rotating_pubkey, rotating_privkey, std::span{*maybe_rotating_seed}.first<32>()); return true; } diff --git a/src/config/protos.cpp b/src/config/protos.cpp index 409d84833..8527b1503 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -1,9 +1,9 @@ #include "session/config/protos.hpp" -#include #include #include +#include #include #include diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index da19455f3..7d4ee58ac 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -315,9 +315,7 @@ std::optional UserGroups::get_community(std::string_view partial } community_info UserGroups::get_or_construct_community( - std::string_view base_url, - std::string_view room, - std::span pubkey) const { + std::string_view base_url, std::string_view room, std::span pubkey) const { community_info result{base_url, room, pubkey.first<32>()}; if (auto* info_dict = community_field(result).dict()) diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index a3bbceab2..77e5875fc 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -81,8 +81,7 @@ void UserProfile::set_profile_pic(profile_pic pic) { set_profile_pic(pic.url, pic.key); } -void UserProfile::set_reupload_profile_pic( - std::string_view url, std::span key) { +void UserProfile::set_reupload_profile_pic(std::string_view url, std::span key) { auto current_url = data["P"].string_view_or(""); auto current_key_str = data["Q"].string_view_or(""); std::string_view new_key_str{reinterpret_cast(key.data()), key.size()}; diff --git a/src/core.cpp b/src/core.cpp index 51b60901e..f050b4d74 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -6,16 +6,16 @@ #include #include #include -#include #include #include #include -#include #include #include #include #include +#include +#include #include #include #include @@ -147,12 +147,8 @@ void Core::_poll() { net->get_swarm( globals.pubkey_x25519(), false, - [this, - net, - namespaces, - ed25519_hex, - now_ms, - ns_sig = std::move(ns_sig)](auto, auto swarm) { + [this, net, namespaces, ed25519_hex, now_ms, ns_sig = std::move(ns_sig)]( + auto, auto swarm) { if (swarm.empty()) return; @@ -490,8 +486,7 @@ void Core::_handle_pfs_response(std::span sid, std::string in.require_signature( "~", [&x25519_pub]( - std::span b, - std::span sig) { + std::span b, std::span sig) { if (sig.size() != 64 || !xed25519::verify(sig.first<64>(), x25519_pub, b)) throw std::runtime_error{"signature verification failed"}; diff --git a/src/core/devices.cpp b/src/core/devices.cpp index ae16378f0..f4e2960f3 100644 --- a/src/core/devices.cpp +++ b/src/core/devices.cpp @@ -3,24 +3,23 @@ #include #include -#include -#include -#include -#include - #include #include #include #include #include #include -#include #include #include #include #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -767,10 +766,10 @@ std::vector Devices::encrypt_device_data(const device::map& devices) return std::span{enc_key_raw.data() + pos_map[i] * (2 + 32), 2}; }); - auto enc_key = indices | std::views::transform([&](int i) { - return std::span{ - enc_key_raw.data() + pos_map[i] * (2 + 32) + 2, 32}; - }); + auto enc_key = + indices | std::views::transform([&](int i) { + return std::span{enc_key_raw.data() + pos_map[i] * (2 + 32) + 2, 32}; + }); cleared_vector ml_ss_raw(mlkem768::SHAREDSECRETBYTES * devices.size()); @@ -779,7 +778,8 @@ std::vector Devices::encrypt_device_data(const device::map& devices) // (because this is never transmitted, and so not shuffled or padded). auto ml_ss = std::views::iota(size_t{0}, devices.size()) | std::views::transform([&](size_t i) { return std::span{ - ml_ss_raw.data() + i * mlkem768::SHAREDSECRETBYTES, mlkem768::SHAREDSECRETBYTES}; + ml_ss_raw.data() + i * mlkem768::SHAREDSECRETBYTES, + mlkem768::SHAREDSECRETBYTES}; }); cleared_b32 rnd; @@ -877,10 +877,9 @@ std::vector Devices::encrypt_device_data(const device::map& devices) o.append("C", ciphertext_raw); o.append("K", enc_key_raw); o.append("d", enc_devices); - o.append_signature( - "~", [seed = core.globals.account_seed()](std::span body) { - return ed25519::sign(seed.ed25519_secret(), body); - }); + o.append_signature("~", [seed = core.globals.account_seed()](std::span body) { + return ed25519::sign(seed.ed25519_secret(), body); + }); assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above @@ -1088,10 +1087,10 @@ std::vector Devices::decrypt_device_data(std::span e indices | std::views::transform([&](int i) { return std::span{enc_key_raw.data() + i * (2 + 32), 2}; }); - auto enc_key = indices | std::views::transform([&](int i) { - return std::span{ - enc_key_raw.data() + i * (2 + 32) + 2, 32}; - }); + auto enc_key = + indices | std::views::transform([&](int i) { + return std::span{enc_key_raw.data() + i * (2 + 32) + 2, 32}; + }); auto active_keys = active_device_keys(); @@ -1407,9 +1406,7 @@ void Devices::parse_account_pubkeys(std::span messages, bool auto X = in.require_span("X"); in.require_signature( "~", - [&x25519_pub]( - std::span body, - std::span sig) { + [&x25519_pub](std::span body, std::span sig) { if (sig.size() != 64 || !xed25519::verify(sig.first<64>(), x25519_pub, body)) throw std::runtime_error{ @@ -1525,10 +1522,9 @@ std::vector Devices::build_account_pubkey_message() { oxenc::bt_dict_producer o{reinterpret_cast(out.data()), out.size()}; o.append("M", k.mlkem768_pub); o.append("X", k.x25519_pub); - o.append_signature( - "~", [seed = core.globals.account_seed()](std::span body) { - return xed25519::sign(seed.x25519_key(), body); - }); + o.append_signature("~", [seed = core.globals.account_seed()](std::span body) { + return xed25519::sign(seed.x25519_key(), body); + }); assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above return out; diff --git a/src/core/globals.cpp b/src/core/globals.cpp index eac1816d1..8bae772a4 100644 --- a/src/core/globals.cpp +++ b/src/core/globals.cpp @@ -1,13 +1,12 @@ #include #include -#include -#include - #include #include #include +#include #include +#include #include #include diff --git a/src/crypto/ed25519.cpp b/src/crypto/ed25519.cpp index f24a16970..0d805756d 100644 --- a/src/crypto/ed25519.cpp +++ b/src/crypto/ed25519.cpp @@ -117,8 +117,8 @@ b32 scalarmult_base(std::span scalar) { } void scalarmult_base_noclamp(std::span out, std::span scalar) { - if (0 != crypto_scalarmult_ed25519_base_noclamp( - to_unsigned(out.data()), to_unsigned(scalar.data()))) + if (0 != + crypto_scalarmult_ed25519_base_noclamp(to_unsigned(out.data()), to_unsigned(scalar.data()))) throw std::runtime_error{"crypto_scalarmult_ed25519_base_noclamp failed"}; } @@ -132,8 +132,9 @@ void scalarmult_noclamp( std::span out, std::span scalar, std::span point) { - if (0 != crypto_scalarmult_ed25519_noclamp( - to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data()))) + if (0 != + crypto_scalarmult_ed25519_noclamp( + to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data()))) throw std::runtime_error{"crypto_scalarmult_ed25519_noclamp failed"}; } @@ -300,8 +301,7 @@ LIBSESSION_C_API bool session_ed25519_pro_privkey_for_ed25519_seed( const unsigned char* ed25519_seed, unsigned char* ed25519_sk_out) { try { auto [pub, sk] = session::ed25519::derive_subkey( - to_byte_span<32>(ed25519_seed), - session::pro_backend::pro_subkey_domain); + to_byte_span<32>(ed25519_seed), session::pro_backend::pro_subkey_domain); std::memcpy(ed25519_sk_out, sk.data(), sk.size()); return true; } catch (...) { diff --git a/src/crypto/x25519.cpp b/src/crypto/x25519.cpp index 1b5c926bc..872399e3c 100644 --- a/src/crypto/x25519.cpp +++ b/src/crypto/x25519.cpp @@ -19,7 +19,8 @@ void seed_keypair( std::span pk, std::span sk, std::span seed) { - crypto_box_seed_keypair(to_unsigned(pk.data()), to_unsigned(sk.data()), to_unsigned(seed.data())); + crypto_box_seed_keypair( + to_unsigned(pk.data()), to_unsigned(sk.data()), to_unsigned(seed.data())); } std::pair seed_keypair(std::span seed) { @@ -42,8 +43,9 @@ bool scalarmult( std::span out, std::span scalar, std::span point) { - return 0 == crypto_scalarmult_curve25519( - to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data())); + return 0 == + crypto_scalarmult_curve25519( + to_unsigned(out.data()), to_unsigned(scalar.data()), to_unsigned(point.data())); } b32 scalarmult(std::span scalar, std::span point) { diff --git a/src/curve25519.cpp b/src/curve25519.cpp index f34d39f22..3d4551fb9 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,8 +1,7 @@ -#include "session/crypto/ed25519.hpp" -#include "session/crypto/x25519.hpp" - #include +#include "session/crypto/ed25519.hpp" +#include "session/crypto/x25519.hpp" #include "session/export.h" #include "session/util.hpp" diff --git a/src/hash.cpp b/src/hash.cpp index 453359e7c..a5ba9aa79 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -1,9 +1,9 @@ #include "session/hash.hpp" -#include "session/hash.h" #include #include "session/export.h" +#include "session/hash.h" #include "session/util.hpp" namespace { diff --git a/src/logging.cpp b/src/logging.cpp index c0fa70d03..9c0a24ca6 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -1,5 +1,4 @@ #include "session/logging.hpp" -#include "session/logging.h" #include @@ -9,6 +8,7 @@ #include "oxen/log/level.hpp" #include "session/export.h" +#include "session/logging.h" namespace session { diff --git a/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 7fc06d238..422c2cde9 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -2,11 +2,11 @@ #include #include #include -#include -#include +#include #include #include +#include #include #include "session/hash.hpp" @@ -261,8 +261,7 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( int pad) { try { - auto [priv, pub] = - session::ed25519::x25519_keypair(to_byte_span<64>(ed25519_secret_key)); + auto [priv, pub] = session::ed25519::x25519_keypair(to_byte_span<64>(ed25519_secret_key)); return session_encrypt_for_multiple_simple( out_len, messages, diff --git a/src/network/backends/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp index 8076e92ac..09a913325 100644 --- a/src/network/backends/quic_file_client.cpp +++ b/src/network/backends/quic_file_client.cpp @@ -51,9 +51,8 @@ QuicFileClient::QuicFileClient( _ep = quic::Endpoint::endpoint( *_loop, quic::Address{}, - (_max_udp_payload - ? std::make_optional(*_max_udp_payload) - : std::nullopt)); + (_max_udp_payload ? std::make_optional(*_max_udp_payload) + : std::nullopt)); // Set up TLS credentials auto [pk, sk] = ed25519::keypair(); @@ -552,16 +551,15 @@ void streaming_file_upload( // Runs on the loop thread where get_stats() is a direct member access (no queuing). std::shared_ptr progress_timer; if (request.progress_interval > 0ms) { - progress_timer = loop->call_every( - request.progress_interval, - [state, &request, upload_size] { + progress_timer = + loop->call_every(request.progress_interval, [state, &request, upload_size] { if (state->done || !state->stream) return; auto now = std::chrono::steady_clock::now(); auto [acked, unacked, unsent] = state->stream->get_stats(); - auto file_acked = - std::max(0, static_cast(acked) - state->preamble_size); + auto file_acked = std::max( + 0, static_cast(acked) - state->preamble_size); if (file_acked > state->last_acked) { state->last_acked = file_acked; diff --git a/src/network/key_types.cpp b/src/network/key_types.cpp index 120940ac1..959c796af 100644 --- a/src/network/key_types.cpp +++ b/src/network/key_types.cpp @@ -3,6 +3,7 @@ #include #include #include + #include #include #include @@ -25,9 +26,8 @@ namespace detail { void load_from_bytes(void* buffer, size_t length, std::string_view bytes) { if (bytes.size() != length) - throw std::runtime_error{ - "Key data is invalid: expected {} bytes, received {}"_format( - length, bytes.size())}; + throw std::runtime_error{"Key data is invalid: expected {} bytes, received {}"_format( + length, bytes.size())}; std::memmove(buffer, bytes.data(), length); } diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index d4f8da49e..8860b5799 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -595,8 +595,7 @@ void OnionRequestRouter::upload_file(FileUploadRequest request, std::span>(std::move(all_data)); bool consumed = false; - legacy_req.next_data = [data_ptr, - consumed]() mutable -> std::vector { + legacy_req.next_data = [data_ptr, consumed]() mutable -> std::vector { if (consumed) return {}; consumed = true; @@ -614,8 +613,7 @@ void OnionRequestRouter::upload_file(FileUploadRequest request, std::span(&result)) - request.on_complete( - std::make_pair(std::move(*meta), key), timeout); + request.on_complete(std::make_pair(std::move(*meta), key), timeout); else request.on_complete(std::get(result), timeout); }); @@ -954,10 +952,7 @@ void OnionRequestRouter::_dispatch_upload( const auto upload_size = req.body->size(); log::debug( - cat, - "[Upload {}]: Accumulated {} bytes, sending request.", - upload_id, - upload_size); + cat, "[Upload {}]: Accumulated {} bytes, sending request.", upload_id, upload_size); _send_request_internal( std::move(req), @@ -989,8 +984,7 @@ void OnionRequestRouter::_dispatch_upload( if (!body) throw std::runtime_error{"No response body."}; - auto metadata = - file_server::parse_upload_response(*body, upload_size); + auto metadata = file_server::parse_upload_response(*body, upload_size); log::info( cat, "[Upload {}]: Successfully uploaded {} bytes as file ID: {}", @@ -1003,18 +997,10 @@ void OnionRequestRouter::_dispatch_upload( log::error(cat, "[Upload {}]: Cancelled", upload_id); on_result(ERROR_REQUEST_CANCELLED, false); } catch (const status_code_exception& e) { - log::error( - cat, - "[Upload {}]: Failure with error: {}", - upload_id, - e.what()); + log::error(cat, "[Upload {}]: Failure with error: {}", upload_id, e.what()); on_result(e.status_code, false); } catch (const std::exception& e) { - log::error( - cat, - "[Upload {}]: Failure with error: {}", - upload_id, - e.what()); + log::error(cat, "[Upload {}]: Failure with error: {}", upload_id, e.what()); on_result(ERROR_UNKNOWN, false); } }); diff --git a/src/network/routing/session_router_router.cpp b/src/network/routing/session_router_router.cpp index 5aa67cf6f..d6c251fbf 100644 --- a/src/network/routing/session_router_router.cpp +++ b/src/network/routing/session_router_router.cpp @@ -61,8 +61,7 @@ namespace { log::trace( cat, "[Request {}]: Using pre-resolved RemoteAddress.", request_id); result.emplace( - as_span(arg.view_remote_key()).template first<32>(), - arg.port()); + as_span(arg.view_remote_key()).template first<32>(), arg.port()); } else if constexpr (std::is_same_v) { log::trace( cat, @@ -248,17 +247,16 @@ void SessionRouter::upload_file(FileUploadRequest request, std::spancall([weak_self = weak_from_this(), this, upload_id] { @@ -1290,7 +1287,8 @@ void SessionRouter::_send_via_tunnel( // auto test_key = // oxenc::from_base64("1n+DAM9hKyJhtXSPR5L/HdemIKPiHs8dZsPn2kEQuMs="); auto test_key // = oxenc::from_base32z("55fxd8stjrt9g6rsbftx7eesy47pj4751xjghinr3k9ffxh4ieyo"); - auto router_target = oxen::quic::RemoteAddress{as_span(test_key), "::1", tunnel.local_port}; + auto router_target = + oxen::quic::RemoteAddress{as_span(test_key), "::1", tunnel.local_port}; // Construct the actual request to send std::optional remaining_overall_timeout = diff --git a/src/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index d5e4948ba..f2a414816 100644 --- a/src/network/transport/quic_transport.cpp +++ b/src/network/transport/quic_transport.cpp @@ -113,9 +113,7 @@ void QuicTransport::verify_connectivity( if (_pending_requests.count(pubkey_hex) == 0 && _pending_verification_callbacks.at(pubkey_hex).size() == 1) _establish_connection( - {node.remote_pubkey.view(), node.host(), node.omq_port}, - request_id, - category); + {node.remote_pubkey.view(), node.host(), node.omq_port}, request_id, category); }); } diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index f728a188e..e6bbfafef 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -407,7 +407,8 @@ LIBSESSION_C_API bool onion_request_builder_build( try { auto& unboxed_builder = unbox(builder); - auto payload = unboxed_builder.build(session::to_vector(std::span{payload_in, payload_in_len})); + auto payload = + unboxed_builder.build(session::to_vector(std::span{payload_in, payload_in_len})); if (unboxed_builder.final_hop_x25519_keypair) { auto key_pair = unboxed_builder.final_hop_x25519_keypair.value(); diff --git a/src/onionreq/hop_encryption.cpp b/src/onionreq/hop_encryption.cpp index 1fcee7da4..1f8ad8b87 100644 --- a/src/onionreq/hop_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -30,7 +30,8 @@ namespace { std::array calculate_shared_secret( const network::x25519_seckey& seckey, const network::x25519_pubkey& pubkey) { std::array secret; - if (crypto_scalarmult(secret.data(), to_unsigned(seckey.data()), to_unsigned(pubkey.data())) != 0) + if (crypto_scalarmult( + secret.data(), to_unsigned(seckey.data()), to_unsigned(pubkey.data())) != 0) throw std::runtime_error("Shared key derivation failed (crypto_scalarmult)"); return secret; } @@ -144,7 +145,7 @@ std::vector HopEncryption::decrypt_aesgcm( if (!response_long_enough(EncryptType::aes_gcm, ciphertext.size())) throw std::invalid_argument{ - fmt::format("Ciphertext data is too short: {}", ciphertext.size())}; + fmt::format("Ciphertext data is too short: {}", ciphertext.size())}; auto key = derive_symmetric_key(private_key_, pubKey); diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index fb8cfd96a..749c64e72 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -43,8 +43,7 @@ OnionReqParser::OnionReqParser( payload_ = enc.decrypt(enc_type, to_vector(ciphertext), remote_pk); } -std::vector OnionReqParser::encrypt_reply( - std::span reply) const { +std::vector OnionReqParser::encrypt_reply(std::span reply) const { return enc.encrypt(enc_type, to_vector(reply), remote_pk); } diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 5c6dcc02f..9ab7ee516 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -1,5 +1,4 @@ #include "session/onionreq/response_parser.hpp" -#include "session/onionreq/response_parser.h" #include #include @@ -12,6 +11,7 @@ #include "session/onionreq/builder.h" #include "session/onionreq/builder.hpp" #include "session/onionreq/hop_encryption.hpp" +#include "session/onionreq/response_parser.h" #include "session/util.hpp" using namespace session; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 8119603fb..af2b35de8 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -736,7 +736,8 @@ session_pro_backend_add_pro_payment_request_build_sigs( try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_payment_id_span = + to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto sigs = AddProPaymentRequest::build_sigs( @@ -772,7 +773,8 @@ session_pro_backend_add_pro_payment_request_build_to_json( try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); + auto payment_tx_payment_id_span = + to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); std::string json = AddProPaymentRequest::build_to_json( @@ -1248,8 +1250,7 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r std::chrono::milliseconds(refund_requested_unix_ts_ms)}; auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = - to_byte_span(payment_tx_order_id, payment_tx_order_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto sig = SetPaymentRefundRequestedRequest::build_sig( request_version, master_span, @@ -1288,8 +1289,7 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( std::chrono::milliseconds(refund_requested_unix_ts_ms)}; auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = - to_byte_span(payment_tx_order_id, payment_tx_order_id_len); + auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto json = SetPaymentRefundRequestedRequest::build_to_json( request_version, master_span, diff --git a/src/random.cpp b/src/random.cpp index d602b2936..0a08f91d5 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -1,5 +1,4 @@ #include "session/random.hpp" -#include "session/random.h" #include #include @@ -8,6 +7,7 @@ #include #include "session/export.h" +#include "session/random.h" #include "session/util.hpp" namespace session::random { diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 9e0df3cb9..83fef355a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,6 +1,5 @@ #include "session/session_encrypt.hpp" -#include #include #include #include @@ -11,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -18,11 +18,11 @@ #include "internal-util.hpp" #include "session/blinding.hpp" #include "session/clock.hpp" -#include "session/crypto/x25519.hpp" #include "session/crypto/ed25519.hpp" +#include "session/crypto/mlkem768.hpp" +#include "session/crypto/x25519.hpp" #include "session/encrypt.hpp" #include "session/hash.hpp" -#include "session/crypto/mlkem768.hpp" #include "session/random.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" @@ -204,7 +204,8 @@ std::vector encrypt_for_recipient_deterministic( // A sealed box is a regular box (using the ephermal keys and nonce), but with the ephemeral // pubkey prepended: - static_assert(encryption::BOX_SEALBYTES == encryption::BOX_PUBLICKEYBYTES + encryption::BOX_MACBYTES); + static_assert( + encryption::BOX_SEALBYTES == encryption::BOX_PUBLICKEYBYTES + encryption::BOX_MACBYTES); std::vector result; result.resize(encryption::BOX_SEALBYTES + signed_msg.size()); @@ -325,8 +326,7 @@ std::vector encrypt_for_recipient_v2( // Step 3: ki = M[0:2] ⊕ kiss (encrypted key indicator; lets recipient quickly identify key) std::array ki{ - recipient_account_mlkem768[0] ^ kiss[0], - recipient_account_mlkem768[1] ^ kiss[1]}; + recipient_account_mlkem768[0] ^ kiss[0], recipient_account_mlkem768[1] ^ kiss[1]}; // Step 4: ML-KEM-768 encapsulate: ssₘ, mlkem_ct = Encapsulate(M) std::array mlkem_ct; @@ -374,10 +374,7 @@ static DecryptV2Result v2_aead_decrypt_and_parse( size_t enc_size = ciphertext.size() - V2_HEADER_SIZE; std::vector plain(enc_size - V2_AEAD_OVERHEAD); if (!encryption::xchacha20poly1305_decrypt( - plain, - ciphertext.subspan(V2_HEADER_SIZE, enc_size), - nonce, - key)) + plain, ciphertext.subspan(V2_HEADER_SIZE, enc_size), nonce, key)) throw DecryptV2Error{"v2 message decryption failed"}; // Strip zero padding from end (the plaintext was padded to a multiple of 256 bytes) @@ -632,8 +629,7 @@ std::vector encrypt_for_blinded_recipient( // Layout: version(1) || ciphertext(buf+ABYTES) || nonce(NPUBBYTES) std::vector ciphertext; ciphertext.resize( - 1 + buf.size() + encryption::XCHACHA20_ABYTES + - encryption::XCHACHA20_NONCEBYTES); + 1 + buf.size() + encryption::XCHACHA20_ABYTES + encryption::XCHACHA20_NONCEBYTES); // Prepend with a version byte, so that the recipient can reliably detect if a future version is // no longer encrypting things the way it expects. @@ -815,15 +811,13 @@ std::pair, std::string> decrypt_from_blinded_recipient( std::span recipient_id, std::span ciphertext) { auto ed_pk = ed25519_privkey.pubkey(); - if (ciphertext.size() < encryption::XCHACHA20_NONCEBYTES + 1 + - encryption::XCHACHA20_ABYTES) + if (ciphertext.size() < encryption::XCHACHA20_NONCEBYTES + 1 + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; cleared_b32 dec_key; - auto blinded_id = recipient_id[0] == std::byte{0x25} - ? blinded25_id_from_ed(ed_pk, server_pk) - : blinded15_id_from_ed(ed_pk, server_pk); + auto blinded_id = recipient_id[0] == std::byte{0x25} ? blinded25_id_from_ed(ed_pk, server_pk) + : blinded15_id_from_ed(ed_pk, server_pk); if (to_string_view(sender_id) == to_string_view(blinded_id)) dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); @@ -862,10 +856,9 @@ std::pair, std::string> decrypt_from_blinded_recipient( auto sender_x_pk = ed25519::pk_to_x25519(sender_ed_pk); // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message - auto extracted_sender = - recipient_id[0] == std::byte{0x25} - ? blinded25_id_from_ed(sender_ed_pk, server_pk) - : blinded15_id_from_ed(sender_ed_pk, server_pk); + auto extracted_sender = recipient_id[0] == std::byte{0x25} + ? blinded25_id_from_ed(sender_ed_pk, server_pk) + : blinded15_id_from_ed(sender_ed_pk, server_pk); bool matched = to_string_view(sender_id) == to_string_view(extracted_sender); if (!matched && extracted_sender[0] == std::byte{0x15}) { @@ -1051,8 +1044,7 @@ std::string decrypt_ons_response( std::vector decrypt_push_notification( std::span payload, std::span enc_key) { - if (payload.size() < - encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) + if (payload.size() < encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; auto nonce = payload.first(); @@ -1076,24 +1068,19 @@ std::vector encrypt_xchacha20( std::span plaintext, std::span key) { std::vector ciphertext( - encryption::XCHACHA20_NONCEBYTES + plaintext.size() + - encryption::XCHACHA20_ABYTES); + encryption::XCHACHA20_NONCEBYTES + plaintext.size() + encryption::XCHACHA20_ABYTES); auto nonce = std::span{ciphertext}.first(); random::fill(nonce); encryption::xchacha20poly1305_encrypt( - std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), - plaintext, - nonce, - key); + std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), plaintext, nonce, key); return ciphertext; } std::vector decrypt_xchacha20( std::span ciphertext, std::span key) { - if (ciphertext.size() < - encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) + if (ciphertext.size() < encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_ABYTES) throw std::invalid_argument{ "Invalid ciphertext: too short to contain valid encrypted data"}; @@ -1109,7 +1096,6 @@ std::vector decrypt_xchacha20( } // namespace session - extern "C" { using namespace session; @@ -1310,13 +1296,12 @@ LIBSESSION_C_API bool session_decrypt_ons_response( const unsigned char* nonce_in, char* session_id_out) { try { - std::optional> - nonce; + std::optional> nonce; if (nonce_in) nonce = to_byte_span(nonce_in); - auto session_id = - session::decrypt_ons_response(name_in, to_byte_span(ciphertext_in, ciphertext_len), nonce); + auto session_id = session::decrypt_ons_response( + name_in, to_byte_span(ciphertext_in, ciphertext_len), nonce); std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); return true; @@ -1351,8 +1336,8 @@ LIBSESSION_C_API bool session_encrypt_xchacha20( unsigned char** ciphertext_out, size_t* ciphertext_len) { try { - auto ciphertext = - session::encrypt_xchacha20(to_byte_span(plaintext_in, plaintext_len), to_byte_span<32>(key_in)); + auto ciphertext = session::encrypt_xchacha20( + to_byte_span(plaintext_in, plaintext_len), to_byte_span<32>(key_in)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1370,8 +1355,8 @@ LIBSESSION_C_API bool session_decrypt_xchacha20( unsigned char** plaintext_out, size_t* plaintext_len) { try { - auto plaintext = - session::decrypt_xchacha20(to_byte_span(ciphertext_in, ciphertext_len), to_byte_span<32>(key_in)); + auto plaintext = session::decrypt_xchacha20( + to_byte_span(ciphertext_in, ciphertext_len), to_byte_span<32>(key_in)); *plaintext_out = static_cast(malloc(plaintext.size())); *plaintext_len = plaintext.size(); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 59a5b3f2c..203d8ddc6 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -62,7 +62,6 @@ session::b32 proof_hash_internal( session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); } - struct array_uc32_from_ptr_result { bool success; session::b32 data; @@ -321,7 +320,8 @@ std::vector encode_for_community( content_w_sig.set_prosigforcommunitymessageonly( reinterpret_cast(pro_sig.data()), pro_sig.size()); std::vector reserialized(content_w_sig.ByteSizeLong()); - [[maybe_unused]] bool ok = content_w_sig.SerializeToArray(reserialized.data(), reserialized.size()); + [[maybe_unused]] bool ok = + content_w_sig.SerializeToArray(reserialized.data(), reserialized.size()); assert(ok); return pad_message(reserialized); } @@ -333,8 +333,7 @@ std::vector encode_for_community_inbox( std::span recipient_pubkey, std::span community_pubkey, const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey) { - std::vector content = - encode_for_community(plaintext, pro_rotating_ed25519_privkey); + std::vector content = encode_for_community(plaintext, pro_rotating_ed25519_privkey); return encrypt_for_blinded_recipient( ed25519_privkey, community_pubkey, recipient_pubkey, content); } @@ -587,8 +586,7 @@ DecodedEnvelope decode_dm_envelope( throw std::runtime_error{"Parse websocket wrapped envelope failed, missing request body"}; SessionProtos::Envelope envelope = {}; - if (!envelope.ParseFromArray( - ws_msg.request().body().data(), ws_msg.request().body().size())) + if (!envelope.ParseFromArray(ws_msg.request().body().data(), ws_msg.request().body().size())) throw std::runtime_error{"Parse envelope from plaintext failed"}; parse_envelope_fields(result, envelope); @@ -908,10 +906,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( to_byte_span(proof->gen_index_hash.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_unix_ts_ms); - return ed25519::verify( - to_byte_span(proof->sig.data), - to_byte_span<32>(verify_pubkey), - hash); + return ed25519::verify(to_byte_span(proof->sig.data), to_byte_span<32>(verify_pubkey), hash); } LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( @@ -1018,8 +1013,8 @@ session_protocol_encoded_for_destination session_protocol_encode_dm_v1( return c_encode_impl(error, error_len, [&] { return encode_dm_v1( std::span{static_cast(plaintext), plaintext_len}, - ed25519::PrivKeySpan{static_cast(ed25519_privkey), - ed25519_privkey_len}, + ed25519::PrivKeySpan{ + static_cast(ed25519_privkey), ed25519_privkey_len}, from_epoch_ms(sent_timestamp_ms), to_byte_span(recipient_pubkey->data), ed25519::OptionalPrivKeySpan{ @@ -1044,8 +1039,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_community_i return c_encode_impl(error, error_len, [&] { return encode_for_community_inbox( std::span{static_cast(plaintext), plaintext_len}, - ed25519::PrivKeySpan{static_cast(ed25519_privkey), - ed25519_privkey_len}, + ed25519::PrivKeySpan{ + static_cast(ed25519_privkey), ed25519_privkey_len}, std::chrono::milliseconds(sent_timestamp_ms), to_byte_span(recipient_pubkey->data), to_byte_span(community_pubkey->data), @@ -1088,8 +1083,8 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( return c_encode_impl(error, error_len, [&] { return encode_for_group( std::span{static_cast(plaintext), plaintext_len}, - ed25519::PrivKeySpan{static_cast(ed25519_privkey), - ed25519_privkey_len}, + ed25519::PrivKeySpan{ + static_cast(ed25519_privkey), ed25519_privkey_len}, std::chrono::milliseconds(sent_timestamp_ms), to_byte_span(group_ed25519_pubkey->data), to_byte_span(group_enc_key->data), @@ -1153,8 +1148,7 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( group_keys, group_pk, payload, pro_backend_pubkey_cpp.data); result.success = true; } catch (const std::exception& e) { - result.error_len_incl_null_terminator = - format_c_str(error, error_len, "{}", e.what()); + result.error_len_incl_null_terminator = format_c_str(error, error_len, "{}", e.what()); } } else if (keys->group_ed25519_pubkey.size) { result.error_len_incl_null_terminator = format_c_str( @@ -1169,8 +1163,7 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( try { ed25519::PrivKeySpan privkey{ keys->decrypt_keys[index].data, keys->decrypt_keys[index].size}; - result_cpp = decode_dm_envelope( - privkey, payload, pro_backend_pubkey_cpp.data); + result_cpp = decode_dm_envelope(privkey, payload, pro_backend_pubkey_cpp.data); result.success = true; break; } catch (const std::exception& e) { diff --git a/src/util.cpp b/src/util.cpp index 85e7fb101..e3f6688ff 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,12 +1,12 @@ #include #include -#include #include #include #include #include #include +#include #include #include diff --git a/src/xed25519-tweetnacl.cpp b/src/xed25519-tweetnacl.cpp index 4acd3ad72..cd1157464 100644 --- a/src/xed25519-tweetnacl.cpp +++ b/src/xed25519-tweetnacl.cpp @@ -4,12 +4,12 @@ // this subset of the portable TweetNaCl for that single function, and libsodium for everything // else. -#include "session/xed25519.hpp" - #include #include #include +#include "session/xed25519.hpp" + namespace session::xed25519 { namespace { diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 57e8c515f..cca107bbc 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -1,5 +1,4 @@ #include "session/xed25519.hpp" -#include "session/xed25519.h" #include #include @@ -14,6 +13,7 @@ #include "session/export.h" #include "session/hash.hpp" #include "session/util.hpp" +#include "session/xed25519.h" namespace session::xed25519 { diff --git a/tests/live/live_utils.hpp b/tests/live/live_utils.hpp index a73fe255a..024563fc5 100644 --- a/tests/live/live_utils.hpp +++ b/tests/live/live_utils.hpp @@ -10,14 +10,13 @@ #include #include #include -#include #include #include #include +#include #include #include #include -#include #include #include "../dns_utils.hpp" diff --git a/tests/quic-files.cpp b/tests/quic-files.cpp index 674f03a42..481dcfedf 100644 --- a/tests/quic-files.cpp +++ b/tests/quic-files.cpp @@ -75,12 +75,17 @@ int do_upload( req.on_progress = [&](int64_t acked, int64_t total) { auto now = clock::now(); auto since_last = std::chrono::duration(now - last_progress).count(); - auto recent_speed = - since_last > 0 ? human_size{static_cast((acked - last_progress_bytes) / - since_last)} - : human_size{0}; + auto recent_speed = since_last > 0 ? human_size{static_cast( + (acked - last_progress_bytes) / since_last)} + : human_size{0}; auto pct = total > 0 ? 100.0 * acked / total : 0.0; - fmt::print(stderr, "[{}/{}] {:.1f}% {}/s\n", human_size{acked}, human_size{total}, pct, recent_speed); + fmt::print( + stderr, + "[{}/{}] {:.1f}% {}/s\n", + human_size{acked}, + human_size{total}, + pct, + recent_speed); last_progress = now; last_progress_bytes = acked; }; @@ -259,7 +264,7 @@ int run(const CliArgs& args, bool is_upload) { auto netid = args.testnet ? net::opt::netid::testnet() : net::opt::netid::mainnet(); auto router = args.srouter ? net::opt::router::session_router() : net::opt::router::direct(); auto cache_dir = std::filesystem::temp_directory_path() / - (args.testnet ? "quic_files_cache_testnet" : "quic_files_cache"); + (args.testnet ? "quic_files_cache_testnet" : "quic_files_cache"); std::filesystem::create_directories(cache_dir); std::vector net_opts; @@ -392,8 +397,7 @@ int main(int argc, char* argv[]) { // For direct mode, default the server pubkey and address from the known file server configs. if (!args.srouter) { if (args.server_pubkey_hex.empty()) { - auto& pk = args.testnet ? fs::QUIC_FS_ED_PUBKEY_TESTNET - : fs::QUIC_FS_ED_PUBKEY_MAINNET; + auto& pk = args.testnet ? fs::QUIC_FS_ED_PUBKEY_TESTNET : fs::QUIC_FS_ED_PUBKEY_MAINNET; args.server_pubkey_hex = oxenc::to_hex(pk.begin(), pk.end()); } if (args.server_address == "::1") { diff --git a/tests/swarm-auth-test.cpp b/tests/swarm-auth-test.cpp index 45d9d0139..05d3bb756 100644 --- a/tests/swarm-auth-test.cpp +++ b/tests/swarm-auth-test.cpp @@ -60,8 +60,7 @@ struct pseudo_client { std::optional gsk) : secret_key{sk_from_seed(seed)}, info{std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) - : std::nullopt, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt}, members{std::span{gpk, 32}, admin ? std::make_optional>({*gsk, 64}) @@ -69,8 +68,7 @@ struct pseudo_client { std::nullopt}, keys{to_usv(secret_key), std::span{gpk, 32}, - admin ? std::make_optional>({*gsk, 64}) - : std::nullopt, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt, info, members} {} diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index b2214799e..cf40630fa 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -18,10 +18,8 @@ constexpr auto seed2 = "8659efdcbe0949e0f81141e6d397e8be75f45d09262f209d5950e97989eb43c7" "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_hex_b; -constexpr auto sid1 = - "05fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; -constexpr auto sid2 = - "0505c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; +constexpr auto sid1 = "05fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; +constexpr auto sid2 = "0505c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; constexpr auto xpub1 = sid1.last<32>(); constexpr auto xpub2 = sid2.last<32>(); const std::string session_id1 = oxenc::to_hex(sid1); @@ -229,8 +227,7 @@ TEST_CASE("Version 07xxx-blinded signing", "[blinding07][sign]") { uint64_t timestamp = 1234567890; std::vector full_message = to_vector("{}{}{}"_format(timestamp, method, path)); - auto req_sig_no_body = - blind_version_sign_request(seed1, timestamp, method, path, std::nullopt); + auto req_sig_no_body = blind_version_sign_request(seed1, timestamp, method, path, std::nullopt); CHECK(ed25519::verify(req_sig_no_body, pk, full_message)); full_message.insert(full_message.end(), body.begin(), body.end()); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 500e5bd7b..bb766abb5 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include #include #include @@ -643,8 +643,7 @@ TEST_CASE("huger contacts with multipart messages", "[config][multipart][contact CHECK(dump.size() < total_dumps + 500 /* ~ various other dump overhead */); if (dump_load_in_between) { - auto c2b = - std::make_unique(seed, c2->dump()); + auto c2b = std::make_unique(seed, c2->dump()); CHECK_FALSE(c2b->needs_dump()); c2 = std::move(c2b); CHECK_FALSE(c2->needs_dump()); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 4a6034c88..8098b5c57 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -365,7 +365,11 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(conf->last_error == "Invalid community URL: room token contains invalid characters"sv); CHECK(convo_info_volatile_get_or_construct_community( - conf, &og, "http://Example.ORG:5678", "SudokuRoom", to_unsigned(community_pubkey.data()))); + conf, + &og, + "http://Example.ORG:5678", + "SudokuRoom", + to_unsigned(community_pubkey.data()))); CHECK(conf->last_error == nullptr); CHECK(og.base_url == "http://example.org:5678"sv); // Note: lower-case CHECK(og.room == "sudokuroom"sv); // Note: lower-case diff --git a/tests/test_config_local.cpp b/tests/test_config_local.cpp index 451456a65..86cde79b9 100644 --- a/tests/test_config_local.cpp +++ b/tests/test_config_local.cpp @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include #include #include "utils.hpp" diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 84092cb0d..1f0b161aa 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -52,7 +52,8 @@ TEST_CASE("Pro", "[config][pro]") { // Write the signature into the proof ed25519::sign(pro_cpp.proof.sig, signing_sk, hash_to_sign_cpp); - ed25519::sign(to_byte_span(pro.proof.sig.data), signing_sk, to_byte_span(hash_to_sign.data)); + ed25519::sign( + to_byte_span(pro.proof.sig.data), signing_sk, to_byte_span(hash_to_sign.data)); } // Verify expiry diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 40ef56d3c..3f2c3c278 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -492,10 +492,11 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { c2b.secretkey = session::to_vector(ed_sk); // This one does match the group ID, so should propagate c2b.auth_data = // should get ignored, since we have a valid secret key set: - to_vector( - "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000000000"_hex_b); + to_vector( + "0102030405000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000"_hex_b); g2.set(c2b); std::tie(seqno, to_push, obs) = g2.push(); @@ -708,9 +709,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { REQUIRE(to_push->n_configs == 1); std::vector>> to_merge; - to_merge.emplace_back( - "fakehash1", - to_byte_span(to_push->config[0], to_push->config_lens[0])); + to_merge.emplace_back("fakehash1", to_byte_span(to_push->config[0], to_push->config_lens[0])); CHECK(c2.merge(to_merge) == std::unordered_set{{"fakehash1"}}); auto grp = c2.get_legacy_group(definitely_real_id); diff --git a/tests/test_configdata.cpp b/tests/test_configdata.cpp index 9da83a81f..e0f602f5d 100644 --- a/tests/test_configdata.cpp +++ b/tests/test_configdata.cpp @@ -1,11 +1,10 @@ #include #include -#include - #include #include #include +#include #include #include @@ -335,11 +334,8 @@ TEST_CASE("config message signature", "[config][signing]") { auto sig = ed25519::sign(sk, data); return std::vector{sig.begin(), sig.end()}; }; - auto verifier = [&sk]( - std::span data, - std::span signature) { - return signature.size() == 64 && - ed25519::verify(signature.first<64>(), sk.pubkey(), data); + auto verifier = [&sk](std::span data, std::span signature) { + return signature.size() == 64 && ed25519::verify(signature.first<64>(), sk.pubkey(), data); }; m.signer = signer; diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp index 8064fe55c..73605940c 100644 --- a/tests/test_ed25519.cpp +++ b/tests/test_ed25519.cpp @@ -95,8 +95,10 @@ TEST_CASE("Ed25519 pro key pair generation seed", "[ed25519][keypair]") { constexpr auto seed1 = "e5481635020d6f7b327e94e6d63e33a431fccabc4d2775845c43a8486a9f2884"_hex_b; constexpr auto seed2 = "743d646706b6b04b97b752036dd6cf5f2adc4b339fcfdfb4b496f0764bb93a84"_hex_b; - auto [pk1, sk1] = session::ed25519::derive_subkey(seed1, session::pro_backend::pro_subkey_domain); - auto [pk2, sk2] = session::ed25519::derive_subkey(seed2, session::pro_backend::pro_subkey_domain); + auto [pk1, sk1] = + session::ed25519::derive_subkey(seed1, session::pro_backend::pro_subkey_domain); + auto [pk2, sk2] = + session::ed25519::derive_subkey(seed2, session::pro_backend::pro_subkey_domain); CHECK(sk1.size() == 64); CHECK(sk1 != sk2); @@ -117,8 +119,7 @@ TEST_CASE("Ed25519", "[ed25519][signature]") { constexpr auto ed_seed = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hex_b; - constexpr auto ed_pk = - "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_b; + constexpr auto ed_pk = "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hex_b; constexpr auto ed_invalid = "010203040506070809"_hex_u; auto sig1 = session::ed25519::sign(ed_seed, to_span("hello")); diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 1d5a06e82..8fbc8a073 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include #include "utils.hpp" @@ -27,8 +27,8 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); std::vector> enc_keys; - enc_keys.push_back(to_vector( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)); + enc_keys.push_back( + to_vector("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"_hex_b)); groups::Info ginfo1{ed_pk, ed_sk, std::nullopt}; @@ -39,10 +39,11 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { enc_keys.insert( enc_keys.begin(), - to_vector( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); - enc_keys.push_back(to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); - enc_keys.push_back(to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); + to_vector("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); + enc_keys.push_back( + to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); + enc_keys.push_back( + to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); groups::Info ginfo2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. @@ -108,8 +109,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.get_name() == "Better name!"); CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); - CHECK(std::ranges::equal(ginfo1.get_profile_pic().key, - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); + CHECK(std::ranges::equal( + ginfo1.get_profile_pic().key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); CHECK(ginfo1.get_expiry_timer() == 1h); CHECK(ginfo1.get_created() == create_time); CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400); @@ -123,8 +125,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.merge(merge_configs) == std::unordered_set{{"fakehash3"s}}); CHECK(ginfo2.get_name() == "Better name!"); CHECK(ginfo2.get_profile_pic().url == "http://example.com/12345"); - CHECK(std::ranges::equal(ginfo2.get_profile_pic().key, - "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); + CHECK(std::ranges::equal( + ginfo2.get_profile_pic().key, + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); CHECK(ginfo2.get_expiry_timer() == 1h); CHECK(ginfo2.get_created() == create_time); CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); @@ -215,8 +218,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // Deliberately use the wrong signing key so that what we produce encrypts successfully but // doesn't verify - const auto seed_bad1 = - "0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; + const auto seed_bad1 = "0023456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; auto [ed_pk_bad1, ed_sk_bad1] = ed25519::keypair(seed_bad1); groups::Info ginfo_bad1{ed_pk, ed_sk, std::nullopt}; @@ -358,5 +360,6 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { auto pic = ginfo.get_profile_pic(); CHECK_FALSE(pic.empty()); CHECK(pic.url == "http://example.com/12345"); - CHECK(std::ranges::equal(pic.key, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); + CHECK(std::ranges::equal( + pic.key, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); } diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 4745ae99b..1ed2bcb6e 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -45,8 +45,10 @@ TEST_CASE("Group Members", "[config][groups][members]") { enc_keys.insert( enc_keys.begin(), to_vector("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"_hex_b)); - enc_keys.push_back(to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); - enc_keys.push_back(to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); + enc_keys.push_back( + to_vector("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"_hex_b)); + enc_keys.push_back( + to_vector("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"_hex_b)); groups::Members gmem2{ed_pk, ed_sk, std::nullopt}; for (const auto& k : enc_keys) // Just for testing, as above. @@ -216,7 +218,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { ? "" : "{} {}"_format(i < 10 ? "Admin" : "Member", i))); if (i < 20) - CHECK(std::ranges::equal(m.profile_picture.key, + CHECK(std::ranges::equal( + m.profile_picture.key, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); else CHECK(m.profile_picture.key.empty()); @@ -302,7 +305,8 @@ TEST_CASE("Group Members", "[config][groups][members]") { ? "" : "{} {}"_format(i < 10 ? "Admin" : "Member", i))); if (i < 20) - CHECK(std::ranges::equal(m.profile_picture.key, + CHECK(std::ranges::equal( + m.profile_picture.key, "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hex_b)); else CHECK(m.profile_picture.key.empty()); diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index a35be41d2..0c9b874fb 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -150,8 +150,10 @@ TEST_CASE("blake2b_hasher", "[hash][blake2b]") { // Pers + multi-update consistency. b32 pers_multi; - blake2b_hasher<32>{nullkey, pers}.update("Test"_bytes).update("Message"_bytes).finalize( - pers_multi); + blake2b_hasher<32>{nullkey, pers} + .update("Test"_bytes) + .update("Message"_bytes) + .finalize(pers_multi); CHECK(pers_out == pers_multi); // Different pers → different output. diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index d0a4d9bcc..ec22de6ad 100644 --- a/tests/test_multi_encrypt.cpp +++ b/tests/test_multi_encrypt.cpp @@ -230,48 +230,27 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim /* de */ 2 + /* 1:# 24:...nonce... */ 3 + 27 + /* 1:e le */ 3 + 2 + - /* XX: then data with overhead */ 3 * - (3 + 5 + encryption::XCHACHA20_ABYTES)); + /* XX: then data with overhead */ 3 * (3 + 5 + encryption::XCHACHA20_ABYTES)); // If we encrypt again the value should be different (because of the default randomized nonce): - CHECK(encrypted != encrypt_for_multiple_simple( - msgs[0], - to_view_vector( - std::next(recipients.begin()), std::prev(recipients.end())), - x_keys[0].first, - x_keys[0].second, - "test suite")); + CHECK(encrypted != + encrypt_for_multiple_simple( + msgs[0], + to_view_vector(std::next(recipients.begin()), std::prev(recipients.end())), + x_keys[0].first, + x_keys[0].second, + "test suite")); auto m1 = decrypt_for_multiple_simple( - encrypted, - x_keys[1].first, - x_keys[1].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[1].first, x_keys[1].second, x_keys[0].second, "test suite"); auto m2 = decrypt_for_multiple_simple( - encrypted, - x_keys[2].first, - x_keys[2].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[2].first, x_keys[2].second, x_keys[0].second, "test suite"); auto m3 = decrypt_for_multiple_simple( - encrypted, - x_keys[3].first, - x_keys[3].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[3].first, x_keys[3].second, x_keys[0].second, "test suite"); auto m3b = decrypt_for_multiple_simple( - encrypted, - x_keys[3].first, - x_keys[3].second, - x_keys[0].second, - "not test suite"); + encrypted, x_keys[3].first, x_keys[3].second, x_keys[0].second, "not test suite"); auto m4 = decrypt_for_multiple_simple( - encrypted, - x_keys[4].first, - x_keys[4].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[4].first, x_keys[4].second, x_keys[0].second, "test suite"); REQUIRE(m1); REQUIRE(m2); @@ -299,35 +278,15 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim "1ecee2215d226817edfdb097f05037eb799309103a"_hex)); m1 = decrypt_for_multiple_simple( - encrypted, - x_keys[1].first, - x_keys[1].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[1].first, x_keys[1].second, x_keys[0].second, "test suite"); m2 = decrypt_for_multiple_simple( - encrypted, - x_keys[2].first, - x_keys[2].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[2].first, x_keys[2].second, x_keys[0].second, "test suite"); m3 = decrypt_for_multiple_simple( - encrypted, - x_keys[3].first, - x_keys[3].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[3].first, x_keys[3].second, x_keys[0].second, "test suite"); m3b = decrypt_for_multiple_simple( - encrypted, - x_keys[3].first, - x_keys[3].second, - x_keys[0].second, - "not test suite"); + encrypted, x_keys[3].first, x_keys[3].second, x_keys[0].second, "not test suite"); m4 = decrypt_for_multiple_simple( - encrypted, - x_keys[4].first, - x_keys[4].second, - x_keys[0].second, - "test suite"); + encrypted, x_keys[4].first, x_keys[4].second, x_keys[0].second, "test suite"); REQUIRE(m1); REQUIRE(m2); diff --git a/tests/test_onion_request_router.cpp b/tests/test_onion_request_router.cpp index a4afbe0b5..98000b2fc 100644 --- a/tests/test_onion_request_router.cpp +++ b/tests/test_onion_request_router.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include #include #include diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index d631368bb..0bc913b23 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include #include #include @@ -17,7 +17,8 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; auto [ed_pk, ed_sk] = ed25519::keypair(seed); auto curve_pk = ed25519::pk_to_x25519(ed_pk); - REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(ed_pk) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk); @@ -86,7 +87,8 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; auto [ed_pk, ed_sk] = ed25519::keypair(seed); auto curve_pk = ed25519::pk_to_x25519(ed_pk); - REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(ed_pk) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk); @@ -145,7 +147,8 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hex_b; auto [ed_pk, ed_sk] = ed25519::keypair(seed); auto curve_pk = ed25519::pk_to_x25519(ed_pk); - REQUIRE(oxenc::to_hex(ed_pk) == "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(ed_pk) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); REQUIRE(oxenc::to_hex(curve_pk) == "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); auto sid = "05" + oxenc::to_hex(curve_pk); @@ -201,7 +204,10 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - ed25519::extract_seed(ed_sk), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); + ed25519::extract_seed(ed_sk), + server_pk, + blind15_pk2_prefixed, + to_span(lorem_ipsum)); CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( @@ -231,7 +237,10 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - ed25519::extract_seed(ed_sk), server_pk, blind15_pk2_prefixed, to_span(lorem_ipsum)); + ed25519::extract_seed(ed_sk), + server_pk, + blind15_pk2_prefixed, + to_span(lorem_ipsum)); CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( @@ -291,7 +300,10 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; auto enc = encrypt_for_blinded_recipient( - ed25519::extract_seed(ed_sk), server_pk, blind25_pk2_prefixed, to_span(lorem_ipsum)); + ed25519::extract_seed(ed_sk), + server_pk, + blind25_pk2_prefixed, + to_span(lorem_ipsum)); CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 71aff0b45..706001547 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -315,8 +315,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Check non-encryptable messages produce only plaintext") { SECTION("Community inbox") { - auto [blind15_pk, blind15_sk] = - session::blind15_key_pair(keys.ed_sk1, to_byte_span<32>(keys.ed_pk1.data()), /*blind factor*/ nullptr); + auto [blind15_pk, blind15_sk] = session::blind15_key_pair( + keys.ed_sk1, to_byte_span<32>(keys.ed_pk1.data()), /*blind factor*/ nullptr); cbytes33 blind15_recipient = {}; blind15_recipient.data[0] = 0x15; std::memcpy(blind15_recipient.data + 1, blind15_pk.data(), blind15_pk.size()); @@ -525,9 +525,14 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { cbytes33 group_v2_session_pk = {}; cbytes32 group_v2_session_sk = {}; group_v2_session_pk.data[0] = 0x03; - std::memcpy(group_v2_session_pk.data + 1, to_unsigned(group_v2_pk.data()), group_v2_pk.size()); std::memcpy( - group_v2_session_sk.data, to_unsigned(group_v2_sk.data()), sizeof(group_v2_session_sk.data)); + group_v2_session_pk.data + 1, + to_unsigned(group_v2_pk.data()), + group_v2_pk.size()); + std::memcpy( + group_v2_session_sk.data, + to_unsigned(group_v2_sk.data()), + sizeof(group_v2_session_sk.data)); encrypt_result = session_protocol_encode_for_group( protobuf_content.plaintext.data(), diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 7440ab940..d54859ebb 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -23,14 +23,11 @@ constexpr auto pub1 = seed1.last<32>(); constexpr auto pub2 = seed2.last<32>(); // Expected X25519 pubkeys derived from the Ed25519 pubkeys -constexpr auto xpub1 = - "fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; -constexpr auto xpub2 = - "05c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; +constexpr auto xpub1 = "fe94b7ad4b7f1cc1bb92671f1f0d243f226e115b33770465e82b503fc3e96e1f"_hex_b; +constexpr auto xpub2 = "05c9a9bf178fa644d44bebf628716dc7f2df3d0842e97881962c723699152073"_hex_b; // The "absolute" (positive) version of pub2's Ed25519 pubkey -constexpr auto pub2_abs = - "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee01"_hex_b; +constexpr auto pub2_abs = "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee01"_hex_b; TEST_CASE("XEd25519 pubkey conversion", "[xed25519][pubkey]") { auto xpk1 = ed25519::pk_to_x25519(pub1); @@ -108,10 +105,14 @@ TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { b64 xed_sig1, xed_sig2; REQUIRE(session_xed25519_sign( - to_unsigned(xed_sig1.data()), to_unsigned(xsk1.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig1.data()), + to_unsigned(xsk1.data()), + to_unsigned(msg.data()), msg.size())); REQUIRE(session_xed25519_sign( - to_unsigned(xed_sig2.data()), to_unsigned(xsk2.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig2.data()), + to_unsigned(xsk2.data()), + to_unsigned(msg.data()), msg.size())); REQUIRE(ed25519::verify(xed_sig1, pub1, msg)); @@ -148,16 +149,24 @@ TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { b64 xed_sig1, xed_sig2; REQUIRE(session_xed25519_sign( - to_unsigned(xed_sig1.data()), to_unsigned(xsk1.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig1.data()), + to_unsigned(xsk1.data()), + to_unsigned(msg.data()), msg.size())); REQUIRE(session_xed25519_sign( - to_unsigned(xed_sig2.data()), to_unsigned(xsk2.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig2.data()), + to_unsigned(xsk2.data()), + to_unsigned(msg.data()), msg.size())); REQUIRE(session_xed25519_verify( - to_unsigned(xed_sig1.data()), to_unsigned(xpub1.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig1.data()), + to_unsigned(xpub1.data()), + to_unsigned(msg.data()), msg.size())); REQUIRE(session_xed25519_verify( - to_unsigned(xed_sig2.data()), to_unsigned(xpub2.data()), to_unsigned(msg.data()), + to_unsigned(xed_sig2.data()), + to_unsigned(xpub2.data()), + to_unsigned(msg.data()), msg.size())); } diff --git a/tests/utils.hpp b/tests/utils.hpp index 4f692a150..c2c109a19 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -149,7 +149,6 @@ class CallTracker { } // namespace session - template inline std::string to_hex(const Container& bytes) { std::string hex; From 8ed0b89baf18b1064f7b726fd75f27077fa19ba8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 6 Jul 2026 17:07:12 -0300 Subject: [PATCH 082/193] Bump session-sqlite submodule to avoid undeclared oxenc dep --- external/session-sqlite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/session-sqlite b/external/session-sqlite index 9d16f90e2..61c600a7b 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit 9d16f90e2c39a42300f9b91e37be82a4cfbbfc30 +Subproject commit 61c600a7bafb278772d9d655bc273be4c2381fe1 From c3e294f3f63ad5e91051fb523772759bb8fe690b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 6 Jul 2026 17:19:05 -0300 Subject: [PATCH 083/193] formatting --- src/pro_backend.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index af2b35de8..d24f2a689 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -171,7 +171,9 @@ std::string AddProPaymentRequest::to_json() const { switch (payment_tx.provider) { case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: {assert(false && "Unimplemented");} break; + case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { + assert(false && "Unimplemented"); + } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { j["payment_tx"]["google_payment_token"] = payment_tx.payment_id; j["payment_tx"]["google_order_id"] = payment_tx.order_id; @@ -588,9 +590,9 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { item.rangeproof_order_id = - json_require(obj, "rangeproof_order_id", result.errors); + json_require(obj, "rangeproof_order_id", result.errors); assert(item.rangeproof_order_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->rangeproof_order_id)); + sizeof(((session_pro_backend_pro_payment_item*)0)->rangeproof_order_id)); } break; } @@ -668,7 +670,9 @@ std::string SetPaymentRefundRequestedRequest::to_json() const { switch (payment_tx.provider) { case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: {assert(false && "Unimplemented");} break; + case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { + assert(false && "Unimplemented"); + } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { j["payment_tx"]["google_payment_token"] = payment_tx.payment_id; j["payment_tx"]["google_order_id"] = payment_tx.order_id; @@ -1213,7 +1217,6 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json src.rangeproof_order_id.data()); } break; - } } From 911d4747177fcae6a5416dfbe31b585938eb9a95 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:45 -0300 Subject: [PATCH 084/193] Bump session-sqlite Brings in a C++20 fix (std::shared_ptr::unique() was removed), libsodium linkage so crypto_pwhash resolves on macOS arm64, and a size-dispatched integer bind template that avoids an overload ambiguity under libc++/LP64. Co-Authored-By: Claude Opus 4.8 (1M context) --- external/session-sqlite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/session-sqlite b/external/session-sqlite index 61c600a7b..0724f97a3 160000 --- a/external/session-sqlite +++ b/external/session-sqlite @@ -1 +1 @@ -Subproject commit 61c600a7bafb278772d9d655bc273be4c2381fe1 +Subproject commit 0724f97a3767a8faea9da7bc88e582666cd1c8fb From 2bcd8e98ff55acfc0d676c8a471e7f420dfb8d7f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:46 -0300 Subject: [PATCH 085/193] Bump session-deps Applies the Apple target architecture to all static dependencies (with an ARCH_TRIPLET fallback for iOS), uses the SDK-appropriate deployment-target flag for dependency CFLAGS/CXXFLAGS, and propagates the C++ standard library and toolchain into the dependency builds so they match the main build's ABI. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmake/session-deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/session-deps b/cmake/session-deps index 3763407c5..70acd6c48 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit 3763407c57d4fefce2b6e764be709d7af9a951c3 +Subproject commit 70acd6c488ed7d96fd42259cd1277b105ad219d3 From 434785137b9b255c3037187761d843ddd3f06083 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:47 -0300 Subject: [PATCH 086/193] Bump session-router Picks up the session-deps Apple target-arch fix required by the static iOS build. Co-Authored-By: Claude Opus 4.8 (1M context) --- external/session-router | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/session-router b/external/session-router index 8d1df2efe..ba9fae9f8 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit 8d1df2efe612c15eb15f50aab6f6c72fda326d25 +Subproject commit ba9fae9f845a7e1825b92440b64c690867838443 From ab3647586f248e90dacdf4e803e558ddb90654da Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:47 -0300 Subject: [PATCH 087/193] Parse JSON from a char view rather than a byte range nlohmann::json::parse() on a std::span/vector instantiates char_traits, which libc++ does not specialize (libstdc++ happened to tolerate it). Feed parse() a char view instead, in both the onionreq code and the tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/onionreq/builder.cpp | 3 ++- src/onionreq/parser.cpp | 6 +++++- src/onionreq/response_parser.cpp | 3 ++- tests/test_helper.hpp | 9 +++++++++ tests/test_pfs_key_cache.cpp | 2 +- tests/test_poll.cpp | 12 ++++++------ 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index e6bbfafef..dbb07c13c 100644 --- a/src/onionreq/builder.cpp +++ b/src/onionreq/builder.cpp @@ -135,7 +135,8 @@ std::vector Builder::_generate_payload( nlohmann::json params_json; if (body && !body->empty()) - params_json = nlohmann::json::parse(*body); + // Parse as a char view: libc++'s std::char_traits has no std::byte specialization. + params_json = nlohmann::json::parse(to_string_view(*body)); else params_json = nlohmann::json::object(); diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index 749c64e72..a510427d3 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -6,6 +6,8 @@ #include #include +#include "session/util.hpp" + namespace session::onionreq { OnionReqParser::OnionReqParser( @@ -29,7 +31,9 @@ OnionReqParser::OnionReqParser( throw std::invalid_argument{"encrypted onion request data segment too small"}; auto ciphertext = req.subspan(0, size); req = req.subspan(size); - auto metadata = nlohmann::json::parse(req); + // Parse as a char view: nlohmann instantiates char_traits, and libc++ only + // specializes std::char_traits for the standard character types (not std::byte). + auto metadata = nlohmann::json::parse(to_string_view(req)); if (auto encit = metadata.find("enc_type"); encit != metadata.end()) enc_type = parse_enc_type(encit->get()); diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 9ab7ee516..f4a9b533c 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -93,7 +93,8 @@ DecryptedResponse ResponseParser::_decrypt_v3_response(const std::string& respon base64_iv_and_ciphertext.end(), std::back_inserter(iv_and_ciphertext)); auto result = decrypt(iv_and_ciphertext); - auto result_json = nlohmann::json::parse(result); + // Parse as a char view: libc++'s std::char_traits has no std::byte specialization. + auto result_json = nlohmann::json::parse(to_string_view(result)); int16_t status_code; std::vector> headers; std::string body; diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp index b162573c7..0d648c784 100644 --- a/tests/test_helper.hpp +++ b/tests/test_helper.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -10,9 +11,17 @@ #include #include #include +#include +#include namespace session { +// nlohmann can't parse a std::byte range directly: libc++'s std::char_traits has no std::byte +// specialization. Parse request/response bodies via a char view instead. +inline nlohmann::json parse_json(std::span body) { + return nlohmann::json::parse(to_string_view(body)); +} + /// A minimal in-process mock of network::Network for unit tests. /// Tests can set `current_node` to control which node get_swarm returns, and inspect /// `sent_requests` to observe outgoing requests and fire their callbacks. diff --git a/tests/test_pfs_key_cache.cpp b/tests/test_pfs_key_cache.cpp index ec8856b54..b0377e752 100644 --- a/tests/test_pfs_key_cache.cpp +++ b/tests/test_pfs_key_cache.cpp @@ -59,7 +59,7 @@ TEST_CASE("prefetch_pfs_keys fetches and caches remote account pubkeys", "[core] REQUIRE(mock_net->sent_requests.size() == 1); CHECK(mock_net->sent_requests[0].request.endpoint == "retrieve"); - auto req = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + auto req = parse_json(*mock_net->sent_requests[0].request.body); CHECK(req["pubkey"] == oxenc::to_hex(sid)); CHECK(req["namespace"] == static_cast(config::Namespace::AccountPubkeys)); diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 0a97ca84b..781d4bde0 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -55,7 +55,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { auto& sent = mock_net->sent_requests[0]; CHECK(sent.request.endpoint == "batch"); - auto batch_json = nlohmann::json::parse(*sent.request.body); + auto batch_json = parse_json(*sent.request.body); auto& reqs = batch_json["requests"]; REQUIRE(reqs.size() == 3); // Subrequest 0: Default (ns 0) — requires auth. @@ -98,7 +98,7 @@ TEST_CASE("Core automatic polling", "[core][poll]") { TestHelper::poll(*core); REQUIRE(mock_net->sent_requests.size() == 1); - auto batch_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + auto batch_json2 = parse_json(*mock_net->sent_requests[0].request.body); CHECK(batch_json2["requests"][1]["params"]["last_hash"] == "hash1"); } @@ -120,7 +120,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse( + auto p = parse_json( *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // No prior hash for any node — must not send last_hash. CHECK_FALSE(p.contains("last_hash")); @@ -136,7 +136,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse( + auto p = parse_json( *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } @@ -147,7 +147,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse( + auto p = parse_json( *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // B has no recorded hash — must not send last_hash so we get everything. CHECK_FALSE(p.contains("last_hash")); @@ -165,7 +165,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = nlohmann::json::parse( + auto p = parse_json( *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } From a66aed3bd6c21d6d4ee9b0cf53f870566a5b92d5 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:48 -0300 Subject: [PATCH 088/193] Link simdutf, nlohmann_json and network into their targets session_protocol.cpp needs simdutf, so link it into crypto; core.cpp includes and constructs network::Request, so core must link nlohmann_json and the network target. These only surfaced on the full-static and macOS builds where the headers/symbols weren't otherwise on the path. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ddb5ae50f..2f28c98f0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -107,6 +107,7 @@ target_link_libraries(crypto session::secure_buffer PRIVATE sessiondep::libsodium + sessiondep::simdutf mlkem_native::mlkem768 nlohmann_json::nlohmann_json libsession::protos @@ -131,6 +132,7 @@ target_link_libraries( PRIVATE config sessiondep::libsodium + nlohmann_json::nlohmann_json session::SQLite mlkem_native::mlkem768 quic @@ -192,6 +194,10 @@ if(ENABLE_NETWORKING) if (BUILD_STATIC_DEPS) target_include_directories(network PUBLIC ${CMAKE_BINARY_DIR}/static-deps/include) endif() + + # core's public headers expose network::Network and it constructs network::Request, so it + # depends on the network target (defined after core above). + target_link_libraries(core PUBLIC network) endif() if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") From 707c3f38ac86359c2ed0ae2ca9abeca8e1fa0e75 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:48 -0300 Subject: [PATCH 089/193] Drive all LTO from USE_LTO; use bundled fmt/spdlog for static builds USE_LTO now controls SESSIONDEPS_LTO and WITH_LTO as well, so a single option governs LTO everywhere; this avoids LTO'd static dependencies being fed to a non-LTO clang link (which fails on the pure-bitcode archives clang produces). Static-dependency builds also force oxen-logging's bundled fmt/spdlog instead of the system packages, whose libstdc++ ABI can't be linked into a libc++ build. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fcc3cc57e..f4e26383d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,17 @@ if(IPO_ENABLED AND NOT DEFINED CMAKE_INTERPROCEDURAL_OPTIMIZATION) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) endif() +# USE_LTO is the single LTO knob. Two submodule-provided options default ON independently of it: +# SESSIONDEPS_LTO (session-deps, several copies sharing one cache variable) controls LTO for static +# dependency builds, and WITH_LTO (session-router's enable_lto.cmake) controls LTO for session-router's +# own targets. Leaving either ON in a non-LTO build breaks the link: LTO'd static archives can't be +# consumed by a non-LTO link -- clang in particular emits pure bitcode archives that a plain bfd link +# reports as "file format not recognized", or silently drops the members (undefined references). Force +# both to follow USE_LTO, seeding these shared cache variables before any submodule evaluates its own +# option() so there is exactly one LTO switch. +set(SESSIONDEPS_LTO ${USE_LTO} CACHE BOOL "Use LTO for static dependency builds, where supported" FORCE) +set(WITH_LTO ${USE_LTO} CACHE BOOL "enable lto on compile time" FORCE) + if(STATIC_LIBSTD) add_link_options(-static-libstdc++) if(NOT CMAKE_CXX_COMPILER_ID MATCHES Clang) @@ -127,6 +138,14 @@ include(AddStaticBundleLib) # Always build PIC set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# For a static-deps build we build everything from source; don't let oxen-logging pick up system +# fmt/spdlog via find_package. Beyond the usual reasons to prefer our own versions, a system +# fmt/spdlog is built against libstdc++ and cannot be linked into a libc++ build (undefined +# std::__1 symbols), so force the bundled submodules which build with our toolchain. +if(BUILD_STATIC_DEPS) + set(OXEN_LOGGING_FORCE_SUBMODULES ON CACHE INTERNAL "") +endif() + add_subdirectory(external) From 4955a49198337d6d7957f7f893691c97551ec5a7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Jul 2026 18:57:48 -0300 Subject: [PATCH 090/193] Miscellaneous build and compatibility fixes - Use copy_c_str for the rangeproof order id in pro_backend, matching the other payment providers. - Give the blind_version_sign C wrappers a uint64_t timestamp to match the header and C++ API. - Remove the deprecated opt::quic_disable_mtu_discovery. - Pass nullptr (not std::nullopt) for the _resync_clock callback argument. - Locally suppress deprecation warnings around deprecated-path tests. - Spell out value_or(core::callbacks{}) in the Core constructor so template argument deduction succeeds. - Disable the Intel macOS CI builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .drone.jsonnet | 2 +- include/session/core.hpp | 3 ++- include/session/network/network_config.hpp | 1 - include/session/network/network_opt.hpp | 6 ------ src/blinding.cpp | 4 ++-- src/network/network_config.cpp | 9 +-------- src/network/session_network.cpp | 2 +- src/pro_backend.cpp | 7 ++----- tests/live/test_file_transfer.cpp | 8 ++++++++ tests/test_hash.cpp | 4 ++++ 10 files changed, 21 insertions(+), 25 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index c15b740b8..8dc1dfbcf 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -394,7 +394,7 @@ local static_build(name, debian_build('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), // Macos builds: - mac_builder('macOS Intel (Release)', allow_test_fail=true/*the current intel mac has issues*/), + //mac_builder('macOS Intel (Release)', allow_test_fail=true/*the current intel mac has issues*/), mac_builder('macOS Arm64 (Release)', arch='arm64'), mac_builder('macOS Arm64 (Debug)', arch='arm64', build_type='Debug'), diff --git a/include/session/core.hpp b/include/session/core.hpp index e4c8a9a19..4bb797243 100644 --- a/include/session/core.hpp +++ b/include/session/core.hpp @@ -313,7 +313,8 @@ class Core { // - database options: see sqlite::DatabaseOption (encryption, behaviour flags, etc.) template Core(std::filesystem::path db_path, Opts&&... opts) : - callbacks{_maybe_instance(std::forward(opts)...).value_or({})}, + callbacks{_maybe_instance(std::forward(opts)...) + .value_or(core::callbacks{})}, db{_make_db(std::move(db_path), std::forward(opts)...)} { if (auto s = _maybe_instance(std::forward(opts)...)) globals._predefined_seed = std::move(s->bytes); diff --git a/include/session/network/network_config.hpp b/include/session/network/network_config.hpp index dca416a8c..46a227701 100644 --- a/include/session/network/network_config.hpp +++ b/include/session/network/network_config.hpp @@ -132,7 +132,6 @@ struct Config { void handle_config_opt(opt::quic_handshake_timeout qht); void handle_config_opt(opt::quic_keep_alive qka); void handle_config_opt(opt::quic_max_udp_payload qmup); - void handle_config_opt(opt::quic_disable_mtu_discovery qdmd); // Onion request router options void handle_config_opt(opt::onionreq_path_strike_threshold pst); diff --git a/include/session/network/network_opt.hpp b/include/session/network/network_opt.hpp index fb3ed8503..ebff998f4 100644 --- a/include/session/network/network_opt.hpp +++ b/include/session/network/network_opt.hpp @@ -403,12 +403,6 @@ namespace opt { explicit quic_max_udp_payload(size_t s) : size{s} {} }; - /// Deprecated: use quic_max_udp_payload{1200} instead. - struct [[deprecated("use quic_max_udp_payload{1200} instead")]] quic_disable_mtu_discovery - : quic_max_udp_payload { - quic_disable_mtu_discovery() : quic_max_udp_payload{1200} {} - }; - // MARK: Onion Request Router Options /// Can be used to override the default (3) number of times a path can receive an error before diff --git a/src/blinding.cpp b/src/blinding.cpp index 8a272e8c7..86241fb78 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -494,7 +494,7 @@ LIBSESSION_C_API bool session_blind25_sign( LIBSESSION_C_API bool session_blind_version_sign_request( const unsigned char* ed25519_seckey, - size_t timestamp, + uint64_t timestamp, const char* method, const char* path, const unsigned char* body, @@ -520,7 +520,7 @@ LIBSESSION_C_API bool session_blind_version_sign_request( LIBSESSION_C_API bool session_blind_version_sign( const unsigned char* ed25519_seckey, CLIENT_PLATFORM platform, - size_t timestamp, + uint64_t timestamp, unsigned char* blinded_sig_out) { try { auto sig = session::blind_version_sign( diff --git a/src/network/network_config.cpp b/src/network/network_config.cpp index 9320418fb..f4241b541 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -59,7 +59,7 @@ Config::Config(const std::vector& opts) { // Quic transport options HANDLE_TYPE(opt::quic_handshake_timeout); HANDLE_TYPE(opt::quic_keep_alive); - HANDLE_TYPE(opt::quic_disable_mtu_discovery); + HANDLE_TYPE(opt::quic_max_udp_payload); // Onion request router options HANDLE_TYPE(opt::onionreq_path_strike_threshold); @@ -317,13 +317,6 @@ void Config::handle_config_opt(opt::quic_max_udp_payload qmup) { log::debug(cat, "Network config max QUIC UDP payload set to {} bytes", qmup.size); } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -void Config::handle_config_opt(opt::quic_disable_mtu_discovery qdmd) { - handle_config_opt(static_cast(qdmd)); -} -#pragma GCC diagnostic pop - // MARK: Onion Request Router Options void Config::handle_config_opt(opt::onionreq_path_strike_threshold pst) { diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 39415bab2..22e8a3737 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -581,7 +581,7 @@ void Network::upload_file(FileUploadRequest request, std::span if (auto* status_code = std::get_if(&result)) { if (*status_code == ERROR_TOO_EARLY) { log::info(cat, "File upload received 425, triggering clock resync."); - _resync_clock(std::nullopt, std::nullopt); + _resync_clock(std::nullopt, nullptr); if (user_callback) user_callback(*status_code, timeout); diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index d24f2a689..9be8a3dd5 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -1211,11 +1211,8 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json 1; } break; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { - dest.rangeproof_order_id_count = snprintf_clamped( - dest.rangeproof_order_id, - sizeof(dest.rangeproof_order_id), - src.rangeproof_order_id.data()); - + dest.rangeproof_order_id_count = + session::copy_c_str(dest.rangeproof_order_id, src.rangeproof_order_id) - 1; } break; } } diff --git a/tests/live/test_file_transfer.cpp b/tests/live/test_file_transfer.cpp index 9883f5c90..b57f04523 100644 --- a/tests/live/test_file_transfer.cpp +++ b/tests/live/test_file_transfer.cpp @@ -59,7 +59,11 @@ TEST_CASE("Live: file upload via QUIC", "[live][file]") { req.ttl = 1min; req.on_complete = [&](auto result, bool) { promise.set_value(std::move(result)); }; + // Intentionally exercising the deprecated upload() path to verify it still works. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" net->upload(std::move(req)); +#pragma GCC diagnostic pop REQUIRE(future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); auto result = future.get(); @@ -158,7 +162,11 @@ TEST_CASE("Live: file upload and download round-trip via QUIC", "[live][file]") upload_promise.set_value(std::move(result)); }; + // Intentionally exercising the deprecated upload() path to verify it still works. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" net->upload(std::move(upload_req)); +#pragma GCC diagnostic pop REQUIRE(upload_future.wait_for(LIVE_TIMEOUT) == std::future_status::ready); auto upload_result = upload_future.get(); diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index 0c9b874fb..9c250294d 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -10,6 +10,9 @@ using namespace session::literals; TEST_CASE("Hash generation", "[hash][hash]") { + // Intentionally exercising the deprecated hash::hash() to verify it still works. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" auto hash1 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); auto hash2 = session::hash::hash(32, session::to_span("TestMessage"), std::nullopt); auto hash3 = @@ -27,6 +30,7 @@ TEST_CASE("Hash generation", "[hash][hash]") { session::to_span("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyTh" "atIsTooLon" "g"))); +#pragma GCC diagnostic pop CHECK(hash1.size() == 32); CHECK(hash2.size() == 32); From 2c1c46eee9425d827206f086d8112f0045f09408 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Jul 2026 19:38:30 -0300 Subject: [PATCH 091/193] Fix endian byte-swap in hash::detail::make_hashable big-endian path The big-endian branch called oxenc::write_big_as_host(swapped.data(), val), which is wrong three ways: wrong function (big<->host instead of host->little) and reversed arguments (oxenc takes (val, to), not (to, val)). It is only instantiated on big-endian builds; on little-endian the branch is if-constexpr-discarded and never type-checked, so it went unnoticed. Use write_host_as_little(val, swapped.data()). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/hash.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/session/hash.hpp b/include/session/hash.hpp index 6c5df3308..7bafe983d 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -91,7 +91,7 @@ namespace detail { return std::span{reinterpret_cast(&val), sizeof(val)}; else { std::array swapped; - oxenc::write_big_as_host(swapped.data(), val); + oxenc::write_host_as_little(val, swapped.data()); return swapped; } } From f18420429d45269201a40a5bcebc1b42fe56ca50 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 00:59:52 -0300 Subject: [PATCH 092/193] formatting --- tests/test_poll.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_poll.cpp b/tests/test_poll.cpp index 781d4bde0..e10129ae8 100644 --- a/tests/test_poll.cpp +++ b/tests/test_poll.cpp @@ -120,8 +120,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = parse_json( - *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = parse_json(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // No prior hash for any node — must not send last_hash. CHECK_FALSE(p.contains("last_hash")); } @@ -136,8 +135,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = parse_json( - *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = parse_json(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } @@ -147,8 +145,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = parse_json( - *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = parse_json(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; // B has no recorded hash — must not send last_hash so we get everything. CHECK_FALSE(p.contains("last_hash")); } @@ -165,8 +162,7 @@ TEST_CASE( TestHelper::poll(*c); REQUIRE(mock_net->sent_requests.size() == 1); { - auto p = parse_json( - *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + auto p = parse_json(*mock_net->sent_requests[0].request.body)["requests"][1]["params"]; CHECK(p["last_hash"] == "xyz"); } } From 5ddcc1cb6a96327c16892aae206a9fb50e6c86f7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 15:14:35 -0300 Subject: [PATCH 093/193] Fix quic linking When quic comes via system lib there won't be a bare quic target, so we need to make sure we either use the oxen::quic target alias that session_router sets up, or set up that alias ourselves, otherwise we can get an attempt to use `-lquic` when linking which is wrong. --- external/CMakeLists.txt | 2 +- src/CMakeLists.txt | 4 ++-- tests/CMakeLists.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 982d2ae1d..74eb414a0 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -41,7 +41,7 @@ if(ENABLE_NETWORKING) libsession_static_bundle(session-router::libsessionrouter) else() set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") - sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic quic) + sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic oxen::quic) endif() endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f28c98f0..958233fbe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -135,7 +135,7 @@ target_link_libraries( nlohmann_json::nlohmann_json session::SQLite mlkem_native::mlkem768 - quic + oxen::quic ) target_link_libraries(config @@ -174,7 +174,7 @@ if(ENABLE_NETWORKING) target_link_libraries(network PUBLIC crypto - quic + oxen::quic PRIVATE nlohmann_json::nlohmann_json sessiondep::libsodium diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a354322d..f1c0a1823 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -129,6 +129,6 @@ target_link_libraries(swarm-auth-test PRIVATE config) if(STATIC_BUNDLE) add_executable(static-bundle-test static_bundle.cpp) target_include_directories(static-bundle-test PUBLIC ../include) - target_link_libraries(static-bundle-test PRIVATE "${PROJECT_BINARY_DIR}/libsession-util.a" oxenc::oxenc quic) + target_link_libraries(static-bundle-test PRIVATE "${PROJECT_BINARY_DIR}/libsession-util.a" oxenc::oxenc oxen::quic) add_dependencies(static-bundle-test session-util) endif() From e80c69ce2e4bd8bad0af899eb302a3b47a41971f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 15:23:46 -0300 Subject: [PATCH 094/193] Fix compilation with session-router is disabled --- src/network/session_network.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/network/session_network.cpp b/src/network/session_network.cpp index 22e8a3737..dd6c039e9 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -215,11 +215,15 @@ Network::Network(config::Config _conf) : break; case opt::router::Type::session_router: +#ifdef ENABLE_NETWORKING_SROUTER _router = SessionRouter::make( std::move(build_session_router_config(config, file_server_config)), _loop, _snode_pool, _transport); +#else + throw std::runtime_error{"Session Router support is not enabled in this build!"}; +#endif break; case opt::router::Type::direct: From fadbd4953bf3aca2434b85ca36fa17535a818f26 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 22:11:25 -0300 Subject: [PATCH 095/193] Build fixes: fmt/spdlog linking, oxen-logging/oxen::quic resolution, always-on networking Squashes two build-infrastructure fixes onto the pfs base branch so that pfs-derived feature branches inherit them. --- Fix fmt/spdlog linking when using system lib --- When we load oxen-logging via system lib, the code that was supposed to also inject fmt/spdlog with it was not working properly because the "did we go via submodule" was falsely returning true. This fixes it by using an intermediate library so that we can properly distinguish between the cases (final oxen::logging exists - submodule, which already gives fmt/spdlog; otherwise use the intermediate target and set final oxen::logging alias manually). This also fixes the default STATIC_BUNDLE to depend on BUILD_STATIC_DEPS, rather than NOT BUILD_SHARED_LIBS, so that -DBUILD_STATIC_DEPS=OFF doesn't accidentally default to an impossible build. --- Always build networking; fix oxen-logging/oxen::quic dep resolution --- Core is designed to always depend on networking: it holds a quic::Loop and a network::Network unconditionally and calls into the network library from _poll/_send_to_swarm/prefetch_pfs_keys, with no ENABLE_NETWORKING guards anywhere. The ENABLE_NETWORKING option was therefore unusable in practice -- disabling it still left core referencing the (now unbuilt) network library, so nothing that linked core could link. Rather than scatter #ifdefs through core, drop the option entirely and always build networking. ENABLE_NETWORKING_SROUTER is kept as a real, independent knob. Removing ENABLE_NETWORKING: - drop the option and its top-level nettle guard - src: build the network library unconditionally (SROUTER piece stays gated) - tests: always compile the network tests and link libsession::network; drop the DISABLE_NETWORKING compile define and the now-unreachable BUILD_LIVE_TESTS guard/fatal-error - test_logging.cpp: drop the #ifndef DISABLE_NETWORKING guards (always true now) oxen::quic resolution: Previously oxen::quic was only resolved inside the networking block, but core uses it unconditionally, so a non-SROUTER (or formerly networking-off) configure failed with 'Target "core"/"static-bundle-test" links to oxen::quic but the target was not found'. Resolve oxen::quic independently of the SROUTER path: session-router provides it transitively when SROUTER is on, otherwise pull in oxen-libquic directly. oxen-logging resolution: sessiondep_or_submodule requires the submodule to produce the exact target name it's given, but the oxen-logging submodule produces oxen-logging / oxen::logging, not oxenlogging::oxenlogging -- so a submodule/static build died with 'Subdirectory ... did not produce the required oxenlogging::oxenlogging target'. Replace the hand-rolled intermediate-target + fmt/spdlog wrapper with oxen-logging's own cmake/load.cmake (the loader oxen-libquic already uses), which resolves oxen::logging as either a system lib bundled with suitable fmt/spdlog or the submodule. The system search is forced off to match how our other deps choose between system and submodule (BUILD_STATIC_DEPS / DEPS_FORCE_SUBMODULE / DEPS_FORCE_liboxen-logging_SUBMODULE). Verified: configure + link of tests/testAll across {BUILD_STATIC_DEPS on/off} x {ENABLE_NETWORKING_SROUTER on/off}. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 10 ++--- external/CMakeLists.txt | 50 ++++++++++----------- src/CMakeLists.txt | 98 ++++++++++++++++++++--------------------- tests/CMakeLists.txt | 33 +++++--------- tests/test_logging.cpp | 6 +-- 5 files changed, 89 insertions(+), 108 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f4e26383d..d9bd6bca1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,7 +64,7 @@ set(BUILD_STATIC_DEPS ${static_default} CACHE BOOL "Build all dependencies stati include(cmake/session-deps/Deps.cmake) -option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${static_default}) +option(STATIC_BUNDLE "Build a single static .a containing everything (both code and dependencies)" ${BUILD_STATIC_DEPS}) if(STATIC_BUNDLE AND NOT BUILD_STATIC_DEPS) message(FATAL_ERROR "STATIC_BUNDLE requires BUILD_STATIC_DEPS to be enabled") @@ -91,9 +91,7 @@ option(STATIC_LIBSTD "Statically link libstdc++/libgcc" ${default_static_libstd} option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) -# Provide this as an option for now because GMP and Desktop are sometimes unhappy with each other. -option(ENABLE_NETWORKING "Build with networking functionality" ON) -option(ENABLE_NETWORKING_SROUTER "Build with session-router networking support (requires ENABLE_NETWORKING)" ON) +option(ENABLE_NETWORKING_SROUTER "Build with session-router networking support" ON) if(USE_LTO) include(CheckIPOSupported) @@ -149,9 +147,7 @@ endif() add_subdirectory(external) -if(ENABLE_NETWORKING) - session_dep(nettle 3) -endif() +session_dep(nettle 3) add_subdirectory(src) add_subdirectory(proto) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 74eb414a0..cf6911a90 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -30,35 +30,35 @@ if(NOT TARGET oxenc::oxenc) sessiondep_or_submodule(liboxenc 1.6.0 session-router/external/oxen-libquic/external/oxen-encoding oxenc::oxenc) endif() -if(ENABLE_NETWORKING) - if(ENABLE_NETWORKING_SROUTER) - set(SROUTER_FULL OFF CACHE BOOL "") - set(SROUTER_DAEMON OFF CACHE BOOL "") - set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") - set(SROUTER_JEMALLOC OFF CACHE BOOL "") - - add_static_subdirectory(session-router EXCLUDE_FROM_ALL) - libsession_static_bundle(session-router::libsessionrouter) - else() - set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") - sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic oxen::quic) - endif() +if(ENABLE_NETWORKING_SROUTER) + set(SROUTER_FULL OFF CACHE BOOL "") + set(SROUTER_DAEMON OFF CACHE BOOL "") + set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") + set(SROUTER_JEMALLOC OFF CACHE BOOL "") + + add_static_subdirectory(session-router EXCLUDE_FROM_ALL) + libsession_static_bundle(session-router::libsessionrouter) +endif() + +# The network library always needs oxen::quic. When session-router is built (SROUTER, above) it +# provides oxen::quic transitively; otherwise pull in oxen-libquic on its own. +if(NOT TARGET oxen::quic) + set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic oxen::quic) endif() if(NOT TARGET oxen::logging) - sessiondep_or_submodule(liboxen-logging 1.2.0 session-router/external/oxen-libquic/external/oxen-logging oxen::logging) - - # If we load oxen-logging via system lib then we won't necessarily have fmt/spdlog targets, - # but this script will give us them: - if(OXENLOGGING_FOUND) - if(NOT TARGET oxen::logging) - include(session-router/external/oxen-libquic/external/oxen-logging/cmake/load_fmt_spdlog.cmake) - - add_library(oxen-logging-fmt-spdlog INTERFACE) - target_link_libraries(oxen-logging-fmt-spdlog INTERFACE oxenlogging::oxenlogging ${OXEN_LOGGING_FMT_TARGET} ${OXEN_LOGGING_SPDLOG_TARGET}) - add_library(oxen::logging ALIAS oxen-logging-fmt-spdlog) - endif() + # oxen-logging can't go through sessiondep_or_submodule: its public headers expose fmt types and + # a system liboxen-logging's .pc doesn't pull in fmt/spdlog, so consumers must add them + # explicitly. oxen-logging ships a cmake/load.cmake that resolves oxen::logging for us -- either + # a system liboxen-logging bundled with suitable fmt/spdlog targets, or the submodule (which + # builds its own fmt/spdlog). This is the same loader oxen-libquic uses. We just force the + # submodule path when our other deps would also prefer submodules over system libs. + option(DEPS_FORCE_liboxen-logging_SUBMODULE "force using liboxen-logging submodule" OFF) + if(BUILD_STATIC_DEPS OR DEPS_FORCE_SUBMODULE OR DEPS_FORCE_liboxen-logging_SUBMODULE) + set(OXEN_LOGGING_FORCE_SUBMODULES ON) endif() + include(session-router/external/oxen-libquic/external/oxen-logging/cmake/load.cmake) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 958233fbe..7d87c3799 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -146,60 +146,58 @@ target_link_libraries(config sessiondep::libsodium ) -if(ENABLE_NETWORKING) - session_dep(libevent_core 2.1) - - add_libsession_util_library(network - onionreq/builder.cpp - onionreq/hop_encryption.cpp - onionreq/parser.cpp - onionreq/response_parser.cpp - network/key_types.cpp - network/network_config.cpp - network/request_queue.cpp - network/service_node.cpp - network/session_network_internal.cpp - network/session_network_types.cpp - network/session_network.cpp - network/snode_pool.cpp - network/swarm.cpp - network/backends/quic_file_client.cpp - network/backends/session_file_server.cpp - network/backends/session_open_group_server.cpp - network/transport/quic_transport.cpp - network/routing/direct_router.cpp - network/routing/onion_request_router.cpp - ) - - target_link_libraries(network - PUBLIC - crypto - oxen::quic - PRIVATE - nlohmann_json::nlohmann_json - sessiondep::libsodium - sessiondep::nettle - date::date - sessiondep::libevent_core - ) - - if(ENABLE_NETWORKING_SROUTER) - target_sources(network - PRIVATE - network/routing/session_router_router.cpp) - target_link_libraries(network PUBLIC session-router::libsessionrouter) - target_compile_definitions(network PUBLIC ENABLE_NETWORKING_SROUTER) - endif() +session_dep(libevent_core 2.1) + +add_libsession_util_library(network + onionreq/builder.cpp + onionreq/hop_encryption.cpp + onionreq/parser.cpp + onionreq/response_parser.cpp + network/key_types.cpp + network/network_config.cpp + network/request_queue.cpp + network/service_node.cpp + network/session_network_internal.cpp + network/session_network_types.cpp + network/session_network.cpp + network/snode_pool.cpp + network/swarm.cpp + network/backends/quic_file_client.cpp + network/backends/session_file_server.cpp + network/backends/session_open_group_server.cpp + network/transport/quic_transport.cpp + network/routing/direct_router.cpp + network/routing/onion_request_router.cpp +) - if (BUILD_STATIC_DEPS) - target_include_directories(network PUBLIC ${CMAKE_BINARY_DIR}/static-deps/include) - endif() +target_link_libraries(network + PUBLIC + crypto + oxen::quic + PRIVATE + nlohmann_json::nlohmann_json + sessiondep::libsodium + sessiondep::nettle + date::date + sessiondep::libevent_core +) - # core's public headers expose network::Network and it constructs network::Request, so it - # depends on the network target (defined after core above). - target_link_libraries(core PUBLIC network) +if(ENABLE_NETWORKING_SROUTER) + target_sources(network + PRIVATE + network/routing/session_router_router.cpp) + target_link_libraries(network PUBLIC session-router::libsessionrouter) + target_compile_definitions(network PUBLIC ENABLE_NETWORKING_SROUTER) endif() +if (BUILD_STATIC_DEPS) + target_include_directories(network PUBLIC ${CMAKE_BINARY_DIR}/static-deps/include) +endif() + +# core's public headers expose network::Network and it constructs network::Request, so it +# depends on the network target (defined after core above). +target_link_libraries(core PUBLIC network) + if(WARNINGS_AS_ERRORS AND NOT USE_LTO AND CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION MATCHES "^11\\.") # GCC 11 has an overzealous (and false) stringop-overread warning, but only when LTO is off. # Um, yeah. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f1c0a1823..6bd55286d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,21 +39,20 @@ set(LIB_SESSION_UTESTS_SOURCES test_session_protocol.cpp test_xed25519.cpp test_unicode_operations.cpp - test_backend_session_file_server.cpp - test_backed_session_open_group_server.cpp case_logger.cpp ) -if(ENABLE_NETWORKING) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_network_swarm.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_onionreq.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_onion_request_router.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_snode_pool.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_core_network.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_poll.cpp) - list(APPEND LIB_SESSION_UTESTS_SOURCES test_pfs_key_cache.cpp) -endif() +list(APPEND LIB_SESSION_UTESTS_SOURCES + test_backend_session_file_server.cpp + test_backed_session_open_group_server.cpp + test_network_swarm.cpp + test_onionreq.cpp + test_onion_request_router.cpp + test_snode_pool.cpp + test_core_network.cpp + test_poll.cpp + test_pfs_key_cache.cpp) add_library(test_libs INTERFACE) @@ -67,11 +66,7 @@ target_link_libraries(test_libs INTERFACE nlohmann_json::nlohmann_json oxen::logging) -if (ENABLE_NETWORKING) - target_link_libraries(test_libs INTERFACE libsession::network) -else() - target_compile_definitions(test_libs INTERFACE DISABLE_NETWORKING) -endif() +target_link_libraries(test_libs INTERFACE libsession::network) add_executable(testAll main.cpp ${LIB_SESSION_UTESTS_SOURCES}) target_link_libraries(testAll PRIVATE @@ -99,13 +94,9 @@ if(NOT TARGET check) COMMAND testAll) endif() -option(BUILD_LIVE_TESTS "Build the live testnet integration tests (requires ENABLE_NETWORKING)" ${ENABLE_NETWORKING}) +option(BUILD_LIVE_TESTS "Build the live testnet integration tests" ON) if(BUILD_LIVE_TESTS) - if(NOT ENABLE_NETWORKING) - message(FATAL_ERROR "BUILD_LIVE_TESTS requires ENABLE_NETWORKING") - endif() - add_executable(testLive live/main.cpp live/test_swarm.cpp diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index f27a59b58..c163e2f9c 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -8,9 +8,7 @@ #include "utils.hpp" -#ifndef DISABLE_NETWORKING #include -#endif using namespace session; using namespace oxen; @@ -89,7 +87,6 @@ TEST_CASE("Logging callbacks", "[logging]") { line1)); } -#ifndef DISABLE_NETWORKING TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { oxen::log::clear_sinks(); simple_logs.clear(); @@ -107,5 +104,4 @@ TEST_CASE("Logging callbacks with quic::Network", "[logging][network]") { CHECK(std::any_of(simple_logs.begin(), simple_logs.end(), [](const std::string& s) { return s.find("[quic:") != std::string::npos; })); -} -#endif \ No newline at end of file +} \ No newline at end of file From 5a4afe79383ab153191ad4466a609675dc6c990c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 16 Jul 2026 20:33:14 -0300 Subject: [PATCH 096/193] Widen revocation ticket to 64-bit; accept signed ints in json_require The revocation-list ticket was a uint32, which can wrap or be rolled back by a disaster-recovery restore, silently making a client that is 'ahead' treat the list as unchanged and miss revocations. Widen it to int64_t in the C and C++ structs, docstrings, and JSON parse. json_require previously accepted only unsigned integer types, so json_require tripped its trailing static_assert. Extend the integer branch to signed types by switching the type dispatch to STL concepts std::floating_point/std::integral/std::same_as (dropping the is_one_of type lists); the bool check moves ahead of the integral one since bool satisfies std::integral. Also drop the needless value-init of the local 'type'. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 4 ++-- include/session/pro_backend.hpp | 8 ++++---- src/pro_backend.cpp | 19 ++++++++++--------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index c84d73bd1..d154e15ba 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -187,7 +187,7 @@ typedef struct session_pro_backend_get_pro_revocations_request session_pro_backend_get_pro_revocations_request; struct session_pro_backend_get_pro_revocations_request { uint8_t version; - uint32_t ticket; + int64_t ticket; }; typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; @@ -200,7 +200,7 @@ typedef struct session_pro_backend_get_pro_revocations_response session_pro_backend_get_pro_revocations_response; struct session_pro_backend_get_pro_revocations_response { session_pro_backend_response_header header; - uint32_t ticket; + int64_t ticket; /// Array of items, with items_count elements session_pro_backend_pro_revocation_item* items; size_t items_count; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index af917509b..c5c7c44fe 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -312,10 +312,10 @@ struct GetProRevocationsRequest { /// Request version. The latest accepted version is 0 std::uint8_t version; - /// 4-byte monotonic integer for the caller's revocation list iteration. Set to 0 if unknown; + /// 64-bit monotonic integer for the caller's revocation list iteration. Set to 0 if unknown; /// otherwise, use the latest known `ticket` from a prior `GetProRevocationResponse` to allow /// the Session Pro Backend to omit the revocation list if it has not changed. - std::uint32_t ticket; + std::int64_t ticket; /// API: pro/GenerateProProofRequest::to_json /// @@ -335,9 +335,9 @@ struct ProRevocationItem { }; struct GetProRevocationsResponse : public ResponseHeader { - /// 4-byte monotonic integer for the latest revocation list iteration. + /// 64-bit monotonic integer for the latest revocation list iteration. /// Update the caller's ticket to this value for subsequent requests. - std::uint32_t ticket; + std::int64_t ticket; /// List of revoked Session Pro proofs std::vector items; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 9be8a3dd5..1a7d158d1 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -84,24 +85,24 @@ const T json_require( errors.push_back(fmt::format("Key '{}' is missing", key)); } else { bool success = false; - std::string_view type = {}; - if constexpr (session::is_one_of) { + std::string_view type; + if constexpr (std::floating_point) { type = "a float"; success = it->is_number_float(); - } else if constexpr (session::is_one_of) { + } else if constexpr (std::same_as) { + type = "a boolean"; + success = it->is_boolean(); + } else if constexpr (std::integral) { type = "a number"; success = it->is_number(); } else if constexpr (session::is_one_of) { type = "a string"; success = it->is_string(); - } else if constexpr (session::is_one_of) { + } else if constexpr (std::same_as) { type = "an array"; success = it->is_array(); - } else if constexpr (session::is_one_of) { - type = "a boolean"; - success = it->is_boolean(); } else { - static_assert(session::is_one_of); + static_assert(std::same_as); type = "an object"; success = it->is_object(); } @@ -369,7 +370,7 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json return result; // Parse payload - result.ticket = json_require(result_obj, "ticket", result.errors); + result.ticket = json_require(result_obj, "ticket", result.errors); auto array = json_require(result_obj, "items", result.errors); result.items.reserve(array.size()); From 6cc223d89a47d623e1531940c819c71de4908c12 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 00:50:17 -0300 Subject: [PATCH 097/193] Pro wire format: milliseconds -> integer seconds Port of the stable-branch conversion, adapted to the pfs hash rewrite: all Session Pro timestamps/durations become integer seconds. Values fed to hash::blake2b_pers are seconds (epoch_seconds); wire JSON keys drop the unit suffix (_unix_ts_ms -> _ts, request nonce -> ts, grace_period_duration_ms -> grace_period_duration); C++ time_points become std::chrono::sys_seconds and durations chrono::seconds; C API surface is seconds (breaking it; clients absorb it in the client phase). Per branch convention: sys_ms typedef kept, but sys_seconds written explicitly (no alias); non-Pro sent_timestamp left in ms. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 54 ++++---- include/session/pro_backend.hpp | 52 +++---- include/session/session_protocol.h | 18 +-- include/session/session_protocol.hpp | 8 +- src/pro_backend.cpp | 197 ++++++++++++--------------- src/session_protocol.cpp | 42 +++--- 6 files changed, 170 insertions(+), 201 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index d154e15ba..192d6ffe1 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -171,7 +171,7 @@ struct session_pro_backend_generate_pro_proof_request { uint8_t version; cbytes32 master_pkey; cbytes32 rotating_pkey; - uint64_t unix_ts_ms; + uint64_t ts; cbytes64 master_sig; cbytes64 rotating_sig; }; @@ -193,7 +193,7 @@ struct session_pro_backend_get_pro_revocations_request { typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { cbytes32 gen_index_hash; - uint64_t expiry_unix_ts_ms; + uint64_t expiry_ts; }; typedef struct session_pro_backend_get_pro_revocations_response @@ -212,7 +212,7 @@ struct session_pro_backend_get_pro_details_request { uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; - uint64_t unix_ts_ms; + uint64_t ts; uint32_t count; }; @@ -226,13 +226,13 @@ struct session_pro_backend_pro_payment_item { const session_pro_backend_payment_provider_metadata* payment_provider_metadata; bool auto_renewing; - uint64_t unredeemed_unix_ts_ms; - uint64_t redeemed_unix_ts_ms; - uint64_t expiry_unix_ts_ms; - uint64_t grace_period_duration_ms; - uint64_t platform_refund_expiry_unix_ts_ms; - uint64_t revoked_unix_ts_ms; - uint64_t refund_requested_unix_ts_ms; + uint64_t unredeemed_ts; + uint64_t redeemed_ts; + uint64_t expiry_ts; + uint64_t grace_period_duration; + uint64_t platform_refund_expiry_ts; + uint64_t revoked_ts; + uint64_t refund_requested_ts; char google_payment_token[128]; size_t google_payment_token_count; @@ -258,9 +258,9 @@ struct session_pro_backend_get_pro_details_response { SESSION_PRO_BACKEND_USER_PRO_STATUS status; SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; bool auto_renewing; - uint64_t expiry_unix_ts_ms; - uint64_t grace_period_duration_ms; - uint64_t refund_requested_unix_ts_ms; + uint64_t expiry_ts; + uint64_t grace_period_duration; + uint64_t refund_requested_ts; uint32_t payments_total; }; @@ -270,8 +270,8 @@ struct session_pro_backend_set_payment_refund_requested_request { uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; - uint64_t unix_ts_ms; - uint64_t refund_requested_unix_ts_ms; + uint64_t ts; + uint64_t refund_requested_ts; session_pro_backend_add_pro_payment_user_transaction payment_tx; }; @@ -355,7 +355,7 @@ session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to /// - `master_privkey_len` -- Length of master_privkey. /// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). /// - `rotating_privkey_len` -- Length of rotating_privkey. -/// - `unix_ts_ms` -- Unix timestamp for the request. +/// - `ts` -- Unix timestamp for the request. /// /// Outputs: /// - `bool` - True if signatures are built successfully, false otherwise. @@ -371,7 +371,7 @@ session_pro_backend_generate_pro_proof_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t unix_ts_ms) NON_NULL_ARG(2, 4); + uint64_t ts) NON_NULL_ARG(2, 4); /// API: session_pro_backend/generate_pro_proof_request_build_to_json /// @@ -387,7 +387,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t unix_ts_ms) NON_NULL_ARG(2, 4); + uint64_t ts) NON_NULL_ARG(2, 4); /// API: session_pro_backend/get_pro_details_request_build_sig /// @@ -398,7 +398,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build /// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. -/// - `unix_ts_ms` -- Unix timestamp for the request. +/// - `ts` -- Unix timestamp for the request. /// - `count` -- Amount of historical payments to request /// /// Outputs: @@ -411,7 +411,7 @@ session_pro_backend_signature session_pro_backend_get_pro_details_request_build_ uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, + uint64_t ts, uint32_t count) NON_NULL_ARG(2); /// API: session_pro_backend/get_pro_details_request_build_to_json @@ -426,7 +426,7 @@ session_pro_backend_to_json session_pro_backend_get_pro_details_request_build_to uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, + uint64_t ts, uint32_t count) NON_NULL_ARG(2); /// API: session_pro_backend/add_pro_payment_request_to_json @@ -515,8 +515,8 @@ session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details /// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. -/// - `unix_ts_ms` -- Unix timestamp for the request -/// - `refund_requested_unix_ts_ms` -- Unix timestamp to set as the timestamp that a refund was +/// - `ts` -- Unix timestamp for the request +/// - `refund_requested_ts` -- Unix timestamp to set as the timestamp that a refund was /// requested on this payment /// - `payment_tx_provider` -- Provider that the payment to register is coming from /// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment provider. @@ -536,8 +536,8 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, - uint64_t refund_requested_unix_ts_ms, + uint64_t ts, + uint64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, @@ -556,8 +556,8 @@ session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_req uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, - uint64_t refund_requested_unix_ts_ms, + uint64_t ts, + uint64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index c5c7c44fe..c38134bd5 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -249,7 +249,7 @@ struct GenerateProProofRequest { b32 rotating_pkey; /// Unix timestamp of the request - sys_ms unix_ts; + std::chrono::sys_seconds unix_ts; /// 64-byte signature proving knowledge of the master key's secret component b64 master_sig; @@ -275,7 +275,7 @@ struct GenerateProProofRequest { std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - sys_ms unix_ts); + std::chrono::sys_seconds unix_ts); /// API: pro/GenerateProProofRequest::build_to_json /// @@ -294,7 +294,7 @@ struct GenerateProProofRequest { std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - sys_ms unix_ts); + std::chrono::sys_seconds unix_ts); /// API: pro/GenerateProProofRequest::to_json /// @@ -331,7 +331,7 @@ struct ProRevocationItem { b32 gen_index_hash; /// Unix timestamp when the proof expires - sys_ms expiry_unix_ts; + std::chrono::sys_seconds expiry_unix_ts; }; struct GetProRevocationsResponse : public ResponseHeader { @@ -366,7 +366,7 @@ struct GetProDetailsRequest { b64 master_sig; /// Unix timestamp of the request - sys_ms unix_ts; + std::chrono::sys_seconds unix_ts; /// Max amount of historical payments to request from the backend uint32_t count; @@ -388,7 +388,7 @@ struct GetProDetailsRequest { static b64 build_sig( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - sys_ms unix_ts, + std::chrono::sys_seconds unix_ts, uint32_t count); /// API: pro/GetProDetailsRequest::build_to_json @@ -407,7 +407,7 @@ struct GetProDetailsRequest { static std::string build_to_json( std::uint8_t version, const ed25519::PrivKeySpan& master_privkey, - sys_ms unix_ts, + std::chrono::sys_seconds unix_ts, uint32_t count); /// API: pro/GenerateProProofRequest::to_json @@ -445,30 +445,30 @@ struct ProPaymentItem { bool auto_renewing; /// Unix timestamp of when the payment was witnessed by the Pro Backend. Always set - sys_ms unredeemed_unix_ts; + std::chrono::sys_seconds unredeemed_unix_ts; /// Unix timestamp of when the payment was redeemed. 0 if not activated - sys_ms redeemed_unix_ts; + std::chrono::sys_seconds redeemed_unix_ts; /// Unix timestamp of when the payment was expiry. 0 if not activated - sys_ms expiry_unix_ts; + std::chrono::sys_seconds expiry_unix_ts; /// Duration of the grace period, e.g. when the payment provider will start to attempt to renew /// the Session Pro subscription. During the period between - /// [expiry_unix_ts, expiry_unix_ts + grace_period_duration_ms] the user continues to have + /// [expiry_unix_ts, expiry_unix_ts + grace_period_duration] the user continues to have /// entitlement to Session Pro. This value is only applicable if `auto_renewing` is `true`. - std::chrono::milliseconds grace_period_duration_ms; + std::chrono::seconds grace_period_duration; /// Unix deadline timestamp of when the user is able to refund the subscription via the payment /// provider. Thereafter the user must initiate a refund manually via Session support. - sys_ms platform_refund_expiry_unix_ts; + std::chrono::sys_seconds platform_refund_expiry_unix_ts; /// Unix timestamp of when the payment was revoked or refunded. 0 if not applicable. - sys_ms revoked_unix_ts; + std::chrono::sys_seconds revoked_unix_ts; /// UNIX timestamp at which a refund request was requested for this payment. This is set to 0 /// if no refund has been requested for this payment yet. - sys_ms refund_requested_unix_ts; + std::chrono::sys_seconds refund_requested_unix_ts; /// When payment provider is set to Google Play Store, this is the platform-specific purchase /// token. This information should be considered as confidential and stored appropriately. @@ -524,10 +524,10 @@ struct GetProDetailsResponse : public ResponseHeader { /// some leeway as to the time required for the payment provider to successfully bill the user. /// This expiry timestamp is hence calculated as: /// - /// expiry_unix_ts_ms = (subscription_expiry_unix_ts + grace_period_duration_ms) + /// expiry_unix_ts_ms = (subscription_expiry_unix_ts + grace_period_duration) /// /// E.g. The subscription expiry timestamp can be calculated by subtracting - /// `grace_period_duration_ms` to determine if the user is currently in a grace period. Some + /// `grace_period_duration` to determine if the user is currently in a grace period. Some /// platforms do not support a grace period so this value can be 0. /// /// Finally, a reminder that the grace period is not activated or included in this deadline @@ -536,18 +536,18 @@ struct GetProDetailsResponse : public ResponseHeader { /// This timestamp may be in the past if the user no longer has active payments. Overtime the /// Pro Backend may prune user history and so after long lapses of activity, a user's /// subscription history may be deleted. - sys_ms expiry_unix_ts; + std::chrono::sys_seconds expiry_unix_ts; /// Duration that a user is entitled to for their grace period. This value is to be ignored if /// `auto_renewing` is false. It can be used to calculate the subscription expiry timestamp by /// subtracting `expiry_unix_ts_ms` from this value. - std::chrono::milliseconds grace_period_duration; + std::chrono::seconds grace_period_duration; /// UNIX timestamp at which a refund request was requested by this user. This timestamp comes /// from the latest payment that the backend has deemed to be active for the user (e.g. the /// payment associated with the `expiry_unix_ts_ms`). This value is 0 if no refund has been /// requested on the active payment. - sys_ms refund_requested_unix_ts; + std::chrono::sys_seconds refund_requested_unix_ts; /// Total number of payments known by the backend for the user. This may be greater than the /// length of items if the request, requested less than the number of payments the user has. @@ -577,10 +577,10 @@ struct SetPaymentRefundRequestedRequest { b64 master_sig; /// Unix timestamp of the current time - sys_ms unix_ts; + std::chrono::sys_seconds unix_ts; /// Unix timestamp to set as the timestamp that a refund was requested on this payment. - sys_ms refund_requested_unix_ts; + std::chrono::sys_seconds refund_requested_unix_ts; /// Payment details to set the refund request on AddProPaymentUserTransaction payment_tx; @@ -611,8 +611,8 @@ struct SetPaymentRefundRequestedRequest { static b64 build_sig( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - sys_ms unix_ts, - sys_ms refund_requested_unix_ts, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, std::span payment_tx_payment_id, std::span payment_tx_order_id); @@ -641,8 +641,8 @@ struct SetPaymentRefundRequestedRequest { static std::string build_to_json( std::uint8_t version, const ed25519::PrivKeySpan& master_privkey, - sys_ms unix_ts, - sys_ms refund_requested_unix_ts, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, std::span payment_tx_payment_id, std::span payment_tx_order_id); diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index d68f4f444..33e39db4f 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -81,7 +81,7 @@ struct session_protocol_pro_proof { uint8_t version; cbytes32 gen_index_hash; cbytes32 rotating_pubkey; - uint64_t expiry_unix_ts_ms; + uint64_t expiry_ts; cbytes64 sig; }; @@ -291,22 +291,22 @@ LIBSESSION_EXPORT bool session_protocol_pro_proof_verify_message( /// API: session_protocol/session_protocol_pro_proof_is_active /// -/// Check if the Pro proof is currently entitled to Pro given the `unix_ts_ms` with respect to the +/// Check if the Pro proof is currently entitled to Pro given the `ts` with respect to the /// proof's `expiry_unix_ts` /// /// Inputs: /// - `proof` -- Proof to verify -/// - `unix_ts_ms` -- The unix timestamp to check the proof expiry time against +/// - `ts` -- The unix timestamp to check the proof expiry time against /// /// Outputs: /// - `bool` -- True if expired, false otherwise LIBSESSION_EXPORT bool session_protocol_pro_proof_is_active( - session_protocol_pro_proof const* proof, uint64_t unix_ts_ms) NON_NULL_ARG(1); + session_protocol_pro_proof const* proof, uint64_t ts) NON_NULL_ARG(1); /// API: session_protocol/session_protocol_pro_proof_status /// /// Evaluate the status of the pro proof by checking it is signed by the `verify_pubkey`, it has -/// not expired via `unix_ts_ms` and optionally verify that the `signed_msg` was signed by the +/// not expired via `ts` and optionally verify that the `signed_msg` was signed by the /// `rotating_pubkey` embedded in the proof. /// /// Internally this function calls `pro_proof_verify_signature`, `pro_proof_verify_message` and @@ -320,7 +320,7 @@ LIBSESSION_EXPORT bool session_protocol_pro_proof_is_active( /// they are the original signatory of the proof. /// - `verify_pubkey_len` -- Length of the `verify_pubkey` should be 32 bytes /// they are the original signatory of the proof. -/// - `unix_ts_ms` -- Unix timestamp to compared against the embedded `expiry_unix_ts` +/// - `ts` -- Unix timestamp to compared against the embedded `expiry_unix_ts` /// to determine if the proof has expired or not /// - `signed_msg` -- Optionally set the payload to the message with the signature to verify if /// the embedded `rotating_pubkey` in the proof signed the given message. @@ -333,7 +333,7 @@ LIBSESSION_EXPORT SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( session_protocol_pro_proof const* proof, const uint8_t* verify_pubkey, size_t verify_pubkey_len, - uint64_t unix_ts_ms, + uint64_t ts, OPTIONAL const session_protocol_pro_signed_message* signed_msg) NON_NULL_ARG(1, 2); /// API: session_protocol/session_protocol_get_pro_features_for_msg @@ -719,7 +719,7 @@ LIBSESSION_EXPORT void session_protocol_decode_envelope_free( /// Inputs: /// - `content_or_envelope_payload` -- the unencrypted content or envelope payload containing the /// community message -/// - `unix_ts_ms` -- pass in the current system time which is used to determine, whether or +/// - `ts` -- pass in the current system time which is used to determine, whether or /// not the Session Pro proof has expired or not if it is in the payload. Ignored if there's no /// proof in the message. /// - `pro_backend_pubkey` -- the Session Pro backend public key to verify the signature embedded in @@ -749,7 +749,7 @@ LIBSESSION_EXPORT void session_protocol_decode_envelope_free( LIBSESSION_EXPORT session_protocol_decoded_community_message session_protocol_decode_for_community( const void* content_or_envelope_payload, size_t content_or_envelope_payload_len, - uint64_t unix_ts_ms, + uint64_t ts, OPTIONAL const void* pro_backend_pubkey, size_t pro_backend_pubkey_len, OPTIONAL char* error, diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 8febafefd..0e525f069 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -97,7 +97,7 @@ class ProProof { b32 rotating_pubkey; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid to - sys_ms expiry_unix_ts; + std::chrono::sys_seconds expiry_unix_ts; /// Signature over the contents of the proof. It is signed by the Session Pro Backend key which /// is the entity responsible for issueing tamper-proof Sesison Pro certificates for Session @@ -146,7 +146,7 @@ class ProProof { /// /// Outputs: /// - `bool` - True if proof is active (i.e. has not expired), false otherwise. - bool is_active(sys_ms unix_ts) const; + bool is_active(std::chrono::sys_seconds unix_ts) const; /// API: pro/Proof::status /// @@ -172,7 +172,7 @@ class ProProof { /// possible enum values. Otherwise this funtion can return all possible values. ProStatus status( std::span verify_pubkey, - sys_ms unix_ts, + std::chrono::sys_seconds unix_ts, const std::optional& signed_msg); /// API: pro/Proof::hash @@ -561,7 +561,7 @@ DecodedEnvelope decode_group_envelope( /// access to pro features if it's using any. DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, - sys_ms unix_ts, + std::chrono::sys_seconds unix_ts, std::span pro_backend_pubkey); } // namespace session diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 1a7d158d1..013ef341c 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -276,9 +276,8 @@ AddProPaymentOrGenerateProProofResponse AddProPaymentOrGenerateProProofResponse: // Parse payload result.proof.version = json_require(result_obj, "version", result.errors); - auto expiry_unix_ts_ms = json_require(result_obj, "expiry_unix_ts_ms", result.errors); - result.proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(expiry_unix_ts_ms)); + auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); json_require_fixed_bytes_from_hex( result_obj, "gen_index_hash", result.errors, result.proof.gen_index_hash); json_require_fixed_bytes_from_hex( @@ -292,7 +291,7 @@ std::string GenerateProProofRequest::to_json() const { j["version"] = version; j["master_pkey"] = oxenc::to_hex(master_pkey); j["rotating_pkey"] = oxenc::to_hex(rotating_pkey); - j["unix_ts_ms"] = epoch_ms(unix_ts); + j["ts"] = epoch_seconds(unix_ts); j["master_sig"] = oxenc::to_hex(master_sig); j["rotating_sig"] = oxenc::to_hex(rotating_sig); std::string result = j.dump(); @@ -303,18 +302,14 @@ MasterRotatingSignatures GenerateProProofRequest::build_sigs( std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - std::chrono::sys_time unix_ts) { + std::chrono::sys_seconds unix_ts) { // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 uint8_t version = 0; - uint64_t unix_ts_ms = epoch_ms(unix_ts); + uint64_t ts = epoch_seconds(unix_ts); auto hash_to_sign = hash::blake2b_pers<32>( - GENERATE_PROOF_PERS, - version, - master_privkey.pubkey(), - rotating_privkey.pubkey(), - unix_ts_ms); + GENERATE_PROOF_PERS, version, master_privkey.pubkey(), rotating_privkey.pubkey(), ts); MasterRotatingSignatures result = {}; result.master_sig = ed25519::sign(master_privkey, hash_to_sign); @@ -326,7 +321,7 @@ std::string GenerateProProofRequest::build_to_json( std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - std::chrono::sys_time unix_ts) { + std::chrono::sys_seconds unix_ts) { auto sigs = GenerateProProofRequest::build_sigs( request_version, master_privkey, rotating_privkey, unix_ts); @@ -384,11 +379,10 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json // Parse revocation item auto obj = it.get(); - auto expiry_unix_ts = json_require(obj, "expiry_unix_ts_ms", result.errors); + auto expiry_unix_ts = json_require(obj, "expiry_ts", result.errors); ProRevocationItem item = {}; - item.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(expiry_unix_ts)); + item.expiry_unix_ts = as_sys_seconds(expiry_unix_ts); json_require_fixed_bytes_from_hex( obj, "gen_index_hash", result.errors, item.gen_index_hash); @@ -406,7 +400,7 @@ std::string GetProDetailsRequest::to_json() const { j["version"] = version; j["master_pkey"] = oxenc::to_hex(master_pkey); j["master_sig"] = oxenc::to_hex(master_sig); - j["unix_ts_ms"] = epoch_ms(unix_ts); + j["ts"] = epoch_seconds(unix_ts); j["count"] = count; std::string result = j.dump(); return result; @@ -415,13 +409,13 @@ std::string GetProDetailsRequest::to_json() const { b64 GetProDetailsRequest::build_sig( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_time unix_ts, + std::chrono::sys_seconds unix_ts, uint32_t count) { // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 - uint64_t unix_ts_ms = epoch_ms(unix_ts); + uint64_t ts = epoch_seconds(unix_ts); auto hash_to_sign = hash::blake2b_pers<32>( - GET_PRO_DETAILS_PERS, version, master_privkey.pubkey(), unix_ts_ms, count); + GET_PRO_DETAILS_PERS, version, master_privkey.pubkey(), ts, count); return ed25519::sign(master_privkey, hash_to_sign); } @@ -429,7 +423,7 @@ b64 GetProDetailsRequest::build_sig( std::string GetProDetailsRequest::build_to_json( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_time unix_ts, + std::chrono::sys_seconds unix_ts, uint32_t count) { GetProDetailsRequest request = {}; request.version = version; @@ -483,17 +477,14 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { result.payments_total = json_require(result_obj, "payments_total", result.errors); - uint64_t expiry_unix_ts_ms = - json_require(result_obj, "expiry_unix_ts_ms", result.errors); - uint64_t grace_period_duration_ms = - json_require(result_obj, "grace_period_duration_ms", result.errors); - uint64_t refund_requested_unix_ts_ms = - json_require(result_obj, "refund_requested_unix_ts_ms", result.errors); - result.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(expiry_unix_ts_ms)); - result.grace_period_duration = std::chrono::milliseconds(grace_period_duration_ms); - result.refund_requested_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(refund_requested_unix_ts_ms)); + uint64_t expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + uint64_t grace_period_duration = + json_require(result_obj, "grace_period_duration", result.errors); + uint64_t refund_requested_ts = + json_require(result_obj, "refund_requested_ts", result.errors); + result.expiry_unix_ts = as_sys_seconds(expiry_ts); + result.grace_period_duration = std::chrono::seconds(grace_period_duration); + result.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); auto array = json_require(result_obj, "items", result.errors); result.items.reserve(array.size()); @@ -511,16 +502,16 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { auto plan = json_require(obj, "plan", result.errors); auto payment_provider = json_require(obj, "payment_provider", result.errors); auto auto_renewing = json_require(obj, "auto_renewing", result.errors); - auto unredeemed_ts = json_require(obj, "unredeemed_unix_ts_ms", result.errors); - auto redeemed_ts = json_require(obj, "redeemed_unix_ts_ms", result.errors); - auto expiry_ts = json_require(obj, "expiry_unix_ts_ms", result.errors); - auto grace_period_duration_ms = - json_require(obj, "grace_period_duration_ms", result.errors); + auto unredeemed_ts = json_require(obj, "unredeemed_ts", result.errors); + auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); + auto expiry_ts = json_require(obj, "expiry_ts", result.errors); + auto grace_period_duration = + json_require(obj, "grace_period_duration", result.errors); auto platform_refund_expiry_ts = - json_require(obj, "platform_refund_expiry_unix_ts_ms", result.errors); - auto revoked_ts = json_require(obj, "revoked_unix_ts_ms", result.errors); + json_require(obj, "platform_refund_expiry_ts", result.errors); + auto revoked_ts = json_require(obj, "revoked_ts", result.errors); auto refund_requested_ts = - json_require(obj, "refund_requested_unix_ts_ms", result.errors); + json_require(obj, "refund_requested_ts", result.errors); ProPaymentItem item = {}; if (status > SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL && @@ -548,19 +539,13 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { } item.auto_renewing = auto_renewing; - item.unredeemed_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(unredeemed_ts)); - item.redeemed_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(redeemed_ts)); - item.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(expiry_ts)); - item.grace_period_duration_ms = std::chrono::milliseconds(grace_period_duration_ms); - item.platform_refund_expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(platform_refund_expiry_ts)); - item.revoked_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(revoked_ts)); - item.refund_requested_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(refund_requested_ts)); + item.unredeemed_unix_ts = as_sys_seconds(unredeemed_ts); + item.redeemed_unix_ts = as_sys_seconds(redeemed_ts); + item.expiry_unix_ts = as_sys_seconds(expiry_ts); + item.grace_period_duration = std::chrono::seconds(grace_period_duration); + item.platform_refund_expiry_unix_ts = as_sys_seconds(platform_refund_expiry_ts); + item.revoked_unix_ts = as_sys_seconds(revoked_ts); + item.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); switch (item.payment_provider) { case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: [[fallthrough]]; case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: { @@ -609,21 +594,21 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { b64 SetPaymentRefundRequestedRequest::build_sig( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_time unix_ts, - std::chrono::sys_time refund_requested_unix_ts, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, std::span payment_tx_payment_id, std::span payment_tx_order_id) { // Hash components to 32 bytes, must match: // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 - uint64_t unix_ts_ms = epoch_ms(unix_ts); - uint64_t refund_requested_unix_ts_ms = epoch_ms(refund_requested_unix_ts); + uint64_t ts = epoch_seconds(unix_ts); + uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); auto hash_to_sign = hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, version, master_privkey.pubkey(), - unix_ts_ms, - refund_requested_unix_ts_ms, + ts, + refund_requested_ts, static_cast(payment_tx_provider), payment_tx_payment_id, payment_tx_order_id); @@ -634,8 +619,8 @@ b64 SetPaymentRefundRequestedRequest::build_sig( std::string SetPaymentRefundRequestedRequest::build_to_json( uint8_t version, const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_time unix_ts, - std::chrono::sys_time refund_requested_unix_ts, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, std::span payment_tx_payment_id, std::span payment_tx_order_id) { @@ -665,8 +650,8 @@ std::string SetPaymentRefundRequestedRequest::to_json() const { nlohmann::json j; j["version"] = version; j["master_pkey"] = oxenc::to_hex(master_pkey); - j["unix_ts_ms"] = epoch_ms(unix_ts); - j["refund_requested_unix_ts_ms"] = epoch_ms(refund_requested_unix_ts); + j["ts"] = epoch_seconds(unix_ts); + j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); j["payment_tx"]["provider"] = payment_tx.provider; switch (payment_tx.provider) { case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; @@ -805,18 +790,14 @@ session_pro_backend_generate_pro_proof_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t unix_ts_ms) { + uint64_t ts) { session_pro_backend_master_rotating_signatures result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - std::chrono::milliseconds ts{unix_ts_ms}; auto sigs = GenerateProProofRequest::build_sigs( - request_version, - master_span, - rotating_span, - std::chrono::sys_time(ts)); + request_version, master_span, rotating_span, session::as_sys_seconds(ts)); std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; @@ -833,17 +814,13 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t unix_ts_ms) { + uint64_t ts) { session_pro_backend_to_json result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - std::chrono::milliseconds ts{unix_ts_ms}; auto json = GenerateProProofRequest::build_to_json( - request_version, - master_span, - rotating_span, - std::chrono::sys_time(ts)); + request_version, master_span, rotating_span, session::as_sys_seconds(ts)); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -857,13 +834,13 @@ session_pro_backend_get_pro_details_request_build_sig( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, + uint64_t ts, uint32_t count) { session_pro_backend_signature result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; - auto sig = GetProDetailsRequest::build_sig(request_version, master_span, ts, count); + auto sig = GetProDetailsRequest::build_sig( + request_version, master_span, session::as_sys_seconds(ts), count); std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; } catch (const std::exception& e) { @@ -877,13 +854,13 @@ session_pro_backend_get_pro_details_request_build_to_json( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, + uint64_t ts, uint32_t count) { session_pro_backend_to_json result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time ts{std::chrono::milliseconds(unix_ts_ms)}; - auto json = GetProDetailsRequest::build_to_json(request_version, master_span, ts, count); + auto json = GetProDetailsRequest::build_to_json( + request_version, master_span, session::as_sys_seconds(ts), count); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -933,8 +910,7 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_generate_pro_pr cpp.version = request->version; std::memcpy(cpp.master_pkey.data(), request->master_pkey.data, cpp.master_pkey.size()); std::memcpy(cpp.rotating_pkey.data(), request->rotating_pkey.data, cpp.rotating_pkey.size()); - cpp.unix_ts = std::chrono::sys_time{ - std::chrono::milliseconds(request->unix_ts_ms)}; + cpp.unix_ts = session::as_sys_seconds(request->ts); std::memcpy(cpp.master_sig.data(), request->master_sig.data, cpp.master_sig.size()); std::memcpy(cpp.rotating_sig.data(), request->rotating_sig.data, cpp.rotating_sig.size()); @@ -984,8 +960,7 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details std::memcpy( cpp.master_pkey.data(), request->master_pkey.data, sizeof(request->master_pkey.data)); std::memcpy(cpp.master_sig.data(), request->master_sig.data, sizeof(request->master_sig.data)); - cpp.unix_ts = std::chrono::sys_time{ - std::chrono::milliseconds{request->unix_ts_ms}}; + cpp.unix_ts = session::as_sys_seconds(request->ts); cpp.count = request->count; try { @@ -1039,7 +1014,7 @@ session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( // error response returns the same struct just with different fields populated. result.header.status = cpp.status; result.proof.version = cpp.proof.version; - result.proof.expiry_unix_ts_ms = session::epoch_ms(cpp.proof.expiry_unix_ts); + result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_unix_ts); std::memcpy( result.proof.gen_index_hash.data, cpp.proof.gen_index_hash.data(), @@ -1121,7 +1096,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t const ProRevocationItem& src = cpp.items[index]; session_pro_backend_pro_revocation_item& dest = result.items[index]; std::memcpy(dest.gen_index_hash.data, src.gen_index_hash.data(), src.gen_index_hash.size()); - dest.expiry_unix_ts_ms = session::epoch_ms(src.expiry_unix_ts); + dest.expiry_ts = session::epoch_seconds(src.expiry_unix_ts); } return result; } @@ -1160,7 +1135,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json result.header.internal_arena_buf_ = arena.data; } - using session::epoch_ms; + using session::epoch_seconds; // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. result.header.status = cpp.status; @@ -1170,9 +1145,9 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json result.items = (session_pro_backend_pro_payment_item*)arena_alloc( &arena, result.items_count * sizeof(*result.items)); result.auto_renewing = cpp.auto_renewing; - result.expiry_unix_ts_ms = epoch_ms(cpp.expiry_unix_ts); - result.grace_period_duration_ms = cpp.grace_period_duration.count(); - result.refund_requested_unix_ts_ms = epoch_ms(cpp.refund_requested_unix_ts); + result.expiry_ts = epoch_seconds(cpp.expiry_unix_ts); + result.grace_period_duration = cpp.grace_period_duration.count(); + result.refund_requested_ts = epoch_seconds(cpp.refund_requested_unix_ts); result.payments_total = cpp.payments_total; for (size_t index = 0; index < result.items_count; ++index) { @@ -1182,13 +1157,13 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json dest.plan = src.plan; dest.payment_provider = src.payment_provider; dest.payment_provider_metadata = src.payment_provider_metadata; - dest.unredeemed_unix_ts_ms = epoch_ms(src.unredeemed_unix_ts); - dest.redeemed_unix_ts_ms = epoch_ms(src.redeemed_unix_ts); - dest.expiry_unix_ts_ms = epoch_ms(src.expiry_unix_ts); - dest.grace_period_duration_ms = session::duration_ms(src.grace_period_duration_ms); - dest.platform_refund_expiry_unix_ts_ms = epoch_ms(src.platform_refund_expiry_unix_ts); - dest.revoked_unix_ts_ms = epoch_ms(src.revoked_unix_ts); - dest.refund_requested_unix_ts_ms = epoch_ms(src.refund_requested_unix_ts); + dest.unredeemed_ts = epoch_seconds(src.unredeemed_unix_ts); + dest.redeemed_ts = epoch_seconds(src.redeemed_unix_ts); + dest.expiry_ts = epoch_seconds(src.expiry_unix_ts); + dest.grace_period_duration = src.grace_period_duration.count(); + dest.platform_refund_expiry_ts = epoch_seconds(src.platform_refund_expiry_unix_ts); + dest.revoked_ts = epoch_seconds(src.revoked_unix_ts); + dest.refund_requested_ts = epoch_seconds(src.refund_requested_unix_ts); switch (dest.payment_provider) { case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; @@ -1235,8 +1210,8 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, - uint64_t refund_requested_unix_ts_ms, + uint64_t ts, + uint64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, @@ -1245,10 +1220,9 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r session_pro_backend_signature result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time unix_ts{ - std::chrono::milliseconds(unix_ts_ms)}; - std::chrono::sys_time refund_requested_unix_ts{ - std::chrono::milliseconds(refund_requested_unix_ts_ms)}; + std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); + std::chrono::sys_seconds refund_requested_unix_ts = + session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); @@ -1273,8 +1247,8 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t unix_ts_ms, - uint64_t refund_requested_unix_ts_ms, + uint64_t ts, + uint64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, @@ -1284,10 +1258,9 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( session_pro_backend_to_json result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_time unix_ts{ - std::chrono::milliseconds(unix_ts_ms)}; - std::chrono::sys_time refund_requested_unix_ts{ - std::chrono::milliseconds(refund_requested_unix_ts_ms)}; + std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); + std::chrono::sys_seconds refund_requested_unix_ts = + session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); @@ -1320,10 +1293,8 @@ session_pro_backend_set_payment_refund_requested_request_to_json( std::memcpy( cpp.master_pkey.data(), request->master_pkey.data, sizeof(request->master_pkey.data)); std::memcpy(cpp.master_sig.data(), request->master_sig.data, sizeof(request->master_sig.data)); - cpp.unix_ts = std::chrono::sys_time{ - std::chrono::milliseconds{request->unix_ts_ms}}; - cpp.refund_requested_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds{request->refund_requested_unix_ts_ms}); + cpp.unix_ts = session::as_sys_seconds(request->ts); + cpp.refund_requested_unix_ts = session::as_sys_seconds(request->refund_requested_ts); cpp.payment_tx.provider = request->payment_tx.provider; cpp.payment_tx.payment_id = std::string(request->payment_tx.payment_id, request->payment_tx.payment_id_count); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 203d8ddc6..e7284f9f7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -54,12 +54,12 @@ session::b32 proof_hash_internal( std::uint8_t version, std::span gen_index_hash, std::span rotating_pubkey, - std::uint64_t expiry_unix_ts_ms) { + std::uint64_t expiry_ts) { // This must match the hashing routine at // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 return session::hash::blake2b_pers<32>( - session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); + session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_ts); } struct array_uc32_from_ptr_result { @@ -101,7 +101,7 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP result.proof.rotating_pubkey.data, cpp.proof.rotating_pubkey.data(), cpp.proof.rotating_pubkey.max_size()); - result.proof.expiry_unix_ts_ms = session::epoch_ms(cpp.proof.expiry_unix_ts); + result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_unix_ts); std::memcpy(result.proof.sig.data, cpp.proof.sig.data(), cpp.proof.sig.max_size()); result.msg_bitset.data = cpp.msg_bitset.data; result.profile_bitset.data = cpp.profile_bitset.data; @@ -124,13 +124,13 @@ bool ProProof::verify_message( return ed25519::verify(sig, rotating_pubkey, msg); } -bool ProProof::is_active(std::chrono::sys_time unix_ts) const { +bool ProProof::is_active(std::chrono::sys_seconds unix_ts) const { return unix_ts <= expiry_unix_ts; } ProStatus ProProof::status( std::span verify_pubkey, - std::chrono::sys_time unix_ts, + std::chrono::sys_seconds unix_ts, const std::optional& signed_msg) { ProStatus result = ProStatus::Valid; // Verify the at the proof is verified by the Session Pro Backend key (e.g.: It was @@ -152,7 +152,7 @@ ProStatus ProProof::status( b32 ProProof::hash() const { b32 result = proof_hash_internal( - version, gen_index_hash, rotating_pubkey, expiry_unix_ts.time_since_epoch().count()); + version, gen_index_hash, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); return result; } @@ -548,8 +548,7 @@ static void parse_content_and_pro( proof.rotating_pubkey.data(), proto_proof.rotatingpublickey().data(), proto_proof.rotatingpublickey().size()); - proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(proto_proof.expiryunixts())); + proof.expiry_unix_ts = session::as_sys_seconds(proto_proof.expiryunixts()); std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); // Evaluate the pro status given the extracted components (was it signed, is it expired, @@ -561,8 +560,9 @@ static void parse_content_and_pro( }; // Note that we sign the envelope content wholesale. For 1o1 which are padded to 160 // bytes, this means that we expected the user to have signed the padding as well. - auto unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(content.sigtimestamp())); + auto unix_ts = std::chrono::floor( + std::chrono::sys_time( + std::chrono::milliseconds(content.sigtimestamp()))); pro.status = proof.status(pro_backend_pubkey, unix_ts, signed_msg); } } @@ -650,7 +650,7 @@ DecodedEnvelope decode_group_envelope( DecodedCommunityMessage decode_for_community( std::span content_or_envelope_payload, - std::chrono::sys_time unix_ts, + std::chrono::sys_seconds unix_ts, std::span pro_backend_pubkey) { // TODO: Community message parsing requires a custom code path for now as we are planning to // migrate from sending plain `Content` to `Content` with a pro signature embedded in `Content` @@ -787,8 +787,7 @@ DecodedCommunityMessage decode_for_community( proof.rotating_pubkey.data(), proto_proof.rotatingpublickey().data(), proto_proof.rotatingpublickey().size()); - proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(proto_proof.expiryunixts())); + proof.expiry_unix_ts = session::as_sys_seconds(proto_proof.expiryunixts()); std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); // Evaluate the pro status given the extracted components (was it signed, is it expired, @@ -890,7 +889,7 @@ LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_p proof->version, to_byte_span(proof->gen_index_hash.data), to_byte_span(proof->rotating_pubkey.data), - proof->expiry_unix_ts_ms); + proof->expiry_ts); std::memcpy(result.data, hash.data(), hash.size()); return result; } @@ -905,7 +904,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( proof->version, to_byte_span(proof->gen_index_hash.data), to_byte_span(proof->rotating_pubkey.data), - proof->expiry_unix_ts_ms); + proof->expiry_ts); return ed25519::verify(to_byte_span(proof->sig.data), to_byte_span<32>(verify_pubkey), hash); } @@ -924,15 +923,15 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( } LIBSESSION_C_API bool session_protocol_pro_proof_is_active( - session_protocol_pro_proof const* proof, uint64_t unix_ts_ms) { - return unix_ts_ms <= proof->expiry_unix_ts_ms; + session_protocol_pro_proof const* proof, uint64_t ts) { + return ts <= proof->expiry_ts; } LIBSESSION_C_API SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( session_protocol_pro_proof const* proof, const uint8_t* verify_pubkey, size_t verify_pubkey_len, - uint64_t unix_ts_ms, + uint64_t ts, const session_protocol_pro_signed_message* signed_msg) { SESSION_PROTOCOL_PRO_STATUS result = SESSION_PROTOCOL_PRO_STATUS_VALID; if (!session_protocol_pro_proof_verify_signature(proof, verify_pubkey, verify_pubkey_len)) @@ -951,7 +950,7 @@ LIBSESSION_C_API SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( // Check if the proof has expired if (result == SESSION_PROTOCOL_PRO_STATUS_VALID && - !session_protocol_pro_proof_is_active(proof, unix_ts_ms)) + !session_protocol_pro_proof_is_active(proof, ts)) result = SESSION_PROTOCOL_PRO_STATUS_EXPIRED; return result; } @@ -1222,7 +1221,7 @@ LIBSESSION_C_API session_protocol_decoded_community_message session_protocol_decode_for_community( const void* content_or_envelope_payload, size_t content_or_envelope_payload_len, - uint64_t unix_ts_ms, + uint64_t ts, OPTIONAL const void* pro_backend_pubkey, size_t pro_backend_pubkey_len, OPTIONAL char* error, @@ -1231,8 +1230,7 @@ session_protocol_decoded_community_message session_protocol_decode_for_community std::span content_or_envelope_payload_span{ static_cast(content_or_envelope_payload), content_or_envelope_payload_len}; - auto unix_ts = - std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); + auto unix_ts = session::as_sys_seconds(ts); array_uc32_from_ptr_result pro_backend_pubkey_cpp = array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); if (!pro_backend_pubkey_cpp.success) { From 0b00131adc6311e0f782bfef86ecb41f6f34a3cb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 15:41:14 -0300 Subject: [PATCH 098/193] Pro & config timestamps: milliseconds -> integer seconds, signed int64_t Extends the ms->seconds wire-format change (a5e1326) into the config layer, and switches all Pro timestamp/duration fields from uint64_t to signed int64_t to match the rest of the codebase (unsigned bought nothing for wall-clock values). - config (convo_info_volatile / user_profile / config-pro): proof expiry, contact pro-expiry, and pro access-expiry are stored and exposed in seconds; synced dict keys "e"/"E" reused (safe: Pro config is not live in shipped clients). Drops the _ms C-API name suffixes. - pro_backend + session_protocol C API: timestamps/durations uint64_t -> int64_t. Bitsets, counts, flags, and the hash-entry values fed to blake2b_pers are left unsigned. - tests: field/key/unit updates; fix test_session_protocol, which a5e1326 left encoding expiryunixts and passing decode ts in ms after the code moved to seconds -- broke the proof-signature round-trip, undetected because the suite could not previously link. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/config/convo_info_volatile.h | 16 +-- .../session/config/convo_info_volatile.hpp | 6 +- include/session/config/user_profile.h | 16 +-- include/session/config/user_profile.hpp | 13 +- include/session/pro_backend.h | 46 +++--- include/session/session_protocol.h | 8 +- src/config/convo_info_volatile.cpp | 21 ++- src/config/pro.cpp | 7 +- src/config/user_profile.cpp | 33 ++--- src/pro_backend.cpp | 45 +++--- src/session_protocol.cpp | 6 +- tests/test_config_convo_info_volatile.cpp | 9 +- tests/test_config_pro.cpp | 11 +- tests/test_config_userprofile.cpp | 11 +- tests/test_pro_backend.cpp | 131 +++++++++--------- tests/test_session_protocol.cpp | 14 +- 16 files changed, 186 insertions(+), 207 deletions(-) diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index b00c14e85..c217de52e 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -14,10 +14,10 @@ typedef struct convo_info_volatile_1to1 { int64_t last_read; // milliseconds since unix epoch bool unread; // true if the conversation is explicitly marked unread - bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend - uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to - // Session Pro features is valid to + bool has_pro_gen_index_hash; // Flag indicating if hash is set + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + int64_t pro_expiry_ts; // Unix epoch timestamp (seconds) until which this contact's entitlement + // to Session Pro features is valid } convo_info_volatile_1to1; typedef struct convo_info_volatile_community { @@ -51,10 +51,10 @@ typedef struct convo_info_volatile_blinded_1to1 { int64_t last_read; // ms since unix epoch bool unread; // true if the conversation is explicitly marked unread - bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend - uint64_t pro_expiry_unix_ts_ms; // Unix epoch timestamp to which this contacts entitlement to - // Session Pro features is valid to + bool has_pro_gen_index_hash; // Flag indicating if hash is set + cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + int64_t pro_expiry_ts; // Unix epoch timestamp (seconds) until which this contact's entitlement + // to Session Pro features is valid } convo_info_volatile_blinded_1to1; /// API: convo_info_volatile/convo_info_volatile_init diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index c2e657415..81814eee4 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -90,9 +90,9 @@ namespace convo { /// Hash of the generation index set by the Session Pro Backend std::optional pro_gen_index_hash; - /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid - /// to - sys_ms pro_expiry_unix_ts{}; + /// Unix epoch timestamp (seconds) until which this proof's entitlement to Session Pro + /// features is valid + std::chrono::sys_seconds pro_expiry_unix_ts{}; protected: using base::base; diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 1cd273fa6..9aad84ada 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -375,7 +375,7 @@ LIBSESSION_EXPORT void user_profile_set_pro_badge(config_object* conf, bool enab /// - `enabled` -- Flag which specifies whether the users display picture is animated or not. LIBSESSION_EXPORT void user_profile_set_animated_avatar(config_object* conf, bool enabled); -/// API: user_profile/user_profile_get_pro_access_expiry_ms +/// API: user_profile/user_profile_get_pro_access_expiry /// /// Retrieves the Session Pro access expiry unix timestamp if it has been set, this should generally /// be the expiry value returned from /get_pro_details. @@ -384,20 +384,20 @@ LIBSESSION_EXPORT void user_profile_set_animated_avatar(config_object* conf, boo /// - `conf` -- [in] Pointer to the config object /// /// Outputs: -/// - `uint64_t` - The unix timestamp in milliseconds that the users pro access will expire, or 0 if +/// - `int64_t` - The unix timestamp in seconds that the users pro access will expire, or 0 if /// unset. -LIBSESSION_EXPORT uint64_t user_profile_get_pro_access_expiry_ms(const config_object* conf); +LIBSESSION_EXPORT int64_t user_profile_get_pro_access_expiry(const config_object* conf); -/// API: user_profile/user_profile_set_pro_access_expiry_ms +/// API: user_profile/user_profile_set_pro_access_expiry /// /// Updates the Session Pro access expiry unix timestamp. /// /// Inputs: /// - `conf` -- [in] Pointer to the config object -/// - `access_expiry_ts_ms` -- The timestamp that the users Session Pro access will expire, or 0 to -/// remove the value. -LIBSESSION_EXPORT void user_profile_set_pro_access_expiry_ms( - config_object* conf, uint64_t access_expiry_ts_ms); +/// - `access_expiry_ts` -- The timestamp (unix epoch seconds) that the users Session Pro access +/// will expire, or 0 to remove the value. +LIBSESSION_EXPORT void user_profile_set_pro_access_expiry( + config_object* conf, int64_t access_expiry_ts); #ifdef __cplusplus } // extern "C" diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index d994da399..a98980004 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -316,19 +316,18 @@ class UserProfile : public ConfigBase { /// Inputs: None /// /// Outputs: - /// - `std::optional` - The unix timestamp in - /// milliseconds that the users pro access will expire, or nullopt if unset. - std::optional get_pro_access_expiry() const; + /// - `std::optional` - The unix timestamp in + /// seconds that the users pro access will expire, or nullopt if unset. + std::optional get_pro_access_expiry() const; /// API: user_profile/UserProfile::set_pro_access_expiry /// /// Updates the Session Pro access expiry unix timestamp. /// /// Inputs: - /// - `access_expiry_ts_ms` -- The timestamp that the users Session Pro access will expire, or - /// nullopt to remove the value. - void set_pro_access_expiry( - std::optional> access_expiry_ts_ms); + /// - `access_expiry_ts` -- The timestamp (unix epoch seconds) that the users Session Pro access + /// will expire, or nullopt to remove the value. + void set_pro_access_expiry(std::optional access_expiry_ts); }; } // namespace session::config diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 192d6ffe1..462894b1f 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -171,7 +171,7 @@ struct session_pro_backend_generate_pro_proof_request { uint8_t version; cbytes32 master_pkey; cbytes32 rotating_pkey; - uint64_t ts; + int64_t ts; cbytes64 master_sig; cbytes64 rotating_sig; }; @@ -193,7 +193,7 @@ struct session_pro_backend_get_pro_revocations_request { typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { cbytes32 gen_index_hash; - uint64_t expiry_ts; + int64_t expiry_ts; }; typedef struct session_pro_backend_get_pro_revocations_response @@ -212,7 +212,7 @@ struct session_pro_backend_get_pro_details_request { uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; - uint64_t ts; + int64_t ts; uint32_t count; }; @@ -226,13 +226,13 @@ struct session_pro_backend_pro_payment_item { const session_pro_backend_payment_provider_metadata* payment_provider_metadata; bool auto_renewing; - uint64_t unredeemed_ts; - uint64_t redeemed_ts; - uint64_t expiry_ts; - uint64_t grace_period_duration; - uint64_t platform_refund_expiry_ts; - uint64_t revoked_ts; - uint64_t refund_requested_ts; + int64_t unredeemed_ts; + int64_t redeemed_ts; + int64_t expiry_ts; + int64_t grace_period_duration; + int64_t platform_refund_expiry_ts; + int64_t revoked_ts; + int64_t refund_requested_ts; char google_payment_token[128]; size_t google_payment_token_count; @@ -258,9 +258,9 @@ struct session_pro_backend_get_pro_details_response { SESSION_PRO_BACKEND_USER_PRO_STATUS status; SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; bool auto_renewing; - uint64_t expiry_ts; - uint64_t grace_period_duration; - uint64_t refund_requested_ts; + int64_t expiry_ts; + int64_t grace_period_duration; + int64_t refund_requested_ts; uint32_t payments_total; }; @@ -270,8 +270,8 @@ struct session_pro_backend_set_payment_refund_requested_request { uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; - uint64_t ts; - uint64_t refund_requested_ts; + int64_t ts; + int64_t refund_requested_ts; session_pro_backend_add_pro_payment_user_transaction payment_tx; }; @@ -371,7 +371,7 @@ session_pro_backend_generate_pro_proof_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t ts) NON_NULL_ARG(2, 4); + int64_t ts) NON_NULL_ARG(2, 4); /// API: session_pro_backend/generate_pro_proof_request_build_to_json /// @@ -387,7 +387,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t ts) NON_NULL_ARG(2, 4); + int64_t ts) NON_NULL_ARG(2, 4); /// API: session_pro_backend/get_pro_details_request_build_sig /// @@ -411,7 +411,7 @@ session_pro_backend_signature session_pro_backend_get_pro_details_request_build_ uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, + int64_t ts, uint32_t count) NON_NULL_ARG(2); /// API: session_pro_backend/get_pro_details_request_build_to_json @@ -426,7 +426,7 @@ session_pro_backend_to_json session_pro_backend_get_pro_details_request_build_to uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, + int64_t ts, uint32_t count) NON_NULL_ARG(2); /// API: session_pro_backend/add_pro_payment_request_to_json @@ -536,8 +536,8 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, - uint64_t refund_requested_ts, + int64_t ts, + int64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, @@ -556,8 +556,8 @@ session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_req uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, - uint64_t refund_requested_ts, + int64_t ts, + int64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 33e39db4f..0293abbc8 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -81,7 +81,7 @@ struct session_protocol_pro_proof { uint8_t version; cbytes32 gen_index_hash; cbytes32 rotating_pubkey; - uint64_t expiry_ts; + int64_t expiry_ts; cbytes64 sig; }; @@ -301,7 +301,7 @@ LIBSESSION_EXPORT bool session_protocol_pro_proof_verify_message( /// Outputs: /// - `bool` -- True if expired, false otherwise LIBSESSION_EXPORT bool session_protocol_pro_proof_is_active( - session_protocol_pro_proof const* proof, uint64_t ts) NON_NULL_ARG(1); + session_protocol_pro_proof const* proof, int64_t ts) NON_NULL_ARG(1); /// API: session_protocol/session_protocol_pro_proof_status /// @@ -333,7 +333,7 @@ LIBSESSION_EXPORT SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( session_protocol_pro_proof const* proof, const uint8_t* verify_pubkey, size_t verify_pubkey_len, - uint64_t ts, + int64_t ts, OPTIONAL const session_protocol_pro_signed_message* signed_msg) NON_NULL_ARG(1, 2); /// API: session_protocol/session_protocol_get_pro_features_for_msg @@ -749,7 +749,7 @@ LIBSESSION_EXPORT void session_protocol_decode_envelope_free( LIBSESSION_EXPORT session_protocol_decoded_community_message session_protocol_decode_for_community( const void* content_or_envelope_payload, size_t content_or_envelope_payload_len, - uint64_t ts, + int64_t ts, OPTIONAL const void* pro_backend_pubkey, size_t pro_backend_pubkey_len, OPTIONAL char* error, diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 41f49b5a1..e28e00fc6 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -38,8 +38,7 @@ namespace convo { pro_gen_index_hash->data(), c.pro_gen_index_hash.data, pro_gen_index_hash->size()); - pro_expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(c.pro_expiry_unix_ts_ms)); + pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); } } @@ -55,10 +54,10 @@ namespace convo { pro_gen_index_hash->data(), pro_gen_index_hash->size()); - c.pro_expiry_unix_ts_ms = epoch_ms(pro_expiry_unix_ts); + c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); } else { c.has_pro_gen_index_hash = false; - c.pro_expiry_unix_ts_ms = 0; + c.pro_expiry_ts = 0; } } @@ -134,8 +133,7 @@ namespace convo { pro_gen_index_hash->data(), c.pro_gen_index_hash.data, pro_gen_index_hash->size()); - pro_expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(c.pro_expiry_unix_ts_ms)); + pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); } } @@ -151,10 +149,10 @@ namespace convo { c.pro_gen_index_hash.data, pro_gen_index_hash->data(), pro_gen_index_hash->size()); - c.pro_expiry_unix_ts_ms = epoch_ms(pro_expiry_unix_ts); + c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); } else { c.has_pro_gen_index_hash = false; - c.pro_expiry_unix_ts_ms = 0; + c.pro_expiry_ts = 0; } } @@ -165,8 +163,7 @@ namespace convo { std::optional> maybe_pro_gen_index_hash = maybe_vector(info_dict, "g"); if (pro_expiry > 0 && maybe_pro_gen_index_hash && maybe_pro_gen_index_hash->size() == 32) { - pro_expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(pro_expiry)); + pro_expiry_unix_ts = as_sys_seconds(pro_expiry); pro_gen_index_hash.emplace(); std::memcpy( pro_gen_index_hash->data(), @@ -336,7 +333,7 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { auto info = data["1"][session_id_to_bytes(c.session_id)]; set_base(c, info); - auto pro_expiry = epoch_ms(c.pro_expiry_unix_ts); + auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); if (pro_expiry > 0 && c.pro_gen_index_hash) { set_nonzero_int(info["e"], pro_expiry); info["g"] = to_span(*c.pro_gen_index_hash); @@ -442,7 +439,7 @@ void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { set_nonzero_int(info["y"], c.legacy_blinding); - auto pro_expiry = epoch_ms(c.pro_expiry_unix_ts); + auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); if (pro_expiry > 0 && c.pro_gen_index_hash) { set_nonzero_int(info["e"], pro_expiry); info["g"] = to_span(*c.pro_gen_index_hash); diff --git a/src/config/pro.cpp b/src/config/pro.cpp index eda2bb5a3..f91c908a1 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -28,8 +28,7 @@ bool ProConfig::load(const dict& root) { { std::optional version = maybe_int(*p, "@"); std::optional> maybe_gen_index_hash = maybe_vector(*p, "g"); - std::optional> maybe_expiry_unix_ts_ms = - maybe_ts_ms(*p, "e"); + std::optional maybe_expiry = maybe_ts(*p, "e"); std::optional> maybe_sig = maybe_vector(*p, "s"); if (!version) @@ -38,7 +37,7 @@ bool ProConfig::load(const dict& root) { return false; if (!maybe_sig || maybe_sig->size() != proof.sig.max_size()) return false; - if (!maybe_expiry_unix_ts_ms) + if (!maybe_expiry) return false; proof.version = *version; @@ -46,7 +45,7 @@ bool ProConfig::load(const dict& root) { proof.gen_index_hash.data(), maybe_gen_index_hash->data(), proof.gen_index_hash.size()); - proof.expiry_unix_ts = *maybe_expiry_unix_ts_ms; + proof.expiry_unix_ts = *maybe_expiry; std::memcpy(proof.sig.data(), maybe_sig->data(), proof.sig.size()); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 77e5875fc..10f70addf 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -168,7 +168,7 @@ void UserProfile::set_pro_config(const ProConfig& pro) { auto proof_dict = root["p"]; proof_dict["@"] = pro.proof.version; proof_dict["g"] = pro.proof.gen_index_hash; - proof_dict["e"] = pro.proof.expiry_unix_ts.time_since_epoch().count(); + proof_dict["e"] = epoch_seconds(pro.proof.expiry_unix_ts); proof_dict["s"] = pro.proof.sig; const auto target_timestamp = @@ -210,17 +210,15 @@ void UserProfile::set_animated_avatar(bool enabled) { } } -std::optional> UserProfile::get_pro_access_expiry() - const { +std::optional UserProfile::get_pro_access_expiry() const { if (auto* E = data["E"].integer(); E) - return std::chrono::sys_time{std::chrono::milliseconds{*E}}; + return std::chrono::sys_seconds{std::chrono::seconds{*E}}; return std::nullopt; } -void UserProfile::set_pro_access_expiry( - std::optional> access_expiry_ts_ms) { - if (access_expiry_ts_ms) - data["E"] = epoch_ms(*access_expiry_ts_ms); +void UserProfile::set_pro_access_expiry(std::optional access_expiry_ts) { + if (access_expiry_ts) + data["E"] = epoch_seconds(*access_expiry_ts); else data["E"].erase(); } @@ -345,7 +343,7 @@ LIBSESSION_C_API bool user_profile_get_pro_config(const config_object* conf, pro pro->proof.rotating_pubkey.data, val->proof.rotating_pubkey.data(), val->proof.rotating_pubkey.size()); - pro->proof.expiry_unix_ts_ms = epoch_ms(val->proof.expiry_unix_ts); + pro->proof.expiry_ts = epoch_seconds(val->proof.expiry_unix_ts); std::memcpy(pro->proof.sig.data, val->proof.sig.data(), val->proof.sig.size()); std::memcpy( pro->rotating_privkey.data, @@ -367,8 +365,7 @@ LIBSESSION_C_API void user_profile_set_pro_config(config_object* conf, const pro val.proof.rotating_pubkey.data(), pro->proof.rotating_pubkey.data, val.proof.rotating_pubkey.size()); - val.proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(pro->proof.expiry_unix_ts_ms)); + val.proof.expiry_unix_ts = as_sys_seconds(pro->proof.expiry_ts); std::memcpy(val.proof.sig.data(), pro->proof.sig.data, val.proof.sig.size()); std::memcpy( val.rotating_privkey.data(), pro->rotating_privkey.data, val.rotating_privkey.size()); @@ -394,20 +391,18 @@ LIBSESSION_C_API void user_profile_set_animated_avatar(config_object* conf, bool unbox(conf)->set_animated_avatar(enabled); } -LIBSESSION_C_API uint64_t user_profile_get_pro_access_expiry_ms(const config_object* conf) { +LIBSESSION_C_API int64_t user_profile_get_pro_access_expiry(const config_object* conf) { if (auto expiry = unbox(conf)->get_pro_access_expiry()) - return epoch_ms(*expiry); + return epoch_seconds(*expiry); return 0; } -LIBSESSION_C_API void user_profile_set_pro_access_expiry_ms( - config_object* conf, uint64_t access_expiry_ts_ms) { - if (access_expiry_ts_ms <= 0) +LIBSESSION_C_API void user_profile_set_pro_access_expiry( + config_object* conf, int64_t access_expiry_ts) { + if (access_expiry_ts <= 0) unbox(conf)->set_pro_access_expiry(std::nullopt); else - unbox(conf)->set_pro_access_expiry( - std::chrono::sys_time{ - std::chrono::milliseconds{access_expiry_ts_ms}}); + unbox(conf)->set_pro_access_expiry(as_sys_seconds(access_expiry_ts)); } } // extern "C" diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 013ef341c..e43833d53 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -276,7 +276,7 @@ AddProPaymentOrGenerateProProofResponse AddProPaymentOrGenerateProProofResponse: // Parse payload result.proof.version = json_require(result_obj, "version", result.errors); - auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); json_require_fixed_bytes_from_hex( result_obj, "gen_index_hash", result.errors, result.proof.gen_index_hash); @@ -379,7 +379,7 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json // Parse revocation item auto obj = it.get(); - auto expiry_unix_ts = json_require(obj, "expiry_ts", result.errors); + auto expiry_unix_ts = json_require(obj, "expiry_ts", result.errors); ProRevocationItem item = {}; item.expiry_unix_ts = as_sys_seconds(expiry_unix_ts); @@ -477,11 +477,11 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { result.payments_total = json_require(result_obj, "payments_total", result.errors); - uint64_t expiry_ts = json_require(result_obj, "expiry_ts", result.errors); - uint64_t grace_period_duration = - json_require(result_obj, "grace_period_duration", result.errors); - uint64_t refund_requested_ts = - json_require(result_obj, "refund_requested_ts", result.errors); + int64_t expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + int64_t grace_period_duration = + json_require(result_obj, "grace_period_duration", result.errors); + int64_t refund_requested_ts = + json_require(result_obj, "refund_requested_ts", result.errors); result.expiry_unix_ts = as_sys_seconds(expiry_ts); result.grace_period_duration = std::chrono::seconds(grace_period_duration); result.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); @@ -502,16 +502,15 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { auto plan = json_require(obj, "plan", result.errors); auto payment_provider = json_require(obj, "payment_provider", result.errors); auto auto_renewing = json_require(obj, "auto_renewing", result.errors); - auto unredeemed_ts = json_require(obj, "unredeemed_ts", result.errors); - auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); - auto expiry_ts = json_require(obj, "expiry_ts", result.errors); + auto unredeemed_ts = json_require(obj, "unredeemed_ts", result.errors); + auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); + auto expiry_ts = json_require(obj, "expiry_ts", result.errors); auto grace_period_duration = - json_require(obj, "grace_period_duration", result.errors); + json_require(obj, "grace_period_duration", result.errors); auto platform_refund_expiry_ts = - json_require(obj, "platform_refund_expiry_ts", result.errors); - auto revoked_ts = json_require(obj, "revoked_ts", result.errors); - auto refund_requested_ts = - json_require(obj, "refund_requested_ts", result.errors); + json_require(obj, "platform_refund_expiry_ts", result.errors); + auto revoked_ts = json_require(obj, "revoked_ts", result.errors); + auto refund_requested_ts = json_require(obj, "refund_requested_ts", result.errors); ProPaymentItem item = {}; if (status > SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL && @@ -790,7 +789,7 @@ session_pro_backend_generate_pro_proof_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t ts) { + int64_t ts) { session_pro_backend_master_rotating_signatures result = {}; try { @@ -814,7 +813,7 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - uint64_t ts) { + int64_t ts) { session_pro_backend_to_json result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; @@ -834,7 +833,7 @@ session_pro_backend_get_pro_details_request_build_sig( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, + int64_t ts, uint32_t count) { session_pro_backend_signature result = {}; try { @@ -854,7 +853,7 @@ session_pro_backend_get_pro_details_request_build_to_json( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, + int64_t ts, uint32_t count) { session_pro_backend_to_json result = {}; try { @@ -1210,8 +1209,8 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, - uint64_t refund_requested_ts, + int64_t ts, + int64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, @@ -1247,8 +1246,8 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, - uint64_t ts, - uint64_t refund_requested_ts, + int64_t ts, + int64_t refund_requested_ts, SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len, diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index e7284f9f7..9dd6b30f7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -923,7 +923,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( } LIBSESSION_C_API bool session_protocol_pro_proof_is_active( - session_protocol_pro_proof const* proof, uint64_t ts) { + session_protocol_pro_proof const* proof, int64_t ts) { return ts <= proof->expiry_ts; } @@ -931,7 +931,7 @@ LIBSESSION_C_API SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( session_protocol_pro_proof const* proof, const uint8_t* verify_pubkey, size_t verify_pubkey_len, - uint64_t ts, + int64_t ts, const session_protocol_pro_signed_message* signed_msg) { SESSION_PROTOCOL_PRO_STATUS result = SESSION_PROTOCOL_PRO_STATUS_VALID; if (!session_protocol_pro_proof_verify_signature(proof, verify_pubkey, verify_pubkey_len)) @@ -1221,7 +1221,7 @@ LIBSESSION_C_API session_protocol_decoded_community_message session_protocol_decode_for_community( const void* content_or_envelope_payload, size_t content_or_envelope_payload_len, - uint64_t ts, + int64_t ts, OPTIONAL const void* pro_backend_pubkey, size_t pro_backend_pubkey_len, OPTIONAL char* error, diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 8098b5c57..e2c6685f0 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -609,8 +609,9 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { c.unread = true; if (i % 7 == 0) { - c.pro_expiry_unix_ts = std::chrono::sys_time{ - std::chrono::milliseconds{unix_timestamp(i)}}; + c.pro_expiry_unix_ts = + std::chrono::sys_seconds{std::chrono::duration_cast( + std::chrono::milliseconds{unix_timestamp(i)})}; auto& hash = c.pro_gen_index_hash.emplace(); std::fill(hash.begin(), hash.end(), static_cast(i % 256)); @@ -811,7 +812,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - c.pro_expiry_unix_ts_ms = 10000; + c.pro_expiry_ts = 10000; std::fill( c.pro_gen_index_hash.data, @@ -847,7 +848,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { CHECK(convo_info_volatile_get_or_construct_1to1( conf2, &c2, "051111111111111111111111111111111111111111111111111111111111111111")); - CHECK(c2.pro_expiry_unix_ts_ms == c.pro_expiry_unix_ts_ms); + CHECK(c2.pro_expiry_ts == c.pro_expiry_ts); CHECK(c.has_pro_gen_index_hash); CHECK(c2.has_pro_gen_index_hash); CHECK(oxenc::to_hex(c2.pro_gen_index_hash.data) == oxenc::to_hex(c.pro_gen_index_hash.data)); diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 1f0b161aa..02a9af2d5 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -24,7 +24,7 @@ TEST_CASE("Pro", "[config][pro]") { pro_cpp.rotating_privkey = rotating_sk; pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; - pro_cpp.proof.expiry_unix_ts = std::chrono::sys_time(1s); + pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); constexpr auto gen_index_hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_u; static_assert(pro_cpp.proof.gen_index_hash.max_size() == gen_index_hash.size()); @@ -35,7 +35,7 @@ TEST_CASE("Pro", "[config][pro]") { std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); - pro.proof.expiry_unix_ts_ms = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); + pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); std::memcpy(pro.proof.gen_index_hash.data, gen_index_hash.data(), gen_index_hash.size()); } @@ -59,11 +59,10 @@ TEST_CASE("Pro", "[config][pro]") { // Verify expiry { CHECK(pro_cpp.proof.is_active(pro_cpp.proof.expiry_unix_ts)); - CHECK_FALSE(pro_cpp.proof.is_active(pro_cpp.proof.expiry_unix_ts + 1ms)); + CHECK_FALSE(pro_cpp.proof.is_active(pro_cpp.proof.expiry_unix_ts + 1s)); - CHECK(session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_unix_ts_ms)); - CHECK_FALSE( - session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_unix_ts_ms + 1)); + CHECK(session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_ts)); + CHECK_FALSE(session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_ts + 1)); } // Verify it can verify messages signed with the rotating public key diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 6eadb0d1b..0cb893ba3 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -602,7 +602,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { pro_cpp.rotating_privkey = rotating_sk; pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; - pro_cpp.proof.expiry_unix_ts = std::chrono::sys_time(1s); + pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); constexpr auto gen_index_hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; static_assert(pro_cpp.proof.gen_index_hash.max_size() == gen_index_hash.size()); @@ -613,7 +613,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); - pro.proof.expiry_unix_ts_ms = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); + pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); std::memcpy(pro.proof.gen_index_hash.data, gen_index_hash.data(), gen_index_hash.size()); } @@ -632,8 +632,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { profile.remove_pro_config(); CHECK_FALSE(profile.get_pro_config().has_value()); - auto access_expiry_ms = - std::chrono::sys_time{std::chrono::milliseconds{500}}; - profile.set_pro_access_expiry(access_expiry_ms); - CHECK(profile.get_pro_access_expiry() == access_expiry_ms); + auto access_expiry = std::chrono::sys_seconds{std::chrono::seconds{500}}; + profile.set_pro_access_expiry(access_expiry); + CHECK(profile.get_pro_access_expiry() == access_expiry); } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 9c43dd5c6..bd2af830e 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -27,7 +27,7 @@ static bool string8_equals(string8 s8, std::string_view str) { "proof.rotating_pubkey: %s\n", oxenc::to_hex(proof.rotating_pubkey.data, std::end(proof.rotating_pubkey.data)) .c_str()); - fprintf(stderr, "proof.expiry_unix_ts_ms: %" PRIu64 "\n", proof.expiry_unix_ts_ms); + fprintf(stderr, "proof.expiry_ts: %" PRId64 "\n", proof.expiry_ts); fprintf(stderr, "proof.sig: %s\n", oxenc::to_hex(proof.sig.data, std::end(proof.sig.data)).c_str()); @@ -39,16 +39,14 @@ static bool string8_equals(string8 s8, std::string_view str) { fprintf(stderr, "item.plan: %d\n", item.plan); fprintf(stderr, "item.payment_provider: %d\n", item.payment_provider); fprintf(stderr, "item.auto_renewing: %d\n", item.auto_renewing); - fprintf(stderr, "item.unredeemed_unix_ts_ms: %" PRIu64 "zu\n", item.unredeemed_unix_ts_ms); - fprintf(stderr, "item.redeemed_unix_ts_ms: %" PRIu64 "zu\n", item.redeemed_unix_ts_ms); - fprintf(stderr, "item.expiry_unix_ts_ms: %" PRIu64 "\n", item.expiry_unix_ts_ms); + fprintf(stderr, "item.unredeemed_ts: %" PRId64 "zu\n", item.unredeemed_ts); + fprintf(stderr, "item.redeemed_ts: %" PRId64 "zu\n", item.redeemed_ts); + fprintf(stderr, "item.expiry_ts: %" PRId64 "\n", item.expiry_ts); + fprintf(stderr, "item.grace_period_duration: %" PRId64 "zu\n", item.grace_period_duration); fprintf(stderr, - "item.grace_period_duration_ms: %" PRIu64 "zu\n", - item.grace_period_duration_ms); - fprintf(stderr, - "item.platform_refund_expiry_unix_ts_ms: %" PRIu64 "zu\n", - item.platform_refund_expiry_unix_ts_ms); - fprintf(stderr, "item.revoked_unix_ts_ms: %" PRIu64 "zu\n", item.revoked_unix_ts_ms); + "item.platform_refund_expiry_ts: %" PRId64 "zu\n", + item.platform_refund_expiry_ts); + fprintf(stderr, "item.revoked_ts: %" PRId64 "zu\n", item.revoked_ts); fprintf(stderr, "item.google_payment_token: %.*s\n", (int)item.google_payment_token_count, @@ -70,7 +68,7 @@ static bool string8_equals(string8 s8, std::string_view str) { [[maybe_unused]] static void dump_pro_revocation( const session_pro_backend_pro_revocation_item& item) { - fprintf(stderr, "item.expiry_unix_ts: %" PRIu64 "zu\n", item.expiry_unix_ts_ms); + fprintf(stderr, "item.expiry_unix_ts: %" PRId64 "zu\n", item.expiry_ts); fprintf(stderr, "item.gen_index_hash: %s\n", oxenc::to_hex(item.gen_index_hash.data, std::end(item.gen_index_hash.data)).c_str()); @@ -107,7 +105,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { std::memcpy( payment_tx.order_id, fake_google_order_id_hex.data(), payment_tx.order_id_count); - uint64_t unix_ts_ms = 1698765432ULL * 1000; // Arbitrary timestamp + int64_t unix_ts = 1698765432; // Arbitrary timestamp (unix epoch seconds) SECTION("session_pro_backend_add_pro_payment_request_build_sigs") { // Valid inputs @@ -171,7 +169,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - unix_ts_ms); + unix_ts); REQUIRE(result.success); REQUIRE(result.error_count == 0); @@ -180,8 +178,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { 0, master_privkey.data, rotating_privkey.data, - std::chrono::sys_time( - std::chrono::milliseconds{unix_ts_ms})); + session::as_sys_seconds(unix_ts)); REQUIRE(std::memcmp( result.master_sig.data, cpp_sigs.master_sig.data(), @@ -198,7 +195,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data) - 1, - unix_ts_ms); + unix_ts); REQUIRE(!result.success); REQUIRE(result.error_count > 0); } @@ -293,7 +290,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; - request.unix_ts_ms = unix_ts_ms; + request.ts = unix_ts; session_pro_backend_master_rotating_signatures sigs = session_pro_backend_generate_pro_proof_request_build_sigs( @@ -302,7 +299,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - request.unix_ts_ms); + request.ts); request.master_sig = sigs.master_sig; request.rotating_sig = sigs.rotating_sig; @@ -323,8 +320,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { cpp.rotating_pkey.data(), rotating_pubkey.data, sizeof(rotating_pubkey.data)); - cpp.unix_ts = std::chrono::sys_time( - std::chrono::milliseconds{unix_ts_ms}); + cpp.unix_ts = session::as_sys_seconds(unix_ts); std::memcpy( cpp.master_sig.data(), sigs.master_sig.data, sizeof(sigs.master_sig.data)); std::memcpy( @@ -341,7 +337,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - request.unix_ts_ms); + request.ts); REQUIRE(one_shot.success); REQUIRE(one_shot.json.size == result.json.size); INFO("One shot: " << one_shot.json.data << "\n\nJSON: " << result.json.data); @@ -395,7 +391,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_get_pro_details_request request = {}; request.version = 0; request.master_pkey = master_pubkey; - request.unix_ts_ms = unix_ts_ms; + request.ts = unix_ts; request.count = 10'000; session_pro_backend_signature sig = @@ -403,7 +399,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.version, master_privkey.data, sizeof(master_privkey.data), - request.unix_ts_ms, + request.ts, request.count); request.master_sig = sig.sig; @@ -421,8 +417,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { cpp.version = 0; std::memcpy(cpp.master_pkey.data(), master_pubkey.data, sizeof(master_pubkey.data)); std::memcpy(cpp.master_sig.data(), sig.sig.data, sizeof(sig.sig.data)); - cpp.unix_ts = std::chrono::sys_time{ - std::chrono::milliseconds{unix_ts_ms}}; + cpp.unix_ts = session::as_sys_seconds(unix_ts); cpp.count = request.count; std::string cpp_json = cpp.to_json(); REQUIRE(string8_equals(result.json, cpp_json)); @@ -432,7 +427,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.version, master_privkey.data, sizeof(master_privkey.data), - request.unix_ts_ms, + request.ts, request.count); REQUIRE(one_shot.success); REQUIRE(one_shot.json.size == result.json.size); @@ -459,7 +454,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; j["result"] = { {"version", 0}, - {"expiry_unix_ts_ms", unix_ts_ms}, + {"expiry_ts", unix_ts}, {"gen_index_hash", oxenc::to_hex(fake_gen_index_hash)}, {"rotating_pkey", oxenc::to_hex(rotating_pubkey.data, std::end(rotating_pubkey.data))}, @@ -481,7 +476,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); - REQUIRE(result.proof.expiry_unix_ts_ms == unix_ts_ms); + REQUIRE(result.proof.expiry_ts == unix_ts); REQUIRE(std::memcmp( result.proof.gen_index_hash.data, fake_gen_index_hash.data(), @@ -510,7 +505,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { result.proof.rotating_pubkey.data, result_cpp.proof.rotating_pubkey.data(), result_cpp.proof.rotating_pubkey.size()) == 0); - REQUIRE(result.proof.expiry_unix_ts_ms == + REQUIRE(result.proof.expiry_ts == result_cpp.proof.expiry_unix_ts.time_since_epoch().count()); REQUIRE(std::memcmp( result.proof.sig.data, @@ -561,7 +556,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); auto obj = nlohmann::json::object(); - obj["expiry_unix_ts_ms"] = unix_ts_ms; + obj["expiry_ts"] = unix_ts; obj["gen_index_hash"] = oxenc::to_hex(fake_gen_index_hash); j["result"]["items"].push_back(obj); @@ -581,7 +576,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.ticket == 123); REQUIRE(result.items_count == 1); REQUIRE(result.items != nullptr); - REQUIRE(result.items[0].expiry_unix_ts_ms == unix_ts_ms); + REQUIRE(result.items[0].expiry_ts == unix_ts); REQUIRE(std::memcmp( result.items[0].gen_index_hash.data, fake_gen_index_hash.data(), @@ -624,9 +619,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"error_report", SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR}, {"auto_renewing", true}, - {"expiry_unix_ts_ms", unix_ts_ms + 2}, - {"grace_period_duration_ms", 1000}, - {"refund_requested_unix_ts_ms", unix_ts_ms + 3602}, + {"expiry_ts", unix_ts + 2}, + {"grace_period_duration", 1000}, + {"refund_requested_ts", unix_ts + 3602}, {"payments_total", 3}, {"items", nlohmann::json::array( @@ -635,13 +630,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"payment_provider", SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE}, {"auto_renewing", false}, - {"unredeemed_unix_ts_ms", unix_ts_ms - 3600}, - {"redeemed_unix_ts_ms", unix_ts_ms - 3600}, - {"expiry_unix_ts_ms", unix_ts_ms}, - {"grace_period_duration_ms", 1001}, - {"platform_refund_expiry_unix_ts_ms", unix_ts_ms + 1}, - {"revoked_unix_ts_ms", unix_ts_ms + 3600}, - {"refund_requested_unix_ts_ms", unix_ts_ms + 3601}, + {"unredeemed_ts", unix_ts - 3600}, + {"redeemed_ts", unix_ts - 3600}, + {"expiry_ts", unix_ts}, + {"grace_period_duration", 1001}, + {"platform_refund_expiry_ts", unix_ts + 1}, + {"revoked_ts", unix_ts + 3600}, + {"refund_requested_ts", unix_ts + 3601}, {"google_payment_token", std::string(payment_tx.payment_id, payment_tx.payment_id_count)}, {"google_order_id", @@ -665,22 +660,22 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR); REQUIRE(result.items_count == 1); REQUIRE(result.auto_renewing == true); - REQUIRE(result.grace_period_duration_ms == 1000); - REQUIRE(result.expiry_unix_ts_ms == unix_ts_ms + 2); - REQUIRE(result.refund_requested_unix_ts_ms == unix_ts_ms + 3602); + REQUIRE(result.grace_period_duration == 1000); + REQUIRE(result.expiry_ts == unix_ts + 2); + REQUIRE(result.refund_requested_ts == unix_ts + 3602); REQUIRE(result.payments_total == 3); REQUIRE(result.items != nullptr); REQUIRE(result.items[0].status == SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED); REQUIRE(result.items[0].plan == SESSION_PRO_BACKEND_PLAN_ONE_MONTH); REQUIRE(result.items[0].payment_provider == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE); - REQUIRE(result.items[0].unredeemed_unix_ts_ms == unix_ts_ms - 3600); - REQUIRE(result.items[0].redeemed_unix_ts_ms == unix_ts_ms - 3600); - REQUIRE(result.items[0].expiry_unix_ts_ms == unix_ts_ms); - REQUIRE(result.items[0].grace_period_duration_ms == 1001); - REQUIRE(result.items[0].platform_refund_expiry_unix_ts_ms == unix_ts_ms + 1); - REQUIRE(result.items[0].revoked_unix_ts_ms == unix_ts_ms + 3600); - REQUIRE(result.items[0].refund_requested_unix_ts_ms == unix_ts_ms + 3601); + REQUIRE(result.items[0].unredeemed_ts == unix_ts - 3600); + REQUIRE(result.items[0].redeemed_ts == unix_ts - 3600); + REQUIRE(result.items[0].expiry_ts == unix_ts); + REQUIRE(result.items[0].grace_period_duration == 1001); + REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); + REQUIRE(result.items[0].revoked_ts == unix_ts + 3600); + REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); REQUIRE(result.items[0].google_payment_token_count == payment_tx.payment_id_count); REQUIRE(std::memcmp( result.items[0].google_payment_token, @@ -773,16 +768,16 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_set_payment_refund_requested_request request = {}; request.version = 0; request.master_pkey = master_pubkey; - request.unix_ts_ms = unix_ts_ms; - request.refund_requested_unix_ts_ms = unix_ts_ms + 1; + request.ts = unix_ts; + request.refund_requested_ts = unix_ts + 1; session_pro_backend_signature sig = session_pro_backend_set_payment_refund_requested_request_build_sigs( request.version, master_privkey.data, sizeof(master_privkey.data), - request.unix_ts_ms, - request.refund_requested_unix_ts_ms, + request.ts, + request.refund_requested_ts, payment_tx.provider, reinterpret_cast(payment_tx.payment_id), payment_tx.payment_id_count, @@ -807,10 +802,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { cpp.master_pkey.data(), request.master_pkey.data, sizeof(request.master_pkey.data)); - cpp.unix_ts = std::chrono::sys_time( - std::chrono::milliseconds{unix_ts_ms}); - cpp.refund_requested_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds{request.refund_requested_unix_ts_ms}); + cpp.unix_ts = session::as_sys_seconds(unix_ts); + cpp.refund_requested_unix_ts = session::as_sys_seconds(request.refund_requested_ts); std::memcpy( cpp.master_sig.data(), request.master_sig.data, @@ -1027,7 +1020,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Authorise new key { - uint64_t now_unix_ts_ms = time(nullptr) * 1000; + int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); // Build request session_pro_backend_master_rotating_signatures pro_sigs = session_pro_backend_generate_pro_proof_request_build_sigs( @@ -1036,13 +1029,13 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - now_unix_ts_ms); + now_unix_ts); session_pro_backend_generate_pro_proof_request request = {}; request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; - request.unix_ts_ms = now_unix_ts_ms; + request.ts = now_unix_ts; request.master_sig = pro_sigs.master_sig; request.rotating_sig = pro_sigs.rotating_sig; @@ -1093,14 +1086,14 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_get_pro_details_request request = {}; request.version = 0; request.master_pkey = master_pubkey; - request.unix_ts_ms = time(nullptr) * 1000; + request.ts = session::epoch_seconds(session::clock_now_s()); request.count = 10'000; session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( request.version, master_privkey.data, sizeof(master_privkey.data), - request.unix_ts_ms, + request.ts, request.count); REQUIRE(sig.success); request.master_sig = sig.sig; @@ -1142,13 +1135,13 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_get_pro_details_request request = {}; request.version = 0; request.master_pkey = master_pubkey; - request.unix_ts_ms = time(nullptr) * 1000; + request.ts = session::epoch_seconds(session::clock_now_s()); session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( request.version, master_privkey.data, sizeof(master_privkey.data), - request.unix_ts_ms, + request.ts, request.count); REQUIRE(sig.success); request.master_sig = sig.sig; @@ -1301,14 +1294,14 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Set payment refund requested { // Build request - uint64_t now_unix_ts_ms = time(nullptr) * 1000; + int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); session_pro_backend_to_json request_json = session_pro_backend_set_payment_refund_requested_request_build_to_json( /*version*/ 0, master_privkey.data, sizeof(master_privkey.data), - /*unix_ts_ms*/ now_unix_ts_ms, - /*refund_requested_unix_ts_ms*/ now_unix_ts_ms, + /*ts*/ now_unix_ts, + /*refund_requested_ts*/ now_unix_ts, another_payment_tx.provider, reinterpret_cast(another_payment_tx.payment_id), another_payment_tx.payment_id_count, diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 706001547..a68ef5d03 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -63,9 +63,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se result.proof.gen_index_hash.data(), result.proof.gen_index_hash.size()); proto_proof->set_rotatingpublickey( result.proof.rotating_pubkey.data(), result.proof.rotating_pubkey.size()); - proto_proof->set_expiryunixts(std::chrono::duration_cast( - result.proof.expiry_unix_ts.time_since_epoch()) - .count()); + proto_proof->set_expiryunixts(session::epoch_seconds(result.proof.expiry_unix_ts)); proto_proof->set_sig(result.proof.sig.data(), result.proof.sig.size()); // Generate the plaintext @@ -761,7 +759,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( encoded.ciphertext.data, encoded.ciphertext.size, - timestamp_ms.time_since_epoch().count(), + timestamp_s.time_since_epoch().count(), keys.ed_pk1.data(), keys.ed_pk1.size(), error, @@ -784,7 +782,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( encoded.ciphertext.data, encoded.ciphertext.size, - timestamp_ms.time_since_epoch().count(), + timestamp_s.time_since_epoch().count(), keys.ed_pk1.data(), keys.ed_pk1.size(), error, @@ -805,7 +803,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( envelope_plaintext.data(), envelope_plaintext.size(), - timestamp_ms.time_since_epoch().count(), + timestamp_s.time_since_epoch().count(), keys.ed_pk1.data(), keys.ed_pk1.size(), error, @@ -829,7 +827,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( envelope_plaintext.data(), envelope_plaintext.size(), - timestamp_ms.time_since_epoch().count(), + timestamp_s.time_since_epoch().count(), keys.ed_pk1.data(), keys.ed_pk1.size(), error, @@ -892,7 +890,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_decoded_community_message decoded = session_protocol_decode_for_community( decrypted_cipher.data(), decrypted_cipher.size(), - timestamp_ms.time_since_epoch().count(), + timestamp_s.time_since_epoch().count(), keys.ed_pk1.data(), keys.ed_pk1.size(), error, From be51dfc977fabe5d8281b6922bcea81141ce876b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 16:11:03 -0300 Subject: [PATCH 099/193] Rename gen_index_hash -> revocation_tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the wire spec (§2.1, delta #2): rename the libsession symbol and the backend-JSON wire keys. The value stays an opaque 32-byte tag that clients compare for equality against the revocation list; the "random value, not BLAKE2b(counter, salt)" nature change is entirely backend-side. - C/C++ struct fields, C API, config fields, core/pro module, and the "gen_index_hash" JSON keys. - Renames the revocation_tag column in the (not-yet-live) revocations schema migration 000_pro_revocations.sql + its core/pro queries. - Protobuf field genIndexHash (ProProof field #2) left as-is: keyed by field number, and renaming touches the shared SessionProtos schema. - Config dict key "g" kept (internal, opaque). - Stale "hash of the generation index" comments updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/config/convo_info_volatile.h | 10 +-- .../session/config/convo_info_volatile.hpp | 8 +-- include/session/config/pro.hpp | 2 +- include/session/core/pro.hpp | 4 +- include/session/pro_backend.h | 2 +- include/session/pro_backend.hpp | 4 +- include/session/session_protocol.h | 2 +- include/session/session_protocol.hpp | 6 +- src/config/convo_info_volatile.cpp | 66 +++++++++---------- src/config/pro.cpp | 10 +-- src/config/user_profile.cpp | 16 ++--- src/core/pro.cpp | 18 ++--- src/core/schema/000_pro_revocations.sql | 2 +- src/pro_backend.cpp | 12 ++-- src/session_protocol.cpp | 28 ++++---- tests/test_config_convo_info_volatile.cpp | 14 ++-- tests/test_config_pro.cpp | 14 ++-- tests/test_config_userprofile.cpp | 8 +-- tests/test_pro_backend.cpp | 38 +++++------ tests/test_session_protocol.cpp | 2 +- 20 files changed, 134 insertions(+), 132 deletions(-) diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index c217de52e..6a501a8d4 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -14,8 +14,9 @@ typedef struct convo_info_volatile_1to1 { int64_t last_read; // milliseconds since unix epoch bool unread; // true if the conversation is explicitly marked unread - bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + bool has_pro_revocation_tag; // Flag indicating if hash is set + cbytes32 pro_revocation_tag; // Opaque revocation tag identifying this proof (from the Session + // Pro backend) int64_t pro_expiry_ts; // Unix epoch timestamp (seconds) until which this contact's entitlement // to Session Pro features is valid } convo_info_volatile_1to1; @@ -51,8 +52,9 @@ typedef struct convo_info_volatile_blinded_1to1 { int64_t last_read; // ms since unix epoch bool unread; // true if the conversation is explicitly marked unread - bool has_pro_gen_index_hash; // Flag indicating if hash is set - cbytes32 pro_gen_index_hash; // Hash of the generation index set by the Session Pro Backend + bool has_pro_revocation_tag; // Flag indicating if hash is set + cbytes32 pro_revocation_tag; // Opaque revocation tag identifying this proof (from the Session + // Pro backend) int64_t pro_expiry_ts; // Unix epoch timestamp (seconds) until which this contact's entitlement // to Session Pro features is valid } convo_info_volatile_blinded_1to1; diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 81814eee4..5f21951ba 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -33,7 +33,7 @@ class val_loader; /// 1 - dict of one-to-one conversations. Each key is the Session ID of the contact (in hex). /// Values are dicts with keys: /// e - contacts pro expiry unix timestamp (in milliseconds) -/// g - contacts pro gen_index_hash +/// g - contacts pro revocation_tag /// r - the unix timestamp (in integer milliseconds) of the last-read message. Always /// included, but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. @@ -63,7 +63,7 @@ class val_loader; /// b - outgoing blinded message request conversations. The key is the blinded Session ID without /// the prefix. Values are dicts with keys: /// e - contacts pro expiry unix timestamp (in milliseconds) -/// g - contacts pro gen_index_hash +/// g - contacts pro revocation_tag /// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, /// but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. @@ -87,8 +87,8 @@ namespace convo { }; struct pro_base : base { - /// Hash of the generation index set by the Session Pro Backend - std::optional pro_gen_index_hash; + /// Opaque revocation tag identifying this proof (from the Session Pro backend) + std::optional pro_revocation_tag; /// Unix epoch timestamp (seconds) until which this proof's entitlement to Session Pro /// features is valid diff --git a/include/session/config/pro.hpp b/include/session/config/pro.hpp index 01b0cffd0..739295a62 100644 --- a/include/session/config/pro.hpp +++ b/include/session/config/pro.hpp @@ -14,7 +14,7 @@ namespace session::config { /// | | /// | +-- @ - version /// | +-- e - expiry unix timestamp (in milliseconds) -/// | +-- g - gen_index_hash +/// | +-- g - revocation_tag /// | +-- s - proof signature, signed by the Session Pro Backend's ed25519 key /// | /// +-- r - rotating ed25519 seed (32b) diff --git a/include/session/core/pro.hpp b/include/session/core/pro.hpp index bd559695f..680e63091 100644 --- a/include/session/core/pro.hpp +++ b/include/session/core/pro.hpp @@ -17,13 +17,13 @@ class Pro final : detail::CoreComponent { public: /// API: core/Pro::pro_proof_is_revoked /// - /// Check if the proof identified by its `gen_index_hash` is revoked as of the given + /// Check if the proof identified by its `revocation_tag` is revoked as of the given /// timestamp from the list of proofs stored in the database. /// /// Outputs: /// - `bool` -- True if the proof was revoked, false otherwise. bool proof_is_revoked( - std::span gen_index_hash, + std::span revocation_tag, std::chrono::sys_time unix_ts); /// API: core/Pro::pro_update_revocations diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 462894b1f..1a66048c2 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -192,7 +192,7 @@ struct session_pro_backend_get_pro_revocations_request { typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { - cbytes32 gen_index_hash; + cbytes32 revocation_tag; int64_t expiry_ts; }; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index c38134bd5..df08eccae 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -327,8 +327,8 @@ struct GetProRevocationsRequest { }; struct ProRevocationItem { - /// 32-byte hash of the generation index, identifying a proof - b32 gen_index_hash; + /// 32-byte opaque revocation tag identifying a proof + b32 revocation_tag; /// Unix timestamp when the proof expires std::chrono::sys_seconds expiry_unix_ts; diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 0293abbc8..d007f3441 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -79,7 +79,7 @@ struct session_protocol_pro_signed_message { typedef struct session_protocol_pro_proof session_protocol_pro_proof; struct session_protocol_pro_proof { uint8_t version; - cbytes32 gen_index_hash; + cbytes32 revocation_tag; cbytes32 rotating_pubkey; int64_t expiry_ts; cbytes64 sig; diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 0e525f069..c9fb337cd 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -88,8 +88,8 @@ class ProProof { /// Version of the proof set by the Session Pro Backend std::uint8_t version; - /// Hash of the generation index set by the Session Pro Backend - b32 gen_index_hash; + /// Opaque revocation tag identifying this proof (from the Session Pro backend) + b32 revocation_tag; /// The public key that the Session client registers their Session Pro entitlement under. /// Session clients must sign messages with this key along side the sending of this proof for @@ -181,7 +181,7 @@ class ProProof { b32 hash() const; bool operator==(const ProProof& other) const { - return version == other.version && gen_index_hash == other.gen_index_hash && + return version == other.version && revocation_tag == other.revocation_tag && rotating_pubkey == other.rotating_pubkey && expiry_unix_ts == other.expiry_unix_ts && sig == other.sig; } diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index e28e00fc6..46fa33226 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -32,12 +32,12 @@ namespace convo { } one_to_one::one_to_one(const convo_info_volatile_1to1& c) : pro_base(c.last_read, c.unread), session_id{c.session_id, 66} { - if (c.has_pro_gen_index_hash) { - pro_gen_index_hash.emplace(); + if (c.has_pro_revocation_tag) { + pro_revocation_tag.emplace(); std::memcpy( - pro_gen_index_hash->data(), - c.pro_gen_index_hash.data, - pro_gen_index_hash->size()); + pro_revocation_tag->data(), + c.pro_revocation_tag.data, + pro_revocation_tag->size()); pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); } } @@ -47,16 +47,16 @@ namespace convo { c.last_read = last_read; c.unread = unread; - if (pro_gen_index_hash) { - c.has_pro_gen_index_hash = true; + if (pro_revocation_tag) { + c.has_pro_revocation_tag = true; std::memcpy( - c.pro_gen_index_hash.data, - pro_gen_index_hash->data(), - pro_gen_index_hash->size()); + c.pro_revocation_tag.data, + pro_revocation_tag->data(), + pro_revocation_tag->size()); c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); } else { - c.has_pro_gen_index_hash = false; + c.has_pro_revocation_tag = false; c.pro_expiry_ts = 0; } } @@ -127,12 +127,12 @@ namespace convo { pro_base(c.last_read, c.unread), blinded_session_id{c.blinded_session_id, 66}, legacy_blinding{c.legacy_blinding} { - if (c.has_pro_gen_index_hash) { - pro_gen_index_hash.emplace(); + if (c.has_pro_revocation_tag) { + pro_revocation_tag.emplace(); std::memcpy( - pro_gen_index_hash->data(), - c.pro_gen_index_hash.data, - pro_gen_index_hash->size()); + pro_revocation_tag->data(), + c.pro_revocation_tag.data, + pro_revocation_tag->size()); pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); } } @@ -143,15 +143,15 @@ namespace convo { c.unread = unread; c.legacy_blinding = legacy_blinding; - if (pro_gen_index_hash) { - c.has_pro_gen_index_hash = true; + if (pro_revocation_tag) { + c.has_pro_revocation_tag = true; std::memcpy( - c.pro_gen_index_hash.data, - pro_gen_index_hash->data(), - pro_gen_index_hash->size()); + c.pro_revocation_tag.data, + pro_revocation_tag->data(), + pro_revocation_tag->size()); c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); } else { - c.has_pro_gen_index_hash = false; + c.has_pro_revocation_tag = false; c.pro_expiry_ts = 0; } } @@ -160,15 +160,15 @@ namespace convo { base::load(info_dict); auto pro_expiry = int_or_0(info_dict, "e"); - std::optional> maybe_pro_gen_index_hash = + std::optional> maybe_pro_revocation_tag = maybe_vector(info_dict, "g"); - if (pro_expiry > 0 && maybe_pro_gen_index_hash && maybe_pro_gen_index_hash->size() == 32) { + if (pro_expiry > 0 && maybe_pro_revocation_tag && maybe_pro_revocation_tag->size() == 32) { pro_expiry_unix_ts = as_sys_seconds(pro_expiry); - pro_gen_index_hash.emplace(); + pro_revocation_tag.emplace(); std::memcpy( - pro_gen_index_hash->data(), - maybe_pro_gen_index_hash->data(), - pro_gen_index_hash->size()); + pro_revocation_tag->data(), + maybe_pro_revocation_tag->data(), + pro_revocation_tag->size()); } } @@ -334,9 +334,9 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { set_base(c, info); auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); - if (pro_expiry > 0 && c.pro_gen_index_hash) { + if (pro_expiry > 0 && c.pro_revocation_tag) { set_nonzero_int(info["e"], pro_expiry); - info["g"] = to_span(*c.pro_gen_index_hash); + info["g"] = to_span(*c.pro_revocation_tag); } } @@ -361,7 +361,7 @@ static bool is_stale(const C& c, std::chrono::system_clock::time_point cutoff) { if (c.unread) return false; if constexpr (std::derived_from) - if (c.pro_gen_index_hash.has_value() && c.pro_expiry_unix_ts >= cutoff) + if (c.pro_revocation_tag.has_value() && c.pro_expiry_unix_ts >= cutoff) return false; return sys_ms{std::chrono::milliseconds{c.last_read}} < cutoff; } @@ -440,9 +440,9 @@ void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { set_nonzero_int(info["y"], c.legacy_blinding); auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); - if (pro_expiry > 0 && c.pro_gen_index_hash) { + if (pro_expiry > 0 && c.pro_revocation_tag) { set_nonzero_int(info["e"], pro_expiry); - info["g"] = to_span(*c.pro_gen_index_hash); + info["g"] = to_span(*c.pro_revocation_tag); } } diff --git a/src/config/pro.cpp b/src/config/pro.cpp index f91c908a1..6cb0c776a 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -27,13 +27,13 @@ bool ProConfig::load(const dict& root) { // NOTE: Load into the proof object { std::optional version = maybe_int(*p, "@"); - std::optional> maybe_gen_index_hash = maybe_vector(*p, "g"); + std::optional> maybe_revocation_tag = maybe_vector(*p, "g"); std::optional maybe_expiry = maybe_ts(*p, "e"); std::optional> maybe_sig = maybe_vector(*p, "s"); if (!version) return false; - if (!maybe_gen_index_hash || maybe_gen_index_hash->size() != proof.gen_index_hash.size()) + if (!maybe_revocation_tag || maybe_revocation_tag->size() != proof.revocation_tag.size()) return false; if (!maybe_sig || maybe_sig->size() != proof.sig.max_size()) return false; @@ -42,9 +42,9 @@ bool ProConfig::load(const dict& root) { proof.version = *version; std::memcpy( - proof.gen_index_hash.data(), - maybe_gen_index_hash->data(), - proof.gen_index_hash.size()); + proof.revocation_tag.data(), + maybe_revocation_tag->data(), + proof.revocation_tag.size()); proof.expiry_unix_ts = *maybe_expiry; std::memcpy(proof.sig.data(), maybe_sig->data(), proof.sig.size()); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 10f70addf..6b5761d1b 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -167,7 +167,7 @@ void UserProfile::set_pro_config(const ProConfig& pro) { auto proof_dict = root["p"]; proof_dict["@"] = pro.proof.version; - proof_dict["g"] = pro.proof.gen_index_hash; + proof_dict["g"] = pro.proof.revocation_tag; proof_dict["e"] = epoch_seconds(pro.proof.expiry_unix_ts); proof_dict["s"] = pro.proof.sig; @@ -331,14 +331,14 @@ LIBSESSION_C_API int64_t user_profile_get_profile_updated(config_object* conf) { LIBSESSION_C_API bool user_profile_get_pro_config(const config_object* conf, pro_pro_config* pro) { if (auto val = unbox(conf)->get_pro_config(); val) { - static_assert(sizeof pro->proof.gen_index_hash == sizeof(val->proof.gen_index_hash)); + static_assert(sizeof pro->proof.revocation_tag == sizeof(val->proof.revocation_tag)); static_assert(sizeof pro->proof.rotating_pubkey == sizeof(val->proof.rotating_pubkey)); static_assert(sizeof pro->proof.sig == sizeof(val->proof.sig)); pro->proof.version = val->proof.version; std::memcpy( - pro->proof.gen_index_hash.data, - val->proof.gen_index_hash.data(), - val->proof.gen_index_hash.size()); + pro->proof.revocation_tag.data, + val->proof.revocation_tag.data(), + val->proof.revocation_tag.size()); std::memcpy( pro->proof.rotating_pubkey.data, val->proof.rotating_pubkey.data(), @@ -358,9 +358,9 @@ LIBSESSION_C_API void user_profile_set_pro_config(config_object* conf, const pro ProConfig val = {}; val.proof.version = pro->proof.version; std::memcpy( - val.proof.gen_index_hash.data(), - pro->proof.gen_index_hash.data, - val.proof.gen_index_hash.size()); + val.proof.revocation_tag.data(), + pro->proof.revocation_tag.data, + val.proof.revocation_tag.size()); std::memcpy( val.proof.rotating_pubkey.data(), pro->proof.rotating_pubkey.data, diff --git a/src/core/pro.cpp b/src/core/pro.cpp index ecdc3466d..0f1a61f88 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -10,12 +10,12 @@ namespace session::core { bool Pro::proof_is_revoked( - std::span gen_index_hash, + std::span revocation_tag, std::chrono::sys_time unix_ts) { return conn().prepared_get( "SELECT EXISTS (SELECT 1 FROM pro_revocations" - " WHERE gen_index_hash = ? AND expiry_unix_ts_ms <= ?)", - gen_index_hash, + " WHERE revocation_tag = ? AND expiry_unix_ts_ms <= ?)", + revocation_tag, unix_ts.time_since_epoch().count()); } @@ -47,23 +47,23 @@ void Pro::update_revocations( std::unordered_set to_remove; for (auto id : - c.prepared_results>("SELECT gen_index_hash FROM pro_revocations")) + c.prepared_results>("SELECT revocation_tag FROM pro_revocations")) to_remove.insert(id); for (auto st = c.prepared_st( - "INSERT INTO pro_revocations (gen_index_hash, expiry_unix_ts_ms) VALUES (?, ?)" - " ON CONFLICT (gen_index_hash) " + "INSERT INTO pro_revocations (revocation_tag, expiry_unix_ts_ms) VALUES (?, ?)" + " ON CONFLICT (revocation_tag) " " DO UPDATE SET expiry_unix_ts_ms = excluded.expiry_unix_ts_ms" " WHERE excluded.expiry_unix_ts_ms != expiry_unix_ts_ms"); const auto& revoke : revocations) { - exec_query(st, revoke.gen_index_hash, revoke.expiry_unix_ts.time_since_epoch().count()); - to_remove.erase(revoke.gen_index_hash); + exec_query(st, revoke.revocation_tag, revoke.expiry_unix_ts.time_since_epoch().count()); + to_remove.erase(revoke.revocation_tag); st->reset(); } if (!to_remove.empty()) { - auto st = c.prepared_st("DELETE FROM pro_revocations WHERE gen_index_hash = ?"); + auto st = c.prepared_st("DELETE FROM pro_revocations WHERE revocation_tag = ?"); for (const auto& id : to_remove) { exec_query(st, id); st->reset(); diff --git a/src/core/schema/000_pro_revocations.sql b/src/core/schema/000_pro_revocations.sql index 0db407d32..ba4df678f 100644 --- a/src/core/schema/000_pro_revocations.sql +++ b/src/core/schema/000_pro_revocations.sql @@ -1,4 +1,4 @@ CREATE TABLE pro_revocations ( - gen_index_hash BLOB PRIMARY KEY NOT NULL, + revocation_tag BLOB PRIMARY KEY NOT NULL, expiry_unix_ts_ms INTEGER NOT NULL ) STRICT diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index e43833d53..b9a05a79d 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -279,7 +279,7 @@ AddProPaymentOrGenerateProProofResponse AddProPaymentOrGenerateProProofResponse: auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); json_require_fixed_bytes_from_hex( - result_obj, "gen_index_hash", result.errors, result.proof.gen_index_hash); + result_obj, "revocation_tag", result.errors, result.proof.revocation_tag); json_require_fixed_bytes_from_hex( result_obj, "rotating_pkey", result.errors, result.proof.rotating_pubkey); json_require_fixed_bytes_from_hex(result_obj, "sig", result.errors, result.proof.sig); @@ -384,7 +384,7 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json ProRevocationItem item = {}; item.expiry_unix_ts = as_sys_seconds(expiry_unix_ts); json_require_fixed_bytes_from_hex( - obj, "gen_index_hash", result.errors, item.gen_index_hash); + obj, "revocation_tag", result.errors, item.revocation_tag); // Handle parsing result if (result.errors.size()) @@ -1015,9 +1015,9 @@ session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( result.proof.version = cpp.proof.version; result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_unix_ts); std::memcpy( - result.proof.gen_index_hash.data, - cpp.proof.gen_index_hash.data(), - cpp.proof.gen_index_hash.size()); + result.proof.revocation_tag.data, + cpp.proof.revocation_tag.data(), + cpp.proof.revocation_tag.size()); std::memcpy( result.proof.rotating_pubkey.data, cpp.proof.rotating_pubkey.data(), @@ -1094,7 +1094,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t for (size_t index = 0; index < result.items_count; ++index) { const ProRevocationItem& src = cpp.items[index]; session_pro_backend_pro_revocation_item& dest = result.items[index]; - std::memcpy(dest.gen_index_hash.data, src.gen_index_hash.data(), src.gen_index_hash.size()); + std::memcpy(dest.revocation_tag.data, src.revocation_tag.data(), src.revocation_tag.size()); dest.expiry_ts = session::epoch_seconds(src.expiry_unix_ts); } return result; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 9dd6b30f7..c87714bd6 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -52,14 +52,14 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { namespace { session::b32 proof_hash_internal( std::uint8_t version, - std::span gen_index_hash, + std::span revocation_tag, std::span rotating_pubkey, std::uint64_t expiry_ts) { // This must match the hashing routine at // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 return session::hash::blake2b_pers<32>( - session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_ts); + session::BUILD_PROOF_PERS, version, revocation_tag, rotating_pubkey, expiry_ts); } struct array_uc32_from_ptr_result { @@ -94,9 +94,9 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP result.status = static_cast(cpp.status); result.proof.version = cpp.proof.version; std::memcpy( - result.proof.gen_index_hash.data, - cpp.proof.gen_index_hash.data(), - cpp.proof.gen_index_hash.max_size()); + result.proof.revocation_tag.data, + cpp.proof.revocation_tag.data(), + cpp.proof.revocation_tag.max_size()); std::memcpy( result.proof.rotating_pubkey.data, cpp.proof.rotating_pubkey.data(), @@ -111,7 +111,7 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP namespace session { -static_assert(sizeof(std::declval().gen_index_hash) == 32); +static_assert(sizeof(std::declval().revocation_tag) == 32); static_assert(sizeof(std::declval().rotating_pubkey) == 32); static_assert(sizeof(std::declval().sig) == 64); @@ -152,7 +152,7 @@ ProStatus ProProof::status( b32 ProProof::hash() const { b32 result = proof_hash_internal( - version, gen_index_hash, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); + version, revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); return result; } @@ -526,7 +526,7 @@ static void parse_content_and_pro( // clang-format off size_t proof_errors = 0; proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.gen_index_hash.max_size(); + proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.revocation_tag.max_size(); proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); proof_errors += !proto_proof.has_expiryunixts(); proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); @@ -541,7 +541,7 @@ static void parse_content_and_pro( std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); std::memcpy( - proof.gen_index_hash.data(), + proof.revocation_tag.data(), proto_proof.genindexhash().data(), proto_proof.genindexhash().size()); std::memcpy( @@ -767,7 +767,7 @@ DecodedCommunityMessage decode_for_community( // clang-format off size_t proof_errors = 0; proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.gen_index_hash.max_size(); + proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.revocation_tag.max_size(); proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); proof_errors += !proto_proof.has_expiryunixts(); proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); @@ -780,7 +780,7 @@ DecodedCommunityMessage decode_for_community( pro.msg_bitset.data = pro_msg.msgbitset(); pro.profile_bitset.data = pro_msg.profilebitset(); std::memcpy( - proof.gen_index_hash.data(), + proof.revocation_tag.data(), proto_proof.genindexhash().data(), proto_proof.genindexhash().size()); std::memcpy( @@ -839,7 +839,7 @@ DecodedCommunityMessage decode_for_community( using namespace session; -static_assert((sizeof((session_protocol_pro_proof*)0)->gen_index_hash) == 32); +static_assert((sizeof((session_protocol_pro_proof*)0)->revocation_tag) == 32); static_assert(sizeof(std::declval().rotating_pubkey) == 32); static_assert(sizeof(std::declval().sig) == 64); @@ -887,7 +887,7 @@ LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_p cbytes32 result = {}; session::b32 hash = proof_hash_internal( proof->version, - to_byte_span(proof->gen_index_hash.data), + to_byte_span(proof->revocation_tag.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_ts); std::memcpy(result.data, hash.data(), hash.size()); @@ -902,7 +902,7 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( return false; session::b32 hash = proof_hash_internal( proof->version, - to_byte_span(proof->gen_index_hash.data), + to_byte_span(proof->revocation_tag.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_ts); return ed25519::verify(to_byte_span(proof->sig.data), to_byte_span<32>(verify_pubkey), hash); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index e2c6685f0..1d3ee9506 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -613,7 +613,7 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { std::chrono::sys_seconds{std::chrono::duration_cast( std::chrono::milliseconds{unix_timestamp(i)})}; - auto& hash = c.pro_gen_index_hash.emplace(); + auto& hash = c.pro_revocation_tag.emplace(); std::fill(hash.begin(), hash.end(), static_cast(i % 256)); } @@ -815,10 +815,10 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { c.pro_expiry_ts = 10000; std::fill( - c.pro_gen_index_hash.data, - c.pro_gen_index_hash.data + 32, + c.pro_revocation_tag.data, + c.pro_revocation_tag.data + 32, static_cast(3)); - c.has_pro_gen_index_hash = true; + c.has_pro_revocation_tag = true; convo_info_volatile_set_1to1(conf, &c); // Fake push: @@ -849,7 +849,7 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { conf2, &c2, "051111111111111111111111111111111111111111111111111111111111111111")); CHECK(c2.pro_expiry_ts == c.pro_expiry_ts); - CHECK(c.has_pro_gen_index_hash); - CHECK(c2.has_pro_gen_index_hash); - CHECK(oxenc::to_hex(c2.pro_gen_index_hash.data) == oxenc::to_hex(c.pro_gen_index_hash.data)); + CHECK(c.has_pro_revocation_tag); + CHECK(c2.has_pro_revocation_tag); + CHECK(oxenc::to_hex(c2.pro_revocation_tag.data) == oxenc::to_hex(c.pro_revocation_tag.data)); } diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index 02a9af2d5..ceee0f5c9 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -25,18 +25,18 @@ TEST_CASE("Pro", "[config][pro]") { pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); - constexpr auto gen_index_hash = + constexpr auto revocation_tag = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_u; - static_assert(pro_cpp.proof.gen_index_hash.max_size() == gen_index_hash.size()); + static_assert(pro_cpp.proof.revocation_tag.max_size() == revocation_tag.size()); std::memcpy( - pro_cpp.proof.gen_index_hash.data(), gen_index_hash.data(), gen_index_hash.size()); + pro_cpp.proof.revocation_tag.data(), revocation_tag.data(), revocation_tag.size()); // C std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); - std::memcpy(pro.proof.gen_index_hash.data, gen_index_hash.data(), gen_index_hash.size()); + std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size()); } // Generate and write the hashes that are signed by the faux pro backend into the proof @@ -86,7 +86,7 @@ TEST_CASE("Pro", "[config][pro]") { {"r", std::string(reinterpret_cast(rotating_sk.data()), crypto_sign_ed25519_SEEDBYTES)}, {"p", session::config::dict{ /*version*/ {"@", proof.version}, - /*gen_index_hash*/ {"g", std::string(reinterpret_cast(proof.gen_index_hash.data()), proof.gen_index_hash.size())}, + /*revocation_tag*/ {"g", std::string(reinterpret_cast(proof.revocation_tag.data()), proof.revocation_tag.size())}, /*rotating pubkey*/ {"r", std::string(reinterpret_cast(proof.rotating_pubkey.data()), proof.rotating_pubkey.size())}, /*expiry unix ts*/ {"e", proof.expiry_unix_ts.time_since_epoch().count()}, /*signature*/ {"s", std::string{reinterpret_cast(proof.sig.data()), proof.sig.size()}}, @@ -98,7 +98,7 @@ TEST_CASE("Pro", "[config][pro]") { CHECK(loaded_pro.load(good_dict)); CHECK(loaded_pro.rotating_privkey == pro_cpp.rotating_privkey); CHECK(loaded_pro.proof.version == pro_cpp.proof.version); - CHECK(loaded_pro.proof.gen_index_hash == pro_cpp.proof.gen_index_hash); + CHECK(loaded_pro.proof.revocation_tag == pro_cpp.proof.revocation_tag); CHECK(loaded_pro.proof.rotating_pubkey == pro_cpp.proof.rotating_pubkey); CHECK(loaded_pro.proof.expiry_unix_ts == pro_cpp.proof.expiry_unix_ts); CHECK(loaded_pro.proof.sig == pro_cpp.proof.sig); @@ -116,7 +116,7 @@ TEST_CASE("Pro", "[config][pro]") { {"r", std::string(reinterpret_cast(rotating_sk.data()), crypto_sign_ed25519_SEEDBYTES)}, {"p", session::config::dict{ /*version*/ {"@", proof.version}, - /*gen_index_hash*/ {"g", std::string(reinterpret_cast(proof.gen_index_hash.data()), proof.gen_index_hash.size())}, + /*revocation_tag*/ {"g", std::string(reinterpret_cast(proof.revocation_tag.data()), proof.revocation_tag.size())}, /*rotating pubkey*/ {"r", std::string(reinterpret_cast(proof.rotating_pubkey.data()), proof.rotating_pubkey.size())}, /*expiry unix ts*/ {"e", proof.expiry_unix_ts.time_since_epoch().count()}, /*signature*/ {"s", std::string{reinterpret_cast(broken_sig.data()), broken_sig.size()}}, diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 0cb893ba3..9d849e8ff 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -603,18 +603,18 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); - constexpr auto gen_index_hash = + constexpr auto revocation_tag = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; - static_assert(pro_cpp.proof.gen_index_hash.max_size() == gen_index_hash.size()); + static_assert(pro_cpp.proof.revocation_tag.max_size() == revocation_tag.size()); std::memcpy( - pro_cpp.proof.gen_index_hash.data(), gen_index_hash.data(), gen_index_hash.size()); + pro_cpp.proof.revocation_tag.data(), revocation_tag.data(), revocation_tag.size()); // C std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); - std::memcpy(pro.proof.gen_index_hash.data, gen_index_hash.data(), gen_index_hash.size()); + std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size()); } UserProfileTester::set_profile_updated(profile, std::chrono::sys_seconds{123s}); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index bd2af830e..25a002c44 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -21,8 +21,8 @@ static bool string8_equals(string8 s8, std::string_view str) { [[maybe_unused]] static void dump_pro_proof_to_stderr(const session_protocol_pro_proof& proof) { fprintf(stderr, "proof.version: %u\n", proof.version); fprintf(stderr, - "proof.gen_index_hash: %s\n", - oxenc::to_hex(proof.gen_index_hash.data, std::end(proof.gen_index_hash.data)).c_str()); + "proof.revocation_tag: %s\n", + oxenc::to_hex(proof.revocation_tag.data, std::end(proof.revocation_tag.data)).c_str()); fprintf(stderr, "proof.rotating_pubkey: %s\n", oxenc::to_hex(proof.rotating_pubkey.data, std::end(proof.rotating_pubkey.data)) @@ -70,8 +70,8 @@ static bool string8_equals(string8 s8, std::string_view str) { const session_pro_backend_pro_revocation_item& item) { fprintf(stderr, "item.expiry_unix_ts: %" PRId64 "zu\n", item.expiry_ts); fprintf(stderr, - "item.gen_index_hash: %s\n", - oxenc::to_hex(item.gen_index_hash.data, std::end(item.gen_index_hash.data)).c_str()); + "item.revocation_tag: %s\n", + oxenc::to_hex(item.revocation_tag.data, std::end(item.revocation_tag.data)).c_str()); } TEST_CASE("Pro Backend C API", "[pro_backend]") { @@ -447,15 +447,15 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } SECTION("session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse") { - b32 fake_gen_index_hash; - randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); + b32 fake_revocation_tag; + randombytes_buf(fake_revocation_tag.data(), fake_revocation_tag.size()); nlohmann::json j; j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; j["result"] = { {"version", 0}, {"expiry_ts", unix_ts}, - {"gen_index_hash", oxenc::to_hex(fake_gen_index_hash)}, + {"revocation_tag", oxenc::to_hex(fake_revocation_tag)}, {"rotating_pkey", oxenc::to_hex(rotating_pubkey.data, std::end(rotating_pubkey.data))}, {"sig", oxenc::to_hex(master_privkey.data, std::end(master_privkey.data))}}; @@ -478,9 +478,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.errors == nullptr); REQUIRE(result.proof.expiry_ts == unix_ts); REQUIRE(std::memcmp( - result.proof.gen_index_hash.data, - fake_gen_index_hash.data(), - fake_gen_index_hash.size()) == 0); + result.proof.revocation_tag.data, + fake_revocation_tag.data(), + fake_revocation_tag.size()) == 0); REQUIRE(std::memcmp( result.proof.rotating_pubkey.data, rotating_pubkey.data, @@ -498,9 +498,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Validate C and CPP variants REQUIRE(result.proof.version == result_cpp.proof.version); REQUIRE(std::memcmp( - result.proof.gen_index_hash.data, - result_cpp.proof.gen_index_hash.data(), - result_cpp.proof.gen_index_hash.size()) == 0); + result.proof.revocation_tag.data, + result_cpp.proof.revocation_tag.data(), + result_cpp.proof.revocation_tag.size()) == 0); REQUIRE(std::memcmp( result.proof.rotating_pubkey.data, result_cpp.proof.rotating_pubkey.data(), @@ -552,12 +552,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { j["result"]["ticket"] = 123; j["result"]["items"] = nlohmann::json::array(); - b32 fake_gen_index_hash; - randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); + b32 fake_revocation_tag; + randombytes_buf(fake_revocation_tag.data(), fake_revocation_tag.size()); auto obj = nlohmann::json::object(); obj["expiry_ts"] = unix_ts; - obj["gen_index_hash"] = oxenc::to_hex(fake_gen_index_hash); + obj["revocation_tag"] = oxenc::to_hex(fake_revocation_tag); j["result"]["items"].push_back(obj); std::string json = j.dump(); @@ -578,9 +578,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items != nullptr); REQUIRE(result.items[0].expiry_ts == unix_ts); REQUIRE(std::memcmp( - result.items[0].gen_index_hash.data, - fake_gen_index_hash.data(), - fake_gen_index_hash.size()) == 0); + result.items[0].revocation_tag.data, + fake_revocation_tag.data(), + fake_revocation_tag.size()) == 0); } // After freeeing diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index a68ef5d03..3f3180db2 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -60,7 +60,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se SessionProtos::ProProof* proto_proof = pro->mutable_proof(); proto_proof->set_version(result.proof.version); proto_proof->set_genindexhash( - result.proof.gen_index_hash.data(), result.proof.gen_index_hash.size()); + result.proof.revocation_tag.data(), result.proof.revocation_tag.size()); proto_proof->set_rotatingpublickey( result.proof.rotating_pubkey.data(), result.proof.rotating_pubkey.size()); proto_proof->set_expiryunixts(session::epoch_seconds(result.proof.expiry_unix_ts)); From 941b721a69b6d2e8a05fc2a504c41e7de3de613a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 16:32:58 -0300 Subject: [PATCH 100/193] Rename protobuf ProProof field genIndexHash -> revocationTag Completes the gen_index_hash -> revocation_tag rename into the shared SessionProtos schema so the name is consistent everywhere (no permanent mismatch between the proto field and the C++/wire names). Field number is unchanged (ProProof field 2), so this is wire-compatible on the binary protobuf; only the schema field *name* and the generated accessors change. Regenerated SessionProtos.pb.{cc,h} with protoc 3.21.12 (rename-only diff; WebSocketResources unchanged). NOTE: SessionProtos is the cross-client message schema -- clients that reference the generated `genIndexHash` / `genindexhash()` accessors must update to `revocationTag` / `revocationtag()`. Co-Authored-By: Claude Opus 4.8 (1M context) --- proto/SessionProtos.pb.cc | 42 +++++++-------- proto/SessionProtos.pb.h | 94 ++++++++++++++++----------------- proto/SessionProtos.proto | 2 +- src/session_protocol.cpp | 12 ++--- tests/test_session_protocol.cpp | 2 +- 5 files changed, 76 insertions(+), 76 deletions(-) diff --git a/proto/SessionProtos.pb.cc b/proto/SessionProtos.pb.cc index a8e0bc2fb..a12d5960e 100644 --- a/proto/SessionProtos.pb.cc +++ b/proto/SessionProtos.pb.cc @@ -483,7 +483,7 @@ PROTOBUF_CONSTEXPR ProProof::ProProof( ::_pbi::ConstantInitialized): _impl_{ /*decltype(_impl_._has_bits_)*/{} , /*decltype(_impl_._cached_size_)*/{} - , /*decltype(_impl_.genindexhash_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} + , /*decltype(_impl_.revocationtag_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} , /*decltype(_impl_.rotatingpublickey_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} , /*decltype(_impl_.sig_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}} , /*decltype(_impl_.expiryunixts_)*/uint64_t{0u} @@ -11002,7 +11002,7 @@ class ProProof::_Internal { static void set_has_version(HasBits* has_bits) { (*has_bits)[0] |= 16u; } - static void set_has_genindexhash(HasBits* has_bits) { + static void set_has_revocationtag(HasBits* has_bits) { (*has_bits)[0] |= 1u; } static void set_has_rotatingpublickey(HasBits* has_bits) { @@ -11028,19 +11028,19 @@ ProProof::ProProof(const ProProof& from) new (&_impl_) Impl_{ decltype(_impl_._has_bits_){from._impl_._has_bits_} , /*decltype(_impl_._cached_size_)*/{} - , decltype(_impl_.genindexhash_){} + , decltype(_impl_.revocationtag_){} , decltype(_impl_.rotatingpublickey_){} , decltype(_impl_.sig_){} , decltype(_impl_.expiryunixts_){} , decltype(_impl_.version_){}}; _internal_metadata_.MergeFrom(from._internal_metadata_); - _impl_.genindexhash_.InitDefault(); + _impl_.revocationtag_.InitDefault(); #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING - _impl_.genindexhash_.Set("", GetArenaForAllocation()); + _impl_.revocationtag_.Set("", GetArenaForAllocation()); #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING - if (from._internal_has_genindexhash()) { - _this->_impl_.genindexhash_.Set(from._internal_genindexhash(), + if (from._internal_has_revocationtag()) { + _this->_impl_.revocationtag_.Set(from._internal_revocationtag(), _this->GetArenaForAllocation()); } _impl_.rotatingpublickey_.InitDefault(); @@ -11072,15 +11072,15 @@ inline void ProProof::SharedCtor( new (&_impl_) Impl_{ decltype(_impl_._has_bits_){} , /*decltype(_impl_._cached_size_)*/{} - , decltype(_impl_.genindexhash_){} + , decltype(_impl_.revocationtag_){} , decltype(_impl_.rotatingpublickey_){} , decltype(_impl_.sig_){} , decltype(_impl_.expiryunixts_){uint64_t{0u}} , decltype(_impl_.version_){0u} }; - _impl_.genindexhash_.InitDefault(); + _impl_.revocationtag_.InitDefault(); #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING - _impl_.genindexhash_.Set("", GetArenaForAllocation()); + _impl_.revocationtag_.Set("", GetArenaForAllocation()); #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING _impl_.rotatingpublickey_.InitDefault(); #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING @@ -11103,7 +11103,7 @@ ProProof::~ProProof() { inline void ProProof::SharedDtor() { GOOGLE_DCHECK(GetArenaForAllocation() == nullptr); - _impl_.genindexhash_.Destroy(); + _impl_.revocationtag_.Destroy(); _impl_.rotatingpublickey_.Destroy(); _impl_.sig_.Destroy(); } @@ -11121,7 +11121,7 @@ void ProProof::Clear() { cached_has_bits = _impl_._has_bits_[0]; if (cached_has_bits & 0x00000007u) { if (cached_has_bits & 0x00000001u) { - _impl_.genindexhash_.ClearNonDefaultToEmpty(); + _impl_.revocationtag_.ClearNonDefaultToEmpty(); } if (cached_has_bits & 0x00000002u) { _impl_.rotatingpublickey_.ClearNonDefaultToEmpty(); @@ -11155,10 +11155,10 @@ const char* ProProof::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) } else goto handle_unusual; continue; - // optional bytes genIndexHash = 2; + // optional bytes revocationTag = 2; case 2: if (PROTOBUF_PREDICT_TRUE(static_cast(tag) == 18)) { - auto str = _internal_mutable_genindexhash(); + auto str = _internal_mutable_revocationtag(); ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx); CHK_(ptr); } else @@ -11228,10 +11228,10 @@ uint8_t* ProProof::_InternalSerialize( target = ::_pbi::WireFormatLite::WriteUInt32ToArray(1, this->_internal_version(), target); } - // optional bytes genIndexHash = 2; + // optional bytes revocationTag = 2; if (cached_has_bits & 0x00000001u) { target = stream->WriteBytesMaybeAliased( - 2, this->_internal_genindexhash(), target); + 2, this->_internal_revocationtag(), target); } // optional bytes rotatingPublicKey = 3; @@ -11270,11 +11270,11 @@ size_t ProProof::ByteSizeLong() const { cached_has_bits = _impl_._has_bits_[0]; if (cached_has_bits & 0x0000001fu) { - // optional bytes genIndexHash = 2; + // optional bytes revocationTag = 2; if (cached_has_bits & 0x00000001u) { total_size += 1 + ::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::BytesSize( - this->_internal_genindexhash()); + this->_internal_revocationtag()); } // optional bytes rotatingPublicKey = 3; @@ -11326,7 +11326,7 @@ void ProProof::MergeFrom(const ProProof& from) { cached_has_bits = from._impl_._has_bits_[0]; if (cached_has_bits & 0x0000001fu) { if (cached_has_bits & 0x00000001u) { - _this->_internal_set_genindexhash(from._internal_genindexhash()); + _this->_internal_set_revocationtag(from._internal_revocationtag()); } if (cached_has_bits & 0x00000002u) { _this->_internal_set_rotatingpublickey(from._internal_rotatingpublickey()); @@ -11363,8 +11363,8 @@ void ProProof::InternalSwap(ProProof* other) { _internal_metadata_.InternalSwap(&other->_internal_metadata_); swap(_impl_._has_bits_[0], other->_impl_._has_bits_[0]); ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( - &_impl_.genindexhash_, lhs_arena, - &other->_impl_.genindexhash_, rhs_arena + &_impl_.revocationtag_, lhs_arena, + &other->_impl_.revocationtag_, rhs_arena ); ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap( &_impl_.rotatingpublickey_, lhs_arena, diff --git a/proto/SessionProtos.pb.h b/proto/SessionProtos.pb.h index d3db9e88a..0e5008224 100644 --- a/proto/SessionProtos.pb.h +++ b/proto/SessionProtos.pb.h @@ -6451,28 +6451,28 @@ class ProProof final : // accessors ------------------------------------------------------- enum : int { - kGenIndexHashFieldNumber = 2, + kRevocationTagFieldNumber = 2, kRotatingPublicKeyFieldNumber = 3, kSigFieldNumber = 5, kExpiryUnixTsFieldNumber = 4, kVersionFieldNumber = 1, }; - // optional bytes genIndexHash = 2; - bool has_genindexhash() const; + // optional bytes revocationTag = 2; + bool has_revocationtag() const; private: - bool _internal_has_genindexhash() const; + bool _internal_has_revocationtag() const; public: - void clear_genindexhash(); - const std::string& genindexhash() const; + void clear_revocationtag(); + const std::string& revocationtag() const; template - void set_genindexhash(ArgT0&& arg0, ArgT... args); - std::string* mutable_genindexhash(); - PROTOBUF_NODISCARD std::string* release_genindexhash(); - void set_allocated_genindexhash(std::string* genindexhash); + void set_revocationtag(ArgT0&& arg0, ArgT... args); + std::string* mutable_revocationtag(); + PROTOBUF_NODISCARD std::string* release_revocationtag(); + void set_allocated_revocationtag(std::string* revocationtag); private: - const std::string& _internal_genindexhash() const; - inline PROTOBUF_ALWAYS_INLINE void _internal_set_genindexhash(const std::string& value); - std::string* _internal_mutable_genindexhash(); + const std::string& _internal_revocationtag() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_revocationtag(const std::string& value); + std::string* _internal_mutable_revocationtag(); public: // optional bytes rotatingPublicKey = 3; @@ -6547,7 +6547,7 @@ class ProProof final : struct Impl_ { ::PROTOBUF_NAMESPACE_ID::internal::HasBits<1> _has_bits_; mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_; - ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr genindexhash_; + ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr revocationtag_; ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr rotatingpublickey_; ::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr sig_; uint64_t expiryunixts_; @@ -13565,72 +13565,72 @@ inline void ProProof::set_version(uint32_t value) { // @@protoc_insertion_point(field_set:SessionProtos.ProProof.version) } -// optional bytes genIndexHash = 2; -inline bool ProProof::_internal_has_genindexhash() const { +// optional bytes revocationTag = 2; +inline bool ProProof::_internal_has_revocationtag() const { bool value = (_impl_._has_bits_[0] & 0x00000001u) != 0; return value; } -inline bool ProProof::has_genindexhash() const { - return _internal_has_genindexhash(); +inline bool ProProof::has_revocationtag() const { + return _internal_has_revocationtag(); } -inline void ProProof::clear_genindexhash() { - _impl_.genindexhash_.ClearToEmpty(); +inline void ProProof::clear_revocationtag() { + _impl_.revocationtag_.ClearToEmpty(); _impl_._has_bits_[0] &= ~0x00000001u; } -inline const std::string& ProProof::genindexhash() const { - // @@protoc_insertion_point(field_get:SessionProtos.ProProof.genIndexHash) - return _internal_genindexhash(); +inline const std::string& ProProof::revocationtag() const { + // @@protoc_insertion_point(field_get:SessionProtos.ProProof.revocationTag) + return _internal_revocationtag(); } template inline PROTOBUF_ALWAYS_INLINE -void ProProof::set_genindexhash(ArgT0&& arg0, ArgT... args) { +void ProProof::set_revocationtag(ArgT0&& arg0, ArgT... args) { _impl_._has_bits_[0] |= 0x00000001u; - _impl_.genindexhash_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); - // @@protoc_insertion_point(field_set:SessionProtos.ProProof.genIndexHash) + _impl_.revocationtag_.SetBytes(static_cast(arg0), args..., GetArenaForAllocation()); + // @@protoc_insertion_point(field_set:SessionProtos.ProProof.revocationTag) } -inline std::string* ProProof::mutable_genindexhash() { - std::string* _s = _internal_mutable_genindexhash(); - // @@protoc_insertion_point(field_mutable:SessionProtos.ProProof.genIndexHash) +inline std::string* ProProof::mutable_revocationtag() { + std::string* _s = _internal_mutable_revocationtag(); + // @@protoc_insertion_point(field_mutable:SessionProtos.ProProof.revocationTag) return _s; } -inline const std::string& ProProof::_internal_genindexhash() const { - return _impl_.genindexhash_.Get(); +inline const std::string& ProProof::_internal_revocationtag() const { + return _impl_.revocationtag_.Get(); } -inline void ProProof::_internal_set_genindexhash(const std::string& value) { +inline void ProProof::_internal_set_revocationtag(const std::string& value) { _impl_._has_bits_[0] |= 0x00000001u; - _impl_.genindexhash_.Set(value, GetArenaForAllocation()); + _impl_.revocationtag_.Set(value, GetArenaForAllocation()); } -inline std::string* ProProof::_internal_mutable_genindexhash() { +inline std::string* ProProof::_internal_mutable_revocationtag() { _impl_._has_bits_[0] |= 0x00000001u; - return _impl_.genindexhash_.Mutable(GetArenaForAllocation()); + return _impl_.revocationtag_.Mutable(GetArenaForAllocation()); } -inline std::string* ProProof::release_genindexhash() { - // @@protoc_insertion_point(field_release:SessionProtos.ProProof.genIndexHash) - if (!_internal_has_genindexhash()) { +inline std::string* ProProof::release_revocationtag() { + // @@protoc_insertion_point(field_release:SessionProtos.ProProof.revocationTag) + if (!_internal_has_revocationtag()) { return nullptr; } _impl_._has_bits_[0] &= ~0x00000001u; - auto* p = _impl_.genindexhash_.Release(); + auto* p = _impl_.revocationtag_.Release(); #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING - if (_impl_.genindexhash_.IsDefault()) { - _impl_.genindexhash_.Set("", GetArenaForAllocation()); + if (_impl_.revocationtag_.IsDefault()) { + _impl_.revocationtag_.Set("", GetArenaForAllocation()); } #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING return p; } -inline void ProProof::set_allocated_genindexhash(std::string* genindexhash) { - if (genindexhash != nullptr) { +inline void ProProof::set_allocated_revocationtag(std::string* revocationtag) { + if (revocationtag != nullptr) { _impl_._has_bits_[0] |= 0x00000001u; } else { _impl_._has_bits_[0] &= ~0x00000001u; } - _impl_.genindexhash_.SetAllocated(genindexhash, GetArenaForAllocation()); + _impl_.revocationtag_.SetAllocated(revocationtag, GetArenaForAllocation()); #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING - if (_impl_.genindexhash_.IsDefault()) { - _impl_.genindexhash_.Set("", GetArenaForAllocation()); + if (_impl_.revocationtag_.IsDefault()) { + _impl_.revocationtag_.Set("", GetArenaForAllocation()); } #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING - // @@protoc_insertion_point(field_set_allocated:SessionProtos.ProProof.genIndexHash) + // @@protoc_insertion_point(field_set_allocated:SessionProtos.ProProof.revocationTag) } // optional bytes rotatingPublicKey = 3; diff --git a/proto/SessionProtos.proto b/proto/SessionProtos.proto index ea95f3f06..51a45c32d 100644 --- a/proto/SessionProtos.proto +++ b/proto/SessionProtos.proto @@ -391,7 +391,7 @@ message GroupUpdateDeleteMemberContentMessage { message ProProof { optional uint32 version = 1; - optional bytes genIndexHash = 2; // Opaque identifier of this proof produced by the Session Pro backend + optional bytes revocationTag = 2; // Opaque identifier of this proof produced by the Session Pro backend optional bytes rotatingPublicKey = 3; // Public key whose signatures is authorised to entitle messages with Session Pro optional uint64 expiryUnixTs = 4; // Epoch timestamps in milliseconds optional bytes sig = 5; // Signature produced by the Session Pro Backend signing over the hash of the proof diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index c87714bd6..b24e1bd13 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -526,7 +526,7 @@ static void parse_content_and_pro( // clang-format off size_t proof_errors = 0; proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.revocation_tag.max_size(); + proof_errors += !proto_proof.has_revocationtag() || proto_proof.revocationtag().size() != proof.revocation_tag.max_size(); proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); proof_errors += !proto_proof.has_expiryunixts(); proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); @@ -542,8 +542,8 @@ static void parse_content_and_pro( std::memcpy( proof.revocation_tag.data(), - proto_proof.genindexhash().data(), - proto_proof.genindexhash().size()); + proto_proof.revocationtag().data(), + proto_proof.revocationtag().size()); std::memcpy( proof.rotating_pubkey.data(), proto_proof.rotatingpublickey().data(), @@ -767,7 +767,7 @@ DecodedCommunityMessage decode_for_community( // clang-format off size_t proof_errors = 0; proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.revocation_tag.max_size(); + proof_errors += !proto_proof.has_revocationtag() || proto_proof.revocationtag().size() != proof.revocation_tag.max_size(); proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); proof_errors += !proto_proof.has_expiryunixts(); proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); @@ -781,8 +781,8 @@ DecodedCommunityMessage decode_for_community( pro.profile_bitset.data = pro_msg.profilebitset(); std::memcpy( proof.revocation_tag.data(), - proto_proof.genindexhash().data(), - proto_proof.genindexhash().size()); + proto_proof.revocationtag().data(), + proto_proof.revocationtag().size()); std::memcpy( proof.rotating_pubkey.data(), proto_proof.rotatingpublickey().data(), diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 3f3180db2..2333b80ee 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -59,7 +59,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se // Create protobuf `Content.proMessage.proof` SessionProtos::ProProof* proto_proof = pro->mutable_proof(); proto_proof->set_version(result.proof.version); - proto_proof->set_genindexhash( + proto_proof->set_revocationtag( result.proof.revocation_tag.data(), result.proof.revocation_tag.size()); proto_proof->set_rotatingpublickey( result.proof.rotating_pubkey.data(), result.proof.rotating_pubkey.size()); From 02a9ca65fbc08af39b426ce3203a22c7c2407d8f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 17:05:39 -0300 Subject: [PATCH 101/193] Reshape revocation list: drop per-entry expiry, add effective_ts/retry_in/retain_for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the wire spec (§4, delta #6). The per-entry expiry_ts (whose value was the entitlement expiry -- up to a year for long subs, absurd for what is only a cleanup timer) is dropped and replaced by: - per-entry `effective_ts`: a matching proof is revoked only once the client clock reaches this (revoking on tag-match alone is too early); - response-level `retry_in` (recommended poll interval) and `retain_for` (memory-only local aging window, ~ the max proof-validity window). Also reshapes the (unused, so no migration) client-side core/pro SQLite store: - schema: expiry_unix_ts_ms -> effective_ts (seconds) + a new seen_at column; - proof_is_revoked now takes seconds and checks `effective_ts <= now`; - update_revocations takes retain_for and ages entries at `seen_at + retain_for` (memory-only) instead of deleting entries absent from the newest list; - fixes a latent bug: revocations_ticket_ was never assigned, so the same-ticket short-circuit never fired. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/core/pro.hpp | 9 ++-- include/session/pro_backend.h | 5 ++- include/session/pro_backend.hpp | 16 +++++-- src/core/pro.cpp | 56 +++++++++++-------------- src/core/schema/000_pro_revocations.sql | 3 +- src/pro_backend.cpp | 12 ++++-- tests/test_pro_backend.cpp | 10 +++-- 7 files changed, 65 insertions(+), 46 deletions(-) diff --git a/include/session/core/pro.hpp b/include/session/core/pro.hpp index 680e63091..57b35e613 100644 --- a/include/session/core/pro.hpp +++ b/include/session/core/pro.hpp @@ -23,8 +23,7 @@ class Pro final : detail::CoreComponent { /// Outputs: /// - `bool` -- True if the proof was revoked, false otherwise. bool proof_is_revoked( - std::span revocation_tag, - std::chrono::sys_time unix_ts); + std::span revocation_tag, std::chrono::sys_seconds unix_ts); /// API: core/Pro::pro_update_revocations /// @@ -36,8 +35,12 @@ class Pro final : detail::CoreComponent { /// comes alongside the revocation list when queried. This ticket changes whenever the /// revocation list is updated and is used to identify when an actual update is needed. /// - `revocations` -- New list of Session Pro revocations. + /// - `retain_for` -- How long to keep each entry after it was last seen in a list, for + /// memory-only aging (from the revocation response's `retain_for`). void update_revocations( - uint32_t ticket, std::span revocations); + uint32_t ticket, + std::span revocations, + std::chrono::seconds retain_for); private: friend class Core; diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 1a66048c2..0f82524bd 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -193,7 +193,8 @@ struct session_pro_backend_get_pro_revocations_request { typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { cbytes32 revocation_tag; - int64_t expiry_ts; + /// Unix timestamp (seconds); a matching proof is revoked once the client clock reaches this + int64_t effective_ts; }; typedef struct session_pro_backend_get_pro_revocations_response @@ -201,6 +202,8 @@ typedef struct session_pro_backend_get_pro_revocations_response struct session_pro_backend_get_pro_revocations_response { session_pro_backend_response_header header; int64_t ticket; + int64_t retry_in; /// Recommended seconds to wait before polling the list again + int64_t retain_for; /// Seconds to retain each item after first seeing it (memory-only aging) /// Array of items, with items_count elements session_pro_backend_pro_revocation_item* items; size_t items_count; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index df08eccae..672a8d376 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -306,8 +306,8 @@ struct GenerateProProofRequest { }; /// Retrieve the current list of revocations for currently active Session Pro proofs (because of -/// refunds for example). The caller should maintain this list until the revocation has expiry and -/// periodically retrieve this list from the backend every hour. +/// refunds for example). The caller retains each returned item for the response's `retain_for` +/// window after first seeing it (memory-only aging) and polls again after `retry_in`. struct GetProRevocationsRequest { /// Request version. The latest accepted version is 0 std::uint8_t version; @@ -330,8 +330,8 @@ struct ProRevocationItem { /// 32-byte opaque revocation tag identifying a proof b32 revocation_tag; - /// Unix timestamp when the proof expires - std::chrono::sys_seconds expiry_unix_ts; + /// A matching proof is revoked once the client's clock reaches this unix timestamp (not before) + std::chrono::sys_seconds effective_unix_ts; }; struct GetProRevocationsResponse : public ResponseHeader { @@ -339,6 +339,14 @@ struct GetProRevocationsResponse : public ResponseHeader { /// Update the caller's ticket to this value for subsequent requests. std::int64_t ticket; + /// Recommended interval to wait before polling the revocation list again. + std::chrono::seconds retry_in; + + /// How long a client should retain each item after first seeing it, for memory-only local + /// aging (roughly the maximum proof-validity window). Applied as `seen + retain_for`; holding + /// an entry longer is harmless. + std::chrono::seconds retain_for; + /// List of revoked Session Pro proofs std::vector items; diff --git a/src/core/pro.cpp b/src/core/pro.cpp index 0f1a61f88..ad7ec8228 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -1,8 +1,8 @@ +#include #include #include #include #include -#include #include "SQLiteCpp/Transaction.h" #include "session/sqlite.hpp" @@ -10,11 +10,10 @@ namespace session::core { bool Pro::proof_is_revoked( - std::span revocation_tag, - std::chrono::sys_time unix_ts) { + std::span revocation_tag, std::chrono::sys_seconds unix_ts) { return conn().prepared_get( "SELECT EXISTS (SELECT 1 FROM pro_revocations" - " WHERE revocation_tag = ? AND expiry_unix_ts_ms <= ?)", + " WHERE revocation_tag = ? AND effective_ts <= ?)", revocation_tag, unix_ts.time_since_epoch().count()); } @@ -30,46 +29,41 @@ bool Pro::proof_is_revoked( /// list is updated and is used to identify when an actual update is needed. /// - `revocations` -- New list of Session Pro revocations. void Pro::update_revocations( - uint32_t ticket, std::span revocations) { + uint32_t ticket, + std::span revocations, + std::chrono::seconds retain_for) { if (revocations_ticket_ && ticket == *revocations_ticket_) return; - auto already_hashed = [](const b32& a) { - size_t h; - std::memcpy(&h, a.data(), sizeof(h)); - return h; - }; + auto now = session::clock_now_s().time_since_epoch().count(); auto c = conn(); - SQLite::Transaction tx{c.sql}; - std::unordered_set to_remove; - for (auto id : - c.prepared_results>("SELECT revocation_tag FROM pro_revocations")) - to_remove.insert(id); - - for (auto st = c.prepared_st( - "INSERT INTO pro_revocations (revocation_tag, expiry_unix_ts_ms) VALUES (?, ?)" - " ON CONFLICT (revocation_tag) " - " DO UPDATE SET expiry_unix_ts_ms = excluded.expiry_unix_ts_ms" - " WHERE excluded.expiry_unix_ts_ms != expiry_unix_ts_ms"); + // Upsert each listed revocation, (re)setting its last-seen time to now. + for (auto st = + c.prepared_st("INSERT INTO pro_revocations (revocation_tag, effective_ts, seen_at)" + " VALUES (?, ?, ?)" + " ON CONFLICT (revocation_tag) DO UPDATE SET" + " effective_ts = excluded.effective_ts, seen_at = excluded.seen_at"); const auto& revoke : revocations) { - - exec_query(st, revoke.revocation_tag, revoke.expiry_unix_ts.time_since_epoch().count()); - to_remove.erase(revoke.revocation_tag); + exec_query( + st, + revoke.revocation_tag, + revoke.effective_unix_ts.time_since_epoch().count(), + now); st->reset(); } - if (!to_remove.empty()) { - auto st = c.prepared_st("DELETE FROM pro_revocations WHERE revocation_tag = ?"); - for (const auto& id : to_remove) { - exec_query(st, id); - st->reset(); - } - } + // Memory-only aging: drop entries not seen within the retain window. Unlike the wire list, + // absent entries are not deleted immediately -- holding a stale entry is harmless (its random + // tag never matches a live proof), and retain_for >= the proof-validity window guarantees we + // never drop an entry while a valid proof could still carry it. + auto del = c.prepared_st("DELETE FROM pro_revocations WHERE seen_at + ? < ?"); + exec_query(del, retain_for.count(), now); + revocations_ticket_ = ticket; tx.commit(); } diff --git a/src/core/schema/000_pro_revocations.sql b/src/core/schema/000_pro_revocations.sql index ba4df678f..2d8d11ed3 100644 --- a/src/core/schema/000_pro_revocations.sql +++ b/src/core/schema/000_pro_revocations.sql @@ -1,4 +1,5 @@ CREATE TABLE pro_revocations ( revocation_tag BLOB PRIMARY KEY NOT NULL, - expiry_unix_ts_ms INTEGER NOT NULL + effective_ts INTEGER NOT NULL, -- unix seconds; a matching proof is revoked once the clock reaches this + seen_at INTEGER NOT NULL -- unix seconds when last seen in a fetched list (for retain_for aging) ) STRICT diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index b9a05a79d..737e027ff 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -366,6 +366,10 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json // Parse payload result.ticket = json_require(result_obj, "ticket", result.errors); + result.retry_in = + std::chrono::seconds(json_require(result_obj, "retry_in", result.errors)); + result.retain_for = + std::chrono::seconds(json_require(result_obj, "retain_for", result.errors)); auto array = json_require(result_obj, "items", result.errors); result.items.reserve(array.size()); @@ -379,10 +383,10 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json // Parse revocation item auto obj = it.get(); - auto expiry_unix_ts = json_require(obj, "expiry_ts", result.errors); + auto effective_ts = json_require(obj, "effective_ts", result.errors); ProRevocationItem item = {}; - item.expiry_unix_ts = as_sys_seconds(expiry_unix_ts); + item.effective_unix_ts = as_sys_seconds(effective_ts); json_require_fixed_bytes_from_hex( obj, "revocation_tag", result.errors, item.revocation_tag); @@ -1076,6 +1080,8 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. result.header.status = cpp.status; result.ticket = cpp.ticket; + result.retry_in = cpp.retry_in.count(); + result.retain_for = cpp.retain_for.count(); // Copy errors result.header.errors_count = cpp.errors.size(); @@ -1095,7 +1101,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t const ProRevocationItem& src = cpp.items[index]; session_pro_backend_pro_revocation_item& dest = result.items[index]; std::memcpy(dest.revocation_tag.data, src.revocation_tag.data(), src.revocation_tag.size()); - dest.expiry_ts = session::epoch_seconds(src.expiry_unix_ts); + dest.effective_ts = session::epoch_seconds(src.effective_unix_ts); } return result; } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 25a002c44..0dec22830 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -68,7 +68,7 @@ static bool string8_equals(string8 s8, std::string_view str) { [[maybe_unused]] static void dump_pro_revocation( const session_pro_backend_pro_revocation_item& item) { - fprintf(stderr, "item.expiry_unix_ts: %" PRId64 "zu\n", item.expiry_ts); + fprintf(stderr, "item.effective_ts: %" PRId64 "\n", item.effective_ts); fprintf(stderr, "item.revocation_tag: %s\n", oxenc::to_hex(item.revocation_tag.data, std::end(item.revocation_tag.data)).c_str()); @@ -550,13 +550,15 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { nlohmann::json j; j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; j["result"]["ticket"] = 123; + j["result"]["retry_in"] = 3600; + j["result"]["retain_for"] = 2592000; j["result"]["items"] = nlohmann::json::array(); b32 fake_revocation_tag; randombytes_buf(fake_revocation_tag.data(), fake_revocation_tag.size()); auto obj = nlohmann::json::object(); - obj["expiry_ts"] = unix_ts; + obj["effective_ts"] = unix_ts; obj["revocation_tag"] = oxenc::to_hex(fake_revocation_tag); j["result"]["items"].push_back(obj); @@ -574,9 +576,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(result.ticket == 123); + REQUIRE(result.retry_in == 3600); + REQUIRE(result.retain_for == 2592000); REQUIRE(result.items_count == 1); REQUIRE(result.items != nullptr); - REQUIRE(result.items[0].expiry_ts == unix_ts); + REQUIRE(result.items[0].effective_ts == unix_ts); REQUIRE(std::memcmp( result.items[0].revocation_tag.data, fake_revocation_tag.data(), From 600333382171b6a06d62c97b14bad817d3eca427 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 21:09:09 -0300 Subject: [PATCH 102/193] Pro payments: collapse provider/plan enums + typed IDs into opaque payment_id Delete the SESSION_PRO_BACKEND_PAYMENT_PROVIDER and SESSION_PRO_BACKEND_PLAN enums and the payment-provider metadata table; providers and plans are now opaque wire strings (canonical SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constants for the known ones). Collapse the per-provider payment fields (google_payment_token/google_order_id/apple_*/rangeproof_order_id) into a single opaque payment_id; multi-part providers fold their parts in per a backend-defined composite. Rename payment status REFUNDED -> REVOKED. The add-payment and set-refund signed hashes now cover provider_code | payment_id (dropping the provider enum byte and the separate order_id). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 137 +++++---------- include/session/pro_backend.hpp | 113 ++++-------- src/pro_backend.cpp | 301 ++++++-------------------------- tests/test_pro_backend.cpp | 203 +++++++++------------ 4 files changed, 216 insertions(+), 538 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 0f82524bd..8a0cbb390 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -19,15 +19,13 @@ enum { SESSION_PRO_BACKEND_STATUS_PARSE_ERROR = 2, }; -/// Store front that a Session Pro payment came from. Must match: -/// https://github.com/session-foundation/session-pro-backend/blob/8ec0aacca2e5975407df1b60f5346477e17de44d/base.py#L78 -typedef enum SESSION_PRO_BACKEND_PAYMENT_PROVIDER { - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT, -} SESSION_PRO_BACKEND_PAYMENT_PROVIDER; +/// Canonical payment-provider `code` strings — the value transmitted on the wire and folded into +/// the add-payment / set-refund signed hashes (spec §1). Reference these constants rather than +/// hardcoding the slug so a sent code cannot drift from what libsession hashes/parses. Unknown +/// codes (e.g. a future provider) are still valid on the wire and pass through as opaque strings. +static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY[] = "google_play"; +static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE[] = "app_store"; +static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF[] = "rangeproof"; /// Must match: /// https://github.com/Doy-lee/session-pro-backend/blob/f4e2c84794470e7932ba1a1968fdb49117bb5870/backend.py#L18 @@ -36,20 +34,10 @@ typedef enum SESSION_PRO_BACKEND_PAYMENT_STATUS { SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED, SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED, SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED, - SESSION_PRO_BACKEND_PAYMENT_STATUS_REFUNDED, + SESSION_PRO_BACKEND_PAYMENT_STATUS_REVOKED, SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT, } SESSION_PRO_BACKEND_PAYMENT_STATUS; -/// Must match: -/// https://github.com/Doy-lee/session-pro-backend/blob/b9fb4301fecbd82e4631536fa378d4c1220b1a4d/base.py#L53 -typedef enum SESSION_PRO_BACKEND_PLAN { - SESSION_PRO_BACKEND_PLAN_NIL, - SESSION_PRO_BACKEND_PLAN_ONE_MONTH, - SESSION_PRO_BACKEND_PLAN_THREE_MONTHS, - SESSION_PRO_BACKEND_PLAN_TWELVE_MONTHS, - SESSION_PRO_BACKEND_PLAN_COUNT, -} SESSION_PRO_BACKEND_PLAN; - /// Must match: /// https://github.com/Doy-lee/session-pro-backend/blob/a0e0ba24bc4ab3a062465d861aa57df2269b6dde/server.py#L373 typedef enum SESSION_PRO_BACKEND_USER_PRO_STATUS { @@ -79,35 +67,6 @@ typedef enum SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS { SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT = 101, } SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS; -/// Bundle of hard-coded strings that are associated with each platform for clients to use for -/// string substitution typically. This structure is stored in a global table -/// `SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA` that is can be indexed into using the -/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER value directly. -typedef struct session_pro_backend_payment_provider_metadata - session_pro_backend_payment_provider_metadata; -struct session_pro_backend_payment_provider_metadata { - string8 device; - string8 store; - string8 platform; - string8 platform_account; - string8 refund_platform_url; - - /// Some platforms disallow a refund via their native support channels after some time period - /// (e.g. 48 hours after a purchase on Google, refunds must be dealt by the developers - /// themselves). If a platform does not have this restriction, this URL is typically the same as - /// the `refund_platform_url`. - string8 refund_support_url; - - string8 refund_status_url; - string8 update_subscription_url; - string8 cancel_subscription_url; -}; - -/// The centralised list of common URLs and properties for handling payment provider specific -/// integrations. Especially useful for cross-device management of Session Pro subscriptions. -extern const session_pro_backend_payment_provider_metadata - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT]; - typedef struct session_pro_backend_response_header session_pro_backend_response_header; struct session_pro_backend_response_header { uint32_t status; @@ -147,11 +106,14 @@ struct session_pro_backend_signature { typedef struct session_pro_backend_add_pro_payment_user_transaction session_pro_backend_add_pro_payment_user_transaction; struct session_pro_backend_add_pro_payment_user_transaction { - SESSION_PRO_BACKEND_PAYMENT_PROVIDER provider; + /// Provider code string (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*); opaque slug + char provider_code[64]; + size_t provider_code_count; + /// Opaque payment identifier from the provider's purchase flow. Multi-part providers fold their + /// parts into this one string per a backend-defined composite (e.g. Google "token|order_id"); + /// libsession treats it as opaque bytes hashed verbatim. char payment_id[128]; size_t payment_id_count; - char order_id[128]; - size_t order_id_count; }; typedef struct session_pro_backend_add_pro_payment_request @@ -222,11 +184,12 @@ struct session_pro_backend_get_pro_details_request { typedef struct session_pro_backend_pro_payment_item session_pro_backend_pro_payment_item; struct session_pro_backend_pro_payment_item { SESSION_PRO_BACKEND_PAYMENT_STATUS status; - SESSION_PRO_BACKEND_PLAN plan; - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_provider; - /// Pointer to payment provider metadata. This pointer is always defined to be pointing to valid - /// memory when the structure is received through the pro_backend APIs. - const session_pro_backend_payment_provider_metadata* payment_provider_metadata; + /// Billing-period code (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for non-period plans + char plan[64]; + size_t plan_count; + /// Provider code (e.g. "google_play"); opaque -- an unknown value passes through as-is + char payment_provider[64]; + size_t payment_provider_count; bool auto_renewing; int64_t unredeemed_ts; @@ -237,18 +200,10 @@ struct session_pro_backend_pro_payment_item { int64_t revoked_ts; int64_t refund_requested_ts; - char google_payment_token[128]; - size_t google_payment_token_count; - char google_order_id[128]; - size_t google_order_id_count; - char apple_original_tx_id[128]; - size_t apple_original_tx_id_count; - char apple_tx_id[128]; - size_t apple_tx_id_count; - char apple_web_line_order_id[128]; - size_t apple_web_line_order_id_count; - char rangeproof_order_id[128]; - size_t rangeproof_order_id_count; + /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their + /// parts in per the backend-defined composite -- libsession does not interpret it) + char payment_id[128]; + size_t payment_id_count; }; typedef struct session_pro_backend_get_pro_details_response @@ -298,13 +253,11 @@ struct session_pro_backend_set_payment_refund_requested_response { /// - `master_privkey_len` -- Length of master_privkey. /// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). /// - `rotating_privkey_len` -- Length of rotating_privkey. -/// - `payment_tx_provider` -- Provider that the payment to register is coming from -/// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment provider. -/// See `AddProPaymentUserTransaction` -/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload -/// - `payment_tx_order_id` -- Order ID that is associated with the payment see +/// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from +/// (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` -/// - `payment_tx_order_id_len` -- Length of the `payment_tx_order_id` payload +/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload /// /// Outputs: /// - `success` - True if signatures are built successfully, false otherwise. @@ -320,11 +273,9 @@ session_pro_backend_add_pro_payment_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) NON_NULL_ARG(2, 4, 7, 9); + size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 4, 6, 7); /// API: session_pro_backend/add_pro_payment_request_build_to_json /// @@ -340,11 +291,9 @@ session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) NON_NULL_ARG(2, 4, 7, 9); + size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 4, 6, 7); /// API: session_pro_backend/generate_pro_proof_request_build_sigs /// @@ -521,13 +470,11 @@ session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details /// - `ts` -- Unix timestamp for the request /// - `refund_requested_ts` -- Unix timestamp to set as the timestamp that a refund was /// requested on this payment -/// - `payment_tx_provider` -- Provider that the payment to register is coming from -/// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment provider. -/// See `AddProPaymentUserTransaction` -/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload -/// - `payment_tx_order_id` -- Order ID that is associated with the payment see +/// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from +/// (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` -/// - `payment_tx_order_id_len` -- Length of the `payment_tx_order_id` payload +/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload /// /// Outputs: /// - `bool` -- True if signatures are built successfully, false otherwise. @@ -541,11 +488,9 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) NON_NULL_ARG(2, 7, 9); + size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 6, 7); /// API: session_pro_backend/set_payment_refund_requested_request_build_to_json /// @@ -561,11 +506,9 @@ session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_req size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) NON_NULL_ARG(2, 7, 9); + size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 6, 7); /// API: session_pro_backend/set_payment_refund_requested_request_to_json /// diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 672a8d376..60f9a91cd 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -110,17 +110,14 @@ struct MasterRotatingSignatures { }; struct AddProPaymentUserTransaction { - SESSION_PRO_BACKEND_PAYMENT_PROVIDER provider; + /// Provider code string (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). Opaque slug; unknown + /// values are accepted and passed through verbatim. + std::string provider_code; - /// The payment ID to claim which is different per platform. - /// - /// Google Play Store => purchase token - /// iOS App Store => transaction ID (note, not the original transaction id) + /// Opaque payment identifier from the provider's purchase flow. Multi-part providers fold their + /// parts into this one string per a backend-defined composite (e.g. Google "token|order_id"); + /// libsession treats it as opaque bytes hashed verbatim. std::string payment_id; - - /// Only for Google Play Store, set this to the purchase's order ID. Ignored for other payment - /// providers - std::string order_id; }; /// Register a new Session Pro proof to the backend. The payment is registered under the @@ -169,11 +166,9 @@ struct AddProPaymentRequest { /// - `request_version` -- Version of the request to build a hash for /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `payment_tx_provider` -- Provider that the payment to register is coming from - /// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment - /// provider. See `AddProPaymentUserTransaction` - /// this is the transaction ID). - /// - `payment_tx_order_id` -- Order ID that is associated with the payment see + /// - `payment_tx_provider_code` -- Provider code string the payment is coming from (see + /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) + /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// /// Outputs: @@ -182,9 +177,8 @@ struct AddProPaymentRequest { std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id); /// API: pro/AddProPaymentRequest::build_to_json /// @@ -195,11 +189,9 @@ struct AddProPaymentRequest { /// - `request_version` -- Version of the request to build a hash for /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `payment_tx_provider` -- Provider that the payment to register is coming from - /// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment - /// provider. See `AddProPaymentUserTransaction` - /// this is the transaction ID). - /// - `payment_tx_order_id` -- Order ID that is associated with the payment see + /// - `payment_tx_provider_code` -- Provider code string the payment is coming from (see + /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) + /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// /// Outputs: @@ -208,9 +200,8 @@ struct AddProPaymentRequest { std::uint8_t request_version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id); }; /// The generated proof from the Session Pro backend that has been parsed from JSON. This structure @@ -436,17 +427,13 @@ struct ProPaymentItem { /// book-keeping purposes. SESSION_PRO_BACKEND_PAYMENT_STATUS status; - /// Session Pro product/plan item that was purchased - SESSION_PRO_BACKEND_PLAN plan; - - /// Store front that this particular payment came from - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_provider; + /// Billing-period code that was purchased (e.g. "1m"/"3m"/"1y"); opaque string, may be + /// free-form for non-period plans + std::string plan; - /// Strings associated with platform's payment provider from - /// `SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA`, provided for convenience. This pointer is - /// always pointing to valid memory. - const session_pro_backend_payment_provider_metadata* payment_provider_metadata = - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA; + /// Provider code this particular payment came from (e.g. "google_play"); opaque string, an + /// unknown value passes through as-is + std::string payment_provider; /// Flag indicating whether or not this payment will automatically bill itself at the end of the /// billing cycle. @@ -478,30 +465,10 @@ struct ProPaymentItem { /// if no refund has been requested for this payment yet. std::chrono::sys_seconds refund_requested_unix_ts; - /// When payment provider is set to Google Play Store, this is the platform-specific purchase - /// token. This information should be considered as confidential and stored appropriately. - std::string google_payment_token; - - /// When payment provider is set to Google Play Store, this is the platform-specific order - /// id. This information should be considered as confidential and stored appropriately. - std::string google_order_id; - - /// When payment provider is set to iOS App Store, this is the platform-specific original - /// transaction ID. This information should be considered as confidential and stored - /// appropriately. - std::string apple_original_tx_id; - - /// When payment provider is set to iOS App Store, this is the platform-specific transaction ID - /// This information should be considered as confidential and stored appropriately. - std::string apple_tx_id; - - /// When payment provider is set to iOS App Store, this is the platform-specific web line order - /// ID. This information should be considered as confidential and stored appropriately. - std::string apple_web_line_order_id; - - /// When payment provider is set to Rangeproof, this is the platform-specific order ID. - /// This information should be considered as confidential and stored appropriately. - std::string rangeproof_order_id; + /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their + /// parts in per the backend-defined composite -- libsession does not interpret it). This + /// information should be considered confidential and stored appropriately. + std::string payment_id; }; struct GetProDetailsResponse : public ResponseHeader { @@ -606,12 +573,9 @@ struct SetPaymentRefundRequestedRequest { /// - `unix_ts` -- Unix timestamp for the request. /// - `refund_requested_unix_ts` -- Unix timestamp to set as the timestamp that a refund was /// requested on this payment - /// - `payment_tx_provider` -- Provider that the payment to set a refund request on is coming - /// from - /// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment - /// provider. See `AddProPaymentUserTransaction` - /// this is the transaction ID). - /// - `payment_tx_order_id` -- Order ID that is associated with the payment see + /// - `payment_tx_provider_code` -- Provider code string the payment to set a refund request on + /// is coming from (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) + /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// /// Outputs: @@ -621,9 +585,8 @@ struct SetPaymentRefundRequestedRequest { const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id); /// API: pro/SetPaymentRefundRequested::build_to_json /// @@ -636,12 +599,9 @@ struct SetPaymentRefundRequestedRequest { /// - `unix_ts` -- Unix timestamp for the request. /// - `refund_requested_unix_ts` -- Unix timestamp to set as the timestamp that a refund was /// requested on this payment - /// - `payment_tx_provider` -- Provider that the payment to set a refund request on is coming - /// from - /// - `payment_tx_payment_id` -- ID that is associated with the payment from the payment - /// provider. See `AddProPaymentUserTransaction` - /// this is the transaction ID). - /// - `payment_tx_order_id` -- Order ID that is associated with the payment see + /// - `payment_tx_provider_code` -- Provider code string the payment to set a refund request on + /// is coming from (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) + /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// /// Outputs: @@ -651,9 +611,8 @@ struct SetPaymentRefundRequestedRequest { const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id); + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id); /// API: pro/SetPaymentRefundRequested::to_json /// diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 737e027ff..91b46a552 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -16,55 +16,6 @@ #include "internal-util.hpp" -// clang-format off -const session_pro_backend_payment_provider_metadata SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT] = { - /*SESSION_PRO_PAYMENT_PROVIDER_NIL*/ { - .device = string8_literal(""), - .store = string8_literal(""), - .platform = string8_literal(""), - .platform_account = string8_literal(""), - .refund_platform_url = string8_literal(""), - .refund_support_url = string8_literal(""), - .refund_status_url = string8_literal(""), - .update_subscription_url = string8_literal(""), - .cancel_subscription_url = string8_literal(""), - }, - /*SESSION_PRO_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE*/ { - .device = string8_literal("Android"), - .store = string8_literal("Google Play Store"), - .platform = string8_literal("Google"), - .platform_account = string8_literal("Google account"), - .refund_platform_url = string8_literal("https://support.google.com/googleplay/workflow/9813244?"), - .refund_support_url = string8_literal("https://getsession.org/android-refund"), - .refund_status_url = string8_literal("https://getsession.org/android-refund"), - .update_subscription_url = string8_literal("https://play.google.com/store/account/subscriptions?package=network.loki.messenger"), - .cancel_subscription_url = string8_literal("https://play.google.com/store/account/subscriptions?package=network.loki.messenger"), - }, - /*SESSION_PRO_PAYMENT_PROVIDER_IOS_APP_STORE*/ { - .device = string8_literal("iOS"), - .store = string8_literal("Apple App Store"), - .platform = string8_literal("Apple"), - .platform_account = string8_literal("Apple account"), - .refund_platform_url = string8_literal("https://support.apple.com/118223"), - .refund_support_url = string8_literal("https://support.apple.com/118223"), - .refund_status_url = string8_literal("https://support.apple.com/118224"), - .update_subscription_url = string8_literal("https://apps.apple.com/account/subscriptions"), - .cancel_subscription_url = string8_literal("https://account.apple.com/account/manage/section/subscriptions"), - }, - /*SESSION_PRO_PAYMENT_PROVIDER_RANGEPROOF*/ { - .device = string8_literal(""), - .store = string8_literal(""), - .platform = string8_literal(""), - .platform_account = string8_literal(""), - .refund_platform_url = string8_literal(""), - .refund_support_url = string8_literal(""), - .refund_status_url = string8_literal(""), - .update_subscription_url = string8_literal(""), - .cancel_subscription_url = string8_literal(""), - } -}; -// clang-format on - namespace { const nlohmann::json json_parse(std::string_view json, std::vector& errors) { nlohmann::json result; @@ -168,21 +119,8 @@ std::string AddProPaymentRequest::to_json() const { j["version"] = version; j["master_pkey"] = oxenc::to_hex(master_pkey); j["rotating_pkey"] = oxenc::to_hex(rotating_pkey); - j["payment_tx"]["provider"] = payment_tx.provider; - switch (payment_tx.provider) { - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { - assert(false && "Unimplemented"); - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { - j["payment_tx"]["google_payment_token"] = payment_tx.payment_id; - j["payment_tx"]["google_order_id"] = payment_tx.order_id; - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: { - j["payment_tx"]["apple_tx_id"] = payment_tx.payment_id; - } break; - } + j["payment_tx"]["provider"] = payment_tx.provider_code; + j["payment_tx"]["payment_id"] = payment_tx.payment_id; j["master_sig"] = oxenc::to_hex(master_sig); j["rotating_sig"] = oxenc::to_hex(rotating_sig); std::string result = j.dump(); @@ -193,31 +131,17 @@ MasterRotatingSignatures AddProPaymentRequest::build_sigs( std::uint8_t version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - if (payment_tx_provider == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE) { - if (payment_tx_order_id.empty()) - throw std::invalid_argument{ - "Invalid payment_tx_order_id: order ID must be set for a Google Play store " - "payment"}; - } else { - if (payment_tx_order_id.size()) - throw std::invalid_argument{ - "Invalid payment_tx_order_id: order ID must not be set for an iOS App store " - "payment"}; - } - - // Hash components to 32 bytes, must match: - // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L171 + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id) { + // Hash components to 32 bytes. The tail is the provider code string followed by the opaque + // payment id (spec §1); must match the backend's signed-hash construction. auto hash_to_sign = hash::blake2b_pers<32>( ADD_PRO_PAYMENT_PERS, version, master_privkey.pubkey(), rotating_privkey.pubkey(), - static_cast(payment_tx_provider), - payment_tx_payment_id, - payment_tx_order_id); + payment_tx_provider_code, + payment_tx_payment_id); MasterRotatingSignatures result = {}; result.master_sig = ed25519::sign(master_privkey, hash_to_sign); @@ -229,24 +153,21 @@ std::string AddProPaymentRequest::build_to_json( std::uint8_t version, const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id) { auto sigs = AddProPaymentRequest::build_sigs( version, master_privkey, rotating_privkey, - payment_tx_provider, - payment_tx_payment_id, - payment_tx_order_id); + payment_tx_provider_code, + payment_tx_payment_id); AddProPaymentRequest request = {}; request.version = version; std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); std::ranges::copy(rotating_privkey.pubkey(), request.rotating_pkey.begin()); - request.payment_tx.provider = payment_tx_provider; + request.payment_tx.provider_code = payment_tx_provider_code; request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); - request.payment_tx.order_id = to_string_view(payment_tx_order_id); request.master_sig = sigs.master_sig; request.rotating_sig = sigs.rotating_sig; @@ -503,8 +424,9 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { // Parse payment item auto obj = it.get(); auto status = json_require(obj, "status", result.errors); - auto plan = json_require(obj, "plan", result.errors); - auto payment_provider = json_require(obj, "payment_provider", result.errors); + auto plan = json_require(obj, "plan", result.errors); + auto payment_provider = json_require(obj, "payment_provider", result.errors); + auto payment_id = json_require(obj, "payment_id", result.errors); auto auto_renewing = json_require(obj, "auto_renewing", result.errors); auto unredeemed_ts = json_require(obj, "unredeemed_ts", result.errors); auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); @@ -524,22 +446,11 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { result.errors.push_back(fmt::format("Status value was out-of-bounds: {}", status)); } - if (plan > SESSION_PRO_BACKEND_PLAN_NIL && plan < SESSION_PRO_BACKEND_PLAN_COUNT) { - item.plan = static_cast(plan); - } else { - result.errors.push_back(fmt::format("Plan value was out-of-bounds: {}", plan)); - } - - if (payment_provider > SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL && - payment_provider < SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT) { - item.payment_provider = - static_cast(payment_provider); - item.payment_provider_metadata = - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA + payment_provider; - } else { - result.errors.push_back( - fmt::format("Payment provider value was out-of-bounds: {}", payment_provider)); - } + // plan / payment_provider / payment_id are opaque strings; libsession does not validate or + // interpret them (an unknown provider or plan code passes through as-is). + item.plan = std::move(plan); + item.payment_provider = std::move(payment_provider); + item.payment_id = std::move(payment_id); item.auto_renewing = auto_renewing; item.unredeemed_unix_ts = as_sys_seconds(unredeemed_ts); @@ -549,41 +460,6 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { item.platform_refund_expiry_unix_ts = as_sys_seconds(platform_refund_expiry_ts); item.revoked_unix_ts = as_sys_seconds(revoked_ts); item.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); - switch (item.payment_provider) { - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: [[fallthrough]]; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: { - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { - item.google_payment_token = - json_require(obj, "google_payment_token", result.errors); - assert(item.google_payment_token.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->google_payment_token)); - item.google_order_id = - json_require(obj, "google_order_id", result.errors); - assert(item.google_order_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->google_order_id)); - } break; - - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: { - item.apple_original_tx_id = - json_require(obj, "apple_original_tx_id", result.errors); - item.apple_tx_id = json_require(obj, "apple_tx_id", result.errors); - item.apple_web_line_order_id = - json_require(obj, "apple_web_line_order_id", result.errors); - assert(item.apple_original_tx_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->apple_original_tx_id)); - assert(item.apple_tx_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->apple_tx_id)); - assert(item.apple_web_line_order_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->apple_web_line_order_id)); - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { - item.rangeproof_order_id = - json_require(obj, "rangeproof_order_id", result.errors); - assert(item.rangeproof_order_id.size() < - sizeof(((session_pro_backend_pro_payment_item*)0)->rangeproof_order_id)); - } break; - } // Handle parsing result if (result.errors.size()) @@ -599,11 +475,10 @@ b64 SetPaymentRefundRequestedRequest::build_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { - // Hash components to 32 bytes, must match: - // https://github.com/Doy-lee/session-pro-backend/blob/5962925d7f18f83a3ff5774885495e5dd55ecb0a/server.py#L634 + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id) { + // Hash components to 32 bytes. The tail is the provider code string followed by the opaque + // payment id (spec §1); must match the backend's signed-hash construction. uint64_t ts = epoch_seconds(unix_ts); uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); auto hash_to_sign = hash::blake2b_pers<32>( @@ -612,9 +487,8 @@ b64 SetPaymentRefundRequestedRequest::build_sig( master_privkey.pubkey(), ts, refund_requested_ts, - static_cast(payment_tx_provider), - payment_tx_payment_id, - payment_tx_order_id); + payment_tx_provider_code, + payment_tx_payment_id); return ed25519::sign(master_privkey, hash_to_sign); } @@ -624,24 +498,21 @@ std::string SetPaymentRefundRequestedRequest::build_to_json( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, - std::span payment_tx_payment_id, - std::span payment_tx_order_id) { + std::string_view payment_tx_provider_code, + std::span payment_tx_payment_id) { auto sig = SetPaymentRefundRequestedRequest::build_sig( version, master_privkey, unix_ts, refund_requested_unix_ts, - payment_tx_provider, - payment_tx_payment_id, - payment_tx_order_id); + payment_tx_provider_code, + payment_tx_payment_id); SetPaymentRefundRequestedRequest request = {}; request.version = version; std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); - request.payment_tx.provider = payment_tx_provider; + request.payment_tx.provider_code = payment_tx_provider_code; request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); - request.payment_tx.order_id = to_string_view(payment_tx_order_id); request.master_sig = sig; request.unix_ts = unix_ts; request.refund_requested_unix_ts = refund_requested_unix_ts; @@ -655,21 +526,8 @@ std::string SetPaymentRefundRequestedRequest::to_json() const { j["master_pkey"] = oxenc::to_hex(master_pkey); j["ts"] = epoch_seconds(unix_ts); j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); - j["payment_tx"]["provider"] = payment_tx.provider; - switch (payment_tx.provider) { - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { - assert(false && "Unimplemented"); - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { - j["payment_tx"]["google_payment_token"] = payment_tx.payment_id; - j["payment_tx"]["google_order_id"] = payment_tx.order_id; - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: { - j["payment_tx"]["apple_tx_id"] = payment_tx.payment_id; - } break; - } + j["payment_tx"]["provider"] = payment_tx.provider_code; + j["payment_tx"]["payment_id"] = payment_tx.payment_id; j["master_sig"] = oxenc::to_hex(master_sig); std::string result = j.dump(); return result; @@ -719,11 +577,9 @@ session_pro_backend_add_pro_payment_request_build_sigs( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) { + size_t payment_tx_payment_id_len) { session_pro_backend_master_rotating_signatures result = {}; try { @@ -731,15 +587,13 @@ session_pro_backend_add_pro_payment_request_build_sigs( ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto sigs = AddProPaymentRequest::build_sigs( request_version, master_span, rotating_span, - payment_tx_provider, - payment_tx_payment_id_span, - payment_tx_order_id_span); + payment_tx_provider_code, + payment_tx_payment_id_span); std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; @@ -756,11 +610,9 @@ session_pro_backend_add_pro_payment_request_build_to_json( size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) { + size_t payment_tx_payment_id_len) { session_pro_backend_to_json result = {}; try { @@ -768,15 +620,13 @@ session_pro_backend_add_pro_payment_request_build_to_json( ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); std::string json = AddProPaymentRequest::build_to_json( request_version, master_span, rotating_span, - payment_tx_provider, - payment_tx_payment_id_span, - payment_tx_order_id_span); + payment_tx_provider_code, + payment_tx_payment_id_span); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -883,11 +733,10 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment cpp.version = request->version; std::memcpy(cpp.master_pkey.data(), request->master_pkey.data, cpp.master_pkey.size()); std::memcpy(cpp.rotating_pkey.data(), request->rotating_pkey.data, cpp.rotating_pkey.size()); - cpp.payment_tx.provider = request->payment_tx.provider; + cpp.payment_tx.provider_code = + std::string(request->payment_tx.provider_code, request->payment_tx.provider_code_count); cpp.payment_tx.payment_id = std::string(request->payment_tx.payment_id, request->payment_tx.payment_id_count); - cpp.payment_tx.order_id = - std::string(request->payment_tx.order_id, request->payment_tx.order_id_count); std::memcpy(cpp.master_sig.data(), request->master_sig.data, cpp.master_sig.size()); std::memcpy(cpp.rotating_sig.data(), request->rotating_sig.data, cpp.rotating_sig.size()); @@ -1159,9 +1008,9 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json const ProPaymentItem& src = cpp.items[index]; session_pro_backend_pro_payment_item& dest = result.items[index]; dest.status = src.status; - dest.plan = src.plan; - dest.payment_provider = src.payment_provider; - dest.payment_provider_metadata = src.payment_provider_metadata; + dest.plan_count = session::copy_c_str(dest.plan, src.plan) - 1; + dest.payment_provider_count = + session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; dest.unredeemed_ts = epoch_seconds(src.unredeemed_unix_ts); dest.redeemed_ts = epoch_seconds(src.redeemed_unix_ts); dest.expiry_ts = epoch_seconds(src.expiry_unix_ts); @@ -1170,32 +1019,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json dest.revoked_ts = epoch_seconds(src.revoked_unix_ts); dest.refund_requested_ts = epoch_seconds(src.refund_requested_unix_ts); - switch (dest.payment_provider) { - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_NIL: [[fallthrough]]; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT: break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE: { - dest.google_payment_token_count = - session::copy_c_str(dest.google_payment_token, src.google_payment_token) - - 1; - dest.google_order_id_count = - session::copy_c_str(dest.google_order_id, src.google_order_id) - 1; - } break; - - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_IOS_APP_STORE: { - dest.apple_original_tx_id_count = - session::copy_c_str(dest.apple_original_tx_id, src.apple_original_tx_id) - - 1; - dest.apple_tx_id_count = session::copy_c_str(dest.apple_tx_id, src.apple_tx_id) - 1; - dest.apple_web_line_order_id_count = - session::copy_c_str( - dest.apple_web_line_order_id, src.apple_web_line_order_id) - - 1; - } break; - case SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF: { - dest.rangeproof_order_id_count = - session::copy_c_str(dest.rangeproof_order_id, src.rangeproof_order_id) - 1; - } break; - } + dest.payment_id_count = session::copy_c_str(dest.payment_id, src.payment_id) - 1; } // Copy errors @@ -1217,11 +1041,9 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) { + size_t payment_tx_payment_id_len) { session_pro_backend_signature result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; @@ -1230,15 +1052,13 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto sig = SetPaymentRefundRequestedRequest::build_sig( request_version, master_span, unix_ts, refund_requested_unix_ts, - payment_tx_provider, - payment_tx_payment_id_span, - payment_tx_order_id_span); + payment_tx_provider_code, + payment_tx_payment_id_span); std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; } catch (const std::exception& e) { @@ -1254,11 +1074,9 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER payment_tx_provider, + const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len, - const unsigned char* payment_tx_order_id, - size_t payment_tx_order_id_len) { + size_t payment_tx_payment_id_len) { session_pro_backend_to_json result = {}; try { @@ -1268,15 +1086,13 @@ session_pro_backend_set_payment_refund_requested_request_build_to_json( session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto payment_tx_order_id_span = to_byte_span(payment_tx_order_id, payment_tx_order_id_len); auto json = SetPaymentRefundRequestedRequest::build_to_json( request_version, master_span, unix_ts, refund_requested_unix_ts, - payment_tx_provider, - payment_tx_payment_id_span, - payment_tx_order_id_span); + payment_tx_provider_code, + payment_tx_payment_id_span); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -1300,11 +1116,10 @@ session_pro_backend_set_payment_refund_requested_request_to_json( std::memcpy(cpp.master_sig.data(), request->master_sig.data, sizeof(request->master_sig.data)); cpp.unix_ts = session::as_sys_seconds(request->ts); cpp.refund_requested_unix_ts = session::as_sys_seconds(request->refund_requested_ts); - cpp.payment_tx.provider = request->payment_tx.provider; + cpp.payment_tx.provider_code = + std::string(request->payment_tx.provider_code, request->payment_tx.provider_code_count); cpp.payment_tx.payment_id = std::string(request->payment_tx.payment_id, request->payment_tx.payment_id_count); - cpp.payment_tx.order_id = - std::string(request->payment_tx.order_id, request->payment_tx.order_id_count); try { std::string json = cpp.to_json(); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 0dec22830..87b9d6a45 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -36,8 +36,11 @@ static bool string8_equals(string8 s8, std::string_view str) { [[maybe_unused]] static void dump_pro_payment_item( const session_pro_backend_pro_payment_item& item) { fprintf(stderr, "item.status: %d\n", item.status); - fprintf(stderr, "item.plan: %d\n", item.plan); - fprintf(stderr, "item.payment_provider: %d\n", item.payment_provider); + fprintf(stderr, "item.plan: %.*s\n", (int)item.plan_count, item.plan); + fprintf(stderr, + "item.payment_provider: %.*s\n", + (int)item.payment_provider_count, + item.payment_provider); fprintf(stderr, "item.auto_renewing: %d\n", item.auto_renewing); fprintf(stderr, "item.unredeemed_ts: %" PRId64 "zu\n", item.unredeemed_ts); fprintf(stderr, "item.redeemed_ts: %" PRId64 "zu\n", item.redeemed_ts); @@ -47,23 +50,7 @@ static bool string8_equals(string8 s8, std::string_view str) { "item.platform_refund_expiry_ts: %" PRId64 "zu\n", item.platform_refund_expiry_ts); fprintf(stderr, "item.revoked_ts: %" PRId64 "zu\n", item.revoked_ts); - fprintf(stderr, - "item.google_payment_token: %.*s\n", - (int)item.google_payment_token_count, - item.google_payment_token); - fprintf(stderr, - "item.apple_original_tx_id: %.*s\n", - (int)item.apple_original_tx_id_count, - item.apple_original_tx_id); - fprintf(stderr, "item.apple_tx_id: %.*s\n", (int)item.apple_tx_id_count, item.apple_tx_id); - fprintf(stderr, - "item.apple_web_line_order_id: %.*s\n", - (int)item.apple_web_line_order_id_count, - item.apple_web_line_order_id); - fprintf(stderr, - "item.rangeproof_order_id: %.*s\n", - (int)item.rangeproof_order_id_count, - item.rangeproof_order_id); + fprintf(stderr, "item.payment_id: %.*s\n", (int)item.payment_id_count, item.payment_id); } [[maybe_unused]] static void dump_pro_revocation( @@ -87,23 +74,22 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::string fake_google_payment_token_hex = - "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); + // Google's composite payment_id is "|"; libsession treats the + // whole thing as one opaque value. + std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + + oxenc::to_hex(fake_google_order_id); session_pro_backend_add_pro_payment_user_transaction payment_tx = {}; - payment_tx.provider = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE; - payment_tx.payment_id_count = fake_google_payment_token_hex.size(); - payment_tx.order_id_count = fake_google_order_id_hex.size(); - std::memcpy( - payment_tx.payment_id, - fake_google_payment_token_hex.data(), - payment_tx.payment_id_count); + payment_tx.provider_code_count = + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; std::memcpy( - payment_tx.order_id, fake_google_order_id_hex.data(), payment_tx.order_id_count); + payment_tx.provider_code, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); + payment_tx.payment_id_count = fake_payment_id.size(); + std::memcpy(payment_tx.payment_id, fake_payment_id.data(), payment_tx.payment_id_count); int64_t unix_ts = 1698765432; // Arbitrary timestamp (unix epoch seconds) @@ -116,11 +102,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - payment_tx.provider, + payment_tx.provider_code, reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count); + payment_tx.payment_id_count); INFO(result.error); REQUIRE(result.success); REQUIRE(result.error_count == 0); @@ -130,9 +114,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { 0, master_privkey.data, rotating_privkey.data, - payment_tx.provider, - to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count), - to_byte_span(payment_tx.order_id, payment_tx.order_id_count)); + payment_tx.provider_code, + to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); REQUIRE(std::memcmp( result.master_sig.data, @@ -150,11 +133,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data) - 1, rotating_privkey.data, sizeof(rotating_privkey.data), - payment_tx.provider, + payment_tx.provider_code, reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count); + payment_tx.payment_id_count); REQUIRE(!result.success); REQUIRE(result.error_count > 0); } @@ -214,11 +195,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - payment_tx.provider, + payment_tx.provider_code, reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count); + payment_tx.payment_id_count); request.master_sig = sigs.master_sig; request.rotating_sig = sigs.rotating_sig; @@ -242,11 +221,10 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { cpp.rotating_pkey.data(), rotating_pubkey.data, sizeof(rotating_pubkey.data)); - cpp.payment_tx.provider = payment_tx.provider; + cpp.payment_tx.provider_code = + std::string(payment_tx.provider_code, payment_tx.provider_code_count); cpp.payment_tx.payment_id = std::string(payment_tx.payment_id, payment_tx.payment_id_count); - cpp.payment_tx.order_id = - std::string(payment_tx.order_id, payment_tx.order_id_count); std::memcpy( cpp.master_sig.data(), sigs.master_sig.data, sizeof(sigs.master_sig.data)); std::memcpy( @@ -263,11 +241,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - request.payment_tx.provider, + request.payment_tx.provider_code, reinterpret_cast(request.payment_tx.payment_id), - request.payment_tx.payment_id_count, - reinterpret_cast(request.payment_tx.order_id), - request.payment_tx.order_id_count); + request.payment_tx.payment_id_count); REQUIRE(one_shot.success); REQUIRE(one_shot.json.size == result.json.size); INFO("One shot: " << one_shot.json.data << "\n\nJSON: " << result.json.data); @@ -630,9 +606,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"items", nlohmann::json::array( {{{"status", SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED}, - {"plan", SESSION_PRO_BACKEND_PLAN_ONE_MONTH}, + {"plan", "1m"}, {"payment_provider", - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE}, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY}, {"auto_renewing", false}, {"unredeemed_ts", unix_ts - 3600}, {"redeemed_ts", unix_ts - 3600}, @@ -641,10 +617,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"platform_refund_expiry_ts", unix_ts + 1}, {"revoked_ts", unix_ts + 3600}, {"refund_requested_ts", unix_ts + 3601}, - {"google_payment_token", - std::string(payment_tx.payment_id, payment_tx.payment_id_count)}, - {"google_order_id", - std::string(payment_tx.order_id, payment_tx.order_id_count)}}})}}; + {"payment_id", + std::string( + payment_tx.payment_id, payment_tx.payment_id_count)}}})}}; std::string json = j.dump(); // Valid Google JSON @@ -670,9 +645,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.payments_total == 3); REQUIRE(result.items != nullptr); REQUIRE(result.items[0].status == SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED); - REQUIRE(result.items[0].plan == SESSION_PRO_BACKEND_PLAN_ONE_MONTH); - REQUIRE(result.items[0].payment_provider == - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE); + REQUIRE(std::string_view(result.items[0].plan, result.items[0].plan_count) == "1m"); + REQUIRE(std::string_view( + result.items[0].payment_provider, + result.items[0].payment_provider_count) == + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY); REQUIRE(result.items[0].unredeemed_ts == unix_ts - 3600); REQUIRE(result.items[0].redeemed_ts == unix_ts - 3600); REQUIRE(result.items[0].expiry_ts == unix_ts); @@ -680,25 +657,18 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); REQUIRE(result.items[0].revoked_ts == unix_ts + 3600); REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); - REQUIRE(result.items[0].google_payment_token_count == payment_tx.payment_id_count); + REQUIRE(result.items[0].payment_id_count == payment_tx.payment_id_count); REQUIRE(std::memcmp( - result.items[0].google_payment_token, + result.items[0].payment_id, payment_tx.payment_id, payment_tx.payment_id_count) == 0); - REQUIRE(result.items[0].google_order_id_count == payment_tx.order_id_count); - REQUIRE(std::memcmp( - result.items[0].google_order_id, - payment_tx.order_id, - payment_tx.order_id_count) == 0); } - // Tweak JSON for Rangeproof + // Tweak JSON for a different provider. payment_id is opaque so nothing + // provider-specific changes in how libsession parses it. j["result"]["items"][0]["payment_provider"] = - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_RANGEPROOF; - j["result"]["items"][0].erase("google_payment_token"); - j["result"]["items"][0].erase("google_order_id"); - j["result"]["items"][0]["rangeproof_order_id"] = - std::string(payment_tx.order_id, payment_tx.order_id_count); + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF; + j["result"]["items"][0]["payment_id"] = "rangeproof-opaque-id"; json = j.dump(); // Valid Rangeproof JSON @@ -712,12 +682,14 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { INFO(result_rangeproof.header.errors[index].data); // Only check what we expect to be different - REQUIRE(result_rangeproof.items[0].rangeproof_order_id_count == - payment_tx.order_id_count); - REQUIRE(std::memcmp( - result_rangeproof.items[0].rangeproof_order_id, - payment_tx.order_id, - payment_tx.order_id_count) == 0); + REQUIRE(std::string_view( + result_rangeproof.items[0].payment_provider, + result_rangeproof.items[0].payment_provider_count) == + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF); + REQUIRE(std::string_view( + result_rangeproof.items[0].payment_id, + result_rangeproof.items[0].payment_id_count) == + "rangeproof-opaque-id"); } // After freeing @@ -782,11 +754,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { sizeof(master_privkey.data), request.ts, request.refund_requested_ts, - payment_tx.provider, + payment_tx.provider_code, reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count); + payment_tx.payment_id_count); request.master_sig = sig.sig; REQUIRE(sig.success); @@ -944,23 +914,22 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { { std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::string fake_google_payment_token_hex = - "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); + // Google's composite payment_id is "|"; libsession treats the + // whole thing as one opaque value. + std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + + oxenc::to_hex(fake_google_order_id); session_pro_backend_add_pro_payment_user_transaction payment_tx = {}; - payment_tx.provider = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE; - payment_tx.payment_id_count = fake_google_payment_token_hex.size(); - payment_tx.order_id_count = fake_google_order_id_hex.size(); - std::memcpy( - payment_tx.payment_id, - fake_google_payment_token_hex.data(), - payment_tx.payment_id_count); + payment_tx.provider_code_count = + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; std::memcpy( - payment_tx.order_id, fake_google_order_id_hex.data(), payment_tx.order_id_count); + payment_tx.provider_code, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); + payment_tx.payment_id_count = fake_payment_id.size(); + std::memcpy(payment_tx.payment_id, fake_payment_id.data(), payment_tx.payment_id_count); // Build request session_pro_backend_master_rotating_signatures add_pro_sigs = @@ -970,11 +939,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - payment_tx.provider, + payment_tx.provider_code, reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count, - reinterpret_cast(payment_tx.order_id), - payment_tx.order_id_count); + payment_tx.payment_id_count); session_pro_backend_add_pro_payment_request request = {}; request.version = 0; @@ -1187,24 +1154,22 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { { std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::string fake_google_payment_token_hex = - "DEV." + oxenc::to_hex(fake_google_payment_token); - std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - std::string fake_google_order_id_hex = "DEV." + oxenc::to_hex(fake_google_order_id); + std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + + oxenc::to_hex(fake_google_order_id); - another_payment_tx.provider = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_GOOGLE_PLAY_STORE; - another_payment_tx.payment_id_count = fake_google_payment_token_hex.size(); - another_payment_tx.order_id_count = fake_google_order_id_hex.size(); + another_payment_tx.provider_code_count = + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; + std::memcpy( + another_payment_tx.provider_code, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); + another_payment_tx.payment_id_count = fake_payment_id.size(); std::memcpy( another_payment_tx.payment_id, - fake_google_payment_token_hex.data(), + fake_payment_id.data(), another_payment_tx.payment_id_count); - std::memcpy( - another_payment_tx.order_id, - fake_google_order_id_hex.data(), - another_payment_tx.order_id_count); // Build request session_pro_backend_master_rotating_signatures add_pro_sigs = @@ -1214,11 +1179,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - another_payment_tx.provider, + another_payment_tx.provider_code, reinterpret_cast(another_payment_tx.payment_id), - another_payment_tx.payment_id_count, - reinterpret_cast(another_payment_tx.order_id), - another_payment_tx.order_id_count); + another_payment_tx.payment_id_count); session_pro_backend_add_pro_payment_request request = {}; request.version = 0; @@ -1306,11 +1269,9 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(master_privkey.data), /*ts*/ now_unix_ts, /*refund_requested_ts*/ now_unix_ts, - another_payment_tx.provider, + another_payment_tx.provider_code, reinterpret_cast(another_payment_tx.payment_id), - another_payment_tx.payment_id_count, - reinterpret_cast(another_payment_tx.order_id), - another_payment_tx.order_id_count); + another_payment_tx.payment_id_count); scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; From 0d752b40f3ffb77660c42cb7eabe44464c7147b0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 22:46:05 -0300 Subject: [PATCH 103/193] Refactor Pro feature bitsets into type-safe enum-class flags Replace the C-style Pro feature bitset structs and bit-position enums with C++ enum-class flag types, and simplify the corresponding C API. The on-wire representations are unchanged (see "Wire format" below); this only reworks the in-code types and the C API surface. C++ types: - session::ProProfileBitset / ProMessageBitset structs become ProProfileFlags / ProMessageFlags, which are `enum class : uint64_t` with single-bit values (1 << N), a `None` enumerator, and a `session::detail::ProFlags` concept that gates a shared set of bitwise operators (| |= & &= ~) plus a contains() helper. This makes the two flag sets distinct types that can't be mixed and removes the raw `uint64_t data` member that could be assigned any integer. C API: - The session_protocol_pro_profile_bitset / session_protocol_pro_message_bitset wrapper structs are removed; a bitset is now a plain uint64_t. - The six helper functions session_protocol_pro_{profile,message}_bitset_{set,unset,is_set} are removed; C callers manipulate the uint64_t directly with |, &= ~, and &. Feature flag constants: - The position-valued C enums SESSION_PROTOCOL_PRO_PROFILE_FEATURES and SESSION_PROTOCOL_PRO_MESSAGE_FEATURES, and the COUNT sentinel, are removed. - They are replaced by `extern const uint64_t` constants defined in session_protocol.cpp from the C++ enum values, which are now the single source of truth. - The constants' VALUES change from bit positions (0, 1, ...) to bit masks (1 << 0, 1 << 1, ...), so they are OR'd/AND'd in directly rather than shifted. - The per-feature constants are renamed to singular FEATURE: SESSION_PROTOCOL_PRO_{PROFILE,MESSAGE}_FEATURES_* -> ..._FEATURE_* (each names one feature). Collective names that refer to the whole feature set stay plural (SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS, session_protocol_pro_features_for_*, user_profile_get_pro_features, ...). C++ field / method renames (the C API struct fields deliberately keep `bitset`): - contact_info::profile_bitset / blinded_contact_info::profile_bitset -> profile_flags - DecodedPro::msg_bitset / profile_bitset -> msg_flags / profile_flags - ProFeaturesForMsg::bitset -> flags - UserProfile::get_profile_bitset() -> get_profile_flags() - C counterparts (contacts_contact::profile_bitset, session_protocol_decoded_pro::msg_bitset/profile_bitset, session_protocol_pro_features_for_msg::bitset) keep the `bitset` name; the C<->C++ marshalling is the only place the two vocabularies meet. Config helpers: - bitset_from_set_of_int64_or_0 / set_int64_set_from_bitset are replaced by the ProFlags-constrained templates to_flags() / set_flags(), so config (de)serialization works in the flag enums directly instead of casting to and from uint64_t at every call site. Wire format (unchanged): - Swarm config still stores each enabled feature as a set of bit *positions* (so the CRDT can merge concurrent per-flag changes as a set union); the position<->mask conversion is handled internally by to_flags()/set_flags() and UserProfile's setters. - The protobuf ProMessage still carries the packed uint64 mask. No stored data changes and no migration is required. BREAKING (C API) -- consumers must: - Treat a feature bitset as a plain uint64_t (no wrapper struct, no `.data`). - Use the SESSION_PROTOCOL_PRO_*_FEATURE_* constants as masks directly (`flags |= X`, `flags &= ~X`, `flags & X`); any code that previously did `1 << FEATURE` must drop the shift, as the constant is now the mask. - Update the renamed constants (_FEATURES_* -> _FEATURE_*). - Replace calls to the removed *_bitset_{set,unset,is_set} helpers with the equivalent bitwise operations. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/config/contacts.h | 4 +- include/session/config/contacts.hpp | 6 +- include/session/config/user_profile.h | 6 +- include/session/config/user_profile.hpp | 20 ++++-- include/session/session_protocol.h | 90 ++++++------------------ include/session/session_protocol.hpp | 91 ++++++++++++++++--------- src/config/contacts.cpp | 24 +++---- src/config/internal.cpp | 22 ------ src/config/internal.hpp | 33 +++++++-- src/config/user_profile.cpp | 35 +++++----- src/session_protocol.cpp | 90 +++++------------------- tests/test_config_contacts.cpp | 28 ++++---- tests/test_config_userprofile.cpp | 27 +++----- tests/test_session_protocol.cpp | 50 +++++++------- 14 files changed, 221 insertions(+), 305 deletions(-) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 87bd35784..e2ecd73a7 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -36,7 +36,7 @@ typedef struct contacts_contact { int64_t created; // unix timestamp (seconds) - session_protocol_pro_profile_bitset profile_bitset; + uint64_t profile_bitset; // Mask of SESSION_PROTOCOL_PRO_PROFILE_FEATURE_* bits } contacts_contact; @@ -55,7 +55,7 @@ typedef struct contacts_blinded_contact { bool legacy_blinding; int64_t created; // unix timestamp (seconds) - session_protocol_pro_profile_bitset profile_bitset; + uint64_t profile_bitset; // Mask of SESSION_PROTOCOL_PRO_PROFILE_FEATURE_* bits } contacts_blinded_contact; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 53c8a2eef..f5749967a 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -96,7 +96,7 @@ struct contact_info { std::chrono::seconds exp_timer{0}; // The expiration timer (in seconds) int64_t created = 0; // Unix timestamp (seconds) when this contact was added - ProProfileBitset profile_bitset = {}; + ProProfileFlags profile_flags = ProProfileFlags::None; explicit contact_info(std::string sid); @@ -141,7 +141,7 @@ struct blinded_contact_info { bool legacy_blinding; std::chrono::sys_seconds created{}; // Unix timestamp (seconds) when this contact was added - ProProfileBitset profile_bitset = {}; + ProProfileFlags profile_flags = ProProfileFlags::None; blinded_contact_info() = default; explicit blinded_contact_info( @@ -443,7 +443,7 @@ class Contacts : public ConfigBase { /// Inputs: /// - `session_id` -- hex string of the session id /// - `features` -- The updated profile features to use - void set_pro_features(std::string_view session_id, ProProfileBitset features); + void set_pro_features(std::string_view session_id, ProProfileFlags features); /// API: contacts/contacts::erase /// diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 1cd273fa6..e2b9d2123 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -348,9 +348,9 @@ LIBSESSION_EXPORT bool user_profile_remove_pro_config(config_object* conf); /// - `conf` -- [in] Pointer to the config object /// /// Outputs: -/// - `session_protocol_pro_profile_bitset` - bitset indicating which profile features are enabled. -LIBSESSION_EXPORT session_protocol_pro_profile_bitset -user_profile_get_pro_features(const config_object* conf); +/// - `uint64_t` - bitset (mask of SESSION_PROTOCOL_PRO_PROFILE_FEATURE_* bits) indicating which +/// profile features are enabled. +LIBSESSION_EXPORT uint64_t user_profile_get_pro_features(const config_object* conf); /// API: user_profile/user_profile_set_pro_badge /// diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index d994da399..89b2becd5 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -274,18 +274,18 @@ class UserProfile : public ConfigBase { /// - `bool` - Flag indicating whether the config had Session Pro config removed or not. bool remove_pro_config(); - /// API: user_profile/UserProfile::get_pro_features + /// API: user_profile/UserProfile::get_profile_flags /// - /// Retrieves the bitset indicating which pro features the user currently has enabled. + /// Retrieves the flags indicating which pro features the user currently has enabled. /// /// Inputs: None /// /// Outputs: - /// - Bitset with individual bits set on it corresponding to - /// SESSION_PROTOCOL_PRO_PROFILE_FEATURES_BITSET. It is possible to receive bits set that don't - /// have a corresponding enum value if you are receiving a bitset from a newer client with newer - /// features enabled. These flags should be ignored by clients that do not recognise them. - ProProfileBitset get_profile_bitset() const; + /// - `ProProfileFlags` with the individual `ProProfileFlags::*` bits set that the user has + /// enabled. It is possible to receive bits set that don't have a corresponding enumerator if + /// you are receiving flags from a newer client with newer features enabled; unrecognised bits + /// should be ignored. + ProProfileFlags get_profile_flags() const; /// API: user_profile/UserProfile::set_pro_badge /// @@ -329,6 +329,12 @@ class UserProfile : public ConfigBase { /// nullopt to remove the value. void set_pro_access_expiry( std::optional> access_expiry_ts_ms); + + private: + // Enables/disables a single profile feature flag in the synced "f" set. The set stores feature + // *bit positions*, so `flag` must be a single-bit ProProfileFlags value (deflated here to its + // position via countr_zero). + void set_profile_feature(ProProfileFlags flag, bool enabled); }; } // namespace session::config diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index d68f4f444..34884ed01 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -85,41 +85,20 @@ struct session_protocol_pro_proof { cbytes64 sig; }; -// Feature flags for profile features where each enum value indicates the bit position in the -// corresponding bitset, e.g. (1 << ENUM_VAL) -typedef enum SESSION_PROTOCOL_PRO_PROFILE_FEATURES { - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE, - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR, - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_COUNT, -} SESSION_PROTOCOL_PRO_PROFILE_FEATURES; - -// Strongly typed bitset for profile features. Each profile enum value corresponds to the bit -// position to set on the bitset (e.g. 1 << ENUM_VALUE). This bitset is wrapped in a struct and has -// helper functions (`session_protocol_pro_profile_bitset_*` family of functions) that accepts the -// typed-enum to mitigate against mixing up the profile features with the message features. +// Session Pro feature flag bits. These are plain `uint64_t` bit masks (`1 << position`) that are +// OR'd together into a profile/message feature bitset (itself just a `uint64_t`). They mirror the +// C++ `session::ProProfileFlags` / `session::ProMessageFlags` enum classes, which are the source +// of truth; these constants are defined from those enum values in session_protocol.cpp. // -// The enums are kept as bit positions (ENUM_VAL = 1 << N) instead of bit values (ENUM_VAL = 1) -// for ergonomic usage in the way we sync and store these bitsets on the protocol swarms. These -// bitsets are stored as sets which allows us to do diffs and deltas on the set of values. The -// syncing scheme does not allow bit-level deltas which makes handling conflicts between competing -// synced configurations, awkward. -typedef struct session_protocol_pro_profile_bitset session_protocol_pro_profile_bitset; -struct session_protocol_pro_profile_bitset { - uint64_t data; -}; +// Manipulate a bitset directly with the standard bitwise operators, e.g.: +// uint64_t features = 0; +// features |= SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE; // set +// features &= ~SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE; // unset +// if (features & SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE) { ... } // test +extern const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE; +extern const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_ANIMATED_AVATAR; -// Feature flags for message features where each enum value indicates the bit position in the -// corresponding bitset. -typedef enum SESSION_PROTOCOL_PRO_MESSAGE_FEATURES { - SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT, -} SESSION_PROTOCOL_PRO_MESSAGE_FEATURES; - -// Strongly typed bitset for Session Pro message features (see -// `session_protocol_pro_profile_bitset`) -typedef struct session_protocol_pro_message_bitset session_protocol_pro_message_bitset; -struct session_protocol_pro_message_bitset { - uint64_t data; -}; +extern const uint64_t SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT; typedef enum SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS { // See session::ProFeaturesForMsgStatus SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS, @@ -159,8 +138,9 @@ typedef struct session_protocol_decoded_pro session_protocol_decoded_pro; struct session_protocol_decoded_pro { SESSION_PROTOCOL_PRO_STATUS status; session_protocol_pro_proof proof; - session_protocol_pro_message_bitset msg_bitset; - session_protocol_pro_profile_bitset profile_bitset; + // Bitsets of SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_* / SESSION_PROTOCOL_PRO_PROFILE_FEATURE_* + uint64_t msg_bitset; + uint64_t profile_bitset; }; typedef struct session_protocol_decoded_envelope session_protocol_decoded_envelope; @@ -198,41 +178,9 @@ struct session_protocol_decoded_community_message { size_t error_len_incl_null_terminator; }; -/// API: session_protocol/session_protocol_pro_profile_bitset_is_set -/// -/// Check if the feature flag is set on the bitset -LIBSESSION_EXPORT bool session_protocol_pro_profile_bitset_is_set( - session_protocol_pro_profile_bitset value, SESSION_PROTOCOL_PRO_PROFILE_FEATURES features); - -/// API: session_protocol/session_protocol_pro_profile_bitset_set -/// -/// Set the feature flag on the bitset -LIBSESSION_EXPORT void session_protocol_pro_profile_bitset_set( - session_protocol_pro_profile_bitset* value, SESSION_PROTOCOL_PRO_PROFILE_FEATURES features); - -/// API: session_protocol/session_protocol_pro_profile_bitset_unset -/// -/// Unset the feature flag on the bitset -LIBSESSION_EXPORT void session_protocol_pro_profile_bitset_unset( - session_protocol_pro_profile_bitset* value, SESSION_PROTOCOL_PRO_PROFILE_FEATURES features); - -/// API: session_protocol/session_protocol_pro_profile_bitset_is_set -/// -/// Check if the feature flag is set on the bitset -LIBSESSION_EXPORT bool session_protocol_pro_message_bitset_is_set( - session_protocol_pro_message_bitset value, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features); - -/// API: session_protocol/session_protocol_pro_profile_bitset_set -/// -/// Set the feature flag on the bitset -LIBSESSION_EXPORT void session_protocol_pro_message_bitset_set( - session_protocol_pro_message_bitset* value, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features); - -/// API: session_protocol/session_protocol_pro_profile_bitset_unset -/// -/// Unset the feature flag on the bitset -LIBSESSION_EXPORT void session_protocol_pro_message_bitset_unset( - session_protocol_pro_message_bitset* value, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features); +// The Pro feature bitsets are plain `uint64_t` masks of SESSION_PROTOCOL_PRO_*_FEATURES_* bits; +// set/unset/test them with the standard bitwise operators (see the feature constants above). No +// accessor functions are needed. /// API: session_protocol/session_protocol_pro_proof_hash /// @@ -340,7 +288,7 @@ LIBSESSION_EXPORT SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( typedef struct session_protocol_pro_features_for_msg { SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS status; string8 error; - session_protocol_pro_message_bitset bitset; + uint64_t bitset; // Mask of SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_* bits size_t codepoint_count; } session_protocol_pro_features_for_msg; diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 8febafefd..c0c8a5056 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -35,27 +35,6 @@ /// messages is libsession itself and that it will provide wrapper/proxy types for and handle /// converting those into the wire format. -// NOTE: In the CPP file we use C-style enums for bitfields and CPP-style enums for non-bitfield -// enums where we can to benefit from the type-safety of strong enums. -// -// CPP doesn't support named bitfields without casting or operator overloads but C-style -// enums support it very well. The only issue is that using a native C-style enum enforces some type -// restrictions that compilers dislike when attempting to manipulate bit fields. For example: -// -// enum Feature {x = 1 << 0, y = 1 << 1} -// Feature f = x | y -// -// Causes the compiler to complain about trying to do bit ops/assign an unsigned integer to an enum -// `Feature`. We use a common C pattern/trick by suffixing an underscore to the the original enum, -// then type define the non-suffixed enum to an unsigned integer: -// -// enum Feature_ {x = 1 << 0, y = 1 << 1} -// typedef U64 Feature -// Feature f = x | y -// -// Does not trigger errors as the underlying type of `f` is actually an unsigned integer. The type -// define is merely a hint to the user to what flags are to be used when manipulating the variable. - namespace session { enum ProProofVersion { ProProofVersion_v0 }; @@ -197,24 +176,68 @@ enum class ProFeaturesForMsgStatus { ExceedsCharacterLimit = SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_EXCEEDS_CHARACTER_LIMIT, }; -struct ProProfileBitset { - uint64_t data; - void set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features); - void unset(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features); - bool is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) const; +// Session Pro profile feature flags. The enumerator values are the single-bit masks (`1 << +// position`) and are the source of truth for these feature bits; the C API re-exposes the same +// values as `extern const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_*` constants. Combine and +// test them with the bitwise operators defined below. +enum class ProProfileFlags : uint64_t { + None = 0, + ProBadge = 1ull << 0, + AnimatedAvatar = 1ull << 1, }; -struct ProMessageBitset { - uint64_t data; - void set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features); - void unset(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features); - bool is_set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) const; +// Session Pro message feature flags (see ProProfileFlags). +enum class ProMessageFlags : uint64_t { + None = 0, + CharLimit10k = 1ull << 0, }; +namespace detail { + template + inline constexpr bool is_pro_flags = false; + template <> + inline constexpr bool is_pro_flags = true; + template <> + inline constexpr bool is_pro_flags = true; + + template + concept ProFlags = is_pro_flags; +} // namespace detail + +// Bitwise algebra for the Pro feature flag sets above. Defined once for every flag enum via the +// `ProFlags` concept so the two types can't be accidentally mixed and so the logic lives in a +// single place. +template +constexpr E operator|(E a, E b) { + return static_cast(static_cast(a) | static_cast(b)); +} +template +constexpr E& operator|=(E& a, E b) { + return a = a | b; +} +template +constexpr E operator&(E a, E b) { + return static_cast(static_cast(a) & static_cast(b)); +} +template +constexpr E& operator&=(E& a, E b) { + return a = a & b; +} +template +constexpr E operator~(E a) { + return static_cast(~static_cast(a)); +} + +// True if every bit set in `flag` is also set in `flags` (i.e. `flags` contains `flag`). +template +constexpr bool contains(E flags, E flag) { + return (flags & flag) == flag; +} + struct ProFeaturesForMsg { ProFeaturesForMsgStatus status; std::string_view error; - ProMessageBitset bitset; + ProMessageFlags flags; size_t codepoint_count; }; @@ -238,8 +261,8 @@ struct DecodedPro { // Session Pro proof that was embedded in the envelope, this is always populated irrespective of // the status but the validity of the contents should be verified by checking `status` ProProof proof; - ProMessageBitset msg_bitset; - ProProfileBitset profile_bitset; + ProMessageFlags msg_flags; + ProProfileFlags profile_flags; }; struct DecodedEnvelope { diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 65ff3110a..8a8249b5f 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -115,9 +115,9 @@ void contact_info::load(const dict& info_dict) { created = to_epoch_seconds(int_or_0(info_dict, "j")); - const session::config::set* profile_bitset_set = maybe_set(info_dict, "f"); - if (profile_bitset_set) - profile_bitset.data = bitset_from_set_of_int64_or_0(*profile_bitset_set); + const session::config::set* profile_flags_set = maybe_set(info_dict, "f"); + if (profile_flags_set) + profile_flags = to_flags(*profile_flags_set); } void contact_info::into(contacts_contact& c) const { @@ -142,7 +142,7 @@ void contact_info::into(contacts_contact& c) const { if (c.exp_seconds <= 0 && c.exp_mode != CONVO_EXPIRATION_NONE) c.exp_mode = CONVO_EXPIRATION_NONE; c.created = to_epoch_seconds(created); - c.profile_bitset.data = profile_bitset.data; + c.profile_bitset = static_cast(profile_flags); } contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, 66} { @@ -169,7 +169,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, if (exp_timer <= 0s && exp_mode != expiration_mode::none) exp_mode = expiration_mode::none; created = to_epoch_seconds(c.created); - profile_bitset.data = c.profile_bitset.data; + profile_flags = static_cast(c.profile_bitset); } std::optional Contacts::get(std::string_view pubkey_hex) const { @@ -229,7 +229,7 @@ void Contacts::set(const contact_info& contact) { contact.exp_timer.count()); set_positive_int(info["j"], to_epoch_seconds(contact.created)); - set_int64_set_from_bitset(info["f"], contact.profile_bitset.data); + set_flags(info["f"], contact.profile_flags); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -300,9 +300,9 @@ void Contacts::set_created(std::string_view session_id, int64_t timestamp) { set(c); } -void Contacts::set_pro_features(std::string_view session_id, ProProfileBitset features) { +void Contacts::set_pro_features(std::string_view session_id, ProProfileFlags features) { auto c = get_or_construct(session_id); - c.profile_bitset = features; + c.profile_flags = features; set(c); } @@ -353,7 +353,7 @@ void blinded_contact_info::load(const dict& info_dict) { auto it = info_dict.find("f"); if (it != info_dict.end()) { if (auto* set = std::get_if(&it->second)) - profile_bitset.data = bitset_from_set_of_int64_or_0(*set); + profile_flags = to_flags(*set); } } @@ -375,7 +375,7 @@ void blinded_contact_info::into(contacts_blinded_contact& c) const { c.priority = priority; c.legacy_blinding = legacy_blinding; c.created = epoch_seconds(created); - c.profile_bitset.data = profile_bitset.data; + c.profile_bitset = static_cast(profile_flags); } blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { @@ -393,7 +393,7 @@ blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { priority = c.priority; legacy_blinding = c.legacy_blinding; created = to_sys_seconds(c.created); - profile_bitset.data = c.profile_bitset.data; + profile_flags = static_cast(c.profile_bitset); } const std::string blinded_contact_info::session_id() const { @@ -510,7 +510,7 @@ void Contacts::set_blinded(const blinded_contact_info& bc) { set_nonzero_int(info["+"], bc.priority); set_positive_int(info["y"], bc.legacy_blinding); set_ts(info["j"], bc.created); - set_int64_set_from_bitset(info["f"], bc.profile_bitset.data); + set_flags(info["f"], bc.profile_flags); } bool Contacts::erase_blinded(std::string_view base_url_, std::string_view blinded_id) { diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 3233b049a..50f9a57f8 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -147,28 +147,6 @@ std::optional maybe_string(const session::config::dict& d, const ch return std::nullopt; } -uint64_t bitset_from_set_of_int64_or_0(const session::config::set& s) { - uint64_t result = 0; - constexpr size_t bits_available = sizeof(result) * 8; - for (auto& v : s) { - auto* val = std::get_if(&v); - if (val && (*val >= 0 && *val < bits_available)) - result |= (1ULL << *val); - } - return result; -} - -void set_int64_set_from_bitset(ConfigBase::DictFieldProxy&& field, uint64_t bitset) { - constexpr size_t bits_available = sizeof(bitset) * 8; - for (size_t index = 0; index < bits_available; index++) { - uint64_t bit = bitset & (1ULL << index); - if (bit) - field.set_insert(index); - else - field.set_erase(index); - } -} - std::string string_or_empty(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 5b2aab0fe..e0d8aa0d1 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -7,12 +7,14 @@ #include #include #include +#include #include "../internal-util.hpp" #include "session/clock.hpp" #include "session/config/base.h" #include "session/config/base.hpp" #include "session/config/error.h" +#include "session/session_protocol.hpp" #include "session/types.hpp" namespace session { @@ -192,11 +194,34 @@ std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* // Digs into a config `dict` to get out a string; nullopt if not there (or not string) std::optional maybe_string(const session::config::dict& d, const char* key); -// Extract a U64 bitset from a set of i64's -uint64_t bitset_from_set_of_int64_or_0(const session::config::set& s); +// A feature flag enum (ProProfileFlags/ProMessageFlags) is stored in a config as the *set of bit +// positions* that are set (e.g. the flags 0b101 are stored as the set {0, 2}) rather than as a +// single packed integer, so that the CRDT merges concurrent per-flag changes as a set union instead +// of clobbering the whole value. These two helpers convert between that stored set and the enum. + +// Converts a config set of bit positions into flags of type `E`. Set elements that aren't int64s or +// fall outside [0, 64) are ignored; an empty or absent set yields no flags. +template +E to_flags(const session::config::set& positions) { + uint64_t mask = 0; + for (const auto& v : positions) + if (auto* pos = std::get_if(&v); pos && *pos >= 0 && *pos < 64) + mask |= 1ULL << *pos; + return static_cast(mask); +} -// Individually write each bit from bitset into a set consisting of int64's -void set_int64_set_from_bitset(ConfigBase::DictFieldProxy&& field, uint64_t bitset); +// Inverse of to_flags(): writes `f` into `field` as the set of its set bit positions (inserting the +// position of each set flag, erasing that of each unset one). +template +void set_flags(ConfigBase::DictFieldProxy&& field, E f) { + auto mask = static_cast(f); + for (int64_t pos = 0; pos < 64; pos++) { + if (mask & (1ULL << pos)) + field.set_insert(pos); + else + field.set_erase(pos); + } +} // Digs into a config `dict` to get out a string; ""s if not there (or not string) std::string string_or_empty(const session::config::dict& d, const char* key); diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 77e5875fc..68d7c0409 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -2,6 +2,8 @@ #include +#include + #include "internal.hpp" #include "session/config/contacts.hpp" #include "session/config/error.h" @@ -183,16 +185,18 @@ bool UserProfile::remove_pro_config() { return result; } -session::ProProfileBitset UserProfile::get_profile_bitset() const { - ProProfileBitset result = {}; +session::ProProfileFlags UserProfile::get_profile_flags() const { + ProProfileFlags result = ProProfileFlags::None; if (const config::set* set = data["f"].set()) - result.data = bitset_from_set_of_int64_or_0(*set); + result = to_flags(*set); return result; } -void UserProfile::set_pro_badge(bool enabled) { - auto feature = SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE; - bool dirtied = enabled ? data["f"].set_insert(feature) : data["f"].set_erase(feature); +void UserProfile::set_profile_feature(ProProfileFlags flag, bool enabled) { + // The "f" set stores feature bit *positions*, so deflate the single-bit mask to its position. + assert(std::has_single_bit(static_cast(flag))); + auto position = std::countr_zero(static_cast(flag)); + bool dirtied = enabled ? data["f"].set_insert(position) : data["f"].set_erase(position); if (dirtied) { const auto target_timestamp = (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); @@ -200,14 +204,12 @@ void UserProfile::set_pro_badge(bool enabled) { } } +void UserProfile::set_pro_badge(bool enabled) { + set_profile_feature(ProProfileFlags::ProBadge, enabled); +} + void UserProfile::set_animated_avatar(bool enabled) { - auto feature = SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR; - bool dirtied = enabled ? data["f"].set_insert(feature) : data["f"].set_erase(feature); - if (dirtied) { - const auto target_timestamp = - (data["t"].integer_or(0) >= data["T"].integer_or(0) ? "t" : "T"); - data[target_timestamp] = clock_now_s(); - } + set_profile_feature(ProProfileFlags::AnimatedAvatar, enabled); } std::optional> UserProfile::get_pro_access_expiry() @@ -379,11 +381,8 @@ LIBSESSION_C_API bool user_profile_remove_pro_config(config_object* conf) { return unbox(conf)->remove_pro_config(); } -LIBSESSION_C_API session_protocol_pro_profile_bitset -user_profile_get_pro_features(const config_object* conf) { - session_protocol_pro_profile_bitset result = {}; - result.data = unbox(conf)->get_profile_bitset().data; - return result; +LIBSESSION_C_API uint64_t user_profile_get_pro_features(const config_object* conf) { + return static_cast(unbox(conf)->get_profile_flags()); } LIBSESSION_C_API void user_profile_set_pro_badge(config_object* conf, bool enabled) { diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 203d8ddc6..afa441b62 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -103,8 +103,8 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP cpp.proof.rotating_pubkey.max_size()); result.proof.expiry_unix_ts_ms = session::epoch_ms(cpp.proof.expiry_unix_ts); std::memcpy(result.proof.sig.data, cpp.proof.sig.data(), cpp.proof.sig.max_size()); - result.msg_bitset.data = cpp.msg_bitset.data; - result.profile_bitset.data = cpp.profile_bitset.data; + result.msg_bitset = static_cast(cpp.msg_flags); + result.profile_bitset = static_cast(cpp.profile_flags); return result; } } // namespace @@ -156,30 +156,6 @@ b32 ProProof::hash() const { return result; } -void ProProfileBitset::set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - data |= (1ULL << static_cast(features)); -} - -void ProProfileBitset::unset(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - data &= ~(1ULL << static_cast(features)); -} - -bool ProProfileBitset::is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) const { - return data & (1ULL << static_cast(features)); -} - -void ProMessageBitset::set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - data |= (1ULL << static_cast(features)); -} - -void ProMessageBitset::unset(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - data &= ~(1ULL << static_cast(features)); -} - -bool ProMessageBitset::is_set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) const { - return data & (1ULL << static_cast(features)); -} - }; // namespace session namespace { @@ -193,7 +169,7 @@ session::ProFeaturesForMsg pro_features_check( if (result.codepoint_count > SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT) { if (result.codepoint_count <= SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT) { - result.bitset.set(SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT); + result.flags |= session::ProMessageFlags::CharLimit10k; } else { result.error = "Message exceeds the maximum character limit allowed"; result.status = session::ProFeaturesForMsgStatus::ExceedsCharacterLimit; @@ -536,8 +512,8 @@ static void parse_content_and_pro( "Parse decrypted message failed, pro metadata was malformed"); // Fill out the resulting proof structure, we have parsed successfully - pro.msg_bitset.data = pro_msg.msgbitset(); - pro.profile_bitset.data = pro_msg.profilebitset(); + pro.msg_flags = static_cast(pro_msg.msgbitset()); + pro.profile_flags = static_cast(pro_msg.profilebitset()); std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); std::memcpy( @@ -777,8 +753,8 @@ DecodedCommunityMessage decode_for_community( "Decoding community message failed, pro metadata was malformed"); // Fill out the resulting proof structure, we have parsed successfully - pro.msg_bitset.data = pro_msg.msgbitset(); - pro.profile_bitset.data = pro_msg.profilebitset(); + pro.msg_flags = static_cast(pro_msg.msgbitset()); + pro.profile_flags = static_cast(pro_msg.profilebitset()); std::memcpy( proof.gen_index_hash.data(), proto_proof.genindexhash().data(), @@ -844,45 +820,15 @@ static_assert((sizeof((session_protocol_pro_proof*)0)->gen_index_hash) == 32); static_assert(sizeof(std::declval().rotating_pubkey) == 32); static_assert(sizeof(std::declval().sig) == 64); -static_assert( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_COUNT <= - sizeof(((session_protocol_pro_profile_bitset*)0)->data) * 8 /*bits per byte*/, - "There are more feature flags than is available in the bitset, the bitset needs to be " - "upgraded into an array of bytes"); - -LIBSESSION_C_API bool session_protocol_pro_profile_bitset_is_set( - session_protocol_pro_profile_bitset value, SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - return value.data & (1ULL << features); -} - -LIBSESSION_C_API void session_protocol_pro_profile_bitset_set( - session_protocol_pro_profile_bitset* value, - SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - value->data |= (1ULL << features); -} - -LIBSESSION_C_API void session_protocol_pro_profile_bitset_unset( - session_protocol_pro_profile_bitset* value, - SESSION_PROTOCOL_PRO_PROFILE_FEATURES features) { - value->data &= ~(1ULL << features); -} - -LIBSESSION_C_API bool session_protocol_pro_message_bitset_is_set( - session_protocol_pro_message_bitset value, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - return value.data & (1ULL << features); -} - -LIBSESSION_C_API void session_protocol_pro_message_bitset_set( - session_protocol_pro_message_bitset* value, - SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - value->data |= (1ULL << features); -} - -LIBSESSION_C_API void session_protocol_pro_message_bitset_unset( - session_protocol_pro_message_bitset* value, - SESSION_PROTOCOL_PRO_MESSAGE_FEATURES features) { - value->data &= ~(1ULL << features); -} +// Session Pro feature flag bit constants exposed to the C API. The C++ enum classes +// (session::ProProfileFlags / session::ProMessageFlags) are the source of truth; these mirror their +// underlying values so C callers can OR/test them against a plain uint64_t bitset. +const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE = + static_cast(ProProfileFlags::ProBadge); +const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_ANIMATED_AVATAR = + static_cast(ProProfileFlags::AnimatedAvatar); +const uint64_t SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT = + static_cast(ProMessageFlags::CharLimit10k); LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { cbytes32 result = {}; @@ -963,7 +909,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf8( return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, - .bitset = {result_cpp.bitset.data}, + .bitset = static_cast(result_cpp.flags), .codepoint_count = result_cpp.codepoint_count, }; } @@ -976,7 +922,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, - .bitset = {result_cpp.bitset.data}, + .bitset = static_cast(result_cpp.flags), .codepoint_count = result_cpp.codepoint_count, }; } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index bb766abb5..2fcef523b 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -1103,50 +1103,50 @@ TEST_CASE("Contacts Pro Storage", "[config][contacts][pro]") { { auto c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000000"sv); - CHECK(c.profile_bitset.data == 0); + CHECK(c.profile_flags == ProProfileFlags::None); - c.profile_bitset.set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE); + c.profile_flags |= ProProfileFlags::ProBadge; contacts.set(c); CHECK(contacts.is_dirty()); c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000000"sv); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); + CHECK(contains(c.profile_flags, ProProfileFlags::ProBadge)); contacts.set(c); c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000000"sv); - CHECK_FALSE(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK_FALSE(contains(c.profile_flags, ProProfileFlags::AnimatedAvatar)); - c.profile_bitset.set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR); + c.profile_flags |= ProProfileFlags::AnimatedAvatar; contacts.set(c); c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000000"sv); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(c.profile_flags, ProProfileFlags::ProBadge)); + CHECK(contains(c.profile_flags, ProProfileFlags::AnimatedAvatar)); } // Set and unset the bitset from the profile on a new contact { auto c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000001"sv); - CHECK(c.profile_bitset.data == 0); + CHECK(c.profile_flags == ProProfileFlags::None); - c.profile_bitset.set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE); + c.profile_flags |= ProProfileFlags::ProBadge; contacts.set(c); c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000001"sv); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); + CHECK(contains(c.profile_flags, ProProfileFlags::ProBadge)); - c.profile_bitset.unset(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE); + c.profile_flags &= ~ProProfileFlags::ProBadge; contacts.set(c); c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000001"sv); - CHECK(!c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); + CHECK(!contains(c.profile_flags, ProProfileFlags::ProBadge)); } CHECK(contacts.needs_push()); @@ -1164,8 +1164,8 @@ TEST_CASE("Contacts Pro Storage", "[config][contacts][pro]") { { auto c = contacts.get_or_construct( "050000000000000000000000000000000000000000000000000000000000000000"sv); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - CHECK(c.profile_bitset.is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(c.profile_flags, ProProfileFlags::ProBadge)); + CHECK(contains(c.profile_flags, ProProfileFlags::AnimatedAvatar)); // This previously exposed a bug in set_erase_impl where the contact was being dirtied even // when nothing was actually being removed. contacts.set(c); diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 6eadb0d1b..fe22368b1 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -557,38 +557,33 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { session::config::UserProfile profile{seed, std::nullopt}; // Ensure the bitset is being updated correctly - CHECK(profile.get_profile_bitset().data == 0); + CHECK(profile.get_profile_flags() == ProProfileFlags::None); profile.set_pro_badge(true); - CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); + CHECK(contains(profile.get_profile_flags(), ProProfileFlags::ProBadge)); profile.set_pro_badge(false); - CHECK(profile.get_profile_bitset().data == 0); + CHECK(profile.get_profile_flags() == ProProfileFlags::None); profile.set_animated_avatar(true); - CHECK(profile.get_profile_bitset().is_set( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(profile.get_profile_flags(), ProProfileFlags::AnimatedAvatar)); profile.set_animated_avatar(false); - CHECK(profile.get_profile_bitset().data == 0); + CHECK(profile.get_profile_flags() == ProProfileFlags::None); profile.set_pro_badge(true); profile.set_animated_avatar(true); - CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - CHECK(profile.get_profile_bitset().is_set( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(profile.get_profile_flags(), ProProfileFlags::ProBadge)); + CHECK(contains(profile.get_profile_flags(), ProProfileFlags::AnimatedAvatar)); profile.set_animated_avatar(false); - CHECK(profile.get_profile_bitset().is_set(SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - CHECK_FALSE(profile.get_profile_bitset().is_set( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(profile.get_profile_flags(), ProProfileFlags::ProBadge)); + CHECK_FALSE(contains(profile.get_profile_flags(), ProProfileFlags::AnimatedAvatar)); { session::config::UserProfile profile2{seed, profile.dump()}; - CHECK(profile2.get_profile_bitset().is_set( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - CHECK_FALSE(profile2.get_profile_bitset().is_set( - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_ANIMATED_AVATAR)); + CHECK(contains(profile2.get_profile_flags(), ProProfileFlags::ProBadge)); + CHECK_FALSE(contains(profile2.get_profile_flags(), ProProfileFlags::AnimatedAvatar)); } // Ensure the pro config is being stored correctly diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 706001547..0b8c494df 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -30,8 +30,8 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se const ed25519::PrivKeySpan& pro_backend_privkey, std::chrono::sys_seconds content_unix_ts, std::chrono::sys_seconds pro_expiry_unix_ts, - session_protocol_pro_message_bitset msg_bitset, - session_protocol_pro_profile_bitset profile_bitset) { + uint64_t msg_bitset, + uint64_t profile_bitset) { SerialisedProtobufContentWithProForTesting result = {}; // Create protobuf `Content.dataMessage` @@ -53,8 +53,8 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se // Create protobuf `Content.proMessage` SessionProtos::ProMessage* pro = content.mutable_promessage(); - pro->set_profilebitset(profile_bitset.data); - pro->set_msgbitset(msg_bitset.data); + pro->set_profilebitset(profile_bitset); + pro->set_msgbitset(msg_bitset); // Create protobuf `Content.proMessage.proof` SessionProtos::ProProof* proto_proof = pro->mutable_proof(); @@ -102,7 +102,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS); - REQUIRE(pro_msg.bitset.data == 0); + REQUIRE(pro_msg.bitset == 0); REQUIRE(pro_msg.codepoint_count == msg.size()); } @@ -122,8 +122,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS); - REQUIRE(session_protocol_pro_message_bitset_is_set( - pro_msg.bitset, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT)); + REQUIRE((pro_msg.bitset & SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT)); REQUIRE(pro_msg.codepoint_count == msg.size()); } @@ -133,8 +132,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS); - REQUIRE(session_protocol_pro_message_bitset_is_set( - pro_msg.bitset, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT)); + REQUIRE((pro_msg.bitset & SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT)); REQUIRE(pro_msg.codepoint_count == msg.size()); } @@ -145,7 +143,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_EXCEEDS_CHARACTER_LIMIT); - REQUIRE(pro_msg.bitset.data == 0); + REQUIRE(pro_msg.bitset == 0); REQUIRE(pro_msg.codepoint_count == msg.size()); } } @@ -272,8 +270,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_NIL); // Pro was not attached - REQUIRE(decrypt_result.pro.msg_bitset.data == 0); - REQUIRE(decrypt_result.pro.profile_bitset.data == 0); + REQUIRE(decrypt_result.pro.msg_bitset == 0); + REQUIRE(decrypt_result.pro.profile_bitset == 0); REQUIRE(std::memcmp( decrypt_result_pro_hash.data, nil_hash.data(), @@ -398,8 +396,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); - REQUIRE(decrypt_result.pro.msg_bitset.data == 0); // No features requested - REQUIRE(decrypt_result.pro.profile_bitset.data == 0); // No features requested + REQUIRE(decrypt_result.pro.msg_bitset == 0); // No features requested + REQUIRE(decrypt_result.pro.profile_bitset == 0); // No features requested // Verify the content can be parsed w/ protobufs SessionProtos::Content decrypt_content = {}; @@ -417,12 +415,10 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(large_message.data(), large_message.size()); - REQUIRE(session_protocol_pro_message_bitset_is_set( - pro_msg.bitset, SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT)); + REQUIRE((pro_msg.bitset & SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT)); - session_protocol_pro_profile_bitset profile_bitset = {}; - session_protocol_pro_profile_bitset_set( - &profile_bitset, SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE); + uint64_t profile_bitset = 0; + profile_bitset |= SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE; SerialisedProtobufContentWithProForTesting protobuf_content_with_pro_and_features = build_protobuf_content_with_session_pro( @@ -474,12 +470,12 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); - REQUIRE(session_protocol_pro_profile_bitset_is_set( - decrypt_result.pro.profile_bitset, - SESSION_PROTOCOL_PRO_PROFILE_FEATURES_PRO_BADGE)); - REQUIRE(session_protocol_pro_message_bitset_is_set( - decrypt_result.pro.msg_bitset, - SESSION_PROTOCOL_PRO_MESSAGE_FEATURES_10K_CHARACTER_LIMIT)); + REQUIRE( + (decrypt_result.pro.profile_bitset & + SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE)); + REQUIRE( + (decrypt_result.pro.msg_bitset & + SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT)); // Verify the content can be parsed w/ protobufs SessionProtos::Content decrypt_content = {}; @@ -621,8 +617,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { REQUIRE(std::memcmp( hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == 0); - REQUIRE(decrypt_result.pro.msg_bitset.data == 0); // No features requested - REQUIRE(decrypt_result.pro.profile_bitset.data == 0); // No features requested + REQUIRE(decrypt_result.pro.msg_bitset == 0); // No features requested + REQUIRE(decrypt_result.pro.profile_bitset == 0); // No features requested // Verify the content can be parsed w/ protobufs SessionProtos::Content decrypt_content = {}; From bec7c25cc194f25815f627305e2e661c2873a1b6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 02:43:29 -0300 Subject: [PATCH 104/193] Reference wire spec instead of backend source permalinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pro hashing personalisation strings and proof digest routine pointed at pinned line numbers in the session-pro-backend GitHub source, which rot as that repo changes. Point them at the authoritative pro-wire-protocol.md sections (§2 proof, §3 signed requests) instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/session_protocol.hpp | 5 ++--- src/session_protocol.cpp | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index c9fb337cd..b92a00969 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -60,9 +60,8 @@ namespace session { enum ProProofVersion { ProProofVersion_v0 }; -/// Session Pro personalisation bytes for hashing. Must match -/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/backend.py#L21 -/// https://github.com/Doy-lee/session-pro-backend/blob/fca5e10c9c5014d394cf15934cd2af8e911607b9/server.py#L571 +/// Session Pro personalisation bytes for hashing; must match the personalisation strings in +/// pro-wire-protocol.md §2 (proof) and §3 (signed requests). inline constexpr auto GENERATE_PROOF_PERS = "ProGenerateProof"_b2b_pers; inline constexpr auto BUILD_PROOF_PERS = "ProProof________"_b2b_pers; inline constexpr auto ADD_PRO_PAYMENT_PERS = "ProAddPayment___"_b2b_pers; diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index b24e1bd13..56ce2a532 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -56,8 +56,7 @@ session::b32 proof_hash_internal( std::span rotating_pubkey, std::uint64_t expiry_ts) { - // This must match the hashing routine at - // https://github.com/Doy-lee/session-pro-backend/blob/9417e00adbff3bf608b7ae831f87045bdab06232/backend.py#L545-L558 + // This must match the Pro proof signed digest in pro-wire-protocol.md §2. return session::hash::blake2b_pers<32>( session::BUILD_PROOF_PERS, version, revocation_tag, rotating_pubkey, expiry_ts); } From 07b325e9555de708a7e620bd797f981d7fdf5c37 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 02:43:29 -0300 Subject: [PATCH 105/193] Redesign pro_backend C/C++ API around free functions Reshape the Session Pro request/response API for a clean, symmetric design ahead of launch: - Request builders are now free functions returning ProRequest{endpoint, body} (add_payment_request, pro_proof_request, payment_details_request, refund_request, revocations_request), each paired with a *_sigs/*_sig signer. The request C++ structs are deleted. - Response parsing is symmetric free functions (parse_add_payment, parse_pro_proof, parse_payment_details, parse_refund, parse_revocations) rather than X::parse methods; a shared fill_proof_response() backs the two proof responses. - Response base renamed ResponseHeader -> Response; the AddProPaymentOrGenerateProProofResponse mega-type becomes ProProofResponse with empty AddProPaymentResponse/GenerateProProofResponse derivations. - Endpoint strings live in a single master table; C SESSION_PRO_BACKEND_*_ENDPOINT symbols and C++ ProRequest.endpoint both view it. - The wire version for each request type is library-owned (a per-type constant), not a caller parameter; the version param and _request_build_to_json helpers are removed. - Key derivation happens once via the ed25519 PrivKeySpan rather than per hash. - Re-add provider support/management URLs (URL-only, no English names) keyed by provider code so clients don't each duplicate them: C++ provider_urls() returns std::optional; C session_pro_backend_get_provider_urls() returns all-NULL fields for a provider with no URLs. The C API (_request_build_sigs/sig, _request_to_json, _response_parse) is preserved for the iOS client. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 149 +++---- include/session/pro_backend.hpp | 579 ++++++++----------------- src/pro_backend.cpp | 744 +++++++++++++++----------------- tests/test_logging.cpp | 3 +- tests/test_pro_backend.cpp | 255 +++-------- 5 files changed, 652 insertions(+), 1078 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 8a0cbb390..0f7468dcb 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -27,6 +27,40 @@ static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY[] = "goo static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE[] = "app_store"; static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF[] = "rangeproof"; +/// Endpoint path (relative to the backend base URL) that each request type is POSTed to. libsession +/// owns the body<->route pairing, so reference these rather than hardcoding the path client-side. +/// The C++ `*_request()` helpers return the matching endpoint alongside the body; C callers read +/// the constant beside each request's build function. Each is a pointer to the single master string +/// owned in the C++ implementation (defined once, shared by the C and C++ sides). +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT; +LIBSESSION_EXPORT extern const char* const + SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT; + +/// Per-provider support/management URLs, keyed by provider code. These are identical for every user +/// (not translation data), so libsession owns them as the single source of truth rather than each +/// client duplicating them; the human-readable provider/store names are translation data and remain +/// the client's job. An unknown provider code (or one with no applicable URLs, e.g. rangeproof) +/// yields all-NULL fields. Otherwise each is a static, null-terminated C string. +typedef struct session_pro_backend_provider_urls session_pro_backend_provider_urls; +struct session_pro_backend_provider_urls { + const char* refund_platform_url; /// Native store refund flow + const char* refund_support_url; /// Session support page for requesting a refund + const char* refund_status_url; /// Where a user checks refund status + const char* update_subscription_url; /// Manage/update the subscription + const char* cancel_subscription_url; /// Cancel the subscription +}; + +/// API: session_pro_backend/get_provider_urls +/// +/// Returns the support/management URLs for `provider_code` (a +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* value). A provider with no applicable URLs (unknown +/// code, or e.g. rangeproof) returns a struct with all fields NULL. +LIBSESSION_EXPORT session_pro_backend_provider_urls +session_pro_backend_get_provider_urls(const char* provider_code) NON_NULL_ARG(1); + /// Must match: /// https://github.com/Doy-lee/session-pro-backend/blob/f4e2c84794470e7932ba1a1968fdb49117bb5870/backend.py#L18 typedef enum SESSION_PRO_BACKEND_PAYMENT_STATUS { @@ -119,7 +153,6 @@ struct session_pro_backend_add_pro_payment_user_transaction { typedef struct session_pro_backend_add_pro_payment_request session_pro_backend_add_pro_payment_request; struct session_pro_backend_add_pro_payment_request { - uint8_t version; cbytes32 master_pkey; cbytes32 rotating_pkey; session_pro_backend_add_pro_payment_user_transaction payment_tx; @@ -130,7 +163,6 @@ struct session_pro_backend_add_pro_payment_request { typedef struct session_pro_backend_generate_pro_proof_request session_pro_backend_generate_pro_proof_request; struct session_pro_backend_generate_pro_proof_request { - uint8_t version; cbytes32 master_pkey; cbytes32 rotating_pkey; int64_t ts; @@ -138,9 +170,8 @@ struct session_pro_backend_generate_pro_proof_request { cbytes64 rotating_sig; }; -typedef struct session_pro_backend_add_pro_payment_or_generate_pro_proof_response - session_pro_backend_add_pro_payment_or_generate_pro_proof_response; -struct session_pro_backend_add_pro_payment_or_generate_pro_proof_response { +typedef struct session_pro_backend_pro_proof_response session_pro_backend_pro_proof_response; +struct session_pro_backend_pro_proof_response { session_pro_backend_response_header header; session_protocol_pro_proof proof; }; @@ -148,7 +179,6 @@ struct session_pro_backend_add_pro_payment_or_generate_pro_proof_response { typedef struct session_pro_backend_get_pro_revocations_request session_pro_backend_get_pro_revocations_request; struct session_pro_backend_get_pro_revocations_request { - uint8_t version; int64_t ticket; }; @@ -174,7 +204,6 @@ struct session_pro_backend_get_pro_revocations_response { typedef struct session_pro_backend_get_pro_details_request session_pro_backend_get_pro_details_request; struct session_pro_backend_get_pro_details_request { - uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; int64_t ts; @@ -225,7 +254,6 @@ struct session_pro_backend_get_pro_details_response { typedef struct session_pro_backend_set_payment_refund_requested_request session_pro_backend_set_payment_refund_requested_request; struct session_pro_backend_set_payment_refund_requested_request { - uint8_t version; cbytes32 master_pkey; cbytes64 master_sig; int64_t ts; @@ -248,13 +276,12 @@ struct session_pro_backend_set_payment_refund_requested_response { /// incorrectly sized. Using 64-byte libsodium keys is more efficient. /// /// Inputs: -/// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. /// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). /// - `rotating_privkey_len` -- Length of rotating_privkey. /// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from -/// (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// (use a SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constant) /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload @@ -268,32 +295,13 @@ struct session_pro_backend_set_payment_refund_requested_response { LIBSESSION_EXPORT session_pro_backend_master_rotating_signatures session_pro_backend_add_pro_payment_request_build_sigs( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 4, 6, 7); - -/// API: session_pro_backend/add_pro_payment_request_build_to_json -/// -/// Builds the JSON for a `add_pro_payment_request`. This function is the same as filling in the -/// struct and calling the corresponding `to_json` function. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. -/// -/// See: session_pro_backend_add_pro_payment_request_build_sigs -LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 4, 6, 7); + size_t payment_tx_payment_id_len) NON_NULL_ARG(1, 3, 5, 6); /// API: session_pro_backend/generate_pro_proof_request_build_sigs /// @@ -302,7 +310,6 @@ session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to /// Using 64-byte libsodium keys is more efficient. /// /// Inputs: -/// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. /// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). @@ -318,28 +325,11 @@ session_pro_backend_to_json session_pro_backend_add_pro_payment_request_build_to LIBSESSION_EXPORT session_pro_backend_master_rotating_signatures session_pro_backend_generate_pro_proof_request_build_sigs( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - int64_t ts) NON_NULL_ARG(2, 4); - -/// API: session_pro_backend/generate_pro_proof_request_build_to_json -/// -/// Builds the JSON for a `generate_pro_proof_request`. This function is the same as filling in the -/// struct and calling the corresponding `to_json` function. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. -/// -/// See: `session_pro_backend_generate_pro_proof_request_build_sigs` -LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build_to_json( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - int64_t ts) NON_NULL_ARG(2, 4); + int64_t ts) NON_NULL_ARG(1, 3); /// API: session_pro_backend/get_pro_details_request_build_sig /// @@ -347,7 +337,6 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build /// 64-byte libsodium format) are incorrectly sized. Using 64-byte libsodium keys is more efficient. /// /// Inputs: -/// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. /// - `ts` -- Unix timestamp for the request. @@ -360,26 +349,8 @@ session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build /// - `sig` -- The generated signature LIBSESSION_EXPORT session_pro_backend_signature session_pro_backend_get_pro_details_request_build_sig( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - int64_t ts, - uint32_t count) NON_NULL_ARG(2); - -/// API: session_pro_backend/get_pro_details_request_build_to_json -/// -/// Builds the JSON for a `get_pro_details_request`. This function is the same as filling in the -/// struct and calling the corresponding `to_json` function. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. -/// -/// See: `session_pro_backend_get_pro_details_request_build_sig` -LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_get_pro_details_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - int64_t ts, - uint32_t count) NON_NULL_ARG(2); + const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, uint32_t count) + NON_NULL_ARG(1); /// API: session_pro_backend/add_pro_payment_request_to_json /// @@ -419,18 +390,17 @@ LIBSESSION_EXPORT session_pro_backend_to_json session_pro_backend_get_pro_details_request_to_json( const session_pro_backend_get_pro_details_request* request); -/// API: session_pro_backend/add_pro_payment_or_generate_pro_proof_response_parse +/// API: session_pro_backend/pro_proof_response_parse /// -/// Parses a JSON string into an `add_pro_payment_or_generate_pro_proof_response` struct. +/// Parses a JSON string into an `pro_proof_response` struct. /// The caller must free the response using -/// `session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free`. +/// `session_pro_backend_pro_proof_response_free`. /// /// Inputs: /// - `json` -- JSON string to parse. /// - `json_len` -- Length of the JSON string. LIBSESSION_EXPORT -session_pro_backend_add_pro_payment_or_generate_pro_proof_response -session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( +session_pro_backend_pro_proof_response session_pro_backend_pro_proof_response_parse( const char* json, size_t json_len); /// API: session_pro_backend/get_pro_revocations_response_parse @@ -464,14 +434,13 @@ session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details /// incorrectly sized. Using 64-byte libsodium keys is more efficient. /// /// Inputs: -/// - `request_version` -- Version of the request. /// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). /// - `master_privkey_len` -- Length of master_privkey. /// - `ts` -- Unix timestamp for the request /// - `refund_requested_ts` -- Unix timestamp to set as the timestamp that a refund was /// requested on this payment /// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from -/// (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// (use a SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constant) /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See /// `AddProPaymentUserTransaction` /// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload @@ -483,32 +452,13 @@ session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details /// - `sig` -- The generated signature LIBSESSION_EXPORT session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - int64_t ts, - int64_t refund_requested_ts, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 6, 7); - -/// API: session_pro_backend/set_payment_refund_requested_request_build_to_json -/// -/// Builds the JSON for a `set_payment_refund_requested_request`. This function is the same as -/// filling in the struct and calling the corresponding `to_json` function. The caller must free the -/// returned string using `session_pro_backend_to_json_free`. -/// -/// See: session_pro_backend_set_payment_refund_requested_request_build_sigs -LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_request_build_to_json( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, const char* payment_tx_provider_code, const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(2, 6, 7); + size_t payment_tx_payment_id_len) NON_NULL_ARG(1, 5, 6); /// API: session_pro_backend/set_payment_refund_requested_request_to_json /// @@ -536,12 +486,11 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json LIBSESSION_EXPORT void session_pro_backend_to_json_free(session_pro_backend_to_json* to_json); -/// API: session_pro_backend/add_pro_payment_or_generate_pro_proof_response_free +/// API: session_pro_backend/pro_proof_response_free /// /// Frees the response LIBSESSION_EXPORT -void session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( - session_pro_backend_add_pro_payment_or_generate_pro_proof_response* response); +void session_pro_backend_pro_proof_response_free(session_pro_backend_pro_proof_response* response); /// API: session_pro_backend/get_pro_revocations_response_free /// diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 60f9a91cd..b949b65a5 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -18,16 +19,15 @@ /// /// The high level summary of the functionality in this file. Clients can: /// -/// 1. Build a request with `AddProPaymentRequest::build_to_json` from a Session Pro payment and -/// submit it to the backend to register the specified Ed25519 keys for Session Pro. +/// 1. Build a request with `add_payment_request(...)` from a Session Pro payment and submit its +/// `{endpoint, body}` to the backend to register the specified Ed25519 keys for Session Pro. /// -/// Server responds JSON to be parsed with `AddProPaymentOrGenerateProProofResponse::parse`. -/// Clients should validate the response and update their `UserProfile` by constructing a -/// `ProConfig` with the `proof` from the response and filling in the relevant rotating private -/// key that the proof was authorised for. +/// Parse the server's reply with `parse_add_payment(...)`. Clients should validate the response +/// and update their `UserProfile` by constructing a `ProConfig` with the `proof` from the +/// response and filling in the relevant rotating private key that the proof was authorised for. /// /// The server will only respond successfully if it can also independently verify the purchase -/// otherwise an error is returned and can be read from the `ResponseHeader` after parsing the +/// otherwise an error is returned and can be read from the `Response` after parsing the /// raw response. /// /// 2. Attach the `ProProof` constructed from (1) into their messages. Libsession has helper @@ -42,21 +42,18 @@ /// to enable pro features for that message. /// /// 3. Periodically poll the global revocation list which overrides the validity of current -/// circulating proofs. This is done by constructing the request via -/// `GetProRevocationsRequest::to_json` and sending it to the backend. +/// circulating proofs. This is done by building the request via `revocations_request(...)` and +/// sending it to the backend. /// -/// Server responds JSON to be parsed with `GetProRevocationResponse::parse` which contains the -/// list that clients should cache. Any incoming messages with a Pro proof that is in the list of -/// revoked proofs will not be entitled to Pro features. +/// Parse the reply with `parse_revocations(...)`, which contains the list that clients should +/// cache. Any incoming messages with a Pro proof that is in the list of revoked proofs will not +/// be entitled to Pro features. /// /// 4. Query the status (and optionally payment history) of a user's Session Pro Master Ed25519 key -/// has registered by building a `GetProDetailsRequest::build_to_json` query and submitting it. +/// by building a `payment_details_request(...)` query and submitting it. /// -/// Server responds JSON to be parsed with `GetProDetailsResponse::parse` which they can use to -/// populate their client's payment history. -/// -/// 5. Get a list of per-payment provider URLs, such as links to the support page for refunds and -/// subscription via the `PAYMENT_PROVIDER_METADATA` global variable defined in the C header. +/// Parse the reply with `parse_payment_details(...)`, which they can use to populate their +/// client's payment history. /// /// See the unit tests for examples of using the APIs mentioned. @@ -88,7 +85,7 @@ enum struct AddProPaymentResponseStatus { UnknownPayment = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT, }; -struct ResponseHeader { +struct Response { /// Status code for the response, maps to a specific enum for some requests otherwise it uses 0 /// for success, other values indicate errors. For the following responses, the status code maps /// to @@ -109,213 +106,114 @@ struct MasterRotatingSignatures { b64 rotating_sig; }; -struct AddProPaymentUserTransaction { - /// Provider code string (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). Opaque slug; unknown - /// values are accepted and passed through verbatim. - std::string provider_code; - - /// Opaque payment identifier from the provider's purchase flow. Multi-part providers fold their - /// parts into this one string per a backend-defined composite (e.g. Google "token|order_id"); - /// libsession treats it as opaque bytes hashed verbatim. - std::string payment_id; +/// Per-provider support/management URLs. These are identical for every user (not translation data), +/// so libsession owns them as the single source of truth rather than each client duplicating them; +/// the human-readable provider/store *names* are translation data and remain the client's job. +/// Returned by `provider_urls()`. The views point at static, null-terminated storage. +struct ProviderUrls { + std::string_view refund_platform_url; ///< Native store refund flow + std::string_view refund_support_url; ///< Session support page for requesting a refund + std::string_view refund_status_url; ///< Where a user checks refund status + std::string_view update_subscription_url; ///< Manage/update the subscription + std::string_view cancel_subscription_url; ///< Cancel the subscription }; -/// Register a new Session Pro proof to the backend. The payment is registered under the -/// `master_pkey` and authorises the `rotating_pkey` to use the proof. In practice this means that -/// the caller will receive a Session Pro Proof that can be attached to messages that have to be -/// signed by the `rotating_pkey` for other clients to entitle that message to Pro privileges. -/// -/// The attached signatures must sign over the contents of the request which can be generated by the -/// helper function `build_sigs`. -struct AddProPaymentRequest { - /// Request version. The latest accepted version is 0 - std::uint8_t version; - - /// 32-byte Ed25519 Session Pro master public key derived from the Session account seed to - /// register a Session Pro payment under. - b32 master_pkey; - - /// 32-byte Ed25519 Session Pro rotating public key to authorise to use the generated Session - /// Pro proof - b32 rotating_pkey; - - /// Transaction containing the payment details to register on the Session Pro backend - AddProPaymentUserTransaction payment_tx; - - /// 64-byte signature proving knowledge of the master key's secret component - b64 master_sig; - - /// 64-byte signature proving knowledge of the rotating key's secret component - b64 rotating_sig; - - /// API: pro/AddProPaymentRequest::to_json - /// - /// Serializes the request to a JSON string. - /// - /// Outputs: - /// - `std::string` - JSON representation of the request. - std::string to_json() const; - - /// API: pro/AddProPaymentRequest::build_sigs - /// - /// Builds the master and rotating signatures using the provided private keys and payment token - /// hash. Throws if the keys (32-byte or 64-byte libsodium format) are incorrectly sized. - /// Using 64-byte libsodium keys is more efficient. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a hash for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `payment_tx_provider_code` -- Provider code string the payment is coming from (see - /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) - /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See - /// `AddProPaymentUserTransaction` - /// - /// Outputs: - /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. - static MasterRotatingSignatures build_sigs( - std::uint8_t request_version, - const ed25519::PrivKeySpan& master_privkey, - const ed25519::PrivKeySpan& rotating_privkey, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id); - - /// API: pro/AddProPaymentRequest::build_to_json - /// - /// Builds a AddProPaymentRequest and serialize it to JSON. This function is the same as filling - /// the struct fields and calling `to_json`. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a hash for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `payment_tx_provider_code` -- Provider code string the payment is coming from (see - /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) - /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See - /// `AddProPaymentUserTransaction` - /// - /// Outputs: - /// - `std::string` -- Request serialised to JSON - static std::string build_to_json( - std::uint8_t request_version, - const ed25519::PrivKeySpan& master_privkey, - const ed25519::PrivKeySpan& rotating_privkey, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id); +/// Look up the support/management URLs for a provider code (see +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). Returns std::nullopt for a provider with no +/// applicable URLs (an unknown code, or e.g. rangeproof) — so callers test the optional rather than +/// hunting for empty members. +std::optional provider_urls(std::string_view provider_code); + +/// Route + body pair for a request to be POSTed to the Session Pro backend, returned by the +/// `*_request()` helpers below. +struct ProRequest { + /// Endpoint path relative to the backend base URL, e.g. "add_pro_payment". As returned from a + /// `*_request()` function this points at a static, null-terminated string, so using + /// `endpoint.data()` as a C string is valid. + std::string_view endpoint; + + /// The JSON request body to POST to `endpoint`. + std::string body; }; -/// The generated proof from the Session Pro backend that has been parsed from JSON. This structure -/// is the raw parse result that can then be converted into the config::ProProof or equivalent -/// structure. -struct AddProPaymentOrGenerateProProofResponse : public ResponseHeader { +/// Register a new Session Pro payment with the backend (endpoint `add_pro_payment`). The payment is +/// registered under the master key and authorises the rotating key to use the resulting proof, so +/// that proof can be attached to messages signed by the rotating key to entitle them to Pro. +/// +/// `add_payment_sigs` computes just the master+rotating signatures over the request; +/// `add_payment_request` builds the whole request (computing the signatures internally) and returns +/// the endpoint + JSON body. Both throw if a key is not a 32-byte Ed25519 seed or 64-byte libsodium +/// key. +/// +/// Inputs: +/// - `master_privkey` / `rotating_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium private key +/// - `provider_code` -- provider code string the payment is coming from (see +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// - `payment_id` -- opaque payment identifier from the provider (multi-part providers fold their +/// parts into this one value per a backend-defined composite; hashed verbatim) +MasterRotatingSignatures add_payment_sigs( + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, + std::string_view provider_code, + std::span payment_id); + +ProRequest add_payment_request( + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, + std::string_view provider_code, + std::span payment_id); + +/// Common base for the responses that carry a freshly-issued Session Pro proof: both add-payment +/// and generate-proof reply with exactly a proof. `AddProPaymentResponse` and +/// `GenerateProProofResponse` are distinct, currently empty types (each parsed by its own free +/// function below) that share every field today but can diverge independently later. `proof` is the +/// raw parse result, convertible to a config::ProProof. +struct ProProofResponse : Response { ProProof proof; - - /// API: pro/AddProPaymentOrGenerateProProofResponse::parse - /// - /// Parses a JSON string into the response struct. - /// - /// Inputs: - /// - `json` -- JSON string to parse. - /// - /// Outputs: - /// - The response struct with `status` set to an error state on failure. Errors are stored in - /// `errors` - static AddProPaymentOrGenerateProProofResponse parse(std::string_view json); }; -/// Request a new Session Pro proof from the backend. The specified `master_pkey` must have -/// previously already registered a payment to the backend that is still active and hence entitled -/// to Session Pro features. This endpoint can then be used to pair a new Ed25519 key to be -/// authorised to use a the Session Pro proof. -struct GenerateProProofRequest { - /// Request version. The latest accepted version is 0 - std::uint8_t version; - - /// 32-byte Ed25519 Session Pro master public key to generate a Session Pro proof from. This key - /// must have had a prior, and still active payment registered under it for a new proof to be - /// generated successfully. - b32 master_pkey; +/// Response to `add_payment_request` (endpoint `add_pro_payment`). +struct AddProPaymentResponse : ProProofResponse {}; - /// 32-byte Ed25519 Session Pro rotating public key authorized to use the generated proof - b32 rotating_pkey; +/// Response to `pro_proof_request` (endpoint `generate_pro_proof`). +struct GenerateProProofResponse : ProProofResponse {}; - /// Unix timestamp of the request - std::chrono::sys_seconds unix_ts; +/// Parse the reply to an add-payment / generate-proof request. On failure `status` is set to an +/// error state and `errors` is populated; on success `proof` holds the issued proof. +AddProPaymentResponse parse_add_payment(std::string_view json); +GenerateProProofResponse parse_pro_proof(std::string_view json); - /// 64-byte signature proving knowledge of the master key's secret component - b64 master_sig; - - /// 64-byte signature proving knowledge of the rotating key's secret component - b64 rotating_sig; - - /// API: pro/GenerateProProofRequest::build_sigs - /// - /// Builds master and rotating signatures using the provided private keys and timestamp. - /// Throws if the keys (32-byte or 64-byte libsodium format) are incorrectly sized. - /// Using 64-byte libsodium keys is more efficient. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a hash for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - /// Outputs: - /// - `MasterRotatingSignatures` - Struct containing the 64-byte master and rotating signatures. - static MasterRotatingSignatures build_sigs( - std::uint8_t request_version, - const ed25519::PrivKeySpan& master_privkey, - const ed25519::PrivKeySpan& rotating_privkey, - std::chrono::sys_seconds unix_ts); - - /// API: pro/GenerateProProofRequest::build_to_json - /// - /// Builds a GenerateProProofRequest and serialize it to JSON. This function is the same as - /// filling the struct fields and calling `to_json`. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a request for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `rotating_privkey` -- 64-byte libsodium style or 32 byte Ed25519 rotating private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - /// Outputs: - /// - `std::string` -- Request serialised to JSON - static std::string build_to_json( - std::uint8_t request_version, - const ed25519::PrivKeySpan& master_privkey, - const ed25519::PrivKeySpan& rotating_privkey, - std::chrono::sys_seconds unix_ts); - - /// API: pro/GenerateProProofRequest::to_json - /// - /// Serializes the request to a JSON string. - /// - /// Outputs: - /// - `std::string` - JSON representation of the request. - std::string to_json() const; -}; - -/// Retrieve the current list of revocations for currently active Session Pro proofs (because of -/// refunds for example). The caller retains each returned item for the response's `retain_for` +/// Request a new Session Pro proof from the backend (endpoint `generate_pro_proof`). The master key +/// must already have a prior, still-active payment registered; this pairs a (new) rotating key to a +/// freshly-issued proof. +/// +/// `pro_proof_sigs` computes the master+rotating signatures; `pro_proof_request` builds the whole +/// request (signing internally) and returns the endpoint + JSON body. Both throw on an +/// incorrectly-sized key. +/// +/// Inputs: +/// - `master_privkey` / `rotating_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium private key +/// - `unix_ts` -- Unix timestamp for the request +MasterRotatingSignatures pro_proof_sigs( + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, + std::chrono::sys_seconds unix_ts); + +ProRequest pro_proof_request( + const ed25519::PrivKeySpan& master_privkey, + const ed25519::PrivKeySpan& rotating_privkey, + std::chrono::sys_seconds unix_ts); + +/// Build a request for the current Session Pro revocation list (endpoint `get_pro_revocations`). +/// This request is unsigned. The caller retains each returned item for the response's `retain_for` /// window after first seeing it (memory-only aging) and polls again after `retry_in`. -struct GetProRevocationsRequest { - /// Request version. The latest accepted version is 0 - std::uint8_t version; - - /// 64-bit monotonic integer for the caller's revocation list iteration. Set to 0 if unknown; - /// otherwise, use the latest known `ticket` from a prior `GetProRevocationResponse` to allow - /// the Session Pro Backend to omit the revocation list if it has not changed. - std::int64_t ticket; - - /// API: pro/GenerateProProofRequest::to_json - /// - /// Serializes the request to a JSON string. - /// - /// Outputs: - /// - `std::string` - JSON representation of the request. - std::string to_json() const; -}; +/// +/// Inputs: +/// - `ticket` -- 64-bit monotonic revocation-list iteration. Pass 0 if unknown; otherwise the +/// latest +/// known `ticket` from a prior `GetProRevocationsResponse`, so the backend may omit an unchanged +/// list. +ProRequest revocations_request(std::int64_t ticket); struct ProRevocationItem { /// 32-byte opaque revocation tag identifying a proof @@ -325,7 +223,7 @@ struct ProRevocationItem { std::chrono::sys_seconds effective_unix_ts; }; -struct GetProRevocationsResponse : public ResponseHeader { +struct GetProRevocationsResponse : Response { /// 64-bit monotonic integer for the latest revocation list iteration. /// Update the caller's ticket to this value for subsequent requests. std::int64_t ticket; @@ -340,83 +238,30 @@ struct GetProRevocationsResponse : public ResponseHeader { /// List of revoked Session Pro proofs std::vector items; - - /// API: pro/GetProRevocationsResponse::parse - /// - /// Parses a JSON string into the response struct. - /// - /// Inputs: - /// - `json` -- JSON string to parse. - /// - /// Outputs: - /// - The response struct with `status` set to an error state on failure. Errors are stored in - /// `errors` - static GetProRevocationsResponse parse(std::string_view json); }; -struct GetProDetailsRequest { - /// Request version for the API - std::uint8_t version; - - /// 32-byte Ed25519 master public key to retrieve payments for - b32 master_pkey; - - /// 64-byte signature proving knowledge of the master public key's secret component - b64 master_sig; - - /// Unix timestamp of the request - std::chrono::sys_seconds unix_ts; - - /// Max amount of historical payments to request from the backend - uint32_t count; +/// Parse the reply to a `revocations_request`. On failure `status` is set to an error state and +/// `errors` is populated. +GetProRevocationsResponse parse_revocations(std::string_view json); - /// API: pro/AddProPaymentRequest::build_sigs - /// - /// Builds the master and rotating signatures using the provided private keys and payment token - /// hash. Throws if the keys (32-byte or 64-byte libsodium format) or 32-byte payment token hash - /// are passed with an incorrect size. Using 64-byte libsodium keys is more efficient. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a hash for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - `count` -- Amount of historical payments to request - /// - /// Outputs: - /// - `b64` - the 64-byte signature - static b64 build_sig( - uint8_t version, - const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_seconds unix_ts, - uint32_t count); - - /// API: pro/GetProDetailsRequest::build_to_json - /// - /// Builds a GetProDetailsRequest and serialize it to JSON. This function is the same as filling - /// the struct fields and calling `to_json`. - /// - /// Inputs: - /// - `version` -- Version of the request to build a request from - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - `count` -- Amount of historical payments to request - /// - /// Outputs: - /// - `std::string` -- Request serialised to JSON - static std::string build_to_json( - std::uint8_t version, - const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_seconds unix_ts, - uint32_t count); - - /// API: pro/GenerateProProofRequest::to_json - /// - /// Serializes the request to a JSON string. - /// - /// Outputs: - /// - `std::string` - JSON representation of the request. - std::string to_json() const; -}; +/// Query a master key's Session Pro payment/subscription details and history (endpoint +/// `get_pro_details`). `payment_details_sig` computes the master signature; +/// `payment_details_request` builds the whole request (signing internally) and returns the endpoint +/// + JSON body. Both throw on an incorrectly-sized key. +/// +/// Inputs: +/// - `master_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium master private key +/// - `unix_ts` -- Unix timestamp for the request +/// - `count` -- maximum number of historical payments to request +b64 payment_details_sig( + const ed25519::PrivKeySpan& master_privkey, + std::chrono::sys_seconds unix_ts, + uint32_t count); + +ProRequest payment_details_request( + const ed25519::PrivKeySpan& master_privkey, + std::chrono::sys_seconds unix_ts, + uint32_t count); struct ProPaymentItem { /// Describes the current status of the consumption of the payment for Session Pro entitlement @@ -427,12 +272,12 @@ struct ProPaymentItem { /// book-keeping purposes. SESSION_PRO_BACKEND_PAYMENT_STATUS status; - /// Billing-period code that was purchased (e.g. "1m"/"3m"/"1y"); opaque string, may be - /// free-form for non-period plans + /// Billing-period code that was purchased (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for + /// non-period plans. The client maps/parses it for display. std::string plan; - /// Provider code this particular payment came from (e.g. "google_play"); opaque string, an - /// unknown value passes through as-is + /// Provider code this payment came from (e.g. "google_play"); opaque -- an unknown value passes + /// through as-is for the client to handle. std::string payment_provider; /// Flag indicating whether or not this payment will automatically bill itself at the end of the @@ -466,12 +311,12 @@ struct ProPaymentItem { std::chrono::sys_seconds refund_requested_unix_ts; /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their - /// parts in per the backend-defined composite -- libsession does not interpret it). This - /// information should be considered confidential and stored appropriately. + /// parts in per the backend-defined composite -- libsession does not interpret it). + /// Confidential; store appropriately. std::string payment_id; }; -struct GetProDetailsResponse : public ResponseHeader { +struct GetProDetailsResponse : Response { /// List of payment items for the master public key std::vector items; @@ -527,120 +372,48 @@ struct GetProDetailsResponse : public ResponseHeader { /// Total number of payments known by the backend for the user. This may be greater than the /// length of items if the request, requested less than the number of payments the user has. uint32_t payments_total; - - /// API: pro/GetProDetailsResponse::parse - /// - /// Parses a JSON string into the response struct. - /// - /// Inputs: - /// - `json` -- JSON string to parse. - /// - /// Outputs: - /// - The response struct with `status` set to an error state on failure. Errors are stored in - /// `errors` - static GetProDetailsResponse parse(std::string_view json); }; -struct SetPaymentRefundRequestedRequest { - /// Request version for the API - std::uint8_t version; - - /// 32-byte Ed25519 master public key to retrieve payments for - b32 master_pkey; +/// Parse the reply to a `payment_details_request`. On failure `status` is set to an error state and +/// `errors` is populated. +GetProDetailsResponse parse_payment_details(std::string_view json); - /// 64-byte signature proving knowledge of the master public key's secret component - b64 master_sig; - - /// Unix timestamp of the current time - std::chrono::sys_seconds unix_ts; - - /// Unix timestamp to set as the timestamp that a refund was requested on this payment. - std::chrono::sys_seconds refund_requested_unix_ts; - - /// Payment details to set the refund request on - AddProPaymentUserTransaction payment_tx; - - /// API: pro/SetPaymentRefundRequested::build_sigs - /// - /// Builds the signature that must be included in the request to authenticate and permit - /// updating the refund requested status of a payment. Throws if the keys (32-byte or - /// 64-byte libsodium format) are incorrectly sized. Using 64-byte libsodium keys is more - /// efficient. - /// - /// Inputs: - /// - `request_version` -- Version of the request to build a hash for - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - `refund_requested_unix_ts` -- Unix timestamp to set as the timestamp that a refund was - /// requested on this payment - /// - `payment_tx_provider_code` -- Provider code string the payment to set a refund request on - /// is coming from (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) - /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See - /// `AddProPaymentUserTransaction` - /// - /// Outputs: - /// - `b64` - the 64-byte signature - static b64 build_sig( - uint8_t version, - const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id); - - /// API: pro/SetPaymentRefundRequested::build_to_json - /// - /// Builds a SetPaymentRefundRequested and serialize it to JSON. This function is the same as - /// filling the struct fields and calling `to_json`. - /// - /// Inputs: - /// - `version` -- Version of the request to build a request from - /// - `master_privkey` -- 64-byte libsodium style or 32 byte Ed25519 master private key - /// - `unix_ts` -- Unix timestamp for the request. - /// - `refund_requested_unix_ts` -- Unix timestamp to set as the timestamp that a refund was - /// requested on this payment - /// - `payment_tx_provider_code` -- Provider code string the payment to set a refund request on - /// is coming from (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) - /// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See - /// `AddProPaymentUserTransaction` - /// - /// Outputs: - /// - `std::string` -- Request serialised to JSON - static std::string build_to_json( - std::uint8_t version, - const ed25519::PrivKeySpan& master_privkey, - std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id); - - /// API: pro/SetPaymentRefundRequested::to_json - /// - /// Serializes the request to a JSON string. - /// - /// Outputs: - /// - `std::string` - JSON representation of the request. - std::string to_json() const; -}; - -struct SetPaymentRefundRequestedResponse : public ResponseHeader { +/// Record a refund request against an existing Session Pro payment (endpoint +/// `set_payment_refund_requested`). `refund_sig` computes the master signature; `refund_request` +/// builds the whole request (signing internally) and returns the endpoint + JSON body. Both throw +/// on an incorrectly-sized key. +/// +/// Inputs: +/// - `master_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium master private key +/// - `unix_ts` -- Unix timestamp for the request +/// - `refund_requested_unix_ts` -- timestamp to record as when the refund was requested +/// - `provider_code` -- provider code string the payment is from (see +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) +/// - `payment_id` -- opaque payment identifier from the provider (hashed verbatim) +b64 refund_sig( + const ed25519::PrivKeySpan& master_privkey, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, + std::string_view provider_code, + std::span payment_id); + +ProRequest refund_request( + const ed25519::PrivKeySpan& master_privkey, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, + std::string_view provider_code, + std::span payment_id); + +struct SetPaymentRefundRequestedResponse : Response { /// Version from the request std::uint8_t version; /// True if a payment was found matching the given payment information and that the refund /// request unix timestamp was set bool updated; - - /// API: pro/SetPaymentRefundRequestedResponse::parse - /// - /// Parses a JSON string into the response struct. - /// - /// Inputs: - /// - `json` -- JSON string to parse. - /// - /// Outputs: - /// - The response struct with `status` set to an error state on failure. Errors are stored in - /// `errors` - static SetPaymentRefundRequestedResponse parse(std::string_view json); }; + +/// Parse the reply to a `refund_request`. On failure `status` is set to an error state and `errors` +/// is populated. +SetPaymentRefundRequestedResponse parse_refund(std::string_view json); } // namespace session::pro_backend diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 91b46a552..95f107392 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -114,158 +115,255 @@ bool json_require_fixed_bytes_from_hex( }; // namespace namespace session::pro_backend { -std::string AddProPaymentRequest::to_json() const { - nlohmann::json j; - j["version"] = version; - j["master_pkey"] = oxenc::to_hex(master_pkey); - j["rotating_pkey"] = oxenc::to_hex(rotating_pkey); - j["payment_tx"]["provider"] = payment_tx.provider_code; - j["payment_tx"]["payment_id"] = payment_tx.payment_id; - j["master_sig"] = oxenc::to_hex(master_sig); - j["rotating_sig"] = oxenc::to_hex(rotating_sig); - std::string result = j.dump(); - return result; + +namespace { + + // Endpoint paths (single master storage). Both the C `SESSION_PRO_BACKEND_*_ENDPOINT` symbols + // and the C++ `*_request()` return values point at these — the path is defined exactly once. + constexpr char add_payment_endpoint[] = "add_pro_payment"; + constexpr char generate_proof_endpoint[] = "generate_pro_proof"; + constexpr char get_pro_details_endpoint[] = "get_pro_details"; + constexpr char get_pro_revocations_endpoint[] = "get_pro_revocations"; + constexpr char set_refund_endpoint[] = "set_payment_refund_requested"; + + // Wire-format version emitted per request type (library-owned; not caller-settable). Kept + // independent so a future bump of one request type doesn't drag the others along. + constexpr uint8_t add_payment_version = 0; + constexpr uint8_t generate_proof_version = 0; + constexpr uint8_t get_pro_details_version = 0; + constexpr uint8_t get_pro_revocations_version = 0; + constexpr uint8_t set_refund_version = 0; + + // --- add-payment (endpoint add_pro_payment) --- + + b32 add_payment_hash( + std::span master_pubkey, + std::span rotating_pubkey, + std::string_view provider_code, + std::span payment_id) { + // The tail is the provider code string followed by the opaque payment id (spec §1); must + // match the backend's signed-hash construction. + return hash::blake2b_pers<32>( + ADD_PRO_PAYMENT_PERS, + add_payment_version, + master_pubkey, + rotating_pubkey, + provider_code, + payment_id); + } + + // Serialise an add-payment request body from already-computed fields (shared by the C++ + // add_payment_request and the C ..._request_to_json wrapper). + std::string add_payment_body( + std::span master_pubkey, + std::span rotating_pubkey, + std::string_view provider_code, + std::string_view payment_id, + std::span master_sig, + std::span rotating_sig) { + nlohmann::json j; + j["version"] = add_payment_version; + j["master_pkey"] = oxenc::to_hex(master_pubkey); + j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); + j["payment_tx"]["provider"] = provider_code; + j["payment_tx"]["payment_id"] = payment_id; + j["master_sig"] = oxenc::to_hex(master_sig); + j["rotating_sig"] = oxenc::to_hex(rotating_sig); + return j.dump(); + } + +} // namespace + +// C endpoint symbols: each points at the single master endpoint string defined above, so the C API +// and the C++ `ProRequest::endpoint` values are backed by one definition. +extern "C" { +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT = + add_payment_endpoint; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT = + generate_proof_endpoint; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT = + get_pro_details_endpoint; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT = + get_pro_revocations_endpoint; +LIBSESSION_EXPORT extern const char* const + SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT = set_refund_endpoint; +} + +std::optional provider_urls(std::string_view provider_code) { + if (provider_code == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) + return ProviderUrls{ + "https://support.google.com/googleplay/workflow/9813244?", + "https://getsession.org/android-refund", + "https://getsession.org/android-refund", + "https://play.google.com/store/account/" + "subscriptions?package=network.loki.messenger", + "https://play.google.com/store/account/" + "subscriptions?package=network.loki.messenger"}; + if (provider_code == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE) + return ProviderUrls{ + "https://support.apple.com/118223", + "https://support.apple.com/118223", + "https://support.apple.com/118224", + "https://apps.apple.com/account/subscriptions", + "https://account.apple.com/account/manage/section/subscriptions"}; + // rangeproof and unknown providers have no applicable URLs + return std::nullopt; } -MasterRotatingSignatures AddProPaymentRequest::build_sigs( - std::uint8_t version, +LIBSESSION_C_API session_pro_backend_provider_urls +session_pro_backend_get_provider_urls(const char* provider_code) { + // No URLs → all-NULL fields; otherwise each view is a static, null-terminated literal. + if (auto u = provider_urls(provider_code)) + return {u->refund_platform_url.data(), + u->refund_support_url.data(), + u->refund_status_url.data(), + u->update_subscription_url.data(), + u->cancel_subscription_url.data()}; + return {nullptr, nullptr, nullptr, nullptr, nullptr}; +} + +MasterRotatingSignatures add_payment_sigs( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id) { - // Hash components to 32 bytes. The tail is the provider code string followed by the opaque - // payment id (spec §1); must match the backend's signed-hash construction. - auto hash_to_sign = hash::blake2b_pers<32>( - ADD_PRO_PAYMENT_PERS, - version, - master_privkey.pubkey(), - rotating_privkey.pubkey(), - payment_tx_provider_code, - payment_tx_payment_id); - + std::string_view provider_code, + std::span payment_id) { + auto hash = add_payment_hash( + master_privkey.pubkey(), rotating_privkey.pubkey(), provider_code, payment_id); MasterRotatingSignatures result = {}; - result.master_sig = ed25519::sign(master_privkey, hash_to_sign); - result.rotating_sig = ed25519::sign(rotating_privkey, hash_to_sign); + result.master_sig = ed25519::sign(master_privkey, hash); + result.rotating_sig = ed25519::sign(rotating_privkey, hash); return result; } -std::string AddProPaymentRequest::build_to_json( - std::uint8_t version, +ProRequest add_payment_request( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id) { - auto sigs = AddProPaymentRequest::build_sigs( - version, - master_privkey, - rotating_privkey, - payment_tx_provider_code, - payment_tx_payment_id); - - AddProPaymentRequest request = {}; - request.version = version; - std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); - std::ranges::copy(rotating_privkey.pubkey(), request.rotating_pkey.begin()); - request.payment_tx.provider_code = payment_tx_provider_code; - request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); - request.master_sig = sigs.master_sig; - request.rotating_sig = sigs.rotating_sig; - - return request.to_json(); + std::string_view provider_code, + std::span payment_id) { + auto sigs = add_payment_sigs(master_privkey, rotating_privkey, provider_code, payment_id); + return {add_payment_endpoint, + add_payment_body( + master_privkey.pubkey(), + rotating_privkey.pubkey(), + provider_code, + to_string_view(payment_id), + sigs.master_sig, + sigs.rotating_sig)}; } -AddProPaymentOrGenerateProProofResponse AddProPaymentOrGenerateProProofResponse::parse( - std::string_view json) { - // Parse basics - AddProPaymentOrGenerateProProofResponse result = {}; - nlohmann::json j = json_parse(json, result.errors); - result.status = json_require(j, "status", result.errors); - if (result.errors.size()) { - result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; - return result; - } +namespace { + // Shared parse for the proof-carrying responses (add-payment and generate-proof both reply with + // exactly a proof); fills the common ProProofResponse base of whichever derived response is + // passed. + void fill_proof_response(std::string_view json, ProProofResponse& result) { + // Parse basics + nlohmann::json j = json_parse(json, result.errors); + result.status = json_require(j, "status", result.errors); + if (result.errors.size()) { + result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; + return; + } - // Parse errors - if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { - parse_json_response_errors(j, result.errors); - return result; - } + // Parse errors + if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { + parse_json_response_errors(j, result.errors); + return; + } - auto result_obj = json_require(j, "result", result.errors); - if (result.errors.size()) - return result; + auto result_obj = json_require(j, "result", result.errors); + if (result.errors.size()) + return; - // Parse payload - result.proof.version = json_require(result_obj, "version", result.errors); - auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); - result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); - json_require_fixed_bytes_from_hex( - result_obj, "revocation_tag", result.errors, result.proof.revocation_tag); - json_require_fixed_bytes_from_hex( - result_obj, "rotating_pkey", result.errors, result.proof.rotating_pubkey); - json_require_fixed_bytes_from_hex(result_obj, "sig", result.errors, result.proof.sig); + // Parse payload + result.proof.version = json_require(result_obj, "version", result.errors); + auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); + json_require_fixed_bytes_from_hex( + result_obj, "revocation_tag", result.errors, result.proof.revocation_tag); + json_require_fixed_bytes_from_hex( + result_obj, "rotating_pkey", result.errors, result.proof.rotating_pubkey); + json_require_fixed_bytes_from_hex(result_obj, "sig", result.errors, result.proof.sig); + } +} // namespace + +AddProPaymentResponse parse_add_payment(std::string_view json) { + AddProPaymentResponse result = {}; + fill_proof_response(json, result); return result; } -std::string GenerateProProofRequest::to_json() const { - nlohmann::json j; - j["version"] = version; - j["master_pkey"] = oxenc::to_hex(master_pkey); - j["rotating_pkey"] = oxenc::to_hex(rotating_pkey); - j["ts"] = epoch_seconds(unix_ts); - j["master_sig"] = oxenc::to_hex(master_sig); - j["rotating_sig"] = oxenc::to_hex(rotating_sig); - std::string result = j.dump(); +GenerateProProofResponse parse_pro_proof(std::string_view json) { + GenerateProProofResponse result = {}; + fill_proof_response(json, result); return result; } -MasterRotatingSignatures GenerateProProofRequest::build_sigs( - std::uint8_t request_version, +namespace { + + // --- generate-proof (endpoint generate_pro_proof) --- + + b32 generate_proof_hash( + std::span master_pubkey, + std::span rotating_pubkey, + std::chrono::sys_seconds unix_ts) { + // Must match: + // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 + uint64_t ts = epoch_seconds(unix_ts); + return hash::blake2b_pers<32>( + GENERATE_PROOF_PERS, generate_proof_version, master_pubkey, rotating_pubkey, ts); + } + + std::string generate_proof_body( + std::span master_pubkey, + std::span rotating_pubkey, + std::chrono::sys_seconds unix_ts, + std::span master_sig, + std::span rotating_sig) { + nlohmann::json j; + j["version"] = generate_proof_version; + j["master_pkey"] = oxenc::to_hex(master_pubkey); + j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); + j["ts"] = epoch_seconds(unix_ts); + j["master_sig"] = oxenc::to_hex(master_sig); + j["rotating_sig"] = oxenc::to_hex(rotating_sig); + return j.dump(); + } + +} // namespace + +MasterRotatingSignatures pro_proof_sigs( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, std::chrono::sys_seconds unix_ts) { - - // Hash components to 32 bytes, must match: - // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 - uint8_t version = 0; - uint64_t ts = epoch_seconds(unix_ts); - auto hash_to_sign = hash::blake2b_pers<32>( - GENERATE_PROOF_PERS, version, master_privkey.pubkey(), rotating_privkey.pubkey(), ts); - + auto hash = generate_proof_hash(master_privkey.pubkey(), rotating_privkey.pubkey(), unix_ts); MasterRotatingSignatures result = {}; - result.master_sig = ed25519::sign(master_privkey, hash_to_sign); - result.rotating_sig = ed25519::sign(rotating_privkey, hash_to_sign); + result.master_sig = ed25519::sign(master_privkey, hash); + result.rotating_sig = ed25519::sign(rotating_privkey, hash); return result; } -std::string GenerateProProofRequest::build_to_json( - std::uint8_t request_version, +ProRequest pro_proof_request( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, std::chrono::sys_seconds unix_ts) { - auto sigs = GenerateProProofRequest::build_sigs( - request_version, master_privkey, rotating_privkey, unix_ts); - - GenerateProProofRequest request = {}; - request.version = request_version; - std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); - std::ranges::copy(rotating_privkey.pubkey(), request.rotating_pkey.begin()); - request.unix_ts = unix_ts; - request.master_sig = sigs.master_sig; - request.rotating_sig = sigs.rotating_sig; - - return request.to_json(); + auto sigs = pro_proof_sigs(master_privkey, rotating_privkey, unix_ts); + return {generate_proof_endpoint, + generate_proof_body( + master_privkey.pubkey(), + rotating_privkey.pubkey(), + unix_ts, + sigs.master_sig, + sigs.rotating_sig)}; } -std::string GetProRevocationsRequest::to_json() const { +ProRequest revocations_request(std::int64_t ticket) { nlohmann::json j; - j["version"] = version; + j["version"] = get_pro_revocations_version; j["ticket"] = ticket; - std::string result = j.dump(); - return result; + return {get_pro_revocations_endpoint, j.dump()}; } -GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json) { +GetProRevocationsResponse parse_revocations(std::string_view json) { // Parse basics GetProRevocationsResponse result = {}; nlohmann::json j = json_parse(json, result.errors); @@ -320,47 +418,55 @@ GetProRevocationsResponse GetProRevocationsResponse::parse(std::string_view json return result; } -std::string GetProDetailsRequest::to_json() const { - nlohmann::json j; - j["version"] = version; - j["master_pkey"] = oxenc::to_hex(master_pkey); - j["master_sig"] = oxenc::to_hex(master_sig); - j["ts"] = epoch_seconds(unix_ts); - j["count"] = count; - std::string result = j.dump(); - return result; -} +namespace { + + // --- payment-details / get-pro-details (endpoint get_pro_details) --- + + b32 payment_details_hash( + std::span master_pubkey, + std::chrono::sys_seconds unix_ts, + uint32_t count) { + // Must match: + // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 + uint64_t ts = epoch_seconds(unix_ts); + return hash::blake2b_pers<32>( + GET_PRO_DETAILS_PERS, get_pro_details_version, master_pubkey, ts, count); + } + + std::string payment_details_body( + std::span master_pubkey, + std::span master_sig, + std::chrono::sys_seconds unix_ts, + uint32_t count) { + nlohmann::json j; + j["version"] = get_pro_details_version; + j["master_pkey"] = oxenc::to_hex(master_pubkey); + j["master_sig"] = oxenc::to_hex(master_sig); + j["ts"] = epoch_seconds(unix_ts); + j["count"] = count; + return j.dump(); + } -b64 GetProDetailsRequest::build_sig( - uint8_t version, +} // namespace + +b64 payment_details_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, uint32_t count) { - // Hash components to 32 bytes, must match: - // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 - uint64_t ts = epoch_seconds(unix_ts); - auto hash_to_sign = hash::blake2b_pers<32>( - GET_PRO_DETAILS_PERS, version, master_privkey.pubkey(), ts, count); - - return ed25519::sign(master_privkey, hash_to_sign); + auto hash = payment_details_hash(master_privkey.pubkey(), unix_ts, count); + return ed25519::sign(master_privkey, hash); } -std::string GetProDetailsRequest::build_to_json( - uint8_t version, +ProRequest payment_details_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, uint32_t count) { - GetProDetailsRequest request = {}; - request.version = version; - std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); - request.master_sig = GetProDetailsRequest::build_sig(version, master_privkey, unix_ts, count); - request.unix_ts = unix_ts; - request.count = count; - - return request.to_json(); + auto sig = payment_details_sig(master_privkey, unix_ts, count); + return {get_pro_details_endpoint, + payment_details_body(master_privkey.pubkey(), sig, unix_ts, count)}; } -GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { +GetProDetailsResponse parse_payment_details(std::string_view json) { // Parse basics GetProDetailsResponse result = {}; nlohmann::json j = json_parse(json, result.errors); @@ -470,70 +576,80 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { return result; } -b64 SetPaymentRefundRequestedRequest::build_sig( - uint8_t version, +namespace { + + // --- refund / set-payment-refund-requested (endpoint set_payment_refund_requested) --- + + b32 refund_hash( + std::span master_pubkey, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, + std::string_view provider_code, + std::span payment_id) { + // The tail is the provider code string followed by the opaque payment id (spec §1); must + // match the backend's signed-hash construction. + uint64_t ts = epoch_seconds(unix_ts); + uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); + return hash::blake2b_pers<32>( + SET_PAYMENT_REFUND_REQUESTED_PERS, + set_refund_version, + master_pubkey, + ts, + refund_requested_ts, + provider_code, + payment_id); + } + + std::string refund_body( + std::span master_pubkey, + std::chrono::sys_seconds unix_ts, + std::chrono::sys_seconds refund_requested_unix_ts, + std::string_view provider_code, + std::string_view payment_id, + std::span master_sig) { + nlohmann::json j; + j["version"] = set_refund_version; + j["master_pkey"] = oxenc::to_hex(master_pubkey); + j["ts"] = epoch_seconds(unix_ts); + j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); + j["payment_tx"]["provider"] = provider_code; + j["payment_tx"]["payment_id"] = payment_id; + j["master_sig"] = oxenc::to_hex(master_sig); + return j.dump(); + } + +} // namespace + +b64 refund_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id) { - // Hash components to 32 bytes. The tail is the provider code string followed by the opaque - // payment id (spec §1); must match the backend's signed-hash construction. - uint64_t ts = epoch_seconds(unix_ts); - uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); - auto hash_to_sign = hash::blake2b_pers<32>( - SET_PAYMENT_REFUND_REQUESTED_PERS, - version, - master_privkey.pubkey(), - ts, - refund_requested_ts, - payment_tx_provider_code, - payment_tx_payment_id); - - return ed25519::sign(master_privkey, hash_to_sign); + std::string_view provider_code, + std::span payment_id) { + auto hash = refund_hash( + master_privkey.pubkey(), unix_ts, refund_requested_unix_ts, provider_code, payment_id); + return ed25519::sign(master_privkey, hash); } -std::string SetPaymentRefundRequestedRequest::build_to_json( - uint8_t version, +ProRequest refund_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_unix_ts, - std::string_view payment_tx_provider_code, - std::span payment_tx_payment_id) { - auto sig = SetPaymentRefundRequestedRequest::build_sig( - version, - master_privkey, - unix_ts, - refund_requested_unix_ts, - payment_tx_provider_code, - payment_tx_payment_id); - - SetPaymentRefundRequestedRequest request = {}; - request.version = version; - std::ranges::copy(master_privkey.pubkey(), request.master_pkey.begin()); - request.payment_tx.provider_code = payment_tx_provider_code; - request.payment_tx.payment_id = to_string_view(payment_tx_payment_id); - request.master_sig = sig; - request.unix_ts = unix_ts; - request.refund_requested_unix_ts = refund_requested_unix_ts; - - return request.to_json(); -} - -std::string SetPaymentRefundRequestedRequest::to_json() const { - nlohmann::json j; - j["version"] = version; - j["master_pkey"] = oxenc::to_hex(master_pkey); - j["ts"] = epoch_seconds(unix_ts); - j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); - j["payment_tx"]["provider"] = payment_tx.provider_code; - j["payment_tx"]["payment_id"] = payment_tx.payment_id; - j["master_sig"] = oxenc::to_hex(master_sig); - std::string result = j.dump(); - return result; + std::string_view provider_code, + std::span payment_id) { + auto sig = refund_sig( + master_privkey, unix_ts, refund_requested_unix_ts, provider_code, payment_id); + return {set_refund_endpoint, + refund_body( + master_privkey.pubkey(), + unix_ts, + refund_requested_unix_ts, + provider_code, + to_string_view(payment_id), + sig)}; } -SetPaymentRefundRequestedResponse SetPaymentRefundRequestedResponse::parse(std::string_view json) { +SetPaymentRefundRequestedResponse parse_refund(std::string_view json) { // Parse basics SetPaymentRefundRequestedResponse result = {}; nlohmann::json j = json_parse(json, result.errors); @@ -572,7 +688,6 @@ static string8 C_PARSE_ERROR_INVALID_ARGS = STRING8_LIT("One or more C arguments LIBSESSION_C_API session_pro_backend_master_rotating_signatures session_pro_backend_add_pro_payment_request_build_sigs( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, @@ -588,12 +703,8 @@ session_pro_backend_add_pro_payment_request_build_sigs( auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto sigs = AddProPaymentRequest::build_sigs( - request_version, - master_span, - rotating_span, - payment_tx_provider_code, - payment_tx_payment_id_span); + auto sigs = add_payment_sigs( + master_span, rotating_span, payment_tx_provider_code, payment_tx_payment_id_span); std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; @@ -603,42 +714,8 @@ session_pro_backend_add_pro_payment_request_build_sigs( return result; } -LIBSESSION_C_API session_pro_backend_to_json -session_pro_backend_add_pro_payment_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) { - session_pro_backend_to_json result = {}; - - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto payment_tx_payment_id_span = - to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - - std::string json = AddProPaymentRequest::build_to_json( - request_version, - master_span, - rotating_span, - payment_tx_provider_code, - payment_tx_payment_id_span); - result.json = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - - return result; -} - LIBSESSION_C_API session_pro_backend_master_rotating_signatures session_pro_backend_generate_pro_proof_request_build_sigs( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, @@ -649,8 +726,7 @@ session_pro_backend_generate_pro_proof_request_build_sigs( try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto sigs = GenerateProProofRequest::build_sigs( - request_version, master_span, rotating_span, session::as_sys_seconds(ts)); + auto sigs = pro_proof_sigs(master_span, rotating_span, session::as_sys_seconds(ts)); std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); result.success = true; @@ -660,31 +736,8 @@ session_pro_backend_generate_pro_proof_request_build_sigs( return result; } -LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - int64_t ts) { - session_pro_backend_to_json result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto json = GenerateProProofRequest::build_to_json( - request_version, master_span, rotating_span, session::as_sys_seconds(ts)); - result.json = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; -} - LIBSESSION_C_API session_pro_backend_signature session_pro_backend_get_pro_details_request_build_sig( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, @@ -692,8 +745,7 @@ session_pro_backend_get_pro_details_request_build_sig( session_pro_backend_signature result = {}; try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - auto sig = GetProDetailsRequest::build_sig( - request_version, master_span, session::as_sys_seconds(ts), count); + auto sig = payment_details_sig(master_span, session::as_sys_seconds(ts), count); std::memcpy(result.sig.data, sig.data(), sig.size()); result.success = true; } catch (const std::exception& e) { @@ -702,46 +754,20 @@ session_pro_backend_get_pro_details_request_build_sig( return result; } -LIBSESSION_C_API session_pro_backend_to_json -session_pro_backend_get_pro_details_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - int64_t ts, - uint32_t count) { - session_pro_backend_to_json result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - auto json = GetProDetailsRequest::build_to_json( - request_version, master_span, session::as_sys_seconds(ts), count); - result.json = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; -} - LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment_request_to_json( const session_pro_backend_add_pro_payment_request* request) { session_pro_backend_to_json result = {}; if (!request) return result; - // Construct C++ struct - AddProPaymentRequest cpp = {}; - cpp.version = request->version; - std::memcpy(cpp.master_pkey.data(), request->master_pkey.data, cpp.master_pkey.size()); - std::memcpy(cpp.rotating_pkey.data(), request->rotating_pkey.data, cpp.rotating_pkey.size()); - cpp.payment_tx.provider_code = - std::string(request->payment_tx.provider_code, request->payment_tx.provider_code_count); - cpp.payment_tx.payment_id = - std::string(request->payment_tx.payment_id, request->payment_tx.payment_id_count); - std::memcpy(cpp.master_sig.data(), request->master_sig.data, cpp.master_sig.size()); - std::memcpy(cpp.rotating_sig.data(), request->rotating_sig.data, cpp.rotating_sig.size()); - try { - std::string json = cpp.to_json(); + std::string json = add_payment_body( + to_byte_span(request->master_pkey.data), + to_byte_span(request->rotating_pkey.data), + {request->payment_tx.provider_code, request->payment_tx.provider_code_count}, + {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, + to_byte_span(request->master_sig.data), + to_byte_span(request->rotating_sig.data)); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -757,17 +783,13 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_generate_pro_pr if (!request) return result; - // Construct C++ struct - GenerateProProofRequest cpp; - cpp.version = request->version; - std::memcpy(cpp.master_pkey.data(), request->master_pkey.data, cpp.master_pkey.size()); - std::memcpy(cpp.rotating_pkey.data(), request->rotating_pkey.data, cpp.rotating_pkey.size()); - cpp.unix_ts = session::as_sys_seconds(request->ts); - std::memcpy(cpp.master_sig.data(), request->master_sig.data, cpp.master_sig.size()); - std::memcpy(cpp.rotating_sig.data(), request->rotating_sig.data, cpp.rotating_sig.size()); - try { - std::string json = cpp.to_json(); + std::string json = generate_proof_body( + to_byte_span(request->master_pkey.data), + to_byte_span(request->rotating_pkey.data), + session::as_sys_seconds(request->ts), + to_byte_span(request->master_sig.data), + to_byte_span(request->rotating_sig.data)); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -784,13 +806,8 @@ session_pro_backend_get_pro_revocations_request_to_json( if (!request) return result; - // Construct C++ struct - GetProRevocationsRequest cpp = {}; - cpp.version = request->version; - cpp.ticket = request->ticket; - try { - std::string json = cpp.to_json(); + std::string json = revocations_request(request->ticket).body; result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -806,17 +823,12 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details if (!request) return result; - // Construct C++ struct - GetProDetailsRequest cpp = {}; - cpp.version = request->version; - std::memcpy( - cpp.master_pkey.data(), request->master_pkey.data, sizeof(request->master_pkey.data)); - std::memcpy(cpp.master_sig.data(), request->master_sig.data, sizeof(request->master_sig.data)); - cpp.unix_ts = session::as_sys_seconds(request->ts); - cpp.count = request->count; - try { - std::string json = cpp.to_json(); + std::string json = payment_details_body( + to_byte_span(request->master_pkey.data), + to_byte_span(request->master_sig.data), + session::as_sys_seconds(request->ts), + request->count); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -826,11 +838,10 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details return result; } -LIBSESSION_C_API session_pro_backend_add_pro_payment_or_generate_pro_proof_response -session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( - const char* json, size_t json_len) { +LIBSESSION_C_API session_pro_backend_pro_proof_response +session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response result = {}; + session_pro_backend_pro_proof_response result = {}; if (!json) { result.header.status = 1; result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; @@ -839,7 +850,9 @@ session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( } // Note, parse is written to not throw so we can safely read without try-catch crap - auto cpp = AddProPaymentOrGenerateProProofResponse::parse({json, json_len}); + // add-payment and generate-proof share the proof-response shape; the C response struct is + // generic, so either derived parser works here. + auto cpp = parse_add_payment({json, json_len}); // Calculate how much memory we need and create an arena arena_t arena = {}; @@ -899,7 +912,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t } // Note, parse is written to not throw so we can safely read without try-catch crap - GetProRevocationsResponse cpp = GetProRevocationsResponse::parse({json, json_len}); + GetProRevocationsResponse cpp = parse_revocations({json, json_len}); // Calculate how much memory we need and create an arena arena_t arena = {}; @@ -966,7 +979,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json } // Note, parse is written to not throw so we can safely read without try-catch crap - auto cpp = GetProDetailsResponse::parse({json, json_len}); + auto cpp = parse_payment_details({json, json_len}); // Calculate how much memory we need and create an arena arena_t arena = {}; @@ -1036,7 +1049,6 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json LIBSESSION_C_API session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( - uint8_t request_version, const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, @@ -1052,8 +1064,7 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto sig = SetPaymentRefundRequestedRequest::build_sig( - request_version, + auto sig = refund_sig( master_span, unix_ts, refund_requested_unix_ts, @@ -1067,40 +1078,6 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r return result; } -LIBSESSION_C_API session_pro_backend_to_json -session_pro_backend_set_payment_refund_requested_request_build_to_json( - uint8_t request_version, - const unsigned char* master_privkey, - size_t master_privkey_len, - int64_t ts, - int64_t refund_requested_ts, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) { - - session_pro_backend_to_json result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); - std::chrono::sys_seconds refund_requested_unix_ts = - session::as_sys_seconds(refund_requested_ts); - auto payment_tx_payment_id_span = - to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto json = SetPaymentRefundRequestedRequest::build_to_json( - request_version, - master_span, - unix_ts, - refund_requested_unix_ts, - payment_tx_provider_code, - payment_tx_payment_id_span); - result.json = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; -} - LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_request_to_json( const session_pro_backend_set_payment_refund_requested_request* request) { @@ -1108,21 +1085,14 @@ session_pro_backend_set_payment_refund_requested_request_to_json( if (!request) return result; - // Construct C++ struct - SetPaymentRefundRequestedRequest cpp = {}; - cpp.version = request->version; - std::memcpy( - cpp.master_pkey.data(), request->master_pkey.data, sizeof(request->master_pkey.data)); - std::memcpy(cpp.master_sig.data(), request->master_sig.data, sizeof(request->master_sig.data)); - cpp.unix_ts = session::as_sys_seconds(request->ts); - cpp.refund_requested_unix_ts = session::as_sys_seconds(request->refund_requested_ts); - cpp.payment_tx.provider_code = - std::string(request->payment_tx.provider_code, request->payment_tx.provider_code_count); - cpp.payment_tx.payment_id = - std::string(request->payment_tx.payment_id, request->payment_tx.payment_id_count); - try { - std::string json = cpp.to_json(); + std::string json = refund_body( + to_byte_span(request->master_pkey.data), + session::as_sys_seconds(request->ts), + session::as_sys_seconds(request->refund_requested_ts), + {request->payment_tx.provider_code, request->payment_tx.provider_code_count}, + {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, + to_byte_span(request->master_sig.data)); result.json = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { @@ -1143,7 +1113,7 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json } // Note, parse is written to not throw so we can safely read without try-catch crap - auto cpp = SetPaymentRefundRequestedResponse::parse({json, json_len}); + auto cpp = parse_refund({json, json_len}); // Calculate how much memory we need and create an arena arena_t arena = {}; @@ -1189,8 +1159,8 @@ LIBSESSION_C_API void session_pro_backend_to_json_free(session_pro_backend_to_js } } -LIBSESSION_C_API void session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( - session_pro_backend_add_pro_payment_or_generate_pro_proof_response* response) { +LIBSESSION_C_API void session_pro_backend_pro_proof_response_free( + session_pro_backend_pro_proof_response* response) { if (response) { free(response->header.internal_arena_buf_); *response = {}; diff --git a/tests/test_logging.cpp b/tests/test_logging.cpp index c163e2f9c..2ff5f38c3 100644 --- a/tests/test_logging.cpp +++ b/tests/test_logging.cpp @@ -3,13 +3,12 @@ #include #include #include +#include #include #include #include "utils.hpp" -#include - using namespace session; using namespace oxen; using namespace oxen::log::literals; diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 87b9d6a45..10e39ff51 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -97,7 +97,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid inputs session_pro_backend_master_rotating_signatures result = session_pro_backend_add_pro_payment_request_build_sigs( - /*version*/ 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -110,8 +109,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.error_count == 0); // Verify signatures match C++ implementation - auto cpp = AddProPaymentRequest::build_sigs( - 0, + auto cpp = add_payment_sigs( master_privkey.data, rotating_privkey.data, payment_tx.provider_code, @@ -128,7 +126,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Invalid master key size result = session_pro_backend_add_pro_payment_request_build_sigs( - 0, master_privkey.data, sizeof(master_privkey.data) - 1, rotating_privkey.data, @@ -145,7 +142,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid inputs result = session_pro_backend_generate_pro_proof_request_build_sigs( - 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -155,11 +151,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.error_count == 0); // Verify signatures match C++ implementation - auto cpp_sigs = GenerateProProofRequest::build_sigs( - 0, - master_privkey.data, - rotating_privkey.data, - session::as_sys_seconds(unix_ts)); + auto cpp_sigs = pro_proof_sigs( + master_privkey.data, rotating_privkey.data, session::as_sys_seconds(unix_ts)); REQUIRE(std::memcmp( result.master_sig.data, cpp_sigs.master_sig.data(), @@ -171,7 +164,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Invalid rotating key size result = session_pro_backend_generate_pro_proof_request_build_sigs( - 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -183,14 +175,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_add_pro_payment_request_to_json") { session_pro_backend_add_pro_payment_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; request.payment_tx = payment_tx; session_pro_backend_master_rotating_signatures sigs = session_pro_backend_add_pro_payment_request_build_sigs( - request.version, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -210,44 +200,14 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); - // Verify JSON matches C++ implementation - AddProPaymentRequest cpp = {}; - static_assert(sizeof(master_pubkey.data) == cpp.master_pkey.max_size()); - static_assert(sizeof(rotating_pubkey.data) == cpp.rotating_pkey.max_size()); - - cpp.version = request.version; - std::memcpy(cpp.master_pkey.data(), master_pubkey.data, sizeof(master_pubkey.data)); - std::memcpy( - cpp.rotating_pkey.data(), - rotating_pubkey.data, - sizeof(rotating_pubkey.data)); - cpp.payment_tx.provider_code = - std::string(payment_tx.provider_code, payment_tx.provider_code_count); - cpp.payment_tx.payment_id = - std::string(payment_tx.payment_id, payment_tx.payment_id_count); - std::memcpy( - cpp.master_sig.data(), sigs.master_sig.data, sizeof(sigs.master_sig.data)); - std::memcpy( - cpp.rotating_sig.data(), - sigs.rotating_sig.data, - sizeof(sigs.rotating_sig.data)); - std::string cpp_json = cpp.to_json(); - REQUIRE(string8_equals(result.json, cpp_json)); - - // Verify that the helper one-shot-to-json function generates the same payload - auto one_shot = session_pro_backend_add_pro_payment_request_build_to_json( - request.version, + // Verify JSON + route match the C++ free-function implementation + auto cpp = add_payment_request( master_privkey.data, - sizeof(master_privkey.data), rotating_privkey.data, - sizeof(rotating_privkey.data), - request.payment_tx.provider_code, - reinterpret_cast(request.payment_tx.payment_id), - request.payment_tx.payment_id_count); - REQUIRE(one_shot.success); - REQUIRE(one_shot.json.size == result.json.size); - INFO("One shot: " << one_shot.json.data << "\n\nJSON: " << result.json.data); - REQUIRE(memcmp(one_shot.json.data, result.json.data, result.json.size) == 0); + payment_tx.provider_code, + to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT); + REQUIRE(string8_equals(result.json, cpp.body)); } // After freeing @@ -263,14 +223,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_generate_pro_proof_request_to_json") { session_pro_backend_generate_pro_proof_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; request.ts = unix_ts; session_pro_backend_master_rotating_signatures sigs = session_pro_backend_generate_pro_proof_request_build_sigs( - request.version, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -288,36 +246,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); - // Verify JSON matches C++ implementation - GenerateProProofRequest cpp = {}; - cpp.version = request.version; - std::memcpy(cpp.master_pkey.data(), master_pubkey.data, sizeof(master_pubkey.data)); - std::memcpy( - cpp.rotating_pkey.data(), - rotating_pubkey.data, - sizeof(rotating_pubkey.data)); - cpp.unix_ts = session::as_sys_seconds(unix_ts); - std::memcpy( - cpp.master_sig.data(), sigs.master_sig.data, sizeof(sigs.master_sig.data)); - std::memcpy( - cpp.rotating_sig.data(), - sigs.rotating_sig.data, - sizeof(sigs.rotating_sig.data)); - std::string cpp_json = cpp.to_json(); - REQUIRE(string8_equals(result.json, cpp_json)); - - // Verify that the helper one-shot-to-json function generates the same payload - auto one_shot = session_pro_backend_generate_pro_proof_request_build_to_json( - request.version, + // Verify JSON + route match the C++ free-function implementation + auto cpp = pro_proof_request( master_privkey.data, - sizeof(master_privkey.data), rotating_privkey.data, - sizeof(rotating_privkey.data), - request.ts); - REQUIRE(one_shot.success); - REQUIRE(one_shot.json.size == result.json.size); - INFO("One shot: " << one_shot.json.data << "\n\nJSON: " << result.json.data); - REQUIRE(memcmp(one_shot.json.data, result.json.data, result.json.size) == 0); + session::as_sys_seconds(unix_ts)); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT); + REQUIRE(string8_equals(result.json, cpp.body)); } // After freeing @@ -333,7 +268,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_get_pro_revocations_request_to_json") { session_pro_backend_get_pro_revocations_request request = {}; - request.version = 0; request.ticket = 123; // Valid request @@ -344,12 +278,10 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); - // Verify JSON matches C++ implementation - GetProRevocationsRequest cpp = {}; - cpp.version = request.version; - cpp.ticket = request.ticket; - std::string cpp_json = cpp.to_json(); - REQUIRE(string8_equals(result.json, cpp_json)); + // Verify JSON + route match the C++ free-function implementation + auto cpp = revocations_request(request.ticket); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT); + REQUIRE(string8_equals(result.json, cpp.body)); } // After freeing @@ -365,14 +297,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_get_pro_details_request_to_json") { session_pro_backend_get_pro_details_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.ts = unix_ts; request.count = 10'000; session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( - request.version, master_privkey.data, sizeof(master_privkey.data), request.ts, @@ -388,27 +318,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); - // Verify JSON matches C++ implementation - GetProDetailsRequest cpp = {}; - cpp.version = 0; - std::memcpy(cpp.master_pkey.data(), master_pubkey.data, sizeof(master_pubkey.data)); - std::memcpy(cpp.master_sig.data(), sig.sig.data, sizeof(sig.sig.data)); - cpp.unix_ts = session::as_sys_seconds(unix_ts); - cpp.count = request.count; - std::string cpp_json = cpp.to_json(); - REQUIRE(string8_equals(result.json, cpp_json)); - - // Verify that the helper one-shot-to-json function generates the same payload - auto one_shot = session_pro_backend_get_pro_details_request_build_to_json( - request.version, - master_privkey.data, - sizeof(master_privkey.data), - request.ts, - request.count); - REQUIRE(one_shot.success); - REQUIRE(one_shot.json.size == result.json.size); - INFO("One shot: " << one_shot.json.data << "\n\nJSON: " << result.json.data); - REQUIRE(memcmp(one_shot.json.data, result.json.data, result.json.size) == 0); + // Verify JSON + route match the C++ free-function implementation + auto cpp = payment_details_request( + master_privkey.data, session::as_sys_seconds(unix_ts), request.count); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT); + REQUIRE(string8_equals(result.json, cpp.body)); } // After freeing @@ -422,7 +336,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.size == 0); } - SECTION("session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse") { + SECTION("session_pro_backend_pro_proof_response_parse") { b32 fake_revocation_tag; randombytes_buf(fake_revocation_tag.data(), fake_revocation_tag.size()); @@ -438,14 +352,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { std::string json = j.dump(); // Valid JSON - session_pro_backend_add_pro_payment_or_generate_pro_proof_response result = - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( - json.data(), json.size()); + session_pro_backend_pro_proof_response result = + session_pro_backend_pro_proof_response_parse(json.data(), json.size()); { - scope_exit result_free{[&]() { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( - &result); - }}; + scope_exit result_free{ + [&]() { session_pro_backend_pro_proof_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); @@ -469,7 +380,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Here we also create the CPP version, we will run the conversion functions into // pro proofs (both C and CPP variants) and then compare the two structures to make // sure the conversion functions are sound. - auto result_cpp = AddProPaymentOrGenerateProProofResponse::parse(json); + auto result_cpp = parse_add_payment(json); // Validate C and CPP variants REQUIRE(result.proof.version == result_cpp.proof.version); @@ -496,25 +407,21 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Invalid JSON json = "{invalid}"; - result = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( - json.data(), json.size()); + result = session_pro_backend_pro_proof_response_parse(json.data(), json.size()); { - scope_exit result_free{[&]() { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( - &result); - }}; + scope_exit result_free{ + [&]() { session_pro_backend_pro_proof_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(result.header.errors_count > 0); REQUIRE(result.header.errors != nullptr); } // After freeing - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&result); + session_pro_backend_pro_proof_response_free(&result); REQUIRE(result.header.internal_arena_buf_ == nullptr); // Null JSON - result = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( - nullptr, 0); + result = session_pro_backend_pro_proof_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(result.header.errors_count == 1); REQUIRE(result.header.errors != nullptr); @@ -726,9 +633,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(to_json.json.data == nullptr); REQUIRE(to_json.json.size == 0); - session_pro_backend_add_pro_payment_or_generate_pro_proof_response proof_response = {}; - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( - &proof_response); + session_pro_backend_pro_proof_response proof_response = {}; + session_pro_backend_pro_proof_response_free(&proof_response); REQUIRE(proof_response.header.internal_arena_buf_ == nullptr); session_pro_backend_get_pro_revocations_response rev_response = {}; @@ -742,14 +648,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_set_payment_refund_requested_request_to_json") { session_pro_backend_set_payment_refund_requested_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.ts = unix_ts; request.refund_requested_ts = unix_ts + 1; + request.payment_tx = payment_tx; session_pro_backend_signature sig = session_pro_backend_set_payment_refund_requested_request_build_sigs( - request.version, master_privkey.data, sizeof(master_privkey.data), request.ts, @@ -769,22 +674,15 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); - // Verify JSON matches C++ implementation - SetPaymentRefundRequestedRequest cpp = {}; - cpp.version = request.version; - std::memcpy( - cpp.master_pkey.data(), - request.master_pkey.data, - sizeof(request.master_pkey.data)); - cpp.unix_ts = session::as_sys_seconds(unix_ts); - cpp.refund_requested_unix_ts = session::as_sys_seconds(request.refund_requested_ts); - std::memcpy( - cpp.master_sig.data(), - request.master_sig.data, - sizeof(request.master_sig.data)); - - std::string cpp_json = cpp.to_json(); - REQUIRE(string8_equals(result.json, cpp_json)); + // Verify JSON + route match the C++ free-function implementation + auto cpp = refund_request( + master_privkey.data, + session::as_sys_seconds(request.ts), + session::as_sys_seconds(request.refund_requested_ts), + payment_tx.provider_code, + to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT); + REQUIRE(string8_equals(result.json, cpp.body)); } // After freeing @@ -934,7 +832,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Build request session_pro_backend_master_rotating_signatures add_pro_sigs = session_pro_backend_add_pro_payment_request_build_sigs( - /*version*/ 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -944,7 +841,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { payment_tx.payment_id_count); session_pro_backend_add_pro_payment_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; request.payment_tx = payment_tx; @@ -963,12 +859,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { std::string_view(request_json.json.data, request_json.json.size)); // Parse response - session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( + session_pro_backend_pro_proof_response response = + session_pro_backend_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); - }}; + scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; for (size_t index = 0; index < response.header.errors_count; index++) { string8 error = response.header.errors[index]; @@ -995,7 +889,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Build request session_pro_backend_master_rotating_signatures pro_sigs = session_pro_backend_generate_pro_proof_request_build_sigs( - /*version*/ 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -1003,7 +896,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { now_unix_ts); session_pro_backend_generate_pro_proof_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; request.ts = now_unix_ts; @@ -1022,12 +914,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { std::string_view(request_json.json.data, request_json.json.size)); // Parse response - session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( + session_pro_backend_pro_proof_response response = + session_pro_backend_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); - }}; + scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; for (size_t index = 0; index < response.header.errors_count; index++) { if (index == 0) @@ -1048,24 +938,19 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(request.rotating_pkey.data)) == 0); session_pro_backend_to_json_free(&request_json); - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); + session_pro_backend_pro_proof_response_free(&response); } // Get pro status { // Build request session_pro_backend_get_pro_details_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.ts = session::epoch_seconds(session::clock_now_s()); request.count = 10'000; session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( - request.version, - master_privkey.data, - sizeof(master_privkey.data), - request.ts, - request.count); + master_privkey.data, sizeof(master_privkey.data), request.ts, request.count); REQUIRE(sig.success); request.master_sig = sig.sig; @@ -1104,16 +989,11 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { { // Build request session_pro_backend_get_pro_details_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.ts = session::epoch_seconds(session::clock_now_s()); session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( - request.version, - master_privkey.data, - sizeof(master_privkey.data), - request.ts, - request.count); + master_privkey.data, sizeof(master_privkey.data), request.ts, request.count); REQUIRE(sig.success); request.master_sig = sig.sig; @@ -1174,7 +1054,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Build request session_pro_backend_master_rotating_signatures add_pro_sigs = session_pro_backend_add_pro_payment_request_build_sigs( - /*version*/ 0, master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, @@ -1184,7 +1063,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { another_payment_tx.payment_id_count); session_pro_backend_add_pro_payment_request request = {}; - request.version = 0; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; request.payment_tx = another_payment_tx; @@ -1203,12 +1081,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { std::string_view(request_json.json.data, request_json.json.size)); // Parse response - session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( + session_pro_backend_pro_proof_response response = + session_pro_backend_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { - session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); - }}; + scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; // Verify response session_protocol_pro_proof proof = response.proof; @@ -1224,7 +1100,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { { // Build request session_pro_backend_get_pro_revocations_request request = {}; - request.version = 0; session_pro_backend_to_json request_json = session_pro_backend_get_pro_revocations_request_to_json(&request); @@ -1262,9 +1137,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { { // Build request int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); - session_pro_backend_to_json request_json = - session_pro_backend_set_payment_refund_requested_request_build_to_json( - /*version*/ 0, + session_pro_backend_signature refund_sig = + session_pro_backend_set_payment_refund_requested_request_build_sigs( master_privkey.data, sizeof(master_privkey.data), /*ts*/ now_unix_ts, @@ -1273,6 +1147,15 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { reinterpret_cast(another_payment_tx.payment_id), another_payment_tx.payment_id_count); + session_pro_backend_set_payment_refund_requested_request request = {}; + request.master_pkey = master_pubkey; + request.master_sig = refund_sig.sig; + request.ts = now_unix_ts; + request.refund_requested_ts = now_unix_ts; + request.payment_tx = another_payment_tx; + + session_pro_backend_to_json request_json = + session_pro_backend_set_payment_refund_requested_request_to_json(&request); scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; // Do curl request From 8399ec40170a3a8e45ee756190e25d1aa0b224eb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 02:51:07 -0300 Subject: [PATCH 106/193] =?UTF-8?q?Preserve=20sub-second=20precision=20on?= =?UTF-8?q?=20purchased=5Fts/revoked=5Fts=20(+=20rename=20unredeemed?= =?UTF-8?q?=E2=86=92purchased)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The get-details wire key `unredeemed_ts` is renamed `purchased_ts` (the field is the provider's purchase instant, not a payment state), and `purchased_ts` and `revoked_ts` are now JSON floats carrying the upstream provider's sub-second precision (pro-wire-protocol.md §1/§5). Parse those two as floating-point seconds and keep the precision rather than truncating: the C++ ProPaymentItem stores them as millisecond-resolution sys_ms, and the C API exposes them as double fractional seconds (millisecond-precise via sys_ms). All other timestamps remain whole-second integers. Also finish the redesign's permalink->spec comment conversion in the two hash helpers that still cited backend GitHub permalinks (generate-proof, get-details). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 7 +++-- include/session/pro_backend.hpp | 12 ++++--- src/pro_backend.cpp | 56 ++++++++++++++++++++++++--------- tests/test_pro_backend.cpp | 15 +++++---- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 0f7468dcb..4db071865 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -221,12 +221,15 @@ struct session_pro_backend_pro_payment_item { size_t payment_provider_count; bool auto_renewing; - int64_t unredeemed_ts; + /// Provider purchase time, fractional UNIX seconds. Millisecond-precise: the value passes + /// through a millisecond-resolution representation, so sub-millisecond digits are not retained. + double purchased_ts; int64_t redeemed_ts; int64_t expiry_ts; int64_t grace_period_duration; int64_t platform_refund_expiry_ts; - int64_t revoked_ts; + /// Provider revocation instant, fractional UNIX seconds (millisecond-precise; 0 if not revoked) + double revoked_ts; int64_t refund_requested_ts; /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index b949b65a5..5d63db24c 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -284,8 +284,10 @@ struct ProPaymentItem { /// billing cycle. bool auto_renewing; - /// Unix timestamp of when the payment was witnessed by the Pro Backend. Always set - std::chrono::sys_seconds unredeemed_unix_ts; + /// Provider purchase time (when the upstream provider recorded the purchase). Always set. + /// Carries the provider's sub-second precision as a millisecond-resolution `sys_ms`; the wire + /// sends it as a float of seconds. + sys_ms purchased_unix_ts; /// Unix timestamp of when the payment was redeemed. 0 if not activated std::chrono::sys_seconds redeemed_unix_ts; @@ -303,8 +305,10 @@ struct ProPaymentItem { /// provider. Thereafter the user must initiate a refund manually via Session support. std::chrono::sys_seconds platform_refund_expiry_unix_ts; - /// Unix timestamp of when the payment was revoked or refunded. 0 if not applicable. - std::chrono::sys_seconds revoked_unix_ts; + /// Provider revocation instant (when the payment was revoked). Epoch (0) if not applicable. + /// Carries the provider's sub-second precision as a millisecond-resolution `sys_ms`; the wire + /// sends it as a float of seconds. + sys_ms revoked_unix_ts; /// UNIX timestamp at which a refund request was requested for this payment. This is set to 0 /// if no refund has been requested for this payment yet. diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 95f107392..c0eb2b01b 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -67,6 +67,34 @@ const T json_require( return result; } +// Reads a JSON number that may be an integer *or* a float, as a double. Used for the two +// upstream-provider event instants (`purchased_ts`, `revoked_ts`) which the backend emits as floats +// carrying the provider's sub-second precision (pro-wire-protocol.md §1). +double json_require_number( + const nlohmann::json& j, std::string_view key, std::vector& errors) { + auto it = j.find(key); + if (it == j.end()) + errors.push_back(fmt::format("Key '{}' is missing", key)); + else if (it->is_number()) + return it->get(); + else + errors.push_back(fmt::format("Key value ({}, {}) was not a number", key, it->dump(1))); + return 0; +} + +// Fractional UNIX seconds (double) -> millisecond-precision system time, preserving the provider's +// sub-second precision (rounded to the nearest millisecond). +session::sys_ms sys_ms_from_seconds(double seconds) { + return session::sys_ms( + std::chrono::round(std::chrono::duration(seconds))); +} + +// Millisecond-precision system time -> fractional UNIX seconds (double); the C API carries these +// two instants as double seconds (millisecond-precise, having passed through sys_ms). +double epoch_seconds_double(session::sys_ms t) { + return std::chrono::duration(t.time_since_epoch()).count(); +} + void parse_json_response_errors(const nlohmann::json& j, std::vector& errors) { const auto& array = json_require(j, "errors", errors); errors.reserve(errors.size() + array.size()); @@ -141,8 +169,7 @@ namespace { std::span rotating_pubkey, std::string_view provider_code, std::span payment_id) { - // The tail is the provider code string followed by the opaque payment id (spec §1); must - // match the backend's signed-hash construction. + // Must match the add-payment signed-request hash in pro-wire-protocol.md §3.2 (+ §3.5). return hash::blake2b_pers<32>( ADD_PRO_PAYMENT_PERS, add_payment_version, @@ -306,8 +333,7 @@ namespace { std::span master_pubkey, std::span rotating_pubkey, std::chrono::sys_seconds unix_ts) { - // Must match: - // https://github.com/Doy-lee/session-pro-backend/blob/5b66b1a4a64dc8da0225507019cbe21d7642fa78/backend.py#L631 + // Must match the generate-proof signed-request hash in pro-wire-protocol.md §3.1. uint64_t ts = epoch_seconds(unix_ts); return hash::blake2b_pers<32>( GENERATE_PROOF_PERS, generate_proof_version, master_pubkey, rotating_pubkey, ts); @@ -426,8 +452,7 @@ namespace { std::span master_pubkey, std::chrono::sys_seconds unix_ts, uint32_t count) { - // Must match: - // https://github.com/Doy-lee/session-pro-backend/blob/635b14fc93302658de6c07c017f705673fc7c57f/server.py#L395 + // Must match the get-pro-details signed-request hash in pro-wire-protocol.md §3.4. uint64_t ts = epoch_seconds(unix_ts); return hash::blake2b_pers<32>( GET_PRO_DETAILS_PERS, get_pro_details_version, master_pubkey, ts, count); @@ -534,14 +559,17 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { auto payment_provider = json_require(obj, "payment_provider", result.errors); auto payment_id = json_require(obj, "payment_id", result.errors); auto auto_renewing = json_require(obj, "auto_renewing", result.errors); - auto unredeemed_ts = json_require(obj, "unredeemed_ts", result.errors); + // purchased_ts and revoked_ts are upstream-provider event instants: floats on the wire + // carrying sub-second precision (kept as millisecond-precision sys_ms). All other + // timestamps are whole-second integers. + auto purchased_ts = json_require_number(obj, "purchased_ts", result.errors); auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); auto expiry_ts = json_require(obj, "expiry_ts", result.errors); auto grace_period_duration = json_require(obj, "grace_period_duration", result.errors); auto platform_refund_expiry_ts = json_require(obj, "platform_refund_expiry_ts", result.errors); - auto revoked_ts = json_require(obj, "revoked_ts", result.errors); + auto revoked_ts = json_require_number(obj, "revoked_ts", result.errors); auto refund_requested_ts = json_require(obj, "refund_requested_ts", result.errors); ProPaymentItem item = {}; @@ -559,12 +587,12 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { item.payment_id = std::move(payment_id); item.auto_renewing = auto_renewing; - item.unredeemed_unix_ts = as_sys_seconds(unredeemed_ts); + item.purchased_unix_ts = sys_ms_from_seconds(purchased_ts); item.redeemed_unix_ts = as_sys_seconds(redeemed_ts); item.expiry_unix_ts = as_sys_seconds(expiry_ts); item.grace_period_duration = std::chrono::seconds(grace_period_duration); item.platform_refund_expiry_unix_ts = as_sys_seconds(platform_refund_expiry_ts); - item.revoked_unix_ts = as_sys_seconds(revoked_ts); + item.revoked_unix_ts = sys_ms_from_seconds(revoked_ts); item.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); // Handle parsing result @@ -586,8 +614,8 @@ namespace { std::chrono::sys_seconds refund_requested_unix_ts, std::string_view provider_code, std::span payment_id) { - // The tail is the provider code string followed by the opaque payment id (spec §1); must - // match the backend's signed-hash construction. + // Must match the set-payment-refund-requested signed-request hash in pro-wire-protocol.md + // §3.3 (+ §3.5 for payment_id). uint64_t ts = epoch_seconds(unix_ts); uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); return hash::blake2b_pers<32>( @@ -1024,12 +1052,12 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json dest.plan_count = session::copy_c_str(dest.plan, src.plan) - 1; dest.payment_provider_count = session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; - dest.unredeemed_ts = epoch_seconds(src.unredeemed_unix_ts); + dest.purchased_ts = epoch_seconds_double(src.purchased_unix_ts); dest.redeemed_ts = epoch_seconds(src.redeemed_unix_ts); dest.expiry_ts = epoch_seconds(src.expiry_unix_ts); dest.grace_period_duration = src.grace_period_duration.count(); dest.platform_refund_expiry_ts = epoch_seconds(src.platform_refund_expiry_unix_ts); - dest.revoked_ts = epoch_seconds(src.revoked_unix_ts); + dest.revoked_ts = epoch_seconds_double(src.revoked_unix_ts); dest.refund_requested_ts = epoch_seconds(src.refund_requested_unix_ts); dest.payment_id_count = session::copy_c_str(dest.payment_id, src.payment_id) - 1; diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 10e39ff51..07e873308 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -42,14 +42,14 @@ static bool string8_equals(string8 s8, std::string_view str) { (int)item.payment_provider_count, item.payment_provider); fprintf(stderr, "item.auto_renewing: %d\n", item.auto_renewing); - fprintf(stderr, "item.unredeemed_ts: %" PRId64 "zu\n", item.unredeemed_ts); + fprintf(stderr, "item.purchased_ts: %f\n", item.purchased_ts); fprintf(stderr, "item.redeemed_ts: %" PRId64 "zu\n", item.redeemed_ts); fprintf(stderr, "item.expiry_ts: %" PRId64 "\n", item.expiry_ts); fprintf(stderr, "item.grace_period_duration: %" PRId64 "zu\n", item.grace_period_duration); fprintf(stderr, "item.platform_refund_expiry_ts: %" PRId64 "zu\n", item.platform_refund_expiry_ts); - fprintf(stderr, "item.revoked_ts: %" PRId64 "zu\n", item.revoked_ts); + fprintf(stderr, "item.revoked_ts: %f\n", item.revoked_ts); fprintf(stderr, "item.payment_id: %.*s\n", (int)item.payment_id_count, item.payment_id); } @@ -517,12 +517,14 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"payment_provider", SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY}, {"auto_renewing", false}, - {"unredeemed_ts", unix_ts - 3600}, + // purchased_ts/revoked_ts are floats on the wire (sub-second + // precision); libsession keeps them as millisecond-precise sys_ms. + {"purchased_ts", unix_ts - 3600 + 0.5}, {"redeemed_ts", unix_ts - 3600}, {"expiry_ts", unix_ts}, {"grace_period_duration", 1001}, {"platform_refund_expiry_ts", unix_ts + 1}, - {"revoked_ts", unix_ts + 3600}, + {"revoked_ts", unix_ts + 3600 + 0.75}, {"refund_requested_ts", unix_ts + 3601}, {"payment_id", std::string( @@ -557,12 +559,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { result.items[0].payment_provider, result.items[0].payment_provider_count) == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY); - REQUIRE(result.items[0].unredeemed_ts == unix_ts - 3600); + // Sub-second precision preserved through sys_ms (0.5s = 500ms) round-trips exactly + REQUIRE(result.items[0].purchased_ts == unix_ts - 3600 + 0.5); REQUIRE(result.items[0].redeemed_ts == unix_ts - 3600); REQUIRE(result.items[0].expiry_ts == unix_ts); REQUIRE(result.items[0].grace_period_duration == 1001); REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); - REQUIRE(result.items[0].revoked_ts == unix_ts + 3600); + REQUIRE(result.items[0].revoked_ts == unix_ts + 3600 + 0.75); // 750ms preserved REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); REQUIRE(result.items[0].payment_id_count == payment_tx.payment_id_count); REQUIRE(std::memcmp( From 96e14ca5c64a265936d557ce2c3be4c3fb6bccdd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 14:18:36 -0300 Subject: [PATCH 107/193] Remove wire version: gone from requests, personalisation-encoded for the proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per pro-wire-protocol.md Delta #11, the in-digest version byte and the JSON `version` field are removed everywhere except the proof carries its version. - Requests (add-payment, generate-proof, get-details, set-refund, get-revocations): no version byte in the signed hash, no `version` in the body; the library-owned per-request version constants are deleted. A new request shape gets a new endpoint. - The proof keeps a plaintext `version` field — it is a free-floating, offline-verified credential with no endpoint. It is no longer hashed as a byte; instead it selects the personalisation (`ProProof________` -> `ProProof_v0_____`), and that choice is what binds the version into the signature. proof_hash_internal no longer takes/hashes it. - SetPaymentRefundRequestedResponse loses its `version` field (C++ + C). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 1 - include/session/pro_backend.hpp | 3 --- include/session/session_protocol.hpp | 2 +- src/pro_backend.cpp | 29 +++------------------------- src/session_protocol.cpp | 11 +++++------ tests/test_pro_backend.cpp | 3 --- 6 files changed, 9 insertions(+), 40 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 4db071865..c288026fe 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -268,7 +268,6 @@ typedef struct session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response; struct session_pro_backend_set_payment_refund_requested_response { session_pro_backend_response_header header; - uint8_t version; bool updated; }; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 5d63db24c..e62063adb 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -409,9 +409,6 @@ ProRequest refund_request( std::span payment_id); struct SetPaymentRefundRequestedResponse : Response { - /// Version from the request - std::uint8_t version; - /// True if a payment was found matching the given payment information and that the refund /// request unix timestamp was set bool updated; diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index b92a00969..6f4c16574 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -63,7 +63,7 @@ enum ProProofVersion { ProProofVersion_v0 }; /// Session Pro personalisation bytes for hashing; must match the personalisation strings in /// pro-wire-protocol.md §2 (proof) and §3 (signed requests). inline constexpr auto GENERATE_PROOF_PERS = "ProGenerateProof"_b2b_pers; -inline constexpr auto BUILD_PROOF_PERS = "ProProof________"_b2b_pers; +inline constexpr auto BUILD_PROOF_PERS = "ProProof_v0_____"_b2b_pers; inline constexpr auto ADD_PRO_PAYMENT_PERS = "ProAddPayment___"_b2b_pers; inline constexpr auto SET_PAYMENT_REFUND_REQUESTED_PERS = "ProSetRefundReq_"_b2b_pers; inline constexpr auto GET_PRO_DETAILS_PERS = "ProGetProDetReq_"_b2b_pers; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index c0eb2b01b..785a57877 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -154,14 +154,6 @@ namespace { constexpr char get_pro_revocations_endpoint[] = "get_pro_revocations"; constexpr char set_refund_endpoint[] = "set_payment_refund_requested"; - // Wire-format version emitted per request type (library-owned; not caller-settable). Kept - // independent so a future bump of one request type doesn't drag the others along. - constexpr uint8_t add_payment_version = 0; - constexpr uint8_t generate_proof_version = 0; - constexpr uint8_t get_pro_details_version = 0; - constexpr uint8_t get_pro_revocations_version = 0; - constexpr uint8_t set_refund_version = 0; - // --- add-payment (endpoint add_pro_payment) --- b32 add_payment_hash( @@ -171,12 +163,7 @@ namespace { std::span payment_id) { // Must match the add-payment signed-request hash in pro-wire-protocol.md §3.2 (+ §3.5). return hash::blake2b_pers<32>( - ADD_PRO_PAYMENT_PERS, - add_payment_version, - master_pubkey, - rotating_pubkey, - provider_code, - payment_id); + ADD_PRO_PAYMENT_PERS, master_pubkey, rotating_pubkey, provider_code, payment_id); } // Serialise an add-payment request body from already-computed fields (shared by the C++ @@ -189,7 +176,6 @@ namespace { std::span master_sig, std::span rotating_sig) { nlohmann::json j; - j["version"] = add_payment_version; j["master_pkey"] = oxenc::to_hex(master_pubkey); j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); j["payment_tx"]["provider"] = provider_code; @@ -335,8 +321,7 @@ namespace { std::chrono::sys_seconds unix_ts) { // Must match the generate-proof signed-request hash in pro-wire-protocol.md §3.1. uint64_t ts = epoch_seconds(unix_ts); - return hash::blake2b_pers<32>( - GENERATE_PROOF_PERS, generate_proof_version, master_pubkey, rotating_pubkey, ts); + return hash::blake2b_pers<32>(GENERATE_PROOF_PERS, master_pubkey, rotating_pubkey, ts); } std::string generate_proof_body( @@ -346,7 +331,6 @@ namespace { std::span master_sig, std::span rotating_sig) { nlohmann::json j; - j["version"] = generate_proof_version; j["master_pkey"] = oxenc::to_hex(master_pubkey); j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); j["ts"] = epoch_seconds(unix_ts); @@ -384,7 +368,6 @@ ProRequest pro_proof_request( ProRequest revocations_request(std::int64_t ticket) { nlohmann::json j; - j["version"] = get_pro_revocations_version; j["ticket"] = ticket; return {get_pro_revocations_endpoint, j.dump()}; } @@ -454,8 +437,7 @@ namespace { uint32_t count) { // Must match the get-pro-details signed-request hash in pro-wire-protocol.md §3.4. uint64_t ts = epoch_seconds(unix_ts); - return hash::blake2b_pers<32>( - GET_PRO_DETAILS_PERS, get_pro_details_version, master_pubkey, ts, count); + return hash::blake2b_pers<32>(GET_PRO_DETAILS_PERS, master_pubkey, ts, count); } std::string payment_details_body( @@ -464,7 +446,6 @@ namespace { std::chrono::sys_seconds unix_ts, uint32_t count) { nlohmann::json j; - j["version"] = get_pro_details_version; j["master_pkey"] = oxenc::to_hex(master_pubkey); j["master_sig"] = oxenc::to_hex(master_sig); j["ts"] = epoch_seconds(unix_ts); @@ -620,7 +601,6 @@ namespace { uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); return hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, - set_refund_version, master_pubkey, ts, refund_requested_ts, @@ -636,7 +616,6 @@ namespace { std::string_view payment_id, std::span master_sig) { nlohmann::json j; - j["version"] = set_refund_version; j["master_pkey"] = oxenc::to_hex(master_pubkey); j["ts"] = epoch_seconds(unix_ts); j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); @@ -698,7 +677,6 @@ SetPaymentRefundRequestedResponse parse_refund(std::string_view json) { return result; // Parse payload - result.version = json_require(result_obj, "version", result.errors); result.updated = json_require(result_obj, "updated", result.errors); return result; } @@ -1165,7 +1143,6 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json // Copy to C struct result.header.status = cpp.status; - result.version = cpp.version; result.updated = cpp.updated; // Copy errors diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 56ce2a532..2b229212d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -51,14 +51,15 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { namespace { session::b32 proof_hash_internal( - std::uint8_t version, std::span revocation_tag, std::span rotating_pubkey, std::uint64_t expiry_ts) { - // This must match the Pro proof signed digest in pro-wire-protocol.md §2. + // This must match the Pro proof signed digest in pro-wire-protocol.md §2. The proof version is + // NOT hashed as a byte; it selects the personalisation (v0 -> "ProProof_v0_____"), and that + // choice of personalisation is what binds the version into the signature. return session::hash::blake2b_pers<32>( - session::BUILD_PROOF_PERS, version, revocation_tag, rotating_pubkey, expiry_ts); + session::BUILD_PROOF_PERS, revocation_tag, rotating_pubkey, expiry_ts); } struct array_uc32_from_ptr_result { @@ -151,7 +152,7 @@ ProStatus ProProof::status( b32 ProProof::hash() const { b32 result = proof_hash_internal( - version, revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); + revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); return result; } @@ -885,7 +886,6 @@ LIBSESSION_C_API void session_protocol_pro_message_bitset_unset( LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { cbytes32 result = {}; session::b32 hash = proof_hash_internal( - proof->version, to_byte_span(proof->revocation_tag.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_ts); @@ -900,7 +900,6 @@ LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( if (verify_pubkey_len != 32) return false; session::b32 hash = proof_hash_internal( - proof->version, to_byte_span(proof->revocation_tag.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_ts); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 07e873308..7d5934679 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -703,7 +703,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { nlohmann::json j; j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; j["result"]["updated"] = true; - j["result"]["version"] = 0; std::string json = j.dump(); // Valid JSON @@ -719,7 +718,6 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(result.updated); - REQUIRE(result.version == 0); } // After freeing @@ -1186,7 +1184,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify the response REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); - REQUIRE(response.version == 0); REQUIRE(response.updated); } } From 1877db3913ba595fc546166c47bedee43385cdfd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 23:18:22 -0300 Subject: [PATCH 108/193] Deduplicate and simplify session_protocol.cpp internal helpers Three behaviour-preserving cleanups of the file's internal (anonymous-namespace / static) helpers. No public API or wire-format change. - Extract parse_pro_message(): the ~40-line block that validates a protobuf ProProof and fills a DecodedPro (proof fields + feature flags) was duplicated nearly verbatim in parse_content_and_pro() and decode_for_community(). Both now call the shared helper and keep only their (genuinely different) status evaluation. This also drops a redundant memcpy of the pro signature that the envelope path had already copied a few lines earlier. - Extract parse_common_envelope_fields(): the optional source-device and server-timestamp parsing was identical in parse_envelope_fields() and the community parser; unify it. Source parsing stays per-path (DMs/groups carry a hex-encoded source, community carries raw bytes), as does the timestamp / pro-signature handling that only one path does. - Replace array_uc32_from_ptr() and its bespoke `{bool success; b32 data;}` result struct with `std::optional optional_uc32_from_ptr()`: nullopt signals the wrong-length error and a zero-filled value the "not provided" case. The two C decode entry points drop the `.success` / `.data` plumbing in favour of `if (!x)` / `*x`. The error strings thrown for a missing or malformed pro proof are now shared and slightly reworded (they previously differed between the two call paths); no test depends on the exact text. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/session_protocol.cpp | 195 +++++++++++++++------------------------ 1 file changed, 75 insertions(+), 120 deletions(-) diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index afa441b62..53276c25d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -62,20 +62,17 @@ session::b32 proof_hash_internal( session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); } -struct array_uc32_from_ptr_result { - bool success; - session::b32 data; -}; - -static array_uc32_from_ptr_result array_uc32_from_ptr(const void* ptr, size_t len) { - array_uc32_from_ptr_result result = {}; +// Copies an optional 32-byte value out of a possibly-null C pointer. A null pointer yields a +// zero-filled value (the "not provided" case); a non-null pointer with the wrong length yields +// nullopt to signal an error. +static std::optional optional_uc32_from_ptr(const void* ptr, size_t len) { + session::b32 out = {}; if (ptr) { - if (len != result.data.max_size()) - return result; - std::memcpy(result.data.data(), ptr, len); + if (len != out.max_size()) + return std::nullopt; + std::memcpy(out.data(), ptr, len); } - result.success = true; - return result; + return out; } static session_protocol_envelope envelope_from_cpp(const session::Envelope& cpp) { @@ -384,6 +381,19 @@ std::vector encode_for_group( /*padding*/ 256); } +// Parses the optional envelope metadata fields that are encoded identically for every message type +// (source device and server timestamp) into an Envelope. +static void parse_common_envelope_fields(Envelope& env, const SessionProtos::Envelope& pb) { + if (pb.has_sourcedevice()) { + env.source_device = pb.sourcedevice(); + env.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SOURCE_DEVICE; + } + if (pb.has_servertimestamp()) { + env.server_timestamp = pb.servertimestamp(); + env.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SERVER_TIMESTAMP; + } +} + // Shared helper 1: parses envelope metadata fields (timestamp, source, etc.) from an // already-parsed Envelope protobuf. static void parse_envelope_fields( @@ -429,17 +439,44 @@ static void parse_envelope_fields( } } - // Parse source device (optional) - if (envelope.has_sourcedevice()) { - result.envelope.source_device = envelope.sourcedevice(); - result.envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SOURCE_DEVICE; - } + parse_common_envelope_fields(result.envelope, envelope); +} - // Parse server timestamp (optional) - if (envelope.has_servertimestamp()) { - result.envelope.server_timestamp = envelope.servertimestamp(); - result.envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SERVER_TIMESTAMP; - } +// Parses and validates the proof and feature flags embedded in a protobuf ProMessage into a +// DecodedPro. Throws if the proof is missing or malformed. The caller is responsible for evaluating +// the resulting proof's `.status`. +static DecodedPro parse_pro_message(const SessionProtos::ProMessage& pro_msg) { + DecodedPro pro = {}; + if (!pro_msg.has_proof()) + throw std::runtime_error{"Parse failed, pro config missing proof"}; + + const SessionProtos::ProProof& proto_proof = pro_msg.proof(); + ProProof& proof = pro.proof; + bool valid = proto_proof.has_version() && + proto_proof.version() == static_cast(ProProofVersion_v0) && + proto_proof.has_genindexhash() && + proto_proof.genindexhash().size() == proof.gen_index_hash.max_size() && + proto_proof.has_rotatingpublickey() && + proto_proof.rotatingpublickey().size() == proof.rotating_pubkey.max_size() && + proto_proof.has_expiryunixts() && proto_proof.has_sig() && + proto_proof.sig().size() == proof.sig.max_size(); + if (!valid) + throw std::runtime_error{"Parse failed, pro metadata was malformed"}; + + pro.msg_flags = static_cast(pro_msg.msgbitset()); + pro.profile_flags = static_cast(pro_msg.profilebitset()); + std::memcpy( + proof.gen_index_hash.data(), + proto_proof.genindexhash().data(), + proto_proof.genindexhash().size()); + std::memcpy( + proof.rotating_pubkey.data(), + proto_proof.rotatingpublickey().data(), + proto_proof.rotatingpublickey().size()); + proof.expiry_unix_ts = std::chrono::sys_time( + std::chrono::milliseconds(proto_proof.expiryunixts())); + std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); + return pro; } // Shared helper 2: parses Content protobuf from result.content_plaintext (which must already be @@ -488,45 +525,7 @@ static void parse_content_and_pro( // Mark the envelope as having a pro signature that the caller can use. result.envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG; - DecodedPro& pro = result.pro.emplace(); - - // Extract the pro message - const SessionProtos::ProMessage& pro_msg = content.promessage(); - if (!pro_msg.has_proof()) - throw std::runtime_error( - "Parse decrypted message failed, pro config missing proof"); - - // Parse the proof from protobufs - const SessionProtos::ProProof& proto_proof = pro_msg.proof(); - session::ProProof& proof = pro.proof; - // clang-format off - size_t proof_errors = 0; - proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.gen_index_hash.max_size(); - proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); - proof_errors += !proto_proof.has_expiryunixts(); - proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); - // clang-format on - if (proof_errors) - throw std::runtime_error( - "Parse decrypted message failed, pro metadata was malformed"); - - // Fill out the resulting proof structure, we have parsed successfully - pro.msg_flags = static_cast(pro_msg.msgbitset()); - pro.profile_flags = static_cast(pro_msg.profilebitset()); - std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); - - std::memcpy( - proof.gen_index_hash.data(), - proto_proof.genindexhash().data(), - proto_proof.genindexhash().size()); - std::memcpy( - proof.rotating_pubkey.data(), - proto_proof.rotatingpublickey().data(), - proto_proof.rotatingpublickey().size()); - proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(proto_proof.expiryunixts())); - std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); + DecodedPro& pro = result.pro.emplace(parse_pro_message(content.promessage())); // Evaluate the pro status given the extracted components (was it signed, is it expired, // was the message signed validly?) @@ -539,7 +538,7 @@ static void parse_content_and_pro( // bytes, this means that we expected the user to have signed the padding as well. auto unix_ts = std::chrono::sys_time( std::chrono::milliseconds(content.sigtimestamp())); - pro.status = proof.status(pro_backend_pubkey, unix_ts, signed_msg); + pro.status = pro.proof.status(pro_backend_pubkey, unix_ts, signed_msg); } } } @@ -669,17 +668,7 @@ DecodedCommunityMessage decode_for_community( envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SOURCE; } - // Parse source device (optional) - if (pb_envelope.has_sourcedevice()) { - envelope.source_device = pb_envelope.sourcedevice(); - envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SOURCE_DEVICE; - } - - // Parse server timestamp (optional) - if (pb_envelope.has_servertimestamp()) { - envelope.server_timestamp = pb_envelope.servertimestamp(); - envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_SERVER_TIMESTAMP; - } + parse_common_envelope_fields(envelope, pb_envelope); // Parse pro signature (optional) if (pb_envelope.has_prosig()) { @@ -731,41 +720,7 @@ DecodedCommunityMessage decode_for_community( } if (result.pro_sig && content.has_promessage()) { - // Extract the pro message - DecodedPro& pro = result.pro.emplace(); - const SessionProtos::ProMessage& pro_msg = content.promessage(); - if (!pro_msg.has_proof()) - throw std::runtime_error("Decoding community message failed, pro config missing proof"); - - // Parse the proof from protobufs - const SessionProtos::ProProof& proto_proof = pro_msg.proof(); - session::ProProof& proof = pro.proof; - // clang-format off - size_t proof_errors = 0; - proof_errors += !proto_proof.has_version() || proto_proof.version() != static_cast(session::ProProofVersion_v0); - proof_errors += !proto_proof.has_genindexhash() || proto_proof.genindexhash().size() != proof.gen_index_hash.max_size(); - proof_errors += !proto_proof.has_rotatingpublickey() || proto_proof.rotatingpublickey().size() != proof.rotating_pubkey.max_size(); - proof_errors += !proto_proof.has_expiryunixts(); - proof_errors += !proto_proof.has_sig() || proto_proof.sig().size() != proof.sig.max_size(); - // clang-format on - if (proof_errors) - throw std::runtime_error( - "Decoding community message failed, pro metadata was malformed"); - - // Fill out the resulting proof structure, we have parsed successfully - pro.msg_flags = static_cast(pro_msg.msgbitset()); - pro.profile_flags = static_cast(pro_msg.profilebitset()); - std::memcpy( - proof.gen_index_hash.data(), - proto_proof.genindexhash().data(), - proto_proof.genindexhash().size()); - std::memcpy( - proof.rotating_pubkey.data(), - proto_proof.rotatingpublickey().data(), - proto_proof.rotatingpublickey().size()); - proof.expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(proto_proof.expiryunixts())); - std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); + DecodedPro& pro = result.pro.emplace(parse_pro_message(content.promessage())); // Evaluate the pro status given the extracted components (was it signed, is it expired, // was the message signed validly?) @@ -778,7 +733,7 @@ DecodedCommunityMessage decode_for_community( // Entering the `pro_sig` and `result.envelope` branch means that the envelope must have // a pro signature. assert(result.envelope->flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG); - pro.status = proof.status( + pro.status = pro.proof.status( pro_backend_pubkey, unix_ts, ProSignedMessage{*result.pro_sig, result.content_plaintext}); @@ -794,7 +749,7 @@ DecodedCommunityMessage decode_for_community( std::vector content_copy_without_sig_payload = pad_message(to_span(content_copy_without_sig.SerializeAsString())); - pro.status = proof.status( + pro.status = pro.proof.status( pro_backend_pubkey, unix_ts, ProSignedMessage{*result.pro_sig, to_span(content_copy_without_sig_payload)}); @@ -1060,9 +1015,9 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( session_protocol_decoded_envelope result = {}; // Setup the pro backend pubkey - array_uc32_from_ptr_result pro_backend_pubkey_cpp = - array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); - if (!pro_backend_pubkey_cpp.success) { + auto pro_backend_pubkey_cpp = + optional_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); + if (!pro_backend_pubkey_cpp) { result.error_len_incl_null_terminator = format_c_str( error, error_len, @@ -1090,8 +1045,8 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( } try { - result_cpp = decode_group_envelope( - group_keys, group_pk, payload, pro_backend_pubkey_cpp.data); + result_cpp = + decode_group_envelope(group_keys, group_pk, payload, *pro_backend_pubkey_cpp); result.success = true; } catch (const std::exception& e) { result.error_len_incl_null_terminator = format_c_str(error, error_len, "{}", e.what()); @@ -1109,7 +1064,7 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( try { ed25519::PrivKeySpan privkey{ keys->decrypt_keys[index].data, keys->decrypt_keys[index].size}; - result_cpp = decode_dm_envelope(privkey, payload, pro_backend_pubkey_cpp.data); + result_cpp = decode_dm_envelope(privkey, payload, *pro_backend_pubkey_cpp); result.success = true; break; } catch (const std::exception& e) { @@ -1179,9 +1134,9 @@ session_protocol_decoded_community_message session_protocol_decode_for_community content_or_envelope_payload_len}; auto unix_ts = std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); - array_uc32_from_ptr_result pro_backend_pubkey_cpp = - array_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); - if (!pro_backend_pubkey_cpp.success) { + auto pro_backend_pubkey_cpp = + optional_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); + if (!pro_backend_pubkey_cpp) { result.error_len_incl_null_terminator = format_c_str( error, error_len, @@ -1192,7 +1147,7 @@ session_protocol_decoded_community_message session_protocol_decode_for_community try { DecodedCommunityMessage decoded = decode_for_community( - content_or_envelope_payload_span, unix_ts, pro_backend_pubkey_cpp.data); + content_or_envelope_payload_span, unix_ts, *pro_backend_pubkey_cpp); result.has_envelope = decoded.envelope.has_value(); if (result.has_envelope) result.envelope = envelope_from_cpp(*decoded.envelope); From 89047453f8d2b8e49615ad6a87b2536470013ae8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 23:23:09 -0300 Subject: [PATCH 109/193] Make the C session_protocol_pro_proof_status delegate to ProProof::status session_protocol_pro_proof_status() reimplemented the same backend-signature -> user-signature -> expiry sequence as the C++ ProProof::status(), so a fix or change to one could silently fail to reach the other. Route the C entry point through the C++ implementation so the evaluation logic lives in one place. - Add proof_from_c(): builds a session::ProProof from the C proof struct (the inverse of the proof half of decoded_pro_from_cpp). - session_protocol_pro_proof_status() now constructs a ProProof and calls ProProof::status(), keeping only the C-API-specific buffer-length validation that the C++ API instead expresses via fixed-extent spans: * verify_pubkey_len != 32 -> INVALID_PRO_BACKEND_SIG (returned first, since the backend signature is the first thing evaluated); * a signed message whose signature is not 64 bytes -> INVALID_USER_SIG, applied after delegation so it supersedes a valid or expired result but not a failed backend signature -- preserving the original backend -> user -> expiry ordering. Behaviour-preserving. Note: session_protocol_pro_proof_status() has no direct test coverage (the decode paths exercise ProProof::status() directly), so the ordering above is matched by construction rather than verified by a test. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/session_protocol.cpp | 57 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 53276c25d..acc59aa0f 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -104,6 +104,20 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP result.profile_bitset = static_cast(cpp.profile_flags); return result; } + +// Builds a C++ ProProof from the C proof struct (the inverse of the proof half of +// decoded_pro_from_cpp). +static session::ProProof proof_from_c(const session_protocol_pro_proof& c) { + session::ProProof proof = {}; + proof.version = c.version; + std::memcpy( + proof.gen_index_hash.data(), c.gen_index_hash.data, proof.gen_index_hash.max_size()); + std::memcpy( + proof.rotating_pubkey.data(), c.rotating_pubkey.data, proof.rotating_pubkey.max_size()); + proof.expiry_unix_ts = session::from_epoch_ms(c.expiry_unix_ts_ms); + std::memcpy(proof.sig.data(), c.sig.data, proof.sig.max_size()); + return proof; +} } // namespace namespace session { @@ -835,26 +849,33 @@ LIBSESSION_C_API SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( size_t verify_pubkey_len, uint64_t unix_ts_ms, const session_protocol_pro_signed_message* signed_msg) { - SESSION_PROTOCOL_PRO_STATUS result = SESSION_PROTOCOL_PRO_STATUS_VALID; - if (!session_protocol_pro_proof_verify_signature(proof, verify_pubkey, verify_pubkey_len)) - result = SESSION_PROTOCOL_PRO_STATUS_INVALID_PRO_BACKEND_SIG; - - // Check if the message was signed if the user passed one in to verify against - if (result == SESSION_PROTOCOL_PRO_STATUS_VALID && signed_msg) { - if (!session_protocol_pro_proof_verify_message( - proof, - signed_msg->sig.data, - signed_msg->sig.size, - signed_msg->msg.data, - signed_msg->msg.size)) - result = SESSION_PROTOCOL_PRO_STATUS_INVALID_USER_SIG; + // ProProof::status is the single source of truth for the backend-sig -> user-sig -> expiry + // evaluation. The C API additionally validates the caller's buffer lengths (which the C++ API + // encodes as fixed-size spans), so handle those here and delegate the rest. + if (verify_pubkey_len != 32) + return SESSION_PROTOCOL_PRO_STATUS_INVALID_PRO_BACKEND_SIG; + + std::optional cpp_signed_msg; + bool bad_user_sig = false; + if (signed_msg) { + if (signed_msg->sig.size == 64) + cpp_signed_msg = ProSignedMessage{ + to_byte_span<64>(signed_msg->sig.data), + to_byte_span(signed_msg->msg.data, signed_msg->msg.size)}; + else + bad_user_sig = true; // a wrong-length signature can never verify } - // Check if the proof has expired - if (result == SESSION_PROTOCOL_PRO_STATUS_VALID && - !session_protocol_pro_proof_is_active(proof, unix_ts_ms)) - result = SESSION_PROTOCOL_PRO_STATUS_EXPIRED; - return result; + ProStatus status = proof_from_c(*proof).status( + to_byte_span<32>(verify_pubkey), from_epoch_ms(unix_ts_ms), cpp_signed_msg); + + // ProProof::status can't see a wrong-length signature (it takes a fixed-size span), so surface + // the C API's length check here while keeping the ordering: a bad user signature supersedes a + // valid or expired result, but not a failed backend signature. + if (bad_user_sig && status != ProStatus::InvalidProBackendSig) + status = ProStatus::InvalidUserSig; + + return static_cast(status); } LIBSESSION_C_API From f7658fc47244d4adb27bb9cef2b15c345ae6f4d1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 15:54:09 -0300 Subject: [PATCH 110/193] tests: purge arcane fprintf debug output in favour of Catch2 INFO Delete the three dead [[maybe_unused]] dump_pro_* stderr helpers (several with malformed format specifiers, e.g. "%" PRId64 "zu"), and convert the remaining fprintf(stderr, "ERROR: ...") diagnostics to Catch2 INFO/UNSCOPED_INFO so they surface through the test framework instead of raw stderr. Per-error messages use UNSCOPED_INFO so they survive to the post-loop assertion. Drops the now-unused include. No test logic change. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_pro_backend.cpp | 63 +++++--------------------------------- 1 file changed, 8 insertions(+), 55 deletions(-) diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 7d5934679..607d871b5 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -18,49 +17,6 @@ extern std::string g_test_pro_backend_dev_server_url; static bool string8_equals(string8 s8, std::string_view str) { return s8.size == str.size() && std::memcmp(s8.data, str.data(), s8.size) == 0; } -[[maybe_unused]] static void dump_pro_proof_to_stderr(const session_protocol_pro_proof& proof) { - fprintf(stderr, "proof.version: %u\n", proof.version); - fprintf(stderr, - "proof.revocation_tag: %s\n", - oxenc::to_hex(proof.revocation_tag.data, std::end(proof.revocation_tag.data)).c_str()); - fprintf(stderr, - "proof.rotating_pubkey: %s\n", - oxenc::to_hex(proof.rotating_pubkey.data, std::end(proof.rotating_pubkey.data)) - .c_str()); - fprintf(stderr, "proof.expiry_ts: %" PRId64 "\n", proof.expiry_ts); - fprintf(stderr, - "proof.sig: %s\n", - oxenc::to_hex(proof.sig.data, std::end(proof.sig.data)).c_str()); -} - -[[maybe_unused]] static void dump_pro_payment_item( - const session_pro_backend_pro_payment_item& item) { - fprintf(stderr, "item.status: %d\n", item.status); - fprintf(stderr, "item.plan: %.*s\n", (int)item.plan_count, item.plan); - fprintf(stderr, - "item.payment_provider: %.*s\n", - (int)item.payment_provider_count, - item.payment_provider); - fprintf(stderr, "item.auto_renewing: %d\n", item.auto_renewing); - fprintf(stderr, "item.purchased_ts: %f\n", item.purchased_ts); - fprintf(stderr, "item.redeemed_ts: %" PRId64 "zu\n", item.redeemed_ts); - fprintf(stderr, "item.expiry_ts: %" PRId64 "\n", item.expiry_ts); - fprintf(stderr, "item.grace_period_duration: %" PRId64 "zu\n", item.grace_period_duration); - fprintf(stderr, - "item.platform_refund_expiry_ts: %" PRId64 "zu\n", - item.platform_refund_expiry_ts); - fprintf(stderr, "item.revoked_ts: %f\n", item.revoked_ts); - fprintf(stderr, "item.payment_id: %.*s\n", (int)item.payment_id_count, item.payment_id); -} - -[[maybe_unused]] static void dump_pro_revocation( - const session_pro_backend_pro_revocation_item& item) { - fprintf(stderr, "item.effective_ts: %" PRId64 "\n", item.effective_ts); - fprintf(stderr, - "item.revocation_tag: %s\n", - oxenc::to_hex(item.revocation_tag.data, std::end(item.revocation_tag.data)).c_str()); -} - TEST_CASE("Pro Backend C API", "[pro_backend]") { // Setup: Generate keys and payment token hash cbytes32 master_pubkey = {}; @@ -920,11 +876,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { response_json.data(), response_json.size()); scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; + INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { - if (index == 0) - fprintf(stderr, "ERROR: JSON response: %s\n", response_json.c_str()); string8 error = response.header.errors[index]; - fprintf(stderr, "ERROR: %s\n", error.data); + UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); @@ -974,11 +929,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; // Verify the response + INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { - if (index == 0) - fprintf(stderr, "ERROR: JSON response: %s\n", response_json.c_str()); string8 error = response.header.errors[index]; - fprintf(stderr, "ERROR: %s\n", error.data); + UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); @@ -1016,11 +970,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { scope_exit response_free{ [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; + INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { - if (index == 0) - fprintf(stderr, "ERROR: JSON response: %s\n", response_json.c_str()); string8 error = response.header.errors[index]; - fprintf(stderr, "ERROR: %s\n", error.data); + UNSCOPED_INFO("ERROR: " << error.data); } // Verify the response @@ -1124,7 +1077,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { string8 error = response.header.errors[index]; - fprintf(stderr, "ERROR: %s\n", error.data); + UNSCOPED_INFO("ERROR: " << error.data); } // Verify the response @@ -1178,7 +1131,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { string8 error = response.header.errors[index]; - fprintf(stderr, "ERROR: %s\n", error.data); + UNSCOPED_INFO("ERROR: " << error.data); } // Verify the response From c82f716bcabc9ae1c391f1fb3ae7db4d1f9256d6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 21:01:19 -0300 Subject: [PATCH 111/193] pro_backend: build request bodies with a single nlohmann literal Replace the incremental `nlohmann::json j; j["k"] = v; ...` pattern in the four *_body helpers with a single braced json literal returned directly (`return nlohmann::json{...}.dump();`, no named temporary). Cosmetic only: nlohmann sorts object keys on dump, so the serialized bodies are byte-identical (the C-vs-C++ request cross-check tests confirm), and the C _request_to_json wrappers call the same helpers. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pro_backend.cpp | 56 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 785a57877..9ccceebef 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -175,14 +175,13 @@ namespace { std::string_view payment_id, std::span master_sig, std::span rotating_sig) { - nlohmann::json j; - j["master_pkey"] = oxenc::to_hex(master_pubkey); - j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); - j["payment_tx"]["provider"] = provider_code; - j["payment_tx"]["payment_id"] = payment_id; - j["master_sig"] = oxenc::to_hex(master_sig); - j["rotating_sig"] = oxenc::to_hex(rotating_sig); - return j.dump(); + return nlohmann::json{ + {"master_pkey", oxenc::to_hex(master_pubkey)}, + {"rotating_pkey", oxenc::to_hex(rotating_pubkey)}, + {"payment_tx", {{"provider", provider_code}, {"payment_id", payment_id}}}, + {"master_sig", oxenc::to_hex(master_sig)}, + {"rotating_sig", oxenc::to_hex(rotating_sig)}} + .dump(); } } // namespace @@ -330,13 +329,13 @@ namespace { std::chrono::sys_seconds unix_ts, std::span master_sig, std::span rotating_sig) { - nlohmann::json j; - j["master_pkey"] = oxenc::to_hex(master_pubkey); - j["rotating_pkey"] = oxenc::to_hex(rotating_pubkey); - j["ts"] = epoch_seconds(unix_ts); - j["master_sig"] = oxenc::to_hex(master_sig); - j["rotating_sig"] = oxenc::to_hex(rotating_sig); - return j.dump(); + return nlohmann::json{ + {"master_pkey", oxenc::to_hex(master_pubkey)}, + {"rotating_pkey", oxenc::to_hex(rotating_pubkey)}, + {"ts", epoch_seconds(unix_ts)}, + {"master_sig", oxenc::to_hex(master_sig)}, + {"rotating_sig", oxenc::to_hex(rotating_sig)}} + .dump(); } } // namespace @@ -445,12 +444,12 @@ namespace { std::span master_sig, std::chrono::sys_seconds unix_ts, uint32_t count) { - nlohmann::json j; - j["master_pkey"] = oxenc::to_hex(master_pubkey); - j["master_sig"] = oxenc::to_hex(master_sig); - j["ts"] = epoch_seconds(unix_ts); - j["count"] = count; - return j.dump(); + return nlohmann::json{ + {"master_pkey", oxenc::to_hex(master_pubkey)}, + {"master_sig", oxenc::to_hex(master_sig)}, + {"ts", epoch_seconds(unix_ts)}, + {"count", count}} + .dump(); } } // namespace @@ -615,14 +614,13 @@ namespace { std::string_view provider_code, std::string_view payment_id, std::span master_sig) { - nlohmann::json j; - j["master_pkey"] = oxenc::to_hex(master_pubkey); - j["ts"] = epoch_seconds(unix_ts); - j["refund_requested_ts"] = epoch_seconds(refund_requested_unix_ts); - j["payment_tx"]["provider"] = provider_code; - j["payment_tx"]["payment_id"] = payment_id; - j["master_sig"] = oxenc::to_hex(master_sig); - return j.dump(); + return nlohmann::json{ + {"master_pkey", oxenc::to_hex(master_pubkey)}, + {"ts", epoch_seconds(unix_ts)}, + {"refund_requested_ts", epoch_seconds(refund_requested_unix_ts)}, + {"payment_tx", {{"provider", provider_code}, {"payment_id", payment_id}}}, + {"master_sig", oxenc::to_hex(master_sig)}} + .dump(); } } // namespace From 607834ad308b976e3a3ebbf5766b52cff1b2c091 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 22:20:13 -0300 Subject: [PATCH 112/193] pro: hash timestamps as int64_t, not uint64_t MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit epoch_seconds() returns int64_t, but the pfs request-hash helpers and proof_hash_internal stored it in uint64_t before folding it into the BLAKE2b digest. Use int64_t throughout, matching the signed-seconds convention (and the stable branch, which already does). Byte-identical: C++20 mandates two's complement and int64_t has no padding bits, so the little-endian serialization (oxenc reads/byteswaps the object representation) is the same bit pattern either way — signatures are unchanged. Purely a signedness/consistency fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pro_backend.cpp | 8 ++++---- src/session_protocol.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 9ccceebef..2b9c626b9 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -319,7 +319,7 @@ namespace { std::span rotating_pubkey, std::chrono::sys_seconds unix_ts) { // Must match the generate-proof signed-request hash in pro-wire-protocol.md §3.1. - uint64_t ts = epoch_seconds(unix_ts); + int64_t ts = epoch_seconds(unix_ts); return hash::blake2b_pers<32>(GENERATE_PROOF_PERS, master_pubkey, rotating_pubkey, ts); } @@ -435,7 +435,7 @@ namespace { std::chrono::sys_seconds unix_ts, uint32_t count) { // Must match the get-pro-details signed-request hash in pro-wire-protocol.md §3.4. - uint64_t ts = epoch_seconds(unix_ts); + int64_t ts = epoch_seconds(unix_ts); return hash::blake2b_pers<32>(GET_PRO_DETAILS_PERS, master_pubkey, ts, count); } @@ -596,8 +596,8 @@ namespace { std::span payment_id) { // Must match the set-payment-refund-requested signed-request hash in pro-wire-protocol.md // §3.3 (+ §3.5 for payment_id). - uint64_t ts = epoch_seconds(unix_ts); - uint64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); + int64_t ts = epoch_seconds(unix_ts); + int64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); return hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, master_pubkey, diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index a8fb76b4b..13eff6ad9 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -53,7 +53,7 @@ namespace { session::b32 proof_hash_internal( std::span revocation_tag, std::span rotating_pubkey, - std::uint64_t expiry_ts) { + std::int64_t expiry_ts) { // This must match the Pro proof signed digest in pro-wire-protocol.md §2. The proof version is // NOT hashed as a byte; it selects the personalisation (v0 -> "ProProof_v0_____"), and that From b0d11fc707ac46b0da62ec6d48cd1a260714870f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 22:58:17 -0300 Subject: [PATCH 113/193] pro_backend: parse payment/user status as opaque string codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per pro-wire-protocol.md §1 (Q13), the get-details payment-item `status` and the account `user_status` travel as stable string codes, not integers — as `payment_provider`/`plan` already do. libsession still parsed both as integers mapped to hand-synced C enums, which (a) was incompatible with the live backend (payment status has been a string on the wire) and (b) hard-failed the entire get-details parse on any unknown value. Parse both as opaque pass-through std::string (C: char[64] + _count), so unknown or future codes degrade gracefully instead of breaking old clients. Delete the SESSION_PRO_BACKEND_PAYMENT_STATUS and SESSION_PRO_BACKEND_USER_PRO_STATUS integer enums and their "must match backend" coupling. Known codes: payment status unredeemed/redeemed/expired/revoked; user_status never/active/expired. Backend shipped the matching change (session-pro-backend b3db4e3); in lockstep. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 29 +++++++---------------------- include/session/pro_backend.hpp | 4 ++-- src/pro_backend.cpp | 27 +++++++++------------------ tests/test_pro_backend.cpp | 13 +++++++------ 4 files changed, 25 insertions(+), 48 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index c288026fe..2e59d856e 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -61,26 +61,6 @@ struct session_pro_backend_provider_urls { LIBSESSION_EXPORT session_pro_backend_provider_urls session_pro_backend_get_provider_urls(const char* provider_code) NON_NULL_ARG(1); -/// Must match: -/// https://github.com/Doy-lee/session-pro-backend/blob/f4e2c84794470e7932ba1a1968fdb49117bb5870/backend.py#L18 -typedef enum SESSION_PRO_BACKEND_PAYMENT_STATUS { - SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL, - SESSION_PRO_BACKEND_PAYMENT_STATUS_UNREDEEMED, - SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED, - SESSION_PRO_BACKEND_PAYMENT_STATUS_EXPIRED, - SESSION_PRO_BACKEND_PAYMENT_STATUS_REVOKED, - SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT, -} SESSION_PRO_BACKEND_PAYMENT_STATUS; - -/// Must match: -/// https://github.com/Doy-lee/session-pro-backend/blob/a0e0ba24bc4ab3a062465d861aa57df2269b6dde/server.py#L373 -typedef enum SESSION_PRO_BACKEND_USER_PRO_STATUS { - SESSION_PRO_BACKEND_USER_PRO_STATUS_NEVER_BEEN_PRO, - SESSION_PRO_BACKEND_USER_PRO_STATUS_ACTIVE, - SESSION_PRO_BACKEND_USER_PRO_STATUS_EXPIRED, - SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT, -} SESSION_PRO_BACKEND_USER_PRO_STATUS; - typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS, SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR, @@ -212,7 +192,10 @@ struct session_pro_backend_get_pro_details_request { typedef struct session_pro_backend_pro_payment_item session_pro_backend_pro_payment_item; struct session_pro_backend_pro_payment_item { - SESSION_PRO_BACKEND_PAYMENT_STATUS status; + /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); + /// unknown values pass through as-is + char status[64]; + size_t status_count; /// Billing-period code (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for non-period plans char plan[64]; size_t plan_count; @@ -245,7 +228,9 @@ struct session_pro_backend_get_pro_details_response { /// Array of payment items, with items_count elements session_pro_backend_pro_payment_item* items; size_t items_count; - SESSION_PRO_BACKEND_USER_PRO_STATUS status; + /// Opaque account Pro status code ("never"/"active"/"expired"); unknown values pass through + char status[64]; + size_t status_count; SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; bool auto_renewing; int64_t expiry_ts; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index e62063adb..c09b7139d 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -270,7 +270,7 @@ struct ProPaymentItem { /// For example, a payment can be in a redeemed state whilst also have a refunded timestamp set /// if the payment was refunded and then the refund was reversed. We preserve all timestamps for /// book-keeping purposes. - SESSION_PRO_BACKEND_PAYMENT_STATUS status; + std::string status; /// Billing-period code that was purchased (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for /// non-period plans. The client maps/parses it for display. @@ -325,7 +325,7 @@ struct GetProDetailsResponse : Response { std::vector items; /// Current Session Pro entitlement status for the master public key - SESSION_PRO_BACKEND_USER_PRO_STATUS user_status; + std::string user_status; /// Error code that indicates that the Session Pro Backend encountered an error book-keeping /// Session Pro entitlement for the user. If this value is not `SUCCESS` implementing clients diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 2b9c626b9..95320f59a 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -491,19 +491,15 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { if (result.errors.size()) return result; - // Parse payload - uint32_t user_status = json_require(result_obj, "status", result.errors); - if (user_status >= SESSION_PRO_BACKEND_USER_PRO_STATUS_COUNT) { - result.errors.push_back( - fmt::format("User pro status value was out-of-bounds: {}", user_status)); - return result; - } - result.user_status = static_cast(user_status); + // Parse payload. The account Pro status is an opaque string code ("never"/"active"/"expired"); + // an unknown value passes through unchanged (§1: enums are codes) rather than failing the + // parse. + result.user_status = json_require(result_obj, "status", result.errors); uint32_t error_report = json_require(result_obj, "error_report", result.errors); if (error_report >= SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT) { result.errors.push_back( - fmt::format("Error report value was out-of-bounds: {}", user_status)); + fmt::format("Error report value was out-of-bounds: {}", error_report)); return result; } result.error_report = @@ -534,7 +530,7 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { // Parse payment item auto obj = it.get(); - auto status = json_require(obj, "status", result.errors); + auto status = json_require(obj, "status", result.errors); auto plan = json_require(obj, "plan", result.errors); auto payment_provider = json_require(obj, "payment_provider", result.errors); auto payment_id = json_require(obj, "payment_id", result.errors); @@ -553,12 +549,7 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { auto refund_requested_ts = json_require(obj, "refund_requested_ts", result.errors); ProPaymentItem item = {}; - if (status > SESSION_PRO_BACKEND_PAYMENT_STATUS_NIL && - status < SESSION_PRO_BACKEND_PAYMENT_STATUS_COUNT) { - item.status = static_cast(status); - } else { - result.errors.push_back(fmt::format("Status value was out-of-bounds: {}", status)); - } + item.status = std::move(status); // plan / payment_provider / payment_id are opaque strings; libsession does not validate or // interpret them (an unknown provider or plan code passes through as-is). @@ -1010,7 +1001,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. result.header.status = cpp.status; - result.status = cpp.user_status; + result.status_count = session::copy_c_str(result.status, cpp.user_status) - 1; result.error_report = cpp.error_report; result.items_count = cpp.items.size(); result.items = (session_pro_backend_pro_payment_item*)arena_alloc( @@ -1024,7 +1015,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json for (size_t index = 0; index < result.items_count; ++index) { const ProPaymentItem& src = cpp.items[index]; session_pro_backend_pro_payment_item& dest = result.items[index]; - dest.status = src.status; + dest.status_count = session::copy_c_str(dest.status, src.status) - 1; dest.plan_count = session::copy_c_str(dest.plan, src.plan) - 1; dest.payment_provider_count = session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 607d871b5..ba8bdf88c 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -458,7 +458,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { nlohmann::json j; j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; j["result"] = { - {"status", SESSION_PRO_BACKEND_USER_PRO_STATUS_EXPIRED}, + {"status", "expired"}, {"error_report", SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR}, {"auto_renewing", true}, @@ -468,7 +468,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"payments_total", 3}, {"items", nlohmann::json::array( - {{{"status", SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED}, + {{{"status", "redeemed"}, {"plan", "1m"}, {"payment_provider", SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY}, @@ -499,7 +499,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); - REQUIRE(result.status == SESSION_PRO_BACKEND_USER_PRO_STATUS_EXPIRED); + REQUIRE(std::string_view(result.status, result.status_count) == "expired"); REQUIRE(result.error_report == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR); REQUIRE(result.items_count == 1); @@ -509,7 +509,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.refund_requested_ts == unix_ts + 3602); REQUIRE(result.payments_total == 3); REQUIRE(result.items != nullptr); - REQUIRE(result.items[0].status == SESSION_PRO_BACKEND_PAYMENT_STATUS_REDEEMED); + REQUIRE(std::string_view(result.items[0].status, result.items[0].status_count) == + "redeemed"); REQUIRE(std::string_view(result.items[0].plan, result.items[0].plan_count) == "1m"); REQUIRE(std::string_view( result.items[0].payment_provider, @@ -936,7 +937,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { } REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); - REQUIRE(response.status == SESSION_PRO_BACKEND_USER_PRO_STATUS_ACTIVE); + REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count > 0); } @@ -979,7 +980,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify the response REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); - REQUIRE(response.status == SESSION_PRO_BACKEND_USER_PRO_STATUS_ACTIVE); + REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count == 0); } From 08228c006a5f7ee919d3731632ebed0f343449f8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 23:19:23 -0300 Subject: [PATCH 114/193] pro_backend: fill in the backend proof-signing pubkey Replace the all-zero PUBKEY placeholder with the Session Pro Backend's real Ed25519 public signing key (current test deployment, expected to carry through to production). Consumers verify a proof was issued by the backend by checking its signature against this key (ProProof::verify_signature). Trivially editable if the backend signing key ever changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.hpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index c09b7139d..2284cafd5 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -59,9 +59,11 @@ namespace session::pro_backend { -/// TODO: Assign the Session Pro backend public key for verifying proofs to allow users of the -/// library to have the pubkey available for verifying proofs. -constexpr auto PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000"_hex_b; +/// The Session Pro Backend's Ed25519 public key: verify that a proof was issued by the backend by +/// checking its signature against this key (see ProProof::verify_signature). This is the current +/// backend signing key (test deployment, expected to carry through to production). +constexpr auto PUBKEY = "479ffca8bcec7b4a0f0f7afe48b8a6d15635a8c7ff15ad16add05752c19414d4"_hex_b; +static_assert(PUBKEY.size() == 32); /// Domain used with ed25519::derive_subkey to derive the Session Pro signing keypair from the /// account's root Ed25519 seed. From ed1fe13defea613f6ac3f4810b65c24ae0d8ec3e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 23:55:07 -0300 Subject: [PATCH 115/193] pro_backend: add backend base URL + C export of the signing pubkey Add session::pro_backend::URL (= https://pro.session.codes) so clients read the canonical production base URL from libsession rather than hand-carrying it, and export the Ed25519 signing pubkey to the C API (SESSION_PRO_BACKEND_PUBKEY) for C consumers (iOS); C++ already has pro_backend::PUBKEY. Both C symbols point at the single C++ definitions (no duplicated literal/key). Clients may still override the URL for dev/test. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 9 +++++++++ include/session/pro_backend.hpp | 5 +++++ src/pro_backend.cpp | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 2e59d856e..a22b3500d 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -39,6 +39,15 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIO LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT; +/// The Session Pro Backend's production base URL (POST a request body to `/`). +/// Canonical production value; clients may override with a dev/test server. Points at the single +/// master string owned in the C++ implementation. +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_URL; + +/// The Session Pro Backend's Ed25519 public signing key, 32 bytes, for verifying a proof was issued +/// by the backend. Points at the same value as the C++ `session::pro_backend::PUBKEY`. +LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY; + /// Per-provider support/management URLs, keyed by provider code. These are identical for every user /// (not translation data), so libsession owns them as the single source of truth rather than each /// client duplicating them; the human-readable provider/store names are translation data and remain diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 2284cafd5..35eb25a5e 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -65,6 +65,11 @@ namespace session::pro_backend { constexpr auto PUBKEY = "479ffca8bcec7b4a0f0f7afe48b8a6d15635a8c7ff15ad16add05752c19414d4"_hex_b; static_assert(PUBKEY.size() == 32); +/// The Session Pro Backend's production base URL: POST a request body to `/` (see +/// the SESSION_PRO_BACKEND_*_ENDPOINT paths). This is the canonical production value; a client may +/// point at a different dev/test server if it chooses. +constexpr std::string_view URL = "https://pro.session.codes"; + /// Domain used with ed25519::derive_subkey to derive the Session Pro signing keypair from the /// account's root Ed25519 seed. constexpr auto pro_subkey_domain = "SessionProRandom"_bytes; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 95320f59a..533aaa3f5 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -199,6 +199,11 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIO get_pro_revocations_endpoint; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT = set_refund_endpoint; + +// Backend base URL + Ed25519 pubkey: C symbols pointing at the single C++ definitions above. +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_URL = URL.data(); +LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY = + reinterpret_cast(PUBKEY.data()); } std::optional provider_urls(std::string_view provider_code) { From 25ce70204f825f6a0f00c4a311017866d6ec5bfe Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 15:05:06 -0300 Subject: [PATCH 116/193] tests: pin blake2b_pers integer little-endian encoding + add big-endian audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hash::detail::make_hashable must serialize integer args as fixed-width little-endian (the byte encoding under every Session Pro signed digest). Add an [endian] known-answer test asserting an integer hashes identically to its explicit little-endian bytes — cheap always-on coverage of the LE path. make_hashable's big-endian byte-swap branch is if-constexpr-discarded on LE hosts (never compiled/tested — where a past bug hid). utils/test-bigendian.sh builds + runs the suite under an emulated s390x (qemu-user) so that branch, and other serialization, run on a real big-endian host. Manual/occasional, not CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_hash.cpp | 36 ++++++++++++++++++++++++++++++++++++ utils/test-bigendian.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100755 utils/test-bigendian.sh diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp index 9c250294d..9e02b45a3 100644 --- a/tests/test_hash.cpp +++ b/tests/test_hash.cpp @@ -238,3 +238,39 @@ TEST_CASE("SHA3-256 and SHAKE-256 known-answer tests", "[hash][sha3_256][shake25 shake256("abc"_bytes)(shake_out); CHECK(sha3_out != shake_out); } + +TEST_CASE("blake2b_pers integer args are little-endian", "[hash][blake2b][endian]") { + using session::hash::blake2b_pers; + + // make_hashable() must serialize integer arguments as fixed-width little-endian, independent of + // host endianness — the byte encoding underpinning every Session Pro signed digest. Pin the + // contract by requiring an integer to hash identically to its explicit little-endian bytes. On + // a little-endian host this guards the direct-reinterpret path; on a big-endian host (run via + // utils/test-bigendian.sh) it is the only thing exercising make_hashable's byte-swap branch. + constexpr auto pers = "EndianTestPers!!"_b2b_pers; + + { + uint16_t v = 0x0102; + std::array le{std::byte{0x02}, std::byte{0x01}}; + CHECK(blake2b_pers<32>(pers, v) == blake2b_pers<32>(pers, le)); + } + { + uint32_t v = 0x01020304; + std::array le{ + std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; + CHECK(blake2b_pers<32>(pers, v) == blake2b_pers<32>(pers, le)); + } + { + uint64_t v = 0x0102030405060708ULL; + std::array le{ + std::byte{0x08}, + std::byte{0x07}, + std::byte{0x06}, + std::byte{0x05}, + std::byte{0x04}, + std::byte{0x03}, + std::byte{0x02}, + std::byte{0x01}}; + CHECK(blake2b_pers<32>(pers, v) == blake2b_pers<32>(pers, le)); + } +} diff --git a/utils/test-bigendian.sh b/utils/test-bigendian.sh new file mode 100755 index 000000000..f31273af7 --- /dev/null +++ b/utils/test-bigendian.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# Occasional big-endian sanity audit for libsession-util. NOT wired into CI — run it by hand now and +# then (especially after touching hashing or any wire/config serialization). +# +# Most of libsession runs only on little-endian hosts, so endian-sensitive byte serialization — the +# Pro signed-digest encoding (hash::detail::make_hashable's byte-swap branch), config data, protobuf +# packing — normally never exercises its big-endian path. This script builds and runs the test suite +# inside an emulated big-endian target (s390x) so those paths run for real. In particular it exercises +# the [endian] known-answer test (hash::blake2b_pers integer args must be little-endian) on an actual +# big-endian machine, where make_hashable takes its otherwise-never-compiled swap branch. +# +# Cost: everything runs under qemu-user emulation, so the build is slow (tens of minutes) — the +# submodule-built networking stack (oxen-libquic / session-router) is the bulk of it. That's fine for +# an occasional manual audit; it is deliberately not in CI. +# +# Requires: Docker able to run foreign-arch images via qemu-user/binfmt. Run from the repo root with +# submodules checked out. +set -euo pipefail + +# Register qemu-user binfmt handlers for s390x (idempotent; needs --privileged once per boot). +docker run --privileged --rm tonistiigi/binfmt --install s390x + +# Build + test inside an emulated s390x Debian. BUILD_STATIC_DEPS=OFF pulls the heavy deps +# (libsodium, protobuf, curl, sqlite, zstd) as prebuilt s390x apt packages rather than compiling them +# from source under emulation. +docker run --platform=linux/s390x --rm -v "$PWD:/src" -w /src s390x/debian:bookworm bash -euxc ' + apt-get update + apt-get install -y --no-install-recommends \ + g++ cmake ninja-build pkg-config git ca-certificates \ + libsodium-dev libprotobuf-dev protobuf-compiler libcurl4-openssl-dev \ + libsqlite3-dev libzstd-dev + cmake -B build-bigendian -G Ninja -DBUILD_STATIC_DEPS=OFF -DCMAKE_BUILD_TYPE=Release + cmake --build build-bigendian --target testAll + # [endian] is the make_hashable little-endian known-answer test; the Pro / protocol / config tags + # also exercise serialization on this big-endian host. Drop the filter for a full run. + ./build-bigendian/tests/testAll "[endian],[hash],[pro_backend],[session_protocol],[config]" +' +echo "big-endian audit passed" From 1346490dbeab71d3d75bff6f0d8b2bea39807214 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 15:13:44 -0300 Subject: [PATCH 117/193] test-bigendian: use local qemu-user-static + debian:sid Drop the privileged tonistiigi/binfmt helper container in favour of a one-time host install of qemu-user-static + binfmt-support (registers the binfmt_misc handlers with the F flag, which is what makes them work inside containers). Guard on the handler being present. Also bump the base image bookworm -> sid so the audit runs against a modern toolchain representative of what the code targets. Co-Authored-By: Claude Opus 4.8 (1M context) --- utils/test-bigendian.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/utils/test-bigendian.sh b/utils/test-bigendian.sh index f31273af7..592a8281b 100755 --- a/utils/test-bigendian.sh +++ b/utils/test-bigendian.sh @@ -14,17 +14,24 @@ # submodule-built networking stack (oxen-libquic / session-router) is the bulk of it. That's fine for # an occasional manual audit; it is deliberately not in CI. # -# Requires: Docker able to run foreign-arch images via qemu-user/binfmt. Run from the repo root with -# submodules checked out. +# Requires: Docker, plus a one-time host install of qemu-user-static + binfmt-support (see below). +# Run from the repo root with submodules checked out. set -euo pipefail -# Register qemu-user binfmt handlers for s390x (idempotent; needs --privileged once per boot). -docker run --privileged --rm tonistiigi/binfmt --install s390x +# One-time host prerequisite (Debian/Ubuntu): +# sudo apt install qemu-user-static binfmt-support +# That registers the binfmt_misc handlers with the "F" (fix-binary) flag, which is what lets Docker +# transparently run foreign-arch images under emulation from inside a container — no privileged helper +# container needed. +if [ ! -e /proc/sys/fs/binfmt_misc/qemu-s390x ]; then + echo "s390x binfmt handler not registered; run: sudo apt install qemu-user-static binfmt-support" >&2 + exit 1 +fi # Build + test inside an emulated s390x Debian. BUILD_STATIC_DEPS=OFF pulls the heavy deps # (libsodium, protobuf, curl, sqlite, zstd) as prebuilt s390x apt packages rather than compiling them # from source under emulation. -docker run --platform=linux/s390x --rm -v "$PWD:/src" -w /src s390x/debian:bookworm bash -euxc ' +docker run --platform=linux/s390x --rm -v "$PWD:/src" -w /src debian:sid bash -euxc ' apt-get update apt-get install -y --no-install-recommends \ g++ cmake ninja-build pkg-config git ca-certificates \ From a48b509cbef7bed70f93480f623f38c9d1ea388b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 16:42:12 -0300 Subject: [PATCH 118/193] Pro: rename *_unix_ts fields to *_at The *_unix_ts members/params are sys_seconds / sys_ms time points, so the name baked the representation into the identifier. Rename them to *_at (as the backend did) to stop embedding the type. Pure rename: JSON wire keys and protobuf accessors are untouched, so there is no wire change. Also drop the stale expiry_unix_ts_ms grace-period doc formula (the wire is integer seconds now; no *_ms field survives). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../session/config/convo_info_volatile.hpp | 2 +- include/session/pro_backend.hpp | 32 +++++----- include/session/session_protocol.h | 4 +- include/session/session_protocol.hpp | 10 +-- src/config/convo_info_volatile.cpp | 16 ++--- src/config/pro.cpp | 2 +- src/config/user_profile.cpp | 6 +- src/core/pro.cpp | 2 +- src/pro_backend.cpp | 62 +++++++++---------- src/session_protocol.cpp | 10 +-- tests/test_config_convo_info_volatile.cpp | 2 +- tests/test_config_pro.cpp | 14 ++--- tests/test_config_userprofile.cpp | 4 +- tests/test_pro_backend.cpp | 2 +- tests/test_session_protocol.cpp | 24 +++---- 15 files changed, 96 insertions(+), 96 deletions(-) diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 5f21951ba..f4285c36c 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -92,7 +92,7 @@ namespace convo { /// Unix epoch timestamp (seconds) until which this proof's entitlement to Session Pro /// features is valid - std::chrono::sys_seconds pro_expiry_unix_ts{}; + std::chrono::sys_seconds pro_expiry_at{}; protected: using base::base; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 35eb25a5e..d5355e8d2 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -227,7 +227,7 @@ struct ProRevocationItem { b32 revocation_tag; /// A matching proof is revoked once the client's clock reaches this unix timestamp (not before) - std::chrono::sys_seconds effective_unix_ts; + std::chrono::sys_seconds effective_at; }; struct GetProRevocationsResponse : Response { @@ -294,32 +294,32 @@ struct ProPaymentItem { /// Provider purchase time (when the upstream provider recorded the purchase). Always set. /// Carries the provider's sub-second precision as a millisecond-resolution `sys_ms`; the wire /// sends it as a float of seconds. - sys_ms purchased_unix_ts; + sys_ms purchased_at; /// Unix timestamp of when the payment was redeemed. 0 if not activated - std::chrono::sys_seconds redeemed_unix_ts; + std::chrono::sys_seconds redeemed_at; /// Unix timestamp of when the payment was expiry. 0 if not activated - std::chrono::sys_seconds expiry_unix_ts; + std::chrono::sys_seconds expiry_at; /// Duration of the grace period, e.g. when the payment provider will start to attempt to renew /// the Session Pro subscription. During the period between - /// [expiry_unix_ts, expiry_unix_ts + grace_period_duration] the user continues to have + /// [expiry_at, expiry_at + grace_period_duration] the user continues to have /// entitlement to Session Pro. This value is only applicable if `auto_renewing` is `true`. std::chrono::seconds grace_period_duration; /// Unix deadline timestamp of when the user is able to refund the subscription via the payment /// provider. Thereafter the user must initiate a refund manually via Session support. - std::chrono::sys_seconds platform_refund_expiry_unix_ts; + std::chrono::sys_seconds platform_refund_expiry_at; /// Provider revocation instant (when the payment was revoked). Epoch (0) if not applicable. /// Carries the provider's sub-second precision as a millisecond-resolution `sys_ms`; the wire /// sends it as a float of seconds. - sys_ms revoked_unix_ts; + sys_ms revoked_at; /// UNIX timestamp at which a refund request was requested for this payment. This is set to 0 /// if no refund has been requested for this payment yet. - std::chrono::sys_seconds refund_requested_unix_ts; + std::chrono::sys_seconds refund_requested_at; /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their /// parts in per the backend-defined composite -- libsession does not interpret it). @@ -355,7 +355,7 @@ struct GetProDetailsResponse : Response { /// some leeway as to the time required for the payment provider to successfully bill the user. /// This expiry timestamp is hence calculated as: /// - /// expiry_unix_ts_ms = (subscription_expiry_unix_ts + grace_period_duration) + /// expiry_at = subscription_expiry + grace_period_duration /// /// E.g. The subscription expiry timestamp can be calculated by subtracting /// `grace_period_duration` to determine if the user is currently in a grace period. Some @@ -367,18 +367,18 @@ struct GetProDetailsResponse : Response { /// This timestamp may be in the past if the user no longer has active payments. Overtime the /// Pro Backend may prune user history and so after long lapses of activity, a user's /// subscription history may be deleted. - std::chrono::sys_seconds expiry_unix_ts; + std::chrono::sys_seconds expiry_at; /// Duration that a user is entitled to for their grace period. This value is to be ignored if /// `auto_renewing` is false. It can be used to calculate the subscription expiry timestamp by - /// subtracting `expiry_unix_ts_ms` from this value. + /// subtracting it from `expiry_at`. std::chrono::seconds grace_period_duration; /// UNIX timestamp at which a refund request was requested by this user. This timestamp comes /// from the latest payment that the backend has deemed to be active for the user (e.g. the - /// payment associated with the `expiry_unix_ts_ms`). This value is 0 if no refund has been + /// payment associated with the `expiry_at`). This value is 0 if no refund has been /// requested on the active payment. - std::chrono::sys_seconds refund_requested_unix_ts; + std::chrono::sys_seconds refund_requested_at; /// Total number of payments known by the backend for the user. This may be greater than the /// length of items if the request, requested less than the number of payments the user has. @@ -397,21 +397,21 @@ GetProDetailsResponse parse_payment_details(std::string_view json); /// Inputs: /// - `master_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium master private key /// - `unix_ts` -- Unix timestamp for the request -/// - `refund_requested_unix_ts` -- timestamp to record as when the refund was requested +/// - `refund_requested_at` -- timestamp to record as when the refund was requested /// - `provider_code` -- provider code string the payment is from (see /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*) /// - `payment_id` -- opaque payment identifier from the provider (hashed verbatim) b64 refund_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id); ProRequest refund_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id); diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index f46b037e0..6b7e3614f 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -240,7 +240,7 @@ LIBSESSION_EXPORT bool session_protocol_pro_proof_verify_message( /// API: session_protocol/session_protocol_pro_proof_is_active /// /// Check if the Pro proof is currently entitled to Pro given the `ts` with respect to the -/// proof's `expiry_unix_ts` +/// proof's `expiry_at` /// /// Inputs: /// - `proof` -- Proof to verify @@ -268,7 +268,7 @@ LIBSESSION_EXPORT bool session_protocol_pro_proof_is_active( /// they are the original signatory of the proof. /// - `verify_pubkey_len` -- Length of the `verify_pubkey` should be 32 bytes /// they are the original signatory of the proof. -/// - `ts` -- Unix timestamp to compared against the embedded `expiry_unix_ts` +/// - `ts` -- Unix timestamp to compared against the embedded `expiry_at` /// to determine if the proof has expired or not /// - `signed_msg` -- Optionally set the payload to the message with the signature to verify if /// the embedded `rotating_pubkey` in the proof signed the given message. diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 9242dfb52..c717a8c80 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -75,7 +75,7 @@ class ProProof { b32 rotating_pubkey; /// Unix epoch timestamp to which this proof's entitlement to Session Pro features is valid to - std::chrono::sys_seconds expiry_unix_ts; + std::chrono::sys_seconds expiry_at; /// Signature over the contents of the proof. It is signed by the Session Pro Backend key which /// is the entity responsible for issueing tamper-proof Sesison Pro certificates for Session @@ -116,10 +116,10 @@ class ProProof { /// API: pro/Proof::is_active /// /// Check if Pro proof is currently entitled to Pro given the `unix_ts` with respect to the - /// proof's `expiry_unix_ts` + /// proof's `expiry_at` /// /// Inputs: - /// - `unix_ts` -- Unix timestamp to compare against the embedded `expiry_unix_ts` + /// - `unix_ts` -- Unix timestamp to compare against the embedded `expiry_at` /// to determine if the proof has expired or not /// /// Outputs: @@ -139,7 +139,7 @@ class ProProof { /// Inputs: /// - `verify_pubkey` -- 32 byte Ed25519 public key of the corresponding secret key to check if /// they are the original signatory of the proof. - /// - `unix_ts` -- Unix timestamp to compared against the embedded `expiry_unix_ts` + /// - `unix_ts` -- Unix timestamp to compared against the embedded `expiry_at` /// to determine if the proof has expired or not /// - `signed_msg` -- Optionally set the payload to the message with the signature to verify if /// the embedded `rotating_pubkey` in the proof signed the given message. @@ -160,7 +160,7 @@ class ProProof { bool operator==(const ProProof& other) const { return version == other.version && revocation_tag == other.revocation_tag && - rotating_pubkey == other.rotating_pubkey && expiry_unix_ts == other.expiry_unix_ts && + rotating_pubkey == other.rotating_pubkey && expiry_at == other.expiry_at && sig == other.sig; } }; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 46fa33226..8d001e3b2 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -38,7 +38,7 @@ namespace convo { pro_revocation_tag->data(), c.pro_revocation_tag.data, pro_revocation_tag->size()); - pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); + pro_expiry_at = as_sys_seconds(c.pro_expiry_ts); } } @@ -54,7 +54,7 @@ namespace convo { pro_revocation_tag->data(), pro_revocation_tag->size()); - c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); + c.pro_expiry_ts = epoch_seconds(pro_expiry_at); } else { c.has_pro_revocation_tag = false; c.pro_expiry_ts = 0; @@ -133,7 +133,7 @@ namespace convo { pro_revocation_tag->data(), c.pro_revocation_tag.data, pro_revocation_tag->size()); - pro_expiry_unix_ts = as_sys_seconds(c.pro_expiry_ts); + pro_expiry_at = as_sys_seconds(c.pro_expiry_ts); } } @@ -149,7 +149,7 @@ namespace convo { c.pro_revocation_tag.data, pro_revocation_tag->data(), pro_revocation_tag->size()); - c.pro_expiry_ts = epoch_seconds(pro_expiry_unix_ts); + c.pro_expiry_ts = epoch_seconds(pro_expiry_at); } else { c.has_pro_revocation_tag = false; c.pro_expiry_ts = 0; @@ -163,7 +163,7 @@ namespace convo { std::optional> maybe_pro_revocation_tag = maybe_vector(info_dict, "g"); if (pro_expiry > 0 && maybe_pro_revocation_tag && maybe_pro_revocation_tag->size() == 32) { - pro_expiry_unix_ts = as_sys_seconds(pro_expiry); + pro_expiry_at = as_sys_seconds(pro_expiry); pro_revocation_tag.emplace(); std::memcpy( pro_revocation_tag->data(), @@ -333,7 +333,7 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { auto info = data["1"][session_id_to_bytes(c.session_id)]; set_base(c, info); - auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); + auto pro_expiry = epoch_seconds(c.pro_expiry_at); if (pro_expiry > 0 && c.pro_revocation_tag) { set_nonzero_int(info["e"], pro_expiry); info["g"] = to_span(*c.pro_revocation_tag); @@ -361,7 +361,7 @@ static bool is_stale(const C& c, std::chrono::system_clock::time_point cutoff) { if (c.unread) return false; if constexpr (std::derived_from) - if (c.pro_revocation_tag.has_value() && c.pro_expiry_unix_ts >= cutoff) + if (c.pro_revocation_tag.has_value() && c.pro_expiry_at >= cutoff) return false; return sys_ms{std::chrono::milliseconds{c.last_read}} < cutoff; } @@ -439,7 +439,7 @@ void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { set_nonzero_int(info["y"], c.legacy_blinding); - auto pro_expiry = epoch_seconds(c.pro_expiry_unix_ts); + auto pro_expiry = epoch_seconds(c.pro_expiry_at); if (pro_expiry > 0 && c.pro_revocation_tag) { set_nonzero_int(info["e"], pro_expiry); info["g"] = to_span(*c.pro_revocation_tag); diff --git a/src/config/pro.cpp b/src/config/pro.cpp index 6cb0c776a..0a32299ab 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -45,7 +45,7 @@ bool ProConfig::load(const dict& root) { proof.revocation_tag.data(), maybe_revocation_tag->data(), proof.revocation_tag.size()); - proof.expiry_unix_ts = *maybe_expiry; + proof.expiry_at = *maybe_expiry; std::memcpy(proof.sig.data(), maybe_sig->data(), proof.sig.size()); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index e847b2936..67145d12c 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -170,7 +170,7 @@ void UserProfile::set_pro_config(const ProConfig& pro) { auto proof_dict = root["p"]; proof_dict["@"] = pro.proof.version; proof_dict["g"] = pro.proof.revocation_tag; - proof_dict["e"] = epoch_seconds(pro.proof.expiry_unix_ts); + proof_dict["e"] = epoch_seconds(pro.proof.expiry_at); proof_dict["s"] = pro.proof.sig; const auto target_timestamp = @@ -345,7 +345,7 @@ LIBSESSION_C_API bool user_profile_get_pro_config(const config_object* conf, pro pro->proof.rotating_pubkey.data, val->proof.rotating_pubkey.data(), val->proof.rotating_pubkey.size()); - pro->proof.expiry_ts = epoch_seconds(val->proof.expiry_unix_ts); + pro->proof.expiry_ts = epoch_seconds(val->proof.expiry_at); std::memcpy(pro->proof.sig.data, val->proof.sig.data(), val->proof.sig.size()); std::memcpy( pro->rotating_privkey.data, @@ -367,7 +367,7 @@ LIBSESSION_C_API void user_profile_set_pro_config(config_object* conf, const pro val.proof.rotating_pubkey.data(), pro->proof.rotating_pubkey.data, val.proof.rotating_pubkey.size()); - val.proof.expiry_unix_ts = as_sys_seconds(pro->proof.expiry_ts); + val.proof.expiry_at = as_sys_seconds(pro->proof.expiry_ts); std::memcpy(val.proof.sig.data(), pro->proof.sig.data, val.proof.sig.size()); std::memcpy( val.rotating_privkey.data(), pro->rotating_privkey.data, val.rotating_privkey.size()); diff --git a/src/core/pro.cpp b/src/core/pro.cpp index ad7ec8228..c15c3b9ec 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -51,7 +51,7 @@ void Pro::update_revocations( exec_query( st, revoke.revocation_tag, - revoke.effective_unix_ts.time_since_epoch().count(), + revoke.effective_at.time_since_epoch().count(), now); st->reset(); } diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 533aaa3f5..18c6c187b 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -294,7 +294,7 @@ namespace { // Parse payload result.proof.version = json_require(result_obj, "version", result.errors); auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); - result.proof.expiry_unix_ts = as_sys_seconds(expiry_ts); + result.proof.expiry_at = as_sys_seconds(expiry_ts); json_require_fixed_bytes_from_hex( result_obj, "revocation_tag", result.errors, result.proof.revocation_tag); json_require_fixed_bytes_from_hex( @@ -418,7 +418,7 @@ GetProRevocationsResponse parse_revocations(std::string_view json) { auto effective_ts = json_require(obj, "effective_ts", result.errors); ProRevocationItem item = {}; - item.effective_unix_ts = as_sys_seconds(effective_ts); + item.effective_at = as_sys_seconds(effective_ts); json_require_fixed_bytes_from_hex( obj, "revocation_tag", result.errors, item.revocation_tag); @@ -519,9 +519,9 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { json_require(result_obj, "grace_period_duration", result.errors); int64_t refund_requested_ts = json_require(result_obj, "refund_requested_ts", result.errors); - result.expiry_unix_ts = as_sys_seconds(expiry_ts); + result.expiry_at = as_sys_seconds(expiry_ts); result.grace_period_duration = std::chrono::seconds(grace_period_duration); - result.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); + result.refund_requested_at = as_sys_seconds(refund_requested_ts); auto array = json_require(result_obj, "items", result.errors); result.items.reserve(array.size()); @@ -563,13 +563,13 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { item.payment_id = std::move(payment_id); item.auto_renewing = auto_renewing; - item.purchased_unix_ts = sys_ms_from_seconds(purchased_ts); - item.redeemed_unix_ts = as_sys_seconds(redeemed_ts); - item.expiry_unix_ts = as_sys_seconds(expiry_ts); + item.purchased_at = sys_ms_from_seconds(purchased_ts); + item.redeemed_at = as_sys_seconds(redeemed_ts); + item.expiry_at = as_sys_seconds(expiry_ts); item.grace_period_duration = std::chrono::seconds(grace_period_duration); - item.platform_refund_expiry_unix_ts = as_sys_seconds(platform_refund_expiry_ts); - item.revoked_unix_ts = sys_ms_from_seconds(revoked_ts); - item.refund_requested_unix_ts = as_sys_seconds(refund_requested_ts); + item.platform_refund_expiry_at = as_sys_seconds(platform_refund_expiry_ts); + item.revoked_at = sys_ms_from_seconds(revoked_ts); + item.refund_requested_at = as_sys_seconds(refund_requested_ts); // Handle parsing result if (result.errors.size()) @@ -587,13 +587,13 @@ namespace { b32 refund_hash( std::span master_pubkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { // Must match the set-payment-refund-requested signed-request hash in pro-wire-protocol.md // §3.3 (+ §3.5 for payment_id). int64_t ts = epoch_seconds(unix_ts); - int64_t refund_requested_ts = epoch_seconds(refund_requested_unix_ts); + int64_t refund_requested_ts = epoch_seconds(refund_requested_at); return hash::blake2b_pers<32>( SET_PAYMENT_REFUND_REQUESTED_PERS, master_pubkey, @@ -606,14 +606,14 @@ namespace { std::string refund_body( std::span master_pubkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::string_view payment_id, std::span master_sig) { return nlohmann::json{ {"master_pkey", oxenc::to_hex(master_pubkey)}, {"ts", epoch_seconds(unix_ts)}, - {"refund_requested_ts", epoch_seconds(refund_requested_unix_ts)}, + {"refund_requested_ts", epoch_seconds(refund_requested_at)}, {"payment_tx", {{"provider", provider_code}, {"payment_id", payment_id}}}, {"master_sig", oxenc::to_hex(master_sig)}} .dump(); @@ -624,27 +624,27 @@ namespace { b64 refund_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { auto hash = refund_hash( - master_privkey.pubkey(), unix_ts, refund_requested_unix_ts, provider_code, payment_id); + master_privkey.pubkey(), unix_ts, refund_requested_at, provider_code, payment_id); return ed25519::sign(master_privkey, hash); } ProRequest refund_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - std::chrono::sys_seconds refund_requested_unix_ts, + std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { auto sig = refund_sig( - master_privkey, unix_ts, refund_requested_unix_ts, provider_code, payment_id); + master_privkey, unix_ts, refund_requested_at, provider_code, payment_id); return {set_refund_endpoint, refund_body( master_privkey.pubkey(), unix_ts, - refund_requested_unix_ts, + refund_requested_at, provider_code, to_string_view(payment_id), sig)}; @@ -879,7 +879,7 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) // error response returns the same struct just with different fields populated. result.header.status = cpp.status; result.proof.version = cpp.proof.version; - result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_unix_ts); + result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_at); std::memcpy( result.proof.revocation_tag.data, cpp.proof.revocation_tag.data(), @@ -963,7 +963,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t const ProRevocationItem& src = cpp.items[index]; session_pro_backend_pro_revocation_item& dest = result.items[index]; std::memcpy(dest.revocation_tag.data, src.revocation_tag.data(), src.revocation_tag.size()); - dest.effective_ts = session::epoch_seconds(src.effective_unix_ts); + dest.effective_ts = session::epoch_seconds(src.effective_at); } return result; } @@ -1012,9 +1012,9 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json result.items = (session_pro_backend_pro_payment_item*)arena_alloc( &arena, result.items_count * sizeof(*result.items)); result.auto_renewing = cpp.auto_renewing; - result.expiry_ts = epoch_seconds(cpp.expiry_unix_ts); + result.expiry_ts = epoch_seconds(cpp.expiry_at); result.grace_period_duration = cpp.grace_period_duration.count(); - result.refund_requested_ts = epoch_seconds(cpp.refund_requested_unix_ts); + result.refund_requested_ts = epoch_seconds(cpp.refund_requested_at); result.payments_total = cpp.payments_total; for (size_t index = 0; index < result.items_count; ++index) { @@ -1024,13 +1024,13 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json dest.plan_count = session::copy_c_str(dest.plan, src.plan) - 1; dest.payment_provider_count = session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; - dest.purchased_ts = epoch_seconds_double(src.purchased_unix_ts); - dest.redeemed_ts = epoch_seconds(src.redeemed_unix_ts); - dest.expiry_ts = epoch_seconds(src.expiry_unix_ts); + dest.purchased_ts = epoch_seconds_double(src.purchased_at); + dest.redeemed_ts = epoch_seconds(src.redeemed_at); + dest.expiry_ts = epoch_seconds(src.expiry_at); dest.grace_period_duration = src.grace_period_duration.count(); - dest.platform_refund_expiry_ts = epoch_seconds(src.platform_refund_expiry_unix_ts); - dest.revoked_ts = epoch_seconds_double(src.revoked_unix_ts); - dest.refund_requested_ts = epoch_seconds(src.refund_requested_unix_ts); + dest.platform_refund_expiry_ts = epoch_seconds(src.platform_refund_expiry_at); + dest.revoked_ts = epoch_seconds_double(src.revoked_at); + dest.refund_requested_ts = epoch_seconds(src.refund_requested_at); dest.payment_id_count = session::copy_c_str(dest.payment_id, src.payment_id) - 1; } @@ -1060,14 +1060,14 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); - std::chrono::sys_seconds refund_requested_unix_ts = + std::chrono::sys_seconds refund_requested_at = session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto sig = refund_sig( master_span, unix_ts, - refund_requested_unix_ts, + refund_requested_at, payment_tx_provider_code, payment_tx_payment_id_span); std::memcpy(result.sig.data, sig.data(), sig.size()); diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 13eff6ad9..0af6daeff 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -98,7 +98,7 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP result.proof.rotating_pubkey.data, cpp.proof.rotating_pubkey.data(), cpp.proof.rotating_pubkey.max_size()); - result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_unix_ts); + result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_at); std::memcpy(result.proof.sig.data, cpp.proof.sig.data(), cpp.proof.sig.max_size()); result.msg_bitset = static_cast(cpp.msg_flags); result.profile_bitset = static_cast(cpp.profile_flags); @@ -114,7 +114,7 @@ static session::ProProof proof_from_c(const session_protocol_pro_proof& c) { proof.revocation_tag.data(), c.revocation_tag.data, proof.revocation_tag.max_size()); std::memcpy( proof.rotating_pubkey.data(), c.rotating_pubkey.data, proof.rotating_pubkey.max_size()); - proof.expiry_unix_ts = session::as_sys_seconds(c.expiry_ts); + proof.expiry_at = session::as_sys_seconds(c.expiry_ts); std::memcpy(proof.sig.data(), c.sig.data, proof.sig.max_size()); return proof; } @@ -136,7 +136,7 @@ bool ProProof::verify_message( } bool ProProof::is_active(std::chrono::sys_seconds unix_ts) const { - return unix_ts <= expiry_unix_ts; + return unix_ts <= expiry_at; } ProStatus ProProof::status( @@ -163,7 +163,7 @@ ProStatus ProProof::status( b32 ProProof::hash() const { b32 result = proof_hash_internal( - revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_unix_ts)); + revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_at)); return result; } @@ -487,7 +487,7 @@ static DecodedPro parse_pro_message(const SessionProtos::ProMessage& pro_msg) { proof.rotating_pubkey.data(), proto_proof.rotatingpublickey().data(), proto_proof.rotatingpublickey().size()); - proof.expiry_unix_ts = session::as_sys_seconds(proto_proof.expiryunixts()); + proof.expiry_at = session::as_sys_seconds(proto_proof.expiryunixts()); std::memcpy(proof.sig.data(), proto_proof.sig().data(), proto_proof.sig().size()); return pro; } diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 1d3ee9506..3307359dd 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -609,7 +609,7 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { c.unread = true; if (i % 7 == 0) { - c.pro_expiry_unix_ts = + c.pro_expiry_at = std::chrono::sys_seconds{std::chrono::duration_cast( std::chrono::milliseconds{unix_timestamp(i)})}; diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index ceee0f5c9..da74412e6 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -24,7 +24,7 @@ TEST_CASE("Pro", "[config][pro]") { pro_cpp.rotating_privkey = rotating_sk; pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; - pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); + pro_cpp.proof.expiry_at = std::chrono::sys_seconds(1s); constexpr auto revocation_tag = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_u; static_assert(pro_cpp.proof.revocation_tag.max_size() == revocation_tag.size()); @@ -35,7 +35,7 @@ TEST_CASE("Pro", "[config][pro]") { std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); - pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); + pro.proof.expiry_ts = pro_cpp.proof.expiry_at.time_since_epoch().count(); std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size()); } @@ -58,8 +58,8 @@ TEST_CASE("Pro", "[config][pro]") { // Verify expiry { - CHECK(pro_cpp.proof.is_active(pro_cpp.proof.expiry_unix_ts)); - CHECK_FALSE(pro_cpp.proof.is_active(pro_cpp.proof.expiry_unix_ts + 1s)); + CHECK(pro_cpp.proof.is_active(pro_cpp.proof.expiry_at)); + CHECK_FALSE(pro_cpp.proof.is_active(pro_cpp.proof.expiry_at + 1s)); CHECK(session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_ts)); CHECK_FALSE(session_protocol_pro_proof_is_active(&pro.proof, pro.proof.expiry_ts + 1)); @@ -88,7 +88,7 @@ TEST_CASE("Pro", "[config][pro]") { /*version*/ {"@", proof.version}, /*revocation_tag*/ {"g", std::string(reinterpret_cast(proof.revocation_tag.data()), proof.revocation_tag.size())}, /*rotating pubkey*/ {"r", std::string(reinterpret_cast(proof.rotating_pubkey.data()), proof.rotating_pubkey.size())}, - /*expiry unix ts*/ {"e", proof.expiry_unix_ts.time_since_epoch().count()}, + /*expiry unix ts*/ {"e", proof.expiry_at.time_since_epoch().count()}, /*signature*/ {"s", std::string{reinterpret_cast(proof.sig.data()), proof.sig.size()}}, }} }; @@ -100,7 +100,7 @@ TEST_CASE("Pro", "[config][pro]") { CHECK(loaded_pro.proof.version == pro_cpp.proof.version); CHECK(loaded_pro.proof.revocation_tag == pro_cpp.proof.revocation_tag); CHECK(loaded_pro.proof.rotating_pubkey == pro_cpp.proof.rotating_pubkey); - CHECK(loaded_pro.proof.expiry_unix_ts == pro_cpp.proof.expiry_unix_ts); + CHECK(loaded_pro.proof.expiry_at == pro_cpp.proof.expiry_at); CHECK(loaded_pro.proof.sig == pro_cpp.proof.sig); CHECK(loaded_pro.proof.verify_signature(signing_pk)); } @@ -118,7 +118,7 @@ TEST_CASE("Pro", "[config][pro]") { /*version*/ {"@", proof.version}, /*revocation_tag*/ {"g", std::string(reinterpret_cast(proof.revocation_tag.data()), proof.revocation_tag.size())}, /*rotating pubkey*/ {"r", std::string(reinterpret_cast(proof.rotating_pubkey.data()), proof.rotating_pubkey.size())}, - /*expiry unix ts*/ {"e", proof.expiry_unix_ts.time_since_epoch().count()}, + /*expiry unix ts*/ {"e", proof.expiry_at.time_since_epoch().count()}, /*signature*/ {"s", std::string{reinterpret_cast(broken_sig.data()), broken_sig.size()}}, }} }; diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 9f8ccdc0b..d31044832 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -597,7 +597,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { pro_cpp.rotating_privkey = rotating_sk; pro_cpp.proof.version = 2; pro_cpp.proof.rotating_pubkey = rotating_pk; - pro_cpp.proof.expiry_unix_ts = std::chrono::sys_seconds(1s); + pro_cpp.proof.expiry_at = std::chrono::sys_seconds(1s); constexpr auto revocation_tag = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hex_b; static_assert(pro_cpp.proof.revocation_tag.max_size() == revocation_tag.size()); @@ -608,7 +608,7 @@ TEST_CASE("UserProfile Pro Storage", "[config][user_profile][pro]") { std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size()); pro.proof.version = pro_cpp.proof.version; std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size()); - pro.proof.expiry_ts = pro_cpp.proof.expiry_unix_ts.time_since_epoch().count(); + pro.proof.expiry_ts = pro_cpp.proof.expiry_at.time_since_epoch().count(); std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size()); } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index ba8bdf88c..408ecc230 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -349,7 +349,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { result_cpp.proof.rotating_pubkey.data(), result_cpp.proof.rotating_pubkey.size()) == 0); REQUIRE(result.proof.expiry_ts == - result_cpp.proof.expiry_unix_ts.time_since_epoch().count()); + result_cpp.proof.expiry_at.time_since_epoch().count()); REQUIRE(std::memcmp( result.proof.sig.data, result_cpp.proof.sig.data(), diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 5caf10df6..3fe200284 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -28,8 +28,8 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se std::string_view data_body, const ed25519::PrivKeySpan& user_rotating_privkey, const ed25519::PrivKeySpan& pro_backend_privkey, - std::chrono::sys_seconds content_unix_ts, - std::chrono::sys_seconds pro_expiry_unix_ts, + std::chrono::sys_seconds content_at, + std::chrono::sys_seconds pro_expiry_at, uint64_t msg_bitset, uint64_t profile_bitset) { SerialisedProtobufContentWithProForTesting result = {}; @@ -37,7 +37,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se // Create protobuf `Content.dataMessage` SessionProtos::Content content = {}; content.set_sigtimestamp(std::chrono::duration_cast( - content_unix_ts.time_since_epoch()) + content_at.time_since_epoch()) .count()); SessionProtos::DataMessage* data = content.mutable_datamessage(); @@ -45,7 +45,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se // Generate a dummy proof std::ranges::copy(user_rotating_privkey.pubkey(), result.proof.rotating_pubkey.begin()); - result.proof.expiry_unix_ts = pro_expiry_unix_ts; + result.proof.expiry_at = pro_expiry_at; // Sign the proof by the dummy "Session Pro Backend" key result.pro_proof_hash = result.proof.hash(); @@ -63,7 +63,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se result.proof.revocation_tag.data(), result.proof.revocation_tag.size()); proto_proof->set_rotatingpublickey( result.proof.rotating_pubkey.data(), result.proof.rotating_pubkey.size()); - proto_proof->set_expiryunixts(session::epoch_seconds(result.proof.expiry_unix_ts)); + proto_proof->set_expiryunixts(session::epoch_seconds(result.proof.expiry_at)); proto_proof->set_sig(result.proof.sig.data(), result.proof.sig.size()); // Generate the plaintext @@ -292,8 +292,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*data_body*/ data_body, /*user_rotating_privkey*/ user_pro_ed_sk, /*pro_backend_privkey*/ keys.ed_sk1, - /*content_unix_ts=*/timestamp_s, - /*pro_expiry_unix_ts*/ timestamp_s, + /*content_at=*/timestamp_s, + /*pro_expiry_at*/ timestamp_s, /*msg_bitset*/ {}, /*profile_bitset*/ {}); @@ -423,8 +423,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*data_body*/ large_message, /*user_rotating_privkey*/ user_pro_ed_sk, /*pro_backend_privkey*/ keys.ed_sk1, - /*content_unix_ts*/ timestamp_s, - /*pro_expiry_unix_ts*/ timestamp_s, + /*content_at*/ timestamp_s, + /*pro_expiry_at*/ timestamp_s, /*msg_bitset*/ pro_msg.bitset, /*proilfe_bitset*/ profile_bitset); @@ -634,7 +634,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // user's "Session Pro" key into `sig_over_plaintext_with_user_pro_key` std::chrono::milliseconds bad_timestamp_ms = std::chrono::duration_cast( - protobuf_content.proof.expiry_unix_ts.time_since_epoch()) + + protobuf_content.proof.expiry_at.time_since_epoch()) + std::chrono::seconds(1); SerialisedProtobufContentWithProForTesting bad_protobuf_content = @@ -642,11 +642,11 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { /*data_body*/ data_body, /*user_rotating_privkey*/ user_pro_ed_sk, /*pro_backend_privkey*/ keys.ed_sk1, - /*content_unix_ts=*/ + /*content_at=*/ std::chrono::sys_seconds( std::chrono::duration_cast( bad_timestamp_ms)), - /*pro_expiry_unix_ts*/ timestamp_s, + /*pro_expiry_at*/ timestamp_s, /*msg_bitset*/ {}, /*profile_bitset*/ {}); From 5ba0fa69fb9ff7525860c98e380ac65f6927b4f8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 17:12:23 -0300 Subject: [PATCH 119/193] Pro: give ProRequest an explicit content_type; body -> opaque data Requests now carry {endpoint, content_type, data} instead of {endpoint, body}. Clients relay `data` verbatim under `content_type` and must NOT assume or parse a format -- libsession owns the wire encoding and may change it. Renames the C mirror to match: session_pro_backend_to_json -> session_pro_backend_request (json field -> data, plus endpoint + content_type), *_request_to_json -> *_request_build, to_json_free -> request_free. The content type is single-sourced ("application/json") next to the endpoint strings. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 69 +++++++----- include/session/pro_backend.hpp | 18 +++- src/pro_backend.cpp | 72 ++++++++----- tests/test_pro_backend.cpp | 181 ++++++++++++++++---------------- 4 files changed, 191 insertions(+), 149 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index a22b3500d..68c93d4c6 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -100,12 +100,22 @@ struct session_pro_backend_response_header { internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch }; -typedef struct session_pro_backend_to_json session_pro_backend_to_json; -struct session_pro_backend_to_json { +/// A built request to POST to the Session Pro backend. Mirrors the C++ +/// `session::pro_backend::ProRequest`: an `endpoint`, the `content_type` to send as the request's +/// Content-Type header, and the opaque `data` payload. `data` is libsession's data for the backend +/// -- relay it verbatim with the given `content_type` and do NOT parse, inspect, or assume a format +/// for it. libsession owns the wire encoding and may change it without client involvement. +typedef struct session_pro_backend_request session_pro_backend_request; +struct session_pro_backend_request { char error[256]; size_t error_count; - bool success; /// True if conversion to JSON was successful, false if out-of-memory - string8 json; + bool success; /// True if the request was built successfully, false if out-of-memory + /// Endpoint path (relative to the backend base URL) to POST to; static, null-terminated string. + const char* endpoint; + /// Value for the request's Content-Type header; static, null-terminated string. Relay verbatim. + const char* content_type; + /// The opaque request payload. Relay untouched; do not parse or modify. + string8 data; }; typedef struct session_pro_backend_master_rotating_signatures @@ -348,42 +358,46 @@ session_pro_backend_signature session_pro_backend_get_pro_details_request_build_ const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, uint32_t count) NON_NULL_ARG(1); -/// API: session_pro_backend/add_pro_payment_request_to_json +/// API: session_pro_backend/add_pro_payment_request_build /// -/// Serializes an `add_pro_payment_request` to a JSON string. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. +/// Builds the request to POST for an `add_pro_payment_request`, as a session_pro_backend_request +/// (endpoint + content_type + opaque data). +/// Free the returned request with `session_pro_backend_request_free`. /// /// Inputs: /// - `request` -- Pointer to the request struct. LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_add_pro_payment_request_to_json( +session_pro_backend_request session_pro_backend_add_pro_payment_request_build( const session_pro_backend_add_pro_payment_request* request); -/// API: session_pro_backend/generate_pro_proof_request_to_json +/// API: session_pro_backend/generate_pro_proof_request_build /// -/// Serializes a `generate_pro_proof_request` to a JSON string. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. +/// Builds the request to POST for a `generate_pro_proof_request`, as a session_pro_backend_request +/// (endpoint + content_type + opaque data). +/// Free the returned request with `session_pro_backend_request_free`. /// /// Inputs: /// - `request` -- Pointer to the request struct. LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_to_json( +session_pro_backend_request session_pro_backend_generate_pro_proof_request_build( const session_pro_backend_generate_pro_proof_request* request); -/// API: session_pro_backend/get_pro_revocations_request_to_json +/// API: session_pro_backend/get_pro_revocations_request_build /// -/// Serializes a `get_pro_revocations_request` to a JSON string. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. +/// Builds the request to POST for a `get_pro_revocations_request`, as a session_pro_backend_request +/// (endpoint + content_type + opaque data). +/// Free the returned request with `session_pro_backend_request_free`. LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_get_pro_revocations_request_to_json( +session_pro_backend_request session_pro_backend_get_pro_revocations_request_build( const session_pro_backend_get_pro_revocations_request* request); -/// API: session_pro_backend/get_pro_details_request_to_json +/// API: session_pro_backend/get_pro_details_request_build /// -/// Serializes a `get_pro_details_request` to a JSON string. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. +/// Builds the request to POST for a `get_pro_details_request`, as a session_pro_backend_request +/// (endpoint + content_type + opaque data). +/// Free the returned request with `session_pro_backend_request_free`. LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_get_pro_details_request_to_json( +session_pro_backend_request session_pro_backend_get_pro_details_request_build( const session_pro_backend_get_pro_details_request* request); /// API: session_pro_backend/pro_proof_response_parse @@ -456,12 +470,13 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r const unsigned char* payment_tx_payment_id, size_t payment_tx_payment_id_len) NON_NULL_ARG(1, 5, 6); -/// API: session_pro_backend/set_payment_refund_requested_request_to_json +/// API: session_pro_backend/set_payment_refund_requested_request_build /// -/// Serializes a `set_payment_refund_requested_request` to a JSON string. -/// The caller must free the returned string using `session_pro_backend_to_json_free`. +/// Builds the request to POST for a `set_payment_refund_requested_request`, as a +/// session_pro_backend_request (endpoint + content_type + opaque data). Free the returned request +/// with `session_pro_backend_request_free`. LIBSESSION_EXPORT -session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_request_to_json( +session_pro_backend_request session_pro_backend_set_payment_refund_requested_request_build( const session_pro_backend_set_payment_refund_requested_request* request); /// API: session_pro_backend/set_payment_refund_requested_response_parse @@ -476,11 +491,11 @@ session_pro_backend_to_json session_pro_backend_set_payment_refund_requested_req LIBSESSION_EXPORT session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response_parse(const char* json, size_t json_len); -/// API: session_pro_backend/to_json_free +/// API: session_pro_backend/request_free /// -/// Frees the JSON +/// Frees a `session_pro_backend_request` returned by a `*_request_build` function. LIBSESSION_EXPORT -void session_pro_backend_to_json_free(session_pro_backend_to_json* to_json); +void session_pro_backend_request_free(session_pro_backend_request* request); /// API: session_pro_backend/pro_proof_response_free /// diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index d5355e8d2..49b4e3dbf 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -20,7 +20,7 @@ /// The high level summary of the functionality in this file. Clients can: /// /// 1. Build a request with `add_payment_request(...)` from a Session Pro payment and submit its -/// `{endpoint, body}` to the backend to register the specified Ed25519 keys for Session Pro. +/// `{endpoint, content_type, data}` to the backend to register the specified Ed25519 keys. /// /// Parse the server's reply with `parse_add_payment(...)`. Clients should validate the response /// and update their `UserProfile` by constructing a `ProConfig` with the `proof` from the @@ -131,16 +131,24 @@ struct ProviderUrls { /// hunting for empty members. std::optional provider_urls(std::string_view provider_code); -/// Route + body pair for a request to be POSTed to the Session Pro backend, returned by the -/// `*_request()` helpers below. +/// Endpoint + content-type + opaque payload for a request to be POSTed to the Session Pro backend, +/// returned by the `*_request()` helpers below. Callers relay `data` verbatim under `content_type` +/// and never inspect or assume its format -- libsession owns the wire encoding. struct ProRequest { /// Endpoint path relative to the backend base URL, e.g. "add_pro_payment". As returned from a /// `*_request()` function this points at a static, null-terminated string, so using /// `endpoint.data()` as a C string is valid. std::string_view endpoint; - /// The JSON request body to POST to `endpoint`. - std::string body; + /// Content type to send as the request's `Content-Type` header. Points at a static, + /// null-terminated string. Clients MUST relay this verbatim and MUST NOT assume a particular + /// format: `data` is libsession's opaque payload for the backend and libsession owns the wire + /// encoding, which may change without client involvement. + std::string_view content_type; + + /// The opaque request payload to POST to `endpoint`. This is libsession's data for the backend; + /// clients relay it untouched and must not parse, inspect, or modify it. + std::string data; }; /// Register a new Session Pro payment with the backend (endpoint `add_pro_payment`). The payment is diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 18c6c187b..1291173ce 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -154,6 +154,11 @@ namespace { constexpr char get_pro_revocations_endpoint[] = "get_pro_revocations"; constexpr char set_refund_endpoint[] = "set_payment_refund_requested"; + // Content type for the request payload (single master storage, shared with the C API's + // session_pro_backend_request.content_type). The wire encoding is libsession's to change; the + // content type travels alongside the payload so clients never hardcode a format. + constexpr char application_json[] = "application/json"; + // --- add-payment (endpoint add_pro_payment) --- b32 add_payment_hash( @@ -167,7 +172,7 @@ namespace { } // Serialise an add-payment request body from already-computed fields (shared by the C++ - // add_payment_request and the C ..._request_to_json wrapper). + // add_payment_request and the C ..._request_build wrapper). std::string add_payment_body( std::span master_pubkey, std::span rotating_pubkey, @@ -259,6 +264,7 @@ ProRequest add_payment_request( std::span payment_id) { auto sigs = add_payment_sigs(master_privkey, rotating_privkey, provider_code, payment_id); return {add_payment_endpoint, + application_json, add_payment_body( master_privkey.pubkey(), rotating_privkey.pubkey(), @@ -362,6 +368,7 @@ ProRequest pro_proof_request( std::chrono::sys_seconds unix_ts) { auto sigs = pro_proof_sigs(master_privkey, rotating_privkey, unix_ts); return {generate_proof_endpoint, + application_json, generate_proof_body( master_privkey.pubkey(), rotating_privkey.pubkey(), @@ -373,7 +380,7 @@ ProRequest pro_proof_request( ProRequest revocations_request(std::int64_t ticket) { nlohmann::json j; j["ticket"] = ticket; - return {get_pro_revocations_endpoint, j.dump()}; + return {get_pro_revocations_endpoint, application_json, j.dump()}; } GetProRevocationsResponse parse_revocations(std::string_view json) { @@ -473,6 +480,7 @@ ProRequest payment_details_request( uint32_t count) { auto sig = payment_details_sig(master_privkey, unix_ts, count); return {get_pro_details_endpoint, + application_json, payment_details_body(master_privkey.pubkey(), sig, unix_ts, count)}; } @@ -638,9 +646,9 @@ ProRequest refund_request( std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { - auto sig = refund_sig( - master_privkey, unix_ts, refund_requested_at, provider_code, payment_id); + auto sig = refund_sig(master_privkey, unix_ts, refund_requested_at, provider_code, payment_id); return {set_refund_endpoint, + application_json, refund_body( master_privkey.pubkey(), unix_ts, @@ -754,13 +762,14 @@ session_pro_backend_get_pro_details_request_build_sig( return result; } -LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment_request_to_json( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_add_pro_payment_request_build( const session_pro_backend_add_pro_payment_request* request) { - session_pro_backend_to_json result = {}; + session_pro_backend_request result = {}; if (!request) return result; try { + result.endpoint = add_payment_endpoint; std::string json = add_payment_body( to_byte_span(request->master_pkey.data), to_byte_span(request->rotating_pkey.data), @@ -768,7 +777,8 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, to_byte_span(request->master_sig.data), to_byte_span(request->rotating_sig.data)); - result.json = session::string8_copy_or_throw(json.data(), json.size()); + result.content_type = application_json; + result.data = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; @@ -777,20 +787,22 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_add_pro_payment return result; } -LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_generate_pro_proof_request_to_json( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_generate_pro_proof_request_build( const session_pro_backend_generate_pro_proof_request* request) { - session_pro_backend_to_json result = {}; + session_pro_backend_request result = {}; if (!request) return result; try { + result.endpoint = generate_proof_endpoint; std::string json = generate_proof_body( to_byte_span(request->master_pkey.data), to_byte_span(request->rotating_pkey.data), session::as_sys_seconds(request->ts), to_byte_span(request->master_sig.data), to_byte_span(request->rotating_sig.data)); - result.json = session::string8_copy_or_throw(json.data(), json.size()); + result.content_type = application_json; + result.data = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; @@ -799,16 +811,17 @@ LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_generate_pro_pr return result; } -LIBSESSION_C_API session_pro_backend_to_json -session_pro_backend_get_pro_revocations_request_to_json( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_revocations_request_build( const session_pro_backend_get_pro_revocations_request* request) { - session_pro_backend_to_json result = {}; + session_pro_backend_request result = {}; if (!request) return result; try { - std::string json = revocations_request(request->ticket).body; - result.json = session::string8_copy_or_throw(json.data(), json.size()); + result.endpoint = get_pro_revocations_endpoint; + std::string json = revocations_request(request->ticket).data; + result.content_type = application_json; + result.data = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; @@ -817,19 +830,21 @@ session_pro_backend_get_pro_revocations_request_to_json( return result; } -LIBSESSION_C_API session_pro_backend_to_json session_pro_backend_get_pro_details_request_to_json( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_details_request_build( const session_pro_backend_get_pro_details_request* request) { - session_pro_backend_to_json result = {}; + session_pro_backend_request result = {}; if (!request) return result; try { + result.endpoint = get_pro_details_endpoint; std::string json = payment_details_body( to_byte_span(request->master_pkey.data), to_byte_span(request->master_sig.data), session::as_sys_seconds(request->ts), request->count); - result.json = session::string8_copy_or_throw(json.data(), json.size()); + result.content_type = application_json; + result.data = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; @@ -1060,8 +1075,7 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r try { ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); - std::chrono::sys_seconds refund_requested_at = - session::as_sys_seconds(refund_requested_ts); + std::chrono::sys_seconds refund_requested_at = session::as_sys_seconds(refund_requested_ts); auto payment_tx_payment_id_span = to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); auto sig = refund_sig( @@ -1078,14 +1092,15 @@ session_pro_backend_signature session_pro_backend_set_payment_refund_requested_r return result; } -LIBSESSION_C_API session_pro_backend_to_json -session_pro_backend_set_payment_refund_requested_request_to_json( +LIBSESSION_C_API session_pro_backend_request +session_pro_backend_set_payment_refund_requested_request_build( const session_pro_backend_set_payment_refund_requested_request* request) { - session_pro_backend_to_json result = {}; + session_pro_backend_request result = {}; if (!request) return result; try { + result.endpoint = set_refund_endpoint; std::string json = refund_body( to_byte_span(request->master_pkey.data), session::as_sys_seconds(request->ts), @@ -1093,7 +1108,8 @@ session_pro_backend_set_payment_refund_requested_request_to_json( {request->payment_tx.provider_code, request->payment_tx.provider_code_count}, {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, to_byte_span(request->master_sig.data)); - result.json = session::string8_copy_or_throw(json.data(), json.size()); + result.content_type = application_json; + result.data = session::string8_copy_or_throw(json.data(), json.size()); result.success = true; } catch (const std::exception& e) { result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; @@ -1151,10 +1167,10 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json return result; } -LIBSESSION_C_API void session_pro_backend_to_json_free(session_pro_backend_to_json* to_json) { - if (to_json) { - free(to_json->json.data); - *to_json = {}; +LIBSESSION_C_API void session_pro_backend_request_free(session_pro_backend_request* request) { + if (request) { + free(request->data.data); + *request = {}; } } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 408ecc230..237ceb2f7 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -129,7 +129,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.error_count > 0); } - SECTION("session_pro_backend_add_pro_payment_request_to_json") { + SECTION("session_pro_backend_add_pro_payment_request_build") { session_pro_backend_add_pro_payment_request request = {}; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; @@ -149,12 +149,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.rotating_sig = sigs.rotating_sig; // Valid request - auto result = session_pro_backend_add_pro_payment_request_to_json(&request); + auto result = session_pro_backend_add_pro_payment_request_build(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.json.data != nullptr); - REQUIRE(result.json.size > 0); + REQUIRE(result.data.data != nullptr); + REQUIRE(result.data.size > 0); // Verify JSON + route match the C++ free-function implementation auto cpp = add_payment_request( @@ -163,21 +163,25 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { payment_tx.provider_code, to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT); - REQUIRE(string8_equals(result.json, cpp.body)); + REQUIRE(cpp.content_type == "application/json"); + REQUIRE(string8_equals(result.data, cpp.data)); + // The C request mirror carries the same endpoint + content_type as the C++ side. + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); + REQUIRE(std::string_view(result.content_type) == cpp.content_type); } // After freeing - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); // Null request - result = session_pro_backend_add_pro_payment_request_to_json(nullptr); + result = session_pro_backend_add_pro_payment_request_build(nullptr); REQUIRE(!result.success); - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); } - SECTION("session_pro_backend_generate_pro_proof_request_to_json") { + SECTION("session_pro_backend_generate_pro_proof_request_build") { session_pro_backend_generate_pro_proof_request request = {}; request.master_pkey = master_pubkey; request.rotating_pkey = rotating_pubkey; @@ -195,12 +199,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.rotating_sig = sigs.rotating_sig; // Valid request - auto result = session_pro_backend_generate_pro_proof_request_to_json(&request); + auto result = session_pro_backend_generate_pro_proof_request_build(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.json.data != nullptr); - REQUIRE(result.json.size > 0); + REQUIRE(result.data.data != nullptr); + REQUIRE(result.data.size > 0); // Verify JSON + route match the C++ free-function implementation auto cpp = pro_proof_request( @@ -208,50 +212,50 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { rotating_privkey.data, session::as_sys_seconds(unix_ts)); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT); - REQUIRE(string8_equals(result.json, cpp.body)); + REQUIRE(string8_equals(result.data, cpp.data)); } // After freeing - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); // Null request - result = session_pro_backend_generate_pro_proof_request_to_json(nullptr); + result = session_pro_backend_generate_pro_proof_request_build(nullptr); REQUIRE(!result.success); - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); } - SECTION("session_pro_backend_get_pro_revocations_request_to_json") { + SECTION("session_pro_backend_get_pro_revocations_request_build") { session_pro_backend_get_pro_revocations_request request = {}; request.ticket = 123; // Valid request - auto result = session_pro_backend_get_pro_revocations_request_to_json(&request); + auto result = session_pro_backend_get_pro_revocations_request_build(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.json.data != nullptr); - REQUIRE(result.json.size > 0); + REQUIRE(result.data.data != nullptr); + REQUIRE(result.data.size > 0); // Verify JSON + route match the C++ free-function implementation auto cpp = revocations_request(request.ticket); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT); - REQUIRE(string8_equals(result.json, cpp.body)); + REQUIRE(string8_equals(result.data, cpp.data)); } // After freeing - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); // Null request - result = session_pro_backend_get_pro_revocations_request_to_json(nullptr); + result = session_pro_backend_get_pro_revocations_request_build(nullptr); REQUIRE(!result.success); - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); } - SECTION("session_pro_backend_get_pro_details_request_to_json") { + SECTION("session_pro_backend_get_pro_details_request_build") { session_pro_backend_get_pro_details_request request = {}; request.master_pkey = master_pubkey; request.ts = unix_ts; @@ -267,29 +271,29 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { request.master_sig = sig.sig; // Valid request - auto result = session_pro_backend_get_pro_details_request_to_json(&request); + auto result = session_pro_backend_get_pro_details_request_build(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.json.data != nullptr); - REQUIRE(result.json.size > 0); + REQUIRE(result.data.data != nullptr); + REQUIRE(result.data.size > 0); // Verify JSON + route match the C++ free-function implementation auto cpp = payment_details_request( master_privkey.data, session::as_sys_seconds(unix_ts), request.count); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT); - REQUIRE(string8_equals(result.json, cpp.body)); + REQUIRE(string8_equals(result.data, cpp.data)); } // After freeing - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); // Null request - result = session_pro_backend_get_pro_details_request_to_json(nullptr); + result = session_pro_backend_get_pro_details_request_build(nullptr); REQUIRE(!result.success); - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); } SECTION("session_pro_backend_pro_proof_response_parse") { @@ -588,10 +592,10 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("Memory management edge cases") { // Test freeing null/empty structs - session_pro_backend_to_json to_json = {}; - session_pro_backend_to_json_free(&to_json); - REQUIRE(to_json.json.data == nullptr); - REQUIRE(to_json.json.size == 0); + session_pro_backend_request to_json = {}; + session_pro_backend_request_free(&to_json); + REQUIRE(to_json.data.data == nullptr); + REQUIRE(to_json.data.size == 0); session_pro_backend_pro_proof_response proof_response = {}; session_pro_backend_pro_proof_response_free(&proof_response); @@ -606,7 +610,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(pay_response.header.internal_arena_buf_ == nullptr); } - SECTION("session_pro_backend_set_payment_refund_requested_request_to_json") { + SECTION("session_pro_backend_set_payment_refund_requested_request_build") { session_pro_backend_set_payment_refund_requested_request request = {}; request.master_pkey = master_pubkey; request.ts = unix_ts; @@ -626,13 +630,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(sig.success); // Valid request - auto result = - session_pro_backend_set_payment_refund_requested_request_to_json(&request); + auto result = session_pro_backend_set_payment_refund_requested_request_build(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.json.data != nullptr); - REQUIRE(result.json.size > 0); + REQUIRE(result.data.data != nullptr); + REQUIRE(result.data.size > 0); // Verify JSON + route match the C++ free-function implementation auto cpp = refund_request( @@ -642,18 +645,18 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { payment_tx.provider_code, to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT); - REQUIRE(string8_equals(result.json, cpp.body)); + REQUIRE(string8_equals(result.data, cpp.data)); } // After freeing - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); // Null request - result = session_pro_backend_set_payment_refund_requested_request_to_json(nullptr); + result = session_pro_backend_set_payment_refund_requested_request_build(nullptr); REQUIRE(!result.success); - REQUIRE(result.json.data == nullptr); - REQUIRE(result.json.size == 0); + REQUIRE(result.data.data == nullptr); + REQUIRE(result.data.size == 0); } SECTION("session_pro_backend_set_payment_refund_requested_response_parse") { @@ -805,16 +808,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.master_sig = add_pro_sigs.master_sig; request.rotating_sig = add_pro_sigs.rotating_sig; - session_pro_backend_to_json request_json = - session_pro_backend_add_pro_payment_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_add_pro_payment_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/add_pro_payment", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_pro_proof_response response = @@ -860,16 +863,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.master_sig = pro_sigs.master_sig; request.rotating_sig = pro_sigs.rotating_sig; - session_pro_backend_to_json request_json = - session_pro_backend_generate_pro_proof_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_generate_pro_proof_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; // Do CURL request std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/generate_pro_proof", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_pro_proof_response response = @@ -894,7 +897,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.rotating_pkey.data, sizeof(request.rotating_pkey.data)) == 0); - session_pro_backend_to_json_free(&request_json); + session_pro_backend_request_free(&request_json); session_pro_backend_pro_proof_response_free(&response); } @@ -912,15 +915,15 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.master_sig = sig.sig; // Do CURL request - session_pro_backend_to_json request_json = - session_pro_backend_get_pro_details_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_get_pro_details_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/get_pro_details", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_get_pro_details_response response = @@ -954,15 +957,15 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.master_sig = sig.sig; // Do CURL request - session_pro_backend_to_json request_json = - session_pro_backend_get_pro_details_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_get_pro_details_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/get_pro_details", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_get_pro_details_response response = @@ -1024,16 +1027,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.master_sig = add_pro_sigs.master_sig; request.rotating_sig = add_pro_sigs.rotating_sig; - session_pro_backend_to_json request_json = - session_pro_backend_add_pro_payment_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_add_pro_payment_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/add_pro_payment", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_pro_proof_response response = @@ -1056,16 +1059,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Build request session_pro_backend_get_pro_revocations_request request = {}; - session_pro_backend_to_json request_json = - session_pro_backend_get_pro_revocations_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_get_pro_revocations_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/get_pro_revocations", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_get_pro_revocations_response response = @@ -1109,16 +1112,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { request.refund_requested_ts = now_unix_ts; request.payment_tx = another_payment_tx; - session_pro_backend_to_json request_json = - session_pro_backend_set_payment_refund_requested_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session_pro_backend_request request_json = + session_pro_backend_set_payment_refund_requested_request_build(&request); + scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, g_test_pro_backend_dev_server_url + "/set_payment_refund_requested", - std::string_view(request_json.json.data, request_json.json.size)); + std::string_view(request_json.data.data, request_json.data.size)); // Parse response session_pro_backend_set_payment_refund_requested_response response = From d92d6c744045dd979b5d808a8d1e413e0587f5fd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 17:12:24 -0300 Subject: [PATCH 120/193] clang-format fixups after the *_unix_ts -> *_at rename Co-Authored-By: Claude Opus 4.8 (1M context) --- src/core/pro.cpp | 6 +----- src/session_protocol.cpp | 4 ++-- tests/test_session_protocol.cpp | 6 +++--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/core/pro.cpp b/src/core/pro.cpp index c15c3b9ec..5e23e3791 100644 --- a/src/core/pro.cpp +++ b/src/core/pro.cpp @@ -48,11 +48,7 @@ void Pro::update_revocations( " ON CONFLICT (revocation_tag) DO UPDATE SET" " effective_ts = excluded.effective_ts, seen_at = excluded.seen_at"); const auto& revoke : revocations) { - exec_query( - st, - revoke.revocation_tag, - revoke.effective_at.time_since_epoch().count(), - now); + exec_query(st, revoke.revocation_tag, revoke.effective_at.time_since_epoch().count(), now); st->reset(); } diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 0af6daeff..68d395f3c 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -162,8 +162,8 @@ ProStatus ProProof::status( } b32 ProProof::hash() const { - b32 result = proof_hash_internal( - revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_at)); + b32 result = + proof_hash_internal(revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_at)); return result; } diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 3fe200284..c4759cc85 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -36,9 +36,9 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se // Create protobuf `Content.dataMessage` SessionProtos::Content content = {}; - content.set_sigtimestamp(std::chrono::duration_cast( - content_at.time_since_epoch()) - .count()); + content.set_sigtimestamp( + std::chrono::duration_cast(content_at.time_since_epoch()) + .count()); SessionProtos::DataMessage* data = content.mutable_datamessage(); data->set_body(std::string(data_body)); From fcd65cd3c2d13a4ecbcae317f19d523a0a1a2b16 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 20:15:23 -0300 Subject: [PATCH 121/193] Pro C API: collapse request builders into thin C++ wrappers The C request API reimplemented the wire pipeline: a per-request _build_sigs that signed, an input struct the caller hand-filled with pubkeys + precomputed sigs, and a _request_build that re-serialized the body from them. Replace all of it with loose-arg builders that take the same inputs as the C++ *_request() functions (private keys + params) and just call them -- one wire implementation, living in C++. session_pro_backend_request now owns the built C++ ProRequest in an opaque void* and points endpoint/content_type/data into it (zero copy); request_free deletes it. Deletes the _build_sigs/_build_sig functions, the per-request input structs, the add_pro_payment_user_transaction struct, and the master_rotating_signatures/signature return structs. Tests and the dev-server integration block are rewritten to the single-call API. Also tightens the internal *_hash/*_body helpers to fixed-extent spans (span<...,32> for pubkeys, span<...,64> for sigs) so sizes are enforced at compile time rather than silently truncated. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 259 ++++-------------- src/pro_backend.cpp | 267 ++++++------------ tests/test_pro_backend.cpp | 495 +++++++++------------------------- 3 files changed, 270 insertions(+), 751 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 68c93d4c6..cfc65ad15 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -116,57 +116,8 @@ struct session_pro_backend_request { const char* content_type; /// The opaque request payload. Relay untouched; do not parse or modify. string8 data; -}; - -typedef struct session_pro_backend_master_rotating_signatures - session_pro_backend_master_rotating_signatures; -struct session_pro_backend_master_rotating_signatures { - bool success; - char error[256]; - size_t error_count; - cbytes64 master_sig; - cbytes64 rotating_sig; -}; - -typedef struct session_pro_backend_signature session_pro_backend_signature; -struct session_pro_backend_signature { - bool success; - char error[256]; - size_t error_count; - cbytes64 sig; -}; - -typedef struct session_pro_backend_add_pro_payment_user_transaction - session_pro_backend_add_pro_payment_user_transaction; -struct session_pro_backend_add_pro_payment_user_transaction { - /// Provider code string (see SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*); opaque slug - char provider_code[64]; - size_t provider_code_count; - /// Opaque payment identifier from the provider's purchase flow. Multi-part providers fold their - /// parts into this one string per a backend-defined composite (e.g. Google "token|order_id"); - /// libsession treats it as opaque bytes hashed verbatim. - char payment_id[128]; - size_t payment_id_count; -}; - -typedef struct session_pro_backend_add_pro_payment_request - session_pro_backend_add_pro_payment_request; -struct session_pro_backend_add_pro_payment_request { - cbytes32 master_pkey; - cbytes32 rotating_pkey; - session_pro_backend_add_pro_payment_user_transaction payment_tx; - cbytes64 master_sig; - cbytes64 rotating_sig; -}; - -typedef struct session_pro_backend_generate_pro_proof_request - session_pro_backend_generate_pro_proof_request; -struct session_pro_backend_generate_pro_proof_request { - cbytes32 master_pkey; - cbytes32 rotating_pkey; - int64_t ts; - cbytes64 master_sig; - cbytes64 rotating_sig; + /// Owned C++ object backing endpoint/content_type/data; do not touch (freed by request_free). + void* internal_; }; typedef struct session_pro_backend_pro_proof_response session_pro_backend_pro_proof_response; @@ -175,12 +126,6 @@ struct session_pro_backend_pro_proof_response { session_protocol_pro_proof proof; }; -typedef struct session_pro_backend_get_pro_revocations_request - session_pro_backend_get_pro_revocations_request; -struct session_pro_backend_get_pro_revocations_request { - int64_t ticket; -}; - typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; struct session_pro_backend_pro_revocation_item { cbytes32 revocation_tag; @@ -200,15 +145,6 @@ struct session_pro_backend_get_pro_revocations_response { size_t items_count; }; -typedef struct session_pro_backend_get_pro_details_request - session_pro_backend_get_pro_details_request; -struct session_pro_backend_get_pro_details_request { - cbytes32 master_pkey; - cbytes64 master_sig; - int64_t ts; - uint32_t count; -}; - typedef struct session_pro_backend_pro_payment_item session_pro_backend_pro_payment_item; struct session_pro_backend_pro_payment_item { /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); @@ -258,16 +194,6 @@ struct session_pro_backend_get_pro_details_response { uint32_t payments_total; }; -typedef struct session_pro_backend_set_payment_refund_requested_request - session_pro_backend_set_payment_refund_requested_request; -struct session_pro_backend_set_payment_refund_requested_request { - cbytes32 master_pkey; - cbytes64 master_sig; - int64_t ts; - int64_t refund_requested_ts; - session_pro_backend_add_pro_payment_user_transaction payment_tx; -}; - typedef struct session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response; struct session_pro_backend_set_payment_refund_requested_response { @@ -275,130 +201,73 @@ struct session_pro_backend_set_payment_refund_requested_response { bool updated; }; -/// API: session_pro_backend/add_pro_payment_request_build_sigs +/// API: session_pro_backend/add_pro_payment_request_build /// -/// Builds master and rotating signatures for an `add_pro_payment_request`. -/// Returns false if the keys (32-byte or 64-byte libsodium format) or payment token hash are -/// incorrectly sized. Using 64-byte libsodium keys is more efficient. +/// Builds the add-payment request to POST, as a session_pro_backend_request (endpoint + +/// content_type + opaque data). Free it with `session_pro_backend_request_free`. On a key-size +/// error `success` is false and `error`/`error_count` describe it. /// /// Inputs: -/// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). -/// - `master_privkey_len` -- Length of master_privkey. -/// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). -/// - `rotating_privkey_len` -- Length of rotating_privkey. -/// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from -/// (use a SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constant) -/// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See -/// `AddProPaymentUserTransaction` -/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload -/// -/// Outputs: -/// - `success` - True if signatures are built successfully, false otherwise. -/// - `error` - Backing error buffer for the signatures if `success` is false -/// - `errors_count` - length of the error if `success` is false -/// - `master_sig` - Generated master signature -/// - `rotating_sig` - Generated rotating signature +/// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte +/// libsodium). +/// - `rotating_privkey` / `rotating_privkey_len` -- Ed25519 rotating private key (32 or 64-byte). +/// - `provider_code` -- null-terminated provider code +/// (SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). +/// - `payment_id` / `payment_id_len` -- opaque provider payment identifier. LIBSESSION_EXPORT -session_pro_backend_master_rotating_signatures -session_pro_backend_add_pro_payment_request_build_sigs( +session_pro_backend_request session_pro_backend_add_pro_payment_request_build( const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(1, 3, 5, 6); + const char* provider_code, + const unsigned char* payment_id, + size_t payment_id_len) NON_NULL_ARG(1, 3, 5, 6); -/// API: session_pro_backend/generate_pro_proof_request_build_sigs +/// API: session_pro_backend/generate_pro_proof_request_build /// -/// Builds master and rotating signatures for a `generate_pro_proof_request`. -/// Returns false if the keys (32-byte or 64-byte libsodium format) are incorrectly sized. -/// Using 64-byte libsodium keys is more efficient. +/// Builds the generate-proof request to POST, as a session_pro_backend_request (endpoint + +/// content_type + opaque data). Free it with `session_pro_backend_request_free`. On a key-size +/// error `success` is false and `error`/`error_count` describe it. /// /// Inputs: -/// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). -/// - `master_privkey_len` -- Length of master_privkey. -/// - `rotating_privkey` -- Ed25519 rotating private key (32-byte or 64-byte libsodium format). -/// - `rotating_privkey_len` -- Length of rotating_privkey. -/// - `ts` -- Unix timestamp for the request. -/// -/// Outputs: -/// - `bool` - True if signatures are built successfully, false otherwise. -/// - `error` - Backing error buffer for the signatures if `success` is false -/// - `errors_count` - length of the error if `success` is false -/// - `master_sig` - Master signature -/// - `rotating_sig` - Rotating signature +/// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte +/// libsodium). +/// - `rotating_privkey` / `rotating_privkey_len` -- Ed25519 rotating private key (32 or 64-byte). +/// - `ts` -- Unix timestamp (seconds) for the request. LIBSESSION_EXPORT -session_pro_backend_master_rotating_signatures -session_pro_backend_generate_pro_proof_request_build_sigs( +session_pro_backend_request session_pro_backend_generate_pro_proof_request_build( const unsigned char* master_privkey, size_t master_privkey_len, const unsigned char* rotating_privkey, size_t rotating_privkey_len, int64_t ts) NON_NULL_ARG(1, 3); -/// API: session_pro_backend/get_pro_details_request_build_sig -/// -/// Builds the JSON for a `get_pro_details_request`. Returns false if the keys (32-byte or -/// 64-byte libsodium format) are incorrectly sized. Using 64-byte libsodium keys is more efficient. -/// -/// Inputs: -/// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). -/// - `master_privkey_len` -- Length of master_privkey. -/// - `ts` -- Unix timestamp for the request. -/// - `count` -- Amount of historical payments to request -/// -/// Outputs: -/// - `bool` -- True if signatures are built successfully, false otherwise. -/// - `error` -- Backing error buffer for the signatures if `success` is false -/// - `errors_count` -- length of the error if `success` is false -/// - `sig` -- The generated signature -LIBSESSION_EXPORT -session_pro_backend_signature session_pro_backend_get_pro_details_request_build_sig( - const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, uint32_t count) - NON_NULL_ARG(1); - -/// API: session_pro_backend/add_pro_payment_request_build +/// API: session_pro_backend/get_pro_revocations_request_build /// -/// Builds the request to POST for an `add_pro_payment_request`, as a session_pro_backend_request -/// (endpoint + content_type + opaque data). -/// Free the returned request with `session_pro_backend_request_free`. +/// Builds the get-revocations request to POST, as a session_pro_backend_request (endpoint + +/// content_type + opaque data). Free it with `session_pro_backend_request_free`. /// /// Inputs: -/// - `request` -- Pointer to the request struct. +/// - `ticket` -- revocation-list ticket to resume from (0 for a full list). LIBSESSION_EXPORT -session_pro_backend_request session_pro_backend_add_pro_payment_request_build( - const session_pro_backend_add_pro_payment_request* request); +session_pro_backend_request session_pro_backend_get_pro_revocations_request_build(int64_t ticket); -/// API: session_pro_backend/generate_pro_proof_request_build +/// API: session_pro_backend/get_pro_details_request_build /// -/// Builds the request to POST for a `generate_pro_proof_request`, as a session_pro_backend_request -/// (endpoint + content_type + opaque data). -/// Free the returned request with `session_pro_backend_request_free`. +/// Builds the get-details request to POST, as a session_pro_backend_request (endpoint + +/// content_type + opaque data). Free it with `session_pro_backend_request_free`. On a key-size +/// error `success` is false and `error`/`error_count` describe it. /// /// Inputs: -/// - `request` -- Pointer to the request struct. -LIBSESSION_EXPORT -session_pro_backend_request session_pro_backend_generate_pro_proof_request_build( - const session_pro_backend_generate_pro_proof_request* request); - -/// API: session_pro_backend/get_pro_revocations_request_build -/// -/// Builds the request to POST for a `get_pro_revocations_request`, as a session_pro_backend_request -/// (endpoint + content_type + opaque data). -/// Free the returned request with `session_pro_backend_request_free`. -LIBSESSION_EXPORT -session_pro_backend_request session_pro_backend_get_pro_revocations_request_build( - const session_pro_backend_get_pro_revocations_request* request); - -/// API: session_pro_backend/get_pro_details_request_build -/// -/// Builds the request to POST for a `get_pro_details_request`, as a session_pro_backend_request -/// (endpoint + content_type + opaque data). -/// Free the returned request with `session_pro_backend_request_free`. +/// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte +/// libsodium). +/// - `ts` -- Unix timestamp (seconds) for the request. +/// - `count` -- number of historical payments to request. LIBSESSION_EXPORT session_pro_backend_request session_pro_backend_get_pro_details_request_build( - const session_pro_backend_get_pro_details_request* request); + const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, uint32_t count) + NON_NULL_ARG(1); /// API: session_pro_backend/pro_proof_response_parse /// @@ -437,47 +306,29 @@ LIBSESSION_EXPORT session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details_response_parse( const char* json, size_t json_len); -/// API: session_pro_backend/set_payment_refund_requested_request_build_sigs +/// API: session_pro_backend/set_payment_refund_requested_request_build /// -/// Builds master and rotating signatures for an `set_payment_refund_requested_request`. -/// Returns false if the keys (32-byte or 64-byte libsodium format) or payment token hash are -/// incorrectly sized. Using 64-byte libsodium keys is more efficient. +/// Builds the set-refund-requested request to POST, as a session_pro_backend_request (endpoint + +/// content_type + opaque data). Free it with `session_pro_backend_request_free`. On a key-size +/// error `success` is false and `error`/`error_count` describe it. /// /// Inputs: -/// - `master_privkey` -- Ed25519 master private key (32-byte or 64-byte libsodium format). -/// - `master_privkey_len` -- Length of master_privkey. -/// - `ts` -- Unix timestamp for the request -/// - `refund_requested_ts` -- Unix timestamp to set as the timestamp that a refund was -/// requested on this payment -/// - `payment_tx_provider_code` -- Null-terminated provider code string the payment is coming from -/// (use a SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constant) -/// - `payment_tx_payment_id` -- Opaque payment identifier from the provider. See -/// `AddProPaymentUserTransaction` -/// - `payment_tx_payment_id_len` -- Length of the `payment_tx_payment_id` payload -/// -/// Outputs: -/// - `bool` -- True if signatures are built successfully, false otherwise. -/// - `error` -- Backing error buffer for the signatures if `success` is false -/// - `errors_count` -- length of the error if `success` is false -/// - `sig` -- The generated signature +/// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte +/// libsodium). +/// - `ts` -- Unix timestamp (seconds) for the request. +/// - `refund_requested_ts` -- Unix timestamp (seconds) to record the refund request at. +/// - `provider_code` -- null-terminated provider code +/// (SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). +/// - `payment_id` / `payment_id_len` -- opaque provider payment identifier. LIBSESSION_EXPORT -session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( +session_pro_backend_request session_pro_backend_set_payment_refund_requested_request_build( const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) NON_NULL_ARG(1, 5, 6); - -/// API: session_pro_backend/set_payment_refund_requested_request_build -/// -/// Builds the request to POST for a `set_payment_refund_requested_request`, as a -/// session_pro_backend_request (endpoint + content_type + opaque data). Free the returned request -/// with `session_pro_backend_request_free`. -LIBSESSION_EXPORT -session_pro_backend_request session_pro_backend_set_payment_refund_requested_request_build( - const session_pro_backend_set_payment_refund_requested_request* request); + const char* provider_code, + const unsigned char* payment_id, + size_t payment_id_len) NON_NULL_ARG(1, 5, 6); /// API: session_pro_backend/set_payment_refund_requested_response_parse /// diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 1291173ce..2e25b542a 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -162,8 +162,8 @@ namespace { // --- add-payment (endpoint add_pro_payment) --- b32 add_payment_hash( - std::span master_pubkey, - std::span rotating_pubkey, + std::span master_pubkey, + std::span rotating_pubkey, std::string_view provider_code, std::span payment_id) { // Must match the add-payment signed-request hash in pro-wire-protocol.md §3.2 (+ §3.5). @@ -174,12 +174,12 @@ namespace { // Serialise an add-payment request body from already-computed fields (shared by the C++ // add_payment_request and the C ..._request_build wrapper). std::string add_payment_body( - std::span master_pubkey, - std::span rotating_pubkey, + std::span master_pubkey, + std::span rotating_pubkey, std::string_view provider_code, std::string_view payment_id, - std::span master_sig, - std::span rotating_sig) { + std::span master_sig, + std::span rotating_sig) { return nlohmann::json{ {"master_pkey", oxenc::to_hex(master_pubkey)}, {"rotating_pkey", oxenc::to_hex(rotating_pubkey)}, @@ -326,8 +326,8 @@ namespace { // --- generate-proof (endpoint generate_pro_proof) --- b32 generate_proof_hash( - std::span master_pubkey, - std::span rotating_pubkey, + std::span master_pubkey, + std::span rotating_pubkey, std::chrono::sys_seconds unix_ts) { // Must match the generate-proof signed-request hash in pro-wire-protocol.md §3.1. int64_t ts = epoch_seconds(unix_ts); @@ -335,11 +335,11 @@ namespace { } std::string generate_proof_body( - std::span master_pubkey, - std::span rotating_pubkey, + std::span master_pubkey, + std::span rotating_pubkey, std::chrono::sys_seconds unix_ts, - std::span master_sig, - std::span rotating_sig) { + std::span master_sig, + std::span rotating_sig) { return nlohmann::json{ {"master_pkey", oxenc::to_hex(master_pubkey)}, {"rotating_pkey", oxenc::to_hex(rotating_pubkey)}, @@ -443,7 +443,7 @@ namespace { // --- payment-details / get-pro-details (endpoint get_pro_details) --- b32 payment_details_hash( - std::span master_pubkey, + std::span master_pubkey, std::chrono::sys_seconds unix_ts, uint32_t count) { // Must match the get-pro-details signed-request hash in pro-wire-protocol.md §3.4. @@ -452,8 +452,8 @@ namespace { } std::string payment_details_body( - std::span master_pubkey, - std::span master_sig, + std::span master_pubkey, + std::span master_sig, std::chrono::sys_seconds unix_ts, uint32_t count) { return nlohmann::json{ @@ -593,7 +593,7 @@ namespace { // --- refund / set-payment-refund-requested (endpoint set_payment_refund_requested) --- b32 refund_hash( - std::span master_pubkey, + std::span master_pubkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, @@ -612,12 +612,12 @@ namespace { } std::string refund_body( - std::span master_pubkey, + std::span master_pubkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::string_view payment_id, - std::span master_sig) { + std::span master_sig) { return nlohmann::json{ {"master_pkey", oxenc::to_hex(master_pubkey)}, {"ts", epoch_seconds(unix_ts)}, @@ -694,162 +694,89 @@ using namespace session::pro_backend; static string8 C_PARSE_ERROR_OUT_OF_MEMORY = STRING8_LIT("Ran out-of-memory creating C response"); static string8 C_PARSE_ERROR_INVALID_ARGS = STRING8_LIT("One or more C arguments were NULL"); -LIBSESSION_C_API session_pro_backend_master_rotating_signatures -session_pro_backend_add_pro_payment_request_build_sigs( - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) { - - session_pro_backend_master_rotating_signatures result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto payment_tx_payment_id_span = - to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - - auto sigs = add_payment_sigs( - master_span, rotating_span, payment_tx_provider_code, payment_tx_payment_id_span); - std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); - std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } +// Wrap a freshly-built C++ ProRequest as an owning C session_pro_backend_request: heap-own the +// ProRequest and point endpoint/content_type/data into it (zero copy). Released by +// session_pro_backend_request_free. +static session_pro_backend_request c_own_request(ProRequest&& req) { + auto* owned = new ProRequest(std::move(req)); + session_pro_backend_request result = {}; + result.internal_ = owned; + result.endpoint = owned->endpoint.data(); + result.content_type = owned->content_type.data(); + result.data = string8{owned->data.data(), owned->data.size()}; + result.success = true; return result; } -LIBSESSION_C_API session_pro_backend_master_rotating_signatures -session_pro_backend_generate_pro_proof_request_build_sigs( - const unsigned char* master_privkey, - size_t master_privkey_len, - const unsigned char* rotating_privkey, - size_t rotating_privkey_len, - int64_t ts) { - - session_pro_backend_master_rotating_signatures result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - ed25519::PrivKeySpan rotating_span{rotating_privkey, rotating_privkey_len}; - auto sigs = pro_proof_sigs(master_span, rotating_span, session::as_sys_seconds(ts)); - std::memcpy(result.master_sig.data, sigs.master_sig.data(), sigs.master_sig.size()); - std::memcpy(result.rotating_sig.data, sigs.rotating_sig.data(), sigs.rotating_sig.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; +// Fill a session_pro_backend_request's error buffer from a caught exception. +static void c_request_error(session_pro_backend_request& result, const std::exception& e) { + result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; } -LIBSESSION_C_API session_pro_backend_signature -session_pro_backend_get_pro_details_request_build_sig( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_add_pro_payment_request_build( const unsigned char* master_privkey, size_t master_privkey_len, - int64_t ts, - uint32_t count) { - session_pro_backend_signature result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - auto sig = payment_details_sig(master_span, session::as_sys_seconds(ts), count); - std::memcpy(result.sig.data, sig.data(), sig.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; -} - -LIBSESSION_C_API session_pro_backend_request session_pro_backend_add_pro_payment_request_build( - const session_pro_backend_add_pro_payment_request* request) { + const unsigned char* rotating_privkey, + size_t rotating_privkey_len, + const char* provider_code, + const unsigned char* payment_id, + size_t payment_id_len) { session_pro_backend_request result = {}; - if (!request) - return result; - try { - result.endpoint = add_payment_endpoint; - std::string json = add_payment_body( - to_byte_span(request->master_pkey.data), - to_byte_span(request->rotating_pkey.data), - {request->payment_tx.provider_code, request->payment_tx.provider_code_count}, - {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, - to_byte_span(request->master_sig.data), - to_byte_span(request->rotating_sig.data)); - result.content_type = application_json; - result.data = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; + result = c_own_request(add_payment_request( + ed25519::PrivKeySpan{master_privkey, master_privkey_len}, + ed25519::PrivKeySpan{rotating_privkey, rotating_privkey_len}, + provider_code, + to_byte_span(payment_id, payment_id_len))); } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; + c_request_error(result, e); } - return result; } LIBSESSION_C_API session_pro_backend_request session_pro_backend_generate_pro_proof_request_build( - const session_pro_backend_generate_pro_proof_request* request) { + const unsigned char* master_privkey, + size_t master_privkey_len, + const unsigned char* rotating_privkey, + size_t rotating_privkey_len, + int64_t ts) { session_pro_backend_request result = {}; - if (!request) - return result; - try { - result.endpoint = generate_proof_endpoint; - std::string json = generate_proof_body( - to_byte_span(request->master_pkey.data), - to_byte_span(request->rotating_pkey.data), - session::as_sys_seconds(request->ts), - to_byte_span(request->master_sig.data), - to_byte_span(request->rotating_sig.data)); - result.content_type = application_json; - result.data = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; + result = c_own_request(pro_proof_request( + ed25519::PrivKeySpan{master_privkey, master_privkey_len}, + ed25519::PrivKeySpan{rotating_privkey, rotating_privkey_len}, + session::as_sys_seconds(ts))); } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; + c_request_error(result, e); } - return result; } -LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_revocations_request_build( - const session_pro_backend_get_pro_revocations_request* request) { +LIBSESSION_C_API session_pro_backend_request +session_pro_backend_get_pro_revocations_request_build(int64_t ticket) { session_pro_backend_request result = {}; - if (!request) - return result; - try { - result.endpoint = get_pro_revocations_endpoint; - std::string json = revocations_request(request->ticket).data; - result.content_type = application_json; - result.data = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; + result = c_own_request(revocations_request(ticket)); } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; + c_request_error(result, e); } - return result; } LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_details_request_build( - const session_pro_backend_get_pro_details_request* request) { + const unsigned char* master_privkey, + size_t master_privkey_len, + int64_t ts, + uint32_t count) { session_pro_backend_request result = {}; - if (!request) - return result; - try { - result.endpoint = get_pro_details_endpoint; - std::string json = payment_details_body( - to_byte_span(request->master_pkey.data), - to_byte_span(request->master_sig.data), - session::as_sys_seconds(request->ts), - request->count); - result.content_type = application_json; - result.data = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; + result = c_own_request(payment_details_request( + ed25519::PrivKeySpan{master_privkey, master_privkey_len}, + session::as_sys_seconds(ts), + count)); } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; + c_request_error(result, e); } - return result; } @@ -1062,59 +989,26 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json return result; } -LIBSESSION_C_API -session_pro_backend_signature session_pro_backend_set_payment_refund_requested_request_build_sigs( +LIBSESSION_C_API session_pro_backend_request +session_pro_backend_set_payment_refund_requested_request_build( const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, int64_t refund_requested_ts, - const char* payment_tx_provider_code, - const unsigned char* payment_tx_payment_id, - size_t payment_tx_payment_id_len) { - session_pro_backend_signature result = {}; - try { - ed25519::PrivKeySpan master_span{master_privkey, master_privkey_len}; - std::chrono::sys_seconds unix_ts = session::as_sys_seconds(ts); - std::chrono::sys_seconds refund_requested_at = session::as_sys_seconds(refund_requested_ts); - auto payment_tx_payment_id_span = - to_byte_span(payment_tx_payment_id, payment_tx_payment_id_len); - auto sig = refund_sig( - master_span, - unix_ts, - refund_requested_at, - payment_tx_provider_code, - payment_tx_payment_id_span); - std::memcpy(result.sig.data, sig.data(), sig.size()); - result.success = true; - } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; - } - return result; -} - -LIBSESSION_C_API session_pro_backend_request -session_pro_backend_set_payment_refund_requested_request_build( - const session_pro_backend_set_payment_refund_requested_request* request) { + const char* provider_code, + const unsigned char* payment_id, + size_t payment_id_len) { session_pro_backend_request result = {}; - if (!request) - return result; - try { - result.endpoint = set_refund_endpoint; - std::string json = refund_body( - to_byte_span(request->master_pkey.data), - session::as_sys_seconds(request->ts), - session::as_sys_seconds(request->refund_requested_ts), - {request->payment_tx.provider_code, request->payment_tx.provider_code_count}, - {request->payment_tx.payment_id, request->payment_tx.payment_id_count}, - to_byte_span(request->master_sig.data)); - result.content_type = application_json; - result.data = session::string8_copy_or_throw(json.data(), json.size()); - result.success = true; + result = c_own_request(refund_request( + ed25519::PrivKeySpan{master_privkey, master_privkey_len}, + session::as_sys_seconds(ts), + session::as_sys_seconds(refund_requested_ts), + provider_code, + to_byte_span(payment_id, payment_id_len))); } catch (const std::exception& e) { - result.error_count = session::copy_c_str(result.error, sizeof(result.error), e.what()) - 1; + c_request_error(result, e); } - return result; } @@ -1169,7 +1063,8 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json LIBSESSION_C_API void session_pro_backend_request_free(session_pro_backend_request* request) { if (request) { - free(request->data.data); + if (request->internal_) + delete static_cast(request->internal_); *request = {}; } } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 237ceb2f7..177e0e0e1 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -37,263 +37,128 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + oxenc::to_hex(fake_google_order_id); - session_pro_backend_add_pro_payment_user_transaction payment_tx = {}; - payment_tx.provider_code_count = - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; - std::memcpy( - payment_tx.provider_code, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); - payment_tx.payment_id_count = fake_payment_id.size(); - std::memcpy(payment_tx.payment_id, fake_payment_id.data(), payment_tx.payment_id_count); + const char* provider_code = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY; + auto payment_id = reinterpret_cast(fake_payment_id.data()); + size_t payment_id_len = fake_payment_id.size(); int64_t unix_ts = 1698765432; // Arbitrary timestamp (unix epoch seconds) - SECTION("session_pro_backend_add_pro_payment_request_build_sigs") { - // Valid inputs - session_pro_backend_master_rotating_signatures result = - session_pro_backend_add_pro_payment_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - payment_tx.provider_code, - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count); - INFO(result.error); - REQUIRE(result.success); - REQUIRE(result.error_count == 0); - - // Verify signatures match C++ implementation - auto cpp = add_payment_sigs( - master_privkey.data, - rotating_privkey.data, - payment_tx.provider_code, - to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); - - REQUIRE(std::memcmp( - result.master_sig.data, - cpp.master_sig.data(), - sizeof(result.master_sig.data)) == 0); - REQUIRE(std::memcmp( - result.rotating_sig.data, - cpp.rotating_sig.data(), - sizeof(result.rotating_sig.data)) == 0); - - // Invalid master key size - result = session_pro_backend_add_pro_payment_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data) - 1, - rotating_privkey.data, - sizeof(rotating_privkey.data), - payment_tx.provider_code, - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count); - REQUIRE(!result.success); - REQUIRE(result.error_count > 0); - } - - SECTION("session_pro_backend_generate_pro_proof_request_build_sigs") { - session_pro_backend_master_rotating_signatures result = {}; - - // Valid inputs - result = session_pro_backend_generate_pro_proof_request_build_sigs( + SECTION("session_pro_backend_add_pro_payment_request_build") { + auto result = session_pro_backend_add_pro_payment_request_build( master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - unix_ts); - REQUIRE(result.success); - REQUIRE(result.error_count == 0); - - // Verify signatures match C++ implementation - auto cpp_sigs = pro_proof_sigs( - master_privkey.data, rotating_privkey.data, session::as_sys_seconds(unix_ts)); - REQUIRE(std::memcmp( - result.master_sig.data, - cpp_sigs.master_sig.data(), - sizeof(result.master_sig.data)) == 0); - REQUIRE(std::memcmp( - result.rotating_sig.data, - cpp_sigs.rotating_sig.data(), - sizeof(result.rotating_sig.data)) == 0); - - // Invalid rotating key size - result = session_pro_backend_generate_pro_proof_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data) - 1, - unix_ts); - REQUIRE(!result.success); - REQUIRE(result.error_count > 0); - } - - SECTION("session_pro_backend_add_pro_payment_request_build") { - session_pro_backend_add_pro_payment_request request = {}; - request.master_pkey = master_pubkey; - request.rotating_pkey = rotating_pubkey; - request.payment_tx = payment_tx; - - session_pro_backend_master_rotating_signatures sigs = - session_pro_backend_add_pro_payment_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - payment_tx.provider_code, - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count); - - request.master_sig = sigs.master_sig; - request.rotating_sig = sigs.rotating_sig; - - // Valid request - auto result = session_pro_backend_add_pro_payment_request_build(&request); + provider_code, + payment_id, + payment_id_len); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; + INFO(result.error); REQUIRE(result.success); REQUIRE(result.data.data != nullptr); REQUIRE(result.data.size > 0); - // Verify JSON + route match the C++ free-function implementation + // Matches the C++ free-function implementation end to end. auto cpp = add_payment_request( master_privkey.data, rotating_privkey.data, - payment_tx.provider_code, - to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); + provider_code, + to_byte_span(payment_id, payment_id_len)); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT); + REQUIRE(std::string_view(result.content_type) == cpp.content_type); REQUIRE(cpp.content_type == "application/json"); REQUIRE(string8_equals(result.data, cpp.data)); - // The C request mirror carries the same endpoint + content_type as the C++ side. - REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); - REQUIRE(std::string_view(result.content_type) == cpp.content_type); } // After freeing REQUIRE(result.data.data == nullptr); REQUIRE(result.data.size == 0); - // Null request - result = session_pro_backend_add_pro_payment_request_build(nullptr); + // Invalid key size -> failure, no allocation + result = session_pro_backend_add_pro_payment_request_build( + master_privkey.data, + sizeof(master_privkey.data) - 1, + rotating_privkey.data, + sizeof(rotating_privkey.data), + provider_code, + payment_id, + payment_id_len); REQUIRE(!result.success); + REQUIRE(result.error_count > 0); REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); } SECTION("session_pro_backend_generate_pro_proof_request_build") { - session_pro_backend_generate_pro_proof_request request = {}; - request.master_pkey = master_pubkey; - request.rotating_pkey = rotating_pubkey; - request.ts = unix_ts; - - session_pro_backend_master_rotating_signatures sigs = - session_pro_backend_generate_pro_proof_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - request.ts); - - request.master_sig = sigs.master_sig; - request.rotating_sig = sigs.rotating_sig; - - // Valid request - auto result = session_pro_backend_generate_pro_proof_request_build(&request); + auto result = session_pro_backend_generate_pro_proof_request_build( + master_privkey.data, + sizeof(master_privkey.data), + rotating_privkey.data, + sizeof(rotating_privkey.data), + unix_ts); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.data.data != nullptr); REQUIRE(result.data.size > 0); - // Verify JSON + route match the C++ free-function implementation auto cpp = pro_proof_request( master_privkey.data, rotating_privkey.data, session::as_sys_seconds(unix_ts)); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT); REQUIRE(string8_equals(result.data, cpp.data)); } - - // After freeing REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); - // Null request - result = session_pro_backend_generate_pro_proof_request_build(nullptr); + // Invalid rotating key size + result = session_pro_backend_generate_pro_proof_request_build( + master_privkey.data, + sizeof(master_privkey.data), + rotating_privkey.data, + sizeof(rotating_privkey.data) - 1, + unix_ts); REQUIRE(!result.success); - REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); + REQUIRE(result.error_count > 0); } SECTION("session_pro_backend_get_pro_revocations_request_build") { - session_pro_backend_get_pro_revocations_request request = {}; - request.ticket = 123; - - // Valid request - auto result = session_pro_backend_get_pro_revocations_request_build(&request); + auto result = session_pro_backend_get_pro_revocations_request_build(123); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.data.data != nullptr); REQUIRE(result.data.size > 0); - // Verify JSON + route match the C++ free-function implementation - auto cpp = revocations_request(request.ticket); + auto cpp = revocations_request(123); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT); REQUIRE(string8_equals(result.data, cpp.data)); } - - // After freeing REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); - - // Null request - result = session_pro_backend_get_pro_revocations_request_build(nullptr); - REQUIRE(!result.success); - REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); } SECTION("session_pro_backend_get_pro_details_request_build") { - session_pro_backend_get_pro_details_request request = {}; - request.master_pkey = master_pubkey; - request.ts = unix_ts; - request.count = 10'000; - - session_pro_backend_signature sig = - session_pro_backend_get_pro_details_request_build_sig( - master_privkey.data, - sizeof(master_privkey.data), - request.ts, - request.count); - - request.master_sig = sig.sig; - - // Valid request - auto result = session_pro_backend_get_pro_details_request_build(&request); + auto result = session_pro_backend_get_pro_details_request_build( + master_privkey.data, sizeof(master_privkey.data), unix_ts, 10'000); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.data.data != nullptr); REQUIRE(result.data.size > 0); - // Verify JSON + route match the C++ free-function implementation auto cpp = payment_details_request( - master_privkey.data, session::as_sys_seconds(unix_ts), request.count); + master_privkey.data, session::as_sys_seconds(unix_ts), 10'000); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT); REQUIRE(string8_equals(result.data, cpp.data)); } - - // After freeing REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); - // Null request - result = session_pro_backend_get_pro_details_request_build(nullptr); + // Invalid key size + result = session_pro_backend_get_pro_details_request_build( + master_privkey.data, sizeof(master_privkey.data) - 1, unix_ts, 10'000); REQUIRE(!result.success); - REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); + REQUIRE(result.error_count > 0); } SECTION("session_pro_backend_pro_proof_response_parse") { @@ -487,8 +352,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { {"revoked_ts", unix_ts + 3600 + 0.75}, {"refund_requested_ts", unix_ts + 3601}, {"payment_id", - std::string( - payment_tx.payment_id, payment_tx.payment_id_count)}}})}}; + std::string(fake_payment_id.data(), fake_payment_id.size())}}})}}; std::string json = j.dump(); // Valid Google JSON @@ -528,11 +392,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); REQUIRE(result.items[0].revoked_ts == unix_ts + 3600 + 0.75); // 750ms preserved REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); - REQUIRE(result.items[0].payment_id_count == payment_tx.payment_id_count); + REQUIRE(result.items[0].payment_id_count == fake_payment_id.size()); REQUIRE(std::memcmp( result.items[0].payment_id, - payment_tx.payment_id, - payment_tx.payment_id_count) == 0); + fake_payment_id.data(), + fake_payment_id.size()) == 0); } // Tweak JSON for a different provider. payment_id is opaque so nothing @@ -611,52 +475,42 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } SECTION("session_pro_backend_set_payment_refund_requested_request_build") { - session_pro_backend_set_payment_refund_requested_request request = {}; - request.master_pkey = master_pubkey; - request.ts = unix_ts; - request.refund_requested_ts = unix_ts + 1; - request.payment_tx = payment_tx; - - session_pro_backend_signature sig = - session_pro_backend_set_payment_refund_requested_request_build_sigs( - master_privkey.data, - sizeof(master_privkey.data), - request.ts, - request.refund_requested_ts, - payment_tx.provider_code, - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count); - request.master_sig = sig.sig; - REQUIRE(sig.success); - - // Valid request - auto result = session_pro_backend_set_payment_refund_requested_request_build(&request); + auto result = session_pro_backend_set_payment_refund_requested_request_build( + master_privkey.data, + sizeof(master_privkey.data), + unix_ts, + unix_ts + 1, + provider_code, + payment_id, + payment_id_len); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); - REQUIRE(result.data.data != nullptr); REQUIRE(result.data.size > 0); - // Verify JSON + route match the C++ free-function implementation auto cpp = refund_request( master_privkey.data, - session::as_sys_seconds(request.ts), - session::as_sys_seconds(request.refund_requested_ts), - payment_tx.provider_code, - to_byte_span(payment_tx.payment_id, payment_tx.payment_id_count)); + session::as_sys_seconds(unix_ts), + session::as_sys_seconds(unix_ts + 1), + provider_code, + to_byte_span(payment_id, payment_id_len)); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT); REQUIRE(string8_equals(result.data, cpp.data)); } - - // After freeing REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); - // Null request - result = session_pro_backend_set_payment_refund_requested_request_build(nullptr); + // Invalid key size + result = session_pro_backend_set_payment_refund_requested_request_build( + master_privkey.data, + sizeof(master_privkey.data) - 1, + unix_ts, + unix_ts + 1, + provider_code, + payment_id, + payment_id_len); REQUIRE(!result.success); - REQUIRE(result.data.data == nullptr); - REQUIRE(result.data.size == 0); + REQUIRE(result.error_count > 0); } SECTION("session_pro_backend_set_payment_refund_requested_response_parse") { @@ -771,46 +625,26 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Add pro payment session_protocol_pro_proof first_pro_proof = {}; { - std::array fake_google_payment_token; + std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::array fake_google_order_id; + std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); // Google's composite payment_id is "|"; libsession treats the // whole thing as one opaque value. std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + oxenc::to_hex(fake_google_order_id); - session_pro_backend_add_pro_payment_user_transaction payment_tx = {}; - payment_tx.provider_code_count = - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; - std::memcpy( - payment_tx.provider_code, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); - payment_tx.payment_id_count = fake_payment_id.size(); - std::memcpy(payment_tx.payment_id, fake_payment_id.data(), payment_tx.payment_id_count); - - // Build request - session_pro_backend_master_rotating_signatures add_pro_sigs = - session_pro_backend_add_pro_payment_request_build_sigs( + session_pro_backend_request request_json = + session_pro_backend_add_pro_payment_request_build( master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - payment_tx.provider_code, - reinterpret_cast(payment_tx.payment_id), - payment_tx.payment_id_count); - - session_pro_backend_add_pro_payment_request request = {}; - request.master_pkey = master_pubkey; - request.rotating_pkey = rotating_pubkey; - request.payment_tx = payment_tx; - request.master_sig = add_pro_sigs.master_sig; - request.rotating_sig = add_pro_sigs.rotating_sig; - - session_pro_backend_request request_json = - session_pro_backend_add_pro_payment_request_build(&request); + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + reinterpret_cast(fake_payment_id.data()), + fake_payment_id.size()); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -837,35 +671,27 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { << ", backend pubkey: " << oxenc::to_hex(DEV_BACKEND_PUBKEY) << ", response: " << response_json); REQUIRE(session_protocol_pro_proof_verify_signature( - &first_pro_proof, DEV_BACKEND_PUBKEY.data(), DEV_BACKEND_PUBKEY.size())); + &first_pro_proof, + reinterpret_cast(DEV_BACKEND_PUBKEY.data()), + DEV_BACKEND_PUBKEY.size())); REQUIRE(std::memcmp( response.proof.rotating_pubkey.data, - request.rotating_pkey.data, - sizeof(request.rotating_pkey.data)) == 0); + rotating_pubkey.data, + sizeof(rotating_pubkey.data)) == 0); } // Authorise new key { int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); - // Build request - session_pro_backend_master_rotating_signatures pro_sigs = - session_pro_backend_generate_pro_proof_request_build_sigs( + session_pro_backend_request request_json = + session_pro_backend_generate_pro_proof_request_build( master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), now_unix_ts); - - session_pro_backend_generate_pro_proof_request request = {}; - request.master_pkey = master_pubkey; - request.rotating_pkey = rotating_pubkey; - request.ts = now_unix_ts; - request.master_sig = pro_sigs.master_sig; - request.rotating_sig = pro_sigs.rotating_sig; - - session_pro_backend_request request_json = - session_pro_backend_generate_pro_proof_request_build(&request); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); // Do CURL request std::string response_json = curl_do_basic_blocking_post_request( @@ -891,33 +717,25 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response session_protocol_pro_proof proof = response.proof; REQUIRE(session_protocol_pro_proof_verify_signature( - &proof, DEV_BACKEND_PUBKEY.data(), DEV_BACKEND_PUBKEY.size())); + &proof, + reinterpret_cast(DEV_BACKEND_PUBKEY.data()), + DEV_BACKEND_PUBKEY.size())); REQUIRE(std::memcmp( response.proof.rotating_pubkey.data, - request.rotating_pkey.data, - sizeof(request.rotating_pkey.data)) == 0); - - session_pro_backend_request_free(&request_json); - session_pro_backend_pro_proof_response_free(&response); + rotating_pubkey.data, + sizeof(rotating_pubkey.data)) == 0); } // Get pro status { - // Build request - session_pro_backend_get_pro_details_request request = {}; - request.master_pkey = master_pubkey; - request.ts = session::epoch_seconds(session::clock_now_s()); - request.count = 10'000; - - session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( - master_privkey.data, sizeof(master_privkey.data), request.ts, request.count); - REQUIRE(sig.success); - request.master_sig = sig.sig; - - // Do CURL request session_pro_backend_request request_json = - session_pro_backend_get_pro_details_request_build(&request); + session_pro_backend_get_pro_details_request_build( + master_privkey.data, + sizeof(master_privkey.data), + session::epoch_seconds(session::clock_now_s()), + 10'000); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); std::string response_json = curl_do_basic_blocking_post_request( curl, @@ -932,7 +750,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { scope_exit response_free{ [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; - // Verify the response INFO("ERROR: JSON response: " << response_json.c_str()); for (size_t index = 0; index < response.header.errors_count; index++) { string8 error = response.header.errors[index]; @@ -946,20 +763,14 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Get pro status without history { - // Build request - session_pro_backend_get_pro_details_request request = {}; - request.master_pkey = master_pubkey; - request.ts = session::epoch_seconds(session::clock_now_s()); - - session_pro_backend_signature sig = session_pro_backend_get_pro_details_request_build_sig( - master_privkey.data, sizeof(master_privkey.data), request.ts, request.count); - REQUIRE(sig.success); - request.master_sig = sig.sig; - - // Do CURL request session_pro_backend_request request_json = - session_pro_backend_get_pro_details_request_build(&request); + session_pro_backend_get_pro_details_request_build( + master_privkey.data, + sizeof(master_privkey.data), + session::epoch_seconds(session::clock_now_s()), + 0); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); std::string response_json = curl_do_basic_blocking_post_request( curl, @@ -979,8 +790,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { string8 error = response.header.errors[index]; UNSCOPED_INFO("ERROR: " << error.data); } - - // Verify the response REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(std::string_view(response.status, response.status_count) == "active"); @@ -988,48 +797,26 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { } // Add _another_ payment, same details - session_pro_backend_add_pro_payment_user_transaction another_payment_tx = {}; + std::string another_payment_id; { - std::array fake_google_payment_token; + std::array fake_google_payment_token; randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::array fake_google_order_id; + std::array fake_google_order_id; randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + - oxenc::to_hex(fake_google_order_id); + another_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + + oxenc::to_hex(fake_google_order_id); - another_payment_tx.provider_code_count = - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) - 1; - std::memcpy( - another_payment_tx.provider_code, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - sizeof(SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); - another_payment_tx.payment_id_count = fake_payment_id.size(); - std::memcpy( - another_payment_tx.payment_id, - fake_payment_id.data(), - another_payment_tx.payment_id_count); - - // Build request - session_pro_backend_master_rotating_signatures add_pro_sigs = - session_pro_backend_add_pro_payment_request_build_sigs( + session_pro_backend_request request_json = + session_pro_backend_add_pro_payment_request_build( master_privkey.data, sizeof(master_privkey.data), rotating_privkey.data, sizeof(rotating_privkey.data), - another_payment_tx.provider_code, - reinterpret_cast(another_payment_tx.payment_id), - another_payment_tx.payment_id_count); - - session_pro_backend_add_pro_payment_request request = {}; - request.master_pkey = master_pubkey; - request.rotating_pkey = rotating_pubkey; - request.payment_tx = another_payment_tx; - request.master_sig = add_pro_sigs.master_sig; - request.rotating_sig = add_pro_sigs.rotating_sig; - - session_pro_backend_request request_json = - session_pro_backend_add_pro_payment_request_build(&request); + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + reinterpret_cast(another_payment_id.data()), + another_payment_id.size()); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1047,21 +834,21 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response session_protocol_pro_proof proof = response.proof; REQUIRE(session_protocol_pro_proof_verify_signature( - &proof, DEV_BACKEND_PUBKEY.data(), DEV_BACKEND_PUBKEY.size())); + &proof, + reinterpret_cast(DEV_BACKEND_PUBKEY.data()), + DEV_BACKEND_PUBKEY.size())); REQUIRE(std::memcmp( response.proof.rotating_pubkey.data, - request.rotating_pkey.data, - sizeof(request.rotating_pkey.data)) == 0); + rotating_pubkey.data, + sizeof(rotating_pubkey.data)) == 0); } // Get revocation list { - // Build request - session_pro_backend_get_pro_revocations_request request = {}; - session_pro_backend_request request_json = - session_pro_backend_get_pro_revocations_request_build(&request); + session_pro_backend_get_pro_revocations_request_build(0); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1083,8 +870,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { string8 error = response.header.errors[index]; UNSCOPED_INFO("ERROR: " << error.data); } - - // Verify the response REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(response.ticket == 0); @@ -1093,28 +878,18 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Set payment refund requested { - // Build request int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); - session_pro_backend_signature refund_sig = - session_pro_backend_set_payment_refund_requested_request_build_sigs( + session_pro_backend_request request_json = + session_pro_backend_set_payment_refund_requested_request_build( master_privkey.data, sizeof(master_privkey.data), /*ts*/ now_unix_ts, /*refund_requested_ts*/ now_unix_ts, - another_payment_tx.provider_code, - reinterpret_cast(another_payment_tx.payment_id), - another_payment_tx.payment_id_count); - - session_pro_backend_set_payment_refund_requested_request request = {}; - request.master_pkey = master_pubkey; - request.master_sig = refund_sig.sig; - request.ts = now_unix_ts; - request.refund_requested_ts = now_unix_ts; - request.payment_tx = another_payment_tx; - - session_pro_backend_request request_json = - session_pro_backend_set_payment_refund_requested_request_build(&request); + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + reinterpret_cast(another_payment_id.data()), + another_payment_id.size()); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; + REQUIRE(request_json.success); // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1137,8 +912,6 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { string8 error = response.header.errors[index]; UNSCOPED_INFO("ERROR: " << error.data); } - - // Verify the response REQUIRE(response.header.errors_count == 0); REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(response.updated); From fd7598e6ed56774fbbeb7b685793722394700367 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 20:20:34 -0300 Subject: [PATCH 122/193] Pro: add PUBKEY_X25519 constant (X25519 form of the backend pubkey) Desktop consumes the backend's X25519 pubkey as a constant, so expose it alongside PUBKEY: session::pro_backend::PUBKEY_X25519 (C++) and the C export SESSION_PRO_BACKEND_PUBKEY_X25519. A unit test asserts the hardcoded bytes equal the runtime crypto_sign_ed25519_pk_to_curve25519(PUBKEY) conversion so they cannot drift. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 5 +++++ include/session/pro_backend.hpp | 8 ++++++++ src/pro_backend.cpp | 2 ++ tests/test_pro_backend.cpp | 13 +++++++++++++ 4 files changed, 28 insertions(+) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index cfc65ad15..2f71eee41 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -48,6 +48,11 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_URL; /// by the backend. Points at the same value as the C++ `session::pro_backend::PUBKEY`. LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY; +/// The X25519 form of SESSION_PRO_BACKEND_PUBKEY, 32 bytes (the Ed25519 key converted via +/// crypto_sign_ed25519_pk_to_curve25519). Points at the same value as the C++ +/// `session::pro_backend::PUBKEY_X25519`. +LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY_X25519; + /// Per-provider support/management URLs, keyed by provider code. These are identical for every user /// (not translation data), so libsession owns them as the single source of truth rather than each /// client duplicating them; the human-readable provider/store names are translation data and remain diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 49b4e3dbf..b61b8ba3f 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -65,6 +65,14 @@ namespace session::pro_backend { constexpr auto PUBKEY = "479ffca8bcec7b4a0f0f7afe48b8a6d15635a8c7ff15ad16add05752c19414d4"_hex_b; static_assert(PUBKEY.size() == 32); +/// The X25519 form of `PUBKEY` (the same key converted via crypto_sign_ed25519_pk_to_curve25519), +/// provided as a constant for clients that need the X25519 pubkey (e.g. to establish an encrypted +/// channel to the backend) without doing the conversion themselves. A unit test asserts these bytes +/// match the runtime conversion of `PUBKEY`, so the two cannot drift. +constexpr auto PUBKEY_X25519 = + "ce5a75f64b6c43db6c1374d362c3ea9d85951c4f42a3d04cf94f87822d4f803b"_hex_b; +static_assert(PUBKEY_X25519.size() == 32); + /// The Session Pro Backend's production base URL: POST a request body to `/` (see /// the SESSION_PRO_BACKEND_*_ENDPOINT paths). This is the canonical production value; a client may /// point at a different dev/test server if it chooses. diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 2e25b542a..8a37cc38e 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -209,6 +209,8 @@ LIBSESSION_EXPORT extern const char* const LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_URL = URL.data(); LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY = reinterpret_cast(PUBKEY.data()); +LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY_X25519 = + reinterpret_cast(PUBKEY_X25519.data()); } std::optional provider_urls(std::string_view provider_code) { diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 177e0e0e1..243a7db8e 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -563,6 +563,19 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } } +TEST_CASE("Pro Backend X25519 pubkey matches the converted Ed25519 pubkey", "[pro_backend]") { + // PUBKEY_X25519 is a hardcoded convenience constant; assert it equals the runtime conversion of + // the Ed25519 PUBKEY so the two can never silently drift. + unsigned char converted[32] = {}; + REQUIRE(crypto_sign_ed25519_pk_to_curve25519( + converted, reinterpret_cast(PUBKEY.data())) == 0); + REQUIRE(std::memcmp(converted, PUBKEY_X25519.data(), sizeof(converted)) == 0); + // The C export points at the same bytes. + REQUIRE(std::memcmp( + SESSION_PRO_BACKEND_PUBKEY_X25519, PUBKEY_X25519.data(), sizeof(converted)) == + 0); +} + #if defined(TEST_PRO_BACKEND_WITH_DEV_SERVER) #include From bf1cdf2145126950154015fa10d6c17d51136ab4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 20:48:51 -0300 Subject: [PATCH 123/193] test-bigendian: local qemu-user-static + sid, quic-deps via apt, guarded tags Iterated the occasional big-endian audit into a working state (validated on real s390x): use the host's qemu-user-static + binfmt-support (no privileged tonistiigi/binfmt container); debian:sid for a modern toolchain; a clean build dir each run; -DENABLE_NETWORKING_SROUTER=OFF plus libngtcp2/gnutls/ libevent from apt to satisfy the mandatory oxen::quic without building it from source; and fail loudly if any filter tag matches no tests (guards against a tag rename silently shrinking the audit). Co-Authored-By: Claude Opus 4.8 (1M context) --- utils/test-bigendian.sh | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/utils/test-bigendian.sh b/utils/test-bigendian.sh index 592a8281b..c7b2463c5 100755 --- a/utils/test-bigendian.sh +++ b/utils/test-bigendian.sh @@ -10,9 +10,13 @@ # the [endian] known-answer test (hash::blake2b_pers integer args must be little-endian) on an actual # big-endian machine, where make_hashable takes its otherwise-never-compiled swap branch. # -# Cost: everything runs under qemu-user emulation, so the build is slow (tens of minutes) — the -# submodule-built networking stack (oxen-libquic / session-router) is the bulk of it. That's fine for -# an occasional manual audit; it is deliberately not in CI. +# session-router (the onion-routing layer) is endian-irrelevant to what we test and drags in the +# heaviest deps, so we build with -DENABLE_NETWORKING_SROUTER=OFF. This branch has no ENABLE_NETWORKING +# toggle (networking is always required), so oxen::quic still builds regardless — we satisfy it with +# libngtcp2 + gnutls from apt (nghttp3 is not needed; libquic uses raw QUIC). +# +# Cost: everything runs under qemu-user emulation, so expect a slow build (oxen-libquic is the bulk of +# it). Fine for an occasional manual audit; it is deliberately not in CI. # # Requires: Docker, plus a one-time host install of qemu-user-static + binfmt-support (see below). # Run from the repo root with submodules checked out. @@ -28,19 +32,28 @@ if [ ! -e /proc/sys/fs/binfmt_misc/qemu-s390x ]; then exit 1 fi -# Build + test inside an emulated s390x Debian. BUILD_STATIC_DEPS=OFF pulls the heavy deps -# (libsodium, protobuf, curl, sqlite, zstd) as prebuilt s390x apt packages rather than compiling them -# from source under emulation. +# Build + test inside an emulated s390x Debian. BUILD_STATIC_DEPS=OFF pulls the heavy deps (libsodium, +# protobuf, sqlite, zstd, and the oxen::quic deps libngtcp2/gnutls/libevent) as prebuilt s390x apt +# packages rather than compiling them from source under emulation. docker run --platform=linux/s390x --rm -v "$PWD:/src" -w /src debian:sid bash -euxc ' apt-get update apt-get install -y --no-install-recommends \ g++ cmake ninja-build pkg-config git ca-certificates \ libsodium-dev libprotobuf-dev protobuf-compiler libcurl4-openssl-dev \ - libsqlite3-dev libzstd-dev - cmake -B build-bigendian -G Ninja -DBUILD_STATIC_DEPS=OFF -DCMAKE_BUILD_TYPE=Release + libsqlite3-dev libzstd-dev \ + libngtcp2-dev libngtcp2-crypto-gnutls-dev libgnutls28-dev libevent-dev + rm -rf build-bigendian # fresh configure each run; the dir lives in the mounted tree and would otherwise reuse a stale CMake cache + cmake -B build-bigendian -G Ninja -DBUILD_STATIC_DEPS=OFF -DENABLE_NETWORKING_SROUTER=OFF -DCMAKE_BUILD_TYPE=Release cmake --build build-bigendian --target testAll - # [endian] is the make_hashable little-endian known-answer test; the Pro / protocol / config tags - # also exercise serialization on this big-endian host. Drop the filter for a full run. - ./build-bigendian/tests/testAll "[endian],[hash],[pro_backend],[session_protocol],[config]" + # Run the endian-sensitive tags on this big-endian host. [endian] is the make_hashable little-endian + # known-answer test; the hash / protocol / Pro / config tags also exercise serialization here. Catch2 + # exits 0 even when a filter clause matches no tests, so capture the output and fail loudly if any tag + # went unmatched — that guards against a tag rename silently shrinking the audit. + out=$(./build-bigendian/tests/testAll "[endian],[hash],[session-protocol],[pro_backend],[config]") + echo "$out" + if echo "$out" | grep -q "No test cases matched"; then + echo "ERROR: a filter tag matched no tests — the audit under-ran (tag renamed?)" >&2 + exit 1 + fi ' echo "big-endian audit passed" From f295a4395032eaea8461b8579931ded76bb12579 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 11:57:05 -0300 Subject: [PATCH 124/193] Pro: drop the overloaded Response.status field Response::status was a uint32_t whose domain changed by endpoint (0/1/2 for most, plus 100/101 for add-payment) -- and for every response except add-payment it just duplicated !errors.empty(). Remove it: Response is now { errors } with a success() helper (C: response_header drops status; success == errors_count == 0), and add-payment collapses to the same -- the backend already sends the human reason (including already-redeemed / unknown-payment) in errors, so display is unaffected. Deletes the AddProPaymentResponseStatus C++ enum, the C SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS enum, and its magic-int values. Parsers still read the wire "status" int internally to decide success; they just no longer surface it. (A future machine-readable discriminator, if needed, should be an opaque string slug on the wire like the Q13 statuses.) Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 18 ++----------- include/session/pro_backend.hpp | 47 +++++++++------------------------ src/pro_backend.cpp | 32 ++++++---------------- tests/test_pro_backend.cpp | 34 ++++++++++++------------ 4 files changed, 40 insertions(+), 91 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 2f71eee41..72397656b 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -81,24 +81,10 @@ typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT, } SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT; -/// Must match: -/// https://github.com/Doy-lee/session-pro-backend/blob/41a794e2998b528566d0c27d34c4faeed5602e26/server.py#L461 -typedef enum SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS { - SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_SUCCESS = - SESSION_PRO_BACKEND_STATUS_SUCCESS, - SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_PARSE_ERROR = - SESSION_PRO_BACKEND_STATUS_PARSE_ERROR, - SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ERROR = - SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR, - - SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ALREADY_REDEEMED = 100, - SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT = 101, -} SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS; - typedef struct session_pro_backend_response_header session_pro_backend_response_header; struct session_pro_backend_response_header { - uint32_t status; - /// Array of error messages (NULL if no errors), with errors_count elements + /// Error messages (NULL if none), with errors_count elements. The response succeeded iff + /// errors_count == 0; on failure the messages carry the reason(s) (backend- or parse-supplied). string8* errors; size_t errors_count; unsigned char* diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index b61b8ba3f..b0c27ec2d 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -82,38 +82,15 @@ constexpr std::string_view URL = "https://pro.session.codes"; /// account's root Ed25519 seed. constexpr auto pro_subkey_domain = "SessionProRandom"_bytes; -enum struct AddProPaymentResponseStatus { - /// Payment was claimed and the pro proof was successfully generated - Success = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_SUCCESS, - - /// Backend encountered an error when attempting to claim the payment - Error = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ERROR, - - /// Request JSON failed to be parsed correctly, payload was malformed or missing values - ParseError = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_PARSE_ERROR, - - /// Payment is already claimed - AlreadyRedeemed = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_ALREADY_REDEEMED, - - /// Payment transaction attempted to claim a payment that the backend does not have. Either the - /// payment doesn't exist or the backend has not witnessed the payment from the provider yet. - UnknownPayment = SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_RESPONSE_STATUS_UNKNOWN_PAYMENT, -}; - struct Response { - /// Status code for the response, maps to a specific enum for some requests otherwise it uses 0 - /// for success, other values indicate errors. For the following responses, the status code maps - /// to - /// - /// | Request | Enum - /// | AddProPayment | AddProPaymentResponseStatus - /// | Everything else ...| SESSION_PRO_BACKEND_STATUS_SUCCESS or - /// SESSION_PRO_BACKEND_STATUS_ERROR - std::uint32_t status; - - /// List of parsing or processing errors. Empty if there are no parsing errors, if there are - /// errors, the parse may be partially complete, always check the errors before proceeding. + /// Parsing or processing errors, from the backend or from libsession while parsing. Empty on + /// success; may hold several messages (one request can fail multiple checks at once). On + /// failure the parse may be partial, so always check `success()`/`errors` before using other + /// fields. std::vector errors; + + /// True iff the response came back without any errors. + [[nodiscard]] bool success() const { return errors.empty(); } }; struct MasterRotatingSignatures { @@ -188,14 +165,16 @@ ProRequest add_payment_request( /// Common base for the responses that carry a freshly-issued Session Pro proof: both add-payment /// and generate-proof reply with exactly a proof. `AddProPaymentResponse` and -/// `GenerateProProofResponse` are distinct, currently empty types (each parsed by its own free -/// function below) that share every field today but can diverge independently later. `proof` is the -/// raw parse result, convertible to a config::ProProof. +/// `GenerateProProofResponse` are distinct types (each parsed by its own free function below) that +/// share `proof` today but can diverge independently. `proof` is the raw parse result, convertible +/// to a config::ProProof. struct ProProofResponse : Response { ProProof proof; }; -/// Response to `add_payment_request` (endpoint `add_pro_payment`). +/// Response to `add_payment_request` (endpoint `add_pro_payment`). On failure the reason(s) are in +/// `errors` (the backend sends a human-readable message, including for already-redeemed / +/// unknown-payment); check `success()`. struct AddProPaymentResponse : ProProofResponse {}; /// Response to `pro_proof_request` (endpoint `generate_pro_proof`). diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 8a37cc38e..cae0ea79a 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -283,14 +283,13 @@ namespace { void fill_proof_response(std::string_view json, ProProofResponse& result) { // Parse basics nlohmann::json j = json_parse(json, result.errors); - result.status = json_require(j, "status", result.errors); + uint32_t status = json_require(j, "status", result.errors); if (result.errors.size()) { - result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; return; } // Parse errors - if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { + if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { parse_json_response_errors(j, result.errors); return; } @@ -389,14 +388,13 @@ GetProRevocationsResponse parse_revocations(std::string_view json) { // Parse basics GetProRevocationsResponse result = {}; nlohmann::json j = json_parse(json, result.errors); - result.status = json_require(j, "status", result.errors); + uint32_t status = json_require(j, "status", result.errors); if (result.errors.size()) { - result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; return result; } // Parse errors - if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { + if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { parse_json_response_errors(j, result.errors); return result; } @@ -490,14 +488,13 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { // Parse basics GetProDetailsResponse result = {}; nlohmann::json j = json_parse(json, result.errors); - result.status = json_require(j, "status", result.errors); + uint32_t status = json_require(j, "status", result.errors); if (result.errors.size()) { - result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; return result; } // Parse errors - if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { + if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { parse_json_response_errors(j, result.errors); return result; } @@ -664,14 +661,13 @@ SetPaymentRefundRequestedResponse parse_refund(std::string_view json) { // Parse basics SetPaymentRefundRequestedResponse result = {}; nlohmann::json j = json_parse(json, result.errors); - result.status = json_require(j, "status", result.errors); + uint32_t status = json_require(j, "status", result.errors); if (result.errors.size()) { - result.status = SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR; return result; } // Parse errors - if (result.status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { + if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { parse_json_response_errors(j, result.errors); return result; } @@ -787,7 +783,6 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) session_pro_backend_pro_proof_response result = {}; if (!json) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; result.header.errors_count = 1; return result; @@ -808,7 +803,6 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; result.header.errors_count = 1; return result; @@ -821,7 +815,6 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. // Note that a response error and success case folds into the same code path. A success and // error response returns the same struct just with different fields populated. - result.header.status = cpp.status; result.proof.version = cpp.proof.version; result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_at); std::memcpy( @@ -849,7 +842,6 @@ LIBSESSION_C_API session_pro_backend_get_pro_revocations_response session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t json_len) { session_pro_backend_get_pro_revocations_response result = {}; if (!json) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; result.header.errors_count = 1; return result; @@ -873,7 +865,6 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; result.header.errors_count = 1; return result; @@ -884,7 +875,6 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t } // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. - result.header.status = cpp.status; result.ticket = cpp.ticket; result.retry_in = cpp.retry_in.count(); result.retain_for = cpp.retain_for.count(); @@ -916,7 +906,6 @@ LIBSESSION_C_API session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details_response_parse(const char* json, size_t json_len) { session_pro_backend_get_pro_details_response result = {}; if (!json) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; result.header.errors_count = 1; return result; @@ -936,7 +925,6 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; result.header.errors_count = 1; return result; @@ -949,7 +937,6 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json using session::epoch_seconds; // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. - result.header.status = cpp.status; result.status_count = session::copy_c_str(result.status, cpp.user_status) - 1; result.error_report = cpp.error_report; result.items_count = cpp.items.size(); @@ -1018,7 +1005,6 @@ LIBSESSION_C_API session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response_parse(const char* json, size_t json_len) { session_pro_backend_set_payment_refund_requested_response result = {}; if (!json) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; result.header.errors_count = 1; return result; @@ -1037,7 +1023,6 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.status = 1; result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; result.header.errors_count = 1; return result; @@ -1048,7 +1033,6 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json } // Copy to C struct - result.header.status = cpp.status; result.updated = cpp.updated; // Copy errors diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 243a7db8e..e3db5561c 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -185,7 +185,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); - REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(result.proof.expiry_ts == unix_ts); @@ -236,7 +236,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_pro_proof_response_free(&result); }}; - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count > 0); REQUIRE(result.header.errors != nullptr); } @@ -247,7 +247,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_pro_proof_response_parse(nullptr, 0); - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count == 1); REQUIRE(result.header.errors != nullptr); @@ -280,7 +280,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); - REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(result.ticket == 123); @@ -308,7 +308,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count > 0); REQUIRE(result.header.errors != nullptr); } @@ -318,7 +318,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_get_pro_revocations_response_parse(nullptr, 0); - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count == 1); REQUIRE(result.header.errors != nullptr); } @@ -364,7 +364,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); - REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(std::string_view(result.status, result.status_count) == "expired"); @@ -438,7 +438,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count > 0); REQUIRE(result.header.errors != nullptr); } @@ -449,7 +449,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_get_pro_details_response_parse(nullptr, 0); - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count == 1); REQUIRE(result.header.errors != nullptr); } @@ -528,7 +528,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { }}; for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); - REQUIRE(result.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors_count == 0); REQUIRE(result.header.errors == nullptr); REQUIRE(result.updated); @@ -546,7 +546,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count > 0); REQUIRE(result.header.errors != nullptr); } @@ -556,7 +556,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_set_payment_refund_requested_response_parse(nullptr, 0); - REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(result.header.errors_count != 0); REQUIRE(result.header.errors_count == 1); REQUIRE(result.header.errors != nullptr); } @@ -725,7 +725,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(response.header.errors_count == 0); // Verify response session_protocol_pro_proof proof = response.proof; @@ -769,7 +769,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(response.header.errors_count == 0); REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count > 0); } @@ -804,7 +804,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(response.header.errors_count == 0); REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count == 0); } @@ -884,7 +884,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(response.header.errors_count == 0); REQUIRE(response.ticket == 0); REQUIRE(response.items_count == 0); } @@ -926,7 +926,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { UNSCOPED_INFO("ERROR: " << error.data); } REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_STATUS_SUCCESS); + REQUIRE(response.header.errors_count == 0); REQUIRE(response.updated); } } From 837f985d4f8fe6f2443e87dbd5d62b3b3cdc9cfc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 14:48:28 -0300 Subject: [PATCH 125/193] Pro: response envelope -> status enum + error_code slug + single error (Delta #12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the response envelope to spec §5 / Delta #12. The wire `status` becomes a string ok/fail/error parsed into a CLOSED ResponseStatus enum -- an unrecognized value is a protocol error, never passed through (backend confirmed status is exhaustive, Q14). The `errors` array becomes a single `error` string (diagnostic/fallback, first-reason-only). A new open/additive `error_code` slug (opaque; unknown values pass through) carries the machine-readable outcome. Response is renamed ResponseBase (it's only ever a base) and is now { ResponseStatus status; optional error_code; optional error; } with an explicit operator bool(). The C response_header mirrors it (status enum + error_code/error string8, {NULL,0} == none). get-details account status key renamed status -> user_status (Q15). Deletes the obsolete integer status enum. A libsession-side parse failure yields a protocol error (error_code "invalid_response"). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 31 +-- include/session/pro_backend.hpp | 47 ++-- src/pro_backend.cpp | 368 +++++++++++++++++--------------- tests/test_pro_backend.cpp | 164 +++++++------- 4 files changed, 319 insertions(+), 291 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 72397656b..0674d7a7b 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -11,14 +11,6 @@ extern "C" { #endif -/// Must match: -/// https://github.com/Doy-lee/session-pro-backend/blob/41a794e2998b528566d0c27d34c4faeed5602e26/server.py#L457 -enum { - SESSION_PRO_BACKEND_STATUS_SUCCESS = 0, - SESSION_PRO_BACKEND_STATUS_GENERIC_ERROR = 1, - SESSION_PRO_BACKEND_STATUS_PARSE_ERROR = 2, -}; - /// Canonical payment-provider `code` strings — the value transmitted on the wire and folded into /// the add-payment / set-refund signed hashes (spec §1). Reference these constants rather than /// hardcoding the slug so a sent code cannot drift from what libsession hashes/parses. Unknown @@ -81,12 +73,27 @@ typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT, } SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT; +/// Response outcome (wire `status`, spec §5). CLOSED/exhaustive: the backend will never add a +/// value, so an unrecognized wire status is reported as a protocol error (RESPONSE_STATUS_ERROR + +/// error_code "invalid_response"). Mirrors C++ session::pro_backend::ResponseStatus. +typedef enum SESSION_PRO_BACKEND_RESPONSE_STATUS { + SESSION_PRO_BACKEND_RESPONSE_STATUS_OK, ///< Success; the response's payload fields are set. + SESSION_PRO_BACKEND_RESPONSE_STATUS_FAIL, ///< Rejected on client input / a precondition. + SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR, ///< Backend fault; the same request may succeed + ///< later. +} SESSION_PRO_BACKEND_RESPONSE_STATUS; + typedef struct session_pro_backend_response_header session_pro_backend_response_header; struct session_pro_backend_response_header { - /// Error messages (NULL if none), with errors_count elements. The response succeeded iff - /// errors_count == 0; on failure the messages carry the reason(s) (backend- or parse-supplied). - string8* errors; - size_t errors_count; + /// Outcome category. Success iff `status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK`. + SESSION_PRO_BACKEND_RESPONSE_STATUS status; + /// On non-OK, the machine-readable outcome slug (spec §5.1); {NULL, 0} on success. Opaque and + /// forward-compatible (unknown slugs pass through); "invalid_response" if libsession could not + /// parse the reply at all. + string8 error_code; + /// On non-OK, an English diagnostic string (NOT user-facing text -- that comes from mapping + /// error_code to a localized string); {NULL, 0} on success. Always safe to log. + string8 error; unsigned char* internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch }; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index b0c27ec2d..5ffa4b0e5 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -27,7 +27,7 @@ /// response and filling in the relevant rotating private key that the proof was authorised for. /// /// The server will only respond successfully if it can also independently verify the purchase -/// otherwise an error is returned and can be read from the `Response` after parsing the +/// otherwise an error is returned and can be read from the `ResponseBase` after parsing the /// raw response. /// /// 2. Attach the `ProProof` constructed from (1) into their messages. Libsession has helper @@ -82,15 +82,36 @@ constexpr std::string_view URL = "https://pro.session.codes"; /// account's root Ed25519 seed. constexpr auto pro_subkey_domain = "SessionProRandom"_bytes; -struct Response { - /// Parsing or processing errors, from the backend or from libsession while parsing. Empty on - /// success; may hold several messages (one request can fail multiple checks at once). On - /// failure the parse may be partial, so always check `success()`/`errors` before using other - /// fields. - std::vector errors; +/// Response outcome category (the wire `status`, spec §5). CLOSED/exhaustive by design: the backend +/// will never add a fourth value, so libsession treats any unrecognized wire status as a protocol +/// error (fail-closed) rather than passing it through. New categories/detail arrive via +/// `error_code`, which IS open/extensible. +enum class ResponseStatus { + Ok, ///< Success; the response's payload fields are populated. + Fail, ///< Request rejected on client input / a precondition; retrying the identical request + ///< won't help (except the `stale_request` error_code). See `error_code`. + Error, ///< Backend fault; the client did nothing wrong and the same request may succeed later. +}; - /// True iff the response came back without any errors. - [[nodiscard]] bool success() const { return errors.empty(); } +struct ResponseBase { + /// Outcome category; the `explicit operator bool()` is the usual check. See ResponseStatus. + ResponseStatus status = ResponseStatus::Ok; + + /// On non-Ok, a stable machine-readable slug identifying the outcome (spec §5.1), e.g. + /// "unknown_payment", "expired", "stale_request". std::nullopt on success. Opaque and + /// forward-compatible: map known slugs to localized text; for an unrecognized one fall back to + /// `error` / the `status` category. (libsession synthesizes "invalid_response" if it cannot + /// parse the backend's reply at all.) + std::optional error_code; + + /// On non-Ok, an English diagnostic string. NOT user-facing text (that comes from mapping + /// `error_code` to a localized string); show it only when the slug is unrecognized, and it is + /// always safe to log. std::nullopt on success. + std::optional error; + + /// Contextually convertible to bool: `if (response) { ... }` is true iff the request succeeded + /// (status == Ok). Explicit, so it can't silently leak into arithmetic or comparisons. + explicit operator bool() const { return status == ResponseStatus::Ok; } }; struct MasterRotatingSignatures { @@ -168,7 +189,7 @@ ProRequest add_payment_request( /// `GenerateProProofResponse` are distinct types (each parsed by its own free function below) that /// share `proof` today but can diverge independently. `proof` is the raw parse result, convertible /// to a config::ProProof. -struct ProProofResponse : Response { +struct ProProofResponse : ResponseBase { ProProof proof; }; @@ -225,7 +246,7 @@ struct ProRevocationItem { std::chrono::sys_seconds effective_at; }; -struct GetProRevocationsResponse : Response { +struct GetProRevocationsResponse : ResponseBase { /// 64-bit monotonic integer for the latest revocation list iteration. /// Update the caller's ticket to this value for subsequent requests. std::int64_t ticket; @@ -322,7 +343,7 @@ struct ProPaymentItem { std::string payment_id; }; -struct GetProDetailsResponse : Response { +struct GetProDetailsResponse : ResponseBase { /// List of payment items for the master public key std::vector items; @@ -410,7 +431,7 @@ ProRequest refund_request( std::string_view provider_code, std::span payment_id); -struct SetPaymentRefundRequestedResponse : Response { +struct SetPaymentRefundRequestedResponse : ResponseBase { /// True if a payment was found matching the given payment information and that the refund /// request unix timestamp was set bool updated; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index cae0ea79a..4bc7d3949 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -95,24 +95,6 @@ double epoch_seconds_double(session::sys_ms t) { return std::chrono::duration(t.time_since_epoch()).count(); } -void parse_json_response_errors(const nlohmann::json& j, std::vector& errors) { - const auto& array = json_require(j, "errors", errors); - errors.reserve(errors.size() + array.size()); - for (size_t index = 0; index < array.size(); index++) { - const auto& it = array[index]; - if (it.is_string()) { - errors.push_back(it.get()); - } else { - errors.push_back(fmt::format( - "Aborting parse, 'result.errors[{}]' was not a string " - "error: '{}'", - index, - it.dump(1))); - break; - } - } -} - bool json_require_fixed_bytes_from_hex( const nlohmann::json& j, std::string_view key, @@ -277,48 +259,87 @@ ProRequest add_payment_request( } namespace { - // Shared parse for the proof-carrying responses (add-payment and generate-proof both reply with - // exactly a proof); fills the common ProProofResponse base of whichever derived response is - // passed. - void fill_proof_response(std::string_view json, ProProofResponse& result) { - // Parse basics - nlohmann::json j = json_parse(json, result.errors); - uint32_t status = json_require(j, "status", result.errors); - if (result.errors.size()) { - return; - } + // libsession-side slug when the backend's reply can't be parsed at all (malformed envelope, + // missing/unrecognized status). Distinct from any backend error_code slug. + constexpr std::string_view invalid_response_code = "invalid_response"; + + // Put a ResponseBase into a protocol-error state (a libsession-side parse failure, distinct + // from a backend-reported fail/error). + void set_protocol_error(ResponseBase& result, std::string message) { + result.status = ResponseStatus::Error; + result.error_code = std::string(invalid_response_code); + result.error = std::move(message); + } - // Parse errors - if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { - parse_json_response_errors(j, result.errors); - return; + // Read the response envelope (spec §5) into `result`: string `status` -> ResponseStatus (a + // CLOSED set -- an unrecognized value is a protocol error, never passed through) and, on + // non-ok, `error_code` + `error`. Returns the `result` object to read the payload from when + // status is "ok" (an empty object otherwise; the caller returns early). libsession-side parse + // problems accumulate in `errs` for the caller to fold into a protocol error. + nlohmann::json::object_t read_envelope( + std::string_view json, ResponseBase& result, std::vector& errs) { + nlohmann::json j = json_parse(json, errs); + auto status = json_require(j, "status", errs); + if (!errs.empty()) + return {}; + if (status == "ok") { + result.status = ResponseStatus::Ok; + return json_require(j, "result", errs); } + if (status == "fail" || status == "error") { + result.status = status == "fail" ? ResponseStatus::Fail : ResponseStatus::Error; + result.error_code = json_require(j, "error_code", errs); + result.error = json_require(j, "error", errs); + return {}; + } + errs.push_back(fmt::format("Unrecognized response status: '{}'", status)); + return {}; + } - auto result_obj = json_require(j, "result", result.errors); - if (result.errors.size()) - return; - - // Parse payload - result.proof.version = json_require(result_obj, "version", result.errors); - auto expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + // Fills the common proof payload (add-payment and generate-proof both reply with exactly a + // proof) from the already-extracted `result` object. + void fill_proof( + const nlohmann::json::object_t& result_obj, + ProProofResponse& result, + std::vector& errs) { + result.proof.version = json_require(result_obj, "version", errs); + auto expiry_ts = json_require(result_obj, "expiry_ts", errs); result.proof.expiry_at = as_sys_seconds(expiry_ts); json_require_fixed_bytes_from_hex( - result_obj, "revocation_tag", result.errors, result.proof.revocation_tag); + result_obj, "revocation_tag", errs, result.proof.revocation_tag); json_require_fixed_bytes_from_hex( - result_obj, "rotating_pkey", result.errors, result.proof.rotating_pubkey); - json_require_fixed_bytes_from_hex(result_obj, "sig", result.errors, result.proof.sig); + result_obj, "rotating_pkey", errs, result.proof.rotating_pubkey); + json_require_fixed_bytes_from_hex(result_obj, "sig", errs, result.proof.sig); } } // namespace AddProPaymentResponse parse_add_payment(std::string_view json) { AddProPaymentResponse result = {}; - fill_proof_response(json, result); + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); + return result; + } + fill_proof(result_obj, result, errs); + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } GenerateProProofResponse parse_pro_proof(std::string_view json) { GenerateProProofResponse result = {}; - fill_proof_response(json, result); + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); + return result; + } + fill_proof(result_obj, result, errs); + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } @@ -387,54 +408,45 @@ ProRequest revocations_request(std::int64_t ticket) { GetProRevocationsResponse parse_revocations(std::string_view json) { // Parse basics GetProRevocationsResponse result = {}; - nlohmann::json j = json_parse(json, result.errors); - uint32_t status = json_require(j, "status", result.errors); - if (result.errors.size()) { + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } - // Parse errors - if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { - parse_json_response_errors(j, result.errors); - return result; - } - - auto result_obj = json_require(j, "result", result.errors); - if (result.errors.size()) - return result; - // Parse payload - result.ticket = json_require(result_obj, "ticket", result.errors); - result.retry_in = - std::chrono::seconds(json_require(result_obj, "retry_in", result.errors)); - result.retain_for = - std::chrono::seconds(json_require(result_obj, "retain_for", result.errors)); + result.ticket = json_require(result_obj, "ticket", errs); + result.retry_in = std::chrono::seconds(json_require(result_obj, "retry_in", errs)); + result.retain_for = std::chrono::seconds(json_require(result_obj, "retain_for", errs)); - auto array = json_require(result_obj, "items", result.errors); + auto array = json_require(result_obj, "items", errs); result.items.reserve(array.size()); for (size_t index = 0; index < array.size(); index++) { const auto& it = array[index]; if (!it.is_object()) { - result.errors.push_back(fmt::format( + errs.push_back(fmt::format( "Aborting parse, 'items[{}]' was not an object: {}", index, it.dump(1))); break; } // Parse revocation item auto obj = it.get(); - auto effective_ts = json_require(obj, "effective_ts", result.errors); + auto effective_ts = json_require(obj, "effective_ts", errs); ProRevocationItem item = {}; item.effective_at = as_sys_seconds(effective_ts); - json_require_fixed_bytes_from_hex( - obj, "revocation_tag", result.errors, item.revocation_tag); + json_require_fixed_bytes_from_hex(obj, "revocation_tag", errs, item.revocation_tag); // Handle parsing result - if (result.errors.size()) + if (errs.size()) break; result.items.emplace_back(std::move(item)); } + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } @@ -487,78 +499,68 @@ ProRequest payment_details_request( GetProDetailsResponse parse_payment_details(std::string_view json) { // Parse basics GetProDetailsResponse result = {}; - nlohmann::json j = json_parse(json, result.errors); - uint32_t status = json_require(j, "status", result.errors); - if (result.errors.size()) { - return result; - } - - // Parse errors - if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { - parse_json_response_errors(j, result.errors); + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } - auto result_obj = json_require(j, "result", result.errors); - if (result.errors.size()) - return result; - // Parse payload. The account Pro status is an opaque string code ("never"/"active"/"expired"); // an unknown value passes through unchanged (§1: enums are codes) rather than failing the // parse. - result.user_status = json_require(result_obj, "status", result.errors); + result.user_status = json_require(result_obj, "user_status", errs); - uint32_t error_report = json_require(result_obj, "error_report", result.errors); + uint32_t error_report = json_require(result_obj, "error_report", errs); if (error_report >= SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT) { - result.errors.push_back( - fmt::format("Error report value was out-of-bounds: {}", error_report)); + errs.push_back(fmt::format("Error report value was out-of-bounds: {}", error_report)); + set_protocol_error(result, errs.front()); return result; } result.error_report = static_cast(error_report); - result.auto_renewing = json_require(result_obj, "auto_renewing", result.errors); + result.auto_renewing = json_require(result_obj, "auto_renewing", errs); - result.payments_total = json_require(result_obj, "payments_total", result.errors); + result.payments_total = json_require(result_obj, "payments_total", errs); - int64_t expiry_ts = json_require(result_obj, "expiry_ts", result.errors); + int64_t expiry_ts = json_require(result_obj, "expiry_ts", errs); int64_t grace_period_duration = - json_require(result_obj, "grace_period_duration", result.errors); - int64_t refund_requested_ts = - json_require(result_obj, "refund_requested_ts", result.errors); + json_require(result_obj, "grace_period_duration", errs); + int64_t refund_requested_ts = json_require(result_obj, "refund_requested_ts", errs); result.expiry_at = as_sys_seconds(expiry_ts); result.grace_period_duration = std::chrono::seconds(grace_period_duration); result.refund_requested_at = as_sys_seconds(refund_requested_ts); - auto array = json_require(result_obj, "items", result.errors); + auto array = json_require(result_obj, "items", errs); result.items.reserve(array.size()); for (size_t index = 0; index < array.size(); index++) { const auto& it = array[index]; if (!it.is_object()) { - result.errors.push_back(fmt::format( + errs.push_back(fmt::format( "Aborting parse, 'items[{}]' was not an object: {}", index, it.dump(1))); break; } // Parse payment item auto obj = it.get(); - auto status = json_require(obj, "status", result.errors); - auto plan = json_require(obj, "plan", result.errors); - auto payment_provider = json_require(obj, "payment_provider", result.errors); - auto payment_id = json_require(obj, "payment_id", result.errors); - auto auto_renewing = json_require(obj, "auto_renewing", result.errors); + auto status = json_require(obj, "status", errs); + auto plan = json_require(obj, "plan", errs); + auto payment_provider = json_require(obj, "payment_provider", errs); + auto payment_id = json_require(obj, "payment_id", errs); + auto auto_renewing = json_require(obj, "auto_renewing", errs); // purchased_ts and revoked_ts are upstream-provider event instants: floats on the wire // carrying sub-second precision (kept as millisecond-precision sys_ms). All other // timestamps are whole-second integers. - auto purchased_ts = json_require_number(obj, "purchased_ts", result.errors); - auto redeemed_ts = json_require(obj, "redeemed_ts", result.errors); - auto expiry_ts = json_require(obj, "expiry_ts", result.errors); - auto grace_period_duration = - json_require(obj, "grace_period_duration", result.errors); + auto purchased_ts = json_require_number(obj, "purchased_ts", errs); + auto redeemed_ts = json_require(obj, "redeemed_ts", errs); + auto expiry_ts = json_require(obj, "expiry_ts", errs); + auto grace_period_duration = json_require(obj, "grace_period_duration", errs); auto platform_refund_expiry_ts = - json_require(obj, "platform_refund_expiry_ts", result.errors); - auto revoked_ts = json_require_number(obj, "revoked_ts", result.errors); - auto refund_requested_ts = json_require(obj, "refund_requested_ts", result.errors); + json_require(obj, "platform_refund_expiry_ts", errs); + auto revoked_ts = json_require_number(obj, "revoked_ts", errs); + auto refund_requested_ts = json_require(obj, "refund_requested_ts", errs); ProPaymentItem item = {}; item.status = std::move(status); @@ -579,11 +581,14 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { item.refund_requested_at = as_sys_seconds(refund_requested_ts); // Handle parsing result - if (result.errors.size()) + if (errs.size()) break; result.items.emplace_back(std::move(item)); } + + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } @@ -660,24 +665,18 @@ ProRequest refund_request( SetPaymentRefundRequestedResponse parse_refund(std::string_view json) { // Parse basics SetPaymentRefundRequestedResponse result = {}; - nlohmann::json j = json_parse(json, result.errors); - uint32_t status = json_require(j, "status", result.errors); - if (result.errors.size()) { + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } - // Parse errors - if (status != SESSION_PRO_BACKEND_STATUS_SUCCESS) { - parse_json_response_errors(j, result.errors); - return result; - } - - auto result_obj = json_require(j, "result", result.errors); - if (result.errors.size()) - return result; - // Parse payload - result.updated = json_require(result_obj, "updated", result.errors); + result.updated = json_require(result_obj, "updated", errs); + if (!errs.empty()) + set_protocol_error(result, errs.front()); return result; } } // namespace session::pro_backend @@ -691,6 +690,41 @@ using namespace session::pro_backend; static string8 C_PARSE_ERROR_OUT_OF_MEMORY = STRING8_LIT("Ran out-of-memory creating C response"); static string8 C_PARSE_ERROR_INVALID_ARGS = STRING8_LIT("One or more C arguments were NULL"); +// error_code slug libsession reports when it can't parse/build the C response at all. +static string8 C_INVALID_RESPONSE_CODE = STRING8_LIT("invalid_response"); + +// Map the C++ ResponseStatus to the C enum. +static SESSION_PRO_BACKEND_RESPONSE_STATUS c_response_status(ResponseStatus status) { + switch (status) { + case ResponseStatus::Ok: return SESSION_PRO_BACKEND_RESPONSE_STATUS_OK; + case ResponseStatus::Fail: return SESSION_PRO_BACKEND_RESPONSE_STATUS_FAIL; + case ResponseStatus::Error: break; + } + return SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; +} + +// Arena bytes needed for a header's error_code + error strings. +static size_t c_header_arena_bytes(const ResponseBase& cpp) { + size_t n = 0; + if (cpp.error_code) + n += cpp.error_code->size() + 1; + if (cpp.error) + n += cpp.error->size() + 1; + return n; +} + +// Fill a C response header's status + error_code/error from a C++ ResponseBase, copying the two +// optional strings into the arena (which must already have room; see c_header_arena_bytes). +static void fill_c_header( + session_pro_backend_response_header& header, const ResponseBase& cpp, arena_t& arena) { + header.status = c_response_status(cpp.status); + header.error_code = + cpp.error_code + ? arena_alloc_to_string8(&arena, cpp.error_code->data(), cpp.error_code->size()) + : string8{}; + header.error = cpp.error ? arena_alloc_to_string8(&arena, cpp.error->data(), cpp.error->size()) + : string8{}; +} // Wrap a freshly-built C++ ProRequest as an owning C session_pro_backend_request: heap-own the // ProRequest and point endpoint/content_type/data into it (zero copy). Released by @@ -783,8 +817,9 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) session_pro_backend_pro_proof_response result = {}; if (!json) { - result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_INVALID_ARGS; return result; } @@ -796,15 +831,15 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) // Calculate how much memory we need and create an arena arena_t arena = {}; { - for (const auto& it : cpp.errors) - arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); + arena.max += c_header_arena_bytes(cpp); if (arena.max) arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; return result; } @@ -827,14 +862,8 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) cpp.proof.rotating_pubkey.size()); std::memcpy(result.proof.sig.data, cpp.proof.sig.data(), cpp.proof.sig.size()); - // Copy errors - result.header.errors_count = cpp.errors.size(); - result.header.errors = static_cast( - arena_alloc(&arena, result.header.errors_count * sizeof(*result.header.errors))); - for (size_t index = 0; index < cpp.errors.size(); index++) { - const std::string& it = cpp.errors[index]; - result.header.errors[index] = arena_alloc_to_string8(&arena, it.data(), it.size()); - } + // Copy status + error code/message + fill_c_header(result.header, cpp, arena); return result; } @@ -842,8 +871,9 @@ LIBSESSION_C_API session_pro_backend_get_pro_revocations_response session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t json_len) { session_pro_backend_get_pro_revocations_response result = {}; if (!json) { - result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_INVALID_ARGS; return result; } @@ -858,15 +888,15 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t sizeof(cpp.items[0]) >= sizeof(*result.items), "Ensure we allocate enough memory. We might slightly over-allocate but that's not " "a big deal"); - for (auto it : cpp.errors) - arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); + arena.max += c_header_arena_bytes(cpp); if (arena.max) arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; return result; } @@ -879,14 +909,8 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t result.retry_in = cpp.retry_in.count(); result.retain_for = cpp.retain_for.count(); - // Copy errors - result.header.errors_count = cpp.errors.size(); - result.header.errors = (string8*)arena_alloc( - &arena, result.header.errors_count * sizeof(*result.header.errors)); - for (size_t index = 0; index < cpp.errors.size(); index++) { - const std::string& it = cpp.errors[index]; - result.header.errors[index] = arena_alloc_to_string8(&arena, it.data(), it.size()); - } + // Copy status + error code/message + fill_c_header(result.header, cpp, arena); // Copy items result.items_count = cpp.items.size(); @@ -906,8 +930,9 @@ LIBSESSION_C_API session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details_response_parse(const char* json, size_t json_len) { session_pro_backend_get_pro_details_response result = {}; if (!json) { - result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_INVALID_ARGS; return result; } @@ -918,15 +943,15 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json arena_t arena = {}; { arena.max += cpp.items.size() * sizeof(*result.items); - for (auto it : cpp.errors) - arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); + arena.max += c_header_arena_bytes(cpp); if (arena.max) arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; return result; } @@ -966,14 +991,8 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json dest.payment_id_count = session::copy_c_str(dest.payment_id, src.payment_id) - 1; } - // Copy errors - result.header.errors_count = cpp.errors.size(); - result.header.errors = (string8*)arena_alloc( - &arena, result.header.errors_count * sizeof(*result.header.errors)); - for (size_t index = 0; index < cpp.errors.size(); index++) { - const std::string& it = cpp.errors[index]; - result.header.errors[index] = arena_alloc_to_string8(&arena, it.data(), it.size()); - } + // Copy status + error code/message + fill_c_header(result.header, cpp, arena); return result; } @@ -1005,8 +1024,9 @@ LIBSESSION_C_API session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response_parse(const char* json, size_t json_len) { session_pro_backend_set_payment_refund_requested_response result = {}; if (!json) { - result.header.errors = &C_PARSE_ERROR_INVALID_ARGS; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_INVALID_ARGS; return result; } @@ -1016,15 +1036,15 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json // Calculate how much memory we need and create an arena arena_t arena = {}; { - for (auto it : cpp.errors) - arena.max += sizeof(*result.header.errors) + (it.size() + 1 /*null-terminator*/); + arena.max += c_header_arena_bytes(cpp); if (arena.max) arena.data = static_cast(calloc(1, arena.max)); if (arena.max && !arena.data) { - result.header.errors = &C_PARSE_ERROR_OUT_OF_MEMORY; - result.header.errors_count = 1; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; return result; } @@ -1035,14 +1055,8 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json // Copy to C struct result.updated = cpp.updated; - // Copy errors - result.header.errors_count = cpp.errors.size(); - result.header.errors = (string8*)arena_alloc( - &arena, result.header.errors_count * sizeof(*result.header.errors)); - for (size_t index = 0; index < cpp.errors.size(); index++) { - const std::string& it = cpp.errors[index]; - result.header.errors[index] = arena_alloc_to_string8(&arena, it.data(), it.size()); - } + // Copy status + error code/message + fill_c_header(result.header, cpp, arena); return result; } diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index e3db5561c..7f011a54d 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -166,7 +166,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { randombytes_buf(fake_revocation_tag.data(), fake_revocation_tag.size()); nlohmann::json j; - j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; + j["status"] = "ok"; j["result"] = { {"version", 0}, {"expiry_ts", unix_ts}, @@ -183,11 +183,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_pro_proof_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - INFO(result.header.errors[index].data); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors == nullptr); + if (result.header.error.data) + INFO(result.header.error.data); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error.data == nullptr); REQUIRE(result.proof.expiry_ts == unix_ts); REQUIRE(std::memcmp( result.proof.revocation_tag.data, @@ -227,8 +227,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // After freeing REQUIRE(result.header.internal_arena_buf_ == nullptr); - REQUIRE(result.header.errors == nullptr); - REQUIRE(result.header.errors_count == 0); + REQUIRE(result.header.error.data == nullptr); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); // Invalid JSON json = "{invalid}"; @@ -236,9 +236,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_pro_proof_response_free(&result); }}; - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count > 0); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } // After freeing @@ -247,16 +247,16 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_pro_proof_response_parse(nullptr, 0); - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count == 1); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); // No need to free, as errors point to static memory } SECTION("session_pro_backend_get_pro_revocations_response_parse") { nlohmann::json j; - j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; + j["status"] = "ok"; j["result"]["ticket"] = 123; j["result"]["retry_in"] = 3600; j["result"]["retain_for"] = 2592000; @@ -278,11 +278,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - INFO(result.header.errors[index].data); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors == nullptr); + if (result.header.error.data) + INFO(result.header.error.data); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error.data == nullptr); REQUIRE(result.ticket == 123); REQUIRE(result.retry_in == 3600); REQUIRE(result.retain_for == 2592000); @@ -307,10 +307,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { json.data(), json.size()); scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count > 0); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } // After freeing @@ -318,16 +317,16 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_get_pro_revocations_response_parse(nullptr, 0); - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count == 1); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } SECTION("session_pro_backend_get_pro_details_response_parse") { nlohmann::json j; - j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; + j["status"] = "ok"; j["result"] = { - {"status", "expired"}, + {"user_status", "expired"}, {"error_report", SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR}, {"auto_renewing", true}, @@ -361,12 +360,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - INFO(result.header.errors[index].data); + if (result.header.error.data) + INFO(result.header.error.data); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors == nullptr); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error.data == nullptr); REQUIRE(std::string_view(result.status, result.status_count) == "expired"); REQUIRE(result.error_report == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR); @@ -413,8 +412,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{[&]() { session_pro_backend_get_pro_details_response_free(&result_rangeproof); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - INFO(result_rangeproof.header.errors[index].data); + if (result_rangeproof.header.error.data) + INFO(result_rangeproof.header.error.data); // Only check what we expect to be different REQUIRE(std::string_view( @@ -438,9 +437,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count > 0); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } // After freeing @@ -449,9 +448,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_get_pro_details_response_parse(nullptr, 0); - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count == 1); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } SECTION("Memory management edge cases") { @@ -515,7 +514,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { SECTION("session_pro_backend_set_payment_refund_requested_response_parse") { nlohmann::json j; - j["status"] = SESSION_PRO_BACKEND_STATUS_SUCCESS; + j["status"] = "ok"; j["result"]["updated"] = true; std::string json = j.dump(); @@ -526,11 +525,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - INFO(result.header.errors[index].data); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors_count == 0); - REQUIRE(result.header.errors == nullptr); + if (result.header.error.data) + INFO(result.header.error.data); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error.data == nullptr); REQUIRE(result.updated); } @@ -545,10 +544,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; - for (size_t index = 0; index < result.header.errors_count; index++) - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count > 0); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } // After freeing @@ -556,9 +554,9 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Null JSON result = session_pro_backend_set_payment_refund_requested_response_parse(nullptr, 0); - REQUIRE(result.header.errors_count != 0); - REQUIRE(result.header.errors_count == 1); - REQUIRE(result.header.errors != nullptr); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code.data != nullptr); } } } @@ -672,10 +670,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { response_json.data(), response_json.size()); scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - INFO("ERROR: " << error.data); - } + if (response.header.error.data) + INFO("ERROR: " << response.header.error.data); // Verify response first_pro_proof = response.proof; @@ -720,12 +716,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - UNSCOPED_INFO("ERROR: " << error.data); - } - REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.errors_count == 0); + if (response.header.error.data) + UNSCOPED_INFO("ERROR: " << response.header.error.data); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); // Verify response session_protocol_pro_proof proof = response.proof; @@ -764,12 +758,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - UNSCOPED_INFO("ERROR: " << error.data); - } - REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.errors_count == 0); + if (response.header.error.data) + UNSCOPED_INFO("ERROR: " << response.header.error.data); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count > 0); } @@ -799,12 +791,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - UNSCOPED_INFO("ERROR: " << error.data); - } - REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.errors_count == 0); + if (response.header.error.data) + UNSCOPED_INFO("ERROR: " << response.header.error.data); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(std::string_view(response.status, response.status_count) == "active"); REQUIRE(response.items_count == 0); } @@ -879,12 +869,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response INFO("ERROR: JSON response: " << response_json.c_str()); - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - UNSCOPED_INFO("ERROR: " << error.data); - } - REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.errors_count == 0); + if (response.header.error.data) + UNSCOPED_INFO("ERROR: " << response.header.error.data); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.ticket == 0); REQUIRE(response.items_count == 0); } @@ -921,12 +909,10 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response INFO("ERROR: JSON response: " << response_json.c_str()); - for (size_t index = 0; index < response.header.errors_count; index++) { - string8 error = response.header.errors[index]; - UNSCOPED_INFO("ERROR: " << error.data); - } - REQUIRE(response.header.errors_count == 0); - REQUIRE(response.header.errors_count == 0); + if (response.header.error.data) + UNSCOPED_INFO("ERROR: " << response.header.error.data); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.updated); } } From ce1d17e9d0bf9b80e3784e807746480ed0747e14 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 17:32:06 -0300 Subject: [PATCH 126/193] Pro signing: sign the message directly (Delta #13), drop BLAKE2b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements pro-wire-protocol.md Delta #13 / §1.1: all five Ed25519 signatures (the offline proof §2 + the four signed requests §3.1–3.4) now sign the message bytes directly instead of a BLAKE2b-256 digest. - Add session::pro::signed_message() (src/pro_message.hpp): builds the signed message per §1.1 — 16-byte domain prefix, fixed-width raw keys/tags (self-delimiting), integers as canonical decimal ASCII (std::to_chars), strings verbatim, a single NUL between adjacent variable-length fields. - Replace ProProof::hash() with ProProof::signed_message() and delete the C session_protocol_pro_proof_hash() (the 32-byte digest no longer exists in the protocol). - Drop the now-dead hash.hpp includes; BLAKE2b is gone from the Pro signing path. - Retype/rename the 16-byte domain-prefix constants (session_protocol.hpp) from _b2b_pers spans to std::string_view, *_PERS -> *_DOMAIN; drop the C session_protocol_pro_proof_hash declaration. - Update tests to sign via signed_message() and to compare the proof's signature (not a hash) across the encode/decode round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/session_protocol.h | 14 ------ include/session/session_protocol.hpp | 28 +++++++---- src/pro_backend.cpp | 71 +++++++++++++++------------- src/pro_message.hpp | 66 ++++++++++++++++++++++++++ src/session_protocol.cpp | 36 +++++--------- tests/test_config_pro.cpp | 18 +++---- tests/test_session_protocol.cpp | 47 +++++++++--------- 7 files changed, 163 insertions(+), 117 deletions(-) create mode 100644 src/pro_message.hpp diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 6b7e3614f..07adfa853 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -182,20 +182,6 @@ struct session_protocol_decoded_community_message { // set/unset/test them with the standard bitwise operators (see the feature constants above). No // accessor functions are needed. -/// API: session_protocol/session_protocol_pro_proof_hash -/// -/// Generate the 32 byte hash that is to be signed by the rotating key or Session Pro Backend key to -/// embed in the envelope or proof respectively which other clients use to authenticate the validity -/// of a proof. -/// -/// Inputs: -/// - `proof` -- Proof to calculate the hash from -/// -/// Outputs: -/// - `cbytes32` -- The 32 byte hash calculated from the proof -LIBSESSION_EXPORT cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) - NON_NULL_ARG(1); - /// API: session_protocol/session_protocol_pro_proof_verify_signature /// /// Verify the proof was signed by the `verify_pubkey` diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index c717a8c80..c77f0ed77 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -39,13 +39,19 @@ namespace session { enum ProProofVersion { ProProofVersion_v0 }; -/// Session Pro personalisation bytes for hashing; must match the personalisation strings in -/// pro-wire-protocol.md §2 (proof) and §3 (signed requests). -inline constexpr auto GENERATE_PROOF_PERS = "ProGenerateProof"_b2b_pers; -inline constexpr auto BUILD_PROOF_PERS = "ProProof_v0_____"_b2b_pers; -inline constexpr auto ADD_PRO_PAYMENT_PERS = "ProAddPayment___"_b2b_pers; -inline constexpr auto SET_PAYMENT_REFUND_REQUESTED_PERS = "ProSetRefundReq_"_b2b_pers; -inline constexpr auto GET_PRO_DETAILS_PERS = "ProGetProDetReq_"_b2b_pers; +// Session Pro 16-byte signing domain prefixes; each prefixes the Ed25519-signed message for its +// endpoint (pro-wire-protocol.md §2 proof, §3 signed requests). ASCII, `_`-right-padded to 16 +// bytes. +inline constexpr std::string_view GENERATE_PROOF_DOMAIN = "ProGenerateProof"; +inline constexpr std::string_view BUILD_PROOF_DOMAIN = "ProProof_v0_____"; +inline constexpr std::string_view ADD_PRO_PAYMENT_DOMAIN = "ProAddPayment___"; +inline constexpr std::string_view SET_PAYMENT_REFUND_REQUESTED_DOMAIN = "ProSetRefundReq_"; +inline constexpr std::string_view GET_PRO_DETAILS_DOMAIN = "ProGetProDetReq_"; +static_assert(GENERATE_PROOF_DOMAIN.size() == 16); +static_assert(BUILD_PROOF_DOMAIN.size() == 16); +static_assert(ADD_PRO_PAYMENT_DOMAIN.size() == 16); +static_assert(SET_PAYMENT_REFUND_REQUESTED_DOMAIN.size() == 16); +static_assert(GET_PRO_DETAILS_DOMAIN.size() == 16); enum class ProStatus { // Pro proof sig was not signed by the Pro backend key @@ -153,10 +159,12 @@ class ProProof { std::chrono::sys_seconds unix_ts, const std::optional& signed_msg); - /// API: pro/Proof::hash + /// API: pro/Proof::signed_message /// - /// Create a 32-byte hash from the proof. This hash is the payload that is signed in the proof. - b32 hash() const; + /// Build the exact byte string that the backend signs to produce this proof's `sig`, and that + /// verification reconstructs to check it (pro-wire-protocol.md §2, per §1.1). The message is + /// Ed25519-signed directly — there is no pre-hash. + std::vector signed_message() const; bool operator==(const ProProof& other) const { return version == other.version && revocation_tag == other.revocation_tag && diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 4bc7d3949..f27896258 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -16,6 +15,7 @@ #include #include "internal-util.hpp" +#include "pro_message.hpp" namespace { const nlohmann::json json_parse(std::string_view json, std::vector& errors) { @@ -143,14 +143,19 @@ namespace { // --- add-payment (endpoint add_pro_payment) --- - b32 add_payment_hash( + std::vector add_payment_message( std::span master_pubkey, std::span rotating_pubkey, std::string_view provider_code, std::span payment_id) { - // Must match the add-payment signed-request hash in pro-wire-protocol.md §3.2 (+ §3.5). - return hash::blake2b_pers<32>( - ADD_PRO_PAYMENT_PERS, master_pubkey, rotating_pubkey, provider_code, payment_id); + // Must match the add-payment signed-request message in pro-wire-protocol.md §3.2 + // (+ §3.5), built per §1.1. + return pro::signed_message( + ADD_PRO_PAYMENT_DOMAIN, + master_pubkey, + rotating_pubkey, + provider_code, + to_string_view(payment_id)); } // Serialise an add-payment request body from already-computed fields (shared by the C++ @@ -233,11 +238,11 @@ MasterRotatingSignatures add_payment_sigs( const ed25519::PrivKeySpan& rotating_privkey, std::string_view provider_code, std::span payment_id) { - auto hash = add_payment_hash( + auto msg = add_payment_message( master_privkey.pubkey(), rotating_privkey.pubkey(), provider_code, payment_id); MasterRotatingSignatures result = {}; - result.master_sig = ed25519::sign(master_privkey, hash); - result.rotating_sig = ed25519::sign(rotating_privkey, hash); + result.master_sig = ed25519::sign(master_privkey, msg); + result.rotating_sig = ed25519::sign(rotating_privkey, msg); return result; } @@ -347,13 +352,14 @@ namespace { // --- generate-proof (endpoint generate_pro_proof) --- - b32 generate_proof_hash( + std::vector generate_proof_message( std::span master_pubkey, std::span rotating_pubkey, std::chrono::sys_seconds unix_ts) { - // Must match the generate-proof signed-request hash in pro-wire-protocol.md §3.1. - int64_t ts = epoch_seconds(unix_ts); - return hash::blake2b_pers<32>(GENERATE_PROOF_PERS, master_pubkey, rotating_pubkey, ts); + // Must match the generate-proof signed-request message in pro-wire-protocol.md §3.1, + // built per §1.1. + return pro::signed_message( + GENERATE_PROOF_DOMAIN, master_pubkey, rotating_pubkey, epoch_seconds(unix_ts)); } std::string generate_proof_body( @@ -377,10 +383,10 @@ MasterRotatingSignatures pro_proof_sigs( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, std::chrono::sys_seconds unix_ts) { - auto hash = generate_proof_hash(master_privkey.pubkey(), rotating_privkey.pubkey(), unix_ts); + auto msg = generate_proof_message(master_privkey.pubkey(), rotating_privkey.pubkey(), unix_ts); MasterRotatingSignatures result = {}; - result.master_sig = ed25519::sign(master_privkey, hash); - result.rotating_sig = ed25519::sign(rotating_privkey, hash); + result.master_sig = ed25519::sign(master_privkey, msg); + result.rotating_sig = ed25519::sign(rotating_privkey, msg); return result; } @@ -454,13 +460,14 @@ namespace { // --- payment-details / get-pro-details (endpoint get_pro_details) --- - b32 payment_details_hash( + std::vector payment_details_message( std::span master_pubkey, std::chrono::sys_seconds unix_ts, uint32_t count) { - // Must match the get-pro-details signed-request hash in pro-wire-protocol.md §3.4. - int64_t ts = epoch_seconds(unix_ts); - return hash::blake2b_pers<32>(GET_PRO_DETAILS_PERS, master_pubkey, ts, count); + // Must match the get-pro-details signed-request message in pro-wire-protocol.md §3.4, + // built per §1.1. + return pro::signed_message( + GET_PRO_DETAILS_DOMAIN, master_pubkey, epoch_seconds(unix_ts), count); } std::string payment_details_body( @@ -482,8 +489,8 @@ b64 payment_details_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, uint32_t count) { - auto hash = payment_details_hash(master_privkey.pubkey(), unix_ts, count); - return ed25519::sign(master_privkey, hash); + auto msg = payment_details_message(master_privkey.pubkey(), unix_ts, count); + return ed25519::sign(master_privkey, msg); } ProRequest payment_details_request( @@ -596,23 +603,21 @@ namespace { // --- refund / set-payment-refund-requested (endpoint set_payment_refund_requested) --- - b32 refund_hash( + std::vector refund_message( std::span master_pubkey, std::chrono::sys_seconds unix_ts, std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { - // Must match the set-payment-refund-requested signed-request hash in pro-wire-protocol.md - // §3.3 (+ §3.5 for payment_id). - int64_t ts = epoch_seconds(unix_ts); - int64_t refund_requested_ts = epoch_seconds(refund_requested_at); - return hash::blake2b_pers<32>( - SET_PAYMENT_REFUND_REQUESTED_PERS, + // Must match the set-payment-refund-requested signed-request message in + // pro-wire-protocol.md §3.3 (+ §3.5 for payment_id), built per §1.1. + return pro::signed_message( + SET_PAYMENT_REFUND_REQUESTED_DOMAIN, master_pubkey, - ts, - refund_requested_ts, + epoch_seconds(unix_ts), + epoch_seconds(refund_requested_at), provider_code, - payment_id); + to_string_view(payment_id)); } std::string refund_body( @@ -639,9 +644,9 @@ b64 refund_sig( std::chrono::sys_seconds refund_requested_at, std::string_view provider_code, std::span payment_id) { - auto hash = refund_hash( + auto msg = refund_message( master_privkey.pubkey(), unix_ts, refund_requested_at, provider_code, payment_id); - return ed25519::sign(master_privkey, hash); + return ed25519::sign(master_privkey, msg); } ProRequest refund_request( diff --git a/src/pro_message.hpp b/src/pro_message.hpp new file mode 100644 index 000000000..3d7687357 --- /dev/null +++ b/src/pro_message.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace session::pro { + +/// Builds the byte string that is Ed25519-signed for a Pro proof (§2) or a signed request (§3), +/// following the signed-message construction in pro-wire-protocol.md §1.1. Pass the 16-byte domain +/// prefix first, then the fields in order; each field is encoded by its type: +/// +/// - **byte-spannable** (public key, revocation tag — fixed-width raw values): appended verbatim +/// and self-delimiting; +/// - **integer**: canonical decimal ASCII (`std::to_chars`, base 10, locale-independent — never a +/// locale-aware formatter); +/// - **string_view** (`provider_code`, and the opaque variable-length `payment_id` via +/// `to_string_view`): its bytes verbatim. +/// +/// A single NUL byte is inserted between two *adjacent* variable-length fields (integer/string); +/// raw fields need no separator, and the domain prefix (also raw) never precedes one. The message +/// is signed directly — there is no pre-hash (Ed25519 hashes internally). +template +std::vector signed_message(std::string_view domain, const Fields&... fields) { + std::vector buf; + bool prev_var = false; // the previously-appended field was variable-length (int/string) + + auto put_chars = [&](std::string_view s) { + for (char c : s) + buf.push_back(static_cast(static_cast(c))); + }; + put_chars(domain); // domain prefix: fixed-width, self-delimiting + + auto append = [&](const auto& field) { + using T = std::remove_cvref_t; + if constexpr (std::is_integral_v) { + if (prev_var) + buf.push_back(std::byte{0}); + char tmp[24]; // enough for -9223372036854775808 (20 chars) + auto [ptr, ec] = std::to_chars(tmp, tmp + sizeof(tmp), field); + put_chars({tmp, static_cast(ptr - tmp)}); + prev_var = true; + } else if constexpr (std::convertible_to) { + if (prev_var) + buf.push_back(std::byte{0}); + put_chars(field); + prev_var = true; + } else { + static_assert( + std::convertible_to>, + "signed_message() fields must be an integer, a string_view, or a " + "byte-spannable raw value (e.g. a public key or tag)"); + std::span b = field; + buf.insert(buf.end(), b.begin(), b.end()); + prev_var = false; + } + }; + (append(fields), ...); + return buf; +} + +} // namespace session::pro diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 68d395f3c..07308f285 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" #include "internal-util.hpp" +#include "pro_message.hpp" #include "session/export.h" // clang-format off @@ -50,16 +50,16 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { // clang-format on namespace { -session::b32 proof_hash_internal( +std::vector proof_signed_message( std::span revocation_tag, std::span rotating_pubkey, std::int64_t expiry_ts) { - // This must match the Pro proof signed digest in pro-wire-protocol.md §2. The proof version is - // NOT hashed as a byte; it selects the personalisation (v0 -> "ProProof_v0_____"), and that - // choice of personalisation is what binds the version into the signature. - return session::hash::blake2b_pers<32>( - session::BUILD_PROOF_PERS, revocation_tag, rotating_pubkey, expiry_ts); + // This must match the Pro proof signed message in pro-wire-protocol.md §2 (built per §1.1). The + // proof version is NOT part of the message; it selects the 16-byte domain prefix (v0 -> + // "ProProof_v0_____"), and that choice of prefix is what binds the version into the signature. + return session::pro::signed_message( + session::BUILD_PROOF_DOMAIN, revocation_tag, rotating_pubkey, expiry_ts); } // Copies an optional 32-byte value out of a possibly-null C pointer. A null pointer yields a @@ -127,7 +127,7 @@ static_assert(sizeof(std::declval().rotating_pubkey) == 32); static_assert(sizeof(std::declval().sig) == 64); bool ProProof::verify_signature(std::span verify_pubkey) const { - return ed25519::verify(sig, verify_pubkey, hash()); + return ed25519::verify(sig, verify_pubkey, signed_message()); } bool ProProof::verify_message( @@ -161,10 +161,8 @@ ProStatus ProProof::status( return result; } -b32 ProProof::hash() const { - b32 result = - proof_hash_internal(revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_at)); - return result; +std::vector ProProof::signed_message() const { + return proof_signed_message(revocation_tag, rotating_pubkey, session::epoch_seconds(expiry_at)); } }; // namespace session @@ -799,27 +797,17 @@ const uint64_t SESSION_PROTOCOL_PRO_PROFILE_FEATURE_ANIMATED_AVATAR = const uint64_t SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_10K_CHARACTER_LIMIT = static_cast(ProMessageFlags::CharLimit10k); -LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { - cbytes32 result = {}; - session::b32 hash = proof_hash_internal( - to_byte_span(proof->revocation_tag.data), - to_byte_span(proof->rotating_pubkey.data), - proof->expiry_ts); - std::memcpy(result.data, hash.data(), hash.size()); - return result; -} - LIBSESSION_C_API bool session_protocol_pro_proof_verify_signature( session_protocol_pro_proof const* proof, uint8_t const* verify_pubkey, size_t verify_pubkey_len) { if (verify_pubkey_len != 32) return false; - session::b32 hash = proof_hash_internal( + auto msg = proof_signed_message( to_byte_span(proof->revocation_tag.data), to_byte_span(proof->rotating_pubkey.data), proof->expiry_ts); - return ed25519::verify(to_byte_span(proof->sig.data), to_byte_span<32>(verify_pubkey), hash); + return ed25519::verify(to_byte_span(proof->sig.data), to_byte_span<32>(verify_pubkey), msg); } LIBSESSION_C_API bool session_protocol_pro_proof_verify_message( diff --git a/tests/test_config_pro.cpp b/tests/test_config_pro.cpp index da74412e6..851707066 100644 --- a/tests/test_config_pro.cpp +++ b/tests/test_config_pro.cpp @@ -39,21 +39,15 @@ TEST_CASE("Pro", "[config][pro]") { std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size()); } - // Generate and write the hashes that are signed by the faux pro backend into the proof + // Sign the proof with the faux pro backend key { - // Generate the hashes + // Sign the proof with the faux pro backend key (Ed25519 over the message directly). The C + // and C++ proof representations mirror each other, so a single message signs both. static_assert(crypto_sign_ed25519_BYTES == pro_cpp.proof.sig.max_size()); - b32 hash_to_sign_cpp = pro_cpp.proof.hash(); - cbytes32 hash_to_sign = session_protocol_pro_proof_hash(&pro.proof); + auto msg_to_sign = pro_cpp.proof.signed_message(); - static_assert(hash_to_sign_cpp.size() == sizeof(hash_to_sign)); - CHECK(std::memcmp(hash_to_sign_cpp.data(), hash_to_sign.data, hash_to_sign_cpp.size()) == - 0); - - // Write the signature into the proof - ed25519::sign(pro_cpp.proof.sig, signing_sk, hash_to_sign_cpp); - ed25519::sign( - to_byte_span(pro.proof.sig.data), signing_sk, to_byte_span(hash_to_sign.data)); + ed25519::sign(pro_cpp.proof.sig, signing_sk, msg_to_sign); + ed25519::sign(to_byte_span(pro.proof.sig.data), signing_sk, msg_to_sign); } // Verify expiry diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index c4759cc85..75d309d52 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -19,9 +19,7 @@ struct SerialisedProtobufContentWithProForTesting { std::vector plaintext_padded; b64 sig_over_plaintext_with_user_pro_key; b64 sig_over_plaintext_padded_with_user_pro_key; - b32 pro_proof_hash; cbytes64 sig_over_plaintext_with_user_pro_key_c; - cbytes32 pro_proof_hash_c; }; static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_session_pro( @@ -47,9 +45,8 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se std::ranges::copy(user_rotating_privkey.pubkey(), result.proof.rotating_pubkey.begin()); result.proof.expiry_at = pro_expiry_at; - // Sign the proof by the dummy "Session Pro Backend" key - result.pro_proof_hash = result.proof.hash(); - result.proof.sig = ed25519::sign(pro_backend_privkey, result.pro_proof_hash); + // Sign the proof by the dummy "Session Pro Backend" key (Ed25519 over the message directly) + result.proof.sig = ed25519::sign(pro_backend_privkey, result.proof.signed_message()); // Create protobuf `Content.proMessage` SessionProtos::ProMessage* pro = content.mutable_promessage(); @@ -83,10 +80,6 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se result.sig_over_plaintext_with_user_pro_key_c.data, result.sig_over_plaintext_with_user_pro_key.data(), sizeof(result.sig_over_plaintext_with_user_pro_key_c.data)); - std::memcpy( - result.pro_proof_hash_c.data, - result.pro_proof_hash.data(), - sizeof(result.pro_proof_hash_c.data)); return result; } @@ -263,17 +256,15 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro ProProof nil_proof = {}; - b32 nil_hash = nil_proof.hash(); - cbytes32 decrypt_result_pro_hash = - session_protocol_pro_proof_hash(&decrypt_result.pro.proof); REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_NIL); // Pro was not attached REQUIRE(decrypt_result.pro.msg_bitset == 0); REQUIRE(decrypt_result.pro.profile_bitset == 0); + // No proof was attached, so the decoded proof is empty (matches a default-constructed one). REQUIRE(std::memcmp( - decrypt_result_pro_hash.data, - nil_hash.data(), - sizeof(decrypt_result_pro_hash.data)) == 0); + decrypt_result.pro.proof.sig.data, + nil_proof.sig.data(), + sizeof(decrypt_result.pro.proof.sig.data)) == 0); // Verify it is decryptable SessionProtos::Content decrypt_content = {}; @@ -391,9 +382,12 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); - REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == - 0); + // The proof survived the encode/decode round-trip: its authenticating signature is + // unchanged (Ed25519 is deterministic over the canonical message). + REQUIRE(std::memcmp( + decrypt_result.pro.proof.sig.data, + protobuf_content.proof.sig.data(), + sizeof(decrypt_result.pro.proof.sig.data)) == 0); REQUIRE(decrypt_result.pro.msg_bitset == 0); // No features requested REQUIRE(decrypt_result.pro.profile_bitset == 0); // No features requested @@ -465,9 +459,12 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); - REQUIRE(std::memcmp(hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == - 0); + // The proof survived the encode/decode round-trip: its authenticating signature is + // unchanged (Ed25519 is deterministic over the canonical message). + REQUIRE(std::memcmp( + decrypt_result.pro.proof.sig.data, + protobuf_content.proof.sig.data(), + sizeof(decrypt_result.pro.proof.sig.data)) == 0); REQUIRE( (decrypt_result.pro.profile_bitset & SESSION_PROTOCOL_PRO_PROFILE_FEATURE_PRO_BADGE)); @@ -611,10 +608,12 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro REQUIRE(decrypt_result.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); // Pro was attached - cbytes32 hash = session_protocol_pro_proof_hash(&decrypt_result.pro.proof); + // The proof survived the encode/decode round-trip: its authenticating signature is + // unchanged (Ed25519 is deterministic over the canonical message). REQUIRE(std::memcmp( - hash.data, protobuf_content.pro_proof_hash.data(), sizeof(hash.data)) == - 0); + decrypt_result.pro.proof.sig.data, + protobuf_content.proof.sig.data(), + sizeof(decrypt_result.pro.proof.sig.data)) == 0); REQUIRE(decrypt_result.pro.msg_bitset == 0); // No features requested REQUIRE(decrypt_result.pro.profile_bitset == 0); // No features requested From 13817c6f5362e7eac99050da80cbbf9b107ae18d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 22:02:35 -0300 Subject: [PATCH 127/193] =?UTF-8?q?pro=5Fbackend:=20add=20visible=5Fplatfo?= =?UTF-8?q?rms()=20=E2=80=94=20user-visible=20purchasable=20slugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client-facing enumeration of the user-visible purchasable payment platforms (provider-code slugs: google_play, app_store) so clients build their "purchase via …" store list at runtime instead of hardcoding two stores. Hidden mechanisms (rangeproof) are handled but never listed — they surface only for users who already hold them. C++ `visible_platforms()` returns std::span; C `session_pro_backend_visible_platforms(size_t*)` returns a static pointer-array derived from it. Both built from the existing SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constants (single source). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 8 ++++++++ include/session/pro_backend.hpp | 7 +++++++ src/pro_backend.cpp | 21 +++++++++++++++++++++ tests/test_pro_backend.cpp | 24 ++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 0674d7a7b..67e609f17 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -67,6 +67,14 @@ struct session_pro_backend_provider_urls { LIBSESSION_EXPORT session_pro_backend_provider_urls session_pro_backend_get_provider_urls(const char* provider_code) NON_NULL_ARG(1); +/// API: session_pro_backend/visible_platforms +/// +/// Returns the user-visible purchasable platforms as an array of `*count` provider-code C strings +/// (a subset of the SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* values). Order is not significant; +/// clients pick their own. Hidden mechanisms (e.g. rangeproof) are excluded. The returned array and +/// its strings are static storage owned by libsession — do not free. +LIBSESSION_EXPORT const char* const* session_pro_backend_visible_platforms(size_t* count); + typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS, SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR, diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 5ffa4b0e5..d71c9988a 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -137,6 +137,13 @@ struct ProviderUrls { /// hunting for empty members. std::optional provider_urls(std::string_view provider_code); +/// The user-visible purchasable platforms, as provider-code slugs (a subset of the +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* values — currently "google_play" and "app_store"). +/// Order is not significant; clients pick their own. Hidden mechanisms (e.g. rangeproof) are +/// excluded — they surface only for users who already hold them — so this is the set that drives a +/// "purchase via …" store list. +std::span visible_platforms(); + /// Endpoint + content-type + opaque payload for a request to be POSTed to the Session Pro backend, /// returned by the `*_request()` helpers below. Callers relay `data` verbatim under `content_type` /// and never inspect or assume its format -- libsession owns the wire encoding. diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index f27896258..e0e6ba65c 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -233,6 +233,27 @@ session_pro_backend_get_provider_urls(const char* provider_code) { return {nullptr, nullptr, nullptr, nullptr, nullptr}; } +std::span visible_platforms() { + static const std::array platforms = { + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE}; + return platforms; +} + +LIBSESSION_C_API const char* const* session_pro_backend_visible_platforms(size_t* count) { + // Derived from the C++ list (single source); each slug's data() is a static, null-terminated + // literal, so the pointer array is safe to hand out as static storage. + static const std::vector codes = [] { + std::vector v; + for (auto slug : visible_platforms()) + v.push_back(slug.data()); + return v; + }(); + if (count) + *count = codes.size(); + return codes.data(); +} + MasterRotatingSignatures add_payment_sigs( const ed25519::PrivKeySpan& master_privkey, const ed25519::PrivKeySpan& rotating_privkey, diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 7f011a54d..330d49131 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -574,6 +574,30 @@ TEST_CASE("Pro Backend X25519 pubkey matches the converted Ed25519 pubkey", "[pr 0); } +TEST_CASE( + "Pro Backend visible_platforms lists purchasable stores, excludes rangeproof", + "[pro_backend]") { + auto platforms = visible_platforms(); + auto has = [](std::span v, std::string_view s) { + for (auto x : v) + if (x == s) + return true; + return false; + }; + REQUIRE(has(platforms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY)); + REQUIRE(has(platforms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE)); + // Hidden mechanisms are handled but never listed. + REQUIRE_FALSE(has(platforms, SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF)); + REQUIRE(platforms.size() == 2); + + // The C export returns the same slugs, in the same order (it is derived from the C++ list). + size_t count = 0; + const char* const* c = session_pro_backend_visible_platforms(&count); + REQUIRE(count == platforms.size()); + for (size_t i = 0; i < count; i++) + REQUIRE(std::string_view{c[i]} == platforms[i]); +} + #if defined(TEST_PRO_BACKEND_WITH_DEV_SERVER) #include From d86a720e37c5f405a475bf7ef5b9c248ac16a1f6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 14:08:18 -0300 Subject: [PATCH 128/193] pro_backend: parse the plan billing-period code (Delta #14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse the closed `plan` grammar (pro-wire-protocol.md §1 / Delta #14) in libsession rather than handing clients the raw wire code to reinterpret. get-details items now carry a parsed ProPlanPeriod{count, unit} (unit ∈ second/day/week/month/year/lifetime) instead of a raw string. - Add parse_plan_period() + ProPlanPeriod/ProPlanUnit (C++), mirrored by SESSION_PRO_BACKEND_PLAN_UNIT + plan_count/plan_unit on the C item. - The unit is preserved, never canonicalized ("12m" != "1y" as values). "lifetime" -> {0, lifetime}; invariant count == 0 iff unit == lifetime. - Closed grammar (a backend<->libsession contract, like the §5 status enum): an unrecognized code is a protocol error, not passed through raw. - Reserves a testing-only `s` (seconds) unit; single-unit only ("1y6m" invalid). Response-parse only -- no wire or signing change. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 21 ++++++++++++--- include/session/pro_backend.hpp | 26 +++++++++++++++--- src/pro_backend.cpp | 48 +++++++++++++++++++++++++++++---- tests/test_pro_backend.cpp | 45 ++++++++++++++++++++++++++++++- 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 67e609f17..6d62bea38 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -151,15 +151,30 @@ struct session_pro_backend_get_pro_revocations_response { size_t items_count; }; +/// Unit of a parsed billing period (`plan_unit` below); mirrors C++ +/// session::pro_backend::ProPlanUnit (same order). A closed set (pro-wire-protocol.md §1 / Delta +/// #14). +typedef enum SESSION_PRO_BACKEND_PLAN_UNIT { + SESSION_PRO_BACKEND_PLAN_UNIT_SECOND, + SESSION_PRO_BACKEND_PLAN_UNIT_DAY, + SESSION_PRO_BACKEND_PLAN_UNIT_WEEK, + SESSION_PRO_BACKEND_PLAN_UNIT_MONTH, + SESSION_PRO_BACKEND_PLAN_UNIT_YEAR, + SESSION_PRO_BACKEND_PLAN_UNIT_LIFETIME, +} SESSION_PRO_BACKEND_PLAN_UNIT; + typedef struct session_pro_backend_pro_payment_item session_pro_backend_pro_payment_item; struct session_pro_backend_pro_payment_item { /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); /// unknown values pass through as-is char status[64]; size_t status_count; - /// Billing-period code (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for non-period plans - char plan[64]; - size_t plan_count; + /// Parsed billing period (pro-wire-protocol.md §1 / Delta #14). `plan_count` is the period + /// count + /// (>= 1 for periodic units); for `plan_unit == ..._LIFETIME` it is 0 and not meaningful + /// (switch on `plan_unit`, never render " " for lifetime). + int plan_count; + SESSION_PRO_BACKEND_PLAN_UNIT plan_unit; /// Provider code (e.g. "google_play"); opaque -- an unknown value passes through as-is char payment_provider[64]; size_t payment_provider_count; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index d71c9988a..c12495560 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -293,6 +293,25 @@ ProRequest payment_details_request( std::chrono::sys_seconds unix_ts, uint32_t count); +/// Unit of a billing period (the `unit` of a parsed `plan` code, pro-wire-protocol.md §1 / Delta +/// #14). A closed set: the backend emits only these, so an unrecognized code is a protocol error. +enum class ProPlanUnit { second, day, week, month, year, lifetime }; + +/// A parsed `plan` billing-period code. The `unit` is preserved exactly as transmitted and never +/// canonicalized ("12m" and "1y" are the same duration but distinct values). `count` is meaningful +/// only for the periodic units and is >= 1; for `lifetime` it is 0 (invariant: count == 0 iff unit +/// == lifetime), so consumers switch on `unit` and never render " " for lifetime. +struct ProPlanPeriod { + int count; + ProPlanUnit unit; +}; + +/// Parse a `plan` billing-period code (pro-wire-protocol.md §1, Delta #14): "" with N a +/// positive integer (no leading zeros) and unit one of s/d/w/m/y (second/day/week/month/year), or +/// the literal "lifetime". Single-unit only ("1y6m" is invalid). Returns std::nullopt for any +/// non-conforming code (the caller treats that as a protocol error). +std::optional parse_plan_period(std::string_view plan_code); + struct ProPaymentItem { /// Describes the current status of the consumption of the payment for Session Pro entitlement /// The status should be used to determine which timestamps should be used. @@ -302,9 +321,10 @@ struct ProPaymentItem { /// book-keeping purposes. std::string status; - /// Billing-period code that was purchased (e.g. "1m"/"3m"/"1y"); opaque, may be free-form for - /// non-period plans. The client maps/parses it for display. - std::string plan; + /// The parsed billing period that was purchased (e.g. "1m" -> {1, month}, "lifetime" -> {0, + /// lifetime}). libsession parses the closed `plan` grammar (§1 / Delta #14); the client just + /// localizes the display. The unit is preserved as transmitted, never canonicalized. + ProPlanPeriod plan; /// Provider code this payment came from (e.g. "google_play"); opaque -- an unknown value passes /// through as-is for the client to handle. diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index e0e6ba65c..fc5266df2 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -240,6 +241,36 @@ std::span visible_platforms() { return platforms; } +std::optional parse_plan_period(std::string_view code) { + // pro-wire-protocol.md §1 / Delta #14: closed grammar. "lifetime", or "" with N a + // positive integer (no leading zeros) and unit one of s/d/w/m/y. Single-unit only. + if (code == "lifetime") + return ProPlanPeriod{0, ProPlanUnit::lifetime}; + + if (code.size() < 2) + return std::nullopt; + ProPlanUnit unit; + switch (code.back()) { + case 's': unit = ProPlanUnit::second; break; + case 'd': unit = ProPlanUnit::day; break; + case 'w': unit = ProPlanUnit::week; break; + case 'm': unit = ProPlanUnit::month; break; + case 'y': unit = ProPlanUnit::year; break; + default: return std::nullopt; + } + + // N = [1-9][0-9]*: reject a leading zero (or sign) up front, then require from_chars to consume + // every remaining digit into a positive int (this also rejects overflow). + std::string_view digits = code.substr(0, code.size() - 1); + if (digits.empty() || digits.front() < '1' || digits.front() > '9') + return std::nullopt; + int count = 0; + auto [ptr, ec] = std::from_chars(digits.data(), digits.data() + digits.size(), count); + if (ec != std::errc{} || ptr != digits.data() + digits.size()) + return std::nullopt; + return ProPlanPeriod{count, unit}; +} + LIBSESSION_C_API const char* const* session_pro_backend_visible_platforms(size_t* count) { // Derived from the C++ list (single source); each slug's data() is a static, null-terminated // literal, so the pointer array is safe to hand out as static storage. @@ -574,7 +605,11 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { // Parse payment item auto obj = it.get(); auto status = json_require(obj, "status", errs); - auto plan = json_require(obj, "plan", errs); + auto plan_code = json_require(obj, "plan", errs); + auto plan = parse_plan_period(plan_code); + if (!plan) + errs.push_back( + fmt::format("'plan' is not a recognized billing-period code: '{}'", plan_code)); auto payment_provider = json_require(obj, "payment_provider", errs); auto payment_id = json_require(obj, "payment_id", errs); auto auto_renewing = json_require(obj, "auto_renewing", errs); @@ -593,9 +628,11 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { ProPaymentItem item = {}; item.status = std::move(status); - // plan / payment_provider / payment_id are opaque strings; libsession does not validate or - // interpret them (an unknown provider or plan code passes through as-is). - item.plan = std::move(plan); + // `plan` is parsed (closed grammar, §1 / Delta #14); an unrecognized code is a protocol + // error (handled above). payment_provider / payment_id are opaque strings that pass through + // as-is. + if (plan) + item.plan = *plan; item.payment_provider = std::move(payment_provider); item.payment_id = std::move(payment_id); @@ -1003,7 +1040,8 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json const ProPaymentItem& src = cpp.items[index]; session_pro_backend_pro_payment_item& dest = result.items[index]; dest.status_count = session::copy_c_str(dest.status, src.status) - 1; - dest.plan_count = session::copy_c_str(dest.plan, src.plan) - 1; + dest.plan_count = src.plan.count; + dest.plan_unit = static_cast(src.plan.unit); dest.payment_provider_count = session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; dest.purchased_ts = epoch_seconds_double(src.purchased_at); diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 330d49131..166d2bdde 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -378,7 +378,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items != nullptr); REQUIRE(std::string_view(result.items[0].status, result.items[0].status_count) == "redeemed"); - REQUIRE(std::string_view(result.items[0].plan, result.items[0].plan_count) == "1m"); + REQUIRE(result.items[0].plan_count == 1); + REQUIRE(result.items[0].plan_unit == SESSION_PRO_BACKEND_PLAN_UNIT_MONTH); REQUIRE(std::string_view( result.items[0].payment_provider, result.items[0].payment_provider_count) == @@ -598,6 +599,48 @@ TEST_CASE( REQUIRE(std::string_view{c[i]} == platforms[i]); } +TEST_CASE("Pro Backend parse_plan_period (closed grammar)", "[pro_backend]") { + using enum ProPlanUnit; + auto ok = [](std::string_view code, int n, ProPlanUnit u) { + auto p = parse_plan_period(code); + REQUIRE(p.has_value()); + CHECK(p->count == n); + CHECK(p->unit == u); + }; + + // Every unit; count preserved verbatim. + ok("30s", 30, second); + ok("14d", 14, day); + ok("2w", 2, week); + ok("1m", 1, month); + ok("3m", 3, month); + ok("1y", 1, year); + // Unit is preserved, never canonicalized: "12m" stays (12, month), not (1, year). + ok("12m", 12, month); + // lifetime: count 0, and the invariant count == 0 iff unit == lifetime. + ok("lifetime", 0, lifetime); + + // Non-conforming codes -> nullopt (caller treats as a protocol error; no raw pass-through). + for (std::string_view bad : + {"", + "m", + "1", + "0m", + "01m", + "1y6m", + "-1m", + "+1m", + "1x", + "1 m", + "1M", + "1mm", + "1.5m", + "9999999999999999999m", + "lifetime2", + "Lifetime"}) + CHECK_FALSE(parse_plan_period(bad).has_value()); +} + #if defined(TEST_PRO_BACKEND_WITH_DEV_SERVER) #include From abe6e70acf004db3e2585c822a404f79a57941d9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 14:13:41 -0300 Subject: [PATCH 129/193] pro_backend.h: collapse redundant forward-typedef struct declarations Use the idiomatic `typedef struct X { ... } X;` instead of the split `typedef struct X X; struct X { ... };`. None of these structs are self- or mutually-referential, so the forward typedef bought nothing but extra lines. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 48 +++++++++++++---------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 6d62bea38..18eedd01c 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -50,14 +50,13 @@ LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY_X /// client duplicating them; the human-readable provider/store names are translation data and remain /// the client's job. An unknown provider code (or one with no applicable URLs, e.g. rangeproof) /// yields all-NULL fields. Otherwise each is a static, null-terminated C string. -typedef struct session_pro_backend_provider_urls session_pro_backend_provider_urls; -struct session_pro_backend_provider_urls { +typedef struct session_pro_backend_provider_urls { const char* refund_platform_url; /// Native store refund flow const char* refund_support_url; /// Session support page for requesting a refund const char* refund_status_url; /// Where a user checks refund status const char* update_subscription_url; /// Manage/update the subscription const char* cancel_subscription_url; /// Cancel the subscription -}; +} session_pro_backend_provider_urls; /// API: session_pro_backend/get_provider_urls /// @@ -91,8 +90,7 @@ typedef enum SESSION_PRO_BACKEND_RESPONSE_STATUS { ///< later. } SESSION_PRO_BACKEND_RESPONSE_STATUS; -typedef struct session_pro_backend_response_header session_pro_backend_response_header; -struct session_pro_backend_response_header { +typedef struct session_pro_backend_response_header { /// Outcome category. Success iff `status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK`. SESSION_PRO_BACKEND_RESPONSE_STATUS status; /// On non-OK, the machine-readable outcome slug (spec §5.1); {NULL, 0} on success. Opaque and @@ -104,15 +102,14 @@ struct session_pro_backend_response_header { string8 error; unsigned char* internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch -}; +} session_pro_backend_response_header; /// A built request to POST to the Session Pro backend. Mirrors the C++ /// `session::pro_backend::ProRequest`: an `endpoint`, the `content_type` to send as the request's /// Content-Type header, and the opaque `data` payload. `data` is libsession's data for the backend /// -- relay it verbatim with the given `content_type` and do NOT parse, inspect, or assume a format /// for it. libsession owns the wire encoding and may change it without client involvement. -typedef struct session_pro_backend_request session_pro_backend_request; -struct session_pro_backend_request { +typedef struct session_pro_backend_request { char error[256]; size_t error_count; bool success; /// True if the request was built successfully, false if out-of-memory @@ -124,24 +121,20 @@ struct session_pro_backend_request { string8 data; /// Owned C++ object backing endpoint/content_type/data; do not touch (freed by request_free). void* internal_; -}; +} session_pro_backend_request; -typedef struct session_pro_backend_pro_proof_response session_pro_backend_pro_proof_response; -struct session_pro_backend_pro_proof_response { +typedef struct session_pro_backend_pro_proof_response { session_pro_backend_response_header header; session_protocol_pro_proof proof; -}; +} session_pro_backend_pro_proof_response; -typedef struct session_pro_backend_pro_revocation_item session_pro_backend_pro_revocation_item; -struct session_pro_backend_pro_revocation_item { +typedef struct session_pro_backend_pro_revocation_item { cbytes32 revocation_tag; /// Unix timestamp (seconds); a matching proof is revoked once the client clock reaches this int64_t effective_ts; -}; +} session_pro_backend_pro_revocation_item; -typedef struct session_pro_backend_get_pro_revocations_response - session_pro_backend_get_pro_revocations_response; -struct session_pro_backend_get_pro_revocations_response { +typedef struct session_pro_backend_get_pro_revocations_response { session_pro_backend_response_header header; int64_t ticket; int64_t retry_in; /// Recommended seconds to wait before polling the list again @@ -149,7 +142,7 @@ struct session_pro_backend_get_pro_revocations_response { /// Array of items, with items_count elements session_pro_backend_pro_revocation_item* items; size_t items_count; -}; +} session_pro_backend_get_pro_revocations_response; /// Unit of a parsed billing period (`plan_unit` below); mirrors C++ /// session::pro_backend::ProPlanUnit (same order). A closed set (pro-wire-protocol.md §1 / Delta @@ -163,8 +156,7 @@ typedef enum SESSION_PRO_BACKEND_PLAN_UNIT { SESSION_PRO_BACKEND_PLAN_UNIT_LIFETIME, } SESSION_PRO_BACKEND_PLAN_UNIT; -typedef struct session_pro_backend_pro_payment_item session_pro_backend_pro_payment_item; -struct session_pro_backend_pro_payment_item { +typedef struct session_pro_backend_pro_payment_item { /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); /// unknown values pass through as-is char status[64]; @@ -195,11 +187,9 @@ struct session_pro_backend_pro_payment_item { /// parts in per the backend-defined composite -- libsession does not interpret it) char payment_id[128]; size_t payment_id_count; -}; +} session_pro_backend_pro_payment_item; -typedef struct session_pro_backend_get_pro_details_response - session_pro_backend_get_pro_details_response; -struct session_pro_backend_get_pro_details_response { +typedef struct session_pro_backend_get_pro_details_response { session_pro_backend_response_header header; /// Array of payment items, with items_count elements session_pro_backend_pro_payment_item* items; @@ -213,14 +203,12 @@ struct session_pro_backend_get_pro_details_response { int64_t grace_period_duration; int64_t refund_requested_ts; uint32_t payments_total; -}; +} session_pro_backend_get_pro_details_response; -typedef struct session_pro_backend_set_payment_refund_requested_response - session_pro_backend_set_payment_refund_requested_response; -struct session_pro_backend_set_payment_refund_requested_response { +typedef struct session_pro_backend_set_payment_refund_requested_response { session_pro_backend_response_header header; bool updated; -}; +} session_pro_backend_set_payment_refund_requested_response; /// API: session_pro_backend/add_pro_payment_request_build /// From 7799a05d178bfdb3b66e7cf279d906d1a8ac07a4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 14:19:03 -0300 Subject: [PATCH 130/193] pro_backend: make C++ the owner of the payment-provider code constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provider-code slugs were defined in the C header (static const char[]) and referenced from C++ — backwards for a library whose C++ side is the primary implementation. Move them to C++ constexpr std::string_view (session::pro_backend::PAYMENT_PROVIDER_*, the single source) and have the C `SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*` symbols reference them as `extern const char* const`, matching how URL / PUBKEY / the endpoints already work. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 14 ++++++++------ include/session/pro_backend.hpp | 9 +++++++++ src/pro_backend.cpp | 15 +++++++++++---- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 18eedd01c..a9cd5435f 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -12,12 +12,14 @@ extern "C" { #endif /// Canonical payment-provider `code` strings — the value transmitted on the wire and folded into -/// the add-payment / set-refund signed hashes (spec §1). Reference these constants rather than -/// hardcoding the slug so a sent code cannot drift from what libsession hashes/parses. Unknown -/// codes (e.g. a future provider) are still valid on the wire and pass through as opaque strings. -static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY[] = "google_play"; -static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE[] = "app_store"; -static const char SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF[] = "rangeproof"; +/// the add-payment / set-refund signed messages (§3). Reference these rather than hardcoding the +/// slug so a sent code cannot drift from what libsession signs/parses. Unknown codes (e.g. a future +/// provider) are still valid on the wire and pass through as opaque strings. Each points at the +/// C++ `session::pro_backend::PAYMENT_PROVIDER_*` value (defined there — the primary; C +/// references). +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF; /// Endpoint path (relative to the backend base URL) that each request type is POSTed to. libsession /// owns the body<->route pairing, so reference these rather than hardcoding the path client-side. diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index c12495560..20af13e19 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -78,6 +78,15 @@ static_assert(PUBKEY_X25519.size() == 32); /// point at a different dev/test server if it chooses. constexpr std::string_view URL = "https://pro.session.codes"; +/// Canonical payment-provider code strings — the value transmitted on the wire and folded into the +/// add-payment / set-refund signed messages (§3). Reference these rather than hardcoding the slug +/// so a sent code cannot drift from what libsession signs/parses. The C +/// `SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*` symbols point at these. An unknown code is still +/// valid on the wire and passes through as an opaque string. +constexpr std::string_view PAYMENT_PROVIDER_GOOGLE_PLAY = "google_play"; +constexpr std::string_view PAYMENT_PROVIDER_APP_STORE = "app_store"; +constexpr std::string_view PAYMENT_PROVIDER_RANGEPROOF = "rangeproof"; + /// Domain used with ed25519::derive_subkey to derive the Session Pro signing keypair from the /// account's root Ed25519 seed. constexpr auto pro_subkey_domain = "SessionProRandom"_bytes; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index fc5266df2..7d7725eea 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -199,10 +199,18 @@ LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY = reinterpret_cast(PUBKEY.data()); LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY_X25519 = reinterpret_cast(PUBKEY_X25519.data()); + +// Payment-provider code strings: C symbols pointing at the single C++ definitions above. +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY = + PAYMENT_PROVIDER_GOOGLE_PLAY.data(); +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE = + PAYMENT_PROVIDER_APP_STORE.data(); +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF = + PAYMENT_PROVIDER_RANGEPROOF.data(); } std::optional provider_urls(std::string_view provider_code) { - if (provider_code == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY) + if (provider_code == PAYMENT_PROVIDER_GOOGLE_PLAY) return ProviderUrls{ "https://support.google.com/googleplay/workflow/9813244?", "https://getsession.org/android-refund", @@ -211,7 +219,7 @@ std::optional provider_urls(std::string_view provider_code) { "subscriptions?package=network.loki.messenger", "https://play.google.com/store/account/" "subscriptions?package=network.loki.messenger"}; - if (provider_code == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE) + if (provider_code == PAYMENT_PROVIDER_APP_STORE) return ProviderUrls{ "https://support.apple.com/118223", "https://support.apple.com/118223", @@ -236,8 +244,7 @@ session_pro_backend_get_provider_urls(const char* provider_code) { std::span visible_platforms() { static const std::array platforms = { - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE}; + PAYMENT_PROVIDER_GOOGLE_PLAY, PAYMENT_PROVIDER_APP_STORE}; return platforms; } From efba4d8b454cdb4c2def57b9993cc502cf0f7178 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 15:45:47 -0300 Subject: [PATCH 131/193] pro_backend: C responses wrap the owned C++ object; drop the arena Rework the C get-details/proof/revocations/refund response structs so each one owns the parsed C++ response object (via an opaque `void* internal_`) and its fields point into it, instead of deep-copying into fixed buffers backed by a hand-rolled bump arena. This mirrors how the request side already owns its C++ ProRequest. - Response string fields (`error_code`/`error`, item `status`/ `payment_provider`/`payment_id`, details `status`) become `const char*` views (NUL-terminated; NULL when absent); the per-item revocation `tag` becomes a `const unsigned char*` view. All the `char[N]`+`_count` pairs are gone -- including the 128-byte `payment_id` buffer that silently truncated long Google composites. - `header.internal_arena_buf_` -> `void* internal_`; each `*_response_free` is a one-liner over a `c_free_response` template constrained to `std::derived_from` (the two item-bearing holders derive from their C++ response type). File-local `to_c()` converters (taking a non-const ref, so a temporary can't bind) replace the manual per-field fills. - Delete `arena_t` / `arena_alloc` / `arena_alloc_to_string8` (types.h, types.cpp) -- now used nowhere. - Reorder pro_backend.h: request struct + `request_free` above the response group (no longer wedged between the header and the responses); each `*_free` decl sits immediately after its struct. C-API-only change (response side); the C++ API is unchanged. iOS/nodejs glue must read the new `const char*`/pointer fields instead of sized buffers. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 145 +++++++------- include/session/types.h | 14 -- src/pro_backend.cpp | 354 ++++++++++++++-------------------- src/types.cpp | 21 -- tests/test_pro_backend.cpp | 137 ++++++------- 5 files changed, 278 insertions(+), 393 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index a9cd5435f..320d3004a 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -76,6 +76,31 @@ session_pro_backend_get_provider_urls(const char* provider_code) NON_NULL_ARG(1) /// its strings are static storage owned by libsession — do not free. LIBSESSION_EXPORT const char* const* session_pro_backend_visible_platforms(size_t* count); +/// A built request to POST to the Session Pro backend. Mirrors the C++ +/// `session::pro_backend::ProRequest`: an `endpoint`, the `content_type` to send as the request's +/// Content-Type header, and the opaque `data` payload. `data` is libsession's data for the backend +/// -- relay it verbatim with the given `content_type` and do NOT parse, inspect, or assume a format +/// for it. libsession owns the wire encoding and may change it without client involvement. +typedef struct session_pro_backend_request { + char error[256]; + size_t error_count; + bool success; /// True if the request was built successfully, false if out-of-memory + /// Endpoint path (relative to the backend base URL) to POST to; static, null-terminated string. + const char* endpoint; + /// Value for the request's Content-Type header; static, null-terminated string. Relay verbatim. + const char* content_type; + /// The opaque request payload. Relay untouched; do not parse or modify. + string8 data; + /// Owned C++ object backing endpoint/content_type/data; do not touch (freed by request_free). + void* internal_; +} session_pro_backend_request; + +/// API: session_pro_backend/request_free +/// +/// Frees a `session_pro_backend_request` returned by a `*_request_build` function. +LIBSESSION_EXPORT +void session_pro_backend_request_free(session_pro_backend_request* request); + typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS, SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR, @@ -95,43 +120,33 @@ typedef enum SESSION_PRO_BACKEND_RESPONSE_STATUS { typedef struct session_pro_backend_response_header { /// Outcome category. Success iff `status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK`. SESSION_PRO_BACKEND_RESPONSE_STATUS status; - /// On non-OK, the machine-readable outcome slug (spec §5.1); {NULL, 0} on success. Opaque and + /// On non-OK, the machine-readable outcome slug (spec §5.1); NULL on success. Opaque and /// forward-compatible (unknown slugs pass through); "invalid_response" if libsession could not - /// parse the reply at all. - string8 error_code; + /// parse the reply at all. NUL-terminated; valid until the response is freed. + const char* error_code; /// On non-OK, an English diagnostic string (NOT user-facing text -- that comes from mapping - /// error_code to a localized string); {NULL, 0} on success. Always safe to log. - string8 error; - unsigned char* - internal_arena_buf_; /// Internal buffer for all the memory allocations, do not touch -} session_pro_backend_response_header; - -/// A built request to POST to the Session Pro backend. Mirrors the C++ -/// `session::pro_backend::ProRequest`: an `endpoint`, the `content_type` to send as the request's -/// Content-Type header, and the opaque `data` payload. `data` is libsession's data for the backend -/// -- relay it verbatim with the given `content_type` and do NOT parse, inspect, or assume a format -/// for it. libsession owns the wire encoding and may change it without client involvement. -typedef struct session_pro_backend_request { - char error[256]; - size_t error_count; - bool success; /// True if the request was built successfully, false if out-of-memory - /// Endpoint path (relative to the backend base URL) to POST to; static, null-terminated string. - const char* endpoint; - /// Value for the request's Content-Type header; static, null-terminated string. Relay verbatim. - const char* content_type; - /// The opaque request payload. Relay untouched; do not parse or modify. - string8 data; - /// Owned C++ object backing endpoint/content_type/data; do not touch (freed by request_free). + /// error_code to a localized string); NULL on success. Always safe to log. NUL-terminated; + /// valid until the response is freed. + const char* error; + /// Opaque implementation detail; do not touch. Released by the response's *_free function. void* internal_; -} session_pro_backend_request; +} session_pro_backend_response_header; typedef struct session_pro_backend_pro_proof_response { session_pro_backend_response_header header; session_protocol_pro_proof proof; } session_pro_backend_pro_proof_response; +/// API: session_pro_backend/pro_proof_response_free +/// +/// Frees the response. +LIBSESSION_EXPORT +void session_pro_backend_pro_proof_response_free(session_pro_backend_pro_proof_response* response); + typedef struct session_pro_backend_pro_revocation_item { - cbytes32 revocation_tag; + /// 32-byte revocation tag (compare for equality against a proof's tag); valid until the + /// response is freed. + const unsigned char* revocation_tag; /// Unix timestamp (seconds); a matching proof is revoked once the client clock reaches this int64_t effective_ts; } session_pro_backend_pro_revocation_item; @@ -146,6 +161,13 @@ typedef struct session_pro_backend_get_pro_revocations_response { size_t items_count; } session_pro_backend_get_pro_revocations_response; +/// API: session_pro_backend/get_pro_revocations_response_free +/// +/// Frees the response. +LIBSESSION_EXPORT +void session_pro_backend_get_pro_revocations_response_free( + session_pro_backend_get_pro_revocations_response* response); + /// Unit of a parsed billing period (`plan_unit` below); mirrors C++ /// session::pro_backend::ProPlanUnit (same order). A closed set (pro-wire-protocol.md §1 / Delta /// #14). @@ -160,18 +182,17 @@ typedef enum SESSION_PRO_BACKEND_PLAN_UNIT { typedef struct session_pro_backend_pro_payment_item { /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); - /// unknown values pass through as-is - char status[64]; - size_t status_count; + /// unknown values pass through as-is. NUL-terminated; points into the response's `internal_`. + const char* status; /// Parsed billing period (pro-wire-protocol.md §1 / Delta #14). `plan_count` is the period /// count /// (>= 1 for periodic units); for `plan_unit == ..._LIFETIME` it is 0 and not meaningful /// (switch on `plan_unit`, never render " " for lifetime). int plan_count; SESSION_PRO_BACKEND_PLAN_UNIT plan_unit; - /// Provider code (e.g. "google_play"); opaque -- an unknown value passes through as-is - char payment_provider[64]; - size_t payment_provider_count; + /// Provider code (e.g. "google_play"); opaque -- an unknown value passes through as-is. + /// NUL-terminated; points into the response's `internal_`. + const char* payment_provider; bool auto_renewing; /// Provider purchase time, fractional UNIX seconds. Millisecond-precise: the value passes @@ -186,9 +207,9 @@ typedef struct session_pro_backend_pro_payment_item { int64_t refund_requested_ts; /// Opaque payment identifier (the value passed at add-payment; multi-part providers fold their - /// parts in per the backend-defined composite -- libsession does not interpret it) - char payment_id[128]; - size_t payment_id_count; + /// parts in per the backend-defined composite -- libsession does not interpret it). + /// NUL-terminated; points into the response's `internal_`. + const char* payment_id; } session_pro_backend_pro_payment_item; typedef struct session_pro_backend_get_pro_details_response { @@ -196,9 +217,9 @@ typedef struct session_pro_backend_get_pro_details_response { /// Array of payment items, with items_count elements session_pro_backend_pro_payment_item* items; size_t items_count; - /// Opaque account Pro status code ("never"/"active"/"expired"); unknown values pass through - char status[64]; - size_t status_count; + /// Opaque account Pro status code ("never"/"active"/"expired"); unknown values pass through. + /// NUL-terminated; points into the response's `internal_`. + const char* status; SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; bool auto_renewing; int64_t expiry_ts; @@ -207,11 +228,25 @@ typedef struct session_pro_backend_get_pro_details_response { uint32_t payments_total; } session_pro_backend_get_pro_details_response; +/// API: session_pro_backend/get_pro_details_response_free +/// +/// Frees the response. +LIBSESSION_EXPORT +void session_pro_backend_get_pro_details_response_free( + session_pro_backend_get_pro_details_response* response); + typedef struct session_pro_backend_set_payment_refund_requested_response { session_pro_backend_response_header header; bool updated; } session_pro_backend_set_payment_refund_requested_response; +/// API: session_pro_backend/set_payment_refund_requested_response_free +/// +/// Frees the response. +LIBSESSION_EXPORT +void session_pro_backend_set_payment_refund_requested_response_free( + session_pro_backend_set_payment_refund_requested_response* response); + /// API: session_pro_backend/add_pro_payment_request_build /// /// Builds the add-payment request to POST, as a session_pro_backend_request (endpoint + @@ -353,38 +388,6 @@ session_pro_backend_request session_pro_backend_set_payment_refund_requested_req LIBSESSION_EXPORT session_pro_backend_set_payment_refund_requested_response session_pro_backend_set_payment_refund_requested_response_parse(const char* json, size_t json_len); -/// API: session_pro_backend/request_free -/// -/// Frees a `session_pro_backend_request` returned by a `*_request_build` function. -LIBSESSION_EXPORT -void session_pro_backend_request_free(session_pro_backend_request* request); - -/// API: session_pro_backend/pro_proof_response_free -/// -/// Frees the response -LIBSESSION_EXPORT -void session_pro_backend_pro_proof_response_free(session_pro_backend_pro_proof_response* response); - -/// API: session_pro_backend/get_pro_revocations_response_free -/// -/// Frees the respone -LIBSESSION_EXPORT -void session_pro_backend_get_pro_revocations_response_free( - session_pro_backend_get_pro_revocations_response* response); - -/// API: session_pro_backend/get_pro_details_response_free -/// -/// Frees the respone -LIBSESSION_EXPORT -void session_pro_backend_get_pro_details_response_free( - session_pro_backend_get_pro_details_response* response); - -/// API: session_pro_backend/session_pro_backend_set_payment_refund_requested_response_free -/// -/// Frees the respone -LIBSESSION_EXPORT -void session_pro_backend_set_payment_refund_requested_response_free( - session_pro_backend_set_payment_refund_requested_response* response); #ifdef __cplusplus } // extern "C" #endif diff --git a/include/session/types.h b/include/session/types.h index 573470325..3a287a8b6 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -43,20 +43,6 @@ struct cbytes64 { unsigned char data[64]; }; -/// Basic bump allocating arena -typedef struct arena_t arena_t; -struct arena_t { - unsigned char* data; - size_t size; - size_t max; -}; - -/// Allocate memory from the basic bump allocating arena. Returns a null pointer on failure. -void* arena_alloc(arena_t* arena, size_t bytes); - -/// Create a string and allocate a copy of the data at pointer and size -string8 arena_alloc_to_string8(arena_t* arena, void const* data, size_t size); - #ifdef __cplusplus } #endif diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 7d7725eea..446044ccd 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -754,14 +754,10 @@ SetPaymentRefundRequestedResponse parse_refund(std::string_view json) { using namespace session; using namespace session::pro_backend; -/// Define a string8 from a c-string literal. The string should not be modified as it'll live in the -/// data-section of the binary (or be interned, e.t.c) -#define STRING8_LIT(val) {(char*)val, sizeof(val) - 1} - -static string8 C_PARSE_ERROR_OUT_OF_MEMORY = STRING8_LIT("Ran out-of-memory creating C response"); -static string8 C_PARSE_ERROR_INVALID_ARGS = STRING8_LIT("One or more C arguments were NULL"); -// error_code slug libsession reports when it can't parse/build the C response at all. -static string8 C_INVALID_RESPONSE_CODE = STRING8_LIT("invalid_response"); +// error / error_code strings libsession synthesizes when it cannot parse or build the C response. +static constexpr const char* C_PARSE_ERROR_OUT_OF_MEMORY = "Ran out-of-memory creating C response"; +static constexpr const char* C_PARSE_ERROR_INVALID_ARGS = "One or more C arguments were NULL"; +static constexpr const char* C_INVALID_RESPONSE_CODE = "invalid_response"; // Map the C++ ResponseStatus to the C enum. static SESSION_PRO_BACKEND_RESPONSE_STATUS c_response_status(ResponseStatus status) { @@ -773,27 +769,63 @@ static SESSION_PRO_BACKEND_RESPONSE_STATUS c_response_status(ResponseStatus stat return SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; } -// Arena bytes needed for a header's error_code + error strings. -static size_t c_header_arena_bytes(const ResponseBase& cpp) { - size_t n = 0; - if (cpp.error_code) - n += cpp.error_code->size() + 1; - if (cpp.error) - n += cpp.error->size() + 1; - return n; +// Fill a C response header from a C++ ResponseBase. error_code/error point into `cpp` +// (NUL-terminated via c_str(), NULL when absent); `cpp` is owned by the response's +// header.internal_. +static void fill_c_header(session_pro_backend_response_header& header, const ResponseBase& cpp) { + header.status = c_response_status(cpp.status); + header.error_code = cpp.error_code ? cpp.error_code->c_str() : nullptr; + header.error = cpp.error ? cpp.error->c_str() : nullptr; } -// Fill a C response header's status + error_code/error from a C++ ResponseBase, copying the two -// optional strings into the arena (which must already have room; see c_header_arena_bytes). -static void fill_c_header( - session_pro_backend_response_header& header, const ResponseBase& cpp, arena_t& arena) { - header.status = c_response_status(cpp.status); - header.error_code = - cpp.error_code - ? arena_alloc_to_string8(&arena, cpp.error_code->data(), cpp.error_code->size()) - : string8{}; - header.error = cpp.error ? arena_alloc_to_string8(&arena, cpp.error->data(), cpp.error->size()) - : string8{}; +// Project a parsed C++ item to its C view. The string members are pointers into `src` (valid while +// the owning response lives) -- kept file-local rather than a public conversion operator, and +// taking a non-const lvalue ref so a temporary (whose aliased strings would immediately dangle) +// can't bind. +static session_pro_backend_pro_revocation_item to_c(ProRevocationItem& src) { + return { + .revocation_tag = reinterpret_cast(src.revocation_tag.data()), + .effective_ts = session::epoch_seconds(src.effective_at), + }; +} + +static session_pro_backend_pro_payment_item to_c(ProPaymentItem& src) { + return { + .status = src.status.c_str(), + .plan_count = src.plan.count, + .plan_unit = static_cast(src.plan.unit), + .payment_provider = src.payment_provider.c_str(), + .auto_renewing = src.auto_renewing, + .purchased_ts = epoch_seconds_double(src.purchased_at), + .redeemed_ts = session::epoch_seconds(src.redeemed_at), + .expiry_ts = session::epoch_seconds(src.expiry_at), + .grace_period_duration = src.grace_period_duration.count(), + .platform_refund_expiry_ts = session::epoch_seconds(src.platform_refund_expiry_at), + .revoked_ts = epoch_seconds_double(src.revoked_at), + .refund_requested_ts = session::epoch_seconds(src.refund_requested_at), + .payment_id = src.payment_id.c_str(), + }; +} + +// Owns the parsed C++ response object that a C response's fields point into, plus the C item-view +// array. `header.internal_` points here; the response's *_free deletes it. (Item-less responses own +// the bare C++ object directly.) +struct GetProRevocationsCResponse : GetProRevocationsResponse { + std::vector item_views; +}; +struct GetProDetailsCResponse : GetProDetailsResponse { + std::vector item_views; +}; + +// Free any C response: delete the owned object (of concrete type `Owned`, as stored by the matching +// *_parse) behind header.internal_, then zero the struct. `Owned` is a proof/refund response or a +// *CResponse holder -- all deriving from ResponseBase. +template Owned, typename Response> +static void c_free_response(Response* response) { + if (response) { + delete static_cast(response->header.internal_); + *response = {}; + } } // Wrap a freshly-built C++ ProRequest as an owning C session_pro_backend_request: heap-own the @@ -893,47 +925,30 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len) return result; } - // Note, parse is written to not throw so we can safely read without try-catch crap - // add-payment and generate-proof share the proof-response shape; the C response struct is - // generic, so either derived parser works here. - auto cpp = parse_add_payment({json, json_len}); - - // Calculate how much memory we need and create an arena - arena_t arena = {}; - { - arena.max += c_header_arena_bytes(cpp); - - if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); - - if (arena.max && !arena.data) { - result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; - result.header.error_code = C_INVALID_RESPONSE_CODE; - result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; - return result; - } - - // Store the pointer to the backing memory. Upon freeing, we release this one pointer - result.header.internal_arena_buf_ = arena.data; + try { + // add-payment and generate-proof share the proof-response shape; either parser works. + auto* owned = new AddProPaymentResponse(parse_add_payment({json, json_len})); + result.header.internal_ = owned; + fill_c_header(result.header, *owned); + + // Success and error responses fold into one path -- different fields populated. + const auto& p = owned->proof; + result.proof.version = p.version; + result.proof.expiry_ts = session::epoch_seconds(p.expiry_at); + std::memcpy( + result.proof.revocation_tag.data, p.revocation_tag.data(), p.revocation_tag.size()); + std::memcpy( + result.proof.rotating_pubkey.data, + p.rotating_pubkey.data(), + p.rotating_pubkey.size()); + std::memcpy(result.proof.sig.data, p.sig.data(), p.sig.size()); + } catch (const std::exception&) { + delete static_cast(result.header.internal_); + result = {}; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; } - - // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. - // Note that a response error and success case folds into the same code path. A success and - // error response returns the same struct just with different fields populated. - result.proof.version = cpp.proof.version; - result.proof.expiry_ts = session::epoch_seconds(cpp.proof.expiry_at); - std::memcpy( - result.proof.revocation_tag.data, - cpp.proof.revocation_tag.data(), - cpp.proof.revocation_tag.size()); - std::memcpy( - result.proof.rotating_pubkey.data, - cpp.proof.rotating_pubkey.data(), - cpp.proof.rotating_pubkey.size()); - std::memcpy(result.proof.sig.data, cpp.proof.sig.data(), cpp.proof.sig.size()); - - // Copy status + error code/message - fill_c_header(result.header, cpp, arena); return result; } @@ -947,51 +962,25 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t return result; } - // Note, parse is written to not throw so we can safely read without try-catch crap - GetProRevocationsResponse cpp = parse_revocations({json, json_len}); - - // Calculate how much memory we need and create an arena - arena_t arena = {}; - { - arena.max += cpp.items.size() * sizeof(*result.items); - static_assert( - sizeof(cpp.items[0]) >= sizeof(*result.items), - "Ensure we allocate enough memory. We might slightly over-allocate but that's not " - "a big deal"); - arena.max += c_header_arena_bytes(cpp); - - if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); - - if (arena.max && !arena.data) { - result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; - result.header.error_code = C_INVALID_RESPONSE_CODE; - result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; - return result; - } - - // Store the pointer to the backing memory. Upon freeing, we release this one pointer - result.header.internal_arena_buf_ = arena.data; - } - - // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. - result.ticket = cpp.ticket; - result.retry_in = cpp.retry_in.count(); - result.retain_for = cpp.retain_for.count(); - - // Copy status + error code/message - fill_c_header(result.header, cpp, arena); - - // Copy items - result.items_count = cpp.items.size(); - result.items = static_cast( - arena_alloc(&arena, result.items_count * sizeof(*result.items))); - - for (size_t index = 0; index < result.items_count; ++index) { - const ProRevocationItem& src = cpp.items[index]; - session_pro_backend_pro_revocation_item& dest = result.items[index]; - std::memcpy(dest.revocation_tag.data, src.revocation_tag.data(), src.revocation_tag.size()); - dest.effective_ts = session::epoch_seconds(src.effective_at); + try { + auto* owned = new GetProRevocationsCResponse{parse_revocations({json, json_len}), {}}; + result.header.internal_ = owned; + fill_c_header(result.header, *owned); + result.ticket = owned->ticket; + result.retry_in = owned->retry_in.count(); + result.retain_for = owned->retain_for.count(); + + owned->item_views.reserve(owned->items.size()); + for (auto& src : owned->items) + owned->item_views.push_back(to_c(src)); + result.items = owned->item_views.data(); + result.items_count = owned->item_views.size(); + } catch (const std::exception&) { + delete static_cast(result.header.internal_); + result = {}; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; } return result; } @@ -1006,65 +995,32 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json return result; } - // Note, parse is written to not throw so we can safely read without try-catch crap - auto cpp = parse_payment_details({json, json_len}); - - // Calculate how much memory we need and create an arena - arena_t arena = {}; - { - arena.max += cpp.items.size() * sizeof(*result.items); - arena.max += c_header_arena_bytes(cpp); - - if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); - - if (arena.max && !arena.data) { - result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; - result.header.error_code = C_INVALID_RESPONSE_CODE; - result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; - return result; - } - - // Store the pointer to the backing memory. Upon freeing, we release this one pointer - result.header.internal_arena_buf_ = arena.data; - } - - using session::epoch_seconds; - - // Copy to C struct, this is guaranteed not to fail because we pre-allocated memory upfront. - result.status_count = session::copy_c_str(result.status, cpp.user_status) - 1; - result.error_report = cpp.error_report; - result.items_count = cpp.items.size(); - result.items = (session_pro_backend_pro_payment_item*)arena_alloc( - &arena, result.items_count * sizeof(*result.items)); - result.auto_renewing = cpp.auto_renewing; - result.expiry_ts = epoch_seconds(cpp.expiry_at); - result.grace_period_duration = cpp.grace_period_duration.count(); - result.refund_requested_ts = epoch_seconds(cpp.refund_requested_at); - result.payments_total = cpp.payments_total; - - for (size_t index = 0; index < result.items_count; ++index) { - const ProPaymentItem& src = cpp.items[index]; - session_pro_backend_pro_payment_item& dest = result.items[index]; - dest.status_count = session::copy_c_str(dest.status, src.status) - 1; - dest.plan_count = src.plan.count; - dest.plan_unit = static_cast(src.plan.unit); - dest.payment_provider_count = - session::copy_c_str(dest.payment_provider, src.payment_provider) - 1; - dest.purchased_ts = epoch_seconds_double(src.purchased_at); - dest.redeemed_ts = epoch_seconds(src.redeemed_at); - dest.expiry_ts = epoch_seconds(src.expiry_at); - dest.grace_period_duration = src.grace_period_duration.count(); - dest.platform_refund_expiry_ts = epoch_seconds(src.platform_refund_expiry_at); - dest.revoked_ts = epoch_seconds_double(src.revoked_at); - dest.refund_requested_ts = epoch_seconds(src.refund_requested_at); - - dest.payment_id_count = session::copy_c_str(dest.payment_id, src.payment_id) - 1; + try { + using session::epoch_seconds; + auto* owned = new GetProDetailsCResponse{parse_payment_details({json, json_len}), {}}; + result.header.internal_ = owned; + fill_c_header(result.header, *owned); + + result.status = owned->user_status.c_str(); + result.error_report = owned->error_report; + result.auto_renewing = owned->auto_renewing; + result.expiry_ts = epoch_seconds(owned->expiry_at); + result.grace_period_duration = owned->grace_period_duration.count(); + result.refund_requested_ts = epoch_seconds(owned->refund_requested_at); + result.payments_total = owned->payments_total; + + owned->item_views.reserve(owned->items.size()); + for (auto& src : owned->items) + owned->item_views.push_back(to_c(src)); + result.items = owned->item_views.data(); + result.items_count = owned->item_views.size(); + } catch (const std::exception&) { + delete static_cast(result.header.internal_); + result = {}; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; } - - // Copy status + error code/message - fill_c_header(result.header, cpp, arena); - return result; } @@ -1101,34 +1057,18 @@ session_pro_backend_set_payment_refund_requested_response_parse(const char* json return result; } - // Note, parse is written to not throw so we can safely read without try-catch crap - auto cpp = parse_refund({json, json_len}); - - // Calculate how much memory we need and create an arena - arena_t arena = {}; - { - arena.max += c_header_arena_bytes(cpp); - - if (arena.max) - arena.data = static_cast(calloc(1, arena.max)); - - if (arena.max && !arena.data) { - result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; - result.header.error_code = C_INVALID_RESPONSE_CODE; - result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; - return result; - } - - // Store the pointer to the backing memory. Upon freeing, we release this one pointer - result.header.internal_arena_buf_ = arena.data; + try { + auto* owned = new SetPaymentRefundRequestedResponse(parse_refund({json, json_len})); + result.header.internal_ = owned; + fill_c_header(result.header, *owned); + result.updated = owned->updated; + } catch (const std::exception&) { + delete static_cast(result.header.internal_); + result = {}; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; } - - // Copy to C struct - result.updated = cpp.updated; - - // Copy status + error code/message - fill_c_header(result.header, cpp, arena); - return result; } @@ -1142,32 +1082,20 @@ LIBSESSION_C_API void session_pro_backend_request_free(session_pro_backend_reque LIBSESSION_C_API void session_pro_backend_pro_proof_response_free( session_pro_backend_pro_proof_response* response) { - if (response) { - free(response->header.internal_arena_buf_); - *response = {}; - } + c_free_response(response); } LIBSESSION_C_API void session_pro_backend_get_pro_revocations_response_free( session_pro_backend_get_pro_revocations_response* response) { - if (response) { - free(response->header.internal_arena_buf_); - *response = {}; - } + c_free_response(response); } LIBSESSION_C_API void session_pro_backend_get_pro_details_response_free( session_pro_backend_get_pro_details_response* response) { - if (response) { - free(response->header.internal_arena_buf_); - *response = {}; - } + c_free_response(response); } LIBSESSION_C_API void session_pro_backend_set_payment_refund_requested_response_free( session_pro_backend_set_payment_refund_requested_response* response) { - if (response) { - free(response->header.internal_arena_buf_); - *response = {}; - } + c_free_response(response); } diff --git a/src/types.cpp b/src/types.cpp index cc8ec5e4a..c70db4f04 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -38,24 +38,3 @@ string8 string8_copy_or_throw(const void* data, size_t size) { return result; } }; // namespace session - -void* arena_alloc(arena_t* arena, size_t bytes) { - void* result = nullptr; - size_t new_size = arena->size + bytes; - if (bytes && new_size <= arena->max) { - result = arena->data + arena->size; - arena->size = new_size; - } - return result; -} - -string8 arena_alloc_to_string8(arena_t* arena, void const* data, size_t size) { - string8 result = {}; - result.data = static_cast(arena_alloc(arena, size + 1)); - if (result.data) { - result.size = size; - std::memcpy(result.data, data, size); - result.data[result.size] = 0; - } - return result; -} diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 166d2bdde..f7594bbab 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -183,11 +183,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_pro_proof_response_free(&result); }}; - if (result.header.error.data) - INFO(result.header.error.data); + if (result.header.error) + INFO(result.header.error); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error.data == nullptr); + REQUIRE(result.header.error == nullptr); REQUIRE(result.proof.expiry_ts == unix_ts); REQUIRE(std::memcmp( result.proof.revocation_tag.data, @@ -226,8 +226,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { } // After freeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); - REQUIRE(result.header.error.data == nullptr); + REQUIRE(result.header.internal_ == nullptr); + REQUIRE(result.header.error == nullptr); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); // Invalid JSON @@ -237,19 +237,19 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_pro_proof_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } // After freeing session_pro_backend_pro_proof_response_free(&result); - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); // Null JSON result = session_pro_backend_pro_proof_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); // No need to free, as errors point to static memory } @@ -278,11 +278,11 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; - if (result.header.error.data) - INFO(result.header.error.data); + if (result.header.error) + INFO(result.header.error); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error.data == nullptr); + REQUIRE(result.header.error == nullptr); REQUIRE(result.ticket == 123); REQUIRE(result.retry_in == 3600); REQUIRE(result.retain_for == 2592000); @@ -290,13 +290,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items != nullptr); REQUIRE(result.items[0].effective_ts == unix_ts); REQUIRE(std::memcmp( - result.items[0].revocation_tag.data, + result.items[0].revocation_tag, fake_revocation_tag.data(), fake_revocation_tag.size()) == 0); } // After freeeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); REQUIRE(result.items == nullptr); REQUIRE(result.items_count == 0); @@ -308,18 +308,18 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } // After freeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); // Null JSON result = session_pro_backend_get_pro_revocations_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } SECTION("session_pro_backend_get_pro_details_response_parse") { @@ -360,13 +360,13 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; - if (result.header.error.data) - INFO(result.header.error.data); + if (result.header.error) + INFO(result.header.error); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error.data == nullptr); - REQUIRE(std::string_view(result.status, result.status_count) == "expired"); + REQUIRE(result.header.error == nullptr); + REQUIRE(std::string_view(result.status) == "expired"); REQUIRE(result.error_report == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR); REQUIRE(result.items_count == 1); @@ -376,13 +376,10 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.refund_requested_ts == unix_ts + 3602); REQUIRE(result.payments_total == 3); REQUIRE(result.items != nullptr); - REQUIRE(std::string_view(result.items[0].status, result.items[0].status_count) == - "redeemed"); + REQUIRE(std::string_view(result.items[0].status) == "redeemed"); REQUIRE(result.items[0].plan_count == 1); REQUIRE(result.items[0].plan_unit == SESSION_PRO_BACKEND_PLAN_UNIT_MONTH); - REQUIRE(std::string_view( - result.items[0].payment_provider, - result.items[0].payment_provider_count) == + REQUIRE(std::string_view(result.items[0].payment_provider) == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY); // Sub-second precision preserved through sys_ms (0.5s = 500ms) round-trips exactly REQUIRE(result.items[0].purchased_ts == unix_ts - 3600 + 0.5); @@ -392,11 +389,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); REQUIRE(result.items[0].revoked_ts == unix_ts + 3600 + 0.75); // 750ms preserved REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); - REQUIRE(result.items[0].payment_id_count == fake_payment_id.size()); - REQUIRE(std::memcmp( - result.items[0].payment_id, - fake_payment_id.data(), - fake_payment_id.size()) == 0); + REQUIRE(std::string_view(result.items[0].payment_id) == fake_payment_id); } // Tweak JSON for a different provider. payment_id is opaque so nothing @@ -413,22 +406,18 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{[&]() { session_pro_backend_get_pro_details_response_free(&result_rangeproof); }}; - if (result_rangeproof.header.error.data) - INFO(result_rangeproof.header.error.data); + if (result_rangeproof.header.error) + INFO(result_rangeproof.header.error); // Only check what we expect to be different - REQUIRE(std::string_view( - result_rangeproof.items[0].payment_provider, - result_rangeproof.items[0].payment_provider_count) == + REQUIRE(std::string_view(result_rangeproof.items[0].payment_provider) == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF); - REQUIRE(std::string_view( - result_rangeproof.items[0].payment_id, - result_rangeproof.items[0].payment_id_count) == + REQUIRE(std::string_view(result_rangeproof.items[0].payment_id) == "rangeproof-opaque-id"); } // After freeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); REQUIRE(result.items == nullptr); REQUIRE(result.items_count == 0); @@ -439,19 +428,19 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } // After freeing session_pro_backend_get_pro_details_response_free(&result); - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); // Null JSON result = session_pro_backend_get_pro_details_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } SECTION("Memory management edge cases") { @@ -463,15 +452,15 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_pro_proof_response proof_response = {}; session_pro_backend_pro_proof_response_free(&proof_response); - REQUIRE(proof_response.header.internal_arena_buf_ == nullptr); + REQUIRE(proof_response.header.internal_ == nullptr); session_pro_backend_get_pro_revocations_response rev_response = {}; session_pro_backend_get_pro_revocations_response_free(&rev_response); - REQUIRE(rev_response.header.internal_arena_buf_ == nullptr); + REQUIRE(rev_response.header.internal_ == nullptr); session_pro_backend_get_pro_details_response pay_response = {}; session_pro_backend_get_pro_details_response_free(&pay_response); - REQUIRE(pay_response.header.internal_arena_buf_ == nullptr); + REQUIRE(pay_response.header.internal_ == nullptr); } SECTION("session_pro_backend_set_payment_refund_requested_request_build") { @@ -526,16 +515,16 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { scope_exit result_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; - if (result.header.error.data) - INFO(result.header.error.data); + if (result.header.error) + INFO(result.header.error); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error.data == nullptr); + REQUIRE(result.header.error == nullptr); REQUIRE(result.updated); } // After freeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); // Invalid JSON json = "{invalid}"; @@ -546,18 +535,18 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } // After freeing - REQUIRE(result.header.internal_arena_buf_ == nullptr); + REQUIRE(result.header.internal_ == nullptr); // Null JSON result = session_pro_backend_set_payment_refund_requested_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(result.header.error_code.data != nullptr); - REQUIRE(result.header.error_code.data != nullptr); + REQUIRE(result.header.error_code != nullptr); + REQUIRE(result.header.error_code != nullptr); } } } @@ -737,8 +726,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { response_json.data(), response_json.size()); scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; - if (response.header.error.data) - INFO("ERROR: " << response.header.error.data); + if (response.header.error) + INFO("ERROR: " << response.header.error); // Verify response first_pro_proof = response.proof; @@ -783,8 +772,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error.data) - UNSCOPED_INFO("ERROR: " << response.header.error.data); + if (response.header.error) + UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); @@ -825,11 +814,11 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error.data) - UNSCOPED_INFO("ERROR: " << response.header.error.data); + if (response.header.error) + UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(std::string_view(response.status, response.status_count) == "active"); + REQUIRE(std::string_view(response.status) == "active"); REQUIRE(response.items_count > 0); } @@ -858,11 +847,11 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error.data) - UNSCOPED_INFO("ERROR: " << response.header.error.data); + if (response.header.error) + UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(std::string_view(response.status, response.status_count) == "active"); + REQUIRE(std::string_view(response.status) == "active"); REQUIRE(response.items_count == 0); } @@ -936,8 +925,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error.data) - UNSCOPED_INFO("ERROR: " << response.header.error.data); + if (response.header.error) + UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.ticket == 0); @@ -976,8 +965,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Verify response INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error.data) - UNSCOPED_INFO("ERROR: " << response.header.error.data); + if (response.header.error) + UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(response.updated); From 7eb85e34a2d28a3ad69501220a0db783b862bc4b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 15:45:47 -0300 Subject: [PATCH 132/193] session_protocol: idiomatic C header + C++-owned constants, misc cleanup Cleans up C-isms the original Pro code sprayed through the header/impl: - Collapse the split `typedef struct X X; struct X {...};` into the idiomatic `typedef struct X {...} X;`. - Move each `*_free` declaration to sit immediately after the struct it frees (it's effectively that struct's destructor). - Move the protocol limit/padding constants to C++ as the source of truth (`session::` inline constexpr) with the C `SESSION_PROTOCOL_*` symbols referencing them as `extern const int`; drop the misleading `PRO_` from the two non-Pro ones (STANDARD_CHARACTER_LIMIT, STANDARD_PINNED_CONVERSATION_LIMIT). - Replace the reinvented `array_uc32_from_ptr_result` with `std::optional` (maybe_uc32_from_ptr). - Replace the `sizeof((T*)0)->member` null-deref idiom with `sizeof(T::member)`. - Rename the meaningless `utf`/`utf_size` params to `text`/`text_size`. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/session_protocol.h | 160 ++++++++++++--------------- include/session/session_protocol.hpp | 13 +++ src/session_protocol.cpp | 41 ++++--- tests/test_session_protocol.cpp | 6 +- 4 files changed, 113 insertions(+), 107 deletions(-) diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 07adfa853..ba30a4c9b 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -12,28 +12,21 @@ extern "C" { #endif -enum { - /// Maximum number of UTF16 code points that a standard message can use. If the message exceeds - /// this then the message must activate the higher character limit feature provided by Session - /// Pro which allows messages up to 10k characters. - SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT = 2000, - - /// Maximum number of UTF16 code points that a Session Pro entitled user can send in a message. - /// This is not used in the codebase, but is provided for convenience to centralise protocol - /// definitions for users of the library to consume. - SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT = 10000, - - /// Amount of conversations that a user without Session Pro can pin - SESSION_PROTOCOL_PRO_STANDARD_PINNED_CONVERSATION_LIMIT = 5, - - /// Amount of bytes that a community or 1o1 message `Content` must be padded by before wrapping - /// in an envelope. - SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING = 160, -}; +/// Protocol limit/padding constants. Each points at the C++ `session::*` value (defined there -- +/// the primary; C references). Statically-initialized `const int`s (not compile-time constants). +/// - `SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT` -- max UTF-16 code points in a standard +/// (non-Pro) message; a longer one must activate the Session Pro higher-character-limit feature. +/// - `SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT` -- max UTF-16 code points a Pro user can send. +/// - `SESSION_PROTOCOL_STANDARD_PINNED_CONVERSATION_LIMIT` -- pins allowed without Session Pro. +/// - `SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING` -- byte multiple a community/1o1 `Content` is +/// padded up to before wrapping in an envelope. +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_STANDARD_PINNED_CONVERSATION_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING; /// Bundle of hard-coded strings that an implementing application may use for various scenarios. -typedef struct session_protocol_strings session_protocol_strings; -struct session_protocol_strings { +typedef struct session_protocol_strings { string8 build_variant_apk; string8 build_variant_fdroid; string8 build_variant_huawei; @@ -59,7 +52,7 @@ struct session_protocol_strings { string8 url_terms_of_service; string8 url_token; string8 url_translate; -}; +} session_protocol_strings; extern const session_protocol_strings SESSION_PROTOCOL_STRINGS; typedef enum SESSION_PROTOCOL_PRO_STATUS { // See session::ProStatus @@ -70,20 +63,18 @@ typedef enum SESSION_PROTOCOL_PRO_STATUS { // See session::ProStatus SESSION_PROTOCOL_PRO_STATUS_EXPIRED, } SESSION_PROTOCOL_PRO_STATUS; -typedef struct session_protocol_pro_signed_message session_protocol_pro_signed_message; -struct session_protocol_pro_signed_message { +typedef struct session_protocol_pro_signed_message { span_u8 sig; span_u8 msg; -}; +} session_protocol_pro_signed_message; -typedef struct session_protocol_pro_proof session_protocol_pro_proof; -struct session_protocol_pro_proof { +typedef struct session_protocol_pro_proof { uint8_t version; cbytes32 revocation_tag; cbytes32 rotating_pubkey; int64_t expiry_ts; cbytes64 sig; -}; +} session_protocol_pro_proof; // Session Pro feature flag bits. These are plain `uint64_t` bit masks (`1 << position`) that are // OR'd together into a profile/message feature bitset (itself just a `uint64_t`). They mirror the @@ -117,34 +108,30 @@ enum ENVELOPE_FLAGS_ { SESSION_PROTOCOL_ENVELOPE_FLAGS_TIMESTAMP = 1 << 4, }; -typedef struct session_protocol_envelope session_protocol_envelope; -struct session_protocol_envelope { +typedef struct session_protocol_envelope { SESSION_PROTOCOL_ENVELOPE_FLAGS flags; uint64_t timestamp_ms; cbytes33 source; uint32_t source_device; uint64_t server_timestamp; cbytes64 pro_sig; -}; +} session_protocol_envelope; -typedef struct session_protocol_decode_envelope_keys session_protocol_decode_envelope_keys; -struct session_protocol_decode_envelope_keys { +typedef struct session_protocol_decode_envelope_keys { span_u8 group_ed25519_pubkey; const span_u8* decrypt_keys; size_t decrypt_keys_len; -}; +} session_protocol_decode_envelope_keys; -typedef struct session_protocol_decoded_pro session_protocol_decoded_pro; -struct session_protocol_decoded_pro { +typedef struct session_protocol_decoded_pro { SESSION_PROTOCOL_PRO_STATUS status; session_protocol_pro_proof proof; // Bitsets of SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_* / SESSION_PROTOCOL_PRO_PROFILE_FEATURE_* uint64_t msg_bitset; uint64_t profile_bitset; -}; +} session_protocol_decoded_pro; -typedef struct session_protocol_decoded_envelope session_protocol_decoded_envelope; -struct session_protocol_decoded_envelope { +typedef struct session_protocol_decoded_envelope { // Indicates if the decryption was successful. If the decryption step failed and threw an // exception, this is false. bool success; @@ -154,20 +141,41 @@ struct session_protocol_decoded_envelope { cbytes32 sender_x25519_pubkey; session_protocol_decoded_pro pro; size_t error_len_incl_null_terminator; -}; +} session_protocol_decoded_envelope; -typedef struct session_protocol_encoded_for_destination session_protocol_encoded_for_destination; -struct session_protocol_encoded_for_destination { +/// API: session_protocol/session_protocol_decode_envelope_free +/// +/// Free the decoded result produced by `session_protocol_decode_envelope`. It is safe to pass a +/// `NULL` or any result returned by the decode function irrespective of if the function succeeded +/// or failed. +/// +/// Inputs: +/// - `envelope` -- decoded result to free. This object is zeroed out on free and should no +/// longer be used after it is freed. +LIBSESSION_EXPORT void session_protocol_decode_envelope_free( + session_protocol_decoded_envelope* envelope); + +typedef struct session_protocol_encoded_for_destination { // Indicates if the encryption was successful. If any step failed and threw an exception, this // is false. bool success; span_u8 ciphertext; size_t error_len_incl_null_terminator; -}; +} session_protocol_encoded_for_destination; -typedef struct session_protocol_decoded_community_message - session_protocol_decoded_community_message; -struct session_protocol_decoded_community_message { +/// API: session_protocol/session_protocol_encrypt_for_destination_free +/// +/// Free the encryption result for a destination produced by +/// `session_protocol_encrypt_for_destination`. It is safe to pass a `NULL` or any result returned +/// by the encrypt function irrespective of if the function succeeded or failed. +/// +/// Inputs: +/// - `encrypt` -- Encryption result to free. This object is zeroed out on free and should no longer +/// be used after it is freed. +LIBSESSION_EXPORT void session_protocol_encode_for_destination_free( + session_protocol_encoded_for_destination* encrypt); + +typedef struct session_protocol_decoded_community_message { bool success; bool has_envelope; session_protocol_envelope envelope; @@ -176,7 +184,19 @@ struct session_protocol_decoded_community_message { cbytes64 pro_sig; session_protocol_decoded_pro pro; size_t error_len_incl_null_terminator; -}; +} session_protocol_decoded_community_message; + +/// API: session_protocol/session_protocol_decode_for_community_free +/// +/// Free the decoded result produced by `session_protocol_decode_for_community`. It is safe to pass +/// a `NULL` or any result returned by the decode function irrespective of if the function +/// succeeded or failed. +/// +/// Inputs: +/// - `community_msg` -- decoded result to free. This object is zeroed out on free and should no +/// longer be used after it is freed. +LIBSESSION_EXPORT void session_protocol_decode_for_community_free( + session_protocol_decoded_community_message* community_msg); // The Pro feature bitsets are plain `uint64_t` masks of SESSION_PROTOCOL_PRO_*_FEATURES_* bits; // set/unset/test them with the standard bitwise operators (see the feature constants above). No @@ -283,9 +303,9 @@ typedef struct session_protocol_pro_features_for_msg { /// Determine the Pro features that are used in a given UTF8 message. /// /// Inputs: -/// - `utf` -- the UTF8 string to count the number of codepoints in to determine if it needs the +/// - `text` -- the UTF8 string to count the number of codepoints in to determine if it needs the /// higher character limit available in Session Pro -/// - `utf_size` -- the number of code units (aka. bytes) the string has +/// - `text_size` -- the number of code units (aka. bytes) the string has /// /// Outputs: /// - `success` -- True if the message was evaluated successfully for PRO features false otherwise. @@ -297,16 +317,16 @@ typedef struct session_protocol_pro_features_for_msg { /// - `codepoint_count` -- Counts the number of unicode codepoints that were in the message. LIBSESSION_EXPORT session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf8( - char const* utf, size_t utf_size) NON_NULL_ARG(1); + char const* text, size_t text_size) NON_NULL_ARG(1); /// API: session_protocol/session_protocol_get_pro_features_for_utf16 /// /// Determine the Pro features that are used in a given UTF16 message. /// /// Inputs: -/// - `utf` -- the UTF16 string to count the number of codepoints in to determine if it needs the +/// - `text` -- the UTF16 string to count the number of codepoints in to determine if it needs the /// higher character limit available in Session Pro -/// - `utf_size` -- the number of code units (aka. bytes) the string has +/// - `text_size` -- the number of code units (aka. bytes) the string has /// /// Outputs: /// - `success` -- True if the message was evaluated successfully for PRO features false otherwise. @@ -318,7 +338,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf8( /// - `codepoint_count` -- Counts the number of unicode codepoints that were in the message. LIBSESSION_EXPORT session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( - uint16_t const* utf, size_t utf_size) NON_NULL_ARG(1); + uint16_t const* text, size_t text_size) NON_NULL_ARG(1); /// API: session_protocol_encode_dm_v1 /// @@ -554,18 +574,6 @@ session_protocol_encoded_for_destination session_protocol_encode_for_group( OPTIONAL char* error, size_t error_len) NON_NULL_ARG(1, 3, 6, 7); -/// API: session_protocol/session_protocol_encrypt_for_destination_free -/// -/// Free the encryption result for a destination produced by -/// `session_protocol_encrypt_for_destination`. It is safe to pass a `NULL` or any result returned -/// by the encrypt function irrespective of if the function succeeded or failed. -/// -/// Inputs: -/// - `encrypt` -- Encryption result to free. This object is zeroed out on free and should no longer -/// be used after it is freed. -LIBSESSION_EXPORT void session_protocol_encode_for_destination_free( - session_protocol_encoded_for_destination* encrypt); - /// API: session_protocol/session_protocol_decode_envelope /// /// Given an envelope payload (i.e.: protobuf encoded stream of `WebsocketRequestMessage` which @@ -633,18 +641,6 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( OPTIONAL char* error, size_t error_len) NON_NULL_ARG(1, 2); -/// API: session_protocol/session_protocol_decode_envelope_free -/// -/// Free the decoded result produced by `session_protocol_decode_envelope`. It is safe to pass a -/// `NULL` or any result returned by the decode function irrespective of if the function succeeded -/// or failed. -/// -/// Inputs: -/// - `envelope` -- decoded result to free. This object is zeroed out on free and should no -/// longer be used after it is freed. -LIBSESSION_EXPORT void session_protocol_decode_envelope_free( - session_protocol_decoded_envelope* envelope); - /// API: session_protocol/session_protocol_decode_for_community /// /// Given an unencrypted content or envelope payload extract the plaintext to the content and any @@ -689,18 +685,6 @@ LIBSESSION_EXPORT session_protocol_decoded_community_message session_protocol_de OPTIONAL char* error, size_t error_len) NON_NULL_ARG(1); -/// API: session_protocol/session_protocol_decode_for_community_free -/// -/// Free the decoded result produced by `session_protocol_decode_for_community`. It is safe to pass -/// a `NULL` or any result returned by the decode function irrespective of if the function -/// succeeded or failed. -/// -/// Inputs: -/// - `community_msg` -- decoded result to free. This object is zeroed out on free and should no -/// longer be used after it is freed. -LIBSESSION_EXPORT void session_protocol_decode_for_community_free( - session_protocol_decoded_community_message* community_msg); - #ifdef __cplusplus } #endif diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index c77f0ed77..d91fd35bf 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -37,6 +37,19 @@ namespace session { +/// Maximum number of UTF-16 code points a standard (non-Pro) message can use; a longer message must +/// activate the Session Pro higher-character-limit feature. (The C `SESSION_PROTOCOL_*` symbols +/// point at these.) +inline constexpr int STANDARD_CHARACTER_LIMIT = 2000; +/// Maximum number of UTF-16 code points a Session Pro entitled user can send. Not used internally; +/// provided to centralise the protocol definition for library consumers. +inline constexpr int PRO_HIGHER_CHARACTER_LIMIT = 10000; +/// Number of conversations a user without Session Pro can pin. +inline constexpr int STANDARD_PINNED_CONVERSATION_LIMIT = 5; +/// Byte multiple a community or 1o1 message `Content` is padded up to before wrapping in an +/// envelope. +inline constexpr int COMMUNITY_OR_1O1_MSG_PADDING = 160; + enum ProProofVersion { ProProofVersion_v0 }; // Session Pro 16-byte signing domain prefixes; each prefixes the Ed25519-signed message for its diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index 07308f285..f9f95b27a 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -65,7 +65,7 @@ std::vector proof_signed_message( // Copies an optional 32-byte value out of a possibly-null C pointer. A null pointer yields a // zero-filled value (the "not provided" case); a non-null pointer with the wrong length yields // nullopt to signal an error. -static std::optional optional_uc32_from_ptr(const void* ptr, size_t len) { +static std::optional maybe_uc32_from_ptr(const void* ptr, size_t len) { session::b32 out = {}; if (ptr) { if (len != out.max_size()) @@ -122,9 +122,9 @@ static session::ProProof proof_from_c(const session_protocol_pro_proof& c) { namespace session { -static_assert(sizeof(std::declval().revocation_tag) == 32); -static_assert(sizeof(std::declval().rotating_pubkey) == 32); -static_assert(sizeof(std::declval().sig) == 64); +static_assert(sizeof(ProProof::revocation_tag) == 32); +static_assert(sizeof(ProProof::rotating_pubkey) == 32); +static_assert(sizeof(ProProof::sig) == 64); bool ProProof::verify_signature(std::span verify_pubkey) const { return ed25519::verify(sig, verify_pubkey, signed_message()); @@ -176,8 +176,8 @@ session::ProFeaturesForMsg pro_features_check( result.status = session::ProFeaturesForMsgStatus::Success; result.codepoint_count = codepoints; - if (result.codepoint_count > SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT) { - if (result.codepoint_count <= SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT) { + if (result.codepoint_count > session::STANDARD_CHARACTER_LIMIT) { + if (result.codepoint_count <= session::PRO_HIGHER_CHARACTER_LIMIT) { result.flags |= session::ProMessageFlags::CharLimit10k; } else { result.error = "Message exceeds the maximum character limit allowed"; @@ -217,10 +217,9 @@ std::vector pad_message(std::span payload) { // Calculate amount of padding required size_t padded_content_size = payload.size() + 1 /*padding byte*/; uint8_t const bytes_for_padding = - SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING - - (padded_content_size % SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING); + COMMUNITY_OR_1O1_MSG_PADDING - (padded_content_size % COMMUNITY_OR_1O1_MSG_PADDING); padded_content_size += bytes_for_padding; - assert(padded_content_size % SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING == 0); + assert(padded_content_size % COMMUNITY_OR_1O1_MSG_PADDING == 0); // Do the padding std::vector result; @@ -783,9 +782,21 @@ DecodedCommunityMessage decode_for_community( using namespace session; -static_assert((sizeof((session_protocol_pro_proof*)0)->revocation_tag) == 32); -static_assert(sizeof(std::declval().rotating_pubkey) == 32); -static_assert(sizeof(std::declval().sig) == 64); +// Protocol limit/padding constants: C symbols pointing at the single C++ definitions above. +extern "C" { +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT = + STANDARD_CHARACTER_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_PRO_HIGHER_CHARACTER_LIMIT = + PRO_HIGHER_CHARACTER_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_STANDARD_PINNED_CONVERSATION_LIMIT = + STANDARD_PINNED_CONVERSATION_LIMIT; +LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING = + COMMUNITY_OR_1O1_MSG_PADDING; +} + +static_assert(sizeof(session_protocol_pro_proof::revocation_tag) == 32); +static_assert(sizeof(session_protocol_pro_proof::rotating_pubkey) == 32); +static_assert(sizeof(session_protocol_pro_proof::sig) == 64); // Session Pro feature flag bit constants exposed to the C API. The C++ enum classes // (session::ProProfileFlags / session::ProMessageFlags) are the source of truth; these mirror their @@ -1022,8 +1033,7 @@ session_protocol_decoded_envelope session_protocol_decode_envelope( session_protocol_decoded_envelope result = {}; // Setup the pro backend pubkey - auto pro_backend_pubkey_cpp = - optional_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); + auto pro_backend_pubkey_cpp = maybe_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); if (!pro_backend_pubkey_cpp) { result.error_len_incl_null_terminator = format_c_str( error, @@ -1140,8 +1150,7 @@ session_protocol_decoded_community_message session_protocol_decode_for_community static_cast(content_or_envelope_payload), content_or_envelope_payload_len}; auto unix_ts = session::as_sys_seconds(ts); - auto pro_backend_pubkey_cpp = - optional_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); + auto pro_backend_pubkey_cpp = maybe_uc32_from_ptr(pro_backend_pubkey, pro_backend_pubkey_len); if (!pro_backend_pubkey_cpp) { result.error_len_incl_null_terminator = format_c_str( error, diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 75d309d52..fe6698ddc 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -89,7 +89,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Ensure get pro fetaures detects large message") { // Try a message below the size threshold { - auto msg = std::string(SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT, 'a'); + auto msg = std::string(SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT, 'a'); session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS); @@ -109,7 +109,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Try a message exceeding the standard size threshold { - auto msg = std::string(SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT + 1, 'a'); + auto msg = std::string(SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT + 1, 'a'); session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS); @@ -403,7 +403,7 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { SECTION("Encrypt/decrypt for contact in default namespace with Pro + features") { std::string large_message; - large_message.resize(SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT + 1); + large_message.resize(SESSION_PROTOCOL_STANDARD_CHARACTER_LIMIT + 1); session_protocol_pro_features_for_msg pro_msg = session_protocol_pro_features_for_utf8(large_message.data(), large_message.size()); From e70cd0d9bf89cedc61a2d26a5c4b15486deee60e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 16:28:07 -0300 Subject: [PATCH 133/193] pro_backend: redesign ProviderURLs (optional fields, static per-provider constants) - Rename ProviderUrls -> ProviderURLs (initialism casing). - All five URL fields are now std::optional: a provider may populate none of them (e.g. a one-shot crypto provider), so clients test the optional instead of an empty-string sentinel. The C struct's matching fields may now individually be NULL. - Replace the per-call constructed value + std::optional return with fixed per-provider `constexpr ProviderURLs` constants and a `const ProviderURLs*` return (nullptr = no URLs at all). URLs are one-line `""sv` literals (guaranteed null-terminated) so clang-format won't reflow them mid-string. - Use designated initialisers for the provider constants. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.hpp | 30 ++++++++-------- src/pro_backend.cpp | 61 ++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 20af13e19..cee6c6c2b 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -128,23 +128,25 @@ struct MasterRotatingSignatures { b64 rotating_sig; }; -/// Per-provider support/management URLs. These are identical for every user (not translation data), -/// so libsession owns them as the single source of truth rather than each client duplicating them; -/// the human-readable provider/store *names* are translation data and remain the client's job. -/// Returned by `provider_urls()`. The views point at static, null-terminated storage. -struct ProviderUrls { - std::string_view refund_platform_url; ///< Native store refund flow - std::string_view refund_support_url; ///< Session support page for requesting a refund - std::string_view refund_status_url; ///< Where a user checks refund status - std::string_view update_subscription_url; ///< Manage/update the subscription - std::string_view cancel_subscription_url; ///< Cancel the subscription +/// Per-provider support/management URLs (from provider_urls()). These are identical for every user +/// (not translation data), so libsession owns them as the single source of truth rather than each +/// client duplicating them; the human-readable provider/store *names* are translation data and +/// remain the client's job. Each field is std::nullopt when that provider has no such URL -- clients +/// test the optional rather than an empty-string sentinel. Present views point at static, +/// null-terminated string literals. +struct ProviderURLs { + std::optional refund_platform_url; ///< Native store refund flow + std::optional refund_support_url; ///< Session page for requesting a refund + std::optional refund_status_url; ///< Where a user checks refund status + std::optional update_subscription_url; ///< Manage/update the subscription + std::optional cancel_subscription_url; ///< Cancel the subscription }; /// Look up the support/management URLs for a provider code (see -/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). Returns std::nullopt for a provider with no -/// applicable URLs (an unknown code, or e.g. rangeproof) — so callers test the optional rather than -/// hunting for empty members. -std::optional provider_urls(std::string_view provider_code); +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_*). Returns a pointer to a static per-provider +/// constant, or nullptr for a provider with no applicable URLs at all (an unknown code, or e.g. +/// rangeproof); within a returned set each field is present or std::nullopt per that provider. +const ProviderURLs* provider_urls(std::string_view provider_code); /// The user-visible purchasable platforms, as provider-code slugs (a subset of the /// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* values — currently "google_play" and "app_store"). diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 446044ccd..24a4920c8 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -209,37 +209,52 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_ PAYMENT_PROVIDER_RANGEPROOF.data(); } -std::optional provider_urls(std::string_view provider_code) { +using namespace std::literals; + +// Fixed per-provider URL sets; provider_urls() just maps a provider code to one of these. The +// values are `""sv` literals so clang-format keeps each URL on one line (it won't split a string +// literal). +constexpr ProviderURLs google_play_urls{ + .refund_platform_url = "https://support.google.com/googleplay/workflow/9813244?"sv, + .refund_support_url = "https://getsession.org/android-refund"sv, + .refund_status_url = "https://getsession.org/android-refund"sv, + .update_subscription_url = + "https://play.google.com/store/account/subscriptions?package=network.loki.messenger"sv, + .cancel_subscription_url = + "https://play.google.com/store/account/subscriptions?package=network.loki.messenger"sv, +}; +constexpr ProviderURLs app_store_urls{ + .refund_platform_url = "https://support.apple.com/118223"sv, + .refund_support_url = "https://support.apple.com/118223"sv, + .refund_status_url = "https://support.apple.com/118224"sv, + .update_subscription_url = "https://apps.apple.com/account/subscriptions"sv, + .cancel_subscription_url = + "https://account.apple.com/account/manage/section/subscriptions"sv, +}; + +const ProviderURLs* provider_urls(std::string_view provider_code) { if (provider_code == PAYMENT_PROVIDER_GOOGLE_PLAY) - return ProviderUrls{ - "https://support.google.com/googleplay/workflow/9813244?", - "https://getsession.org/android-refund", - "https://getsession.org/android-refund", - "https://play.google.com/store/account/" - "subscriptions?package=network.loki.messenger", - "https://play.google.com/store/account/" - "subscriptions?package=network.loki.messenger"}; + return &google_play_urls; if (provider_code == PAYMENT_PROVIDER_APP_STORE) - return ProviderUrls{ - "https://support.apple.com/118223", - "https://support.apple.com/118223", - "https://support.apple.com/118224", - "https://apps.apple.com/account/subscriptions", - "https://account.apple.com/account/manage/section/subscriptions"}; + return &app_store_urls; // rangeproof and unknown providers have no applicable URLs - return std::nullopt; + return nullptr; } LIBSESSION_C_API session_pro_backend_provider_urls session_pro_backend_get_provider_urls(const char* provider_code) { - // No URLs → all-NULL fields; otherwise each view is a static, null-terminated literal. + // Each present field is a static, null-terminated literal; an absent one (or no URLs at all) is + // NULL. + auto c = [](const std::optional& u) -> const char* { + return u ? u->data() : nullptr; + }; if (auto u = provider_urls(provider_code)) - return {u->refund_platform_url.data(), - u->refund_support_url.data(), - u->refund_status_url.data(), - u->update_subscription_url.data(), - u->cancel_subscription_url.data()}; - return {nullptr, nullptr, nullptr, nullptr, nullptr}; + return {.refund_platform_url = c(u->refund_platform_url), + .refund_support_url = c(u->refund_support_url), + .refund_status_url = c(u->refund_status_url), + .update_subscription_url = c(u->update_subscription_url), + .cancel_subscription_url = c(u->cancel_subscription_url)}; + return {}; } std::span visible_platforms() { From 2a2112386641d684e5b78f7cdaabfee2b8a0e672 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 16:36:33 -0300 Subject: [PATCH 134/193] pro_backend: add `found` to session_pro_backend_provider_urls (C) Now that every ProviderURLs field is optional, the by-value C return could no longer distinguish an unknown provider code from a recognised provider that simply has no URLs -- both produced an all-NULL struct. Add a `bool found` member (true iff the C++ provider_urls() returned non-null) so C clients get the same distinction the C++ `const ProviderURLs*` gives, without a returned pointer they'd have to reason about freeing. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 14 ++++++++++---- src/pro_backend.cpp | 8 +++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 320d3004a..e888d9eb0 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -50,9 +50,14 @@ LIBSESSION_EXPORT extern const unsigned char* const SESSION_PRO_BACKEND_PUBKEY_X /// Per-provider support/management URLs, keyed by provider code. These are identical for every user /// (not translation data), so libsession owns them as the single source of truth rather than each /// client duplicating them; the human-readable provider/store names are translation data and remain -/// the client's job. An unknown provider code (or one with no applicable URLs, e.g. rangeproof) -/// yields all-NULL fields. Otherwise each is a static, null-terminated C string. +/// the client's job. Each URL field is NULL when that provider has no such URL, and is otherwise a +/// static, null-terminated C string. Use `found` (not the URL fields) to tell an unknown provider +/// code from a known provider that simply has no URLs. typedef struct session_pro_backend_provider_urls { + /// True if `provider_code` is a recognised provider; false if it is unknown (or e.g. + /// rangeproof), in which case every URL field below is NULL. A recognised provider may still + /// leave any or all URL fields NULL. + bool found; const char* refund_platform_url; /// Native store refund flow const char* refund_support_url; /// Session support page for requesting a refund const char* refund_status_url; /// Where a user checks refund status @@ -63,8 +68,9 @@ typedef struct session_pro_backend_provider_urls { /// API: session_pro_backend/get_provider_urls /// /// Returns the support/management URLs for `provider_code` (a -/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* value). A provider with no applicable URLs (unknown -/// code, or e.g. rangeproof) returns a struct with all fields NULL. +/// SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* value). On an unrecognised code the result has +/// `found == false` and all URL fields NULL; on a recognised code `found == true` and each URL +/// field is either a static, null-terminated C string or NULL if that provider lacks that URL. LIBSESSION_EXPORT session_pro_backend_provider_urls session_pro_backend_get_provider_urls(const char* provider_code) NON_NULL_ARG(1); diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 24a4920c8..11011d18c 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -243,13 +243,15 @@ const ProviderURLs* provider_urls(std::string_view provider_code) { LIBSESSION_C_API session_pro_backend_provider_urls session_pro_backend_get_provider_urls(const char* provider_code) { - // Each present field is a static, null-terminated literal; an absent one (or no URLs at all) is - // NULL. + // Each present field is a static, null-terminated literal; an absent one is NULL. `found` + // distinguishes an unknown provider ({} -> found == false) from a recognised provider that + // simply has some/all URLs absent. auto c = [](const std::optional& u) -> const char* { return u ? u->data() : nullptr; }; if (auto u = provider_urls(provider_code)) - return {.refund_platform_url = c(u->refund_platform_url), + return {.found = true, + .refund_platform_url = c(u->refund_platform_url), .refund_support_url = c(u->refund_support_url), .refund_status_url = c(u->refund_status_url), .update_subscription_url = c(u->update_subscription_url), From c3d7d5715c03314e3f3b695b7770dc72cb871490 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 18:12:33 -0300 Subject: [PATCH 135/193] Eliminate string8; it duplicated span_u8 `string8` (`{char* data; size_t size}`) was a structural duplicate of the existing `span_u8` (`{unsigned char* data; size_t size}`) -- same ptr+len byte span, right down to a duplicate pair of alloc/copy_or_throw helpers. Once its uses are mapped, none actually need a string-with-length: - SESSION_PROTOCOL_STRINGS (24 fields) and session_protocol_pro_features_for_msg `error` are static, null-terminated strings -> `const char*`. The `error` string_view only ever views static literals ("..." + simdutf::error_to_string, which returns string_view over literals), so there is nothing to free and no dangling. SESSION_PROTOCOL_STRINGS drops the `string8_literal` macro for plain string literals. - session_pro_backend_request `data` is an opaque (possibly binary) payload that genuinely needs ptr+len -> `span_u8`, the type already used for sig/msg. Then delete the dead string8_alloc_or_throw/string8_copy_or_throw helpers (no callers; duplicates of span_u8_*), and the `string8` struct + `string8_literal` macro. Test helper string8_equals -> span_u8_equals, now a std::string_view compare instead of a hand-rolled memcmp. C-API change (clients): session_protocol_strings fields and session_protocol_pro_features_for_msg.error are now `const char*`; session_pro_backend_request.data is now `span_u8`. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 4 +-- include/session/session_protocol.h | 55 ++++++++++++++++-------------- include/session/types.h | 8 ----- include/session/types.hpp | 6 ---- src/pro_backend.cpp | 2 +- src/session_protocol.cpp | 54 ++++++++++++++--------------- src/types.cpp | 18 ---------- tests/test_pro_backend.cpp | 14 ++++---- tests/test_session_protocol.cpp | 3 +- 9 files changed, 68 insertions(+), 96 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index e888d9eb0..64b53b6df 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -95,8 +95,8 @@ typedef struct session_pro_backend_request { const char* endpoint; /// Value for the request's Content-Type header; static, null-terminated string. Relay verbatim. const char* content_type; - /// The opaque request payload. Relay untouched; do not parse or modify. - string8 data; + /// The opaque request payload (raw bytes). Relay untouched; do not parse or modify. + span_u8 data; /// Owned C++ object backing endpoint/content_type/data; do not touch (freed by request_free). void* internal_; } session_pro_backend_request; diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index ba30a4c9b..64b792c64 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -26,32 +26,33 @@ LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_STANDARD_PINNED_CONVERSATION LIBSESSION_EXPORT extern const int SESSION_PROTOCOL_COMMUNITY_OR_1O1_MSG_PADDING; /// Bundle of hard-coded strings that an implementing application may use for various scenarios. +/// Each is a static, null-terminated C string. typedef struct session_protocol_strings { - string8 build_variant_apk; - string8 build_variant_fdroid; - string8 build_variant_huawei; - string8 build_variant_ipa; - string8 url_donations; - string8 url_donations_app; - string8 url_download; - string8 url_faq; - string8 url_feedback; - string8 url_network; - string8 url_privacy_policy; - string8 url_pro_access_not_found; - string8 url_pro_faq; - string8 url_pro_page; - string8 url_pro_privacy_policy; - string8 url_pro_roadmap; - string8 url_pro_support; - string8 url_pro_terms_of_service; - string8 url_pro_upgrade; - string8 url_staking; - string8 url_support; - string8 url_survey; - string8 url_terms_of_service; - string8 url_token; - string8 url_translate; + const char* build_variant_apk; + const char* build_variant_fdroid; + const char* build_variant_huawei; + const char* build_variant_ipa; + const char* url_donations; + const char* url_donations_app; + const char* url_download; + const char* url_faq; + const char* url_feedback; + const char* url_network; + const char* url_privacy_policy; + const char* url_pro_access_not_found; + const char* url_pro_faq; + const char* url_pro_page; + const char* url_pro_privacy_policy; + const char* url_pro_roadmap; + const char* url_pro_support; + const char* url_pro_terms_of_service; + const char* url_pro_upgrade; + const char* url_staking; + const char* url_support; + const char* url_survey; + const char* url_terms_of_service; + const char* url_token; + const char* url_translate; } session_protocol_strings; extern const session_protocol_strings SESSION_PROTOCOL_STRINGS; @@ -293,7 +294,9 @@ LIBSESSION_EXPORT SESSION_PROTOCOL_PRO_STATUS session_protocol_pro_proof_status( /// API: session_protocol/session_protocol_get_pro_features_for_msg typedef struct session_protocol_pro_features_for_msg { SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS status; - string8 error; + /// On error (status != OK), a static, null-terminated English diagnostic string; NULL when + /// there is no error. + const char* error; uint64_t bitset; // Mask of SESSION_PROTOCOL_PRO_MESSAGE_FEATURE_* bits size_t codepoint_count; } session_protocol_pro_features_for_msg; diff --git a/include/session/types.h b/include/session/types.h index 3a287a8b6..cd87d28e6 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -20,14 +20,6 @@ struct span_u8 { size_t size; }; -typedef struct string8 string8; -struct string8 { - char* data; - size_t size; -}; - -#define string8_literal(literal) {(char*)literal, sizeof(literal) - 1} - typedef struct cbytes32 cbytes32; struct cbytes32 { unsigned char data[32]; diff --git a/include/session/types.hpp b/include/session/types.hpp index 57e537d3b..1ea5e418b 100644 --- a/include/session/types.hpp +++ b/include/session/types.hpp @@ -33,10 +33,4 @@ span_u8 span_u8_alloc_or_throw(size_t size); /// this function throws a runtime exception. The `data` pointer is span must be freed once the span /// is no longer needed. span_u8 span_u8_copy_or_throw(const void* data, size_t size); - -/// Allocate the string with the specific size. Throws on allocation failure. -string8 string8_alloc_or_throw(size_t size); - -/// Create a string by copying the given pointer and size. Throws on allocation failure -string8 string8_copy_or_throw(const void* data, size_t size); } // namespace session diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 11011d18c..d75e8ff37 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -854,7 +854,7 @@ static session_pro_backend_request c_own_request(ProRequest&& req) { result.internal_ = owned; result.endpoint = owned->endpoint.data(); result.content_type = owned->content_type.data(); - result.data = string8{owned->data.data(), owned->data.size()}; + result.data = span_u8{reinterpret_cast(owned->data.data()), owned->data.size()}; result.success = true; return result; } diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index f9f95b27a..4d1b15991 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -21,31 +21,31 @@ // clang-format off const session_protocol_strings SESSION_PROTOCOL_STRINGS = { - .build_variant_apk = string8_literal("APK"), - .build_variant_fdroid = string8_literal("F-Droid Store"), - .build_variant_huawei = string8_literal("Huawei App Gallery"), - .build_variant_ipa = string8_literal("IPA"), - .url_donations = string8_literal("https://getsession.org/donate"), - .url_donations_app = string8_literal("https://getsession.org/donate#app"), - .url_download = string8_literal("https://getsession.org/download"), - .url_faq = string8_literal("https://getsession.org/faq"), - .url_feedback = string8_literal("https://getsession.org/feedback"), - .url_network = string8_literal("https://docs.getsession.org/session-network"), - .url_privacy_policy = string8_literal("https://getsession.org/privacy-policy"), - .url_pro_access_not_found = string8_literal("https://sessionapp.zendesk.com/hc/sections/4416517450649-Support"), - .url_pro_faq = string8_literal("https://getsession.org/pro#faq"), - .url_pro_page = string8_literal("https://getsession.org/pro"), - .url_pro_privacy_policy = string8_literal("https://getsession.org/pro-privacy"), - .url_pro_roadmap = string8_literal("https://getsession.org/pro#roadmap"), - .url_pro_support = string8_literal("https://getsession.org/pro-support"), - .url_pro_terms_of_service = string8_literal("https://getsession.org/pro-terms"), - .url_pro_upgrade = string8_literal("https://getsession.org/pro#upgrade"), - .url_staking = string8_literal("https://docs.getsession.org/session-network/staking"), - .url_support = string8_literal("https://getsession.org/support"), - .url_survey = string8_literal("https://getsession.org/survey"), - .url_terms_of_service = string8_literal("https://getsession.org/terms-of-service"), - .url_token = string8_literal("https://token.getsession.org"), - .url_translate = string8_literal("https://getsession.org/translate"), + .build_variant_apk = "APK", + .build_variant_fdroid = "F-Droid Store", + .build_variant_huawei = "Huawei App Gallery", + .build_variant_ipa = "IPA", + .url_donations = "https://getsession.org/donate", + .url_donations_app = "https://getsession.org/donate#app", + .url_download = "https://getsession.org/download", + .url_faq = "https://getsession.org/faq", + .url_feedback = "https://getsession.org/feedback", + .url_network = "https://docs.getsession.org/session-network", + .url_privacy_policy = "https://getsession.org/privacy-policy", + .url_pro_access_not_found = "https://sessionapp.zendesk.com/hc/sections/4416517450649-Support", + .url_pro_faq = "https://getsession.org/pro#faq", + .url_pro_page = "https://getsession.org/pro", + .url_pro_privacy_policy = "https://getsession.org/pro-privacy", + .url_pro_roadmap = "https://getsession.org/pro#roadmap", + .url_pro_support = "https://getsession.org/pro-support", + .url_pro_terms_of_service = "https://getsession.org/pro-terms", + .url_pro_upgrade = "https://getsession.org/pro#upgrade", + .url_staking = "https://docs.getsession.org/session-network/staking", + .url_support = "https://getsession.org/support", + .url_survey = "https://getsession.org/survey", + .url_terms_of_service = "https://getsession.org/terms-of-service", + .url_token = "https://token.getsession.org", + .url_translate = "https://getsession.org/translate", }; // clang-format on @@ -881,7 +881,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf8( auto result_cpp = pro_features_for_utf8({msg, msg_size}); return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), - .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, + .error = result_cpp.error.data(), .bitset = static_cast(result_cpp.flags), .codepoint_count = result_cpp.codepoint_count, }; @@ -894,7 +894,7 @@ session_protocol_pro_features_for_msg session_protocol_pro_features_for_utf16( {std::launder(reinterpret_cast(msg)), msg_size}); return session_protocol_pro_features_for_msg{ .status = static_cast(result_cpp.status), - .error = {const_cast(result_cpp.error.data()), result_cpp.error.size()}, + .error = result_cpp.error.data(), .bitset = static_cast(result_cpp.flags), .codepoint_count = result_cpp.codepoint_count, }; diff --git a/src/types.cpp b/src/types.cpp index c70db4f04..9da1d21a9 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -19,22 +19,4 @@ span_u8 span_u8_copy_or_throw(const void* data, size_t size) { std::memcpy(result.data, data, result.size); return result; } - -string8 string8_alloc_or_throw(size_t size) { - string8 result = {}; - result.size = size; - result.data = static_cast(malloc(size + 1 /*null-terminator*/)); - if (!result.data) - throw std::runtime_error( - fmt::format("Failed to allocate {} bytes for string8, out of memory", size + 1)); - result.data[result.size] = 0; - return result; -} - -string8 string8_copy_or_throw(const void* data, size_t size) { - string8 result = string8_alloc_or_throw(size); - std::memcpy(result.data, data, result.size); - result.data[result.size] = 0; - return result; -} }; // namespace session diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index f7594bbab..34da0195e 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -14,8 +14,8 @@ using namespace session::pro_backend; // NOTE: This is defined in main.cpp because it accepts a value from the CLI extern std::string g_test_pro_backend_dev_server_url; -static bool string8_equals(string8 s8, std::string_view str) { - return s8.size == str.size() && std::memcmp(s8.data, str.data(), s8.size) == 0; +static bool span_u8_equals(span_u8 s, std::string_view str) { + return std::string_view{reinterpret_cast(s.data), s.size} == str; } TEST_CASE("Pro Backend C API", "[pro_backend]") { // Setup: Generate keys and payment token hash @@ -69,7 +69,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT); REQUIRE(std::string_view(result.content_type) == cpp.content_type); REQUIRE(cpp.content_type == "application/json"); - REQUIRE(string8_equals(result.data, cpp.data)); + REQUIRE(span_u8_equals(result.data, cpp.data)); } // After freeing @@ -108,7 +108,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session::as_sys_seconds(unix_ts)); REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT); - REQUIRE(string8_equals(result.data, cpp.data)); + REQUIRE(span_u8_equals(result.data, cpp.data)); } REQUIRE(result.data.data == nullptr); @@ -133,7 +133,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { auto cpp = revocations_request(123); REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT); - REQUIRE(string8_equals(result.data, cpp.data)); + REQUIRE(span_u8_equals(result.data, cpp.data)); } REQUIRE(result.data.data == nullptr); } @@ -150,7 +150,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { master_privkey.data, session::as_sys_seconds(unix_ts), 10'000); REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT); - REQUIRE(string8_equals(result.data, cpp.data)); + REQUIRE(span_u8_equals(result.data, cpp.data)); } REQUIRE(result.data.data == nullptr); @@ -485,7 +485,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { to_byte_span(payment_id, payment_id_len)); REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT); - REQUIRE(string8_equals(result.data, cpp.data)); + REQUIRE(span_u8_equals(result.data, cpp.data)); } REQUIRE(result.data.data == nullptr); diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index fe6698ddc..3b47a586e 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -104,7 +104,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { session_protocol_pro_features_for_utf8(msg.data(), msg.size()); REQUIRE(pro_msg.status == SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_UTF_DECODING_ERROR); - REQUIRE(pro_msg.error.size); + REQUIRE(pro_msg.error != nullptr); + REQUIRE(pro_msg.error[0] != '\0'); } // Try a message exceeding the standard size threshold From ed23ecd78e53218ae9d4a7c2935c0796b5485416 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 18:57:04 -0300 Subject: [PATCH 136/193] pro_backend: split get_pro_details into get_pro_status + get_payment_details (Delta #15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old combined get-details endpoint conflated a cheap entitlement check with a heavy payment ledger; no client used the ledger beyond the latest item. Split it into two endpoints (both a signed-request flip, new domains): - get_pro_status (domain ProGetProStatus_; signed `master_pkey ‖ ts`) -- the hot "am I Pro?" path. Response: user_status, auto_renewing, expiry_ts, refund_requested_ts, grace_period_duration, error_report, and a single `latest_payment` (or none). No list, no pagination. C++ ProStatusResponse / parse_pro_status; C session_pro_backend_get_pro_status_response with a bool has_latest_payment + embedded latest_payment. - get_payment_details (domain ProGetPayDetails; signed `master_pkey ‖ ts ‖ \0 ‖ limit ‖ \0 ‖ before`) -- paginated history. Request {limit, before}; response {payments_total, items, next_cursor}, no user_status. Keyset pagination: `before`/`next_cursor` is an opaque cursor (std::optional / const char* NULL at end-of-data), echoed verbatim, never parsed. Payment-item parsing is factored into a shared parse_payment_item helper used by both `latest_payment` and `items`. No "pro details" terminology survives (identifiers, endpoints, domains, the error-report enum -> GET_PRO_STATUS_*). C-API change (clients): get_pro_details_{request_build,response,response_parse, response_free} and SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT are replaced by get_pro_status_* and get_payment_details_* equivalents. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/config/user_profile.h | 2 +- include/session/config/user_profile.hpp | 2 +- include/session/pro_backend.h | 107 ++++++--- include/session/pro_backend.hpp | 73 ++++-- include/session/session_protocol.hpp | 6 +- src/pro_backend.cpp | 295 +++++++++++++++++------- tests/test_pro_backend.cpp | 287 +++++++++++++++-------- 7 files changed, 549 insertions(+), 223 deletions(-) diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index d46102157..9bfca296b 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -378,7 +378,7 @@ LIBSESSION_EXPORT void user_profile_set_animated_avatar(config_object* conf, boo /// API: user_profile/user_profile_get_pro_access_expiry /// /// Retrieves the Session Pro access expiry unix timestamp if it has been set, this should generally -/// be the expiry value returned from /get_pro_details. +/// be the expiry value returned from /get_pro_status. /// /// Inputs: /// - `conf` -- [in] Pointer to the config object diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index ddf0b1e41..9cfceecc7 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -311,7 +311,7 @@ class UserProfile : public ConfigBase { /// API: user_profile/UserProfile::get_pro_access_expiry /// /// Retrieves the Session Pro access expiry unix timestamp if it has been set, this should - /// generally be the expiry value returned from /get_pro_details. + /// generally be the expiry value returned from /get_pro_status. /// /// Inputs: None /// diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 64b53b6df..951c54d41 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -28,7 +28,8 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_PAYMENT_PROVIDER_ /// owned in the C++ implementation (defined once, shared by the C and C++ sides). LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT; -LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_STATUS_ENDPOINT; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PAYMENT_DETAILS_ENDPOINT; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT; @@ -107,11 +108,11 @@ typedef struct session_pro_backend_request { LIBSESSION_EXPORT void session_pro_backend_request_free(session_pro_backend_request* request); -typedef enum SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT { - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_SUCCESS, - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR, - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT, -} SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT; +typedef enum SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT { + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_SUCCESS, + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_GENERIC_ERROR, + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_COUNT, +} SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT; /// Response outcome (wire `status`, spec §5). CLOSED/exhaustive: the backend will never add a /// value, so an unrecognized wire status is reported as a protocol error (RESPONSE_STATUS_ERROR + @@ -218,28 +219,50 @@ typedef struct session_pro_backend_pro_payment_item { const char* payment_id; } session_pro_backend_pro_payment_item; -typedef struct session_pro_backend_get_pro_details_response { +typedef struct session_pro_backend_get_pro_status_response { session_pro_backend_response_header header; - /// Array of payment items, with items_count elements - session_pro_backend_pro_payment_item* items; - size_t items_count; /// Opaque account Pro status code ("never"/"active"/"expired"); unknown values pass through. /// NUL-terminated; points into the response's `internal_`. const char* status; - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT error_report; bool auto_renewing; int64_t expiry_ts; int64_t grace_period_duration; int64_t refund_requested_ts; - uint32_t payments_total; -} session_pro_backend_get_pro_details_response; + /// True if the account has at least one payment, in which case `latest_payment` is populated + /// with the most recent one; false means the account has no payments and `latest_payment` is + /// unset. + bool has_latest_payment; + session_pro_backend_pro_payment_item latest_payment; +} session_pro_backend_get_pro_status_response; + +/// API: session_pro_backend/get_pro_status_response_free +/// +/// Frees the response. +LIBSESSION_EXPORT +void session_pro_backend_get_pro_status_response_free( + session_pro_backend_get_pro_status_response* response); -/// API: session_pro_backend/get_pro_details_response_free +typedef struct session_pro_backend_get_payment_details_response { + session_pro_backend_response_header header; + /// Array of payment items (one keyset page, newest-first), with items_count elements. + session_pro_backend_pro_payment_item* items; + size_t items_count; + /// Total number of payments the backend knows for the user (may exceed items_count). + uint32_t payments_total; + /// Opaque keyset-pagination cursor: pass as `before` to + /// session_pro_backend_get_payment_details_request_build to fetch the next (older) page. NULL + /// at end-of-data. NUL-terminated and points into the response's `internal_`; echo it verbatim, + /// do NOT parse or synthesize it. + const char* next_cursor; +} session_pro_backend_get_payment_details_response; + +/// API: session_pro_backend/get_payment_details_response_free /// /// Frees the response. LIBSESSION_EXPORT -void session_pro_backend_get_pro_details_response_free( - session_pro_backend_get_pro_details_response* response); +void session_pro_backend_get_payment_details_response_free( + session_pro_backend_get_payment_details_response* response); typedef struct session_pro_backend_set_payment_refund_requested_response { session_pro_backend_response_header header; @@ -305,9 +328,9 @@ session_pro_backend_request session_pro_backend_generate_pro_proof_request_build LIBSESSION_EXPORT session_pro_backend_request session_pro_backend_get_pro_revocations_request_build(int64_t ticket); -/// API: session_pro_backend/get_pro_details_request_build +/// API: session_pro_backend/get_pro_status_request_build /// -/// Builds the get-details request to POST, as a session_pro_backend_request (endpoint + +/// Builds the get-pro-status request to POST, as a session_pro_backend_request (endpoint + /// content_type + opaque data). Free it with `session_pro_backend_request_free`. On a key-size /// error `success` is false and `error`/`error_count` describe it. /// @@ -315,11 +338,31 @@ session_pro_backend_request session_pro_backend_get_pro_revocations_request_buil /// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte /// libsodium). /// - `ts` -- Unix timestamp (seconds) for the request. -/// - `count` -- number of historical payments to request. LIBSESSION_EXPORT -session_pro_backend_request session_pro_backend_get_pro_details_request_build( - const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, uint32_t count) - NON_NULL_ARG(1); +session_pro_backend_request session_pro_backend_get_pro_status_request_build( + const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts) NON_NULL_ARG(1); + +/// API: session_pro_backend/get_payment_details_request_build +/// +/// Builds the get-payment-details (paginated history) request to POST, as a +/// session_pro_backend_request (endpoint + content_type + opaque data). Free it with +/// `session_pro_backend_request_free`. On a key-size error `success` is false and +/// `error`/`error_count` describe it. +/// +/// Inputs: +/// - `master_privkey` / `master_privkey_len` -- Ed25519 master private key (32 or 64-byte +/// libsodium). +/// - `ts` -- Unix timestamp (seconds) for the request. +/// - `limit` -- maximum payments to return on this page (the backend may clamp it). +/// - `before` -- opaque pagination cursor from a previous response's `next_cursor`, or NULL / the +/// empty string for the newest page. Pass through verbatim; do not parse or synthesize it. +LIBSESSION_EXPORT +session_pro_backend_request session_pro_backend_get_payment_details_request_build( + const unsigned char* master_privkey, + size_t master_privkey_len, + int64_t ts, + uint32_t limit, + const char* before) NON_NULL_ARG(1); /// API: session_pro_backend/pro_proof_response_parse /// @@ -346,18 +389,30 @@ LIBSESSION_EXPORT session_pro_backend_get_pro_revocations_response session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t json_len); -/// API: session_pro_backend/get_pro_details_response_parse +/// API: session_pro_backend/get_pro_status_response_parse /// -/// Parses a JSON string into a GetProPaymentsResponse struct. -/// The caller must free the response using session_pro_backend_get_pro_details_response_free. +/// Parses a JSON string into a session_pro_backend_get_pro_status_response struct. +/// The caller must free the response using session_pro_backend_get_pro_status_response_free. /// /// Inputs: /// - `json` -- JSON string to parse. /// - `json_len` -- Length of the JSON string. LIBSESSION_EXPORT -session_pro_backend_get_pro_details_response session_pro_backend_get_pro_details_response_parse( +session_pro_backend_get_pro_status_response session_pro_backend_get_pro_status_response_parse( const char* json, size_t json_len); +/// API: session_pro_backend/get_payment_details_response_parse +/// +/// Parses a JSON string into a session_pro_backend_get_payment_details_response struct. +/// The caller must free the response using session_pro_backend_get_payment_details_response_free. +/// +/// Inputs: +/// - `json` -- JSON string to parse. +/// - `json_len` -- Length of the JSON string. +LIBSESSION_EXPORT +session_pro_backend_get_payment_details_response +session_pro_backend_get_payment_details_response_parse(const char* json, size_t json_len); + /// API: session_pro_backend/set_payment_refund_requested_request_build /// /// Builds the set-refund-requested request to POST, as a session_pro_backend_request (endpoint + diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index cee6c6c2b..cca64c881 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -49,11 +49,12 @@ /// cache. Any incoming messages with a Pro proof that is in the list of revoked proofs will not /// be entitled to Pro features. /// -/// 4. Query the status (and optionally payment history) of a user's Session Pro Master Ed25519 key -/// by building a `payment_details_request(...)` query and submitting it. +/// 4. Query a user's Session Pro entitlement status for their Master Ed25519 key by building a +/// `pro_status_request(...)` query (the hot "am I Pro?" path) and submitting it. Parse the reply +/// with `parse_pro_status(...)`, which yields the account status plus its single latest payment. /// -/// Parse the reply with `parse_payment_details(...)`, which they can use to populate their -/// client's payment history. +/// For the full payment history (e.g. a receipts view), page through it with +/// `payment_details_request(...)` / `parse_payment_details(...)` using the opaque keyset cursor. /// /// See the unit tests for examples of using the APIs mentioned. @@ -285,24 +286,42 @@ struct GetProRevocationsResponse : ResponseBase { /// `errors` is populated. GetProRevocationsResponse parse_revocations(std::string_view json); -/// Query a master key's Session Pro payment/subscription details and history (endpoint -/// `get_pro_details`). `payment_details_sig` computes the master signature; -/// `payment_details_request` builds the whole request (signing internally) and returns the endpoint -/// + JSON body. Both throw on an incorrectly-sized key. +/// Query a master key's Session Pro status (endpoint `get_pro_status`): the account entitlement +/// state plus its single latest payment -- the hot "am I Pro?" path. `pro_status_sig` computes the +/// master signature; `pro_status_request` builds the whole request (signing internally) and returns +/// the endpoint + JSON body. Both throw on an incorrectly-sized key. /// /// Inputs: /// - `master_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium master private key /// - `unix_ts` -- Unix timestamp for the request -/// - `count` -- maximum number of historical payments to request +b64 pro_status_sig(const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts); + +ProRequest pro_status_request( + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts); + +/// Query a master key's Session Pro payment history (endpoint `get_payment_details`), one keyset +/// page at a time. `payment_details_sig` computes the master signature; `payment_details_request` +/// builds the whole request (signing internally) and returns the endpoint + JSON body. Both throw +/// on an incorrectly-sized key. +/// +/// Inputs: +/// - `master_privkey` -- 32-byte Ed25519 seed or 64-byte libsodium master private key +/// - `unix_ts` -- Unix timestamp for the request +/// - `limit` -- maximum payments to return on this page (the backend may clamp it) +/// - `before` -- opaque pagination cursor from a previous page's `next_cursor` (see +/// PaymentDetailsResponse); the empty string requests the newest page. Pass it through verbatim; +/// it must not be parsed or synthesized. b64 payment_details_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - uint32_t count); + uint32_t limit, + std::string_view before); ProRequest payment_details_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - uint32_t count); + uint32_t limit, + std::string_view before); /// Unit of a billing period (the `unit` of a parsed `plan` code, pro-wire-protocol.md §1 / Delta /// #14). A closed set: the backend emits only these, so an unrecognized code is a protocol error. @@ -381,17 +400,19 @@ struct ProPaymentItem { std::string payment_id; }; -struct GetProDetailsResponse : ResponseBase { - /// List of payment items for the master public key - std::vector items; - +struct ProStatusResponse : ResponseBase { /// Current Session Pro entitlement status for the master public key + /// ("never"/"active"/"expired"; opaque -- an unknown value passes through as-is). std::string user_status; + /// The single most-recent payment for the master public key, or std::nullopt when the account + /// has no payments. (The full payment history is a separate query -- parse_payment_details.) + std::optional latest_payment; + /// Error code that indicates that the Session Pro Backend encountered an error book-keeping /// Session Pro entitlement for the user. If this value is not `SUCCESS` implementing clients /// can optionally prompt the user that they should contact support for investigation. - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT error_report; + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT error_report; /// Flag to indicate if the user will automatically renew their subscription. bool auto_renewing; @@ -433,15 +454,31 @@ struct GetProDetailsResponse : ResponseBase { /// payment associated with the `expiry_at`). This value is 0 if no refund has been /// requested on the active payment. std::chrono::sys_seconds refund_requested_at; +}; + +struct PaymentDetailsResponse : ResponseBase { + /// One keyset page of the master public key's payment history, newest-first (at most the + /// `limit` passed to payment_details_request). + std::vector items; /// Total number of payments known by the backend for the user. This may be greater than the - /// length of items if the request, requested less than the number of payments the user has. + /// length of `items` when the requested page did not cover the full history. uint32_t payments_total; + + /// Opaque keyset-pagination cursor for the next (older) page: re-request with this value as + /// `before` to continue, and stop when it is std::nullopt (end-of-data). Store and echo it + /// verbatim -- it is an encrypted token and MUST NOT be parsed or synthesized (a tampered or + /// foreign cursor is rejected). (pro-wire-protocol.md §5.3.) + std::optional next_cursor; }; +/// Parse the reply to a `pro_status_request`. On failure `status` is set to an error state and +/// `errors` is populated. +ProStatusResponse parse_pro_status(std::string_view json); + /// Parse the reply to a `payment_details_request`. On failure `status` is set to an error state and /// `errors` is populated. -GetProDetailsResponse parse_payment_details(std::string_view json); +PaymentDetailsResponse parse_payment_details(std::string_view json); /// Record a refund request against an existing Session Pro payment (endpoint /// `set_payment_refund_requested`). `refund_sig` computes the master signature; `refund_request` diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index d91fd35bf..1ec1d72c8 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -59,12 +59,14 @@ inline constexpr std::string_view GENERATE_PROOF_DOMAIN = "ProGenerateProof"; inline constexpr std::string_view BUILD_PROOF_DOMAIN = "ProProof_v0_____"; inline constexpr std::string_view ADD_PRO_PAYMENT_DOMAIN = "ProAddPayment___"; inline constexpr std::string_view SET_PAYMENT_REFUND_REQUESTED_DOMAIN = "ProSetRefundReq_"; -inline constexpr std::string_view GET_PRO_DETAILS_DOMAIN = "ProGetProDetReq_"; +inline constexpr std::string_view GET_PRO_STATUS_DOMAIN = "ProGetProStatus_"; +inline constexpr std::string_view GET_PAYMENT_DETAILS_DOMAIN = "ProGetPayDetails"; static_assert(GENERATE_PROOF_DOMAIN.size() == 16); static_assert(BUILD_PROOF_DOMAIN.size() == 16); static_assert(ADD_PRO_PAYMENT_DOMAIN.size() == 16); static_assert(SET_PAYMENT_REFUND_REQUESTED_DOMAIN.size() == 16); -static_assert(GET_PRO_DETAILS_DOMAIN.size() == 16); +static_assert(GET_PRO_STATUS_DOMAIN.size() == 16); +static_assert(GET_PAYMENT_DETAILS_DOMAIN.size() == 16); enum class ProStatus { // Pro proof sig was not signed by the Pro backend key diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index d75e8ff37..ff65721fb 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -133,7 +133,8 @@ namespace { // and the C++ `*_request()` return values point at these — the path is defined exactly once. constexpr char add_payment_endpoint[] = "add_pro_payment"; constexpr char generate_proof_endpoint[] = "generate_pro_proof"; - constexpr char get_pro_details_endpoint[] = "get_pro_details"; + constexpr char get_pro_status_endpoint[] = "get_pro_status"; + constexpr char get_payment_details_endpoint[] = "get_payment_details"; constexpr char get_pro_revocations_endpoint[] = "get_pro_revocations"; constexpr char set_refund_endpoint[] = "set_payment_refund_requested"; @@ -186,8 +187,10 @@ LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_E add_payment_endpoint; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT = generate_proof_endpoint; -LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT = - get_pro_details_endpoint; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_STATUS_ENDPOINT = + get_pro_status_endpoint; +LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PAYMENT_DETAILS_ENDPOINT = + get_payment_details_endpoint; LIBSESSION_EXPORT extern const char* const SESSION_PRO_BACKEND_GET_PRO_REVOCATIONS_ENDPOINT = get_pro_revocations_endpoint; LIBSESSION_EXPORT extern const char* const @@ -534,54 +537,139 @@ GetProRevocationsResponse parse_revocations(std::string_view json) { namespace { - // --- payment-details / get-pro-details (endpoint get_pro_details) --- + // --- get-pro-status (endpoint get_pro_status) --- + + std::vector pro_status_message( + std::span master_pubkey, std::chrono::sys_seconds unix_ts) { + // Must match the get-pro-status signed-request message in pro-wire-protocol.md §3.4, built + // per §1.1. + return pro::signed_message(GET_PRO_STATUS_DOMAIN, master_pubkey, epoch_seconds(unix_ts)); + } + + std::string pro_status_body( + std::span master_pubkey, + std::span master_sig, + std::chrono::sys_seconds unix_ts) { + return nlohmann::json{ + {"master_pkey", oxenc::to_hex(master_pubkey)}, + {"master_sig", oxenc::to_hex(master_sig)}, + {"ts", epoch_seconds(unix_ts)}} + .dump(); + } + + // --- get-payment-details (endpoint get_payment_details) --- std::vector payment_details_message( std::span master_pubkey, std::chrono::sys_seconds unix_ts, - uint32_t count) { - // Must match the get-pro-details signed-request message in pro-wire-protocol.md §3.4, - // built per §1.1. + uint32_t limit, + std::string_view before) { + // Must match the get-payment-details signed-request message in pro-wire-protocol.md §3.4, + // built per §1.1. `before` is the opaque pagination cursor (§5.3), empty for the newest + // page. return pro::signed_message( - GET_PRO_DETAILS_DOMAIN, master_pubkey, epoch_seconds(unix_ts), count); + GET_PAYMENT_DETAILS_DOMAIN, master_pubkey, epoch_seconds(unix_ts), limit, before); } std::string payment_details_body( std::span master_pubkey, std::span master_sig, std::chrono::sys_seconds unix_ts, - uint32_t count) { + uint32_t limit, + std::string_view before) { return nlohmann::json{ {"master_pkey", oxenc::to_hex(master_pubkey)}, {"master_sig", oxenc::to_hex(master_sig)}, {"ts", epoch_seconds(unix_ts)}, - {"count", count}} + {"limit", limit}, + {"before", before}} .dump(); } + // Parse one payment item object (shared by get-pro-status's `latest_payment` and + // get-payment-details's `items`). Appends to `errs` and returns a best-effort item on any + // field error; the caller checks `errs`. + ProPaymentItem parse_payment_item( + const nlohmann::json::object_t& obj, std::vector& errs) { + auto status = json_require(obj, "status", errs); + auto plan_code = json_require(obj, "plan", errs); + auto plan = parse_plan_period(plan_code); + if (!plan) + errs.push_back( + fmt::format("'plan' is not a recognized billing-period code: '{}'", plan_code)); + auto payment_provider = json_require(obj, "payment_provider", errs); + auto payment_id = json_require(obj, "payment_id", errs); + auto auto_renewing = json_require(obj, "auto_renewing", errs); + // purchased_ts and revoked_ts are upstream-provider event instants: floats on the wire + // carrying sub-second precision (kept as millisecond-precision sys_ms). All other + // timestamps are whole-second integers. + auto purchased_ts = json_require_number(obj, "purchased_ts", errs); + auto redeemed_ts = json_require(obj, "redeemed_ts", errs); + auto expiry_ts = json_require(obj, "expiry_ts", errs); + auto grace_period_duration = json_require(obj, "grace_period_duration", errs); + auto platform_refund_expiry_ts = + json_require(obj, "platform_refund_expiry_ts", errs); + auto revoked_ts = json_require_number(obj, "revoked_ts", errs); + auto refund_requested_ts = json_require(obj, "refund_requested_ts", errs); + + ProPaymentItem item = {}; + item.status = std::move(status); + // `plan` is parsed (closed grammar, §1 / Delta #14); an unrecognized code is a protocol + // error (handled above). payment_provider / payment_id are opaque strings that pass through + // as-is. + if (plan) + item.plan = *plan; + item.payment_provider = std::move(payment_provider); + item.payment_id = std::move(payment_id); + item.auto_renewing = auto_renewing; + item.purchased_at = sys_ms_from_seconds(purchased_ts); + item.redeemed_at = as_sys_seconds(redeemed_ts); + item.expiry_at = as_sys_seconds(expiry_ts); + item.grace_period_duration = std::chrono::seconds(grace_period_duration); + item.platform_refund_expiry_at = as_sys_seconds(platform_refund_expiry_ts); + item.revoked_at = sys_ms_from_seconds(revoked_ts); + item.refund_requested_at = as_sys_seconds(refund_requested_ts); + return item; + } + } // namespace +b64 pro_status_sig(const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts) { + auto msg = pro_status_message(master_privkey.pubkey(), unix_ts); + return ed25519::sign(master_privkey, msg); +} + +ProRequest pro_status_request( + const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts) { + auto sig = pro_status_sig(master_privkey, unix_ts); + return {get_pro_status_endpoint, + application_json, + pro_status_body(master_privkey.pubkey(), sig, unix_ts)}; +} + b64 payment_details_sig( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - uint32_t count) { - auto msg = payment_details_message(master_privkey.pubkey(), unix_ts, count); + uint32_t limit, + std::string_view before) { + auto msg = payment_details_message(master_privkey.pubkey(), unix_ts, limit, before); return ed25519::sign(master_privkey, msg); } ProRequest payment_details_request( const ed25519::PrivKeySpan& master_privkey, std::chrono::sys_seconds unix_ts, - uint32_t count) { - auto sig = payment_details_sig(master_privkey, unix_ts, count); - return {get_pro_details_endpoint, + uint32_t limit, + std::string_view before) { + auto sig = payment_details_sig(master_privkey, unix_ts, limit, before); + return {get_payment_details_endpoint, application_json, - payment_details_body(master_privkey.pubkey(), sig, unix_ts, count)}; + payment_details_body(master_privkey.pubkey(), sig, unix_ts, limit, before)}; } -GetProDetailsResponse parse_payment_details(std::string_view json) { +ProStatusResponse parse_pro_status(std::string_view json) { // Parse basics - GetProDetailsResponse result = {}; + ProStatusResponse result = {}; std::vector errs; auto result_obj = read_envelope(json, result, errs); if (!result || !errs.empty()) { @@ -596,18 +684,16 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { result.user_status = json_require(result_obj, "user_status", errs); uint32_t error_report = json_require(result_obj, "error_report", errs); - if (error_report >= SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_COUNT) { + if (error_report >= SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_COUNT) { errs.push_back(fmt::format("Error report value was out-of-bounds: {}", error_report)); set_protocol_error(result, errs.front()); return result; } result.error_report = - static_cast(error_report); + static_cast(error_report); result.auto_renewing = json_require(result_obj, "auto_renewing", errs); - result.payments_total = json_require(result_obj, "payments_total", errs); - int64_t expiry_ts = json_require(result_obj, "expiry_ts", errs); int64_t grace_period_duration = json_require(result_obj, "grace_period_duration", errs); @@ -616,6 +702,39 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { result.grace_period_duration = std::chrono::seconds(grace_period_duration); result.refund_requested_at = as_sys_seconds(refund_requested_ts); + // `latest_payment` is a single payment item, or null when the account has no payments. + if (auto it = result_obj.find("latest_payment"); it == result_obj.end()) { + errs.push_back("Key 'latest_payment' is missing"); + } else if (it->second.is_null()) { + // No payments: latest_payment stays std::nullopt. + } else if (it->second.is_object()) { + auto obj = it->second.get(); + auto item = parse_payment_item(obj, errs); + if (errs.empty()) + result.latest_payment = std::move(item); + } else { + errs.push_back(fmt::format( + "Key value (latest_payment, {}) was not an object or null", it->second.dump(1))); + } + + if (!errs.empty()) + set_protocol_error(result, errs.front()); + return result; +} + +PaymentDetailsResponse parse_payment_details(std::string_view json) { + // Parse basics + PaymentDetailsResponse result = {}; + std::vector errs; + auto result_obj = read_envelope(json, result, errs); + if (!result || !errs.empty()) { + if (!errs.empty()) + set_protocol_error(result, errs.front()); + return result; + } + + result.payments_total = json_require(result_obj, "payments_total", errs); + auto array = json_require(result_obj, "items", errs); result.items.reserve(array.size()); for (size_t index = 0; index < array.size(); index++) { @@ -626,56 +745,25 @@ GetProDetailsResponse parse_payment_details(std::string_view json) { break; } - // Parse payment item auto obj = it.get(); - auto status = json_require(obj, "status", errs); - auto plan_code = json_require(obj, "plan", errs); - auto plan = parse_plan_period(plan_code); - if (!plan) - errs.push_back( - fmt::format("'plan' is not a recognized billing-period code: '{}'", plan_code)); - auto payment_provider = json_require(obj, "payment_provider", errs); - auto payment_id = json_require(obj, "payment_id", errs); - auto auto_renewing = json_require(obj, "auto_renewing", errs); - // purchased_ts and revoked_ts are upstream-provider event instants: floats on the wire - // carrying sub-second precision (kept as millisecond-precision sys_ms). All other - // timestamps are whole-second integers. - auto purchased_ts = json_require_number(obj, "purchased_ts", errs); - auto redeemed_ts = json_require(obj, "redeemed_ts", errs); - auto expiry_ts = json_require(obj, "expiry_ts", errs); - auto grace_period_duration = json_require(obj, "grace_period_duration", errs); - auto platform_refund_expiry_ts = - json_require(obj, "platform_refund_expiry_ts", errs); - auto revoked_ts = json_require_number(obj, "revoked_ts", errs); - auto refund_requested_ts = json_require(obj, "refund_requested_ts", errs); - - ProPaymentItem item = {}; - item.status = std::move(status); - - // `plan` is parsed (closed grammar, §1 / Delta #14); an unrecognized code is a protocol - // error (handled above). payment_provider / payment_id are opaque strings that pass through - // as-is. - if (plan) - item.plan = *plan; - item.payment_provider = std::move(payment_provider); - item.payment_id = std::move(payment_id); - - item.auto_renewing = auto_renewing; - item.purchased_at = sys_ms_from_seconds(purchased_ts); - item.redeemed_at = as_sys_seconds(redeemed_ts); - item.expiry_at = as_sys_seconds(expiry_ts); - item.grace_period_duration = std::chrono::seconds(grace_period_duration); - item.platform_refund_expiry_at = as_sys_seconds(platform_refund_expiry_ts); - item.revoked_at = sys_ms_from_seconds(revoked_ts); - item.refund_requested_at = as_sys_seconds(refund_requested_ts); - - // Handle parsing result - if (errs.size()) + auto item = parse_payment_item(obj, errs); + if (!errs.empty()) break; - result.items.emplace_back(std::move(item)); } + // `next_cursor` is the opaque keyset cursor (§5.3), or null at end-of-data. + if (auto it = result_obj.find("next_cursor"); it == result_obj.end()) { + errs.push_back("Key 'next_cursor' is missing"); + } else if (it->second.is_null()) { + // End of data: next_cursor stays std::nullopt. + } else if (it->second.is_string()) { + result.next_cursor = it->second.get(); + } else { + errs.push_back(fmt::format( + "Key value (next_cursor, {}) was not a string or null", it->second.dump(1))); + } + if (!errs.empty()) set_protocol_error(result, errs.front()); return result; @@ -830,7 +918,7 @@ static session_pro_backend_pro_payment_item to_c(ProPaymentItem& src) { struct GetProRevocationsCResponse : GetProRevocationsResponse { std::vector item_views; }; -struct GetProDetailsCResponse : GetProDetailsResponse { +struct GetPaymentDetailsCResponse : PaymentDetailsResponse { std::vector item_views; }; @@ -914,17 +1002,32 @@ session_pro_backend_get_pro_revocations_request_build(int64_t ticket) { return result; } -LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_details_request_build( +LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_pro_status_request_build( + const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts) { + session_pro_backend_request result = {}; + try { + result = c_own_request(pro_status_request( + ed25519::PrivKeySpan{master_privkey, master_privkey_len}, + session::as_sys_seconds(ts))); + } catch (const std::exception& e) { + c_request_error(result, e); + } + return result; +} + +LIBSESSION_C_API session_pro_backend_request session_pro_backend_get_payment_details_request_build( const unsigned char* master_privkey, size_t master_privkey_len, int64_t ts, - uint32_t count) { + uint32_t limit, + const char* before) { session_pro_backend_request result = {}; try { result = c_own_request(payment_details_request( ed25519::PrivKeySpan{master_privkey, master_privkey_len}, session::as_sys_seconds(ts), - count)); + limit, + before ? std::string_view{before} : std::string_view{})); } catch (const std::exception& e) { c_request_error(result, e); } @@ -1002,9 +1105,9 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t return result; } -LIBSESSION_C_API session_pro_backend_get_pro_details_response -session_pro_backend_get_pro_details_response_parse(const char* json, size_t json_len) { - session_pro_backend_get_pro_details_response result = {}; +LIBSESSION_C_API session_pro_backend_get_pro_status_response +session_pro_backend_get_pro_status_response_parse(const char* json, size_t json_len) { + session_pro_backend_get_pro_status_response result = {}; if (!json) { result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; result.header.error_code = C_INVALID_RESPONSE_CODE; @@ -1014,7 +1117,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json try { using session::epoch_seconds; - auto* owned = new GetProDetailsCResponse{parse_payment_details({json, json_len}), {}}; + auto* owned = new ProStatusResponse(parse_pro_status({json, json_len})); result.header.internal_ = owned; fill_c_header(result.header, *owned); @@ -1024,7 +1127,36 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json result.expiry_ts = epoch_seconds(owned->expiry_at); result.grace_period_duration = owned->grace_period_duration.count(); result.refund_requested_ts = epoch_seconds(owned->refund_requested_at); + result.has_latest_payment = owned->latest_payment.has_value(); + if (owned->latest_payment) + result.latest_payment = to_c(*owned->latest_payment); + } catch (const std::exception&) { + delete static_cast(result.header.internal_); + result = {}; + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_OUT_OF_MEMORY; + } + return result; +} + +LIBSESSION_C_API session_pro_backend_get_payment_details_response +session_pro_backend_get_payment_details_response_parse(const char* json, size_t json_len) { + session_pro_backend_get_payment_details_response result = {}; + if (!json) { + result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; + result.header.error_code = C_INVALID_RESPONSE_CODE; + result.header.error = C_PARSE_ERROR_INVALID_ARGS; + return result; + } + + try { + auto* owned = new GetPaymentDetailsCResponse{parse_payment_details({json, json_len}), {}}; + result.header.internal_ = owned; + fill_c_header(result.header, *owned); + result.payments_total = owned->payments_total; + result.next_cursor = owned->next_cursor ? owned->next_cursor->c_str() : nullptr; owned->item_views.reserve(owned->items.size()); for (auto& src : owned->items) @@ -1032,7 +1164,7 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json result.items = owned->item_views.data(); result.items_count = owned->item_views.size(); } catch (const std::exception&) { - delete static_cast(result.header.internal_); + delete static_cast(result.header.internal_); result = {}; result.header.status = SESSION_PRO_BACKEND_RESPONSE_STATUS_ERROR; result.header.error_code = C_INVALID_RESPONSE_CODE; @@ -1107,9 +1239,14 @@ LIBSESSION_C_API void session_pro_backend_get_pro_revocations_response_free( c_free_response(response); } -LIBSESSION_C_API void session_pro_backend_get_pro_details_response_free( - session_pro_backend_get_pro_details_response* response) { - c_free_response(response); +LIBSESSION_C_API void session_pro_backend_get_pro_status_response_free( + session_pro_backend_get_pro_status_response* response) { + c_free_response(response); +} + +LIBSESSION_C_API void session_pro_backend_get_payment_details_response_free( + session_pro_backend_get_payment_details_response* response) { + c_free_response(response); } LIBSESSION_C_API void session_pro_backend_set_payment_refund_requested_response_free( diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 34da0195e..e1eeee81c 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -138,25 +138,56 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.data.data == nullptr); } - SECTION("session_pro_backend_get_pro_details_request_build") { - auto result = session_pro_backend_get_pro_details_request_build( - master_privkey.data, sizeof(master_privkey.data), unix_ts, 10'000); + SECTION("session_pro_backend_get_pro_status_request_build") { + auto result = session_pro_backend_get_pro_status_request_build( + master_privkey.data, sizeof(master_privkey.data), unix_ts); + { + scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; + REQUIRE(result.success); + REQUIRE(result.data.size > 0); + + auto cpp = + pro_status_request(master_privkey.data, session::as_sys_seconds(unix_ts)); + REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_STATUS_ENDPOINT); + REQUIRE(span_u8_equals(result.data, cpp.data)); + } + REQUIRE(result.data.data == nullptr); + + // Invalid key size + result = session_pro_backend_get_pro_status_request_build( + master_privkey.data, sizeof(master_privkey.data) - 1, unix_ts); + REQUIRE(!result.success); + REQUIRE(result.error_count > 0); + } + + SECTION("session_pro_backend_get_payment_details_request_build") { + auto result = session_pro_backend_get_payment_details_request_build( + master_privkey.data, sizeof(master_privkey.data), unix_ts, 100, ""); { scope_exit result_free{[&]() { session_pro_backend_request_free(&result); }}; REQUIRE(result.success); REQUIRE(result.data.size > 0); auto cpp = payment_details_request( - master_privkey.data, session::as_sys_seconds(unix_ts), 10'000); + master_privkey.data, session::as_sys_seconds(unix_ts), 100, ""); REQUIRE(std::string_view(result.endpoint) == cpp.endpoint); - REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PRO_DETAILS_ENDPOINT); + REQUIRE(cpp.endpoint == SESSION_PRO_BACKEND_GET_PAYMENT_DETAILS_ENDPOINT); REQUIRE(span_u8_equals(result.data, cpp.data)); + + // A NULL cursor is equivalent to the empty (newest-page) cursor. + auto result_null = session_pro_backend_get_payment_details_request_build( + master_privkey.data, sizeof(master_privkey.data), unix_ts, 100, nullptr); + scope_exit result_null_free{ + [&]() { session_pro_backend_request_free(&result_null); }}; + REQUIRE(result_null.success); + REQUIRE(span_u8_equals(result_null.data, cpp.data)); } REQUIRE(result.data.data == nullptr); // Invalid key size - result = session_pro_backend_get_pro_details_request_build( - master_privkey.data, sizeof(master_privkey.data) - 1, unix_ts, 10'000); + result = session_pro_backend_get_payment_details_request_build( + master_privkey.data, sizeof(master_privkey.data) - 1, unix_ts, 100, ""); REQUIRE(!result.success); REQUIRE(result.error_count > 0); } @@ -322,98 +353,164 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { REQUIRE(result.header.error_code != nullptr); } - SECTION("session_pro_backend_get_pro_details_response_parse") { + // A single payment item (shared by the get-pro-status latest_payment and the + // get-payment-details items[] tests). purchased_ts/revoked_ts are floats on the wire + // (sub-second precision); the rest are whole-second integers. + auto make_payment_item = [&](std::string payment_id) { + return nlohmann::json{ + {"status", "redeemed"}, + {"plan", "1m"}, + {"payment_provider", SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY}, + {"auto_renewing", false}, + {"purchased_ts", unix_ts - 3600 + 0.5}, + {"redeemed_ts", unix_ts - 3600}, + {"expiry_ts", unix_ts}, + {"grace_period_duration", 1001}, + {"platform_refund_expiry_ts", unix_ts + 1}, + {"revoked_ts", unix_ts + 3600 + 0.75}, + {"refund_requested_ts", unix_ts + 3601}, + {"payment_id", std::move(payment_id)}}; + }; + auto check_payment_item = [&](const session_pro_backend_pro_payment_item& item, + std::string_view payment_id) { + REQUIRE(std::string_view(item.status) == "redeemed"); + REQUIRE(item.plan_count == 1); + REQUIRE(item.plan_unit == SESSION_PRO_BACKEND_PLAN_UNIT_MONTH); + REQUIRE(std::string_view(item.payment_provider) == + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY); + // Sub-second precision preserved through sys_ms (0.5s = 500ms) round-trips exactly + REQUIRE(item.purchased_ts == unix_ts - 3600 + 0.5); + REQUIRE(item.redeemed_ts == unix_ts - 3600); + REQUIRE(item.expiry_ts == unix_ts); + REQUIRE(item.grace_period_duration == 1001); + REQUIRE(item.platform_refund_expiry_ts == unix_ts + 1); + REQUIRE(item.revoked_ts == unix_ts + 3600 + 0.75); // 750ms preserved + REQUIRE(item.refund_requested_ts == unix_ts + 3601); + REQUIRE(std::string_view(item.payment_id) == payment_id); + }; + + SECTION("session_pro_backend_get_pro_status_response_parse") { nlohmann::json j; j["status"] = "ok"; j["result"] = { {"user_status", "expired"}, - {"error_report", - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR}, + {"error_report", SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_GENERIC_ERROR}, {"auto_renewing", true}, {"expiry_ts", unix_ts + 2}, {"grace_period_duration", 1000}, {"refund_requested_ts", unix_ts + 3602}, - {"payments_total", 3}, - {"items", - nlohmann::json::array( - {{{"status", "redeemed"}, - {"plan", "1m"}, - {"payment_provider", - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY}, - {"auto_renewing", false}, - // purchased_ts/revoked_ts are floats on the wire (sub-second - // precision); libsession keeps them as millisecond-precise sys_ms. - {"purchased_ts", unix_ts - 3600 + 0.5}, - {"redeemed_ts", unix_ts - 3600}, - {"expiry_ts", unix_ts}, - {"grace_period_duration", 1001}, - {"platform_refund_expiry_ts", unix_ts + 1}, - {"revoked_ts", unix_ts + 3600 + 0.75}, - {"refund_requested_ts", unix_ts + 3601}, - {"payment_id", - std::string(fake_payment_id.data(), fake_payment_id.size())}}})}}; + {"latest_payment", + make_payment_item( + std::string(fake_payment_id.data(), fake_payment_id.size()))}}; std::string json = j.dump(); - // Valid Google JSON + // Valid response with a latest payment auto result = - session_pro_backend_get_pro_details_response_parse(json.data(), json.size()); + session_pro_backend_get_pro_status_response_parse(json.data(), json.size()); { scope_exit result_free{ - [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; + [&]() { session_pro_backend_get_pro_status_response_free(&result); }}; if (result.header.error) INFO(result.header.error); - REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.error == nullptr); REQUIRE(std::string_view(result.status) == "expired"); REQUIRE(result.error_report == - SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT_GENERIC_ERROR); - REQUIRE(result.items_count == 1); + SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT_GENERIC_ERROR); REQUIRE(result.auto_renewing == true); REQUIRE(result.grace_period_duration == 1000); REQUIRE(result.expiry_ts == unix_ts + 2); REQUIRE(result.refund_requested_ts == unix_ts + 3602); + REQUIRE(result.has_latest_payment); + check_payment_item(result.latest_payment, fake_payment_id); + } + + // A null latest_payment (account with no payments) + j["result"]["latest_payment"] = nullptr; + json = j.dump(); + auto result_empty = + session_pro_backend_get_pro_status_response_parse(json.data(), json.size()); + { + scope_exit result_free{ + [&]() { session_pro_backend_get_pro_status_response_free(&result_empty); }}; + if (result_empty.header.error) + INFO(result_empty.header.error); + REQUIRE(result_empty.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result_empty.has_latest_payment == false); + } + + // After freeing + REQUIRE(result.header.internal_ == nullptr); + + // Invalid JSON + json = "{invalid}"; + result = session_pro_backend_get_pro_status_response_parse(json.data(), json.size()); + { + scope_exit result_free{ + [&]() { session_pro_backend_get_pro_status_response_free(&result); }}; + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code != nullptr); + } + session_pro_backend_get_pro_status_response_free(&result); + REQUIRE(result.header.internal_ == nullptr); + + // Null JSON + result = session_pro_backend_get_pro_status_response_parse(nullptr, 0); + REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error_code != nullptr); + } + + SECTION("session_pro_backend_get_payment_details_response_parse") { + nlohmann::json j; + j["status"] = "ok"; + j["result"] = { + {"payments_total", 3}, + {"items", + nlohmann::json::array({make_payment_item( + std::string(fake_payment_id.data(), fake_payment_id.size()))})}, + {"next_cursor", "opaque-cursor-token"}}; + std::string json = j.dump(); + + // Valid page with a cursor + auto result = session_pro_backend_get_payment_details_response_parse( + json.data(), json.size()); + { + scope_exit result_free{ + [&]() { session_pro_backend_get_payment_details_response_free(&result); }}; + if (result.header.error) + INFO(result.header.error); + + REQUIRE(result.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(result.header.error == nullptr); REQUIRE(result.payments_total == 3); + REQUIRE(result.items_count == 1); REQUIRE(result.items != nullptr); - REQUIRE(std::string_view(result.items[0].status) == "redeemed"); - REQUIRE(result.items[0].plan_count == 1); - REQUIRE(result.items[0].plan_unit == SESSION_PRO_BACKEND_PLAN_UNIT_MONTH); - REQUIRE(std::string_view(result.items[0].payment_provider) == - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY); - // Sub-second precision preserved through sys_ms (0.5s = 500ms) round-trips exactly - REQUIRE(result.items[0].purchased_ts == unix_ts - 3600 + 0.5); - REQUIRE(result.items[0].redeemed_ts == unix_ts - 3600); - REQUIRE(result.items[0].expiry_ts == unix_ts); - REQUIRE(result.items[0].grace_period_duration == 1001); - REQUIRE(result.items[0].platform_refund_expiry_ts == unix_ts + 1); - REQUIRE(result.items[0].revoked_ts == unix_ts + 3600 + 0.75); // 750ms preserved - REQUIRE(result.items[0].refund_requested_ts == unix_ts + 3601); - REQUIRE(std::string_view(result.items[0].payment_id) == fake_payment_id); + check_payment_item(result.items[0], fake_payment_id); + REQUIRE(result.next_cursor != nullptr); + REQUIRE(std::string_view(result.next_cursor) == "opaque-cursor-token"); } - // Tweak JSON for a different provider. payment_id is opaque so nothing - // provider-specific changes in how libsession parses it. + // End-of-data: null next_cursor, and payment_id is opaque so a different provider's + // value passes through unchanged. j["result"]["items"][0]["payment_provider"] = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF; j["result"]["items"][0]["payment_id"] = "rangeproof-opaque-id"; + j["result"]["next_cursor"] = nullptr; json = j.dump(); - - // Valid Rangeproof JSON - auto result_rangeproof = - session_pro_backend_get_pro_details_response_parse(json.data(), json.size()); + auto result_end = session_pro_backend_get_payment_details_response_parse( + json.data(), json.size()); { scope_exit result_free{[&]() { - session_pro_backend_get_pro_details_response_free(&result_rangeproof); + session_pro_backend_get_payment_details_response_free(&result_end); }}; - if (result_rangeproof.header.error) - INFO(result_rangeproof.header.error); - - // Only check what we expect to be different - REQUIRE(std::string_view(result_rangeproof.items[0].payment_provider) == + if (result_end.header.error) + INFO(result_end.header.error); + REQUIRE(result_end.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); + REQUIRE(std::string_view(result_end.items[0].payment_provider) == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_RANGEPROOF); - REQUIRE(std::string_view(result_rangeproof.items[0].payment_id) == - "rangeproof-opaque-id"); + REQUIRE(std::string_view(result_end.items[0].payment_id) == "rangeproof-opaque-id"); + REQUIRE(result_end.next_cursor == nullptr); // end-of-data } // After freeing @@ -423,24 +520,21 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Invalid JSON json = "{invalid}"; - result = session_pro_backend_get_pro_details_response_parse(json.data(), json.size()); + result = session_pro_backend_get_payment_details_response_parse( + json.data(), json.size()); { scope_exit result_free{ - [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; + [&]() { session_pro_backend_get_payment_details_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.error_code != nullptr); - REQUIRE(result.header.error_code != nullptr); } - - // After freeing - session_pro_backend_get_pro_details_response_free(&result); + session_pro_backend_get_payment_details_response_free(&result); REQUIRE(result.header.internal_ == nullptr); // Null JSON - result = session_pro_backend_get_pro_details_response_parse(nullptr, 0); + result = session_pro_backend_get_payment_details_response_parse(nullptr, 0); REQUIRE(result.header.status != SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(result.header.error_code != nullptr); - REQUIRE(result.header.error_code != nullptr); } SECTION("Memory management edge cases") { @@ -458,8 +552,12 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_get_pro_revocations_response_free(&rev_response); REQUIRE(rev_response.header.internal_ == nullptr); - session_pro_backend_get_pro_details_response pay_response = {}; - session_pro_backend_get_pro_details_response_free(&pay_response); + session_pro_backend_get_pro_status_response status_response = {}; + session_pro_backend_get_pro_status_response_free(&status_response); + REQUIRE(status_response.header.internal_ == nullptr); + + session_pro_backend_get_payment_details_response pay_response = {}; + session_pro_backend_get_payment_details_response_free(&pay_response); REQUIRE(pay_response.header.internal_ == nullptr); } @@ -789,70 +887,67 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { sizeof(rotating_pubkey.data)) == 0); } - // Get pro status + // Get pro status (the hot path: account status + single latest payment) { - session_pro_backend_request request_json = - session_pro_backend_get_pro_details_request_build( - master_privkey.data, - sizeof(master_privkey.data), - session::epoch_seconds(session::clock_now_s()), - 10'000); + session_pro_backend_request request_json = session_pro_backend_get_pro_status_request_build( + master_privkey.data, + sizeof(master_privkey.data), + session::epoch_seconds(session::clock_now_s())); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; REQUIRE(request_json.success); std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, - g_test_pro_backend_dev_server_url + "/get_pro_details", + g_test_pro_backend_dev_server_url + "/get_pro_status", std::string_view(request_json.data.data, request_json.data.size)); // Parse response - session_pro_backend_get_pro_details_response response = - session_pro_backend_get_pro_details_response_parse( + session_pro_backend_get_pro_status_response response = + session_pro_backend_get_pro_status_response_parse( response_json.data(), response_json.size()); scope_exit response_free{ - [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; + [&]() { session_pro_backend_get_pro_status_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); if (response.header.error) UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); REQUIRE(std::string_view(response.status) == "active"); - REQUIRE(response.items_count > 0); + REQUIRE(response.has_latest_payment); } - // Get pro status without history + // Get payment history (paginated) -- newest page { session_pro_backend_request request_json = - session_pro_backend_get_pro_details_request_build( + session_pro_backend_get_payment_details_request_build( master_privkey.data, sizeof(master_privkey.data), session::epoch_seconds(session::clock_now_s()), - 0); + 100, + ""); scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; REQUIRE(request_json.success); std::string response_json = curl_do_basic_blocking_post_request( curl, curl_headers, - g_test_pro_backend_dev_server_url + "/get_pro_details", + g_test_pro_backend_dev_server_url + "/get_payment_details", std::string_view(request_json.data.data, request_json.data.size)); // Parse response - session_pro_backend_get_pro_details_response response = - session_pro_backend_get_pro_details_response_parse( + session_pro_backend_get_payment_details_response response = + session_pro_backend_get_payment_details_response_parse( response_json.data(), response_json.size()); scope_exit response_free{ - [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; + [&]() { session_pro_backend_get_payment_details_response_free(&response); }}; INFO("ERROR: JSON response: " << response_json.c_str()); if (response.header.error) UNSCOPED_INFO("ERROR: " << response.header.error); REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(std::string_view(response.status) == "active"); - REQUIRE(response.items_count == 0); + REQUIRE(response.items_count > 0); + REQUIRE(response.payments_total > 0); } // Add _another_ payment, same details From bac43bbd3d5c293da9188015a975c2c0d25f511c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 14:30:59 -0300 Subject: [PATCH 137/193] pro_backend: drop stale backend server.py link from the header preamble The doc preamble linked a pinned old commit of server.py on the Doy-lee session-pro-backend fork; the backend has since merged upstream, making the fork+commit reference doubly stale. Cite the authoritative pro-wire-protocol.md spec instead, matching the stable branch's wording. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index cca64c881..cd670633e 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -13,9 +13,9 @@ #include /// Helper functions to construct payloads to communicate with the Session Pro Backend. The data -/// structures here are largely bindings to the endpoints exposed on the Session Pro Backend: -/// -/// https://github.com/Doy-lee/session-pro-backend/blob/06e82c9d5b5a0a881d12d0182358219a4081acf5/server.py#L2 +/// structures here are largely bindings to the endpoints exposed on the Session Pro Backend; the +/// wire format they produce and parse is byte-pinned in pro-wire-protocol.md (the authoritative +/// spec). /// /// The high level summary of the functionality in this file. Clients can: /// From 1867b2bc3f94c139eae5c00995dc88eb9acd3fe0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 14:44:16 -0300 Subject: [PATCH 138/193] pro_backend: drop stale "Delta #14" spec citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend removed the "Delta N" change-history annotations from pro-wire-protocol.md (the spec now reflects only the current state), so those citations no longer resolve. Drop the "Delta #14" references from the `plan` billing-period doc comments, keeping the still-valid §1 spec reference (the plan grammar lives in §1). Comment-only; all other §-references verified against the current spec. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/pro_backend.h | 6 ++---- include/session/pro_backend.hpp | 8 ++++---- src/pro_backend.cpp | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/include/session/pro_backend.h b/include/session/pro_backend.h index 951c54d41..24c5209b7 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -176,8 +176,7 @@ void session_pro_backend_get_pro_revocations_response_free( session_pro_backend_get_pro_revocations_response* response); /// Unit of a parsed billing period (`plan_unit` below); mirrors C++ -/// session::pro_backend::ProPlanUnit (same order). A closed set (pro-wire-protocol.md §1 / Delta -/// #14). +/// session::pro_backend::ProPlanUnit (same order). A closed set (pro-wire-protocol.md §1). typedef enum SESSION_PRO_BACKEND_PLAN_UNIT { SESSION_PRO_BACKEND_PLAN_UNIT_SECOND, SESSION_PRO_BACKEND_PLAN_UNIT_DAY, @@ -191,8 +190,7 @@ typedef struct session_pro_backend_pro_payment_item { /// Opaque payment lifecycle status code (e.g. "unredeemed"/"redeemed"/"expired"/"revoked"); /// unknown values pass through as-is. NUL-terminated; points into the response's `internal_`. const char* status; - /// Parsed billing period (pro-wire-protocol.md §1 / Delta #14). `plan_count` is the period - /// count + /// Parsed billing period (pro-wire-protocol.md §1). `plan_count` is the period count /// (>= 1 for periodic units); for `plan_unit == ..._LIFETIME` it is 0 and not meaningful /// (switch on `plan_unit`, never render " " for lifetime). int plan_count; diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index cd670633e..e6af1cded 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -323,8 +323,8 @@ ProRequest payment_details_request( uint32_t limit, std::string_view before); -/// Unit of a billing period (the `unit` of a parsed `plan` code, pro-wire-protocol.md §1 / Delta -/// #14). A closed set: the backend emits only these, so an unrecognized code is a protocol error. +/// Unit of a billing period (the `unit` of a parsed `plan` code, pro-wire-protocol.md §1). A closed +/// set: the backend emits only these, so an unrecognized code is a protocol error. enum class ProPlanUnit { second, day, week, month, year, lifetime }; /// A parsed `plan` billing-period code. The `unit` is preserved exactly as transmitted and never @@ -336,7 +336,7 @@ struct ProPlanPeriod { ProPlanUnit unit; }; -/// Parse a `plan` billing-period code (pro-wire-protocol.md §1, Delta #14): "" with N a +/// Parse a `plan` billing-period code (pro-wire-protocol.md §1): "" with N a /// positive integer (no leading zeros) and unit one of s/d/w/m/y (second/day/week/month/year), or /// the literal "lifetime". Single-unit only ("1y6m" is invalid). Returns std::nullopt for any /// non-conforming code (the caller treats that as a protocol error). @@ -352,7 +352,7 @@ struct ProPaymentItem { std::string status; /// The parsed billing period that was purchased (e.g. "1m" -> {1, month}, "lifetime" -> {0, - /// lifetime}). libsession parses the closed `plan` grammar (§1 / Delta #14); the client just + /// lifetime}). libsession parses the closed `plan` grammar (§1); the client just /// localizes the display. The unit is preserved as transmitted, never canonicalized. ProPlanPeriod plan; diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index ff65721fb..671fa3631 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -269,7 +269,7 @@ std::span visible_platforms() { } std::optional parse_plan_period(std::string_view code) { - // pro-wire-protocol.md §1 / Delta #14: closed grammar. "lifetime", or "" with N a + // pro-wire-protocol.md §1: closed grammar. "lifetime", or "" with N a // positive integer (no leading zeros) and unit one of s/d/w/m/y. Single-unit only. if (code == "lifetime") return ProPlanPeriod{0, ProPlanUnit::lifetime}; @@ -614,7 +614,7 @@ namespace { ProPaymentItem item = {}; item.status = std::move(status); - // `plan` is parsed (closed grammar, §1 / Delta #14); an unrecognized code is a protocol + // `plan` is parsed (closed grammar, §1); an unrecognized code is a protocol // error (handled above). payment_provider / payment_id are opaque strings that pass through // as-is. if (plan) From 43862ca367c55cc68879304f19e9e851f8129748 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 19:09:16 -0300 Subject: [PATCH 139/193] Add live Pro-backend integration tests ([pro_live]) + CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up an ephemeral session-pro-backend (throwaway postgres + flask with provider egress stubbed) and drive all Pro endpoints through libsession's C++ API over real HTTP (direct + zero-hop onion v4), asserting every request round-trips, the backend accepts each signed digest, responses parse, and issued proofs verify against the backend's live signing key. This catches wire-contract / lockstep drift the self-consistent unit tests structurally cannot. - tests/pro_backend/run-dev-backend.sh: hermetic launcher (initdb/pg_ctl + ephemeral signing key + flask, polls /status, runs testAll, tears down). - tests/pro_backend/seed_payment.py: injects witnessed-unredeemed payments and revocations straight into the DB via the backend's own helpers. - test_pro_backend.cpp: replace the legacy C-API [dev_server] block with a transport-abstracted [pro_live] suite: /status round-trip, full lifecycle flow (both providers, both transports), get_pro_revocations. The lifecycle flow covers the Delta #15 endpoint split — get_pro_status + paginated get_payment_details, including an opaque next_cursor round-trip. - .drone.jsonnet: "Debian sid (Pro backend live)" pipeline running [pro_live]. Tracks Delta #12 envelope, #13 sign-the-message-directly, #14 parsed plan, #15 get_pro_status/get_payment_details split, and the generations-table revocation reshape. Co-Authored-By: Claude Opus 4.8 (1M context) --- .drone.jsonnet | 101 +++- tests/pro_backend/run-dev-backend.sh | 101 ++++ tests/pro_backend/seed_payment.py | 155 ++++++ tests/test_pro_backend.cpp | 692 ++++++++++++++++----------- 4 files changed, 741 insertions(+), 308 deletions(-) create mode 100755 tests/pro_backend/run-dev-backend.sh create mode 100644 tests/pro_backend/seed_payment.py diff --git a/.drone.jsonnet b/.drone.jsonnet index 8dc1dfbcf..53ee10e15 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -38,6 +38,18 @@ local add_stf_repo(image) = [ 'eatmydata ' + apt_get_quiet + ' update', ]; +// Fresh-container apt bootstrap shared by every post-build test step: eatmydata, optional STF repo, +// upgrade, then install `pkgs`. When `pkgs` is empty the repo/upgrade block is skipped entirely +// (only eatmydata is installed), matching the original inline behaviour. +local apt_setup(image, pkgs, stf_repo=true) = + [apt_get_quiet + ' install -y eatmydata'] + + (if std.length(pkgs) > 0 then + (if stf_repo then add_stf_repo(image) else []) + [ + 'eatmydata ' + apt_get_quiet + ' update', + 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', + 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', pkgs), + ] else []); + // Do something on a debian-like system local debian_pipeline(name, image, @@ -102,6 +114,7 @@ local debian_build(name, stf_repo=true, kitware_repo=''/* ubuntu codename, if wanted */, extra_setup=[], + extra_steps=[], allow_fail=false) = debian_pipeline( name, @@ -131,21 +144,12 @@ local debian_build(name, image: image, pull: 'always', [if allow_fail then 'failure']: 'ignore', - commands: - [apt_get_quiet + ' install -y eatmydata'] + ( - if std.length(test_deps) > 0 then ( - if stf_repo then add_stf_repo(image) else [] - ) + [ - 'eatmydata ' + apt_get_quiet + ' update', - 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', test_deps), - ] else [] - ) + [ - 'cd build', - './tests/testLogging --colour-mode ansi -d yes', - './tests/testAll --colour-mode ansi -d yes', - ], - }] else []) + commands: apt_setup(image, test_deps, stf_repo=stf_repo) + [ + 'cd build', + './tests/testLogging --colour-mode ansi -d yes', + './tests/testAll --colour-mode ansi -d yes', + ], + }] else []) + extra_steps ); // windows cross compile on debian local windows_cross_pipeline(name, @@ -201,17 +205,62 @@ local live_test_step(image, mode) = { name: 'live tests (' + mode + ')', image: image, pull: 'always', - commands: - [apt_get_quiet + ' install -y eatmydata'] + - add_stf_repo(image) + [ - 'eatmydata ' + apt_get_quiet + ' update', - 'eatmydata ' + apt_get_quiet + ' dist-upgrade -y', - 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', default_test_deps), - 'cd build', - './tests/testLive --' + mode + ' --log-level warning --colour-mode ansi -d yes "[file]"', - ], + commands: apt_setup(image, default_test_deps) + [ + 'cd build', + './tests/testLive --' + mode + ' --log-level warning --colour-mode ansi -d yes "[file]"', + ], }; +// Live Pro-backend integration test: build testAll with the dev-server hook, stand up an ephemeral +// backend (throwaway postgres + flask, provider_dry_run) via tests/pro_backend/run-dev-backend.sh, +// and run the [pro_live] suite against it. The backend is a separate Python service, checked out at +// a pinned ref (repos stay decoupled — CI keeps its own clone). +local pro_backend_git = 'https://github.com/session-foundation/session-pro-backend.git'; +local pro_backend_ref = 'phase2-foundation'; // bump/pin as the backend advances in lockstep + +// Extra apt packages (beyond the standard test deps) the live Pro-backend step needs: postgres, the +// session-router key tool, the backend's python3-* runtime, and clone/venv tooling. +local pro_backend_pkgs = [ + 'postgresql', + 'session-router-bin', + 'python3-session-util', + 'python3-nacl', + 'python3-psycopg', + 'python3-flask', + 'python3-venv', + 'python3-pip', + 'git', + 'curl', + 'ca-certificates', +] + default_test_deps; + +local pro_backend_live_pipeline(name, image) = debian_build( + name, + image, + // tests=false suppresses the default no-server testAll step; WITH_TESTS=ON (later -D wins) still + // builds testAll so the live step below can run it. The step goes through debian_build's own + // extra_steps hook -- no manual `steps` surgery. + tests=false, + cmake_extra='-DWITH_TESTS=ON -DTEST_PRO_BACKEND_WITH_DEV_SERVER=ON ', + extra_steps=[{ + name: 'pro-backend live tests', + image: image, + pull: 'always', + commands: apt_setup(image, pro_backend_pkgs) + [ + // Check out + provision the backend (venv reuses the apt-installed python3-* via system site + // packages; only the pip-only provider libraries are installed). + 'git clone --depth=1 --branch ' + pro_backend_ref + ' ' + pro_backend_git + ' /opt/pro-backend', + 'python3 -m venv --system-site-packages /opt/pro-backend/.venv', + '/opt/pro-backend/.venv/bin/pip install --no-input app-store-server-library google-cloud-pubsub google-api-python-client', + // initdb/pg_ctl refuse to run as root, so run the whole launcher as a non-root user; its temp + // dirs (postgres cluster, key) are created by that user, so initdb is satisfied. + 'useradd -m pro && chown -R pro /opt/pro-backend', + 'ws="$(pwd)"', + 'su pro -c "cd $ws && SESSION_PRO_BACKEND_DIR=/opt/pro-backend PRO_BACKEND_PORT=5544 tests/pro_backend/run-dev-backend.sh ./build/tests/testAll --colour-mode ansi -d yes \'[pro_live]\'"', + ], + }], +); + local clang(version) = debian_build( 'Debian sid/clang-' + version, docker_base + 'debian-sid-clang', @@ -380,6 +429,10 @@ local static_build(name, live_test_step(live_image, 'direct'), ], }, + + // Live Pro-backend integration tests (ephemeral backend + [pro_live]). + pro_backend_live_pipeline('Debian sid (Pro backend live)', docker_base + 'debian-sid'), + debian_build('Debian sid/Debug', docker_base + 'debian-sid', build_type='Debug'), debian_build('Debian testing', docker_base + 'debian-testing'), clang(19), diff --git a/tests/pro_backend/run-dev-backend.sh b/tests/pro_backend/run-dev-backend.sh new file mode 100755 index 000000000..438686813 --- /dev/null +++ b/tests/pro_backend/run-dev-backend.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# Stand up an ephemeral, hermetic Session Pro backend and run the libsession [pro_live] integration +# tests against it, then tear everything down. No CTest, no Docker: this script IS the entry point. +# +# run-dev-backend.sh [catch2 args...] +# +# It: +# 1. generates an ephemeral Ed25519 signing key (session-router-config -k) +# 2. spins up a throwaway PostgreSQL cluster (initdb + pg_ctl, unix socket in a temp dir) +# 3. starts the backend (flask --app main, KEY_PATH, no dev mode) +# 4. polls /status until ready +# 5. runs testAll against it (default filter [pro_live]) +# 6. tears it all down on exit +# +# Env overrides: +# SESSION_PRO_BACKEND_DIR backend repo (default ~/src/session-pro-backend-itest) +# PRO_BACKEND_PORT flask + pg port (default 5055) +# +# NB: SESSION_PRO_BACKEND_DIR should be our OWN tracking clone of the backend (with its own .venv), +# NOT the backend agent's live working repo -- that repo is actively edited and may be mid-change. +# Update the clone deliberately (git -C pull) when we want to track the backend's latest. +# +set -euo pipefail + +TESTALL="${1:?usage: run-dev-backend.sh [catch2 args...]}" +shift || true +BACKEND_DIR="${SESSION_PRO_BACKEND_DIR:-$HOME/src/session-pro-backend-itest}" +PORT="${PRO_BACKEND_PORT:-5055}" +PGBIN="$(ls -d /usr/lib/postgresql/*/bin 2>/dev/null | sort -V | tail -1)" +VENV_PY="$BACKEND_DIR/.venv/bin/python" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +[ -x "$TESTALL" ] || { echo "!! testAll not executable: $TESTALL" >&2; exit 2; } +[ -x "$VENV_PY" ] || { echo "!! backend venv python missing: $VENV_PY" >&2; exit 2; } +[ -n "$PGBIN" ] || { echo "!! postgres bin dir not found under /usr/lib/postgresql" >&2; exit 2; } +command -v session-router-config >/dev/null || { echo "!! session-router-config not on PATH" >&2; exit 2; } + +WORK="$(mktemp -d)" +PGDATA="$WORK/pgdata" +KEYFILE="$WORK/backend.key" +FLASK_LOG="$WORK/flask.log" +PG_LOG="$WORK/pg.log" +FLASK_PID="" + +cleanup() { + rc=$? + [ -n "$FLASK_PID" ] && kill "$FLASK_PID" 2>/dev/null || true + [ -d "$PGDATA" ] && "$PGBIN/pg_ctl" -D "$PGDATA" -m immediate stop >/dev/null 2>&1 || true + rm -rf "$WORK" + exit $rc +} +trap cleanup EXIT INT TERM + +echo ">> ephemeral signing key: $KEYFILE" +session-router-config -f -k "$KEYFILE" >/dev/null + +echo ">> initdb: $PGDATA" +"$PGBIN/initdb" -D "$PGDATA" --auth-local=trust --no-instructions -U "$USER" >/dev/null + +echo ">> start postgres (unix socket in $WORK, port $PORT)" +"$PGBIN/pg_ctl" -D "$PGDATA" -l "$PG_LOG" \ + -o "-c listen_addresses= -k $WORK -p $PORT" -w start >/dev/null +createdb -h "$WORK" -p "$PORT" -U "$USER" pro_backend + +export SESH_PRO_BACKEND_DB_URL="postgresql:///pro_backend?host=$WORK&port=$PORT&user=$USER" +export SESH_PRO_BACKEND_KEY_PATH="$KEYFILE" +# Stub all payment-provider egress so a real add_pro_payment redeems (google/app_store/rangeproof) +# without contacting Google/Apple. Landed backend-side (base.PROVIDER_DRY_RUN). No platform creds and +# no with_platform_* needed: the provider comes from the seeded row, not from platform config. +export SESH_PRO_BACKEND_PROVIDER_DRY_RUN=1 + +echo ">> start flask on 127.0.0.1:$PORT (log: $FLASK_LOG)" +( cd "$BACKEND_DIR" && exec "$VENV_PY" -m flask --app main run --port "$PORT" ) >"$FLASK_LOG" 2>&1 & +FLASK_PID=$! + +echo ">> waiting for /status ..." +ready="" +for _ in $(seq 1 60); do + if curl -fsS "http://127.0.0.1:$PORT/status" >/dev/null 2>&1; then ready=1; break; fi + if ! kill -0 "$FLASK_PID" 2>/dev/null; then + echo "!! flask exited during startup:" >&2; cat "$FLASK_LOG" >&2; exit 3 + fi + sleep 0.5 +done +[ "$ready" = 1 ] || { echo "!! backend not ready in time:" >&2; cat "$FLASK_LOG" >&2; exit 3; } +echo ">> backend ready" + +# Expose the seeding helper to the test process (it shells out to inject witnessed payments / +# revocations directly into the DB). DB URL is already exported above and inherited by testAll. +export PRO_BACKEND_DIR="$BACKEND_DIR" +export PRO_SEED_PYTHON="$VENV_PY" +export PRO_SEED_SCRIPT="$SCRIPT_DIR/seed_payment.py" + +echo ">> running: $TESTALL ${*:-[pro_live]}" +set +e +"$TESTALL" --pro-backend-dev-server-url="http://127.0.0.1:$PORT" "${@:-[pro_live]}" +rc=$? +set -e +echo ">> testAll exited $rc" +exit $rc diff --git a/tests/pro_backend/seed_payment.py b/tests/pro_backend/seed_payment.py new file mode 100644 index 000000000..7fb23de69 --- /dev/null +++ b/tests/pro_backend/seed_payment.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Seed backend DB state for the libsession [pro_live] integration tests. + +Injects the "as if a provider purchase notification was witnessed" state straight into postgres, so +a subsequent *real* add_pro_payment from the client redeems it -- no dev-mode, no fake provider +egress (provider_dry_run stubs the egress; this just supplies the witnessed row the egress would +otherwise have created). This mirrors what the backend's own test.py does in-process +(test_provider_dry_run_redeems_google_without_egress / test_server_add_payment_flow), except we run +out-of-process against the same DB. + +Run with the backend clone's venv python. Requires env: + SESH_PRO_BACKEND_DB_URL postgres DSN (same one the backend uses) + PRO_BACKEND_DIR path to the backend clone (for importing base/backend/db) + +Usage: + seed_payment.py payment --provider {google_play,app_store,rangeproof} --payment-id ID + --master-pubkey HEX64 [--plan 1m|3m|1y] [--expiry-days N] + seed_payment.py revoke --provider {google_play,app_store} --payment-id ID +""" +import argparse +import datetime +import os +import sys + +sys.path.insert(0, os.environ["PRO_BACKEND_DIR"]) + +import nacl.signing # noqa: E402 + +import backend # noqa: E402 +import base # noqa: E402 +import db # noqa: E402 + + +def _open(): + err = base.ErrorSink() + url = os.environ["SESH_PRO_BACKEND_DB_URL"] + # bootstrap_db no longer takes/uses an ErrorSink (it raises on failure); the err sink is still + # used by the add_unredeemed_payment / add_*_revocation calls below. + engine = backend.bootstrap_db(database_url=url) + if engine is None: + sys.exit("seed: bootstrap_db failed") + return engine, engine.getconn(), err + + +def _payment_tx(provider, payment_id): + tx = base.PaymentProviderTransaction() + if provider == "google_play": + tx.provider = base.PaymentProvider.GooglePlayStore + # Google's composite payment_id is "|", split once on the first '|' + # (matches the backend's own split). + token, _, order = payment_id.partition("|") + tx.google_payment_token = token + tx.google_order_id = order + elif provider == "app_store": + tx.provider = base.PaymentProvider.iOSAppStore + # The wire payment_id is the transaction id; the backend row also wants the original tx id + # (same value for a first purchase) and a web-line order id (empty). + tx.apple_tx_id = payment_id + tx.apple_original_tx_id = payment_id + tx.apple_web_line_order_tx_id = "" + elif provider == "rangeproof": + tx.provider = base.PaymentProvider.Rangeproof + tx.rangeproof_order_id = payment_id + else: + sys.exit(f"seed: unknown provider {provider}") + return tx + + +def _obfuscated_id(provider, vk): + if provider == "google_play": + return backend.google_obfuscated_account_id_from_master_pkey(vk) + if provider == "app_store": + return backend.apple_obfuscated_account_id_from_master_pkey(vk) + return b"" + + +def cmd_payment(args): + engine, conn, err = _open() + try: + vk = nacl.signing.VerifyKey(bytes.fromhex(args.master_pubkey)) + now = datetime.datetime.now(datetime.timezone.utc) + plan = base.ProPlan.from_string(args.plan) + if plan is None: + sys.exit(f"seed: bad plan {args.plan}") + # add_unredeemed_payment is @db.transactional: pass the connection positionally (it opens a + # transaction for the call). The wrapper's first param is named `tx`, so `conn=` won't bind. + backend.add_unredeemed_payment( + conn, + payment_tx=_payment_tx(args.provider, args.payment_id), + plan=plan, + purchased_at=now, + expires_at=base.round_datetime_to_next_day(now) + + datetime.timedelta(days=args.expiry_days), + platform_refund_expires_at=base.EPOCH, + platform_obfuscated_account_id=_obfuscated_id(args.provider, vk), + err=err, + ) + if err.msg_list: + sys.exit(f"seed: add_unredeemed_payment failed: {err.msg_list}") + finally: + engine.putconn(conn) + engine.close() + + +def cmd_revoke(args): + # Revocation is keyed by the *provider* payment identifier (the generations-table refactor + # replaced the old master-pkey-keyed set_revocation_tx). Revoking the payment marks its + # generation revoked (revoked_at set, terminal), which is what get_pro_revocations surfaces. + engine, conn, err = _open() + try: + now = datetime.datetime.now(datetime.timezone.utc) + with db.transaction(conn) as tx: + if args.provider == "app_store": + ok = backend.add_apple_revocation( + tx, apple_original_tx_id=args.payment_id, revoke_at=now, err=err + ) + elif args.provider == "google_play": + token, _, _ = args.payment_id.partition("|") + ok = backend.add_google_revocation( + tx, google_payment_token=token, revoke_at=now, err=err + ) + else: + sys.exit(f"seed: revoke unsupported for provider {args.provider}") + if err.msg_list or not ok: + sys.exit(f"seed: revoke failed (ok={ok}): {err.msg_list}") + finally: + engine.putconn(conn) + engine.close() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("payment", help="seed a witnessed-unredeemed payment") + p.add_argument("--provider", required=True, + choices=["google_play", "app_store", "rangeproof"]) + p.add_argument("--payment-id", required=True) + p.add_argument("--master-pubkey", required=True, help="64 hex chars") + p.add_argument("--plan", default="1m") + p.add_argument("--expiry-days", type=int, default=30) + p.set_defaults(fn=cmd_payment) + + r = sub.add_parser("revoke", help="revoke the generation for a provider payment") + r.add_argument("--provider", required=True, choices=["google_play", "app_store"]) + r.add_argument("--payment-id", required=True, + help="same id used to seed the payment (app_store: tx id; google: |)") + r.set_defaults(fn=cmd_revoke) + + args = ap.parse_args() + args.fn(args) + + +if __name__ == "__main__": + main() diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index e1eeee81c..a8ff252e0 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -731,6 +731,13 @@ TEST_CASE("Pro Backend parse_plan_period (closed grammar)", "[pro_backend]") { #if defined(TEST_PRO_BACKEND_WITH_DEV_SERVER) #include +#include +#include +#include +#include +#include +#include + size_t curl_perform_callback(void* contents, size_t size, size_t nmemb, void* userp) { size_t total = size * nmemb; auto* response_json = static_cast(userp); @@ -761,310 +768,427 @@ std::string curl_do_basic_blocking_post_request( return result; } -TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { - // Setup: Generate keys and payment token hash - cbytes32 master_pubkey = {}; - cbytes64 master_privkey = {}; - crypto_sign_ed25519_keypair(master_pubkey.data, master_privkey.data); +// --------------------------------------------------------------------------- +// Dev-server transport helpers. +// +// A transport delivers a Pro request body to a named endpoint on the running dev backend and +// returns the response body (JSON). We drive the identical libsession C++ request builders/parsers +// through each, so the same test flow can run over the two production request modes on pfs/dev: +// +// * `make_direct_http_transport` -- plain HTTP POST to `/`. This is the faithful +// representation of session-router mode, where libsession opens a tunnel to a private local +// address and makes unencrypted HTTP through it. +// * `make_onion_v4_transport` -- builds only the innermost v4 onion piece (a zero-hop +// `onionreq::Builder` addressed to the backend's x25519 key), POSTs it to +// `/oxen/v4/lsrpc`, and decrypts the reply with `onionreq::ResponseParser`. This is +// onion-request mode. (The stable backport uses only this transport.) +// +// Both move bytes with libcurl. The onion path does not build a real multi-hop route (that is a +// separate, stable protocol tested elsewhere) -- only the final destination-encrypted layer, which +// is exactly what `/oxen/v4/lsrpc` decrypts. +// --------------------------------------------------------------------------- + +// A transport delivers a request (endpoint + content_type + opaque body) to the backend and returns +// the response body. `content_type` is what libsession's *_request() builders emit; clients must +// relay it verbatim (libsession owns the wire encoding and it may change). +using PostFn = std::function; + +// Drive a transport straight from a built ProRequest. +static std::string send(const PostFn& transport, const ProRequest& r) { + return transport(r.endpoint, r.content_type, r.data); +} - cbytes32 rotating_pubkey = {}; - cbytes64 rotating_privkey = {}; - crypto_sign_ed25519_keypair(rotating_pubkey.data, rotating_privkey.data); +// Split e.g. "http://127.0.0.1:5000" into protocol / host / port. +struct ParsedBackendUrl { + std::string protocol; + std::string host; + uint16_t port; +}; +static ParsedBackendUrl parse_backend_url(std::string_view url) { + ParsedBackendUrl r; + auto pos = url.find("://"); + r.protocol = pos == std::string_view::npos ? "http" : std::string{url.substr(0, pos)}; + std::string_view rest = pos == std::string_view::npos ? url : url.substr(pos + 3); + if (auto slash = rest.find('/'); slash != std::string_view::npos) + rest = rest.substr(0, slash); + if (auto colon = rest.find(':'); colon != std::string_view::npos) { + r.host = std::string{rest.substr(0, colon)}; + r.port = static_cast(std::stoi(std::string{rest.substr(colon + 1)})); + } else { + r.host = std::string{rest}; + r.port = r.protocol == "https" ? 443 : 80; + } + return r; +} + +static std::string join_url(std::string_view base, std::string_view path) { + std::string url{base}; + if (!url.empty() && url.back() == '/') + url.pop_back(); + if (!path.empty() && path.front() != '/') + url += '/'; + url += path; + return url; +} + +// Plain HTTP POST to `/` (session-router tunnel mode), relaying content_type. +static PostFn make_direct_http_transport(CURL* curl, std::string base_url) { + return [curl, base_url = std::move(base_url)]( + std::string_view endpoint, + std::string_view content_type, + std::string_view body) { + curl_slist* headers = + curl_slist_append(nullptr, ("Content-Type: " + std::string{content_type}).c_str()); + scope_exit free_headers{[&]() { curl_slist_free_all(headers); }}; + return curl_do_basic_blocking_post_request( + curl, headers, join_url(base_url, endpoint), body); + }; +} - const auto DEV_BACKEND_PUBKEY = - "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"_hex_b; +// Innermost v4 onion piece to `/oxen/v4/lsrpc` (onion-request mode). `backend_ed25519_pubkey` +// is the backend's signing pubkey (fetched from /status); its x25519 form is the destination key. +static PostFn make_onion_v4_transport( + CURL* curl, std::string base_url, std::span backend_ed25519_pubkey) { + using namespace session::network; + using namespace session::onionreq; + + auto parts = parse_backend_url(base_url); + x25519_pubkey backend_x25519 = compute_x25519_pubkey(backend_ed25519_pubkey); + + return [curl, base_url = std::move(base_url), parts, backend_x25519]( + std::string_view endpoint, + std::string_view content_type, + std::string_view body) { + // Relay content_type as the inner (proxied) request's Content-Type. + std::vector> inner_headers{ + {"Content-Type", std::string{content_type}}}; + ServerDestination dest{ + parts.protocol, + parts.host, + backend_x25519, + parts.port, + std::move(inner_headers), + "POST"}; + + Builder builder{ + network_destination{dest}, + std::string{endpoint}, + /*nodes=*/{}, + EncryptType::xchacha20}; + + std::vector body_bytes{ + reinterpret_cast(body.data()), + reinterpret_cast(body.data()) + body.size()}; + std::vector blob = builder.generate_onion_blob(std::move(body_bytes)); + + // The lsrpc body is the raw encrypted onion blob; post it as opaque bytes. (An unheadered + // curl POST defaults to application/x-www-form-urlencoded, which makes Werkzeug consume the + // body into request.form and leaves request.data empty -> the backend 400s.) + curl_slist* octet_headers = + curl_slist_append(nullptr, "Content-Type: application/octet-stream"); + scope_exit octet_free{[&]() { curl_slist_free_all(octet_headers); }}; + + std::string encrypted_response = curl_do_basic_blocking_post_request( + curl, + octet_headers, + join_url(base_url, "/oxen/v4/lsrpc"), + std::string_view{reinterpret_cast(blob.data()), blob.size()}); + + // ResponseParser reuses the builder's saved ephemeral keypair to decrypt the reply. + ResponseParser parser{builder}; + DecryptedResponse resp = parser.decrypted_response(encrypted_response); + return resp.body.value_or(std::string{}); + }; +} - // Setup CURL +// Live-backend tests (tag [pro_live]), run against an ephemeral dev backend stood up by +// tests/pro_backend/run-dev-backend.sh: they drive the C++ API over the real request path against a +// real backend (redemption via provider_dry_run + a DB-seeded witnessed payment). This first case +// validates the infrastructure + both transports end-to-end via /status. +TEST_CASE("Pro backend live /status round-trip", "[pro_backend][pro_live]") { curl_global_init(CURL_GLOBAL_DEFAULT); scope_exit curl_cleanup{[&]() { curl_global_cleanup(); }}; - CURL* curl = curl_easy_init(); REQUIRE(curl); scope_exit curl_free{[&]() { curl_easy_cleanup(curl); }}; - struct curl_slist* curl_headers = nullptr; - curl_headers = curl_slist_append(curl_headers, "Content-Type: application/json"); - REQUIRE(curl_headers); - scope_exit curl_headers_free{[&]() { curl_slist_free_all(curl_headers); }}; + const std::string& base_url = g_test_pro_backend_dev_server_url; + + // Parse the /status success envelope and return the signing pubkey (hex). + auto status_pubkey_hex = [](std::string_view response_json) -> std::string { + INFO("/status response: " << response_json); + auto j = nlohmann::json::parse(response_json); + REQUIRE(j.at("status").get() == "ok"); + const auto& result = j.at("result"); + CHECK(result.contains("version")); + CHECK(result.contains("timestamp")); + auto pubkey = result.at("signing_pubkey").get(); + REQUIRE(pubkey.size() == 64); + REQUIRE(oxenc::is_hex(pubkey)); + return pubkey; + }; - // Add pro payment - session_protocol_pro_proof first_pro_proof = {}; - { - std::array fake_google_payment_token; - randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::array fake_google_order_id; - randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - // Google's composite payment_id is "|"; libsession treats the - // whole thing as one opaque value. - std::string fake_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + - oxenc::to_hex(fake_google_order_id); + // 1) Fetch /status over the direct (session-router-style) HTTP transport. + PostFn direct = make_direct_http_transport(curl, base_url); + std::string direct_pubkey = status_pubkey_hex(direct("status", "application/json", "")); - session_pro_backend_request request_json = - session_pro_backend_add_pro_payment_request_build( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - reinterpret_cast(fake_payment_id.data()), - fake_payment_id.size()); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - // Do curl request - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/add_pro_payment", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_pro_proof_response response = - session_pro_backend_pro_proof_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; - - if (response.header.error) - INFO("ERROR: " << response.header.error); - - // Verify response - first_pro_proof = response.proof; - INFO("Signature: " << oxenc::to_hex( - first_pro_proof.sig.data, std::end(first_pro_proof.sig.data)) - << ", backend pubkey: " << oxenc::to_hex(DEV_BACKEND_PUBKEY) - << ", response: " << response_json); - REQUIRE(session_protocol_pro_proof_verify_signature( - &first_pro_proof, - reinterpret_cast(DEV_BACKEND_PUBKEY.data()), - DEV_BACKEND_PUBKEY.size())); - REQUIRE(std::memcmp( - response.proof.rotating_pubkey.data, - rotating_pubkey.data, - sizeof(rotating_pubkey.data)) == 0); - } + // 2) Build the onion transport from the pubkey we just fetched, then re-fetch /status over it: + // this exercises the full v4 onion encrypt/decrypt round-trip against the real backend. + auto pubkey_bytes = oxenc::from_hex(direct_pubkey); + REQUIRE(pubkey_bytes.size() == 32); + std::array ed_pubkey{}; + std::memcpy(ed_pubkey.data(), pubkey_bytes.data(), ed_pubkey.size()); - // Authorise new key - { - int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); - session_pro_backend_request request_json = - session_pro_backend_generate_pro_proof_request_build( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - now_unix_ts); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - // Do CURL request - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/generate_pro_proof", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_pro_proof_response response = - session_pro_backend_pro_proof_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; - - INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error) - UNSCOPED_INFO("ERROR: " << response.header.error); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - - // Verify response - session_protocol_pro_proof proof = response.proof; - REQUIRE(session_protocol_pro_proof_verify_signature( - &proof, - reinterpret_cast(DEV_BACKEND_PUBKEY.data()), - DEV_BACKEND_PUBKEY.size())); - REQUIRE(std::memcmp( - response.proof.rotating_pubkey.data, - rotating_pubkey.data, - sizeof(rotating_pubkey.data)) == 0); - } + PostFn onion = make_onion_v4_transport(curl, base_url, ed_pubkey); + std::string onion_pubkey = status_pubkey_hex(onion("status", "application/json", "")); - // Get pro status (the hot path: account status + single latest payment) - { - session_pro_backend_request request_json = session_pro_backend_get_pro_status_request_build( - master_privkey.data, - sizeof(master_privkey.data), - session::epoch_seconds(session::clock_now_s())); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/get_pro_status", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_get_pro_status_response response = - session_pro_backend_get_pro_status_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{ - [&]() { session_pro_backend_get_pro_status_response_free(&response); }}; - - INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error) - UNSCOPED_INFO("ERROR: " << response.header.error); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(std::string_view(response.status) == "active"); - REQUIRE(response.has_latest_payment); - } + // Both transports must report the same signing key. + CHECK(onion_pubkey == direct_pubkey); +} - // Get payment history (paginated) -- newest page - { - session_pro_backend_request request_json = - session_pro_backend_get_payment_details_request_build( - master_privkey.data, - sizeof(master_privkey.data), - session::epoch_seconds(session::clock_now_s()), - 100, - ""); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/get_payment_details", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_get_payment_details_response response = - session_pro_backend_get_payment_details_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{ - [&]() { session_pro_backend_get_payment_details_response_free(&response); }}; - - INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error) - UNSCOPED_INFO("ERROR: " << response.header.error); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.items_count > 0); - REQUIRE(response.payments_total > 0); - } +// Shell out to the python seeding helper (tests/pro_backend/seed_payment.py) to inject backend DB +// state (a witnessed-unredeemed payment, or a revocation) directly. The launcher exports +// PRO_SEED_PYTHON / PRO_SEED_SCRIPT / PRO_BACKEND_DIR and the shared SESH_PRO_BACKEND_DB_URL. Args +// are test-controlled, so the naive single-quote wrapping is sufficient. +static void run_seed_helper(const std::vector& args) { + const char* py = std::getenv("PRO_SEED_PYTHON"); + const char* script = std::getenv("PRO_SEED_SCRIPT"); + REQUIRE(py != nullptr); + REQUIRE(script != nullptr); + std::string cmd = std::string(py) + " " + script; + for (const auto& a : args) + cmd += " '" + a + "'"; + INFO("seed: " << cmd); + REQUIRE(std::system(cmd.c_str()) == 0); +} - // Add _another_ payment, same details - std::string another_payment_id; - { - std::array fake_google_payment_token; - randombytes_buf(fake_google_payment_token.data(), fake_google_payment_token.size()); - std::array fake_google_order_id; - randombytes_buf(fake_google_order_id.data(), fake_google_order_id.size()); - another_payment_id = "DEV." + oxenc::to_hex(fake_google_payment_token) + "|DEV." + - oxenc::to_hex(fake_google_order_id); +static std::array fetch_backend_pubkey(const PostFn& transport) { + auto j = nlohmann::json::parse(transport("status", "application/json", "")); + REQUIRE(j.at("status").get() == "ok"); + auto hex = j.at("result").at("signing_pubkey").get(); + auto bytes = oxenc::from_hex(hex); + REQUIRE(bytes.size() == 32); + std::array out{}; + std::memcpy(out.data(), bytes.data(), out.size()); + return out; +} - session_pro_backend_request request_json = - session_pro_backend_add_pro_payment_request_build( - master_privkey.data, - sizeof(master_privkey.data), - rotating_privkey.data, - sizeof(rotating_privkey.data), - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - reinterpret_cast(another_payment_id.data()), - another_payment_id.size()); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - // Do curl request - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/add_pro_payment", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_pro_proof_response response = - session_pro_backend_pro_proof_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{[&]() { session_pro_backend_pro_proof_response_free(&response); }}; - - // Verify response - session_protocol_pro_proof proof = response.proof; - REQUIRE(session_protocol_pro_proof_verify_signature( - &proof, - reinterpret_cast(DEV_BACKEND_PUBKEY.data()), - DEV_BACKEND_PUBKEY.size())); - REQUIRE(std::memcmp( - response.proof.rotating_pubkey.data, - rotating_pubkey.data, - sizeof(rotating_pubkey.data)) == 0); - } +// The core wire-contract flow: build *real* requests with the C++ API, send them over onion to a +// backend that redeems a witnessed (seeded) payment, and check every response parses + the issued +// proofs verify against the backend's signing key. Any signed-message drift on either side (field +// order, integer encoding, `\0` framing, domain prefix -- Delta #13 signs the message bytes +// directly, no hash) or a renamed JSON key breaks this. Runs the whole flow once per provider via +// SECTIONs: google_play exercises the composite "token|order_id" payment_id, app_store the plain tx +// id. Each SECTION uses fresh keys + a freshly-seeded payment, so the runs are independent on the +// shared ephemeral backend. +TEST_CASE("Pro backend live full flow", "[pro_backend][pro_live]") { + curl_global_init(CURL_GLOBAL_DEFAULT); + scope_exit curl_cleanup{[&]() { curl_global_cleanup(); }}; + CURL* curl = curl_easy_init(); + REQUIRE(curl); + scope_exit curl_free{[&]() { curl_easy_cleanup(curl); }}; - // Get revocation list - { - session_pro_backend_request request_json = - session_pro_backend_get_pro_revocations_request_build(0); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); + const std::string& base_url = g_test_pro_backend_dev_server_url; + PostFn direct = make_direct_http_transport(curl, base_url); + std::array backend_pubkey = fetch_backend_pubkey(direct); + PostFn onion = make_onion_v4_transport(curl, base_url, backend_pubkey); - // Do curl request - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/get_pro_revocations", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_get_pro_revocations_response response = - session_pro_backend_get_pro_revocations_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{ - [&]() { session_pro_backend_get_pro_revocations_response_free(&response); }}; - - // Verify response - INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error) - UNSCOPED_INFO("ERROR: " << response.header.error); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.ticket == 0); - REQUIRE(response.items_count == 0); + // Provider under test; the whole flow below re-runs once per SECTION. + std::string provider; + SECTION("google_play") { + provider = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY; + } + SECTION("app_store") { + provider = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE; + } + INFO("provider=" << provider); + + // Fresh account keys. + std::array master_pk{}, rotating_pk{}; + std::array master_sk{}, rotating_sk{}; + crypto_sign_ed25519_keypair(master_pk.data(), master_sk.data()); + crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); + std::string master_hex = oxenc::to_hex(master_pk); + + // Opaque per-provider payment_id: google is the composite "|" (backend splits + // on the first '|'); app_store is a bare transaction id. + std::string payment_id = provider == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY + ? master_hex.substr(0, 16) + "|GPA." + master_hex.substr(16, 12) + : "20000000" + master_hex.substr(0, 10); + + // Seed the witnessed-unredeemed payment; the client's real add_pro_payment (below) redeems it. + run_seed_helper( + {"payment", + "--provider", + provider, + "--payment-id", + payment_id, + "--master-pubkey", + master_hex, + "--plan", + "1m"}); + + auto now = session::clock_now_s(); + + // 1) add_pro_payment -> signed proof. + ProRequest add_req = add_payment_request( + master_sk, rotating_sk, provider, to_byte_span(payment_id.data(), payment_id.size())); + AddProPaymentResponse add = parse_add_payment(send(onion, add_req)); + INFO("add_pro_payment" << add.error.value_or("")); + REQUIRE(add.status == ResponseStatus::Ok); + CHECK(add.status == ResponseStatus::Ok); + CHECK(add.proof.verify_signature(backend_pubkey)); + CHECK(std::memcmp(add.proof.rotating_pubkey.data(), rotating_pk.data(), 32) == 0); + + // 1b) generate_pro_proof: pair a NEW rotating key to the same subscription -> a fresh proof. + std::array rotating2_pk{}; + std::array rotating2_sk{}; + crypto_sign_ed25519_keypair(rotating2_pk.data(), rotating2_sk.data()); + ProRequest gen_req = pro_proof_request(master_sk, rotating2_sk, now); + GenerateProProofResponse gen = parse_pro_proof(send(onion, gen_req)); + INFO("generate_pro_proof" << gen.error.value_or("")); + REQUIRE(gen.status == ResponseStatus::Ok); + CHECK(gen.status == ResponseStatus::Ok); + CHECK(gen.proof.verify_signature(backend_pubkey)); + CHECK(std::memcmp(gen.proof.rotating_pubkey.data(), rotating2_pk.data(), 32) == 0); + // Same subscription epoch -> the fresh proof shares the add-payment proof's revocation_tag. + CHECK(gen.proof.revocation_tag == add.proof.revocation_tag); + + // 2) get_pro_status (Delta #15): account ACTIVE, no refund yet, and the single latest payment + // is our redeemed one. + ProStatusResponse status = parse_pro_status(send(onion, pro_status_request(master_sk, now))); + INFO("get_pro_status " << status.error.value_or("")); + REQUIRE(status.status == ResponseStatus::Ok); + CHECK(status.user_status == "active"); + CHECK(status.refund_requested_at.time_since_epoch().count() == 0); // no refund requested yet + REQUIRE(status.latest_payment.has_value()); + CHECK(status.latest_payment->payment_id == payment_id); + CHECK(status.latest_payment->payment_provider == provider); + // plan is the parsed billing period (Delta #14): "1m" -> {count 1, month}. + CHECK(status.latest_payment->plan.count == 1); + CHECK(status.latest_payment->plan.unit == ProPlanUnit::month); + CHECK(status.latest_payment->status == "redeemed"); + + // 2b) get_payment_details (Delta #15): newest page (empty cursor) carries our payment. This + // master has exactly one payment, so it fits in one page and next_cursor is empty. + PaymentDetailsResponse det = + parse_payment_details(send(onion, payment_details_request(master_sk, now, 10, ""))); + INFO("get_payment_details " << det.error.value_or("")); + REQUIRE(det.status == ResponseStatus::Ok); + CHECK(det.payments_total >= 1); + CHECK(det.items.size() >= 1); + CHECK_FALSE(det.next_cursor.has_value()); // limit 10 >= 1 payment -> no further page + bool found = false; + for (const auto& item : det.items) { + if (item.payment_id == payment_id) { + found = true; + CHECK(item.payment_provider == provider); + CHECK(item.plan.count == 1); + CHECK(item.plan.unit == ProPlanUnit::month); + CHECK(item.status == "redeemed"); + } + } + CHECK(found); + + // 3) get_payment_details pagination + opaque-cursor round-trip: a limit-1 page returns one item + // and (being a full page) a next_cursor; re-requesting with that cursor echoed back verbatim + // must be accepted and return the next page. With a single payment on this master, page 2 is + // empty and terminates (next_cursor cleared). This is the real test of the opaque keyset + // cursor -- a tampered/synthesized cursor would be rejected, so a clean round-trip proves + // the client relayed it faithfully. + PaymentDetailsResponse page1 = + parse_payment_details(send(onion, payment_details_request(master_sk, now, 1, ""))); + REQUIRE(page1.status == ResponseStatus::Ok); + CHECK(page1.items.size() == 1); + if (det.payments_total == 1) { + REQUIRE(page1.next_cursor.has_value()); // a full page yields a cursor to try next + PaymentDetailsResponse page2 = parse_payment_details( + send(onion, payment_details_request(master_sk, now, 1, *page1.next_cursor))); + REQUIRE(page2.status == + ResponseStatus::Ok); // the opaque cursor was accepted (echo round-trip) + CHECK(page2.items.empty()); // no more payments + CHECK_FALSE(page2.next_cursor.has_value()); // end of data } - // Set payment refund requested - { - int64_t now_unix_ts = session::epoch_seconds(session::clock_now_s()); - session_pro_backend_request request_json = - session_pro_backend_set_payment_refund_requested_request_build( - master_privkey.data, - sizeof(master_privkey.data), - /*ts*/ now_unix_ts, - /*refund_requested_ts*/ now_unix_ts, - SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_GOOGLE_PLAY, - reinterpret_cast(another_payment_id.data()), - another_payment_id.size()); - scope_exit request_json_free{[&]() { session_pro_backend_request_free(&request_json); }}; - REQUIRE(request_json.success); - - // Do curl request - std::string response_json = curl_do_basic_blocking_post_request( - curl, - curl_headers, - g_test_pro_backend_dev_server_url + "/set_payment_refund_requested", - std::string_view(request_json.data.data, request_json.data.size)); - - // Parse response - session_pro_backend_set_payment_refund_requested_response response = - session_pro_backend_set_payment_refund_requested_response_parse( - response_json.data(), response_json.size()); - scope_exit response_free{[&]() { - session_pro_backend_set_payment_refund_requested_response_free(&response); - }}; - - // Verify response - INFO("ERROR: JSON response: " << response_json.c_str()); - if (response.header.error) - UNSCOPED_INFO("ERROR: " << response.header.error); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.header.status == SESSION_PRO_BACKEND_RESPONSE_STATUS_OK); - REQUIRE(response.updated); + // 4) set_payment_refund_requested. Apple-only at the HTTP layer: app_store -> updated + + // reflected + // in get_pro_status; google_play -> rejected (Google refunds are handled out-of-band). + ProRequest refund_req = refund_request( + master_sk, now, now, provider, to_byte_span(payment_id.data(), payment_id.size())); + SetPaymentRefundRequestedResponse refund = parse_refund(send(onion, refund_req)); + if (provider == SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE) { + INFO("refund" << refund.error.value_or("")); + REQUIRE(refund.status == ResponseStatus::Ok); + CHECK(refund.updated); + // The soft "refund requested" marker is now reflected back in get_pro_status. + ProStatusResponse status2 = + parse_pro_status(send(onion, pro_status_request(master_sk, now))); + REQUIRE(status2.status == ResponseStatus::Ok); + CHECK(status2.refund_requested_at.time_since_epoch().count() != 0); + } else { + // The endpoint rejects non-App-Store providers. + CHECK_FALSE(refund.updated); + CHECK(refund.status != ResponseStatus::Ok); } } + +// Revocation-list round-trip: seed + redeem a payment, seed a revocation for that generation, then +// confirm get_pro_revocations surfaces it and its tag matches the issued proof. Kept independent of +// the lifecycle flow so the revocation doesn't perturb those assertions. +TEST_CASE("Pro backend live get_pro_revocations", "[pro_backend][pro_live]") { + curl_global_init(CURL_GLOBAL_DEFAULT); + scope_exit curl_cleanup{[&]() { curl_global_cleanup(); }}; + CURL* curl = curl_easy_init(); + REQUIRE(curl); + scope_exit curl_free{[&]() { curl_easy_cleanup(curl); }}; + + const std::string& base_url = g_test_pro_backend_dev_server_url; + PostFn direct = make_direct_http_transport(curl, base_url); + std::array backend_pubkey = fetch_backend_pubkey(direct); + PostFn onion = make_onion_v4_transport(curl, base_url, backend_pubkey); + + std::array master_pk{}, rotating_pk{}; + std::array master_sk{}, rotating_sk{}; + crypto_sign_ed25519_keypair(master_pk.data(), master_sk.data()); + crypto_sign_ed25519_keypair(rotating_pk.data(), rotating_sk.data()); + std::string master_hex = oxenc::to_hex(master_pk); + + // Seed + redeem an app_store payment so the master has an active generation to revoke. + std::string payment_id = "20000000" + master_hex.substr(0, 10); + run_seed_helper( + {"payment", + "--provider", + "app_store", + "--payment-id", + payment_id, + "--master-pubkey", + master_hex, + "--plan", + "1m"}); + ProRequest add_req = add_payment_request( + master_sk, + rotating_sk, + SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_APP_STORE, + to_byte_span(payment_id.data(), payment_id.size())); + AddProPaymentResponse add = parse_add_payment(send(onion, add_req)); + REQUIRE(add.status == ResponseStatus::Ok); + CHECK(add.status == ResponseStatus::Ok); + + // Revoke that payment's generation (revocation is terminal now -- revoked_at set once, no + // per-entry expiry), then poll: the list must carry our proof's revocation_tag. + run_seed_helper({"revoke", "--provider", "app_store", "--payment-id", payment_id}); + + ProRequest rev_req = revocations_request(0); + GetProRevocationsResponse rev = parse_revocations(send(onion, rev_req)); + INFO("get_pro_revocations" << rev.error.value_or("")); + REQUIRE(rev.status == ResponseStatus::Ok); + CHECK(rev.retain_for.count() > 0); + CHECK_FALSE(rev.items.empty()); + bool found = false; + for (const auto& it : rev.items) + if (it.revocation_tag == add.proof.revocation_tag) + found = true; + CHECK(found); +} #endif From fe88f80fddcaf7744d10e2627c4e1a5d605718f3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 19:10:10 -0300 Subject: [PATCH 140/193] Add server-less known-answer vectors for Pro signed messages ([pro_kat]) The live [pro_live] suite catches libsession<->backend signing drift, but not a fault where BOTH sides share the same wrong construction -- they would still agree. Add frozen, spec-anchored known-answer vectors that pin libsession's signed-message construction (wire spec 1.1 / 2 / 3, Delta #13) with no running backend, and that freeze the wire format against silent future drift. - tests/pro_backend/gen_kat.py: generates the vectors from the backend's independent implementation for fixed deterministic inputs. - test_pro_backend.cpp [pro_kat]: for each signed request, asserts the request's Ed25519 signature covers exactly the expected (hand-verified) message bytes; for the proof, asserts ProProof::signed_message() equals the expected bytes and that a frozen backend signature verifies (and a tampered key does not). Covers the Delta #15 split endpoints: get_pro_status, and get_payment_details with both an empty `before` (newest page, trailing NUL) and a non-empty opaque cursor. Ed25519 determinism makes each signature its own known answer. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/pro_backend/gen_kat.py | 77 +++++++++++++++++ tests/test_pro_backend.cpp | 158 +++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 tests/pro_backend/gen_kat.py diff --git a/tests/pro_backend/gen_kat.py b/tests/pro_backend/gen_kat.py new file mode 100644 index 000000000..f68c94169 --- /dev/null +++ b/tests/pro_backend/gen_kat.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Generate the known-answer vectors asserted by the [pro_kat] test in tests/test_pro_backend.cpp. + +These pin libsession's signed-message construction (wire spec 1.1 / 2 / 3.1-3.4, Delta #13). The +vectors are produced here from the *backend's* independent implementation (backend.signed_message + +the make_*_message / build_proof_message builders) for fixed, deterministic inputs, then hand-verified +against the spec field layout before being frozen into the C++ test. Re-run this if the signed-message +format ever changes (it is a deliberate, both-sides wire change) and update the constants + the +hand-verification in the test to match. + +Run with the backend clone's venv python; requires env: + PRO_BACKEND_DIR path to the backend checkout (for importing backend/base) + + PRO_BACKEND_DIR=~/src/session-pro-backend .venv/bin/python gen_kat.py +""" +import datetime +import os +import sys + +sys.path.insert(0, os.environ["PRO_BACKEND_DIR"]) + +import nacl.signing # noqa: E402 + +import backend # noqa: E402 +import base # noqa: E402 + + +def hx(b): + return bytes(b).hex() + + +def main(): + # Fixed deterministic keypairs from 32-byte seeds (backend signing key 0x03). + master = nacl.signing.SigningKey(bytes([1] * 32)) + rotating = nacl.signing.SigningKey(bytes([2] * 32)) + backkey = nacl.signing.SigningKey(bytes([3] * 32)) + mpk, rpk = master.verify_key, rotating.verify_key + + ts = datetime.datetime.fromtimestamp(1700000000, datetime.timezone.utc) + refund = datetime.datetime.fromtimestamp(1700000123, datetime.timezone.utc) + expiry = datetime.datetime.fromtimestamp(1704067200, datetime.timezone.utc) + limit = 10 + before_cursor = "0a1b2c3d4e5f" # opaque to the signed input; any fixed non-empty string works + provider = base.PaymentProvider.GooglePlayStore + pid = "test-payment-id-123" + rtag = bytes([0x11] * 32) + + add_msg = backend.signed_message( + backend.ADD_PRO_PAYMENT_DOMAIN, mpk, rpk, provider.value, pid) + gen_msg = backend.make_generate_pro_proof_message(mpk, rpk, ts) + # Delta #15: get_pro_details split into get_pro_status + paginated get_payment_details. + status_msg = backend.make_get_pro_status_message(mpk, ts) + # Two payment_details cases: empty `before` (newest page) ends in the adjacency `\0`; + # a non-empty `before` appends the opaque cursor verbatim as the final field (no trailing sep). + details_empty_msg = backend.make_get_payment_details_message(mpk, ts, limit, "") + details_cursor_msg = backend.make_get_payment_details_message(mpk, ts, limit, before_cursor) + ref_msg = backend.signed_message( + backend.SET_PAYMENT_REFUND_REQUESTED_DOMAIN, mpk, ts, refund, provider.value, pid) + prf_msg = backend.build_proof_message(rtag, rpk, expiry) + + print("master_pkey =", hx(mpk.encode())) + print("rotating_pkey=", hx(rpk.encode())) + print("backend_pkey =", hx(backkey.verify_key.encode())) + print() + for name, m in [("add", add_msg), ("gen", gen_msg), ("status", status_msg), + ("details_empty", details_empty_msg), ("details_cursor", details_cursor_msg), + ("refund", ref_msg), ("proof", prf_msg)]: + print(f"{name}_msg_hex = {hx(m)}") + print(f" repr = {m!r}") + print() + print("add_master_sig =", hx(master.sign(add_msg).signature)) + print("add_rotating_sig =", hx(rotating.sign(add_msg).signature)) + print("proof_sig =", hx(backkey.sign(prf_msg).signature)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index a8ff252e0..9f51b2ed4 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -728,6 +728,164 @@ TEST_CASE("Pro Backend parse_plan_period (closed grammar)", "[pro_backend]") { CHECK_FALSE(parse_plan_period(bad).has_value()); } +// --------------------------------------------------------------------------- +// Known-answer vectors (tag [pro_kat]) -- SERVER-LESS (runs in an ordinary testAll build). +// +// These pin libsession's signed-message construction (wire spec 1.1 / 2 / 3.1-3.4, Delta #13) to +// fixed byte vectors that were generated from the backend's *independent* implementation and then +// hand-verified against the spec layout (generator: tests/pro_backend/gen_kat.py). The live +// [pro_live] suite catches libsession<->backend *drift*; a KAT additionally catches a fault where +// BOTH sides share the same wrong construction (they would still agree with each other), needs no +// running backend, and freezes the wire format against silent future drift. Ed25519 is +// deterministic (RFC 8032), so a signature over a fixed message under a fixed key is itself a known +// answer. +// +// Fixed inputs: keypairs from 32-byte seeds 0x01.. / 0x02.. (backend key 0x03..), ts=1700000000, +// refund_ts=1700000123, expiry=1704067200, count=10, provider="google_play", +// payment_id="test-payment-id-123", revocation_tag=0x11 x32. +TEST_CASE("Pro backend known-answer vectors", "[pro_backend][pro_kat]") { + auto keypair = [](unsigned char seed_byte) { + std::array pk{}; + std::array sk{}; + std::array seed{}; + seed.fill(seed_byte); + crypto_sign_seed_keypair(pk.data(), sk.data(), seed.data()); + return std::pair{pk, sk}; + }; + auto [master_pk, master_sk] = keypair(0x01); + auto [rotating_pk, rotating_sk] = keypair(0x02); + auto [backend_pk, backend_sk] = keypair(0x03); + (void)backend_sk; + + const auto ts = session::as_sys_seconds(1700000000); + const auto refund_ts = session::as_sys_seconds(1700000123); + const auto expiry = session::as_sys_seconds(1704067200); + const std::string provider = "google_play"; + const std::string payment_id = "test-payment-id-123"; + + // Expected signed-message bytes (hex), each hand-verified against the spec field layout. + const std::string add_msg_hex = + "50726f4164645061796d656e745f5f5f8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01" + "b40f6f5c8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394676f6f676c655f" + "70" + "6c617900746573742d7061796d656e742d69642d313233"; + const std::string gen_msg_hex = + "50726f47656e657261746550726f6f668a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01" + "b40f6f5c8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b39431373030303030" + "303030"; + // Delta #15: get_pro_details split into get_pro_status (master + ts) and paginated + // get_payment_details (master + ts + limit + before). Two details vectors pin the `before` + // framing: empty `before` (newest page) ends in the adjacency \0; a non-empty cursor is the + // opaque final field (no trailing separator). + const std::string status_msg_hex = + "50726f47657450726f5374617475735f8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01b40f6f5c31373030303030303030"; + const std::string details_empty_msg_hex = + "50726f47657450617944657461696c738a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01b40f6f5c3137303030303030303000313000"; + const std::string details_cursor_msg_hex = + "50726f47657450617944657461696c738a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01b40f6f5c3137303030303030303000313000306131623263336434653566"; + const std::string refund_msg_hex = + "50726f536574526566756e645265715f8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf37488" + "01" + "b40f6f5c31373030303030303030003137303030303031323300676f6f676c655f706c617900746573742d" + "70" + "61796d656e742d69642d313233"; + const std::string proof_msg_hex = + "50726f50726f6f665f76305f5f5f5f5f111111111111111111111111111111111111111111111111111111" + "1111" + "1111118139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3943137303430363732" + "3030"; + + // Assert `field`'s hex signature in request body `body` is a valid Ed25519 signature of the + // expected message bytes under `pubkey` -- i.e. libsession signed *exactly* the spec message. + auto sig_covers = [](const std::string& body, + const char* field, + const std::string& expected_msg_hex, + const std::array& pubkey) -> bool { + auto j = nlohmann::json::parse(body); + if (!j.contains(field)) + return false; + auto sig = oxenc::from_hex(j.at(field).get()); + auto msg = oxenc::from_hex(expected_msg_hex); + return sig.size() == 64 && crypto_sign_verify_detached( + reinterpret_cast(sig.data()), + reinterpret_cast(msg.data()), + msg.size(), + pubkey.data()) == 0; + }; + + SECTION("add_pro_payment") { + auto req = add_payment_request( + master_sk, + rotating_sk, + provider, + to_byte_span(payment_id.data(), payment_id.size())); + CHECK(req.endpoint == SESSION_PRO_BACKEND_ADD_PRO_PAYMENT_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", add_msg_hex, master_pk)); + CHECK(sig_covers(req.data, "rotating_sig", add_msg_hex, rotating_pk)); + // Determinism pin: the exact master signature is itself a known answer. + CHECK(nlohmann::json::parse(req.data).at("master_sig").get() == + "746861ae1681d996e837d603dbd21c06c9544c5fc536eb4d3e3cad4f13692b79" + "0ef2b1282b729aa42f537d560b3b97b231b7f940be7021c0e16f894817a57609"); + } + SECTION("generate_pro_proof") { + auto req = pro_proof_request(master_sk, rotating_sk, ts); + CHECK(req.endpoint == SESSION_PRO_BACKEND_GENERATE_PRO_PROOF_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", gen_msg_hex, master_pk)); + CHECK(sig_covers(req.data, "rotating_sig", gen_msg_hex, rotating_pk)); + } + SECTION("get_pro_status") { + auto req = pro_status_request(master_sk, ts); + CHECK(req.endpoint == SESSION_PRO_BACKEND_GET_PRO_STATUS_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", status_msg_hex, master_pk)); + } + SECTION("get_payment_details (newest page, empty before)") { + auto req = payment_details_request(master_sk, ts, 10, ""); + CHECK(req.endpoint == SESSION_PRO_BACKEND_GET_PAYMENT_DETAILS_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", details_empty_msg_hex, master_pk)); + } + SECTION("get_payment_details (with cursor)") { + auto req = payment_details_request(master_sk, ts, 10, "0a1b2c3d4e5f"); + CHECK(req.endpoint == SESSION_PRO_BACKEND_GET_PAYMENT_DETAILS_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", details_cursor_msg_hex, master_pk)); + } + SECTION("set_payment_refund_requested") { + auto req = refund_request( + master_sk, + ts, + refund_ts, + provider, + to_byte_span(payment_id.data(), payment_id.size())); + CHECK(req.endpoint == SESSION_PRO_BACKEND_SET_PAYMENT_REFUND_REQUESTED_ENDPOINT); + CHECK(sig_covers(req.data, "master_sig", refund_msg_hex, master_pk)); + } + SECTION("pro proof") { + ProProof proof; + proof.version = 0; + std::memset(proof.revocation_tag.data(), 0x11, proof.revocation_tag.size()); + std::memcpy(proof.rotating_pubkey.data(), rotating_pk.data(), 32); + proof.expiry_at = expiry; + auto sig = oxenc::from_hex( + "767e7f14cec2d04fb67c6407e890cbe6a5cfdb7a0df3e729adda93164b13bc3f" + "b20ed4ea1b8f1ffc74115ff2598a51ac52285d923f0864c6bca08bfc771fb708"); + std::memcpy(proof.sig.data(), sig.data(), 64); + + // Direct pin of the proof (spec section 2) message construction. + CHECK(oxenc::to_hex(proof.signed_message()) == proof_msg_hex); + // The frozen backend signature (key 0x03 over that message) verifies; a wrong key does not. + std::array bpk{}; + std::memcpy(bpk.data(), backend_pk.data(), 32); + CHECK(proof.verify_signature(bpk)); + auto wrong = bpk; + wrong[0] = static_cast(std::to_integer(wrong[0]) ^ 0x01); + CHECK_FALSE(proof.verify_signature(wrong)); + } +} + #if defined(TEST_PRO_BACKEND_WITH_DEV_SERVER) #include From 51cbdcd4ffb776fb1695423844fe354c6e011308 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 14:58:39 -0300 Subject: [PATCH 141/193] Harness: drop stale Delta-N spec citations; track backend upstream dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two housekeeping changes now that the backend merged to session-foundation dev: - The wire spec dropped its "Delta N" change-history annotations, so those citations in the harness comments/vectors are dangling. Reworded to cite the current spec sections instead (plan -> §1, signed-message construction -> §1.1/§2/§3, the read-request split -> §3.4) or dropped the parenthetical. Verified remaining §-references resolve against the current section map. - .drone.jsonnet: point the CI backend clone at session-foundation dev (was the now-merged jagerman phase2-foundation). An integration/drift harness should track the counterpart's mainline; a pinned ref is what went stale before. No behavior change to the tests themselves. Co-Authored-By: Claude Opus 4.8 (1M context) --- .drone.jsonnet | 2 +- tests/pro_backend/gen_kat.py | 4 ++-- tests/test_pro_backend.cpp | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 53ee10e15..1ee771ad3 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -216,7 +216,7 @@ local live_test_step(image, mode) = { // and run the [pro_live] suite against it. The backend is a separate Python service, checked out at // a pinned ref (repos stay decoupled — CI keeps its own clone). local pro_backend_git = 'https://github.com/session-foundation/session-pro-backend.git'; -local pro_backend_ref = 'phase2-foundation'; // bump/pin as the backend advances in lockstep +local pro_backend_ref = 'dev'; // track upstream mainline (session-foundation dev) -- the [pro_live] drift check is meant to follow it // Extra apt packages (beyond the standard test deps) the live Pro-backend step needs: postgres, the // session-router key tool, the backend's python3-* runtime, and clone/venv tooling. diff --git a/tests/pro_backend/gen_kat.py b/tests/pro_backend/gen_kat.py index f68c94169..bb43784bd 100644 --- a/tests/pro_backend/gen_kat.py +++ b/tests/pro_backend/gen_kat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Generate the known-answer vectors asserted by the [pro_kat] test in tests/test_pro_backend.cpp. -These pin libsession's signed-message construction (wire spec 1.1 / 2 / 3.1-3.4, Delta #13). The +These pin libsession's signed-message construction (wire spec §1.1 / §2 / §3). The vectors are produced here from the *backend's* independent implementation (backend.signed_message + the make_*_message / build_proof_message builders) for fixed, deterministic inputs, then hand-verified against the spec field layout before being frozen into the C++ test. Re-run this if the signed-message @@ -48,7 +48,7 @@ def main(): add_msg = backend.signed_message( backend.ADD_PRO_PAYMENT_DOMAIN, mpk, rpk, provider.value, pid) gen_msg = backend.make_generate_pro_proof_message(mpk, rpk, ts) - # Delta #15: get_pro_details split into get_pro_status + paginated get_payment_details. + # get_pro_details read request is split (§3.4) into get_pro_status + paginated get_payment_details. status_msg = backend.make_get_pro_status_message(mpk, ts) # Two payment_details cases: empty `before` (newest page) ends in the adjacency `\0`; # a non-empty `before` appends the opaque cursor verbatim as the final field (no trailing sep). diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 9f51b2ed4..8212bc52a 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -731,7 +731,7 @@ TEST_CASE("Pro Backend parse_plan_period (closed grammar)", "[pro_backend]") { // --------------------------------------------------------------------------- // Known-answer vectors (tag [pro_kat]) -- SERVER-LESS (runs in an ordinary testAll build). // -// These pin libsession's signed-message construction (wire spec 1.1 / 2 / 3.1-3.4, Delta #13) to +// These pin libsession's signed-message construction (wire spec §1.1 / §2 / §3) to // fixed byte vectors that were generated from the backend's *independent* implementation and then // hand-verified against the spec layout (generator: tests/pro_backend/gen_kat.py). The live // [pro_live] suite catches libsession<->backend *drift*; a KAT additionally catches a fault where @@ -775,7 +775,7 @@ TEST_CASE("Pro backend known-answer vectors", "[pro_backend][pro_kat]") { "01" "b40f6f5c8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b39431373030303030" "303030"; - // Delta #15: get_pro_details split into get_pro_status (master + ts) and paginated + // get_pro_details is split (§3.4) into get_pro_status (master + ts) and paginated // get_payment_details (master + ts + limit + before). Two details vectors pin the `before` // framing: empty `before` (newest page) ends in the adjacency \0; a non-empty cursor is the // opaque final field (no trailing separator). @@ -1135,7 +1135,7 @@ static std::array fetch_backend_pubkey(const PostFn& transport) { // The core wire-contract flow: build *real* requests with the C++ API, send them over onion to a // backend that redeems a witnessed (seeded) payment, and check every response parses + the issued // proofs verify against the backend's signing key. Any signed-message drift on either side (field -// order, integer encoding, `\0` framing, domain prefix -- Delta #13 signs the message bytes +// order, integer encoding, `\0` framing, domain prefix -- the scheme signs the message bytes // directly, no hash) or a renamed JSON key breaks this. Runs the whole flow once per provider via // SECTIONs: google_play exercises the composite "token|order_id" payment_id, app_store the plain tx // id. Each SECTION uses fresh keys + a freshly-seeded payment, so the runs are independent on the @@ -1213,7 +1213,7 @@ TEST_CASE("Pro backend live full flow", "[pro_backend][pro_live]") { // Same subscription epoch -> the fresh proof shares the add-payment proof's revocation_tag. CHECK(gen.proof.revocation_tag == add.proof.revocation_tag); - // 2) get_pro_status (Delta #15): account ACTIVE, no refund yet, and the single latest payment + // 2) get_pro_status: account ACTIVE, no refund yet, and the single latest payment // is our redeemed one. ProStatusResponse status = parse_pro_status(send(onion, pro_status_request(master_sk, now))); INFO("get_pro_status " << status.error.value_or("")); @@ -1223,12 +1223,12 @@ TEST_CASE("Pro backend live full flow", "[pro_backend][pro_live]") { REQUIRE(status.latest_payment.has_value()); CHECK(status.latest_payment->payment_id == payment_id); CHECK(status.latest_payment->payment_provider == provider); - // plan is the parsed billing period (Delta #14): "1m" -> {count 1, month}. + // plan is the parsed billing period (§1): "1m" -> {count 1, month}. CHECK(status.latest_payment->plan.count == 1); CHECK(status.latest_payment->plan.unit == ProPlanUnit::month); CHECK(status.latest_payment->status == "redeemed"); - // 2b) get_payment_details (Delta #15): newest page (empty cursor) carries our payment. This + // 2b) get_payment_details: newest page (empty cursor) carries our payment. This // master has exactly one payment, so it fits in one page and next_cursor is empty. PaymentDetailsResponse det = parse_payment_details(send(onion, payment_details_request(master_sk, now, 10, ""))); From 9937d65cd2db0edbe8294f31537dc518686df7e0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 20:05:20 -0300 Subject: [PATCH 142/193] CI: install python3-psycopg-pool for the Pro-backend live pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend's main.py imports psycopg_pool (psycopg 3's connection pool, requirements.txt: psycopg-pool~=3.2) — a separate Debian package from python3-psycopg. Without it flask fails to import `main` and the ephemeral backend never starts, so the [pro_live] step's launcher reports "flask exited during startup: ModuleNotFoundError: No module named 'psycopg_pool'". Co-Authored-By: Claude Opus 4.8 (1M context) --- .drone.jsonnet | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index 1ee771ad3..d5b86c452 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -226,6 +226,7 @@ local pro_backend_pkgs = [ 'python3-session-util', 'python3-nacl', 'python3-psycopg', + 'python3-psycopg-pool', 'python3-flask', 'python3-venv', 'python3-pip', From 21dd62cbff23c9f39b7f1879acdebaf611a29b1e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 21:23:44 -0300 Subject: [PATCH 143/193] CI: force a UTF-8 locale in the ephemeral-backend launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pro-backend-live pipeline runs in a minimal Debian container whose default locale is C/POSIX -> ASCII. The backend's migration SQL carries non-ASCII bytes (an em-dash in a comment); psycopg encodes each query with the connection's client encoding, which follows the DB/locale, so bootstrap_db died with `UnicodeEncodeError: 'ascii' codec can't encode character '—'` and flask never started. run-dev-backend.sh now exports LC_ALL/LANG=C.UTF-8 (always present in glibc) and initdb's the cluster with --encoding=UTF8, so the cluster and every client (flask, seed helper, psql) speak UTF-8 as in production. Reproduced locally under LC_ALL=C and confirmed fixed. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/pro_backend/run-dev-backend.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/pro_backend/run-dev-backend.sh b/tests/pro_backend/run-dev-backend.sh index 438686813..89d7144b5 100755 --- a/tests/pro_backend/run-dev-backend.sh +++ b/tests/pro_backend/run-dev-backend.sh @@ -23,6 +23,14 @@ # set -euo pipefail +# Force a UTF-8 locale for the ephemeral cluster and every DB client. Backend migration SQL contains +# non-ASCII bytes (e.g. an em-dash in a comment); psycopg encodes each query with the connection's +# client encoding, which follows the DB/locale. A minimal CI container often runs a C/POSIX locale -> +# ASCII client encoding -> `UnicodeEncodeError: 'ascii' codec can't encode '—'` while bootstrapping +# the DB. C.UTF-8 is always present in glibc (no locale-gen needed), so this makes initdb build a UTF8 +# cluster and flask/seed/psql all speak UTF-8, matching production. Overrides any inherited C locale. +export LC_ALL=C.UTF-8 LANG=C.UTF-8 + TESTALL="${1:?usage: run-dev-backend.sh [catch2 args...]}" shift || true BACKEND_DIR="${SESSION_PRO_BACKEND_DIR:-$HOME/src/session-pro-backend-itest}" @@ -56,7 +64,7 @@ echo ">> ephemeral signing key: $KEYFILE" session-router-config -f -k "$KEYFILE" >/dev/null echo ">> initdb: $PGDATA" -"$PGBIN/initdb" -D "$PGDATA" --auth-local=trust --no-instructions -U "$USER" >/dev/null +"$PGBIN/initdb" -D "$PGDATA" --encoding=UTF8 --auth-local=trust --no-instructions -U "$USER" >/dev/null echo ">> start postgres (unix socket in $WORK, port $PORT)" "$PGBIN/pg_ctl" -D "$PGDATA" -l "$PG_LOG" \ From d40f8d645d4fdf180c95756e78f580a5f9884964 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:00:08 -0300 Subject: [PATCH 144/193] Fix to_string_view causing problems with fmt9 In some cases this causes fmt-9 compilation problems because the to_string_view template matches too broadly. This confines it down to containers that will actually work to avoid the problem. Also drops the OutChar template, because it is useless: it can never be anything other than char because of the fixed string_view return type (I think this is a holdover from when we supposed ustring_view/bstring_view). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/session/util.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/session/util.hpp b/include/session/util.hpp index 0ff1d3614..d1dfed9da 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -101,9 +101,13 @@ inline std::string to_string(const Container& c) { return convert(c); } -template +template + requires requires(const Container& c) { + requires sizeof(*c.data()) == 1; + { c.size() } -> std::convertible_to; + } inline std::string_view to_string_view(const Container& c) { - return {reinterpret_cast(c.data()), c.size()}; + return {reinterpret_cast(c.data()), static_cast(c.size())}; } /// Returns a fixed-extent std::byte span viewing N bytes starting at p. Primarily useful for From 098e88ddd1db6709b6f849a86ec7319532324bff Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:00:08 -0300 Subject: [PATCH 145/193] Add missing include onion_request_router.cpp uses std::views::values but relied on a transitive include for ; include it explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/network/routing/onion_request_router.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/network/routing/onion_request_router.cpp b/src/network/routing/onion_request_router.cpp index 8860b5799..3bb6fda83 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -6,6 +6,8 @@ #include #include #include +#include + #include #include From 57c29e032255fe1597cb203b80d5455baadb0227 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:00:09 -0300 Subject: [PATCH 146/193] Fall back to room-less file URL format when parsing open group download URLs Some clients (e.g. older Android versions) generate community file download urls without the room segment (`{base_url}/file/{file_id}`). Accept these as a fallback so their attachments remain downloadable, leaving the room empty for the caller to populate from context. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../backends/session_open_group_server.cpp | 33 ++++++++++++++----- .../test_backed_session_open_group_server.cpp | 18 ++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/network/backends/session_open_group_server.cpp b/src/network/backends/session_open_group_server.cpp index a4c65ff05..0ad07efd4 100644 --- a/src/network/backends/session_open_group_server.cpp +++ b/src/network/backends/session_open_group_server.cpp @@ -19,24 +19,41 @@ namespace session::network::open_group_server { constexpr std::string_view ENDPOINT_ROOM = "room/{}"; constexpr std::string_view ENDPOINT_FILE = "room/{}/file/{}"; +// Some clients (e.g. older Android versions) generate community file download urls without the +// room segment (`{base_url}/file/{file_id}`). We accept these as a fallback so their attachments +// remain downloadable. +constexpr std::string_view LEGACY_ENDPOINT_FILE = "file/{}"; + std::optional parse_download_url(std::string_view url) { // Expected format: {base_url}/room/{room}/file/{file_id}(?:#d) // Examples: - // https://example.com/room/file/123 - // https://example.com/room/file/123#d + // https://example.com/room/test/file/123 + // https://example.com/room/test/file/123#d + // Legacy fallback (no room, e.g. from older Android clients): {base_url}/file/{file_id} + // https://example.com/file/123 DownloadInfo info{}; + int64_t file_id; + auto match = backends::match_endpoint(ENDPOINT_FILE, url); - if (!match || match->base.empty() || match->captures.size() != 2) - return std::nullopt; + if (match && !match->base.empty() && match->captures.size() == 2) { + info.room = std::string{match->captures[0]}; - info.room = std::string{match->captures[0]}; + if (!quic::parse_int(match->captures[1], file_id)) + return std::nullopt; + } else { + // Fall back to the legacy room-less endpoint. The room can't be recovered from such a url + // so it's left empty for the caller to populate from context (the conversation the + // attachment belongs to). + match = backends::match_endpoint(LEGACY_ENDPOINT_FILE, url); - int64_t file_id; + if (!match || match->base.empty() || match->captures.size() != 1) + return std::nullopt; - if (!quic::parse_int(match->captures[1], file_id)) - return std::nullopt; + if (!quic::parse_int(match->captures[0], file_id)) + return std::nullopt; + } info.file_id = file_id; info.base_url = match->base; diff --git a/tests/test_backed_session_open_group_server.cpp b/tests/test_backed_session_open_group_server.cpp index bff9f8ffc..2dbc2b4e3 100644 --- a/tests/test_backed_session_open_group_server.cpp +++ b/tests/test_backed_session_open_group_server.cpp @@ -44,6 +44,24 @@ TEST_CASE("Download url parsing", "[backend][session_open_group_server]") { CHECK(parsed_download_url->file_id == 123); CHECK_FALSE(parsed_download_url->wants_stream_decryption); + // Falls back to the legacy room-less format (used by some older clients, e.g. Android), + // leaving the room empty for the caller to populate from context + parsed_download_url = open_group_server::parse_download_url("https://example.com/file/123"sv); + REQUIRE(parsed_download_url.has_value()); + CHECK(parsed_download_url->base_url == "https://example.com"sv); + CHECK(parsed_download_url->room.empty()); + CHECK(parsed_download_url->file_id == 123); + CHECK_FALSE(parsed_download_url->wants_stream_decryption); + + // Handles the legacy room-less format with a trailing slash and stream-decryption fragment + parsed_download_url = + open_group_server::parse_download_url("https://example.com/file/123/#d"sv); + REQUIRE(parsed_download_url.has_value()); + CHECK(parsed_download_url->base_url == "https://example.com"sv); + CHECK(parsed_download_url->room.empty()); + CHECK(parsed_download_url->file_id == 123); + CHECK(parsed_download_url->wants_stream_decryption); + // Doesn't have an issue with a url that isn't in the right format parsed_download_url = open_group_server::parse_download_url("https://example.com/test/test2/test3/5432"sv); From c666e8fec731ee73dc39e3e265348464df16e565 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:00:22 -0300 Subject: [PATCH 147/193] Clamp revocation list retry/cleanup values Makes sure both have a reasonably sane value; mainly defensive. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pro_backend.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index 671fa3631..d61e15f47 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -506,6 +507,13 @@ GetProRevocationsResponse parse_revocations(std::string_view json) { result.retry_in = std::chrono::seconds(json_require(result_obj, "retry_in", errs)); result.retain_for = std::chrono::seconds(json_require(result_obj, "retain_for", errs)); + // Clamp values against non-sensical/catastrophic values: retry_in of very small would result in + // excess retries, and an overly long retry (e.g. 10 years) would make the client not properly + // refresh at all. The retain_for clamp is deliberately larger because that is only a hint as + // to when the record can be cleaned up. + result.retry_in = std::clamp(result.retry_in, 60s, 48h); + result.retain_for = std::clamp(result.retain_for, 24h, 365 * 24h); + auto array = json_require(result_obj, "items", errs); result.items.reserve(array.size()); for (size_t index = 0; index < array.size(); index++) { From 702332f891a65c055af93e75ff1f0f14c27a056c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:00:22 -0300 Subject: [PATCH 148/193] tests/pro_backend: fix seed_payment obfuscated-account-id derivation The backend dropped the `{google,apple}_obfuscated_account_id_from_master_pkey` helpers, so seed_payment.py's [pro_live] seeding died with `AttributeError: module 'backend' has no attribute 'apple_obfuscated_account_id_from_master_pkey'`. The account tag each provider attests changed, and the seeded witnessed-payment row must equal what the backend now recomputes from the request-signed master key at redeem time (backend.py add_pro_payment): - Google: the client sets setObfuscatedAccountId to the master pubkey verbatim (no more sha256), so the id is the raw 32-byte master pubkey -> `bytes(vk)`. - Apple: appAccountToken = UUID(first 16 bytes of the master pubkey) via providers.app_store.uuid_from_master_pk, stored lowercased. Import the Apple provider lazily (its module pulls the Apple SDK) so seeding a non-Apple payment doesn't require it. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/pro_backend/seed_payment.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/pro_backend/seed_payment.py b/tests/pro_backend/seed_payment.py index 7fb23de69..25db079e5 100644 --- a/tests/pro_backend/seed_payment.py +++ b/tests/pro_backend/seed_payment.py @@ -67,10 +67,20 @@ def _payment_tx(provider, payment_id): def _obfuscated_id(provider, vk): + # The witnessed `platform_obfuscated_account_id` must equal what the backend recomputes from the + # request-signed master key at redeem time (backend.py add_pro_payment binding), per provider. if provider == "google_play": - return backend.google_obfuscated_account_id_from_master_pkey(vk) + # Google: the client sets setObfuscatedAccountId to the master pubkey verbatim, so the + # attested id IS the raw 32-byte master pubkey; the redeem binds by byte-equality. + return bytes(vk) if provider == "app_store": - return backend.apple_obfuscated_account_id_from_master_pkey(vk) + # Apple: appAccountToken = UUID(first 16 bytes of the master pubkey); the redeem binds by + # equality against the same recomputed UUID (stored lowercased on receipt). Import the Apple + # provider lazily -- its module pulls the Apple SDK -- so seeding a non-Apple payment doesn't + # need it. + from providers import app_store + + return app_store.uuid_from_master_pk(bytes(vk)).lower() return b"" From 046f188b4431cf2c0c612b6ec1abc5725ba883ff Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 19:35:37 -0300 Subject: [PATCH 149/193] Network config runtime option cleanup This code was using std::any, which has warts when used for fixed types: it ended up needing some gross macro uses rather than cleaner templating, and could not catch invalid types at compile time. Additionally it broke under older distros for some reason (perhaps because of a missing header, but no matter). This replaces it with a simpler `network::opt::any` instead of `std::any` which is actually a variant, and results in cleaner code: both getting rid of the macro, allowing compile time checks, and removing the need for the `base` class layer under every option (is-a-base is now replaced with is_option). Also changes the std::optional