From cc586789dade0b9d826dee76d4971573388d2942 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Feb 2026 14:30:16 -0400 Subject: [PATCH 01/81] Fix unpleasant string chopping --- .clang-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index 5846e195..407695b0 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 8dd64b48a00ebe6617f717bc4678d5362131a207 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 18:08:56 -0300 Subject: [PATCH 02/81] 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 a029c208..79febb6f 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 123cd110..864ff6ed 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 49863f8c..00000000 --- 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 00000000..58839b50 --- /dev/null +++ b/cmake/session-deps @@ -0,0 +1 @@ +Subproject commit 58839b50a0a2ad21b074c42e949613aad69ad396 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index b79fbf87..aceb8330 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 7b3f5afc..00000000 --- a/external/simdutf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7b3f5afcae322391a03736809ef6eea0c2934388 diff --git a/external/zstd b/external/zstd deleted file mode 160000 index e47e674c..00000000 --- a/external/zstd +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e47e674cd09583ff0503f0f6defd6d23d8b718d3 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7530af29..bec697e9 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 2667d342..fa361e33 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 f831735e0ee76fb395befc890ab68baa87243f46 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 18:12:31 -0300 Subject: [PATCH 03/81] 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 79febb6f..8d6f7cfe 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 aceb8330..99edc1f4 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 e12e612d..00000000 --- a/external/libsodium-internal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e12e612dc735909a59be552f9bf03fe98e320703 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bec697e9..0409512f 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 927aa5ea..659bf011 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 e35b4d04..85e24287 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 00000000..2891dcb0 --- /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 9d23390a..3110eea5 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 2ed0d1af..002dd876 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 143ba82b..74a9ae35 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 70d9463a487dd91f443fb4cb463b1319d42e711b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 19:04:40 -0300 Subject: [PATCH 04/81] 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 24e1a238..45892306 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 9a37cff8..2571e61f 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -32,6 +32,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 34efb16f..144bc39a 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 1ff3c21e..f66a1c3c 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 a0a52de0..c14ed7f5 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 75c9b475..1baead82 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 4963ce89..d6530728 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 1cedd9ca..ef62d988 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 5d12a0c4..8620088a 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -48,6 +48,7 @@ const session_pro_backend_payment_provider_metadata SESSION_PRO_BACKEND_PAYMENT_ .cancel_subscription_url = string8_literal("https://account.apple.com/account/manage/section/subscriptions"), } }; +// clang-format on namespace { const nlohmann::json json_parse(std::string_view json, std::vector& errors) { @@ -211,12 +212,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, @@ -392,12 +391,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); @@ -559,13 +556,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, @@ -796,12 +791,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 434b859a..7e325d58 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 56eba9de..8b232b5c 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 fe2b33fd..99264ec0 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"), @@ -76,11 +56,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()); @@ -1120,19 +1098,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 2449c441db40f05c4ebe7939932e64454da0fcfe Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 19:11:42 -0300 Subject: [PATCH 05/81] 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 8d6f7cfe..5a2ee4c5 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 99edc1f4..62ea1bd1 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 00000000..75746b6f --- /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 b9670323..47220525 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 00000000..fbc9b1b2 --- /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 00000000..ebd4af70 --- /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 00000000..82c7823e --- /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 00000000..aee56d9c --- /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 00000000..61a3d125 --- /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 00000000..be8f862d --- /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 00000000..913a5af6 --- /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 0409512f..b52b88a6 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 00000000..503ca7eb --- /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 00000000..0854bd85 --- /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 00000000..74c8bdda --- /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 00000000..df0fdf5b --- /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 00000000..ebbebcd6 --- /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 00000000..e73775d1 --- /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 00000000..908ba2bd --- /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 00000000..0db407d3 --- /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 00000000..45539511 --- /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 00000000..37fb9fc1 --- /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 00000000..38c9f65f --- /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 00000000..d7ef7a0e --- /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 00000000..0d64d309 --- /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 9db10314b77406fa30813986fa0e170f68afce4d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 20:48:26 -0300 Subject: [PATCH 06/81] 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 58839b50..3384e624 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit 58839b50a0a2ad21b074c42e949613aad69ad396 +Subproject commit 3384e624291b2e38c5840c6c962d564819d0af1c From cb8499c2613997fcbdfe69107a76074ce5acab27 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 10 Mar 2026 23:27:36 -0300 Subject: [PATCH 07/81] 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 864ff6ed..6b547e5a 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 62ea1bd1..72d7c1be 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 324b116b..a7ab778a 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 b52b88a6..8fb16b20 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 3fdbd83cf4eeff468db238e4fa6554d61fe7becd Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 00:31:07 -0300 Subject: [PATCH 08/81] 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 3384e624..e5677c8f 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 d2def4c9..e9cfe3be 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 8fb16b20..6cdfd57d 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 90c95e42c3cef1eab7ab729620d3b7910299fe29 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 15:08:57 -0300 Subject: [PATCH 09/81] 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 6945f70a..16f23b79 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 aee56d9c..f9430471 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 74c8bdda..927220ea 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 86f7c0b1c7cc299e731f70f7d3ff8556abf398e8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 15:11:59 -0300 Subject: [PATCH 10/81] 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 82c7823e..394280e7 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 0854bd85..49392c8b 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 8e929085c696e104802a2ea027690c0b29eb9258 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:07:57 -0300 Subject: [PATCH 11/81] 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 b4b46ef4..406de602 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 fbc9b1b2..9b0e9a1f 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 ebd4af70..8fec3710 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 394280e7..450c8a95 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 f9430471..b7ab454c 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 6cdfd57d..fbc936f3 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 23a87cd9..85c73bd7 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 503ca7eb..b5755514 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 49392c8b..0138d8b1 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 927220ea..f2cbc975 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 e73775d1..1e5a30fc 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 f91ac490ce22791789988effd2ec10d324e772a7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:21:02 -0300 Subject: [PATCH 12/81] 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 9b0e9a1f..4ee75974 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 b7ab454c..3aeb619a 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 b5755514..bc4283b5 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 f2cbc975..935b54ed 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 3983a219bfe1824f231194425795f80c89446c2d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 20:27:35 -0300 Subject: [PATCH 13/81] 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 4ee75974..9562b341 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 61a3d125..a5268441 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 bc4283b5..875872a1 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 935b54ed..bdec9fc4 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 edd15b6c4b5dfb2165f7805d6e4fabb3259eeacc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 23:06:07 -0300 Subject: [PATCH 14/81] 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 9280110f..5a2770ed 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 45892306..b93eefa9 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 1480f792..18d81c73 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, @@ -563,10 +563,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; @@ -599,8 +599,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 f66a1c3c..139fdff6 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 d28dbc14..57e537d3 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 1baead82..94a3288b 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 ef437419..56ed1e07 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 { @@ -230,13 +231,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 659bf011..81aa25c5 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 a8e2cf1f..c1df4f69 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 85c73bd7..8a37e20b 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 85e24287..83331719 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 ebbebcd6..ad7d1d7f 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 bce3d269..943e2d27 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 8620088a..b0ca2131 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -180,7 +181,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; @@ -190,7 +191,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; @@ -212,7 +213,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()); @@ -266,7 +267,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; @@ -276,7 +277,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; @@ -369,7 +370,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; @@ -379,7 +380,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; @@ -391,18 +392,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 = {}; @@ -429,7 +427,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; @@ -439,7 +437,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; @@ -539,14 +537,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; @@ -556,24 +554,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, @@ -590,7 +584,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; @@ -771,7 +765,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, @@ -781,7 +775,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; @@ -791,7 +785,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()); @@ -827,7 +821,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, @@ -845,7 +839,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 8b232b5c..3f763b17 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 99264ec0..d41cfc23 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 @@ -46,25 +46,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; } @@ -104,7 +103,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) { @@ -161,7 +160,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; } @@ -200,8 +199,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; } @@ -277,7 +276,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; @@ -293,8 +292,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; @@ -323,7 +322,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 = {}; @@ -577,7 +576,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, @@ -655,7 +654,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; @@ -914,7 +913,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 @@ -1152,7 +1151,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, @@ -1168,7 +1167,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 3110eea5..0d7411a9 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 1f8048cc3c57f782d5be0456b992f3009d0c0d85 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 11 Mar 2026 23:27:49 -0300 Subject: [PATCH 15/81] 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 94a3288b..aa5dd8a5 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 b0ca2131..f03aea89 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] = { @@ -214,32 +214,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 = {}; @@ -396,11 +381,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 = {}; @@ -559,12 +543,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 = {}; @@ -786,39 +768,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 d41cfc23..883c4f53 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -58,12 +58,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 384666c512446c56e4921b554e749388e4f56b65 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 14:02:44 -0300 Subject: [PATCH 16/81] 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 aa5dd8a5..2821e4af 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 b265fde2b5cbd874b12b070a9142da2a3823d8c3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 14:06:08 -0300 Subject: [PATCH 17/81] 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 fe812f23..74b30dc5 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 8b154c5f..d1ed14be 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 047a3ba7..b2e9fd6a 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 559984dbda285ddd22d83359e05a2d8147b1ff07 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 15:31:20 -0300 Subject: [PATCH 18/81] 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 b93eefa9..c65b8d2e 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 2821e4af..94a3288b 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 f03aea89..1f59e181 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -217,12 +217,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); @@ -381,10 +381,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 = {}; @@ -543,10 +543,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 = {}; @@ -773,12 +773,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 883c4f53..9358b99b 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -58,10 +58,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 5a07faf2e16ef7e3eccc498576e6346a82a2807f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 16:58:22 -0300 Subject: [PATCH 19/81] 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 3aeb619a..000f87c1 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 00000000..e3725c13 --- /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 fbc936f3..68e3d8d1 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 875872a1..140f3825 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 bdec9fc4..10a7e305 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 00000000..52a396c4 --- /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 851d90d9692d9f1fbcee539e2331be3597e30d3a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 12 Mar 2026 20:14:07 -0300 Subject: [PATCH 20/81] 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 8fec3710..0dbc8fc8 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 000f87c1..b8c091dc 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 e3725c13..e065ea77 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 10a7e305..2d11595e 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 52a396c4..c2a77197 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 1e5a30fc..cbf4aa15 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 ac8b746b8e66119cba9c8e647018f93f5f0e5e33 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Mar 2026 17:30:33 -0300 Subject: [PATCH 21/81] 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 47220525..f9a5ae1c 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 16f23b79..62e5cbe4 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 b8c091dc..911343a5 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 140f3825..500822ff 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 2d11595e..cb894517 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 cbf4aa15..4e6d44f4 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 e8c9576e39dc9a178759a4226c57b493c8c914d0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 13 Mar 2026 18:10:59 -0300 Subject: [PATCH 22/81] 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 f9a5ae1c..f0e2b490 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 62e5cbe4..89de2eec 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 911343a5..327788d5 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 500822ff..66a21635 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 cb894517..0a3e971b 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 3c4bf00582ad3de1c7cae9fdf841f2d1e413e746 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 16 Mar 2026 19:40:11 -0300 Subject: [PATCH 23/81] 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 75746b6f..e3105bae 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 327788d5..c48fa913 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 c65b8d2e..7215a2d8 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 0a3e971b..4477f8bc 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 4e6d44f4..abe36d76 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 2337208cc6ddeafe806c2c7892ef37c24296bc97 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 16 Mar 2026 19:59:04 -0300 Subject: [PATCH 24/81] 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 4477f8bc..2f275f98 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 abe36d76..fb653ccb 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 2bf96e931df49179ff467e350b1cdbe2a27c406d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 16:55:30 -0300 Subject: [PATCH 25/81] 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 e5677c8f..d66944ab 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit e5677c8fa649043e7b985ba45459fa2e9b96bc09 +Subproject commit d66944abf027dacc07d7cb0e58aae53aaecd8b8a From a41637234e68222849b6f7e8e8ff51295dd063e0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 22:38:32 -0300 Subject: [PATCH 26/81] 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 9562b341..1fc583a1 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 68e3d8d1..86466548 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 df0fdf5b..9b97e216 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 fb653ccb..c35f3fcb 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 002dd876..47ba2c8d 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 00000000..e4808f3a --- /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 e201e595b5fe2ef77d52b333bdf68b3da18cc478 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 19:08:13 -0300 Subject: [PATCH 27/81] 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 c48fa913..4b7da548 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 2f275f98..678dcffd 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 c35f3fcb..c8b2772e 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 c410b92bb802e9c809128c3fa49eecb15dafc3c0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 17 Mar 2026 22:36:50 -0300 Subject: [PATCH 28/81] 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 4b7da548..e881adfb 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 678dcffd..86336aa2 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 0e9ba19b4f8b3f04b5f1909a77c63c4d158900d1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 18 Mar 2026 14:32:38 -0300 Subject: [PATCH 29/81] 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 fad7c7d7..ed6d57d8 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 5a2770ed..8147ca82 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 fd9f1821..56242f98 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 0dbc8fc8..66b9a2bc 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 e881adfb..0f0848e7 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 7215a2d8..6eab2730 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 a66a5978..edb66ee2 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 18d81c73..82673322 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 139fdff6..1ff2e093 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 94a3288b..611eb243 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 440844d1..b5f0cd4a 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 7f62f9b2..4ec9ba58 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 83331719..cbbc3bc4 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 e64e699f..33c503a6 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 37c86cf0..2edfb50f 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 86336aa2..72ccb033 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 0fd485a2..c6d0c1a9 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" @@ -308,7 +309,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 a8158e63..9585769f 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -760,7 +760,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()); @@ -941,7 +944,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) { @@ -1039,7 +1042,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 41f2c692..5c271a22 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 3f763b17..e1ebb764 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 e4808f3a..450f10a1 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 b2e9fd6a..a0795f32 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 6bbbf22fa7bf8825b23355aa0944250ca327cf97 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:13:42 -0300 Subject: [PATCH 30/81] 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 1fc583a1..b5907396 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 00000000..97663291 --- /dev/null +++ b/tests/test_helper.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace session { + +class TestHelper { +public: +}; + +} // namespace session From 68ad1c14edfe681316cd64f4c489c942ac32dc74 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:45:55 -0300 Subject: [PATCH 31/81] 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 b5907396..3b3f6cd4 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 66b9a2bc..4367c3a2 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 a5268441..09f30afa 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 edb66ee2..2762c42d 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 66a21635..cbc1ebeb 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 9b97e216..b599750b 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 47ba2c8d..9e5c6fc5 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 97663291..a1f89345 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 5aa76f768eb8c82510dcd8d7dfbe0989a520e7fa Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 14:46:55 -0300 Subject: [PATCH 32/81] 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 d66944ab..a6d07f47 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit d66944abf027dacc07d7cb0e58aae53aaecd8b8a +Subproject commit a6d07f47310bcabbe5ee194cc9f8b2d60d163b5d From 677cb2b62f60bf87991c21d13d658ebb9d7b5651 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 16:11:04 -0300 Subject: [PATCH 33/81] 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 3b3f6cd4..dfa5f08c 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 cbc1ebeb..e00c8367 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 72ccb033..2c272605 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 b599750b..bb336cb3 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 a1f89345..a56558f9 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 d46ce2703ae9d6f9a1ce5fa00dcbf22fdcdc9dde Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 16:14:05 -0300 Subject: [PATCH 34/81] 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 00000000..434f7daf --- /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 8acf6e9e9fdc527a83ce91280d348549232a10f2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:00:46 -0300 Subject: [PATCH 35/81] 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 e3105bae..f96ae204 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 0f0848e7..2e798087 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 2c272605..e4be4ce8 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 c8b2772e..14858741 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 38c9f65f..048b0b91 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 9e5c6fc5..1d0d8864 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 00000000..e627ff3a --- /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 a56558f9..f3781214 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 84d0d07612b480c72f514d3e81110f9e6146268f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:09:10 -0300 Subject: [PATCH 36/81] 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 00000000..6e181960 --- /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 00000000..4f886314 --- /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 b6bd34519aba962362c3900179864780a2c66fbf Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:15:55 -0300 Subject: [PATCH 37/81] 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 e00c8367..a8bae492 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 89cf0a76f3b39d7507b799358b78d64081cb437e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 19 Mar 2026 17:32:31 -0300 Subject: [PATCH 38/81] 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 a6d07f47..3763407c 160000 --- a/cmake/session-deps +++ b/cmake/session-deps @@ -1 +1 @@ -Subproject commit a6d07f47310bcabbe5ee194cc9f8b2d60d163b5d +Subproject commit 3763407c57d4fefce2b6e764be709d7af9a951c3 From f74901572935101213751a66be04d37cd2fdd02e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 14:19:36 -0300 Subject: [PATCH 39/81] 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 00000000..559ff78d --- /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 86466548..b3856063 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 a8bae492..b198ac90 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 00000000..15f8f1e3 --- /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 00000000..d8b08b4e --- /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 00000000..59a5adfe --- /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 00000000..e162ee07 --- /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 00000000..755b1544 --- /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 00000000..3238b89c --- /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 00000000..13b8996b --- /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 00000000..bec65251 --- /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 00000000..16613df4 --- /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 00000000..bbf1f4c4 --- /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 00000000..a8b41c12 --- /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 00000000..a50b53ad --- /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 00000000..18abd4a1 --- /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 00000000..a5891ebb --- /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 00000000..4a343169 --- /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 1d0d8864..245ca736 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 00000000..57b2088e --- /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 00000000..bee98d06 --- /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 69217d222fd387459b4aad45b9f90386c434d896 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 16:48:49 -0300 Subject: [PATCH 40/81] 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 dfa5f08c..fba036c3 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 09f30afa..70c028c8 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 e4be4ce8..a68cfce3 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 bb336cb3..4e6b78f8 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 e627ff3a..9d49bed9 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 450f10a1..7b1a8fcb 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 f3781214..f2fdc0ab 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 4f886314..f5306a8d 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 1542652b134186918e91470b6b5b339b7ad7656c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 18:12:30 -0300 Subject: [PATCH 41/81] 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 559ff78d..f25f6bea 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 15f8f1e3..681b1b59 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 4a343169..52bdf47d 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 57b2088e..fc2b4109 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 825941d74a1fbd4997f88dcdbab14bc9d41f6573 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 19:04:40 -0300 Subject: [PATCH 42/81] 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 f25f6bea..4f143400 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 52bdf47d..a1383605 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 fc2b4109..2eb0a5b1 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 ad17263a57b3ed3a96cc201770f626d7899a251c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 22:57:39 -0300 Subject: [PATCH 43/81] 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 f96ae204..8ea436bb 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 fba036c3..4eefe4c5 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 70c028c8..804f4e2d 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 4f143400..3149ec9f 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 b3856063..a607deae 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 b198ac90..396761f5 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 4e6b78f8..6edcc471 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 a1383605..9ae7c0a5 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 2eb0a5b1..2103af48 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 85959c508f142ad0c349d14d2163afa21fa4173b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 18:55:27 -0300 Subject: [PATCH 44/81] 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 396761f5..c582d1e5 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 6e181960..4d94660b 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 f2fdc0ab..15853ac2 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 f5306a8d..b7be2e7c 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 49721e3462c63f0cac0e4e9848fb462764e3f1a0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 22:26:34 -0300 Subject: [PATCH 45/81] 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 434f7daf..f8941240 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 4eefe4c5..e32e9019 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 ef62d988..e0e314c2 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 c582d1e5..b8e39ebd 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 00000000..d439d691 --- /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 0d7411a9..db35fce8 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 245ca736..5e1c2fcd 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 15853ac2..b47a9ee4 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 00000000..ea7c9621 --- /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 b7be2e7c..d2846ea6 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 5db8927e..572229e1 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 74a9ae35..e31f9f9c 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 39ca6dab90beac2a3219e5a534a1e36275594ee7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 20 Mar 2026 23:20:50 -0300 Subject: [PATCH 46/81] 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 8ea436bb..9d16f90e 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 e32e9019..6c2b6452 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 b8e39ebd..b984fcac 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 d439d691..62025ee5 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 b47a9ee4..407026df 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 ea7c9621..b783d1f3 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 6394a31b0e569163abf7006f70f52e197a240306 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 21 Mar 2026 00:25:31 -0300 Subject: [PATCH 47/81] 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 9d49bed9..2aae98c7 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 b783d1f3..7a42c4a1 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 c8b07bc11d1fcbfb40f2226cdea7985456704daf Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 23 Mar 2026 12:59:02 -0300 Subject: [PATCH 48/81] 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 6c2b6452..50c0c0c6 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 4367c3a2..f09a4e45 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 b984fcac..eabf6205 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 2aae98c7..f62c4ec0 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 a3045324ad22f4f960eda9dc87a5b44fe8fcdfd8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 23 Mar 2026 22:18:21 -0300 Subject: [PATCH 49/81] 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 6eab2730..bd43158d 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 a68cfce3..e7c827ea 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 b698f6b3..4e6d467a 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 3278568f758d9e24963f11f88eeaf93f45e4b37c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:07:47 -0300 Subject: [PATCH 50/81] 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 bd43158d..3196cb2e 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 4e6d467a..b698f6b3 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 ab1e2bc1..d89786ac 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 7e82f4e2be986bd98b8672a25ac85cd7ebc70fb2 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:10:14 -0300 Subject: [PATCH 51/81] 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 e1ebb764..7a10b245 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 395cf652537483862a3096ac4d8158022a7776ad Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 15:30:47 -0300 Subject: [PATCH 52/81] 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 e9cfe3be..3a899845 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit e9cfe3beaa2d9a9169f1ba99a7025aa534a6f2d0 +Subproject commit 3a899845f56e4293bffafbc82a8e9624420ba1b2 From 44ee036e17b4c28789de72b8c8889d9d7a9ec90a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 19:21:28 -0300 Subject: [PATCH 53/81] 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 72d7c1be..982d2ae1 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 8a6de921..743c97a2 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 7635c77e..29e579ef 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 1ff2e093..bebb2b15 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 81aa25c5..21538d19 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 943e2d27..b03dce82 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 7a10b245..72e12fa1 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 9358b99b..83f017e7 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -272,7 +272,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) { @@ -282,13 +282,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, @@ -300,8 +299,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( @@ -311,14 +309,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, @@ -330,8 +326,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`. @@ -386,7 +381,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, @@ -409,10 +404,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; @@ -454,7 +446,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; } @@ -502,7 +494,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), @@ -605,7 +597,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); @@ -630,7 +622,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( @@ -1378,10 +1370,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 e82092af..1175d4c4 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 1ff6236e..7b65418a 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 8824b1e22524dad94ede00522e96c091e61b657c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:22:56 -0300 Subject: [PATCH 54/81] 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 3a899845..faa8c0e4 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 2762c42d..46b3280b 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 a607deae..9e1d16c4 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 eabf6205..0e5247d6 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 e7c827ea..7a48ef8b 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 73c9e310..5c73d7e5 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 c9c16127..02c36030 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 5e1c2fcd..ea7f38b4 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 00000000..2b51c91d --- /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 00000000..1fd25fdc --- /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 00000000..401b59a2 --- /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 00000000..75c7f1fe --- /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 00000000..f05ddd30 --- /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 76c4cc49..4a9d4e5f 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 407026df..733aebbf 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 a0795f32..0db72c64 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 a66684dd202e1e4238f13571de070ef79dd18961 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 24 Mar 2026 20:11:12 -0300 Subject: [PATCH 55/81] 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 3196cb2e..68010540 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 29e579ef..87837a8a 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 9e1d16c4..0a5d2d94 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 72e12fa1..a95c2daf 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 ea7f38b4..9508537a 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 7b65418a..d445ceb8 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 21db80b6c944b8bc805259a098b847184edab035 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:29:01 -0300 Subject: [PATCH 56/81] 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 2571e61f..2a3271ac 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -75,7 +75,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 7e325d58..e42b815a 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 733aebbf..683fd1c4 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 d2846ea6..141cfcd2 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 791bd9405ba365857f727b45cbc05bd741b497e6 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 16:31:37 -0300 Subject: [PATCH 57/81] 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 804f4e2d..5bb759aa 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 87837a8a..11a55cd5 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 0e5247d6..eb768dc4 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 7a48ef8b..88fc7ad8 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 6edcc471..67a51f77 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 a95c2daf..3a133188 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 28b146ad8bedbd01b15d895be40f0075f7bacdb3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 25 Mar 2026 17:16:31 -0300 Subject: [PATCH 58/81] 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 2b51c91d..49c942d6 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 7a42c4a1..11f3a69c 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 141cfcd2..b83ee2a5 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 3f710cd182374fa45285d2a89b6aaa205bbbb409 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 00:06:47 -0300 Subject: [PATCH 59/81] 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 4aa45e9c..f19e8099 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 11a55cd5..40899888 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 bebb2b15..aa0a1534 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 3a133188..067a1d58 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 83f017e7..55900e0d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -384,20 +384,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; @@ -490,12 +484,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, @@ -599,7 +590,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) { @@ -1442,9 +1433,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 d445ceb8..337961d5 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 d1ed14be..ff65e1eb 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 bb55fa95612ff4efbf84da669338df0c7be48ebe Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 01:16:47 -0300 Subject: [PATCH 60/81] 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 6e997e79..50d9e7c5 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -113,7 +113,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; @@ -309,14 +309,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 @@ -329,14 +329,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 @@ -363,9 +363,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); @@ -379,9 +379,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); @@ -405,7 +405,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); @@ -420,7 +420,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); @@ -530,14 +530,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 @@ -550,14 +550,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 82673322..aae7805e 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); @@ -603,12 +603,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 /// @@ -633,12 +633,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 2a3271ac..209fb240 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -20,7 +20,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 aa0a1534..9c00b88d 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 5029cab3..57110522 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 56ed1e07..039e7e69 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -231,9 +231,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(); @@ -305,7 +305,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 cbbc3bc4..024795b9 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 69a491c6..5e0b8cdf 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 1f59e181..14e53080 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -123,7 +123,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); @@ -174,11 +174,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; @@ -222,7 +222,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); @@ -245,11 +245,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; @@ -349,8 +349,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; @@ -405,8 +405,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; @@ -523,7 +523,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; @@ -561,7 +561,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; @@ -749,12 +749,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; @@ -778,7 +778,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); @@ -795,12 +795,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, @@ -892,22 +892,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 { @@ -936,23 +936,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( @@ -980,15 +980,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 = {}; @@ -1016,14 +1016,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 = {}; @@ -1050,12 +1050,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 = {}; @@ -1078,12 +1078,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 = {}; @@ -1256,7 +1256,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; @@ -1321,7 +1321,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; @@ -1382,7 +1382,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; @@ -1470,23 +1470,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 { @@ -1515,24 +1515,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 { @@ -1618,7 +1618,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 067a1d58..8bb30379 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 55900e0d..2ad6ef5d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -48,8 +48,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 @@ -66,9 +66,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); @@ -82,19 +82,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; } @@ -152,7 +149,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: {})", @@ -163,7 +160,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())}; @@ -176,7 +173,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; @@ -270,59 +267,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; @@ -333,12 +330,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*/; @@ -349,14 +346,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) { @@ -374,23 +371,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); @@ -420,14 +417,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 " @@ -436,9 +433,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; @@ -467,14 +464,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(), @@ -484,7 +481,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, @@ -528,7 +525,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) @@ -587,7 +584,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 @@ -603,7 +600,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; @@ -611,7 +608,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) { @@ -628,22 +625,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 @@ -760,8 +757,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) = @@ -780,7 +777,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); @@ -892,7 +889,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 @@ -911,7 +908,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( @@ -920,7 +917,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 @@ -956,13 +953,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{ @@ -1059,7 +1056,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); @@ -1147,7 +1144,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, @@ -1163,8 +1160,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; } @@ -1356,9 +1353,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; @@ -1367,7 +1364,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, @@ -1434,7 +1431,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 = @@ -1449,13 +1446,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; @@ -1533,8 +1530,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 5efa976d..8f92b201 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 053905c2..93b8a524 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 f307eae9..630c9d06 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 4a4f7240..864bf350 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -83,12 +83,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); @@ -115,9 +115,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); @@ -129,11 +129,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( @@ -153,9 +153,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); @@ -218,9 +218,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; @@ -452,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; + std::array fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); nlohmann::json j; @@ -557,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; + std::array fake_gen_index_hash; randombytes_buf(fake_gen_index_hash.data(), fake_gen_index_hash.size()); auto obj = nlohmann::json::object(); @@ -756,9 +756,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); @@ -917,12 +917,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); @@ -946,9 +946,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 = {}; @@ -1160,12 +1160,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); @@ -1190,9 +1190,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 = {}; @@ -1282,9 +1282,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 ff65e1eb..856d1226 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 b41cc79cb3c6cee7ac66006a796d79ccd9cc31a9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 15:37:49 -0300 Subject: [PATCH 61/81] 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 50c0c0c6..cd480d5d 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 f09a4e45..a8e3dfac 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 2e798087..1fdcadb4 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 00000000..60e73a13 --- /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 aae7805e..a2609ac5 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 9c00b88d..61765775 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 eb768dc4..63d7b409 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 88fc7ad8..c7c46219 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 2ad6ef5d..91979b8c 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -632,7 +632,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; @@ -891,7 +891,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 9508537a..d201d743 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 00000000..8433d0df --- /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 b83ee2a5..62153941 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 efe62b10a16fa5994d58d3b09c5047e40895e341 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 18:25:46 -0300 Subject: [PATCH 62/81] 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 cd480d5d..f413c11d 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 a8e3dfac..6c53b98b 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 1fdcadb4..68761373 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 46b3280b..77a124bc 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 50d9e7c5..161cdb50 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -113,7 +113,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 61765775..61bb8bf4 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 63d7b409..62a65954 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 c7c46219..ec586a71 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 14e53080..5dfe8faf 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -907,7 +907,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 { @@ -952,7 +953,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( @@ -1486,7 +1488,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 { @@ -1532,7 +1535,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 8bb30379..e2921a44 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 91979b8c..0c27029f 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -160,7 +160,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())}; @@ -275,8 +276,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); @@ -291,8 +293,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; @@ -304,8 +307,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); } @@ -318,8 +322,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; @@ -371,7 +376,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; } @@ -381,7 +387,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, @@ -423,8 +430,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 " @@ -600,7 +607,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; @@ -1431,8 +1439,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 8433d0df..ba9dd52e 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 683fd1c4..19cb28ee 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 62153941..23e25050 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 337961d5..e86c9ed6 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 0db72c64..72515b0a 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 523fec7a54cac10a1ab4aec7e0d238c5ffdd0ae4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 18:44:35 -0300 Subject: [PATCH 63/81] 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 f413c11d..f79010f4 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 62a65954..20348b4c 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 2a03826ac40c64c13170648ef1aaed822cba321e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 19:50:01 -0300 Subject: [PATCH 64/81] 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 743c97a2..99901b9d 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 144bc39a..3a370793 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -125,25 +125,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; @@ -403,13 +384,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. @@ -449,7 +430,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, @@ -637,55 +618,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 61bb8bf4..b967bbcb 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 e2921a44..31d6b4b3 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 0c27029f..a639ab57 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -268,77 +268,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) { @@ -381,260 +310,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( @@ -1233,8 +1065,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, @@ -1245,23 +1101,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 @@ -1277,24 +1125,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 @@ -1305,15 +1145,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 @@ -1329,78 +1166,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 ba9dd52e..a898d4fd 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 1175d4c4..98b71553 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 856d1226..8dabe228 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 12ec37e7f8c8279cf620e0041a728b0ecfb00280 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 22:33:12 -0300 Subject: [PATCH 65/81] 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 68010540..8ff7961e 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 08c78a4b..89a95b73 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 039e7e69..4c40b2e4 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -307,19 +306,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 21538d19..d8ddae8a 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 b5f0cd4a..e1229ba9 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 6fee7c95..b55c36aa 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 4ec9ba58..bee7de48 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 8a37e20b..12dfee9a 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 fb6b6e43..f526cacc 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 024795b9..84857fca 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 3b6575d3..f95c2bb6 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 192189ef..4a53871e 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 a87638d7..26c5281c 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 2edfb50f..3de898a8 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 ec586a71..16c78ce1 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 c2a77197..fa5a39c5 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 b698f6b3..2eb885ac 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 93c7f72c..ae4b69f2 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 5399f1a7..ca26d77f 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 5c271a22..033b6a39 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 5dfe8faf..104044d3 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -213,9 +213,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( @@ -377,9 +375,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>(), @@ -539,14 +535,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 = {}; @@ -767,11 +757,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 31d6b4b3..a359ff0f 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 a639ab57..99bb23e6 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -54,15 +54,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 959a8c0b..97a64fb2 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 24948943..748a2458 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 87383753..940db8a8 100644 --- a/tests/test_encrypt.cpp +++ b/tests/test_encrypt.cpp @@ -1,5 +1,4 @@ #include -#include #include #include From 70a5c55f03b8962b32fed54fb615080f96c66d1b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 26 Mar 2026 22:09:22 -0300 Subject: [PATCH 66/81] 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 00000000..0b3de7e4 --- /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 00000000..2bbbbb60 --- /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 8ff7961e..f2dddc90 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 00000000..e9dc0c50 --- /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 611eb243..a4b0ddb1 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 5e0b8cdf..f03c39b7 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 a359ff0f..338a4ea6 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 18559f96dc87d9c993ea256783f35a37b9acff35 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 19:48:48 -0300 Subject: [PATCH 67/81] 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 6c53b98b..06344446 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 5bb759aa..bd97c254 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 40899888..594ee9bc 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 20348b4c..b97b4a62 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 338a4ea6..5ebdf7a7 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 a898d4fd..bb936c77 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 590b0d6eb91c706e56c95a85b75d4322b40bb8d8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 23:40:25 -0300 Subject: [PATCH 68/81] 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 57110522..6c2d59c8 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 4c40b2e4..aeeb94d5 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -923,7 +923,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; } } @@ -947,7 +948,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; } } @@ -975,7 +977,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; } } @@ -1009,7 +1012,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; } } @@ -1034,7 +1037,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(); } } @@ -1054,7 +1057,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; } } @@ -1069,7 +1073,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 2525666a..05c857bc 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 9585769f..6c8b36c1 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" @@ -1104,17 +1105,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" { @@ -1214,8 +1204,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 @@ -1408,7 +1400,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 104044d3..c76d23b4 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*/ { @@ -911,13 +913,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; } @@ -955,13 +951,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; @@ -992,13 +982,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; } @@ -1026,13 +1010,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; } @@ -1054,13 +1032,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; } @@ -1082,13 +1054,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; } @@ -1117,13 +1083,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; @@ -1150,13 +1110,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; @@ -1179,13 +1133,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; @@ -1212,13 +1160,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; @@ -1420,27 +1362,22 @@ session_pro_backend_get_pro_details_response_parse(const char* json, size_t json 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; } } @@ -1492,13 +1429,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; } @@ -1539,13 +1470,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; } @@ -1578,13 +1503,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 5ebdf7a7..59989791 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 99bb23e6..d4f3755a 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 @@ -1070,14 +1071,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; } @@ -1194,13 +1188,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; } @@ -1211,13 +1203,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; } @@ -1234,20 +1224,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 @@ -1255,15 +1238,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); @@ -1315,13 +1291,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; } @@ -1340,15 +1314,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 8f92b201..cc8ec5e4 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 7eee001bfd2e824e887305ac2db1ec6dd6a99ff0 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 22:47:53 -0300 Subject: [PATCH 69/81] 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 f79010f4..0d1b9b3d 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 06344446..8a60b858 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 594ee9bc..0fd37bec 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 b967bbcb..6f2a2371 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 b97b4a62..116aaa5e 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 59989791..10ee8558 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 d4f3755a..7ff85c49 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -396,7 +396,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: @@ -410,7 +410,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); @@ -1092,7 +1092,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 d201d743..d2f6501c 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 bb936c77..d0c2ec4a 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 00000000..c4e82c62 --- /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 19cb28ee..15cbc420 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 e86c9ed6..bbe4b524 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 fd5bf5766255e021e99deb1ded413210a6ff0c1f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 00:34:18 -0300 Subject: [PATCH 70/81] 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 9d9444af..c15b740b 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 faa8c0e4..07f70d4e 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 3cb568f1..5550645f 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 00000000..43f3e9eb --- /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 d72ce9ac..61dc0b1a 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 b3f417c9..339aa78e 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 b89f58f3..af6d0b75 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 0743e427..16e13c56 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 2bcefa29..7e49afd0 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 a4b0ddb1..b9066f96 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 0a5d2d94..a9b12358 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 00000000..2e22a215 --- /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 c6d0c1a9..e37b9e71 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"; @@ -85,12 +95,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 ff8517fa..59d5b916 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); @@ -159,6 +164,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 acba0302..c7293ead 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 ca26d77f..ab3cbf6e 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -1043,7 +1043,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 5c73d7e5..76c32772 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 6c8b36c1..c5432862 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( @@ -1869,7 +1877,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'; @@ -1877,7 +1885,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 afe3d441..ab290a9b 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 d2f6501c..e8181390 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 00000000..37796108 --- /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 49c942d6..167f6622 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 00000000..b7966dc8 --- /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 00000000..1c05e5bf --- /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 914e27a8561c0b28f9907ed102151e510184d9c1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 31 Mar 2026 16:44:03 -0300 Subject: [PATCH 71/81] 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 5550645f..244851ee 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 { 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 aeeb94d5..fe90f1e8 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -118,249 +118,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); + auto out = make_buffer(encrypted_size(enc.data_size())); - std::array nonce_key; - - 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( @@ -376,82 +208,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; @@ -886,6 +669,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 61d02843..4aef7dd2 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -362,3 +362,94 @@ TEST_CASE( CHECK(contents.size() == data.size()); CHECK(!!(contents == data)); } + +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 5518fc2c438d165caec4d33990c8fadf513e4f73 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 1 Apr 2026 18:13:30 -0300 Subject: [PATCH 72/81] 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 244851ee..b11841ce 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 43f3e9eb..93a0b5ea 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 16e13c56..5d5efbe4 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 1dbbbcee..e57d2371 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 d20526d6..c7fd06b6 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 77a124bc..a66f9a10 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 7e49afd0..f3681593 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 fe90f1e8..76009e5e 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -700,8 +700,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) { @@ -710,12 +714,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) { @@ -850,33 +849,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 2e22a215..5f7e5a58 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 e37b9e71..4e6e6fd0 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -122,12 +122,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 59d5b916..5e1ea7fd 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -173,10 +173,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 c7293ead..8c12bef0 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 ab3cbf6e..69767ab2 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -576,6 +576,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()) @@ -853,6 +926,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); @@ -877,100 +1050,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()); @@ -978,12 +1065,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 76c32772..bcedb13c 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 c5432862..cc586d38 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( @@ -505,21 +512,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]( @@ -550,25 +548,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]( @@ -1833,8 +1860,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 37796108..be94abdc 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 167f6622..a5225374 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 b7966dc8..b80f1b5c 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 1c05e5bf..56452cac 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 4aef7dd2..b4d4222a 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -365,16 +365,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); @@ -393,13 +384,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; @@ -415,8 +405,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 b3cbf6934a53db1863b1ec5b1880f32133b02a02 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 1 Apr 2026 20:30:31 -0300 Subject: [PATCH 73/81] 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 07f70d4e..a920d2a8 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit 07f70d4e401705b6d70c1ef5f77724d2904dd2f6 +Subproject commit a920d2a8c33de60ef7d462f7c85be08e8aed5f9b From ba100b285bde21d48ea8b51044dcaeef975673a3 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 00:55:54 -0300 Subject: [PATCH 74/81] 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 6b547e5a..bc045120 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 ed6d57d8..1a44c5ec 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 7fd175c4..87bd3578 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 61dc0b1a..7d9178c1 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 6f2a2371..c451f71b 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 a9b12358..0b6f3012 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 d8ddae8a..762f347a 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 e1229ba9..84146fdb 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 b55c36aa..9df541d2 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 12dfee9a..a0bb3450 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 16c78ce1..8050fc49 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 45539511..41b3e3d7 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 048b0b91..8f1c47b5 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 00000000..10befb2b --- /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 d7ef7a0e..f90c4869 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 a9daea6c..8cea8180 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 b03dce82..bcef80fc 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 2eb885ac..162558a0 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 779b1f66..c0fa70d0 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 ae4b69f2..a7e7e2bf 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 cc586d38..d189d157 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))} { @@ -1662,7 +1658,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); }); } @@ -1675,7 +1671,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 89230aee..1e2ff54c 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 e42b815a..4ac7ed4f 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 7ff85c49..7ac297df 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" @@ -221,18 +222,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) { @@ -244,22 +243,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; @@ -1035,28 +1041,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 2891dcb0..a540a0c7 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 db35fce8..940dc369 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 748a2458..05ba68f6 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 06187405..f27a59b5 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 9eed94ed..8507575d 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 887ea581..d4f41e48 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 f79706fb..1b5701a6 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 8ebfde3f8604798d2f8380ea6ab496b5f12d9cc1 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 01:03:11 -0300 Subject: [PATCH 75/81] 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 a920d2a8..08550d47 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 93a0b5ea..fee84469 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 7d9178c1..d1db4a1a 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 339aa78e..dca416a8 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 af6d0b75..b751ab6d 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 c7fd06b6..24c90048 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 43451f18..fdc51334 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 f3681593..90e41e74 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 fbccb768..fa039c3b 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 5f7e5a58..c5e2ed63 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 4e6e6fd0..67010cf0 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 @@ -111,22 +112,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 5e1ea7fd..57e9ce2d 100644 --- a/src/network/network_config.cpp +++ b/src/network/network_config.cpp @@ -311,10 +311,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 bcedb13c..da0de8be 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 d189d157..e563e5e7 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( @@ -1228,7 +1228,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; } @@ -1417,8 +1418,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 02c36030..9f22c8e7 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 56452cac..674f03a4 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 01d7e749f0e3c3219dbf706263d4328f50cef798 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 27 Mar 2026 15:49:03 -0300 Subject: [PATCH 76/81] 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 8d8d667d..f211ea6e 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 05420f57..4928f072 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 161cdb50..93615562 100644 --- a/include/session/pro_backend.h +++ b/include/session/pro_backend.h @@ -131,8 +131,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; @@ -140,7 +140,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 @@ -157,22 +157,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 @@ -191,7 +191,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; }; @@ -209,8 +209,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; }; @@ -265,8 +265,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 3a370793..17328ccf 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -77,10 +77,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 @@ -140,10 +140,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; @@ -168,8 +168,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; }; @@ -191,7 +191,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; }; @@ -242,8 +242,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 @@ -436,7 +436,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, @@ -497,8 +497,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, @@ -611,8 +611,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 6c2d59c8..57347032 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 b9066f96..27f681a8 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 7ac297df..0fe5922c 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -967,8 +967,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, @@ -1088,7 +1088,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, @@ -1111,8 +1111,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, @@ -1152,8 +1152,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 93b8a524..1fe7845c 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 864bf350..cbdc4f5a 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -74,12 +74,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); { @@ -890,12 +890,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 8dabe228..9bf49c39 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 58c821ea94c2fca807fa6872db7da2136f930bab Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 12:57:56 -0300 Subject: [PATCH 77/81] 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 1fe18bd5..d413e083 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 00000000..82af458b --- /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 08550d47..faa8c0e4 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 b11841ce..8d6de2ac 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 fed3d8ff..a42122e7 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 1be5f283..29a74491 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 1a44c5ec..cb1a2933 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 232e53e3..6803ff67 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 7490c202..b777c3bc 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 8147ca82..c2e65741 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 406de602..4f54ca0e 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 66c0149c..33a43c69 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 5eaa27bb..e3d36707 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 0ea32b00..1d0a8c74 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 7c8e1159..febd1d6c 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 d8523be7..01b0cffd 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 7c82b457..2f73f51b 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 2880aed3..e8f59c1a 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 8a60913b..8b354cc1 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 56242f98..d994da39 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 0d1b9b3d..cb5e5877 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 8a60b858..3a1dc58e 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 68761373..fca59b0b 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 bd97c254..605e9d9e 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 be8f862d..bd559695 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 60e73a13..679989f5 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 0b3de7e4..00000000 --- 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 00000000..03fb00c7 --- /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 00000000..967812f3 --- /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 00000000..748cf0a6 --- /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 b476f5ac..00000000 --- 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 573a66fe..a85adc10 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 99901b9d..00000000 --- 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 2bbbbb60..00721f8c 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 b70980d1..7376328e 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 f2dddc90..27169b86 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 e9dc0c50..00000000 --- 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 89a95b73..cf92b11e 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 d1db4a1a..62433d4b 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 d3d2b1ef..98861e43 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 b751ab6d..fb3ed850 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 24c90048..9e8550f4 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 8adc1ef4..c9813b37 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 90e41e74..a1112e1c 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 99890cbe..71a4486d 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 47bb1f28..45b92a53 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 91857904..bffd8b94 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 6a1d7c0a..6638dbce 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 a2609ac5..bb31fb6c 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); @@ -562,10 +567,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; @@ -598,15 +603,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 /// @@ -631,12 +636,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 209fb240..878150b6 100644 --- a/include/session/random.hpp +++ b/include/session/random.hpp @@ -41,9 +41,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. @@ -53,7 +58,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 f19e8099..673f26d3 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 0fd37bec..fde18fa2 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 c451f71b..dc26b81b 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 c14ed7f5..0a071287 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 27f681a8..f2be5def 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 e0e314c2..7ffbbc3d 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 0b6f3012..58e382aa 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 76009e5e..44851014 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -97,15 +97,15 @@ std::optional decrypted_max_size(size_t encrypted_size) { // 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; @@ -261,12 +261,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(); @@ -296,7 +295,7 @@ size_t decrypt( reinterpret_cast(padbuf.data()), nullptr, &tag, - inpos, + to_unsigned(inpos), chunk_size + ENCRYPT_CHUNK_OVERHEAD, nullptr, 0) != 0) @@ -355,7 +354,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 762f347a..b9384ed4 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 c1df4f69..fd056172 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 84146fdb..96037e4f 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 1832f6f6..0b89b520 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 9df541d2..db511741 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 bee7de48..7c2878fb 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 a0bb3450..3072044e 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 f526cacc..1ede3a39 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 84857fca..d7ee8dfd 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 5d40d5fe..bd1c086b 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 d386dad3..f7c53935 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 33c503a6..828ffdf6 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 f95c2bb6..4f8c79ed 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 4a53871e..9c32b8bf 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 bfd5d7af..311dd0e1 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 26c5281c..93ac2be3 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 3de898a8..a3bbceab 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 116aaa5e..1c8a941a 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 8050fc49..df80c115 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 67a51f77..4119c004 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 fa5a39c5..85e1703b 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 ad7d1d7f..ecdc3466 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 00000000..28c521ce --- /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 00000000..f49d4fba --- /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 00000000..1b5c926b --- /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 8cea8180..f34d39f2 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 bcef80fc..00000000 --- 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 162558a0..453359e7 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 a7e7e2bf..8b9b3205 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 67010cf0..950a90be 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -167,7 +167,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()) @@ -276,7 +276,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) { @@ -299,7 +299,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(); @@ -346,9 +346,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); @@ -442,7 +443,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 1e65a048..fcfa7664 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 69767ab2..6eb5ca0e 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; @@ -1598,7 +1598,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 da0de8be..5aa67cf6 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 d9d50d49..d0e5a3d0 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 e563e5e7..d97bcad1 100644 --- a/src/network/session_network.cpp +++ b/src/network/session_network.cpp @@ -1735,9 +1735,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) @@ -1826,9 +1826,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 {}; @@ -1914,11 +1914,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 4b40e132..b49c8dee 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 9f22c8e7..f1a8e390 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 dcb8b2c0..648fa315 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 f03c39b7..1fcee7da 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 e83640d6..fb8cfd96 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 1e2ff54c..bd049b57 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 c76d23b4..3fcf86e8 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -125,7 +125,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); @@ -176,31 +176,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{ @@ -218,59 +198,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, @@ -280,25 +227,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( @@ -349,30 +286,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; @@ -380,72 +297,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 { @@ -519,64 +397,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) { @@ -739,24 +586,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); @@ -764,34 +601,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, @@ -802,22 +630,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 { @@ -870,6 +691,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 @@ -892,16 +714,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, @@ -932,15 +751,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, @@ -966,13 +782,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, @@ -995,13 +809,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, @@ -1022,12 +834,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; @@ -1044,12 +854,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; @@ -1406,15 +1214,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 { @@ -1447,15 +1252,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 4ac7ed4f..d602b293 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 10ee8558..38f33e75 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 0fe5922c..2f3b786a 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -48,10 +48,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 @@ -60,40 +60,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) { @@ -140,28 +110,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 { @@ -169,7 +128,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; @@ -190,8 +149,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; } @@ -268,8 +227,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*/; @@ -280,19 +239,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. @@ -305,9 +264,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 @@ -315,36 +272,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); @@ -366,48 +312,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. @@ -429,20 +369,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 " @@ -469,70 +409,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()) { @@ -580,58 +465,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 @@ -658,9 +499,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()) { @@ -714,24 +555,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 @@ -748,7 +670,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( @@ -757,8 +679,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) @@ -793,13 +714,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{ @@ -874,9 +795,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` @@ -885,8 +804,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()); @@ -896,11 +817,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)}); } } @@ -969,10 +892,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; @@ -982,16 +905,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( @@ -1000,10 +924,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( @@ -1095,12 +1021,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}); }); } @@ -1119,13 +1047,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}); }); } @@ -1139,9 +1069,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}); }); } @@ -1160,13 +1091,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}); }); } @@ -1201,41 +1134,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 @@ -1288,9 +1239,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 ab290a9b..0681f3e7 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 a540a0c7..4acd3ad7 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 940dc369..57e8c515 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 a5225374..a73fe255 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 401b59a2..8391ed33 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 a760a178..b90084b1 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 98c565f4..45d9d013 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 97a64fb2..b2214799 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 e8b4a6cc..03d9b5e8 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 fa361e33..430f6dfc 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 e7bb0ae5..500e5bd7 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 74b30dc5..4a6034c8 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 fc3e2939..451456a6 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 1fe7845c..84092cb0 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 dfb5ce33..40ef56d3 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 630c9d06..b43e2a31 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 05ba68f6..9da83a81 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 f62c4ec0..6f3c37ff 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 14323dd2..ad03537b 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 d0c2ec4a..00907b5a 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 98b71553..8064fe55 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 940db8a8..2c304e58 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 4dfea5c1..73d8ed41 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 0af36f31..93212af5 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 017abe75..87485067 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 d89786ac..a35be41d 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 15cbc420..b162573c 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 8507575d..fbbcf7db 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 e48e3169..a4afbe0b 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 79f3bfd7..6509267a 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 11f3a69c..ec8856b5 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 23e25050..0a97ca84 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 cbdc4f5a..dc266909 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -129,12 +129,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, @@ -452,7 +448,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; @@ -557,7 +553,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(); @@ -899,7 +895,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 1b5701a6..bb673c30 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 bbe4b524..d631368b 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 9bf49c39..4c84e448 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 107233ec..0aa1bc2b 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 e31f9f9c..7440ab94 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 72515b0a..4f692a15 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 dd0b5bd271e1255d95ee6ffa5ef31abcfca6473c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 8 Apr 2026 21:24:46 -0300 Subject: [PATCH 78/81] 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 faa8c0e4..8d1df2ef 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 cb5e5877..3ac2d1e7 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 03fb00c7..2fcdefb7 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 44851014..a1906e92 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -713,7 +713,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) { @@ -748,12 +748,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 28c521ce..ca13757b 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 8b9b3205..1cfd09e1 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 c5e2ed63..8076e92a 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 6eb5ca0e..54ce45cc 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -596,12 +596,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; @@ -609,10 +607,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 f1a8e390..d5e4948b 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 38f33e75..45f83698 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 2f3b786a..55a4324d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -1153,17 +1153,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) @@ -1176,16 +1174,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 b80f1b5c..9883f5c9 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 00907b5a..cb48d79b 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 c4e82c62..a200b4f1 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 ffe66ea19ea38e09ca478543bf8019319de4bf93 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 9 Apr 2026 16:37:47 -0300 Subject: [PATCH 79/81] 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 00000000..7783e32f --- /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 e8181390..19535d8b 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 00000000..00459888 --- /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 65c2958c881f54eab93f278236d517cc61ec3d2e Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 Apr 2026 00:33:44 -0300 Subject: [PATCH 80/81] 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 cb1a2933..dc18673d 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 e3d36707..786d5d75 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 3ac2d1e7..e4c8a9a1 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 605e9d9e..41692b41 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 2fcdefb7..a44dcd4d 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 00721f8c..2c5bb580 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 7376328e..46cba9de 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 7783e32f..5b2a9936 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 27169b86..275809a0 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 fde18fa2..23ab4486 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 dc26b81b..9efa38b5 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 0a071287..66da6622 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 58e382aa..ddb5ae50 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 b9384ed4..d4ceb500 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 96037e4f..758de390 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 0b89b520..6a74ca94 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 db511741..70664be3 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 7c2878fb..dc91b385 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 1ede3a39..72490eb6 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 d7ee8dfd..b96fb1e1 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 f7c53935..3233b049 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 828ffdf6..2f036615 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 9c32b8bf..2d931585 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 311dd0e1..409d8483 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 93ac2be3..da19455f 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 1c8a941a..51b60901 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 df80c115..ae16378f 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 4119c004..eac1816d 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 ca13757b..f24a1697 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 b9b2515f..00000000 --- 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 05c857bc..f76ba498 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 1cfd09e1..7fc06d23 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 950a90be..b4d66d38 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" @@ -112,10 +113,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) { @@ -351,18 +350,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 fcfa7664..120940ac 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 648fa315..f728a188 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 bd049b57..5c6dcc02 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 3fcf86e8..75d9c576 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -1214,15 +1214,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, @@ -1252,15 +1254,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 45f83698..9e0df3cb 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 55a4324d..740f8c3d 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -111,8 +111,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()); @@ -164,8 +164,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) { @@ -177,8 +176,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 @@ -613,7 +611,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) { @@ -742,7 +740,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"); @@ -843,10 +841,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 <= @@ -856,8 +852,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( @@ -874,8 +869,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( @@ -1142,11 +1136,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 0681f3e7..85e7fb10 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 19535d8b..4a354322 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 b43e2a31..6eadb0d1 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 00459888..d712c64c 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 73d8ed41..1d5a06e8 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 93212af5..7dc89009 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 87485067..4745ae99 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 fbbcf7db..d0a4d9bc 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 4c84e448..71aff0b4 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 950feaf28bfc2577da1e6c3f554e4e6fef9f57e7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 10 Apr 2026 00:40:11 -0300 Subject: [PATCH 81/81] 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 a42122e7..e4d90de7 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 29a74491..f0129ed9 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 dc18673d..a6991a8e 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 6803ff67..ee2f3f58 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 b777c3bc..53c8a2ee 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 f211ea6e..b00c14e8 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 4f54ca0e..709df747 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 786d5d75..433ae5ed 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 8b354cc1..183d0207 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 3a1dc58e..8a2825d0 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 679989f5..371ecbe6 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 a44dcd4d..bdb9cf8c 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 2c5bb580..d396be79 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 46cba9de..e70c5027 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 5b2a9936..24e73ccb 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 275809a0..6c5df330 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 cf92b11e..5c93a015 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 71a4486d..3ebda64f 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 23ab4486..243039c1 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 9efa38b5..8febafef 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 66da6622..841799e8 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 f2be5def..0ff1d361 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 a1906e92..853dadab 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -104,8 +104,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 d4ceb500..8a272e8c 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 758de390..3a352ce1 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 6a74ca94..fb4703a5 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 70664be3..65ff3110 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 dc91b385..41f49b5a 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 3072044e..d5f029f5 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 b96fb1e1..161cd364 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 2f036615..5b2aab0f 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 2d931585..eda2bb5a 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 409d8483..8527b150 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 da19455f..7d4ee58a 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 a3bbceab..77e5875f 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 51b60901..f050b4d7 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 ae16378f..f4e2960f 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 eac1816d..8bae772a 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 f24a1697..0d805756 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 1b5c926b..872399e3 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 f34d39f2..3d4551fb 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 453359e7..a5ba9aa7 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 c0fa70d0..9c0a24ca 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 7fc06d23..422c2cde 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 8076e92a..09a91332 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 120940ac..959c796a 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 54ce45cc..d8ffda3d 100644 --- a/src/network/routing/onion_request_router.cpp +++ b/src/network/routing/onion_request_router.cpp @@ -609,8 +609,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; @@ -628,8 +627,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); }); @@ -957,10 +955,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), @@ -992,8 +987,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: {}", @@ -1006,18 +1000,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 5aa67cf6..d6c251fb 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 d5e4948b..f2a41481 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 f728a188..e6bbfafe 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 1fcee7da..1f8ad8b8 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 fb8cfd96..749c64e7 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 5c6dcc02..9ab7ee51 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 75d9c576..c9201947 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -718,7 +718,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( @@ -754,7 +755,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( @@ -1223,8 +1225,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, @@ -1263,8 +1264,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 d602b293..0a08f91d 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 9e0df3cb..83fef355 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 740f8c3d..3a9169be 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -60,7 +60,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; @@ -319,7 +318,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); } @@ -331,8 +331,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); } @@ -585,8 +584,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); @@ -906,10 +904,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( @@ -1016,8 +1011,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{ @@ -1042,8 +1037,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), @@ -1086,8 +1081,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), @@ -1151,8 +1146,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( @@ -1167,8 +1161,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 85e7fb10..e3f6688f 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 4acd3ad7..cd115746 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 57e8c515..cca107bb 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 a73fe255..024563fc 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 674f03a4..481dcfed 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 45d9d013..05d3bb75 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 b2214799..cf40630f 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 500e5bd7..bb766abb 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 4a6034c8..8098b5c5 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 451456a6..86cde79b 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 84092cb0..1f0b161a 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 40ef56d3..3f2c3c27 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 9da83a81..e0f602f5 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 8064fe55..73605940 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 1d5a06e8..8fbc8a07 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 4745ae99..1ed2bcb6 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 a35be41d..0c9b874f 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 d0a4d9bc..ec22de6a 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 a4afbe0b..98000b2f 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 d631368b..0bc913b2 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 71aff0b4..70600154 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 7440ab94..d54859eb 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 4f692a15..c2c109a1 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;