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' 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/.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/.gitmodules b/.gitmodules index a029c208..5a2ee4c5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,24 +1,21 @@ -[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 [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/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 +[submodule "cmake/session-deps"] + path = cmake/session-deps + url = https://github.com/session-foundation/session-deps.git 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/CMakeLists.txt b/CMakeLists.txt index 123cd110..bc045120 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,9 +58,18 @@ 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}) +# 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() @@ -76,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}) @@ -121,26 +131,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..3763407c --- /dev/null +++ b/cmake/session-deps @@ -0,0 +1 @@ +Subproject commit 3763407c57d4fefce2b6e764be709d7af9a951c3 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index b79fbf87..982d2ae1 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,125 +1,63 @@ -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(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) if(NOT BUILD_STATIC_DEPS AND NOT FORCE_ALL_SUBMODULES) find_package(PkgConfig REQUIRED) endif() -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") - else() - set(cross_host "--host=${ARCH_TRIPLET}") - if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) - set(cross_rc "WINDRES=${CMAKE_RC_COMPILER}") - endif() - endif() +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) - set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") - libsession_system_or_submodule(OXENQUIC quic oxen::quic liboxenquic>=1.8 session-router/external/oxen-libquic) + if(ENABLE_NETWORKING_SROUTER) + set(SROUTER_FULL OFF CACHE BOOL "") + set(SROUTER_DAEMON OFF CACHE BOOL "") + set(SROUTER_NATIVE_BUILD OFF CACHE BOOL "") + set(SROUTER_JEMALLOC OFF CACHE BOOL "") + + add_static_subdirectory(session-router EXCLUDE_FROM_ALL) + libsession_static_bundle(session-router::libsessionrouter) + else() + set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") + sessiondep_or_submodule(liboxenquic 1.8.0 session-router/external/oxen-libquic quic) + endif() 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() endif() @@ -143,15 +81,6 @@ 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) - - set(protobuf_VERBOSE ON CACHE BOOL "" FORCE) set(protobuf_INSTALL ON CACHE BOOL "" FORCE) set(protobuf_WITH_ZLIB OFF CACHE BOOL "" FORCE) @@ -162,60 +91,52 @@ 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) + + +# 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) + +# 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}) -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) - - -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) - -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) + 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 +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) +libsession_static_bundle(sessiondep::simdutf) + + +add_subdirectory(session-sqlite) -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) # 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/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/external/session-router b/external/session-router index d2def4c9..8d1df2ef 160000 --- a/external/session-router +++ b/external/session-router @@ -1 +1 @@ -Subproject commit d2def4c91d024f9d896b43e817520f27a41a7995 +Subproject commit 8d1df2efe612c15eb15f50aab6f6c72fda326d25 diff --git a/external/session-sqlite b/external/session-sqlite new file mode 160000 index 00000000..9d16f90e --- /dev/null +++ b/external/session-sqlite @@ -0,0 +1 @@ +Subproject commit 9d16f90e2c39a42300f9b91e37be82a4cfbbfc30 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/include/session/attachments.hpp b/include/session/attachments.hpp index 3cb568f1..8d6de2ac 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. /// @@ -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, @@ -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); @@ -317,6 +317,115 @@ 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(); + + /// 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, + 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/include/session/blinding.hpp b/include/session/blinding.hpp index fed3d8ff..e4d90de7 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,12 @@ 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 +76,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 +85,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 +108,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 +166,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 +184,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 +200,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/clock.hpp b/include/session/clock.hpp new file mode 100644 index 00000000..f8941240 --- /dev/null +++ b/include/session/clock.hpp @@ -0,0 +1,117 @@ +#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()); +} + +// 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/config.hpp b/include/session/config.hpp index 1be5f283..f0129ed9 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,10 @@ 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)>; + using sign_callable = std::function(std::span data)>; ConfigMessage(); ConfigMessage(const ConfigMessage&) = default; @@ -138,7 +138,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 +174,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,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. @@ -245,11 +243,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( - const oxenc::bt_dict& diff, bool enable_signing = true); + std::vector serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true); }; // Constructor tag @@ -297,7 +294,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 +304,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 +352,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 +393,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 fad7c7d7..a6991a8e 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include +#include "../crypto/ed25519.hpp" #include "../hash.hpp" #include "../logging.hpp" #include "../sodium_array.hpp" @@ -54,8 +56,8 @@ enum class ConfigState : int { Waiting = 2, }; -using Ed25519PubKey = std::array; -using Ed25519Secret = sodium_array; +using Ed25519PubKey = b32; +using Ed25519Secret = sodium_vector; // Helper base class for holding a config signing keypair class ConfigSig { @@ -73,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; @@ -83,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; @@ -112,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 /// @@ -122,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 /// @@ -132,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 /// @@ -159,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()`. @@ -173,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 - std::vector data; // Data chunk + 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 { @@ -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; } }; @@ -230,8 +231,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; @@ -252,9 +253,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 @@ -265,9 +266,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). @@ -551,20 +552,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; } @@ -703,15 +703,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()}; } @@ -932,7 +930,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 /// @@ -974,7 +972,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; @@ -1064,9 +1062,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: @@ -1083,12 +1081,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 /// @@ -1248,12 +1246,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 @@ -1290,8 +1288,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 /// @@ -1302,8 +1300,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 /// @@ -1366,14 +1364,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); @@ -1407,7 +1405,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 /// @@ -1421,7 +1419,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,8 +1433,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 /// @@ -1457,7 +1455,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,10 +1467,10 @@ 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()}; + return _keys[i]; } }; @@ -1509,56 +1507,8 @@ 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); } // namespace session::config diff --git a/include/session/config/community.hpp b/include/session/config/community.hpp index 232e53e3..ee2f3f58 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 /// @@ -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 /// @@ -269,8 +267,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 +283,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 +295,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 +347,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.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/config/contacts.hpp b/include/session/config/contacts.hpp index 7490c202..53c8a2ee 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 /// @@ -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 8d8d667d..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 - 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/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 9280110f..c2e65741 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" @@ -87,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 @@ -230,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 /// @@ -284,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 @@ -427,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; /// ``` /// @@ -443,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) /// @@ -507,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 b4b46ef4..709df747 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,10 +51,24 @@ 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 +/// +/// 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 @@ -82,10 +97,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 @@ -98,8 +113,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 @@ -126,6 +141,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..433ae5ed 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 /// @@ -269,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 /// @@ -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,14 +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, - bool write = false, - bool del = false) const; + std::span signing_value, bool write = false, bool del = false) const; /// API: groups/Keys::swarm_auth /// @@ -487,8 +484,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 +507,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 +537,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 +576,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 +645,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 +656,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,10 +684,8 @@ 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::vector encrypt_message( + std::span plaintext, bool compress = true, size_t padding = 256) const; /// API: groups/Keys::decrypt_message /// @@ -707,7 +702,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 +710,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/namespaces.h b/include/session/config/namespaces.h index b9670323..f0e2b490 100644 --- a/include/session/config/namespaces.h +++ b/include/session/config/namespaces.h @@ -24,6 +24,10 @@ typedef enum NAMESPACE { NAMESPACE_GROUP_INFO = 13, NAMESPACE_GROUP_MEMBERS = 14, + // Device group namespaces: + NAMESPACE_DEVICES = 21, + NAMESPACE_ACCOUNT_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/config/namespaces.hpp b/include/session/config/namespaces.hpp index 6945f70a..89de2eec 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -24,6 +24,10 @@ enum class Namespace : std::int16_t { GroupInfo = NAMESPACE_GROUP_INFO, GroupMembers = NAMESPACE_GROUP_MEMBERS, + // Device group namespaces: + Devices = NAMESPACE_DEVICES, + AccountPubkeys = NAMESPACE_ACCOUNT_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/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/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..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 @@ -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 fd9f1821..d994da39 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" @@ -58,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 /// @@ -127,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); /// ``` /// @@ -137,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 @@ -146,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); /// ``` /// @@ -156,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 new file mode 100644 index 00000000..e4c8a9a1 --- /dev/null +++ b/include/session/core.hpp @@ -0,0 +1,405 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/callbacks.hpp" +#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 +/// 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_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 +/// +/// 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 oxen::quic { +class Loop; +struct Ticker; +} // namespace oxen::quic + +namespace session::pro_backend { +struct ProRevocationItem; +}; // namespace session::pro_backend + +namespace session::network { +class Network; +} + +namespace session { +class TestHelper; +} + +namespace session::core { + +using namespace std::literals; +namespace quic = oxen::quic; + +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()); + } + + /// 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 +/// (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 + + // 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; + + std::shared_ptr _network; + + 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(); + + // Polling-related members and methods + std::chrono::milliseconds _poll_interval = 20s; + 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); + + // 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 ed25519::OptionalPrivKeySpan& 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. + 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 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(); + } + + /// 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; + /// How long a NAK (successful fetch that returned no keys) suppresses re-fetching. + static constexpr auto PFS_KEY_NAK_DURATION = 1h; + + /// 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. + /// + /// 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); + + /// 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 ed25519::OptionalPrivKeySpan& 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; } + + // 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}; + + // 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/callbacks.hpp b/include/session/core/callbacks.hpp new file mode 100644 index 00000000..8a2825d0 --- /dev/null +++ b/include/session/core/callbacks.hpp @@ -0,0 +1,205 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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) +}; + +/// Reason code passed to the message_decrypt_failed callback. +enum class MessageDecryptFailure { + 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; + ///< 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 + 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(). +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 { + + /// 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 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 + /// (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; + + /// 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; + + /// 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; + + /// 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/core/component.hpp b/include/session/core/component.hpp new file mode 100644 index 00000000..450c8a95 --- /dev/null +++ b/include/session/core/component.hpp @@ -0,0 +1,46 @@ +#pragma once + +namespace session::sqlite { +class Connection; +} +namespace oxen::quic { +class Loop; +} // namespace oxen::quic +namespace session::core { + +namespace quic = oxen::quic; + +class Core; +struct callbacks; + +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(); + + // 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 + // 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..fca59b0b --- /dev/null +++ b/include/session/core/devices.hpp @@ -0,0 +1,303 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "component.hpp" +#include "swarm_message.hpp" + +namespace session { +class TestHelper; +} // namespace session + +namespace session::core { + +using namespace std::literals; + +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, ///< 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. + }; + + // 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 = 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 + // 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; + + // 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). + 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; + + // 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; + } + } + + // 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>; + + struct decryption_failed : std::runtime_error { + using std::runtime_error::runtime_error; + }; +}; // namespace device + +class Devices final : detail::CoreComponent { + public: + private: + friend class Core; + friend class session::TestHelper; + 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); + + // 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_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 + // "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. + std::string device_id() const; + + // 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. + device::map devices( + bool include_registered = true, + bool include_pending = false, + bool include_unregistered = false, + std::span only_device = {}); + + // 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::Devices + std::array sas; // emoji SAS sequence for user display + }; + + // 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::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(); + + // 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_b32 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(); + + struct AccountKeys : XWingKeys { + std::chrono::sys_seconds created; + 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; + + // 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 + // 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 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. + 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. + std::optional next_device_rotation(); + + bool device_rotation_due() { + auto t = next_device_rotation(); + return t && *t <= 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 <= 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(); + + // 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/include/session/core/globals.hpp b/include/session/core/globals.hpp new file mode 100644 index 00000000..41692b41 --- /dev/null +++ b/include/session/core/globals.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#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. + 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::string _session_id_hex; // hex encoding of _session_id, computed once in init() + + 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 + 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); + + /// 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 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 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 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 buf().last<32>(); } + std::span x25519_key() const&& = delete; + }; + + 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 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; } + + /// 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/core/link_sas.hpp b/include/session/core/link_sas.hpp new file mode 100644 index 00000000..e065ea77 --- /dev/null +++ b/include/session/core/link_sas.hpp @@ -0,0 +1,53 @@ +#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 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) +/// +/// 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 +/// 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/include/session/core/pro.hpp b/include/session/core/pro.hpp new file mode 100644 index 00000000..bd559695 --- /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/include/session/core/swarm_message.hpp b/include/session/core/swarm_message.hpp new file mode 100644 index 00000000..371ecbe6 --- /dev/null +++ b/include/session/core/swarm_message.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#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/crypto/ed25519.hpp b/include/session/crypto/ed25519.hpp new file mode 100644 index 00000000..bdb9cf8c --- /dev/null +++ b/include/session/crypto/ed25519.hpp @@ -0,0 +1,295 @@ +#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); + 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. +/// 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) { + 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()); +} + +/// 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 +/// 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(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). +/// 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 8a6de921..00000000 --- a/include/session/ed25519.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include -#include -#include - -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 libsodium-style secret key, 64 bytes. -/// - `msg` -- the data to generate a signature for. -/// -/// Outputs: -/// - The ed25519 signature -std::vector sign( - std::span 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 new file mode 100644 index 00000000..d396be79 --- /dev/null +++ b/include/session/encrypt.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "util.hpp" + +namespace session::encryption { + +// ─── 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 + +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 +/// `out`. +inline void xchacha20poly1305_encrypt( + std::span out, + std::span msg, + 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)); +} + +/// Decrypts `ciphertext` with `key` and `nonce`, writing plaintext (ciphertext.size() - ABYTES +/// bytes) into `out`. Returns false if authentication fails. +inline bool xchacha20poly1305_decrypt( + std::span out, + std::span ciphertext, + 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)); +} + +// ─── 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() + 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() + 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() - 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::encryption diff --git a/include/session/fields.hpp b/include/session/fields.hpp index b70980d1..e70c5027 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; @@ -28,17 +30,4 @@ 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 unsigned char netid = 0x05; - - /// The raw x25519 pubkey, as bytes - std::array pubkey; - - /// Returns the full pubkey in hex, including the netid prefix. - std::string hex() const; -}; - } // namespace session diff --git a/include/session/format.hpp b/include/session/format.hpp new file mode 100644 index 00000000..24e73ccb --- /dev/null +++ b/include/session/format.hpp @@ -0,0 +1,218 @@ +#pragma once + +#include +#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 { + +// 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: +/// {} 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/include/session/hash.hpp b/include/session/hash.hpp index 24e1a238..6c5df330 100644 --- a/include/session/hash.hpp +++ b/include/session/hash.hpp @@ -1,13 +1,23 @@ #pragma once #include +#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include #include +#include #include -#include "types.hpp" +#include "session/util.hpp" namespace session::hash { @@ -22,10 +32,13 @@ 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, - std::optional> key = std::nullopt); + std::span hash, + std::span msg, + std::optional> key = std::nullopt); /// API: hash/hash /// @@ -40,10 +53,381 @@ void hash( /// /// Outputs: /// - a `size` byte hash. -std::vector 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, - 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 || std::same_as; + +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)), + 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; + } + } + // 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)), ...); + } + 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)), ...); + } + +} // namespace detail + +/// Concept for a fixed-size, writable byte container — the basic requirement for any hash output. +template +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; + +template +concept Blake2BOutputContainer = HashOutputContainer && 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_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) { + detail::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 +/// +/// 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) { + 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; + blake2b_key(result, key, args...); + return result; +} + +/// API: hash/blake2b +/// +/// 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. +/// +/// 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. +/// +/// 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 +/// +/// 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 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` +/// - return-value: `blake2b_key_pers(key, pers, args...)` returns a `std::array` +template + requires(sizeof...(T) > 0) +void blake2b_key_pers( + 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; + 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 +/// +/// 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"_bytes, seed)(out_a, out_b); +/// +/// // Or squeeze incrementally: +/// hash::shake256 sq{"SessionMyKey"_bytes, 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) { + detail::keccak_absorb(st, crypto_xof_shake256_DOMAIN_STANDARD, args...); + } + + ~shake256() { sodium_memzero(&st, sizeof(st)); } + + 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; + } + + /// 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 +/// +/// 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. +/// +/// 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) { + 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)); +} +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. @@ -57,4 +441,99 @@ 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)); +} + +// ─── 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). +/// +/// 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?)"}; +} + } // namespace session::hash + +namespace session { inline namespace literals { + + /// 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; // or `using namespace session;` + /// constexpr auto PERS_XYZ = "XYZ-XYZ-XYZ-WXYZ"_b2b_pers; + /// + template + requires(Lit.size == 16) + consteval auto operator""_b2b_pers() { + return operator""_bytes < Lit>(); + } + +}} // namespace session::literals diff --git a/include/session/mnemonics.hpp b/include/session/mnemonics.hpp new file mode 100644 index 00000000..3149ec9f --- /dev/null +++ b/include/session/mnemonics.hpp @@ -0,0 +1,194 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace session::mnemonics { + +/** + * The number of words in each mnemonic language word list. + * + * 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; + +/** + * 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; +}; + +/// 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_; +}; + +/// 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(); +}; + +/// 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. + */ +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); + +/** + * 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 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. + */ +secure_mnemonic bytes_to_words( + std::span bytes, const Mnemonics& lang, bool checksum = true); + +/// 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 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 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. + */ +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 bytes, writing directly into a caller-provided output span. + * + * 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 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. + */ +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/include/session/multi_encrypt.hpp b/include/session/multi_encrypt.hpp index 08c78a4b..5c93a015 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, - const unsigned char* a, - const unsigned char* A, - const unsigned char* 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,24 +129,25 @@ 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; 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; 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)); } } @@ -154,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)...); } @@ -162,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 @@ -191,34 +188,36 @@ void encrypt_for_multiple(std::span message, Args&&... args) { template < typename NextCiphertext, typename = std::enable_if_t< + std::is_invocable_r_v>, NextCiphertext> || + std::is_invocable_r_v>, NextCiphertext> || std::is_invocable_r_v< std::optional>, - NextCiphertext> || - std::is_invocable_r_v>, NextCiphertext> || + NextCiphertext> || // legacy + std::is_invocable_r_v< + std::optional>, + 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.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>(); + 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(); @@ -243,12 +242,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 @@ -292,28 +291,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 @@ -323,19 +322,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 @@ -356,36 +350,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/quic_file_client.hpp b/include/session/network/backends/quic_file_client.hpp new file mode 100644 index 00000000..fee84469 --- /dev/null +++ b/include/session/network/backends/quic_file_client.hpp @@ -0,0 +1,143 @@ +#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 { + 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)>; + + /// 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, + std::optional max_udp_payload = std::nullopt, + 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; + std::optional _max_udp_payload; + + // 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(); +}; + +/// 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/backends/session_file_server.hpp b/include/session/network/backends/session_file_server.hpp index d72ce9ac..62433d4b 100644 --- a/include/session/network/backends/session_file_server.hpp +++ b/include/session/network/backends/session_file_server.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #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 +21,28 @@ 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; + +/// 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" + uint16_t port; +}; struct DownloadInfo { std::string scheme; @@ -26,6 +50,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 +64,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. @@ -122,11 +155,33 @@ 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); +/// 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/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_config.hpp b/include/session/network/network_config.hpp index b3f417c9..dca416a8 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; @@ -63,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( @@ -118,9 +123,15 @@ 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); + 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 b89f58f3..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)); @@ -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. @@ -376,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/direct_router.hpp b/include/session/network/routing/direct_router.hpp index 0743e427..5d5efbe4 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; @@ -45,7 +54,8 @@ class DirectRouter : public IRouter, public std::enable_shared_from_this seed) override; void download(DownloadRequest request) override; private: @@ -54,7 +64,12 @@ 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 64e5c4e2..9e8550f4 100644 --- a/include/session/network/routing/session_router_router.hpp +++ b/include/session/network/routing/session_router_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" @@ -41,9 +42,13 @@ 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; + std::vector> _pending_operations; std::unordered_map> _active_uploads; std::unordered_map _active_downloads; @@ -63,7 +68,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: @@ -84,9 +90,32 @@ 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, + std::optional max_udp_payload = std::nullopt); + + void _quic_upload_via_tunnel( + UploadRequest upload_request, + std::string upload_id, + std::vector 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, + 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.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.hpp b/include/session/network/session_network.hpp index a66a5978..a66f9a10 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" @@ -52,14 +53,16 @@ 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(); 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; }; @@ -84,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); @@ -101,13 +104,14 @@ 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); + [[deprecated("use upload_file() instead")]] void upload(UploadRequest request); + void upload_file(FileUploadRequest request, std::span seed); void download(DownloadRequest request); 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/network/session_network_types.hpp b/include/session/network/session_network_types.hpp index 2bcefa29..a1112e1c 100644 --- a/include/session/network/session_network_types.hpp +++ b/include/session/network/session_network_types.hpp @@ -1,13 +1,17 @@ #pragma once +#include +#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 +34,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 = { @@ -146,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. @@ -175,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, @@ -184,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, @@ -213,9 +218,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) @@ -230,19 +236,38 @@ 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 { - std::function()> next_data; + std::function()> next_data; std::optional file_name; 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; - // 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/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/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp index 99890cbe..3ebda64f 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,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/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.h b/include/session/pro_backend.h index 6e997e79..93615562 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; - 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; @@ -130,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; @@ -139,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 @@ -156,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 @@ -190,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; }; @@ -208,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; }; @@ -264,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; @@ -309,14 +310,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 +330,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 +364,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 +380,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 +406,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 +421,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 +531,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 +551,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 1480f792..bb31fb6c 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -1,8 +1,11 @@ #pragma once +#include #include #include +#include +#include #include #include #include @@ -61,10 +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 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 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 @@ -101,8 +105,8 @@ struct ResponseHeader { }; struct MasterRotatingSignatures { - array_uc64 master_sig; - array_uc64 rotating_sig; + b64 master_sig; + b64 rotating_sig; }; struct AddProPaymentUserTransaction { @@ -132,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. - array_uc32 master_pkey; + b32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key to authorise to use the generated Session /// Pro proof - array_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 - array_uc64 master_sig; + b64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - array_uc64 rotating_sig; + b64 rotating_sig; /// API: pro/AddProPaymentRequest::to_json /// @@ -176,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 /// @@ -202,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 @@ -239,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. - array_uc32 master_pkey; + b32 master_pkey; /// 32-byte Ed25519 Session Pro rotating public key authorized to use the generated proof - array_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 - array_uc64 master_sig; + b64 master_sig; /// 64-byte signature proving knowledge of the rotating key's secret component - array_uc64 rotating_sig; + b64 rotating_sig; /// API: pro/GenerateProProofRequest::build_sigs /// @@ -269,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 @@ -288,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 @@ -324,7 +328,7 @@ struct GetProRevocationsRequest { struct ProRevocationItem { /// 32-byte hash of the generation index, identifying a proof - array_uc32 gen_index_hash; + b32 gen_index_hash; /// Unix timestamp when the proof expires sys_ms expiry_unix_ts; @@ -356,10 +360,10 @@ struct GetProDetailsRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - array_uc32 master_pkey; + b32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - array_uc64 master_sig; + b64 master_sig; /// Unix timestamp of the request sys_ms unix_ts; @@ -380,10 +384,10 @@ struct GetProDetailsRequest { /// - `count` -- Amount of historical payments to request /// /// Outputs: - /// - `array_uc64` - the 64-byte signature - static array_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); @@ -402,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); @@ -563,10 +567,10 @@ struct SetPaymentRefundRequestedRequest { std::uint8_t version; /// 32-byte Ed25519 master public key to retrieve payments for - array_uc32 master_pkey; + b32 master_pkey; /// 64-byte signature proving knowledge of the master public key's secret component - array_uc64 master_sig; + b64 master_sig; /// Unix timestamp of the current time sys_ms unix_ts; @@ -599,15 +603,15 @@ struct SetPaymentRefundRequestedRequest { /// `AddProPaymentUserTransaction` /// /// Outputs: - /// - `array_uc64` - the 64-byte signature - static array_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 /// @@ -632,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 9a37cff8..878150b6 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; }; }; @@ -32,6 +32,23 @@ 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); + +/// 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. @@ -41,7 +58,7 @@ namespace session::random { /// /// Outputs: /// - random bytes of the specified length. -std::vector random(size_t size); +std::vector random(size_t size); /// API: random/random_base32 /// @@ -63,7 +80,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/include/session/session_encrypt.h b/include/session/session_encrypt.h index 4aa45e9c..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 @@ -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 7635c77e..243039c1 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -2,11 +2,15 @@ #include +#include #include #include +#include #include #include +#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. // @@ -53,9 +57,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. @@ -63,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( - std::span 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 /// @@ -84,10 +87,10 @@ std::vector encrypt_for_recipient( /// /// Outputs: /// Identical to `encrypt_for_recipient`. -std::vector encrypt_for_recipient_deterministic( - std::span 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 /// @@ -104,11 +107,188 @@ 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( - std::span 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 +/// +/// 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_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. +/// - Throws on invalid keys or encryption failure. +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., +/// 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. + 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 +/// +/// 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); + +/// 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_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). +/// - Throws on key or encryption failure. +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 +/// +/// 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; @@ -163,8 +343,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). @@ -177,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( - std::span 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); @@ -204,14 +384,15 @@ 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, - 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 /// @@ -219,18 +400,16 @@ 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: -/// - `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 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 /// @@ -246,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 /// @@ -261,17 +439,16 @@ 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: /// - `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 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 /// @@ -286,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 /// @@ -301,10 +478,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), @@ -317,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( - std::span 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 @@ -357,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> group_enc_keys, + std::span group_ed25519_pubkey, + std::span ciphertext); /// API: crypto/decrypt_ons_response /// @@ -375,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 /// @@ -391,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 /// @@ -400,12 +575,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::vector encrypt_xchacha20( + std::span plaintext, std::span key); /// API: crypto/decrypt_xchacha20 /// @@ -413,11 +588,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::vector` -- the resulting plaintext. +std::vector decrypt_xchacha20( + std::span ciphertext, std::span key); } // namespace session diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 34efb16f..17328ccf 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 { @@ -88,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 @@ -136,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; @@ -170,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; @@ -198,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; }; @@ -221,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; }; @@ -272,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 @@ -414,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. @@ -460,13 +430,13 @@ 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, 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, @@ -527,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, @@ -641,62 +611,13 @@ 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, 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 1ff3c21e..8febafef 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -5,9 +5,13 @@ #include #include #include +#include +#include +#include #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 @@ -56,6 +60,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, @@ -66,8 +79,8 @@ enum class ProStatus { }; struct ProSignedMessage { - std::span sig; - std::span msg; + std::span sig; + std::span msg; }; class ProProof { @@ -76,12 +89,12 @@ class ProProof { std::uint8_t version; /// Hash of the generation index set by the Session Pro Backend - array_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 - array_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; @@ -89,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. - array_uc64 sig; + b64 sig; /// API: pro/Proof::verify_signature /// @@ -105,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 /// @@ -120,7 +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, const std::span msg) const; + bool verify_message(std::span sig, std::span msg) const; /// API: pro/Proof::is_active /// @@ -158,14 +171,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. - array_uc32 hash() const; + b32 hash() const; bool operator==(const ProProof& other) const { return version == other.version && gen_index_hash == other.gen_index_hash && @@ -205,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 - array_uc33 recipient_pubkey; - - // When type => CommunityInbox: set this pubkey to the server's key - array_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; - - // 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; @@ -249,12 +225,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; + b33 source; uint32_t source_device; uint64_t server_timestamp; // Signature by the sending client's rotating key - array_uc64 pro_sig; + b64 pro_sig; }; struct DecodedPro { @@ -271,16 +247,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. - array_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. - array_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. @@ -295,55 +271,27 @@ 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 /// /// 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. @@ -353,17 +301,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. @@ -373,64 +321,57 @@ 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 /// /// 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 +/// 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. -/// -/// 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. -/// - 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. -/// - 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. +/// - 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 "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( - std::span plaintext, - std::span ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, - std::optional> pro_rotating_ed25519_privkey); +/// - Encrypted, encoded payload, with all required protobuf encoding and wrapping. +std::vector encode_dm_v1( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, + sys_ms sent_timestamp, + std::span recipient_pubkey, + const ed25519::OptionalPrivKeySpan& 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). /// /// 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). @@ -442,20 +383,20 @@ 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::span ed25519_privkey, +std::vector encode_for_community_inbox( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_uc33& recipient_pubkey, - const array_uc32& community_pubkey, - std::optional> pro_rotating_ed25519_privkey); + std::span recipient_pubkey, + std::span community_pubkey, + const ed25519::OptionalPrivKeySpan& 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 @@ -472,27 +413,24 @@ 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, + const ed25519::OptionalPrivKeySpan& 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). /// /// 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 @@ -505,45 +443,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::span ed25519_privkey, +std::vector encode_for_group( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, std::chrono::milliseconds sent_timestamp, - const array_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 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. -/// -/// 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::span ed25519_privkey, - const Destination& dest); + std::span group_ed25519_pubkey, + std::span group_enc_key, + const ed25519::OptionalPrivKeySpan& pro_rotating_ed25519_privkey); /// API: session_protocol/decode_envelope /// @@ -601,10 +507,28 @@ std::vector encode_for_destination( /// /// 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, - const array_uc32& 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 /// @@ -636,11 +560,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, - const array_uc32& pro_backend_pubkey); + std::span 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..841799e8 100644 --- a/include/session/sodium_array.hpp +++ b/include/session/sodium_array.hpp @@ -11,48 +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. template >> @@ -62,145 +20,23 @@ struct sodium_cleared : T { ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } }; -template -using cleared_array = sodium_cleared>; - -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; +template +struct cleared_array : sodium_cleared> { + using sodium_cleared>::sodium_cleared; -// 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; - } - } - } + // 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)}; } - - ~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(); } - - 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; }; +template +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. @@ -228,4 +64,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/types.h b/include/session/types.h index 5029cab3..57347032 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; }; @@ -29,47 +28,29 @@ struct string8 { #define string8_literal(literal) {(char*)literal, sizeof(literal) - 1} -typedef struct bytes32 bytes32; -struct bytes32 { - uint8_t data[32]; +typedef struct cbytes32 cbytes32; +struct cbytes32 { + unsigned char data[32]; }; -typedef struct bytes33 bytes33; -struct bytes33 { - uint8_t data[33]; +typedef struct cbytes33 cbytes33; +struct cbytes33 { + unsigned char data[33]; }; -typedef struct bytes64 bytes64; -struct bytes64 { - uint8_t data[64]; +typedef struct cbytes64 cbytes64; +struct cbytes64 { + 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; }; -/// 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/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 75c9b475..0ff1d361 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -29,29 +29,34 @@ 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 - requires(!oxenc::bt_input_string) -inline std::span to_span(const Container& c) { - return {reinterpret_cast(c.data()), c.size()}; +template + 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 @@ -62,34 +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()), - std::min(N, sp.size()), - result.begin()); + reinterpret_cast(sp.data()), std::min(N, sp.size()), result.begin()); return result; } @@ -103,7 +106,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); @@ -126,68 +174,31 @@ 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()); +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 +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. @@ -198,15 +209,19 @@ 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; -/// 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 @@ -217,25 +232,25 @@ 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()); } @@ -245,6 +260,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", ""} @@ -359,15 +377,48 @@ 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::vector zstd_compress( + 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. -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" +struct human_size { + int64_t bytes; + 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 @@ -389,4 +440,4 @@ Fn make_callback_atomic(Fn cb) { if (!called->exchange(true)) cb(std::forward(args)...); }; -} \ No newline at end of file +} 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..7ffbbc3d 100644 --- a/include/session/xed25519.hpp +++ b/include/session/xed25519.hpp @@ -1,26 +1,26 @@ #pragma once #include +#include #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); +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::span signature, + std::span curve25519_pubkey, + std::span msg); /// "Softer" version that takes strings of regular chars [[nodiscard]] bool verify( @@ -32,19 +32,20 @@ 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); +b32 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. 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/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 7530af29..ddb5ae50 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) @@ -45,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 @@ -54,6 +65,7 @@ add_libsession_util_library(crypto session_protocol.cpp sodium_array.cpp xed25519.cpp + xed25519-tweetnacl.cpp pro_backend.cpp types.cpp ) @@ -76,7 +88,6 @@ add_libsession_util_library(config config/pro.cpp config/user_groups.cpp config/user_profile.cpp - fields.cpp ) @@ -85,35 +96,56 @@ target_link_libraries(util PUBLIC common oxen::logging - libzstd::static - simdutf + PRIVATE + sessiondep::libzstd + sessiondep::simdutf ) target_link_libraries(crypto PUBLIC util + session::secure_buffer PRIVATE - libsodium::sodium-internal + sessiondep::libsodium + mlkem_native::mlkem768 nlohmann_json::nlohmann_json libsession::protos ) +# libsession "Core" for maintaining persistent Session client state +add_libsession_util_library(core + core.cpp + core/component.cpp + core/devices.cpp + core/globals.cpp + core/link_sas.cpp + core/pro.cpp +) +add_subdirectory(core/schema) +add_subdirectory(mnemonics) + +target_link_libraries( + core + PUBLIC + crypto + PRIVATE + config + sessiondep::libsodium + session::SQLite + mlkem_native::mlkem768 + quic +) + target_link_libraries(config PUBLIC crypto libsession::protos PRIVATE - libsodium::sodium-internal + sessiondep::libsodium ) 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 @@ -129,6 +161,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 @@ -142,10 +175,10 @@ if(ENABLE_NETWORKING) quic PRIVATE nlohmann_json::nlohmann_json - libsodium::sodium-internal - nettle::nettle + sessiondep::libsodium + sessiondep::nettle date::date - libevent::core + sessiondep::libevent_core ) if(ENABLE_NETWORKING_SROUTER) @@ -153,6 +186,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) @@ -210,11 +244,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) diff --git a/src/attachments.cpp b/src/attachments.cpp index ef437419..853dadab 100644 --- a/src/attachments.cpp +++ b/src/attachments.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -17,6 +16,7 @@ #include #include "internal-util.hpp" +#include "session/hash.hpp" namespace session::attachment { @@ -97,15 +97,14 @@ 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); + 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; @@ -118,256 +117,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()}; - - 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()); - std::memcpy(key.data(), nonce_key.data() + ENCRYPT_HEADER, ENCRYPT_KEY_SIZE); + Encryptor enc{seed, domain}; + enc.update_key(data); + + size_t pos = 0; + auto key = enc.start_encryption( + [&](std::span buf) -> size_t { + size_t avail = std::min(buf.size(), data.size() - pos); + std::memcpy(buf.data(), data.data() + pos, avail); + pos += avail; + return avail; + }, + allow_large); - inpos = udata.data(); - inend = inpos + udata.size(); + size_t written = 0; + for (auto chunk = enc.next(); !chunk.empty(); chunk = enc.next()) { + assert(written + chunk.size() <= out.size()); + std::memcpy(out.data() + written, chunk.data(), chunk.size()); + written += chunk.size(); + } - return result; + return {std::move(key), written}; } -std::array encrypt( +cleared_b32 encrypt( std::span seed, std::span data, Domain domain, std::span out, bool allow_large) { - - auto [nonce_key, key, inpos, inend] = encrypt_buffer_init(seed, data, domain, allow_large); - - encrypt_impl( - out, - data.size(), - nonce_key, - [&inpos, &inend](size_t size) -> std::pair, bool> { - auto* start = inpos; - auto* end = std::min(inpos + size, inend); - inpos = end; - return {{start, end}, inpos != inend}; - }); - - return key; + return encrypt_to_span(seed, data, domain, out, allow_large).first; } -std::pair, std::array> encrypt( +std::pair, cleared_b32> encrypt( std::span seed, std::span data, Domain domain, bool allow_large) { - - if (seed.size() < 32) - throw std::invalid_argument{"attachment::encrypt requires a 32-byte uploader seed"}; - - if (data.size() > MAX_REGULAR_SIZE && !allow_large) - throw std::invalid_argument{"data to encrypt is too large"}; - - std::pair, std::array> result; - auto& [out, key] = result; - - out.resize(encrypted_size(data.size())); - - key = encrypt(seed, data, domain, out, allow_large); - - return result; + std::vector out(encrypted_size(data.size())); + auto key = encrypt_to_span(seed, data, domain, out, allow_large).first; + return {std::move(out), std::move(key)}; } -std::array encrypt( +cleared_b32 encrypt( std::span seed, const std::filesystem::path& file, Domain domain, std::function(size_t enc_size)> make_buffer, bool allow_large) { + auto [enc, key] = Encryptor::from_file(seed, domain, file, allow_large); - std::ifstream in; - in.exceptions(std::ios::badbit); - in.open(file, std::ios::binary | std::ios::ate); - size_t size = in.tellg(); - in.seekg(0, std::ios::beg); - - size = encrypted_size(size); - - std::array nonce_key; + auto out = make_buffer(encrypted_size(enc.data_size())); - crypto_generichash_blake2b_state b_st; - const auto domain_byte = static_cast(domain); - crypto_generichash_blake2b_init(&b_st, &domain_byte, 1, nonce_key.size()); - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(seed.data()), 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()); - in_size += chunk.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(); } - if (in.gcount() > 0) { - crypto_generichash_blake2b_update( - &b_st, reinterpret_cast(chunk.data()), in.gcount()); - in_size += in.gcount(); - } - - 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( @@ -383,82 +207,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; @@ -485,12 +260,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(); @@ -520,7 +294,7 @@ size_t decrypt( reinterpret_cast(padbuf.data()), nullptr, &tag, - inpos, + to_unsigned(inpos), chunk_size + ENCRYPT_CHUNK_OVERHEAD, nullptr, 0) != 0) @@ -579,7 +353,7 @@ size_t decrypt( reinterpret_cast(decrypted), nullptr, &tag, - inpos, + to_unsigned(inpos), chunk_size + ENCRYPT_CHUNK_OVERHEAD, nullptr, 0) != 0) @@ -893,6 +667,234 @@ 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, uc(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'}; + ss_st(ss_st_data) = secretstream_xchacha20poly1305_init_push_with_nonce( + 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); + + 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}; +} + +cleared_b32 Encryptor::load_key_from_file( + const std::filesystem::path& file, + bool allow_large, + std::function progress) { + auto in = std::make_shared(); + in->exceptions(std::ios::badbit); + in->open(file, std::ios::binary | std::ios::ate); + int64_t total = in->tellg(); + in->seekg(0, std::ios::beg); + + // 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); + + 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)}; +} + } // namespace session::attachment extern "C" { @@ -930,7 +932,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; } } @@ -954,7 +957,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; } } @@ -982,7 +986,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; } } @@ -1016,7 +1021,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; } } @@ -1041,7 +1046,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(); } } @@ -1061,7 +1066,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; } } @@ -1076,7 +1082,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/blinding.cpp b/src/blinding.cpp index 927aa5ea..8a272e8c 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -1,16 +1,17 @@ #include "session/blinding.hpp" +#include #include -#include -#include -#include -#include #include +#include +#include #include -#include "session/ed25519.hpp" +#include "session/blinding.h" +#include "session/crypto/ed25519.hpp" #include "session/export.h" +#include "session/hash.hpp" #include "session/platform.h" #include "session/platform.hpp" #include "session/xed25519.hpp" @@ -18,91 +19,112 @@ namespace session { using namespace std::literals; +using namespace oxen::log::literals; -using uc32 = std::array; -using uc33 = std::array; -using uc64 = std::array; +b32 blind15_factor(std::span server_pk) { + auto blind_hash = hash::blake2b<64>(server_pk); -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()); - - 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) { - 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()); + 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); - auto ed_pk = xed25519::pubkey(session_id); - 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; + 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>())); + 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); - auto ed_pk = xed25519::pubkey(session_id); - 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; } @@ -114,34 +136,30 @@ 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; - result[1] = oxenc::to_hex(blinded.begin(), blinded.end()); + result[0] = oxenc::to_hex(blinded); + blinded.back() ^= std::byte{0x80}; + result[1] = oxenc::to_hex(blinded); 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; } @@ -153,378 +171,210 @@ 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()); - return oxenc::to_hex(blinded.begin(), blinded.end()); + b33 blinded; + blind25_id_impl(raw_sid, raw_server_pk, blinded); + return oxenc::to_hex(blinded); } -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())) - 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 - 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"}; +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()); - 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::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 const auto version_blinding_hash_key_sig = to_span("VersionCheckKey_sig"); - -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; - crypto_generichash_blake2b( - blind_seed.data(), - 32, - ed25519_sk.data(), - 32, - version_blinding_hash_key_sig.data(), - version_blinding_hash_key_sig.size()); - - // 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"}; +static constexpr auto version_blinding_hash_key_sig = "VersionCheckKey_sig"_bytes; - 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 const auto hash_key_seed = to_span("SessCommBlind25_seed"); -static const auto hash_key_sig = to_span("SessCommBlind25_sig"); +static constexpr auto hash_key_seed = "SessCommBlind25_seed"_bytes; +static constexpr auto hash_key_sig = "SessCommBlind25_sig"_bytes; -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"}; - - 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()); - - 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()); - - 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()); - - 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 +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); - return result; + b32 seedhash; + hash::blake2b_key(seedhash, hash_key_seed, ed25519_sk.seed()); + + b64 r_hash; + hash::blake2b_key(r_hash, hash_key_sig, seedhash, A, message); + + b32 r; + ed25519::scalar_reduce(r, r_hash); + + 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); +} + +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); } -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) { 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.data(), sk.size()}, buf); + 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.data(), sk.size()}, buf); + return blind_version_sign_request(ed25519_sk, timestamp, "GET", url, std::nullopt); } bool session_id_matches_blinded_id( @@ -542,7 +392,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': { @@ -566,7 +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}, {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; @@ -581,7 +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}, {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; @@ -614,7 +466,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 (...) { @@ -632,7 +484,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 (...) { @@ -651,9 +503,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 a8e2cf1f..fd056172 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; @@ -345,9 +345,8 @@ namespace { return std::string_view{reinterpret_cast(hash.data()), hash.size()}; } - hash_t& hash_msg(hash_t& into, std::span serialized) { - crypto_generichash_blake2b( - into.data(), into.size(), serialized.data(), serialized.size(), nullptr, 0); + hash_t& hash_msg(hash_t& into, std::span serialized) { + hash::blake2b(into, serialized); return into; } @@ -429,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)) @@ -482,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(); } @@ -523,7 +522,7 @@ ConfigMessage::ConfigMessage() { } ConfigMessage::ConfigMessage( - std::span serialized, + std::span serialized, verify_callable verifier_, sign_callable signer_, int lag, @@ -561,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, @@ -691,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, @@ -707,7 +706,7 @@ MutableConfigMessage::MutableConfigMessage( } MutableConfigMessage::MutableConfigMessage( - std::span config, + std::span config, verify_callable verifier, sign_callable signer, int lag) : @@ -729,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{}; @@ -776,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{ @@ -784,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 440844d1..3a352ce1 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -20,10 +19,12 @@ #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" #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" using namespace std::literals; @@ -70,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); @@ -79,28 +80,22 @@ 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()}, - 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( - std::span{ - _keys.front().data(), _keys.front().size()}, - unwrapped, - storage_namespace()); + auto unwrapped2 = + protos::unwrap_config(_keys.front(), unwrapped, storage_namespace()); log::warning( cat, "Found double wraped message in namespace {}", @@ -120,9 +115,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: @@ -139,7 +134,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"}; @@ -155,7 +150,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"}; @@ -200,7 +195,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; @@ -213,13 +208,11 @@ 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( - oxenc::to_hex(actual_hash.begin(), actual_hash.end()), - oxenc::to_hex(final_hash.begin(), final_hash.end()))}; + actual_hash, final_hash)}; } log::debug( @@ -228,7 +221,7 @@ 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( cat, "multipart message {} inflated to {}B plaintext from {}B compressed", - oxenc::to_hex(final_hash.begin(), final_hash.end()), + final_hash, decompressed->size(), 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()) throw std::runtime_error{"recombined data is empty"}; - if (recombined[0] != 'd') + if (recombined[0] != std::byte{'d'}) throw std::runtime_error{"Recombined data has invalid/unsupported type {:?}"_format( static_cast(recombined[0]))}; return {true, std::move(result)}; } else { - parts.expiry = std::chrono::system_clock::now() + MULTIPART_MAX_WAIT; + parts.expiry = clock_now() + MULTIPART_MAX_WAIT; log::debug( cat, "message {} (part {} of {}) stored without completing a multipart set for {}", msg_id, index, parts.size, - oxenc::to_hex(final_hash.begin(), final_hash.end())); + final_hash); return {true, std::nullopt}; } @@ -288,7 +281,7 @@ ConfigBase::_handle_multipart(std::string_view msg_id, std::span(); 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 +357,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 +389,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 +402,7 @@ std::unordered_set ConfigBase::_merge( } } - std::vector>> plaintexts; + std::vector>> plaintexts; std::unordered_set good_hashes; @@ -440,7 +433,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 +444,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 +464,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 +477,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 {:?}", @@ -656,7 +649,7 @@ std::unordered_set 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) @@ -685,23 +678,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; @@ -728,8 +721,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 @@ -754,17 +746,17 @@ ConfigBase::push() { cat, "splitting large config message ({}B, hash {}) into {} parts", msg.size(), - oxenc::to_hex(final_hash.begin(), final_hash.end()), + final_hash, 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,8 +782,7 @@ ConfigBase::push() { encrypt_inplace(msg, key(), encryption_domain()); if (accepts_protobuf() && !_keys.empty()) { - auto pbwrapped = protos::wrap_config( - {_keys.front().data(), _keys.front().size()}, msg, s, storage_namespace()); + auto pbwrapped = protos::wrap_config(_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. @@ -825,7 +816,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(); @@ -836,7 +827,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; @@ -856,9 +847,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!"}; @@ -867,11 +858,10 @@ 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))) + 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) { @@ -882,9 +872,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(); @@ -952,10 +942,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) @@ -963,22 +950,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)) @@ -1011,7 +995,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; @@ -1019,10 +1003,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); @@ -1036,7 +1016,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; @@ -1059,52 +1039,36 @@ 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()); - - 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()); + _sign_sk.assign(secret.begin(), secret.end()); + ed25519::sk_to_pk(_sign_pk.emplace(), secret); + + 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); }); } void ConfigSig::clear_sig_keys() { _sign_pk.reset(); - _sign_sk.reset(); + _sign_sk.clear(); set_signer(nullptr); set_verifier(nullptr); } @@ -1117,25 +1081,23 @@ void ConfigBase::set_signer(ConfigMessage::sign_callable s) { _config->signer = std::move(s); } -std::array ConfigSig::seed_hash(std::string_view key) const { - if (!_sign_sk) +cleared_b32 ConfigSig::seed_hash(std::string_view key) const { + if (_sign_sk.empty()) 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; + cleared_b32 result; + hash::blake2b_key(result, key, std::span{_sign_sk.data(), 32}); + return result; } -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 { + + 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 @@ -1161,11 +1123,10 @@ 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]}); + confs.emplace_back(msg_hashes[i], to_byte_span(configs[i], lengths[i])); return make_string_list(config.merge(confs)); }); @@ -1313,7 +1274,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); @@ -1323,7 +1284,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); @@ -1332,20 +1293,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) { @@ -1356,7 +1317,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); @@ -1366,7 +1327,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); @@ -1375,7 +1336,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..fb4703a5 100644 --- a/src/config/community.cpp +++ b/src/config/community.cpp @@ -4,14 +4,13 @@ #include #include +#include #include #include #include #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" @@ -24,7 +23,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 +45,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) { @@ -56,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) { @@ -80,13 +74,8 @@ 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; + std::string_view base_url, std::string_view room, std::span pubkey) { + return "{}/{}?public_key={:x}"_format(base_url, room, pubkey); } void community::canonicalize_url(std::string& url) { @@ -112,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"}; @@ -130,9 +117,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 +143,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 +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{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 6fee7c95..65ff3110 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -59,8 +58,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 +153,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 +322,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))} { @@ -331,7 +332,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) { @@ -378,13 +379,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 +419,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 +428,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 +467,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 { @@ -513,7 +519,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 7f62f9b2..41f49b5a 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -3,15 +3,16 @@ #include #include #include -#include #include #include #include +#include #include #include #include "internal.hpp" +#include "session/clock.hpp" #include "session/config/convo_info_volatile.h" #include "session/config/error.h" #include "session/export.h" @@ -62,7 +63,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 +162,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 +183,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 +209,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 +223,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; @@ -239,10 +240,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::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()) result.load(*info_dict); @@ -312,7 +311,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)); @@ -340,7 +339,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); } } @@ -353,7 +352,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 +370,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) @@ -410,7 +409,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 +445,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 +741,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 23a87cd9..d5f029f5 100644 --- a/src/config/encrypt.cpp +++ b/src/config/encrypt.cpp @@ -2,12 +2,13 @@ #include #include -#include #include #include +#include "session/config/encrypt.h" #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" using namespace std::literals; @@ -28,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) @@ -40,99 +41,100 @@ 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; } -std::vector encrypt( - std::span message, - std::span key_base, +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"}; + 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(), - message.size(), - to_unsigned(nonce_key.data()), - nonce_key.size()); - - size_t plaintext_len = message.size(); - message.resize(plaintext_len + ENCRYPT_DATA_OVERHEAD); + auto nonce = + hash::blake2b_key(nonce_key, plaintext); unsigned long long outlen = 0; crypto_aead_xchacha20poly1305_ietf_encrypt( - message.data(), + to_unsigned(message.data()), &outlen, - message.data(), - plaintext_len, + to_unsigned(plaintext.data()), + plaintext.size(), nullptr, 0, nullptr, - nonce.data(), + to_unsigned(nonce.data()), key.data()); - assert(outlen == message.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - std::memcpy(message.data() + outlen, nonce.data(), nonce.size()); + assert(outlen == plaintext.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + std::memcpy(to_unsigned(message.data()) + outlen, to_unsigned(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); -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"}; @@ -140,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 @@ -157,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; } @@ -177,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 fb6b6e43..72490eb6 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 @@ -17,10 +16,10 @@ using namespace std::literals; namespace session::config::groups { Info::Info( - std::span ed25519_pubkey, - std::optional> ed25519_secretkey, - std::optional> dumped) : - id{"03" + oxenc::to_hex(ed25519_pubkey.begin(), ed25519_pubkey.end())} { + std::span ed25519_pubkey, + const ed25519::OptionalPrivKeySpan& ed25519_secretkey, + std::optional> dumped) : + id{"03{:x}"_format(ed25519_pubkey)} { init(dumped, ed25519_pubkey, ed25519_secretkey); } @@ -62,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); } @@ -257,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 e35b4d04..161cd364 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -3,25 +3,24 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include +#include #include #include +#include "../../internal-util.hpp" #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" +#include "session/crypto/ed25519.hpp" +#include "session/encrypt.hpp" +#include "session/hash.hpp" #include "session/multi_encrypt.hpp" +#include "session/random.hpp" #include "session/session_encrypt.hpp" #include "session/xed25519.hpp" @@ -34,26 +33,19 @@ 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); + user_ed25519_sk.assign(user_ed25519_secretkey.begin(), user_ed25519_secretkey.end()); if (dumped) { load_dump(*dumped); @@ -67,14 +59,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"); @@ -108,7 +100,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")) { @@ -138,8 +130,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")) @@ -172,8 +164,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()); } } @@ -182,20 +174,20 @@ 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; } -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()) @@ -205,50 +197,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; - const std::span junk_seed_hash_key = to_span("SessionGroupJunkMembers"); + 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"}; @@ -256,10 +228,9 @@ 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 @@ -287,54 +258,33 @@ 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; + auto h2 = seed_hash(seed_hash_key); - 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 hasher{ + enc_key_hash_key, std::nullopt}; for (const auto& m : members) - crypto_generichash_blake2b_update( - &st, to_unsigned(m.session_id.data()), m.session_id.size()); + hasher.update(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); + hasher.update("{}"_format(gen), 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()); + encryption::xchacha20poly1305_encrypt(encrypted, enc_key, nonce, member_k); d.append("G", gen); d.append("K", enc_sv); @@ -342,8 +292,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) { @@ -358,7 +308,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++; }, @@ -368,17 +318,12 @@ 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); - 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()); + auto rng_seed = 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())); @@ -389,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; @@ -408,17 +352,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"}; @@ -430,10 +374,8 @@ 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 @@ -463,24 +405,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) - crypto_generichash_blake2b_update(&st, to_unsigned(sid.data()), sid.size()); - - crypto_generichash_blake2b_update(&st, to_unsigned(supp_keys.data()), supp_keys.size()); + nonce_hasher.update(sid); - std::array h2 = seed_hash(seed_hash_key); - crypto_generichash_blake2b_update(&st, h2.data(), h2.size()); + auto h2 = seed_hash(seed_hash_key); + nonce_hasher.update(supp_keys, h2); - crypto_generichash_blake2b_final(&st, h1.data(), h1.size()); - - std::span nonce{h1.data(), h1.size()}; + auto nonce = nonce_hasher.finalize(); oxenc::bt_dict_producer d{}; @@ -488,13 +421,13 @@ std::vector Keys::key_supplement(const std::vector& { auto list = d.append_list("+"); - std::vector encrypted; - encrypted.resize(supp_keys.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES); + std::vector encrypted; + encrypted.resize(supp_keys.size() + encryption::XCHACHA20_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) { @@ -509,7 +442,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++; }, @@ -525,49 +458,38 @@ 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); }); + 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. -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); - - 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()); - - std::array out; - crypto_core_ed25519_scalar_reduce(out.data(), h.data()); - return out; + + auto h = hash::blake2b_key<64>(mask, std::byte{0x05}, session_xpk, std::byte{0x03}, *_sign_pk); + + 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"}; @@ -596,33 +518,33 @@ 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; - - 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"}; @@ -633,23 +555,22 @@ 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); + + auto kT = ed25519::scalarmult_noclamp(k, T); - std::vector out; + 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, - 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"}; @@ -661,7 +582,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: @@ -669,30 +590,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). @@ -716,46 +634,28 @@ 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; - 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()); - - 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()); - - std::array r; - crypto_core_ed25519_scalar_reduce(r.data(), tmp.data()); + 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()); + + b64 tmp; + hash::blake2b_key(tmp, subacc_sig_key, hseed, kT, msg); + + 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) @@ -769,12 +669,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()}, + "03{:x}"_format(*_sign_pk), + ed25519::PrivKeySpan::from(user_ed25519_sk), sign_val, write, del); @@ -782,8 +682,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"); @@ -791,46 +691,43 @@ 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) { @@ -864,10 +761,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. @@ -875,39 +772,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 encryption::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) { @@ -917,22 +792,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() != encryption::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("+")) { @@ -941,7 +820,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()); @@ -954,11 +833,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 @@ -1026,13 +904,13 @@ 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()) { 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; @@ -1046,14 +924,14 @@ 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()); - 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; @@ -1183,60 +1061,46 @@ 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 { +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); // // 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&) { - } - } - } + // 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); - 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))}; + auto decrypt = decrypt_group_message(key_list, *_sign_pk, ciphertext); - std::pair> result; + std::pair> result; result.first = std::move(decrypt.session_id); result.second = std::move(decrypt.plaintext); return result; @@ -1306,14 +1170,13 @@ 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); @@ -1353,7 +1216,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( @@ -1363,9 +1226,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(to_unsigned(src_key.data())); dest_key->size = src_key.size(); } } @@ -1375,8 +1238,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(to_unsigned(key.data())); result.size = key.size(); assert(result.size == 32); } catch (const std::exception& e) { @@ -1397,7 +1260,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; @@ -1413,14 +1276,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; @@ -1432,7 +1295,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; } @@ -1453,7 +1316,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)); @@ -1491,10 +1354,9 @@ 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}); + 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(); @@ -1516,8 +1378,8 @@ LIBSESSION_C_API bool groups_keys_decrypt_message( return wrap_exceptions( conf, [&] { - auto [sid, plaintext] = unbox(conf).decrypt_message( - std::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()); @@ -1587,8 +1449,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 (...) { @@ -1603,8 +1465,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; } @@ -1624,8 +1486,7 @@ 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); @@ -1654,9 +1515,7 @@ 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}, - 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/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..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) { @@ -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 e64e699f..5b2aab0f 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -8,6 +8,8 @@ #include #include +#include "../internal-util.hpp" +#include "session/clock.hpp" #include "session/config/base.h" #include "session/config/base.hpp" #include "session/config/error.h" @@ -63,10 +65,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); } @@ -81,13 +83,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); } @@ -148,7 +150,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). @@ -157,7 +159,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); @@ -172,12 +174,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); @@ -213,10 +209,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( - 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); @@ -266,6 +261,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/local.cpp b/src/config/local.cpp index 3b6575d3..4f8c79ed 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" @@ -11,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 192189ef..eda2bb5a 100644 --- a/src/config/pro.cpp +++ b/src/config/pro.cpp @@ -1,9 +1,8 @@ #include #include -#include -#include #include +#include #include #include "internal.hpp" @@ -21,17 +20,17 @@ bool ProConfig::load(const dict& root) { if (!p) return false; - std::optional> maybe_rotating_seed = maybe_vector(root, "r"); - if (!maybe_rotating_seed || maybe_rotating_seed->size() != crypto_sign_ed25519_SEEDBYTES) + std::optional> maybe_rotating_seed = maybe_vector(root, "r"); + if (!maybe_rotating_seed || maybe_rotating_seed->size() != 32) 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,8 +52,8 @@ 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()); + 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 bfd5d7af..8527b150 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -1,15 +1,16 @@ #include "session/config/protos.hpp" -#include -#include +#include #include +#include #include #include #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" #include "session/session_encrypt.hpp" +#include "session/util.hpp" namespace session::config::protos { @@ -33,24 +34,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 +80,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 +110,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 +133,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 +161,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 a87638d7..7d4ee58a 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -3,8 +3,6 @@ #include #include #include -#include -#include #include #include @@ -58,7 +56,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); } @@ -80,8 +78,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)); } } @@ -205,9 +203,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 { @@ -231,12 +229,11 @@ 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; - secretkey.resize(64); - crypto_sign_seed_keypair(pk.data() + 1, secretkey.data(), 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); @@ -280,20 +277,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()]; } @@ -302,11 +299,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; @@ -318,10 +315,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::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()) result.load(*info_dict); @@ -383,17 +378,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; - sk.resize(64); - crypto_sign_keypair(pk.data(), 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; } @@ -446,13 +434,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 @@ -668,7 +656,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 37c86cf0..77e5875f 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" @@ -15,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); } @@ -38,7 +37,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)); @@ -55,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()}; @@ -75,15 +74,14 @@ void UserProfile::set_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()}; @@ -93,7 +91,7 @@ void UserProfile::set_reupload_profile_pic( return; set_pair_if(!url.empty() && key.size() == 32, data["P"], url, data["Q"], key); - data["T"] = ts_now(); + data["T"] = clock_now_s(); } void UserProfile::set_reupload_profile_pic(profile_pic pic) { @@ -132,7 +130,7 @@ void UserProfile::set_blinded_msgreqs(std::optional 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 { @@ -164,7 +162,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"]; @@ -175,7 +173,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 +196,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 +206,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(); } } @@ -272,9 +270,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, @@ -287,9 +285,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 new file mode 100644 index 00000000..f050b4d7 --- /dev/null +++ b/src/core.cpp @@ -0,0 +1,962 @@ +#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 "session/config/namespaces.hpp" +#include "session/core/component.hpp" + +namespace session::core { + +namespace log = oxen::log; +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(); + 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; +} + +void Core::init() { + if (sodium_init() < 0) + throw std::runtime_error{"libsodium initialization failed!"}; + + _loop.reset(new quic::Loop()); + + apply_migrations(); + + for (auto* component : _comp_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(_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) + return; + + constexpr std::array namespaces = { + config::Namespace::Default, + config::Namespace::Devices, + config::Namespace::AccountPubkeys}; + + auto now_ms = epoch_ms(clock_now_ms()); + auto ed25519_hex = globals.pubkey_ed25519().hex(); + + // 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); + auto sig = ed25519::sign(seed.ed25519_secret(), to_span(to_sign)); + ns_sig[i] = "{:b}"_format(sig); + } + } + + net->get_swarm( + globals.pubkey_x25519(), + false, + [this, net, namespaces, ed25519_hex, now_ms, ns_sig = std::move(ns_sig)]( + auto, auto swarm) { + if (swarm.empty()) + return; + + auto& node = swarm.front(); + + // Build one batch subrequest per namespace. + nlohmann::json requests = nlohmann::json::array(); + { + auto conn = db.conn(); + for (size_t i = 0; i < namespaces.size(); ++i) { + auto ns = namespaces[i]; + auto ns_val = static_cast(ns); + nlohmann::json params = { + {"pubkey", globals.session_id_hex()}, + {"namespace", ns_val}, + }; + + if (!ns_sig[i].empty()) { + params["pubkey_ed25519"] = ed25519_hex; + params["timestamp"] = now_ms; + params["signature"] = ns_sig[i]; + } + + 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) + params["last_hash"] = *last_hash; + + requests.push_back({{"method", "retrieve"}, {"params", std::move(params)}}); + } + } + + auto body_str = nlohmann::json{{"requests", std::move(requests)}}.dump(); + net->send_request( + network::Request{ + node, + "batch", + to_vector(body_str), + network::RequestCategory::standard_small, + 20s}, + [this, sn_pubkey = node.remote_pubkey, namespaces]( + bool success, + bool timeout, + int16_t /*status_code*/, + std::vector> /*headers*/, + std::optional 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()); + + 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()); + } +} + +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. + 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. + auto status = PfsKeyStatus::fetching; + { + auto conn = db.conn(); + 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", + session_id, + age); + return PfsKeyStatus::fresh; + } + log::debug( + cat, + "prefetch_pfs_keys: cached key for {} is stale ({} old), re-fetching", + session_id, + age); + status = PfsKeyStatus::stale; + } 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", + session_id, + age); + return PfsKeyStatus::nak; + } + log::debug( + cat, + "prefetch_pfs_keys: expired NAK for {} ({} old), re-fetching", + session_id, + age); + } + } else { + log::debug(cat, "prefetch_pfs_keys: no cached key for {}, fetching", session_id); + } + } + + // 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 now_ms = epoch_ms(clock_now_ms()); + + // AccountPubkeys (-21) allows unauthenticated retrieve: no signature needed. + nlohmann::json params = { + {"pubkey", oxenc::to_hex(session_id)}, + {"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; + } + + 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::warning( + cat, + "Failed to fetch PFS keys for {}: {}", + sid, + timeout ? "timed out" + : body ? *body + : "request failed"); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::failed); + return; + } + + return _handle_pfs_response(sid, std::move(*body)); + }); + }); + return status; +} + +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"); + 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. + auto x25519_pub = sid.subspan<1>(); + + // 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 (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()); + 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"); + _store_pfs_nak(sid); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched(sid, PfsKeyFetch::not_found); + return; + } + + bool changed = _store_pfs_keys(sid, *pk_x25519, *pk_mlkem768); + if (callbacks.pfs_keys_fetched) + callbacks.pfs_keys_fetched( + 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 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); + b64 sig; + { + auto seed = globals.account_seed(); + auto to_sign_bytes = std::as_bytes(std::span{to_sign}); + ed25519::sign(sig, seed.ed25519_secret(), to_sign_bytes); + } + + nlohmann::json params = { + {"pubkey", globals.session_id_hex()}, + {"pubkey_ed25519", ed25519_hex}, + {"namespace", ns_val}, + {"data", "{:b}"_format(payload)}, + {"timestamp", now_ms}, + {"sig_timestamp", now_ms}, + {"signature", "{:b}"_format(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 ed25519::OptionalPrivKeySpan& 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()); + } + } + }; + + // 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); + + 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); + } + } + + // 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 payload; + try { + auto seed = globals.account_seed(); + auto ed_sec = seed.ed25519_secret(); + + if (pfs_x25519) + payload = encrypt_for_recipient_v2( + ed_sec, recipient, *pfs_x25519, *pfs_mlkem768, content, pro_privkey); + else if (force_v2) + payload = encrypt_for_recipient_v2_nopfs(ed_sec, recipient, content, pro_privkey); + else + 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; + } + + // 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 ? ed25519::OptionalPrivKeySpan{*pending.pro_privkey} + : ed25519::OptionalPrivKeySpan{}, + 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 ed25519::OptionalPrivKeySpan& pro_privkey, + std::chrono::milliseconds ttl, + bool force_v2) { + auto id = _next_message_id++; + + // 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_session_id); + + 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_session_id); + } 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; + + 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. + auto ed_sec = seed.ed25519_secret(); + + 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] == std::byte{0x00}) { + // Version 2 (PFS+PQ) or an unrecognised future version. + 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; + 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); + + 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; + out.pfs_encrypted = true; + 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 non-PFS fallback attempt. + break; + } + } + 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. + try { + auto decoded = decode_dm_envelope(ed_sec, 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] = 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) + 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, 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: + log::warning( + cat, + "receive_messages: ignoring unhandled namespace {}", + static_cast(ns)); + } +} + +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..0138d8b1 --- /dev/null +++ b/src/core/component.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +namespace session::core::detail { + +sqlite::Connection CoreComponent::conn() { + return core.db.conn(); +} + +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); +} + +} // namespace session::core::detail diff --git a/src/core/devices.cpp b/src/core/devices.cpp new file mode 100644 index 00000000..f4e2960f --- /dev/null +++ b/src/core/devices.cpp @@ -0,0 +1,1533 @@ +#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 +#include + +namespace session::core { + +using namespace fmt::literals; +using namespace oxen::log::literals; +using namespace session::literals; +using namespace std::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: {}", self_id); + else { + random::fill(self_id); + core.globals.set(dev_key, self_id); + log::info(cat, "Generated new unique device id: {}", self_id); + } +} + +std::string Devices::device_id() const { + return oxenc::to_hex(self_id); +} + +template +consteval auto KEY_DOMAIN() = delete; +template <> +consteval auto KEY_DOMAIN() { + return "SessionDeviceKeys"_bytes; +} +template <> +consteval auto KEY_DOMAIN() { + return "SessionAccountKeys"_bytes; +} + +template Keys> +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)); + + // 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; + hash::shake256(KEY_DOMAIN(), seed)(x_sec, ml_seed); + x25519::scalarmult_base(x_pub, x_sec); + + mlkem768::keygen(ml_pub, ml_sec, ml_seed); + + return keys; +} + +namespace { + +} // 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[{:9.4}], MLKEM768[{:9.4}]"_format(k.x25519_pub, k.mlkem768_pub); +} + +Devices::DeviceKeys Devices::rotate_device_keys() { + // 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); + + // Call this mainly to ensure that we can successfully produce keys from this seed. + auto keys = keys_from_seed(seed); + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + 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. + // 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); + + 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(clock_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(); + bool have_active = false; + for (auto [seed, rotated] : c.prepared_results, std::optional>( + "SELECT seed, rotated FROM device_privkeys" + " ORDER BY rotated DESC NULLS FIRST, created DESC")) { + auto& k = keys.emplace_back(keys_from_seed(seed)); + if (rotated) + k.rotated.emplace(std::chrono::seconds{*rotated}); + else + 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::optional> key_indicator) { + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + c.prepared_exec( + "DELETE FROM device_account_keys WHERE rotated < ?", + epoch_seconds(clock_now_s() - ACCOUNT_KEY_RETENTION)); + + std::vector keys; + bool have_active = false; + + 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) + k.rotated.emplace(std::chrono::seconds{*rotated}); + if (!rotated) + have_active = true; + 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 " + "pubkeys", + id); + keys.pop_back(); + } + } + + tx.commit(); + + if (!key_indicator && !have_active) { + log::info(cat, "No currently active account keys; generating a new one"); + rotate_account_keys(); + return active_account_keys(); + } + + return keys; +} + +namespace { + + // 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; + 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()); + 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", + row_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()); + } + } + } + + // 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. + 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) + 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)", + 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( + 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, 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, seqno, timestamp, type, desc, ver, pk_ml, pk_x] : + sqlite::IterableStatementWrapper< + int64_t, + sqlite::blob_guts>, + 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}; +} + +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 = clock_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 + // 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 + // 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); + } + + // 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_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()}; + + 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 + 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", k.seed); + } + } + + 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", device_id); + 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) { + 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.is_finished() && btdc.key() < key) + consume_extra(btdc, extra); + } + + 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("#"); + + 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.maybe("t").value_or(""sv); + 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); + } + + // 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"); + + 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] = result.devices.try_emplace(id); + if (!ins) + throw std::runtime_error{"Invalid encoded device data: duplicate device ids"}; + + auto& info = it->second; + info.id = id; + + 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. + // + // 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{devs.consume_integer()}); + } else { + decode_one(info, devs.consume_dict_consumer(), device::State::Registered); + } + } + + 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 + // 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; + 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; + + 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_b32 a; + random::fill(a); + + auto A = x25519::scalarmult_base(a); + + 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}; + }); + + 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::SHAREDSECRETBYTES, + mlkem768::SHAREDSECRETBYTES}; + }); + + cleared_b32 rnd; + int i = -1; + for (auto& [devid, info] : devices) { + ++i; + random::fill(rnd); + 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; + hash::blake2b_key_pers(nonce, A, PERS_DEV_NONCE, ciphertext_raw); + + cleared_b32 key_base; + random::fill(key_base); + + // 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() + encryption::XCHACHA20_ABYTES); + encryption::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; + auto eind = enc_indicator[i]; + auto ekey = enc_key[i]; + auto ct = ciphertext[i]; + + 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( + 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()); + 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. + 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 + + 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)..." + + 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("", "G"); + 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) { + return ed25519::sign(seed.ed25519_secret(), body); + }); + + assert(o.view().size() == out.size()); // Ensure we calculated exactly the right size above + + 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 { + 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; + } + + auto c = conn(); + SQLite::Transaction tx{c.sql}; + + // 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( + "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 + // (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(KICK_DEVICE_SQL, info.kicked->time_since_epoch().count(), id); + continue; + } + + // 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; + + // Mark as newly registered only on a state transition (not for info-only updates). + if (!was_registered) + c.prepared_exec(REGISTER_DEVICE_SQL, *dev_id); + } + + tx.commit(); +} + +Devices::LinkRequestResult 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 = 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. + 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. 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) + 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)", + 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); + auto sas = link_request_sas(to_span(plaintext)); + + // 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(encrypted, seed.seed(), "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}; +} + +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 + 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 (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) + 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() <= encryption::XCHACHA20_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(); + + auto devices_nonce = hash::blake2b_pers<24>(PERS_DEV_NONCE, ciphertext_raw); + + cleared_b32 ml_ss, aB, ki, key_base; + + std::vector plaintext_devices; + 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 + // 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]; + + 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]; + const auto& b = k.x25519_sec; + 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. + if (!std::ranges::equal( + hash::blake2b_pers<2>(PERS_KEY_KEY_IND, A, B, M, ct, ekey), eind)) + continue; + + if (!x25519::scalarmult(aB, b, A)) { + log::warning(cat, "X25519 multiplication failed; ignoring encrypted entry"); + continue; + } + + if (!mlkem768::decapsulate(ml_ss, ct, k.mlkem768_sec)) { + 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()); + encryption::xchacha20_xor(key_base, ekey, knonce, ki); + + // Now we can decrypt the encrypted payload: + if (encryption::xchacha20poly1305_decrypt( + plaintext_devices, enc_devices, devices_nonce, key_base)) { + 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 plaintext_devices; +} + +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, seed.seed(), "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() && pt.key() < "i") + consume_extra(pt, extra_outer); + 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) { + 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(clock_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_device_messages(std::span messages, bool is_final) { + for (const auto& msg : messages) { + try { + oxenc::bt_dict_consumer in{msg.data}; + auto type = in.require(""); + if (type == "G") + receive_device_group_message(msg.data); + else if (type == "L") + receive_link_request(msg.data); + 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(clock_now_s() - LINK_REQUEST_MAX_AGE)); +} + +void Devices::parse_account_pubkeys(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.data}; + auto M = in.require_span("M"); + auto X = in.require_span("X"); + in.require_signature( + "~", + [&x25519_pub](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"}; + }); + + // 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() { + 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) { + return xed25519::sign(seed.x25519_key(), 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/globals.cpp b/src/core/globals.cpp new file mode 100644 index 00000000..8bae772a --- /dev/null +++ b/src/core/globals.cpp @@ -0,0 +1,175 @@ +#include +#include + +#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 = sqlite::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(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 = sqlite::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); + const cleared_b32* seed_to_use = &seed; + if (!have_seed) { + 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: + 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); + + 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 + 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: {}", _session_id_hex); +} + +mnemonics::secure_mnemonic Globals::seed_mnemonic(const mnemonics::Mnemonics& lang, bool force_24) { + auto seed = _account_seed.access(); + // _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; + 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/core/link_sas.cpp b/src/core/link_sas.cpp new file mode 100644 index 00000000..85e1703b --- /dev/null +++ b/src/core/link_sas.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include +#include +#include + +using namespace session::literals; + +namespace session::core { + +std::array derive_sas_seed(std::span plaintext) { + auto salt = hash::blake2b_pers<16>("SessionLinkEmoji"_b2b_pers, plaintext); + + std::array seed; + if (0 != crypto_pwhash( + reinterpret_cast(seed.data()), + seed.size(), + reinterpret_cast(plaintext.data()), + plaintext.size(), + reinterpret_cast(salt.data()), + /*opslimit=*/2, + /*memlimit=*/16ULL * 1024 * 1024, + crypto_pwhash_ALG_ARGON2ID13)) + 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); + + 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; +} + +std::array link_request_sas(std::span plaintext) { + return sas_from_seed(derive_sas_seed(plaintext)); +} + +} // namespace session::core diff --git a/src/core/pro.cpp b/src/core/pro.cpp new file mode 100644 index 00000000..ecdc3466 --- /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 b32& 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..14858741 --- /dev/null +++ b/src/core/schema/000_devices.sql @@ -0,0 +1,95 @@ + +-- Table storing all the device group info +CREATE TABLE devices ( + id INTEGER PRIMARY KEY NOT NULL, + 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 + 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 + 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 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 ( + 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) +) 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 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_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), + -- 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_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; 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_namespaces.sql b/src/core/schema/000_namespaces.sql new file mode 100644 index 00000000..4d94660b --- /dev/null +++ b/src/core/schema/000_namespaces.sql @@ -0,0 +1,6 @@ +CREATE TABLE namespace_sync ( + 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/src/core/schema/000_pfs_key_cache.sql b/src/core/schema/000_pfs_key_cache.sql new file mode 100644 index 00000000..62025ee5 --- /dev/null +++ b/src/core/schema/000_pfs_key_cache.sql @@ -0,0 +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, -- 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/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..41b3e3d7 --- /dev/null +++ b/src/core/schema/CMakeLists.txt @@ -0,0 +1,46 @@ +# 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 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") + +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..8f1c47b5 --- /dev/null +++ b/src/core/schema/apply_schema.cpp.in @@ -0,0 +1,12 @@ +// Auto-generated by CMake from @SCHEMA_FULL_FILENAME@. Do not edit. +#include "schema_migrations.hpp" + +namespace session::core::schema { + +void @FUNC_NAME@(session::sqlite::Connection& conn, Core&) { + conn.sql.exec(R"_SQL_DELIM_( +@SCHEMA_SQL@ +)_SQL_DELIM_"); +} + +} // 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 new file mode 100644 index 00000000..f90c4869 --- /dev/null +++ b/src/core/schema/schema_registry.cpp.in @@ -0,0 +1,14 @@ +// Auto-generated by CMake from src/core/schema/schema_registry.cpp.in. Do not edit. + +#include +#include "schema_migrations.hpp" + +namespace session::core::schema { + +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); + } +} diff --git a/src/crypto/ed25519.cpp b/src/crypto/ed25519.cpp new file mode 100644 index 00000000..0d805756 --- /dev/null +++ b/src/crypto/ed25519.cpp @@ -0,0 +1,310 @@ +#include "session/crypto/ed25519.hpp" + +#include +#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 { + +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; + 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; +} + +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) { + 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"}; +} + +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..872399e3 --- /dev/null +++ b/src/crypto/x25519.cpp @@ -0,0 +1,58 @@ +#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 a9daea6c..3d4551fb 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,63 +1,22 @@ -#include "session/curve25519.hpp" - -#include -#include - -#include +#include +#include "session/crypto/ed25519.hpp" +#include "session/crypto/x25519.hpp" #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; @@ -67,9 +26,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; @@ -79,9 +37,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 bce3d269..00000000 --- a/src/ed25519.cpp +++ /dev/null @@ -1,199 +0,0 @@ -#include "session/ed25519.hpp" - -#include -#include -#include - -#include - -#include "session/export.h" -#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() { - 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( - 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"}; - } - - 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) { - auto result = derived_ed25519_privkey(ed25519_seed, "SessionProRandom"); - return result; -} -} // 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/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/hash.cpp b/src/hash.cpp index b698f6b3..a5ba9aa7 100644 --- a/src/hash.cpp +++ b/src/hash.cpp @@ -3,14 +3,17 @@ #include #include "session/export.h" +#include "session/hash.h" #include "session/util.hpp" -namespace session::hash { +namespace { -void hash( - std::span hash, - std::span msg, - std::optional> key) { +using namespace session; + +void hash_impl( + 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)"}; @@ -19,22 +22,31 @@ void hash( 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); } -std::vector hash( - const size_t size, - std::span msg, - std::optional> key) { - std::vector result; - result.resize(size); - hash(result, msg, key); +} // 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(size); + hash_impl(result, msg, key); return result; } @@ -50,13 +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}; - std::vector result = session::hash::hash(size, {msg_in, msg_len}, key); - std::memcpy(hash_out, result.data(), size); + 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/internal-util.hpp b/src/internal-util.hpp index 2525666a..f76ba498 100644 --- a/src/internal-util.hpp +++ b/src/internal-util.hpp @@ -1,22 +1,43 @@ #pragma once +#include + +#include #include +#include #include +using namespace session::literals; + 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/logging.cpp b/src/logging.cpp index 779b1f66..9c0a24ca 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -8,6 +8,7 @@ #include "oxen/log/level.hpp" #include "session/export.h" +#include "session/logging.h" namespace session { diff --git a/src/mnemonics/CMakeLists.txt b/src/mnemonics/CMakeLists.txt new file mode 100644 index 00000000..681b1b59 --- /dev/null +++ b/src/mnemonics/CMakeLists.txt @@ -0,0 +1,114 @@ +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 "") +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..9ae7c0a5 --- /dev/null +++ b/src/mnemonics/mnemonics.cpp @@ -0,0 +1,222 @@ +#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)} {} + +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) + return lang; + } + 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. + std::string word_prefix(std::string_view s, int n_codepoints) { + std::string result; + result.reserve(s.size()); + const char* it = s.data(); + const char* end = it + s.size(); + 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 + 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 result; + } + + using WordMap = std::unordered_map; + + const WordMap& get_word_map(const Mnemonics& lang) { + auto langs = get_languages(); + size_t idx = std::find(langs.begin(), langs.end(), &lang) - langs.begin(); + + static std::vector maps(langs.size()); + static std::vector flags(langs.size()); + + 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]; + } + + 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 + +// 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; + 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) { + 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; + + 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) + std::construct_at(out + n, out[sum % n]); + + return result; +} + +secure_mnemonic bytes_to_words( + std::span bytes, std::string_view lang_name, bool checksum) { + return bytes_to_words(bytes, get_language(lang_name), checksum); +} + +// 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( + "Output buffer size must be a multiple of 4 (got {})"_format(out.size())); + + 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 < expected_seed_words; i += 3) { + 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("Seed phrase encodes an invalid value"); + + oxenc::write_host_as_little(x, &out[(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 % 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; +} + +session::secure_buffer words_to_bytes( + std::span words, std::string_view lang_name) { + 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/src/multi_encrypt.cpp b/src/multi_encrypt.cpp index 93c7f72c..422c2cde 100644 --- a/src/multi_encrypt.cpp +++ b/src/multi_encrypt.cpp @@ -2,120 +2,77 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include #include +#include #include +#include "session/hash.hpp" +#include "session/util.hpp" + 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 { void encrypt_multi_key( - std::array& key, - const unsigned char* a, - const unsigned char* A, - const unsigned char* 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, B)) - 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()); + 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 // 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( - 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() + encryption::XCHACHA20_ABYTES); + encryption::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() < encryption::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); - } - - 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; + out.resize(ciphertext.size() - encryption::XCHACHA20_ABYTES); + return encryption::xchacha20poly1305_decrypt(out, ciphertext, nonce, key); } } // namespace detail -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++; @@ -127,21 +84,21 @@ 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; - std::array random_nonce; + std::array random_nonce; if (!nonce) { - randombytes_buf(random_nonce.data(), random_nonce.size()); - nonce.emplace(random_nonce.data(), random_nonce.size()); + random::fill(random_nonce); + nonce.emplace(random_nonce); } else if (nonce->size() != 24) { throw std::invalid_argument{"Invalid nonce: nonce must be 24 bytes"}; } @@ -158,16 +115,16 @@ 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); + random::fill(junk); enc_list.append(to_string(junk)); } } @@ -176,38 +133,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] = detail::x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = ed25519::x25519_keypair(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, @@ -219,38 +176,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] = detail::x_keys(ed25519_secret_key); + auto [x_privkey, x_pubkey] = ed25519::x25519_keypair(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()); @@ -270,23 +222,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); @@ -309,8 +261,7 @@ LIBSESSION_C_API unsigned char* session_encrypt_for_multiple_simple_ed25519( int pad) { try { - auto [priv, pub] = - session::detail::x_keys(std::span{ed25519_secret_key, 64}); + auto [priv, pub] = session::ed25519::x25519_keypair(to_byte_span<64>(ed25519_secret_key)); return session_encrypt_for_multiple_simple( out_len, messages, @@ -318,8 +269,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); @@ -339,10 +290,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); } @@ -362,9 +313,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); } @@ -384,9 +335,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/quic_file_client.cpp b/src/network/backends/quic_file_client.cpp new file mode 100644 index 00000000..09a91332 --- /dev/null +++ b/src/network/backends/quic_file_client.cpp @@ -0,0 +1,666 @@ +#include "session/network/backends/quic_file_client.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "session/clock.hpp" +#include "session/crypto/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, + 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{}, + (_max_udp_payload ? std::make_optional(*_max_udp_payload) + : std::nullopt)); + + // Set up TLS credentials + 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) { + _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)); + } + }); +} + +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; + // 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, bool timeout = false) { + if (request.on_complete) + loop->call([request, err, timeout] { request.on_complete(err, timeout); }); + }; + + 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(); + 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(); + }); + + { + std::lock_guard lock{state->mutex}; + if (state->done) + 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; + 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; }); + } + + // 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([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 { + 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 0fd485a2..b4d66d38 100644 --- a/src/network/backends/session_file_server.cpp +++ b/src/network/backends/session_file_server.cpp @@ -1,13 +1,16 @@ #include "session/network/backends/session_file_server.hpp" #include +#include #include #include #include +#include #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" @@ -33,6 +36,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"; @@ -84,12 +97,37 @@ 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; } +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) { + // 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{QUIC_FS_SESH_ADDRESS_MAINNET, QUIC_DEFAULT_PORT}; + + if (http_config.pubkey_hex == TESTNET_CONFIG.pubkey_hex && netid == opt::netid::Target::testnet) + return SRouterTarget{QUIC_FS_SESH_ADDRESS_TESTNET, 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); @@ -128,7 +166,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()) @@ -237,7 +275,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) { @@ -260,7 +298,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(); @@ -307,22 +345,17 @@ 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 signature = blind_version_sign(to_span(seckey.view()), platform, timestamp); + 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(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"), @@ -403,7 +436,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..959c796a 100644 --- a/src/network/key_types.cpp +++ b/src/network/key_types.cpp @@ -3,9 +3,11 @@ #include #include #include -#include #include +#include +#include +#include #include namespace session::network { @@ -17,16 +19,15 @@ 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())}; + throw std::runtime_error{"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(pk.data(), data()); + ed25519::scalarmult_base_noclamp(pk, *this); return pk; }; ed25519_pubkey ed25519_seckey::pubkey() const { ed25519_pubkey pk; - crypto_sign_ed25519_sk_to_pk(pk.data(), 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(pk.data(), data()); + x25519::scalarmult_base(pk, *this); return pk; }; @@ -81,13 +82,8 @@ 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())) - 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}); +x25519_pubkey compute_x25519_pubkey(std::span ed25519_pk) { + return x25519_pubkey::from_bytes(ed25519::pk_to_x25519(ed25519_pk)); } } // namespace session::network diff --git a/src/network/network_config.cpp b/src/network/network_config.cpp index ff8517fa..57e9ce2d 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,23 @@ 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) { @@ -289,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/direct_router.cpp b/src/network/routing/direct_router.cpp index acba0302..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()) @@ -165,21 +208,148 @@ 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] = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->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) + // 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.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; + upload_thread = std::thread([weak_self = weak_from_this(), this, upload_request = request, @@ -189,8 +359,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 +371,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 +394,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 +450,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 +461,63 @@ 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 +563,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 5399f1a7..d8ffda3d 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; @@ -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; } @@ -575,6 +576,75 @@ 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()) + 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; + 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()) @@ -852,6 +922,94 @@ 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); @@ -876,100 +1034,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()); @@ -977,12 +1049,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); }); } @@ -1042,7 +1109,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&) { @@ -1515,7 +1582,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 73c9e310..d6c251fb 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"); @@ -49,22 +51,23 @@ 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); @@ -72,9 +75,6 @@ namespace { if (!result) throw std::runtime_error{"Invalid destination"}; - if (result->first.size() != 32) - throw std::runtime_error{"Invalid remote key"}; - return *result; } @@ -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 { @@ -225,6 +222,94 @@ 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 (on the caller's thread), consuming the seed. + auto enc = std::make_shared(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{})) + .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, info.suggested_mtu); + }); + + _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()) @@ -240,14 +325,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()) { @@ -258,6 +345,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() { @@ -490,7 +580,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()); @@ -554,22 +644,253 @@ 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, + 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, max_udp_payload); + 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, info.suggested_mtu) + .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, info.suggested_mtu) + .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 + 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); - // 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] = - _active_uploads.emplace(upload_id, std::make_pair(request, std::thread{})) - .first->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) + 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.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; + upload_thread = std::thread([weak_self = weak_from_this(), this, upload_request = request, @@ -579,8 +900,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); @@ -593,11 +912,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; } @@ -620,11 +935,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()) @@ -680,12 +991,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,13 +999,58 @@ void SessionRouter::_upload_internal(UploadRequest request) { } 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); - // 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 { @@ -745,7 +1096,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&) { @@ -779,7 +1130,7 @@ void SessionRouter::_download_internal(DownloadRequest request) { } 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); @@ -819,9 +1170,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>()}; @@ -936,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{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 a8158e63..d97bcad1 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" @@ -41,8 +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 = 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) @@ -79,12 +91,16 @@ 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( 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( @@ -120,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; @@ -136,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))} { @@ -496,21 +508,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]( @@ -541,25 +544,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]( @@ -760,7 +792,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 +976,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 +1074,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()); @@ -1101,17 +1136,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" { @@ -1204,15 +1228,18 @@ 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; } 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 @@ -1391,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; } @@ -1405,7 +1434,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; } } @@ -1631,7 +1661,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); }); } @@ -1644,7 +1674,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); }); } @@ -1705,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) @@ -1796,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 {}; @@ -1829,8 +1859,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 (...) { @@ -1873,7 +1906,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'; @@ -1881,7 +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, 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/snode_pool.cpp b/src/network/snode_pool.cpp index 41f2c692..033b6a39 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" @@ -69,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; } @@ -175,7 +177,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 +940,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 +987,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/network/transport/quic_transport.cpp b/src/network/transport/quic_transport.cpp index c9c16127..f2a41481 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; @@ -113,7 +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, node.host(), node.omq_port}, request_id, category); + {node.remote_pubkey.view(), node.host(), node.omq_port}, request_id, category); }); } @@ -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() { @@ -234,7 +235,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.emplace(arg.remote_pubkey.view(), arg.host(), arg.omq_port); } }, request.destination); @@ -294,8 +295,8 @@ void QuicTransport::_establish_connection( if (!_endpoint) throw std::runtime_error{"Network is invalid"}; - auto conn_key_pair = ed25519::ed25519_key_pair(); - 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 || @@ -538,7 +539,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/src/onionreq/builder.cpp b/src/onionreq/builder.cpp index dcb8b2c0..e6bbfafe 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; @@ -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( @@ -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,8 @@ 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 69a491c6..1f8ad8b8 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" @@ -27,21 +27,22 @@ 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; - if (crypto_scalarmult(secret.data(), seckey.data(), pubkey.data()) != 0) + std::array secret; + 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; } 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); - auto usalt = to_span(salt); + auto usalt = to_span(salt); crypto_auth_hmacsha256_state state; @@ -64,17 +65,14 @@ 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"}; - 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; } @@ -89,9 +87,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); @@ -100,9 +98,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); @@ -111,8 +109,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 @@ -120,21 +118,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()); @@ -142,13 +140,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); @@ -157,18 +154,19 @@ 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; + 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) @@ -177,10 +175,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); @@ -197,7 +195,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 @@ -209,22 +207,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; @@ -232,11 +230,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..749c64e7 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,11 @@ 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 89230aee..9ab7ee51 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -4,13 +4,15 @@ #include #include +#include #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/onionreq/response_parser.h" +#include "session/util.hpp" using namespace session; @@ -34,7 +36,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 @@ -85,7 +87,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(), @@ -180,18 +182,17 @@ 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{ - 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 @@ -200,14 +201,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 5d12a0c4..c9201947 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -2,15 +2,18 @@ #include #include #include -#include #include #include #include +#include #include #include #include #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] = { @@ -48,6 +51,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) { @@ -121,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); @@ -172,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) { - array_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) { - array_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{ @@ -211,81 +195,29 @@ 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}); - 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()); + auto hash_to_sign = hash::blake2b_pers<32>( + ADD_PRO_PAYMENT_PERS, + version, + master_privkey.pubkey(), + rotating_privkey.pubkey(), + static_cast(payment_tx_provider), + payment_tx_payment_id, + payment_tx_order_id); - // 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) { - array_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) { - array_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, @@ -295,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( @@ -364,111 +286,44 @@ 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) { - array_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) { - array_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; 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}); - 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()); - - // Sign the hash with both keys + auto hash_to_sign = hash::blake2b_pers<32>( + GENERATE_PROOF_PERS, + version, + master_privkey.pubkey(), + rotating_privkey.pubkey(), + unix_ts_ms); + 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) { - array_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) { - array_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 { @@ -542,78 +397,33 @@ std::string GetProDetailsRequest::to_json() const { return result; } -array_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) { - array_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 - 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_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()); - - // Sign the hash - array_uc64 result = {}; - crypto_sign_ed25519_detached( - result.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - return result; + auto hash_to_sign = hash::blake2b_pers<32>( + 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) { - array_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) { @@ -776,83 +586,40 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { return result; } -array_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) { - array_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 - 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}); - 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()); - - // Sign the hash - array_uc64 result = {}; - crypto_sign_ed25519_detached( - result.data(), - nullptr, - hash_to_sign.data(), - hash_to_sign.size(), - master_privkey.data()); - return result; + auto hash_to_sign = hash::blake2b_pers<32>( + SET_PAYMENT_REFUND_REQUESTED_PERS, + version, + master_privkey.pubkey(), + unix_ts_ms, + refund_requested_unix_ts_ms, + static_cast(payment_tx_provider), + payment_tx_payment_id, + payment_tx_order_id); + + 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) { - array_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, @@ -863,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 { @@ -931,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 @@ -943,25 +704,24 @@ 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( - 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, @@ -973,13 +733,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; } @@ -987,25 +741,24 @@ 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( - 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, @@ -1016,13 +769,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; @@ -1031,19 +778,17 @@ 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::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, @@ -1053,13 +798,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; } @@ -1067,18 +806,16 @@ 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::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, @@ -1087,13 +824,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; } @@ -1101,27 +832,19 @@ 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::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; } 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; } @@ -1129,27 +852,19 @@ 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::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; } 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; } @@ -1178,13 +893,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; @@ -1211,13 +920,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; @@ -1240,13 +943,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; @@ -1273,13 +970,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; @@ -1307,7 +998,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; @@ -1372,7 +1063,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; @@ -1433,7 +1124,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; @@ -1481,27 +1172,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; } } @@ -1521,26 +1207,25 @@ 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::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); - 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, @@ -1552,13 +1237,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; } @@ -1566,27 +1245,26 @@ 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::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); - 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, @@ -1598,13 +1276,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; } @@ -1637,13 +1309,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; @@ -1669,7 +1335,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/random.cpp b/src/random.cpp index 434b859a..0a08f91d 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -7,15 +7,30 @@ #include #include "session/export.h" +#include "session/random.h" #include "session/util.hpp" namespace session::random { -std::vector random(size_t size) { - std::vector result; - result.resize(size); - randombytes_buf(result.data(), size); +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()}); +} +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; } @@ -39,10 +54,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 @@ -50,9 +69,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{reinterpret_cast(ret), size}); return ret; } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 56eba9de..83fef355 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -5,31 +5,30 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include +#include #include #include #include +#include "internal-util.hpp" #include "session/blinding.hpp" +#include "session/clock.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/random.hpp" #include "session/sodium_array.hpp" #include "session/types.hpp" using namespace std::literals; +using namespace session::literals; namespace session { @@ -66,28 +65,80 @@ namespace detail { // 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, - 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"}; - } +// 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"(/^\)"_bytes; + +// SHAKE256 domain prefix for deriving the XChaCha20+Poly1305 key and nonce from the X-Wing SS. +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 = 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; + +// 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] != std::byte{0x00} || ciphertext[1] != std::byte{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), 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( + std::span key_buf, + std::span nonce_out, + std::span ssx, + std::span E, + std::span X) { + 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); +} + +// 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) { + 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 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( @@ -96,10 +147,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); @@ -108,12 +156,12 @@ std::vector sign_for_recipient( return buf; } -static const std::span BOX_HASHKEY = to_span("SessionBoxEphemeralHashKey"); +static constexpr auto BOX_HASHKEY = "SessionBoxEphemeralHashKey"_bytes; -std::vector encrypt_for_recipient( - std::span 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); @@ -122,19 +170,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; - 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"}; + std::vector result; + result.resize(signed_msg.size() + encryption::BOX_SEALBYTES); + encryption::box_seal(result, signed_msg, recipient_pubkey.first<32>()); return result; } -std::vector encrypt_for_recipient_deterministic( - std::span 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); @@ -144,47 +190,335 @@ 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; - 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()); - - cleared_array eph_sk; - cleared_array eph_pk; + cleared_b32 seed; + hash::blake2b_key( + seed, BOX_HASHKEY, ed25519_privkey.seed(), recipient_pubkey.first(32), message); - 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_array 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()); + 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; - 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"}; + static_assert( + encryption::BOX_SEALBYTES == encryption::BOX_PUBLICKEYBYTES + encryption::BOX_MACBYTES); + + std::vector result; + result.resize(encryption::BOX_SEALBYTES + signed_msg.size()); + std::ranges::copy(eph_pk, result.begin()); + encryption::box_easy( + std::span{result}.subspan(encryption::BOX_PUBLICKEYBYTES), + signed_msg, + nonce, + recipient_pubkey.first<32>(), + eph_sk); + + return result; +} + +// 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 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(); + + // 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_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 = + (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; + + // 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, std::byte{0}); + + 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); + + { + 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_b64 h; + hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); + return ed25519::sign(sender_ed25519_privkey, h); + }); + if (pro_ed25519_privkey) + dict.append_signature("~P", [&](std::span body) { + return ed25519::sign(*pro_ed25519_privkey, body); + }); + assert(dict.view().size() == inner_dict_size); + } + + // In-place AEAD encrypt (libsodium explicitly supports c == m) + encryption::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 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}; + + // Step 1: Generate ephemeral X25519 keypair e/E + 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_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{ + 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; + random::fill(ssx_buf); // repurpose ssx_buf as random ML-KEM coins + 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) + 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( + ki, + E, + mlkem_ct, + enc_key_buf, + enc_nonce, + sender_ed25519_privkey, + recipient_session_id, + content, + pro_ed25519_privkey); +} +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 {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) { + + 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)) + 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() == 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"); + + // 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"}; + b64 h; + hash::blake2b_key_pers(h, recipient_session_id, V2_MSG_SIG_PERS, body); + 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; + if (dict.skip_until("~P")) + 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(); + + // Convert sender Ed25519 pubkey to X25519 and build the 33-byte session ID + 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] = 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); 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_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 (!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 + 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); + + 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"_bytes; +constexpr auto V2_NONPFS_SS_DOMAIN = "SessionV2NonPFSSS"_bytes; + +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 + 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; + random::fill(ki); + random::fill(outer_ct); + + // ss = eR, then overwritten in-place with SHA3-256(ss || R || E || V2_NONPFS_KDF_LABEL). + 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_b32 enc_key; + std::array 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_ed25519_privkey); +} + +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). + 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_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); +} + // Calculate the shared encryption key, sending from blinded sender kS (k = S's blinding factor) to // blinded receiver jR (j = R's blinding factor). // @@ -213,11 +547,11 @@ std::vector encrypt_for_recipient_deterministic( // 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, - std::span jB, - 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 @@ -225,175 +559,111 @@ 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; - - 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) - blinded_key_pair = blind15_key_pair(seed, server_pk, &k); - else if (kA[0] == 0x25 && jB[0] == 0x25) - blinded_key_pair = blind25_key_pair(seed, server_pk, &k); + std::pair blinded_key_pair; + cleared_b32 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[0] == 0x25; + bool blind25 = kA_prefixed[0] == std::byte{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` - 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; // 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; } -std::vector encrypt_for_blinded_recipient( - std::span 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) - throw std::invalid_argument{"Invalid recipient_blinded_id: expected 33 bytes"}; +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, 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()); // 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()); - } - - // Encrypt using xchacha20-poly1305 - cleared_array nonce; - randombytes_buf(nonce.data(), nonce.size()); + auto pk = ed25519_privkey.pubkey(); + buf.insert(buf.end(), pk.begin(), pk.end()); - 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); + 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] = BLINDED_ENCRYPT_VERSION; + ciphertext[0] = std::byte{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"}; + auto nonce = std::span{ciphertext}.last(); + random::fill(nonce); - assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - - // append the nonce, so that we have: data = b'\x00' + ciphertext + nonce - std::memcpy(ciphertext.data() + (1 + outlen), nonce.data(), nonce.size()); + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(1, buf.size() + encryption::XCHACHA20_ABYTES), + buf, + nonce, + enc_key); return ciphertext; } 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( - std::span 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"}; - // 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) - throw std::invalid_argument{"Invalid group_ed25519_pubkey: expected 32 bytes"}; - std::vector _compressed; + std::vector _compressed; if (compress) { _compressed = zstd_compress(plaintext); if (_compressed.size() < plaintext.size()) @@ -415,9 +685,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)); @@ -426,16 +695,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) @@ -453,119 +720,82 @@ 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); + + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), + to_span(encoded), + nonce, + group_enc_key.first()); return ciphertext; } -std::pair, std::string> decrypt_incoming_session_id( - std::span 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 - 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}; } -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 - 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}; } -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"}; - } - - 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) + 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, 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 (!encryption::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 @@ -574,97 +804,66 @@ std::pair, std::vector> decrypt_incomi return result; } -std::pair, std::string> decrypt_from_blinded_recipient( - std::span 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"}; - if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + - crypto_aead_xchacha20poly1305_ietf_ABYTES) +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() < encryption::XCHACHA20_NONCEBYTES + 1 + encryption::XCHACHA20_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); + 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)}; + fmt::format("Invalid ciphertext: version is not {}", BLINDED_ENCRYPT_VERSION)}; - std::vector nonce; 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); - 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 (!encryption::xchacha20poly1305_decrypt( + buf, + ciphertext.subspan(1, msg_size + encryption::XCHACHA20_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); + 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] == 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) @@ -673,50 +872,36 @@ std::pair, std::string> decrypt_from_blinded_recipien // 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_ed25519_pubkey, - std::span ciphertext) { + std::span> group_enc_keys, + 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"}; - 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 - // 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; + std::vector plain; - auto nonce = ciphertext.subspan(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - 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 = 0 == crypto_aead_xchacha20poly1305_ietf_decrypt( - plain.data(), - nullptr, - nullptr, - ciphertext.data(), - ciphertext.size(), - nullptr, - 0, - nonce.data(), - decrypt_ed25519_privkey.data()); + 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) { - result.index = index; + res_index = index; break; } } @@ -727,15 +912,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)}; @@ -744,27 +929,20 @@ 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())}; - 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)); + session_id = "05{:x}"_format(x_pk); - 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()) @@ -776,7 +954,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")) { @@ -794,14 +972,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) { @@ -815,219 +992,105 @@ 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) + if (ciphertext.size() < encryption::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; - 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())) + b32 key; + hash::argon2( + key, + {lowercase_name.data(), lowercase_name.size()}, + ONS_ARGON2_SALT, + hash::ARGON2_OPSLIMIT_MODERATE, + hash::ARGON2_MEMLIMIT_MODERATE, + hash::ARGON2ID13); + + std::vector msg; + msg.resize(ciphertext.size() - encryption::SECRETBOX_MACBYTES); + + 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); } - 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"}; + 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 // // xchacha-based encryption // 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()); - - 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"}; + b32 name_hash; + hash::blake2b(name_hash, lowercase_name); + 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 (!encryption::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) { - if (payload.size() < - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) +std::vector decrypt_push_notification( + std::span payload, std::span enc_key) { + if (payload.size() < encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_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; - 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(encryption::XCHACHA20_NONCEBYTES); + + std::vector buf(ct.size() - encryption::XCHACHA20_ABYTES); + + 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 - 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; } -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::vector encrypt_xchacha20( + std::span plaintext, std::span key) { -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 ciphertext( + encryption::XCHACHA20_NONCEBYTES + plaintext.size() + encryption::XCHACHA20_ABYTES); -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"}; + auto nonce = std::span{ciphertext}.first(); + random::fill(nonce); - std::vector ciphertext; - ciphertext.resize( - 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()), - enc_key.data()); - assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); - ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); + encryption::xchacha20poly1305_encrypt( + std::span{ciphertext}.subspan(encryption::XCHACHA20_NONCEBYTES), plaintext, nonce, key); return ciphertext; } -std::vector decrypt_xchacha20( - std::span ciphertext, std::span enc_key) { - if (ciphertext.size() < - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) +std::vector decrypt_xchacha20( + std::span ciphertext, std::span key) { + if (ciphertext.size() < encryption::XCHACHA20_NONCEBYTES + encryption::XCHACHA20_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); + 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(), - enc_key.data())) + 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)"}; - assert(mlen <= plaintext.size()); - plaintext.resize(mlen); return plaintext; } @@ -1046,9 +1109,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}); + 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(); @@ -1069,10 +1132,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}); + 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(); @@ -1098,11 +1161,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}, - {group_ed25519_pubkey, group_ed25519_pubkey_len}, - {group_enc_key, group_enc_key_len}, - {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 = { @@ -1110,11 +1173,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; } @@ -1128,8 +1187,7 @@ 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}); + 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); @@ -1152,9 +1210,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}); + 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); @@ -1179,11 +1237,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}); + 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); @@ -1206,32 +1264,27 @@ 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}, - {group_ed25519_pubkey, group_ed25519_pubkey_len}, - {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) { - 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; + try { + std::vector> keys; + keys.reserve(decrypt_ed25519_privkey_len); + 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), + 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) { + result.error_len_incl_null_terminator = format_c_str(error, error_len, "{}", e.what()); } return result; } @@ -1243,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 = std::span{ - nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}; + nonce = to_byte_span(nonce_in); auto session_id = session::decrypt_ons_response( - name_in, std::span{ciphertext_in, ciphertext_len}, nonce); + 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; @@ -1266,8 +1318,7 @@ 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}); + 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(); @@ -1281,13 +1332,12 @@ 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}); + to_byte_span(plaintext_in, plaintext_len), to_byte_span<32>(key_in)); *ciphertext_out = static_cast(malloc(ciphertext.size())); *ciphertext_len = ciphertext.size(); @@ -1301,13 +1351,12 @@ 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}); + 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 fe2b33fd..3a9169be 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -2,41 +2,23 @@ #include #include #include -#include #include #include #include +#include #include #include #include #include #include +#include #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" +#include "internal-util.hpp" #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"), @@ -66,67 +48,21 @@ const session_protocol_strings SESSION_PROTOCOL_STRINGS = { // clang-format on namespace { -session::array_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) { - 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 = {}; - session::make_blake2b32_hasher( - &state, - {SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION, - sizeof(SESSION_PROTOCOL_BUILD_PROOF_HASH_PERSONALISATION) - 1}); - 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()); - return result; -} - -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( - reinterpret_cast(sig.data()), - msg.data(), - msg.size(), - reinterpret_cast(rotating_pubkey.data())); - bool result = verify_result == 0; - return result; + return session::hash::blake2b_pers<32>( + session::BUILD_PROOF_PERS, version, gen_index_hash, rotating_pubkey, expiry_unix_ts_ms); } struct array_uc32_from_ptr_result { bool success; - session::array_uc32 data; + session::b32 data; }; static array_uc32_from_ptr_result array_uc32_from_ptr(const void* ptr, size_t len) { @@ -173,27 +109,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); - -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())}; +static_assert(sizeof(std::declval().gen_index_hash) == 32); +static_assert(sizeof(std::declval().rotating_pubkey) == 32); +static_assert(sizeof(std::declval().sig) == 64); - array_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; +bool ProProof::verify_message( + 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 { @@ -201,7 +127,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; @@ -222,8 +148,8 @@ ProStatus ProProof::status( return result; } -array_uc32 ProProof::hash() const { - array_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; } @@ -237,8 +163,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) { @@ -250,22 +175,19 @@ 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)); } -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) { @@ -277,98 +199,33 @@ 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_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_utf8(std::u8string_view msg) { + return pro_features_for_utf8({reinterpret_cast(msg.data()), msg.size()}); } - -std::vector encode_for_1o1( - std::span plaintext, - std::span ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const array_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; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - return result; +ProFeaturesForMsg pro_features_for_utf8(std::string_view msg) { + return pro_features_for_utf8({reinterpret_cast(msg.data()), msg.size()}); } -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, - 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; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - 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); } -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 nil_ed25519_privkey; - std::vector result = encode_for_destination(plaintext, nil_ed25519_privkey, dest); - return result; -} - -std::vector encode_for_group( - std::span plaintext, - std::span ed25519_privkey, - std::chrono::milliseconds sent_timestamp, - const array_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; - std::vector result = encode_for_destination(plaintext, ed25519_privkey, dest); - return result; -} - -// 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) { +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*/; @@ -379,19 +236,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. @@ -404,340 +261,160 @@ 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); - return result; + return payload.first(size_without_padding); } -enum class UseMalloc { No, Yes }; -static EncryptedForDestinationInternal encode_for_destination_internal( - std::span plaintext, - std::span 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) { - // 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; - 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); - } - - // 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 ed25519::OptionalPrivKeySpan& pro_key) { + b64 signature; + if (!pro_key) { + auto [dummy_pk, dummy_sk] = ed25519::keypair(); + signature = ed25519::sign(dummy_sk, content); + } else { + signature = ed25519::sign(*pro_key, content); } + std::string* pro_sig = envelope.mutable_prosig(); + pro_sig->assign(reinterpret_cast(signature.data()), signature.size()); +} - 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; - } - - // 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()); - } - } +// 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 ed25519::OptionalPrivKeySpan& 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"}; - 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_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); - array_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; - } + 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); + 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( + 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); +} - // 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, // recipient 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; - } - return result; +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 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_for_destination( - std::span plaintext, - std::span 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); +std::vector encode_dm_v1( + std::span plaintext, + const ed25519::PrivKeySpan& ed25519_privkey, + sys_ms sent_timestamp, + 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 = + 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(epoch_ms(sent_timestamp)); + 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; } -DecodedEnvelope decode_envelope( - const DecodeEnvelopeKey& keys, - std::span envelope_payload, - const array_uc32& 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); +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 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 " + "no longer supported"}; } - if (!envelope.ParseFromArray(envelope_plaintext.data(), envelope_plaintext.size())) - throw std::runtime_error{"Parse envelope from plaintext 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(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); +} - // 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. +// 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 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()) { @@ -785,58 +462,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 @@ -863,9 +496,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()) { @@ -919,24 +552,104 @@ 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, - const array_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 @@ -953,7 +666,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( @@ -962,8 +675,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) @@ -998,13 +710,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{ @@ -1026,7 +738,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"); @@ -1079,9 +791,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` @@ -1090,8 +800,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()); @@ -1101,11 +813,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)}); } } @@ -1120,28 +834,13 @@ 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; 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 <= @@ -1151,8 +850,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( @@ -1169,8 +867,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( @@ -1185,12 +882,12 @@ 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 = {}; - session::array_uc32 hash = proof_hash_internal( +LIBSESSION_C_API cbytes32 session_protocol_pro_proof_hash(session_protocol_pro_proof const* proof) { + cbytes32 result = {}; + session::b32 hash = proof_hash_internal( proof->version, - 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; @@ -1200,16 +897,14 @@ 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::array_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( @@ -1218,10 +913,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( @@ -1259,59 +956,69 @@ 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, }; +} + +// 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) { + result.error_len_incl_null_terminator = copy_c_str(error, error_len, e.what()); + } 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, 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, 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( + std::span{static_cast(plaintext), plaintext_len}, + ed25519::PrivKeySpan{ + static_cast(ed25519_privkey), ed25519_privkey_len}, + from_epoch_ms(sent_timestamp_ms), + to_byte_span(recipient_pubkey->data), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1321,30 +1028,24 @@ 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, 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( + std::span{static_cast(plaintext), plaintext_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), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1355,15 +1056,13 @@ 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( + std::span{static_cast(plaintext), plaintext_len}, + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API @@ -1373,80 +1072,24 @@ 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, 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); - - EncryptedForDestinationInternal result_internal = encode_for_destination_internal( - /*plaintext=*/{static_cast(plaintext), plaintext_len}, - /*ed25519_privkey=*/ - {static_cast(ed25519_privkey), ed25519_privkey_len}, - /*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( + std::span{static_cast(plaintext), plaintext_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), + ed25519::OptionalPrivKeySpan{ + static_cast(pro_rotating_ed25519_privkey), + pro_rotating_ed25519_privkey_len}); + }); } LIBSESSION_C_API void session_protocol_encode_for_destination_free( @@ -1472,50 +1115,65 @@ 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; } - // 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); - } + 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++) { + 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_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) { - 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 = 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) + 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) { + 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 keys ed25519_privkeys were provided") + 1; + if (keys->decrypt_keys_len == 0) { + result.error_len_incl_null_terminator = + format_c_str(error, error_len, "No ed25519 private keys were provided"); + } } // Marshall into c type @@ -1523,15 +1181,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); @@ -1575,21 +1226,19 @@ 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 = 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; } @@ -1608,15 +1257,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 5efa976d..cc8ec5e4 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -1,14 +1,13 @@ #include #include -#include #include 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)); @@ -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; diff --git a/src/util.cpp b/src/util.cpp index afe3d441..e3f6688f 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,10 +1,12 @@ #include +#include #include #include #include #include #include +#include #include #include @@ -39,6 +41,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> @@ -112,9 +127,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 { @@ -128,14 +143,14 @@ 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; } -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(); @@ -144,7 +159,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 { @@ -155,7 +170,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 new file mode 100644 index 00000000..cd115746 --- /dev/null +++ b/src/xed25519-tweetnacl.cpp @@ -0,0 +1,150 @@ +// 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 + +#include "session/xed25519.hpp" + +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, reinterpret_cast(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(reinterpret_cast(ed_pk.data()), y); + return ed_pk; +} + +} // namespace session::xed25519 diff --git a/src/xed25519.cpp b/src/xed25519.cpp index 9d23390a..cca107bb 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -1,36 +1,30 @@ #include "session/xed25519.hpp" #include -#include -#include #include #include #include +#include #include #include #include #include "session/export.h" +#include "session/hash.hpp" #include "session/util.hpp" +#include "session/xed25519.h" 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 { - 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`. @@ -38,24 +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 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()); + 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; } @@ -64,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()); @@ -79,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()); @@ -113,42 +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; + 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); + std::span signature, + std::span curve25519_pubkey, + std::span msg) { auto ed_pubkey = pubkey(curve25519_pubkey); return 0 == crypto_sign_ed25519_verify_detached( - signature.data(), msg.data(), msg.size(), ed_pubkey.data()); + 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)); } -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(std::span{ + reinterpret_cast(curve25519_pubkey.data()), 32}); return std::string{reinterpret_cast(ed_pk.data()), ed_pk.size()}; } @@ -163,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 (...) { @@ -176,19 +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 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; - } + 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/CMakeLists.txt b/tests/CMakeLists.txt index 2ed0d1af..4a354322 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,9 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release") 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 @@ -20,12 +23,14 @@ set(LIB_SESSION_UTESTS_SOURCES test_curve25519.cpp test_ed25519.cpp test_encrypt.cpp + test_format.cpp test_group_keys.cpp test_group_info.cpp test_group_members.cpp 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 @@ -45,13 +50,20 @@ 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) + 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) target_link_libraries(test_libs INTERFACE libsession::config - libsodium::sodium-internal + libsession::core + libsession::crypto + session::SQLite + sessiondep::libsodium + mlkem_native::mlkem768 nlohmann_json::nlohmann_json oxen::logging) @@ -87,6 +99,30 @@ 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 + 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..be94abdc --- /dev/null +++ b/tests/dns_utils.hpp @@ -0,0 +1,40 @@ +#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 new file mode 100644 index 00000000..024563fc --- /dev/null +++ b/tests/live/live_utils.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dns_utils.hpp" +#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; + +// 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) { + 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 +// 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); + auto seed = core.globals.account_seed(); + auto sig = session::ed25519::sign(seed.ed25519_secret(), session::to_span(to_sign)); + + 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_file_transfer.cpp b/tests/live/test_file_transfer.cpp new file mode 100644 index 00000000..9883f5c9 --- /dev/null +++ b/tests/live/test_file_transfer.cpp @@ -0,0 +1,197 @@ +#include + +#include +#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 {encrypted.begin(), encrypted.end()}; + }; + 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: 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. + + 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 {encrypted.begin(), encrypted.end()}; + }; + 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/live/test_pubkey_xfer.cpp b/tests/live/test_pubkey_xfer.cpp new file mode 100644 index 00000000..8391ed33 --- /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(); + + b33 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. + b33 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/quic-files.cpp b/tests/quic-files.cpp new file mode 100644 index 00000000..481dcfed --- /dev/null +++ b/tests/quic-files.cpp @@ -0,0 +1,431 @@ +#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; + +namespace { + +using session::human_size; +using clock = std::chrono::steady_clock; + +namespace net = session::network; +namespace fs = net::file_server; +namespace attachment = session::attachment; + +using upload_result = std::variant, int16_t>; +using download_result = std::variant; +using on_data_t = std::function)>; + +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::shared_ptr network, + std::string download_cmd_hint) { + auto path = std::filesystem::path{filename}; + if (!std::filesystem::exists(path)) { + fmt::print(stderr, "File not found: {}\n", path.string()); + return 1; + } + auto file_size = static_cast(std::filesystem::file_size(path)); + fmt::print(stderr, "Uploading {} ({})...\n", path.string(), human_size{file_size}); + + std::array seed; + randombytes_buf(seed.data(), seed.size()); + + auto start = clock::now(); + std::promise promise; + auto future = promise.get_future(); + + 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.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(); + auto elapsed_s = std::chrono::duration(clock::now() - start).count(); + + 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(meta.size / std::max(elapsed_s, 0.001))}; + + fmt::print( + "\nUpload complete!\n" + " File ID: {}\n" + " Key: {}\n" + " Size: {}\n" + " Time: {:.1f}s\n" + " Speed: {}/s\n" + "\n" + "To download:\n" + "{} {} {}\n", + meta.id, + key_hex, + human_size{meta.size}, + elapsed_s, + speed, + download_cmd_hint, + meta.id, + key_hex); + return 0; + } + + fmt::print(stderr, "Upload failed with error {}\n", 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)) { + fmt::print(stderr, "Invalid key: expected 64 hex characters\n"); + 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); + fmt::print( + stderr, + "Transfer started after {:.0f}ms (file size: {})\n", + 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)}; + fmt::print( + stderr, + "[{}/{}] {}/s\n", + human_size{received_bytes}, + human_size{info.size}, + recent_speed); + last_progress = now; + last_progress_bytes = received_bytes; + } + }, + [&](download_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); + } + 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))}; + 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()) + fmt::print("Written to {}\n", output); + return 0; + } + + if (out_file.is_open()) { + out_file.close(); + std::filesystem::remove(output); + } + fmt::print(stderr, "Download failed with error {}\n", std::get(result)); + return 1; +} + +// --- Mode-specific runners --- + +struct CliArgs { + // Mode + bool srouter = 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; + 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() / + (args.testnet ? "quic_files_cache_testnet" : "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) { + auto resolved = resolve_host(args.server_address); + if (resolved != args.server_address) + 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}); + } + + 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 = fmt::format( + "{}{}{}", + args.argv0, + args.srouter ? " --srouter" : "", + args.testnet ? "" : " --mainnet"); + + if (is_upload) { + return do_upload( + args.upload_filename, + args.domain, + args.ttl, + network, + 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); + + 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; + 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); + app.fallthrough(); // Allow global options after subcommand + + CliArgs args; + args.argv0 = argv[0]; + + 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 (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( + "--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 (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; + 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); + cats.apply(); + } + + return run(args, upload_cmd->parsed()); +} 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..05d3bb75 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,23 +54,21 @@ 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}) - : std::nullopt, + 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::nullopt, + std::span{gpk, 32}, + admin ? std::make_optional>({*gsk, 64}) : std::nullopt, std::nullopt, info, members} {} @@ -78,15 +76,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 +102,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 +112,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 +132,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_attachment_encrypt.cpp b/tests/test_attachment_encrypt.cpp index 61d02843..b4d4222a 100644 --- a/tests/test_attachment_encrypt.cpp +++ b/tests/test_attachment_encrypt.cpp @@ -362,3 +362,84 @@ 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)); + } +} diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index 959a8c0b..cf40630f 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -1,43 +1,29 @@ #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, -}; - -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 seed1 = + "fecd9a6034bc9aba273925dee7062b123334587c3c6257341afae2d7fe85e122" + "f4ef873908f6a5377ba3853f0e2fa326eed9e741edf9f7d0311a3ecc66a57b32"_hex_b; +constexpr auto seed2 = + "8659efdcbe0949e0f81141e6d397e8be75f45d09262f209d5950e97989eb43c7" + "3570b69a47dc094544c1c5089c40414bbda1ffdde8aab2617fe937ee74a5ee81"_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); +const std::string session_id2 = oxenc::to_hex(sid2); TEST_CASE("Communities 25xxx-blinded pubkey derivation", "[blinding25][pubkey]") { REQUIRE(sodium_init() >= 0); @@ -55,216 +41,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"); @@ -272,10 +200,8 @@ 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; - 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()); + cleared_b32 expect_seed; + 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())); @@ -288,31 +214,25 @@ 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); + 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()); - 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 2667d342..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,12 +122,12 @@ 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[0] == std::byte{'z'}); + CHECK(d.size() == 156); CHECK(d.size() < data2.size()); CHECK(to_hex(d) == - "7a28b52ffd20aa9d0400e40764313a23693165313a2664313a6e31323a4b616c6c6965313a7032393a68" - "7474703a2f2f6b2e6578616d706c652e6f72672f4b626d70313a71323473656372657465313a3c6c6c69" - "306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564653d6431" - "3a6e303a313a7071303a6565070028812c55282f03fceac460149b57cd509a"); + "7a28b52ffd20aa95040022881f1f907d9c93291a7627219a79d06bb82c3c69341b104115dbf3c0860176" + "f63013ff7ba4247de211d1275be493fffff6eb7892db81b9dc9da26f40955e5d868586cd577bb69e00f7" + "caf2110f04219f7cf49bda3f19a5f4091966d5c199a3f14132c4d26f7cc7e14914edbca3903ef91e0862" + "955712d1275be1939f78844fb606008c12e0cb50be3a1c18c5e655339426"); } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index e7bb0ae5..bb766abb 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -2,12 +2,12 @@ #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}; @@ -656,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(session::to_span(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()); @@ -687,37 +673,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 +719,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 +727,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 +833,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 +867,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 +878,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 +966,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 +1012,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 +1093,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 fe812f23..8098b5c5 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,22 @@ 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 +409,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 +491,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 +571,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 +582,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 +612,8 @@ 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{}; - 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 +684,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 +696,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 +725,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 +791,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 +803,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 +813,10 @@ TEST_CASE("Conversation pro data", "[config][conversations][pro]") { .count(); c.pro_expiry_unix_ts_ms = 10000; - session::array_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 +839,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 +851,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..86cde79b 100644 --- a/tests/test_config_local.cpp +++ b/tests/test_config_local.cpp @@ -1,29 +1,26 @@ #include #include #include -#include #include #include #include #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 053905c2..1f0b161a 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,29 +43,17 @@ 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); + 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)); CHECK(std::memcmp(hash_to_sign_cpp.data(), hash_to_sign.data, hash_to_sign_cpp.size()) == 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,20 +69,13 @@ 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()), + reinterpret_cast(body.data()), body.size())); } @@ -128,7 +108,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..3f2c3c27 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,18 @@ 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: - "01020304050000000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000000000000000000000000000"_hexbytes; + to_vector( + "0102030405000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000"_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 +572,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 +585,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 +705,11 @@ 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; - to_merge.emplace_back( - "fakehash1", - std::span{to_push->config[0], to_push->config_lens[0]}); + std::vector>> to_merge; + 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); @@ -739,18 +723,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 +805,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 +848,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 +877,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 f307eae9..6eadb0d1 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -1,17 +1,19 @@ #include #include #include -#include #include #include #include #include +#include #include #include +#include "../src/config/internal.hpp" #include "utils.hpp" +using namespace session; using namespace std::literals; namespace { @@ -51,13 +53,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 +64,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 +91,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 +164,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 +179,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 +212,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 +276,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 +284,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 +406,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 +420,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 +447,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 +458,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 +468,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 +526,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 +552,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 +584,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 +592,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 +604,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 +625,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 24948943..e0f602f5 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 #include "session/bt_merge.hpp" @@ -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,12 +109,7 @@ 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; -} +} // namespace TEST_CASE("config diff", "[config][diff]") { MutableConfigMessage m; @@ -330,20 +327,15 @@ 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; @@ -377,15 +369,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()); @@ -398,8 +388,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); @@ -426,7 +416,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), @@ -445,10 +435,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" @@ -489,7 +479,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 @@ -556,7 +546,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(); @@ -697,7 +687,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}}}, @@ -719,7 +709,7 @@ 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 @@ -760,7 +750,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:" @@ -806,8 +796,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( @@ -995,7 +985,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 new file mode 100644 index 00000000..6f3c37ff --- /dev/null +++ b/tests/test_core_devices.cpp @@ -0,0 +1,373 @@ +#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; + +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_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 + 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 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()); + } + + 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 = + sig.size() == 64 && xed25519::verify(sig.first<64>(), x25519_pub, body); + }); + CHECK(sig_valid); + } +} diff --git a/tests/test_core_network.cpp b/tests/test_core_network.cpp new file mode 100644 index 00000000..7b1a8fcb --- /dev/null +++ b/tests/test_core_network.cpp @@ -0,0 +1,29 @@ +#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{db_path, callbacks}; + + 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); +} 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 new file mode 100644 index 00000000..cb48d79b --- /dev/null +++ b/tests/test_dm_receive.cpp @@ -0,0 +1,431 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "session/crypto/ed25519.hpp" +#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_b; + +struct SenderKeys { + b32 ed_pk; + b64 ed_sk; + b33 session_id; // 0x05-prefixed long-term X25519 pubkey + + SenderKeys() { + 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; + 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} {} +}; + +} // 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}; + + 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_b; + 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); + + 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); + b33 recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + constexpr auto content = "deadbeef"_hex_b; + auto ct = encrypt_for_recipient_v2( + 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); + + 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()); + CHECK(msg.pfs_encrypted); +} + +// ── 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_b); + 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_b); + 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); + 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, + x25519_bytes, + mlkem_bytes, + "01"_hex_b, + std::nullopt); + deliver(std::span{ct}); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::no_pfs_key); + } + + 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 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); + 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, + 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() ^= std::byte{0xff}; + deliver(std::span{ct}); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::no_pfs_key); + } + + SECTION("v1 malformed ciphertext → decrypt_failed") { + deliver("0102030405060708"_hex_b); + CHECK(received.empty()); + REQUIRE(failures.size() == 1); + CHECK(failures[0] == MessageDecryptFailure::decrypt_failed); + } +} + +// ── 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. + + b33 recip_session_id; + std::ranges::copy(recipient->globals.session_id(), recip_session_id.begin()); + + 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)}; + 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(); + + 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}; + + // 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{mlkem_bytes[0], mlkem_bytes[1]}; + + 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. + 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}; + + 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 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{m[0], 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_b; + auto ct = encrypt_for_recipient_v2( + sender.ed_sk, + recip_session_id, + target_pubkeys.first, + 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( + "_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}; + + 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_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); + + 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_dm_send.cpp b/tests/test_dm_send.cpp new file mode 100644 index 00000000..a200b4f1 --- /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_b; + +// 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 = 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 = 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 = 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_ed25519.cpp b/tests/test_ed25519.cpp index e82092af..73605940 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,13 @@ 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,12 +118,12 @@ 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")); - CHECK_THROWS(session::ed25519::sign(ed_invalid, to_span("hello"))); + CHECK_THROWS(session::ed25519::sign({ed_invalid.data(), ed_invalid.size()}, to_span("hello"))); auto expected_sig_hex = "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" @@ -134,6 +131,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 87383753..2c304e58 100644 --- a/tests/test_encrypt.cpp +++ b/tests/test_encrypt.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -15,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" @@ -25,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_format.cpp b/tests/test_format.cpp new file mode 100644 index 00000000..d712c64c --- /dev/null +++ b/tests/test_format.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include +#include +#include + +#include "session/format.hpp" +#include "utils.hpp" + +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", "[format]") { + 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", "[format]") { + 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", "[format]") { + 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", "[format]") { + 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", "[format]") { + CHECK(fmt::format("{:b}", "00010203"_hex_b) == "AAECAw=="); + CHECK(fmt::format("{:B}", "00010203"_hex_b) == "AAECAw"); +} + +TEST_CASE("byte span formatting - raw", "[format]") { + CHECK(fmt::format("{:r}", "6869"_hex_b) == "hi"); +} + +TEST_CASE("byte span formatting - ellipsis", "[format]") { + // 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", "[format]") { + 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", "[format]") { + 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", "[format]") { + using namespace session::literals; + auto val = "deadbeef"_hex_b; + CHECK("key: {}"_format(val) == "key: deadbeef"); + CHECK("key: {:z}"_format(val) == "key: deadbeef"); +} diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 4dfea5c1..8fbc8a07 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -1,11 +1,11 @@ #include #include #include -#include #include #include #include +#include #include #include @@ -14,39 +14,40 @@ 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. 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(), - "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); + ginfo2.add_key(std::span{k}.first<32>(), false); ginfo1.set_name("GROUP Name"); CHECK(ginfo1.is_dirty()); @@ -64,7 +65,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 +74,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 +97,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 +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(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 +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(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,31 +147,28 @@ 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); + 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,10 +177,10 @@ 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); + ginfo_rw.add_key(std::span{k}.first<32>(), false); ginfo_rw.set_name("Super Group!!"); CHECK(ginfo_rw.is_dirty()); @@ -195,15 +195,15 @@ 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); + 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()); @@ -218,22 +218,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())); + const auto seed_bad1 = "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.add_key(std::span{k}.first<32>(), 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,10 +304,10 @@ 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); + ginfo2.add_key(std::span{k}.first<32>(), false); CHECK(!ginfo.needs_dump()); CHECK(!ginfo2.needs_dump()); @@ -328,10 +322,10 @@ 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); + 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"); @@ -348,7 +342,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); @@ -360,11 +354,12 @@ 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(); 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..7dc89009 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,16 +409,16 @@ 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"); + "unable to decrypt ciphertext with any current group keys"); // Duplicate members[1] from dumps 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..1ed2bcb6 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,43 +24,42 @@ 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. 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(), - "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); + gmem2.add_key(std::span{k}.first<32>(), 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 +70,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 +80,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 +101,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 +204,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 +217,12 @@ 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 +304,12 @@ 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 +376,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 +403,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 ab1e2bc1..0c9b874f 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,184 @@ TEST_CASE("Hash generation", "[hash][hash]") { CHECK(to_hex(hash5) == expected_hash5); 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. + // 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::b32; + using session::hash::sha3_256; + using session::hash::shake256; + + b32 sha3_out, shake_out; + + // --- SHA3-256 NIST vectors --- + + // Empty input + sha3_256(sha3_out, ""_bytes); + CHECK(oxenc::to_hex(sha3_out) == + "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"); + + // "abc" (24 bits) + sha3_256(sha3_out, "abc"_bytes); + CHECK(oxenc::to_hex(sha3_out) == + "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); + + // 448-bit message + sha3_256(sha3_out, "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"_bytes); + CHECK(oxenc::to_hex(sha3_out) == + "41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376"); + + // 896-bit message + sha3_256( + sha3_out, + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklm" + "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(""_bytes)(shake_out); + CHECK(oxenc::to_hex(shake_out) == + "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f"); + + // "abc" (24 bits) + 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"_bytes); + shake256("abc"_bytes)(shake_out); + CHECK(sha3_out != shake_out); +} diff --git a/tests/test_helper.hpp b/tests/test_helper.hpp new file mode 100644 index 00000000..b162573c --- /dev/null +++ b/tests/test_helper.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include + +#include +#include +#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. +// 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 + explicit TempCore(Opts&&... opts) : + 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; + 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(); } + 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+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 = ? AND sn_pubkey = ?", + ns, + sn_pubkey); + } + + // 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; + } + + // 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}; + } + + // 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 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; + } + + // 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) { + 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, 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}; + } +}; + +} // namespace session 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_mnemonics.cpp b/tests/test_mnemonics.cpp new file mode 100644 index 00000000..2103af48 --- /dev/null +++ b/tests/test_mnemonics.cpp @@ -0,0 +1,354 @@ +#include +#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 mnemonic = bytes_to_words(seed, *lang, false); + REQUIRE(mnemonic.size() == 48); + auto wspan = mnemonic.open(); + for (size_t i = 0; i < 48; i++) + CHECK(wspan[i] == exp_words[i]); + } + } +} + +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, false); + CHECK(words12.size() == 12); + 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.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.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.open().words, *lang); + CHECK(std::ranges::equal(back25.access().buf, 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, false); + REQUIRE(words.size() == 3); + + SECTION("Exact match") { + 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.open()) { + 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(std::ranges::equal(back.access().buf, data)); + } + + SECTION("Prefix match") { + std::vector prefix_words; + std::vector storage; + 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(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.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(std::ranges::equal(back.access().buf, 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 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 + 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] == 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(s4[3] == s3[expected_pos]); + } + + SECTION("Checksum round-trip") { + 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 + 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") { + 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); + } +} + +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") { + // 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); + } +} diff --git a/tests/test_multi_encrypt.cpp b/tests/test_multi_encrypt.cpp index 9eed94ed..ec22de6a 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 -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,87 +191,66 @@ 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() == /* de */ 2 + /* 1:# 24:...nonce... */ 3 + 27 + /* 1:e le */ 3 + 2 + - /* XX: then data with overhead */ 3 * - (3 + 5 + crypto_aead_xchacha20poly1305_ietf_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 != session::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), - "test suite")); - - auto m1 = session::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), - "test suite"); - auto m2 = session::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), - "test suite"); - auto m3 = session::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), - "test suite"); - auto m3b = session::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), - "not test suite"); - auto m4 = session::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), - "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"); + auto m2 = decrypt_for_multiple_simple( + 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"); + auto m3b = decrypt_for_multiple_simple( + 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"); REQUIRE(m1); REQUIRE(m2); @@ -288,11 +262,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,36 +277,16 @@ TEST_CASE("Multi-recipient encryption, simpler interface", "[encrypt][multi][sim "bcb642c49c6da03f70cdaab2ed6666721318afd631"_hex, "1ecee2215d226817edfdb097f05037eb799309103a"_hex)); - m1 = session::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), - "test suite"); - m2 = session::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), - "test suite"); - m3 = session::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), - "test suite"); - m3b = session::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), - "not test suite"); - m4 = session::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), - "test suite"); + m1 = decrypt_for_multiple_simple( + 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"); + m3 = decrypt_for_multiple_simple( + 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"); + m4 = decrypt_for_multiple_simple( + encrypted, x_keys[4].first, x_keys[4].second, x_keys[0].second, "test suite"); REQUIRE(m1); REQUIRE(m2); @@ -344,10 +298,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_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_onion_request_router.cpp b/tests/test_onion_request_router.cpp index e48e3169..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 #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 new file mode 100644 index 00000000..ec8856b5 --- /dev/null +++ b/tests/test_pfs_key_cache.cpp @@ -0,0 +1,262 @@ +#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. +// 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 msg_item; + msg_item["data"] = oxenc::to_base64( + std::string_view{reinterpret_cast(msg_data.data()), msg_data.size()}); + 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["messages"] = nlohmann::json::array(); + return resp; +} + +TEST_CASE("prefetch_pfs_keys throws without network", "[core][pfs]") { + TempCore c; + TempCore remote; + auto session_id = remote->globals.session_id(); + b33 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(); + b33 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); + CHECK(mock_net->sent_requests[0].request.endpoint == "retrieve"); + auto req = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + 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()); + + 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); + 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); + 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") { + // 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 advance_past_fresh{core::Core::PFS_KEY_FRESH_DURATION + 1s}; + c->prefetch_pfs_keys(sid); + CHECK(mock_net->sent_requests.size() == 1); + } +} + +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(); + b33 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 advance_past_nak_expiry{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 advance_past_fresh{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 advance_past_fresh{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(); + c->set_network(mock_net); + + TempCore remote; + auto session_id_span = remote->globals.session_id(); + b33 sid; + std::ranges::copy(session_id_span, sid.begin()); + + 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 msg_item; + msg_item["data"] = oxenc::to_base64("not a bt-dict"); + 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); + REQUIRE(entry.has_value()); + CHECK(entry->nak_at.has_value()); + CHECK_FALSE(entry->pubkey_x25519.has_value()); + } + + SECTION("Bad signature: NAK written, no valid pubkeys stored") { + c->prefetch_pfs_keys(sid); + REQUIRE(mock_net->sent_requests.size() == 1); + + // 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()); + 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: nothing 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 new file mode 100644 index 00000000..0a97ca84 --- /dev/null +++ b/tests/test_poll.cpp @@ -0,0 +1,172 @@ +#include +#include + +#include +#include +#include +#include + +#include "test_helper.hpp" + +using namespace session; + +// 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 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", empty}}, + nlohmann::json{{"code", 200}, {"body", std::move(devices_body)}}, + nlohmann::json{{"code", 200}, {"body", std::move(empty)}}}); + return response; +} + +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; }; + + 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] = std::byte{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]; + + CHECK(sent.request.endpoint == "batch"); + auto batch_json = nlohmann::json::parse(*sent.request.body); + auto& reqs = batch_json["requests"]; + REQUIRE(reqs.size() == 3); + // Subrequest 0: Default (ns 0) — requires auth. + CHECK(reqs[0]["method"] == "retrieve"); + 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 any subrequest. + CHECK_FALSE(params.contains("last_hash")); + // 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; + { + 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 outer_msg = linker->devices.build_link_request().message; + + 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 in the Devices subrequest. + mock_net->sent_requests.clear(); + TestHelper::poll(*core); + + REQUIRE(mock_net->sent_requests.size() == 1); + auto batch_json2 = nlohmann::json::parse(*mock_net->sent_requests[0].request.body); + CHECK(batch_json2["requests"][1]["params"]["last_hash"] == "hash1"); +} + +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] = std::byte{0xAA}; + node_b.remote_pubkey[0] = std::byte{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 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")); + } + // Respond with hash "xyz" from node A. + mock_net->sent_requests[0].callback( + 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()); + + // ── 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 p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + CHECK(p["last_hash"] == "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 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")); + } + // 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, {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"); + + // ── 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 p = nlohmann::json::parse( + *mock_net->sent_requests[0].request.body)["requests"][1]["params"]; + CHECK(p["last_hash"] == "xyz"); + } +} diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 4a4f7240..dc266909 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -74,21 +74,21 @@ 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); { - 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,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, @@ -153,9 +149,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 +214,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 +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(); @@ -756,9 +752,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); @@ -890,16 +886,16 @@ 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 = - "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"_hexbytes; + "fc947730f49eb01427a66e050733294d9e520e545c7a27125a780634e0860a27"_hex_b; // Setup CURL curl_global_init(CURL_GLOBAL_DEFAULT); @@ -917,12 +913,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 +942,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 +1156,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 +1186,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 +1278,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_proto.cpp b/tests/test_proto.cpp index f79706fb..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; -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 1ff6236e..0bc913b2 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,8 +1,10 @@ #include -#include #include #include +#include +#include +#include #include #include @@ -12,51 +14,47 @@ 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()) == + 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.begin(), curve_pk.end()) == + 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 = @@ -67,22 +65,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).data(), 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)); } } @@ -90,43 +84,39 @@ 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()) == + 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.begin(), curve_pk.end()) == + 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()); @@ -136,13 +126,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; @@ -152,86 +142,58 @@ TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][e using namespace session; - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; - const auto server_pk = - "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_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()) == + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex_b; + constexpr auto server_pk = + "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.begin(), curve_pk.end()) == + 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), to_span(server_pk)); - auto [blind25_pk, blind25_sk] = blind25_key_pair(to_span(ed_sk), to_span(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), to_span(server_pk)); - auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_span(ed_sk2), to_span(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), - to_span(server_pk), - {blind15_pk2_prefixed.data(), 33}, - to_span("hello")); + ed_sk, 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}, - 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), - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, - broken)); + ed_sk2, server_pk, blind15_pk_prefixed, blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, sender decrypt") { constexpr auto lorem_ipsum = @@ -242,32 +204,28 @@ 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(server_pk), - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk), + 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()); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk).data(), 32}, - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk), + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, enc); CHECK(sender == sid); 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).data(), 32}, - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk), + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, broken)); } SECTION("blind15, only seed, recipient decrypt") { @@ -279,111 +237,59 @@ 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(server_pk), - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk), + 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()); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk2), + server_pk, + blind15_pk_prefixed, + blind15_pk2_prefixed, enc); CHECK(sender == sid); 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).data(), 32}, - to_span(server_pk), - {blind15_pk_prefixed.data(), 33}, - {blind15_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk2), + 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}, - to_span("hello")); + ed_sk, 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}, - 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), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, - 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), - to_span(server_pk), - {blind25_pk2_prefixed.data(), 33}, - to_span("hello")); + ed_sk, 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}, - 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), - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, - broken)); + ed_sk2, server_pk, blind25_pk_prefixed, blind25_pk2_prefixed, broken)); } SECTION("blind25, only seed, recipient decrypt") { constexpr auto lorem_ipsum = @@ -394,32 +300,28 @@ 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(server_pk), - {blind25_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk), + 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()); + CHECK_FALSE(std::ranges::search(enc, "dolore magna"_bytes)); auto [msg, sender] = decrypt_from_blinded_recipient( - {to_span(ed_sk2).data(), 32}, - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk2), + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, enc); CHECK(sender == sid); 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).data(), 32}, - to_span(server_pk), - {blind25_pk_prefixed.data(), 33}, - {blind25_pk2_prefixed.data(), 33}, + ed25519::extract_seed(ed_sk2), + server_pk, + blind25_pk_prefixed, + blind25_pk2_prefixed, broken)); } } @@ -430,17 +332,16 @@ 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; - auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + "dbd4bc89bd2c9e5322fd9f4cadcaa66a0c38f15d0c927a86cc36e895fe1f3c532a3958d972563f52ca858e94eec22dc360"_hex_b; + constexpr auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hex_b; 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]") { @@ -449,10 +350,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( @@ -470,30 +371,123 @@ TEST_CASE("Session push notification decryption", "[session-notification][decryp auto payload = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + "ab4aca5f0991e99eb0344ceeafa"_hex_b; auto payload_padded = "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" - "ab4aca5f0991e99eb0344ceeafa"_hexbytes; - auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + "ab4aca5f0991e99eb0344ceeafa"_hex_b; + constexpr auto enc_key = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; 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]") { using namespace session; auto payload = - "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hexbytes; - auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + "da74ac6e96afda1c5a07d5bde1b8b1e1c05be73cb3c84112f31f00369d67154d00ff029090b069b48c3cf603d838d4ef623d54"_hex_b; + constexpr auto enc_key = + "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hex_b; 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]") { + using namespace session; + + // Sender: existing well-known test keypair 1 + 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"_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); + + 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_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"_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( + 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] == std::byte{0x05}); + CHECK(!result.pro_signature); + + // The recovered sender session ID matches the sender's X25519 pubkey + 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) + 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); + 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 + auto [pro_pk, pro_sk] = ed25519::keypair(); + auto ct_pro = encrypt_for_recipient_v2( + sender_ed_sk, + recip_session_id, + pfs_x25519_pub, + pfs_mlkem_pub, + to_span("hello world"), + 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()); + // The signature should be 64 bytes and verifiable with the pro public key. + CHECK(result_pro.pro_signature->size() == 64); } diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 8b154c5f..70600154 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; - array_uc64 sig_over_plaintext_with_user_pro_key; - array_uc64 sig_over_plaintext_padded_with_user_pro_key; - array_uc32 pro_proof_hash; - bytes64 sig_over_plaintext_with_user_pro_key_c; - bytes32 pro_proof_hash_c; + 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 array_uc64& user_rotating_privkey, - const array_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, - reinterpret_cast(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,22 +163,19 @@ 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; - array_uc32 user_pro_ed_pk; - array_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 // 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 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 +191,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(), @@ -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 array_uc64& pro_backend_ed_sk = keys.ed_sk1; - const array_uc32& pro_backend_ed_pk = keys.ed_pk1; char error[256]; SECTION("Encrypt/decrypt for contact in default namespace w/o pro attached") { @@ -250,9 +231,9 @@ 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_for_1o1( + encrypt_result = session_protocol_encode_dm_v1( plaintext.data(), plaintext.size(), keys.ed_sk0.data(), @@ -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,8 +267,8 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Verify pro ProProof nil_proof = {}; - array_uc32 nil_hash = nil_proof.hash(); - bytes32 decrypt_result_pro_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 == SESSION_PROTOCOL_PRO_STATUS_NIL); // Pro was not attached @@ -314,54 +295,44 @@ 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*/ {}, /*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)); - 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(); + 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()); 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, 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()); + cbytes32 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 +340,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, @@ -395,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); @@ -406,7 +395,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 @@ -439,20 +428,20 @@ 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, /*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, @@ -469,20 +458,20 @@ 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); 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 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( @@ -503,19 +492,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); + cbytes32 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); @@ -525,28 +516,30 @@ 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; - array_uc64 group_v2_sk = {}; - array_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 = {}; { - 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( - group_v2_session_sk.data, 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(), 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(), @@ -559,9 +552,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()), 32}; 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; @@ -571,8 +564,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); @@ -591,13 +584,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, @@ -614,8 +607,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); @@ -624,7 +617,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); @@ -654,7 +647,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( @@ -664,13 +657,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, @@ -681,8 +674,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); @@ -693,14 +686,14 @@ 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; - 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); @@ -720,8 +713,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( @@ -742,8 +735,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); @@ -769,8 +762,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); }}; @@ -792,8 +785,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); }}; @@ -813,8 +806,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); }}; @@ -837,8 +830,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); }}; @@ -849,32 +842,29 @@ 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 = {}; - crypto_sign_ed25519_seed_keypair( - community_pk.data(), community_sk.data(), community_seed.data()); - - bytes32 session_blind15_sk0 = {}; - bytes33 session_blind15_pk0 = {}; + "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); - 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(), - community_pk.data(), + to_unsigned(community_pk.data()), 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 = @@ -895,16 +885,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, sizeof(session_blind15_pk0.data)}, - {session_blind15_pk1.data, sizeof(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_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 143ba82b..d54859eb 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -1,184 +1,172 @@ #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(session::to_span(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(session::to_span(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()); - - 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()); + 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_sig1 = xed25519::sign(xsk1, msg); - rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); - REQUIRE(rc == 0); + REQUIRE(ed25519::verify(xed_sig1, pub1, msg)); - 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(session::to_span(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(session::to_span(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()); + 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 +} - 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()); +TEST_CASE("XEd25519 std::byte overloads", "[xed25519][byte]") { + auto xsk1 = ed25519::sk_to_x25519(ed25519::PrivKeySpan{seed1}); - const auto msg = session::to_span("hello world"); + const auto msg = "hello world"_bytes; - 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())); + // sign() byte overload should return a std::byte array. + auto sig_b = xed25519::sign(std::span{xsk1}, msg); + static_assert(std::same_as>); - rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); - REQUIRE(rc == 0); + // The signature must verify via the ed25519 helper. + REQUIRE(ed25519::verify(sig_b, pub1, msg)); - rc = crypto_sign_ed25519_verify_detached(xed_sig2.data(), msg.data(), msg.size(), pub2.data()); - REQUIRE(rc != 0); // Failure expected (pub2 is negative) + // verify() byte overload. + REQUIRE(xed25519::verify(sig_b, xpub1, msg)); - rc = crypto_sign_ed25519_verify_detached( - xed_sig2.data(), msg.data(), msg.size(), pub2_abs.data()); - REQUIRE(rc == 0); // Flipped sign should work + // pubkey() byte overload should return a std::byte array. + auto ed_pk_b = xed25519::pubkey(xpub1); + static_assert(std::same_as>); + 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())); +TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][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(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 047a3ba7..c2c109a1 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -5,20 +5,40 @@ #include #include +#include #include +#include #include #include #include #include #include +#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; +using namespace session; namespace session { @@ -129,17 +149,8 @@ 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; @@ -171,16 +182,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)); } @@ -191,7 +206,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 @@ -205,17 +220,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() { @@ -225,7 +240,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 @@ -243,7 +258,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 @@ -279,3 +294,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__) 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()