diff --git a/.gitmodules b/.gitmodules index 5461d327..dce6425b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "external/simdutf"] path = external/simdutf url = https://github.com/simdutf/simdutf.git +[submodule "external/sqlcipher"] + path = external/sqlcipher + url = https://github.com/sqlcipher/sqlcipher diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ee0e551..df3efe57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,9 @@ option(USE_LTO "Use Link-Time Optimization" ${use_lto_default}) # Provide this as an option for now because GMP and Desktop are sometimes unhappy with each other. option(ENABLE_ONIONREQ "Build with onion request functionality" ON) +# Provide this as an option for now so clients that don't use any database logic can exclude it. +option(ENABLE_DATABASE "Build with database functionality" ON) + if(USE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error) @@ -136,10 +139,6 @@ endif() add_subdirectory(src) add_subdirectory(proto) -if (BUILD_STATIC_DEPS) - include(StaticBuild) -endif() - if(STATIC_BUNDLE) include(combine_archives) diff --git a/README.md b/README.md index ae491063..c4e09cb2 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,47 @@ ## Build ``` +# Pre-requisites +apt install cmake build-essential git libssl-dev m4 pkg-config ninja-build + # Configure the build # # Options -# Enable APIs for creating onion-requests with: +# - Build libsession with its dependencies statically linked into the library (default: ON) +# +# -D BUILD_STATIC_DEPS=ON +# +# This currently influences top-level dependencies and forces OpenSSL on Linux/Windows to be +# statically linked into SQLCipher for database support. +# +# - Enable APIs for creating onion-requests with (default: ON) +# +# -D ENABLE_ONIONREQ=ON +# +# - Enable SQLCipher database support (default: ON) # -# -D ENABLE_ONIONERQ +# -D ENABLE_DATABASE=ON # -# Enable testing of a Session Pro Backend by defining on the configure line: +# - Enable testing of a Session Pro Backend by defining on the configure line (default: OFF) # -# -D TEST_PRO_BACKEND_WITH_DEV_SERVER=1 +# -D TEST_PRO_BACKEND_WITH_DEV_SERVER=ON # -# These tests require the Session Pro Backend running in development mode (SESH_PRO_BACKEND_DEV=1) -# to be running and tests the request and response flow of registering, updating and revoking -# Session Pro from the development backend. You must also have a libcurl available such that -# `find_package(CURL)` succeeds (e.g. a system installed libcurl) for this to compile -# successfully. +# These tests require the Session Pro Backend running in development mode +# (SESH_PRO_BACKEND_DEV=1) to be running and tests the request and response flow of registering, +# updating and revoking Session Pro from the development backend. You must also have a libcurl +# available such that `find_package(CURL)` succeeds (e.g. a system installed libcurl) for this +# to compile successfully. # -# By default, it contacts http://127.0.0.1:5000 but this URL can be changed using the CLI arg -# --pro-backend-dev-server-url="" when invoking the test suite. +# These tests do not run by default, they can be invoked by passing the dev server URL in the +# CLI arg --pro-backend-dev-server-url="" when invoking the test suite. # cmake -G Ninja -S . -B Build -# Regenerate protobuf files -cmake --build Build --target regen-protobuf --parallel --verbose - # Build cmake --build Build --parallel --verbose + +# Regenerate protobuf files +cmake --build Build --target regen-protobuf --parallel --verbose ``` ## Docs 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/external/CMakeLists.txt b/external/CMakeLists.txt index 25f42b8e..b5906fc3 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,3 +1,5 @@ +option(BUILD_STATIC_DEPS "Link top-level libsession dependencies statically into the generated library" ON) + option(SUBMODULE_CHECK "Enables checking that vendored library submodules are up to date" ON) if(SUBMODULE_CHECK) find_package(Git) @@ -62,44 +64,6 @@ macro(libsession_system_or_submodule BIGNAME smallname pkgconf subdir) 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() - - 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() -endif() - set(LIBQUIC_BUILD_TESTS OFF CACHE BOOL "") if(ENABLE_ONIONREQ) libsession_system_or_submodule(OXENQUIC quic liboxenquic>=1.3.0 oxen-libquic) @@ -126,25 +90,6 @@ if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL AppleClang AND NOT CMAKE_CXX_COMPILE oxen_logging_add_source_dir("${CMAKE_CURRENT_SOURCE_DIR}/oxen-libquic/external/oxen-logging/include/oxen") endif() -if(CMAKE_C_COMPILER_LAUNCHER) - set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") -endif() -set(deps_CFLAGS "-O2") - -if(IPO_ENABLED) - set(deps_CFLAGS "${deps_CFLAGS} -flto") -endif() - -if(APPLE) - foreach(lang C CXX) - string(APPEND deps_${lang}FLAGS " ${CMAKE_${lang}_SYSROOT_FLAG} ${CMAKE_OSX_SYSROOT} ${CMAKE_${lang}_OSX_DEPLOYMENT_TARGET_FLAG}${CMAKE_OSX_DEPLOYMENT_TARGET}") - foreach(arch ${CMAKE_OSX_ARCHITECTURES}) - string(APPEND deps_${lang}FLAGS " -arch ${arch}") - endforeach() - endforeach() -endif() - - function(libsodium_internal_subdir) set(BUILD_SHARED_LIBS OFF) add_subdirectory(libsodium-internal) @@ -152,7 +97,6 @@ endfunction() libsodium_internal_subdir() 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) @@ -168,7 +112,6 @@ if(TARGET PkgConfig::PROTOBUF_LITE AND NOT TARGET protobuf::libprotobuf-lite) add_library(protobuf::libprotobuf-lite ALIAS PkgConfig::PROTOBUF_LITE) endif() - set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "") set(ZSTD_BUILD_TESTS OFF CACHE BOOL "") set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "") @@ -189,7 +132,6 @@ export( ) 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>=3.7.0 nlohmann-json) @@ -205,3 +147,8 @@ function(simdutf_subdir) endfunction() simdutf_subdir() libsession_static_bundle(simdutf) + +if(ENABLE_DATABASE) + libsession_system_or_submodule(OPENSSL openssl openssl>=3.5.4 openssl-cmake) + libsession_system_or_submodule(SQLCIPHER sqlcipher sqlcipher>=4.12.0 sqlcipher-cmake) +endif() diff --git a/external/openssl-cmake/CMakeLists.txt b/external/openssl-cmake/CMakeLists.txt new file mode 100644 index 00000000..95f12070 --- /dev/null +++ b/external/openssl-cmake/CMakeLists.txt @@ -0,0 +1,204 @@ +# NOTE: Apple does not build OpenSSL which we use for SQLCipher as SQLCipher compiles natively +# against Apple's Common Crypto +if (APPLE) + return() +endif() + +set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") + +set(desired_version 3.5.4) +set(OPENSSL_VERSION ${desired_version} CACHE STRING "openssl version") + +# NOTE: Workaround issue on CI where OpenSSL version seems to get blanked out on +# Debian 12 for some reason. Those machines have 3.0.18 found via pkgconfig +# which is insufficient and I suspect something there is blanking out the +# version causing this to fail that I can't figure out how to fix. +if(NOT OPENSSL_VERSION OR OPENSSL_VERSION STREQUAL "") + set(OPENSSL_VERSION "${desired_version}" CACHE STRING "openssl version" FORCE) +endif() + +set(OPENSSL_MIRROR ${LOCAL_MIRROR} https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION} CACHE STRING "openssl download mirror(s)") +set(OPENSSL_SOURCE openssl-${OPENSSL_VERSION}.tar.gz) +set(OPENSSL_HASH SHA256=967311f84955316969bdb1d8d4b983718ef42338639c621ec4c34fddef355e99 + CACHE STRING "openssl source hash") + +set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) +set(DEPS_SOURCEDIR ${CMAKE_BINARY_DIR}/static-deps-sources) +include_directories(BEFORE SYSTEM ${DEPS_DESTDIR}/include) +file(MAKE_DIRECTORY ${DEPS_DESTDIR}/include) + +set(deps_cc "${CMAKE_C_COMPILER}") +set(deps_cxx "${CMAKE_CXX_COMPILER}") +if (ANDROID) + if(NOT ANDROID_TOOLCHAIN_NAME) + message(FATAL_ERROR "ANDROID_TOOLCHAIN_NAME not set; did you run with the proper android toolchain options?") + endif() + if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) + set(android_clang x86_64-linux-android${ANDROID_PLATFORM_LEVEL}-clang) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86) + set(android_clang i686-linux-android${ANDROID_PLATFORM_LEVEL}-clang) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a) + set(android_clang armv7a-linux-androideabi${ANDROID_PLATFORM_LEVEL}-clang) + elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a) + set(android_clang aarch64-linux-android${ANDROID_PLATFORM_LEVEL}-clang) + else() + message(FATAL_ERROR "Don't know how to build for android arch abi ${CMAKE_ANDROID_ARCH_ABI}") + endif() + set(deps_cc "${ANDROID_TOOLCHAIN_ROOT}/bin/${android_clang}") + set(deps_cxx "${deps_cc}++") +endif() + +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() + +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}) + set_target_properties(${target} PROPERTIES + IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname} + ) + if(ARGN) + target_link_libraries(${target} INTERFACE ${ARGN}) + endif() +endfunction() + +if(USE_LTO) + set(flto "-flto") +else() + set(flto "") +endif() + +set(cross_host "") +set(cross_extra "") +if (ANDROID) + set(cross_host "--host=${CMAKE_LIBRARY_ARCHITECTURE}") + set(cross_extra "LD=${ANDROID_TOOLCHAIN_ROOT}/bin/${CMAKE_LIBRARY_ARCHITECTURE}-ld" "RANLIB=${CMAKE_RANLIB}" "AR=${CMAKE_AR}") +elseif(CMAKE_CROSSCOMPILING) + set(cross_host "--host=${ARCH_TRIPLET}") + if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER) + set(cross_extra "WINDRES=${CMAKE_RC_COMPILER}") + endif() +endif() + +set(deps_CFLAGS "-O2 ${flto}") +set(deps_CXXFLAGS "-O2 ${flto}") +set(deps_noarch_CFLAGS "${deps_CFLAGS}") +set(deps_noarch_CXXFLAGS "${deps_CXXFLAGS}") + +# 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 ${sane_cross_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic + "CC=${deps_cc}" "CXX=${deps_cxx}" "CFLAGS=${deps_CFLAGS}" "CXXFLAGS=${deps_CXXFLAGS}" ${cross_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) +set(build_dep_TARGET_SUFFIX "") + +function(build_external target) + set(options TARGET_SUFFIX DEPENDS PATCH_COMMAND CONFIGURE_COMMAND 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}") + + set(externalproject_extra) + if(NOT CMAKE_VERSION VERSION_LESS 3.24) + # Default in cmake 3.24+ is to not extract timestamps for ExternalProject, which breaks pretty + # much every autotools package (which thinks it must reconfigure) because timestamps got + # updated). + list(APPEND externalproject_extra DOWNLOAD_EXTRACT_TIMESTAMP ON) + endif() + + string(TOUPPER "${target}" prefix) + expand_urls(urls ${${prefix}_SOURCE} ${${prefix}_MIRROR}) + ExternalProject_Add("${target}${arg_TARGET_SUFFIX}_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} + BUILD_COMMAND ${arg_BUILD_COMMAND} + INSTALL_COMMAND ${arg_INSTALL_COMMAND} + BUILD_BYPRODUCTS ${arg_BUILD_BYPRODUCTS} + ${externalproject_extra} + ) +endfunction() + +if(CMAKE_CROSSCOMPILING) + if(ARCH_TRIPLET STREQUAL x86_64-w64-mingw32) + set(openssl_extra_opts mingw64) + set(openssl_extra_env RC=${CMAKE_RC_COMPILER}) + elseif(ARCH_TRIPLET STREQUAL i686-w64-mingw32) + set(openssl_extra_opts mingw) + set(openssl_extra_env RC=${CMAKE_RC_COMPILER}) + elseif(ANDROID) + set(openssl_extra_opts no-asm) + set(openssl_extra_env SYSTEM=Linux) + set(deps_CFLAGS "${deps_CFLAGS} --sysroot=${ANDROID_TOOLCHAIN_ROOT}/sysroot") + if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64 OR CMAKE_ANDROID_ARCH_ABI MATCHES x86) + # NOTE: Sysroot isn't sufficient to find the asm/ folder sitting in the host-tagged folder + # /usr/lib/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/linux/types.h:9:10: fatal error: 'asm/types.h' file not found + # At + # /usr/lib/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android/asm/ + if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64) + set(deps_CFLAGS "${deps_CFLAGS} -I${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr/include/x86_64-linux-android") + else() + set(deps_CFLAGS "${deps_CFLAGS} -I${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr/include/i686-linux-android") + endif() + endif() + endif() +endif() +build_external(openssl + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env + CC=${deps_cc} + CFLAGS=${deps_CFLAGS} + ${openssl_extra_env} + ${cross_extra} + ./Configure --prefix=${DEPS_DESTDIR} --libdir=lib ${openssl_extra_opts} + no-shared no-capieng no-dso no-dtls1 no-ec_nistp_64_gcc_128 no-gost + no-heartbeats no-md2 no-rc5 no-rdrand no-rfc3779 no-sctp no-ssl-trace no-ssl2 no-ssl3 + no-static-engine no-tests no-weak-ssl-ciphers no-zlib no-zlib-dynamic + BUILD_COMMAND make build_libs + INSTALL_COMMAND make install_dev + BUILD_BYPRODUCTS + ${DEPS_DESTDIR}/lib/libssl.a ${DEPS_DESTDIR}/lib/libcrypto.a + ${DEPS_DESTDIR}/include/openssl/ssl.h ${DEPS_DESTDIR}/include/openssl/crypto.h +) +add_static_target(OpenSSL::SSL openssl_external libssl.a) +add_static_target(OpenSSL::Crypto openssl_external libcrypto.a) +target_link_libraries(OpenSSL::SSL INTERFACE OpenSSL::Crypto) +set(OPENSSL_ROOT_DIR ${DEPS_DESTDIR} CACHE PATH "" FORCE) + +# NOTE: FindOpenSSL defines OPENSSL_INCLUDE_DIR, we use pkg_check_modules which defines +# OPENSSL_INCLUDE_DIRS, we define both for maximum compatibility... +set(OPENSSL_INCLUDE_DIRS ${DEPS_DESTDIR}/include CACHE PATH "" FORCE) +set(OPENSSL_INCLUDE_DIR ${DEPS_DESTDIR}/include CACHE PATH "" FORCE) + +# NOTE: Create target so then libsession_system_or_submodule exports the +# :: target so that we can universally use that target for both PkgConfig +# and non-pkconfig versions of OpenSSL. +add_library(openssl INTERFACE) +target_link_libraries(openssl INTERFACE OpenSSL::SSL OpenSSL::Crypto) +target_include_directories(openssl INTERFACE ${OPENSSL_INCLUDE_DIRS}) diff --git a/external/sqlcipher b/external/sqlcipher new file mode 160000 index 00000000..d41a25f4 --- /dev/null +++ b/external/sqlcipher @@ -0,0 +1 @@ +Subproject commit d41a25f448ba08ce24c0a599cf322046bdaa135a diff --git a/external/sqlcipher-cmake/CMakeLists.txt b/external/sqlcipher-cmake/CMakeLists.txt new file mode 100644 index 00000000..a2539ef2 --- /dev/null +++ b/external/sqlcipher-cmake/CMakeLists.txt @@ -0,0 +1,177 @@ +cmake_minimum_required(VERSION 3.14) + +project(sqlcipher-cmake LANGUAGES C) +include(ExternalProject) + +set(SQLCIPHER_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/install) +set(SQLCIPHER_LIBDIR ${SQLCIPHER_PREFIX}/lib) +set(SQLCIPHER_INCLUDEDIR ${SQLCIPHER_PREFIX}/include) +file(MAKE_DIRECTORY ${SQLCIPHER_INCLUDEDIR}) + +# NOTE: Detect if openssl is being built locally for libsession, if so this build step depends on +# that first before proceeding +set(sqlcipher_depends) +if (NOT APPLE AND TARGET openssl_external) + set(sqlcipher_depends openssl_external) +endif() + +set(sqlcipher_cflags "${sqlcipher_cflags} -O2") +if(USE_LTO) + string(APPEND sqlcipher_cflags "${sqlcipher_cflags} -flto") +endif() + +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() + +set(deps_cc ${CMAKE_C_COMPILER}) +if(CMAKE_C_COMPILER_LAUNCHER) + set(deps_cc "${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}") +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") +endif() + +set(deps_CFLAGS "-O2") +set(deps_CXXFLAGS "-O2") + +set(apple_cflags_arch) +set(apple_cxxflags_arch) +set(apple_ldflags_arch) +if(APPLE AND CMAKE_CROSSCOMPILING) + if(cross_host MATCHES "^(.*-.*-)ios([0-9.]+)(-.*)?$") + set(cross_host "${CMAKE_MATCH_1}darwin${CMAKE_MATCH_2}${CMAKE_MATCH_3}") + endif() + if(cross_host MATCHES "^(.*-.*-.*)-simulator$") + set(cross_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(cross_host STREQUAL "" AND CMAKE_LIBRARY_ARCHITECTURE) + set(cross_host "--build=${CMAKE_LIBRARY_ARCHITECTURE}") +endif() + +set(sqlcipher_ldflags "") +set(sqlcipher_cflags "${sqlcipher_cflags} -DSQLITE_HAS_CODEC -DSQLITE_EXTRA_INIT=sqlcipher_extra_init -DSQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown") + +if(APPLE) + set(sqlcipher_cflags "${sqlcipher_cflags} -DSQLCIPHER_CRYPTO_CC") + set(sqlcipher_ldflags "${sqlcipher_ldflags} -framework Security -framework Foundation -framework CoreFoundation") +else() + if(WIN32) + # TODO: I'm unable to stop SQLCipher from building sqlite3.exe in the mingw cross-compile + # so we need to augment that executable with some Window's libraries to get us through... + set(sqlcipher_ldflags "${sqlcipher_ldflags} -lws2_32 -lcrypt32") + endif() + + if(ANDROID) + # NOTE: SQLCipher seems to have trouble finding liblog and libm which is in this folder here + # in the sysroot + set(sqlcipher_ldflags "${sqlcipher_ldflags} -L${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr/lib/${android_toolchain_prefix}-${android_toolchain_suffix}/${ANDROID_PLATFORM_LEVEL}") + + # NOTE: SQLite uses __android_log_print and __android_log_write which is in their liblog library + set(sqlcipher_ldflags "${sqlcipher_ldflags} -lm -llog") + endif() + + set(sqlcipher_cflags "${sqlcipher_cflags} -DSQLCIPHER_CRYPTO_OPENSSL -I${OPENSSL_INCLUDE_DIRS}") + if (TARGET openssl_external) + set(sqlcipher_ldflags "${sqlcipher_ldflags} -L${OPENSSL_ROOT_DIR}/lib -lcrypto -lssl") + else() + set(sqlcipher_ldflags "${sqlcipher_ldflags} -lcrypto -lssl") + endif() + +endif() + +ExternalProject_Add(sqlcipher_external + SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../sqlcipher + DEPENDS ${sqlcipher_depends} + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_OUTPUT_ON_FAILURE ON + CONFIGURE_COMMAND + ${CMAKE_CURRENT_LIST_DIR}/../sqlcipher/configure ${cross_host} --disable-shared + --enable-static --disable-tcl --disable-readline --fts5 --with-tempstore=always + --prefix=${SQLCIPHER_PREFIX} CC=${deps_cc} CFLAGS=${apple_cflags_arch}${sqlcipher_cflags} + LDFLAGS=${apple_ldflags_arch}${sqlcipher_ldflags} ${cross_rc} + BUILD_COMMAND make libsqlite3.a + INSTALL_COMMAND make install-lib install-headers + BUILD_BYPRODUCTS + ${SQLCIPHER_LIBDIR}/libsqlite3.a +) + +add_library(sqlcipher::sqlcipher STATIC IMPORTED GLOBAL) +add_dependencies(sqlcipher::sqlcipher sqlcipher_external) +set_target_properties(sqlcipher::sqlcipher PROPERTIES + IMPORTED_LOCATION ${SQLCIPHER_LIBDIR}/libsqlite3.a + INTERFACE_INCLUDE_DIRECTORIES ${SQLCIPHER_INCLUDEDIR} +) +target_compile_definitions(sqlcipher::sqlcipher INTERFACE SQLITE_HAS_CODEC) +if(APPLE) + target_link_libraries(sqlcipher::sqlcipher INTERFACE "-framework Security" "-framework Foundation" "-framework CoreFoundation") +else() + target_link_libraries(sqlcipher::sqlcipher INTERFACE openssl::openssl) + if (WIN32 OR MINGW) + target_link_libraries(sqlcipher::sqlcipher INTERFACE ws2_32 crypt32) + endif() +endif() diff --git a/include/session/core.h b/include/session/core.h new file mode 100644 index 00000000..017b4bf1 --- /dev/null +++ b/include/session/core.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +#include "export.h" +#include "types.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct session_core_core session_core_core; +struct session_core_core { + /// ~184 bytes on debian sid libstdc++ 3.4.33 + /// ~208 bytes on macos intel clang 16.0.0.16000026 + uint64_t opaque[26]; +}; + +/// API: core/session_core_core_init +/// +/// Pass in a zero-initialised session core object to initialise core for usage in the core layer +/// This object should be considered unique, it should not be copied and it must only be initialised +/// and deinitialised once. +/// +/// After initialisation you must call `session_database_connection_open` if you wish to use +/// functions requiring the database +LIBSESSION_EXPORT void session_core_core_init(session_core_core* core); + +/// API: core/session_core_core_deinit +/// +/// Shutdown a initialised core object. This function does a no-op if a NULL pointer is passed in +LIBSESSION_EXPORT void session_core_core_deinit(session_core_core* core); + +/// API: core/session_core_core_db_conn +/// +/// Get a DB handle from the core object if the DB has been opened before. If the DB has not been +/// opened, this function returns a nullptr. If libsession is built without DB support this will +/// also cause the function to return a nullptr. +/// +/// This pointer's lifetime is bound to the current instance of the DB associated with the Core. The +/// caller must take care not to deinitialise the connection independently from the Core as +/// ownership of the database is bound to `session_core_core_deinit`. +LIBSESSION_EXPORT session_database_connection* session_core_core_db_conn(session_core_core* core) + NON_NULL_ARG(1); + +/// API: core/session_core_core_open_db +/// +/// Create/open the SQLCipher DB at the specified `path`. Upon load the core in-memory runtime +/// state will be reset and populated with the contents of the DB. This closes the DB automatically +/// if the core previously opened it. +// +/// This function returns an error if there were issues opening the database. No-op if libsession +/// has been compiled without database support. +LIBSESSION_EXPORT session_c_result +session_core_core_open_db(session_core_core* core, string8 path, span_u8 raw_key); + +/// API: core/session_core_core_pro_proof_is_revoked +/// +/// Update the list of pro-revocations being managed by the core. This updates the in-memory +/// list as well as the copy stored in the database. If the `revocations_ticket` matches the +/// in-memory ticket, this is a no-op. +LIBSESSION_EXPORT bool session_core_core_pro_proof_is_revoked( + session_core_core* core, const bytes32* gen_index_hash, uint64_t unix_ts_ms) + NON_NULL_ARG(1, 2); + +/// API: core/session_core_core_pro_update_revocations +/// +/// Update the list of pro-revocations being managed by the core. This updates the in-memory list as +/// well as the copy stored in the database. If the `revocations_ticket` matches the in-memory +/// ticket, this is a no-op. +/// +/// If the handle returned by `session_core_core_db_conn` does not have an open connection then only +/// the in-memory revocation list is updated. +LIBSESSION_EXPORT session_c_result session_core_core_pro_update_revocations( + session_core_core* core, + uint32_t revocations_ticket, + session_pro_backend_pro_revocation_item* revocations, + size_t revocations_count) NON_NULL_ARG(1); + +#if defined(__cplusplus) +} +#endif diff --git a/include/session/core.hpp b/include/session/core.hpp new file mode 100644 index 00000000..4231ff21 --- /dev/null +++ b/include/session/core.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#if !defined(DISABLE_SQLCIPHER) +#include +#endif +#include +#include +#include +#include +#include + +/// The fundamental library context that an application should instantiate at the start of their +/// libsession integrated application. Its goal is to maintain libsession data structures for +/// communicating on the protocol at runtime but also persist it to disk if/where necessary to +/// maintain state across application restarts. +/// +/// A typical application will instantiate the Core context, open a DB connection at the desired +/// path where libsession will persist state. Periodically the integrating application will invoke +/// the Core context to feed it data that it will manage. In future, the Core context will be +/// runnable in a background thread for it to maintain itself and automatically subscribe to the +/// Session Pro Backend, the swarms of the Session Account it manages to send and receive messages +/// in a way that abstracts that book-keeping from the implementing application. +/// +/// Currently the integrating application must update the Core context when it receives the +/// appropriate data from the network and you can opt out of using a database by either, +/// +/// - Compiling without database support +/// - Not opening the database and/or ensuring the database is on the Core object is is closed +/// during use. +/// +/// The typical intended flow for using the Core is as follows: +/* +``` + #include + #include + + int main() { + session::core::Core core = {}; + + // Optionally create/open the DB to persist state to. If this step is skipped the core will only + // maintain libsession state (like the user's long term seed or the pro revocation list) in + // runtime memory and will be lost on shutdown. Persisting user state is then left to the + // integrating application's discretion. + try { + // Generate the encryption key for the DB (if you had a pre-existing DB this is where you + // would load the key to pass in). + session::cleared_array<48> db_enc_key = {}; + randombytes_buf(db_enc_key.data(), db_enc_key.size()); + + core.open_db(":memory:", db_enc_key); + } catch (const std::exception& e) { + // ... error handling + } + + // Update the revocation list stored in Core (if the DB was opened successfully, this will also + // persist the revocation list to the DB for example). + // + // In a production application you would sleep on an event loop responsible for dispatching and + // receiving the revocation list queries and call this function to update the revocation list + // that is cached and the DB + if (core.pro_update_revocations(...)) { ... } + + // Interfacing code calls this API to check if the specific proof in question is revoked or not + if (core.pro_proof_is_revoked(...)) { ... } + } +``` +*/ + +namespace session::pro_backend { +struct ProRevocationItem; +}; // namespace session::pro_backend + +namespace session::core { +struct ProRevocationItemComparer { + bool operator()( + const pro_backend::ProRevocationItem& lhs, + const pro_backend::ProRevocationItem& rhs) const noexcept; +}; + +struct Core { + /// List of Session Pro revocations that the core will reject proofs from + std::set revocations_; + + /// Version of the revocation list that is currently stored in this core context. It is received + /// from the Session Pro Backend when the revocation list is queried. + uint32_t revocations_ticket_; + + /// This class is intended to be use on an network event loop alongside the application which + /// calls into functions that lookup the cache. When the event loop updates the data stored in + /// the in-memory cache and database it requires an exclusive lock. When the application queries + /// the in-memory caches and database, concurrent reads are accepted if there are ongoing writes + mutable std::shared_mutex shared_mutex_; + +#if !defined(DISABLE_SQLCIPHER) + session::database::Connection db_conn_; +#endif + + /// API: core/Core::open_db + /// + /// Create/open the SQLCipher DB at the specified `path`. Upon load the core in-memory runtime + /// state will be reset and populated with the contents of the DB. This closes the DB + /// automatically if the core previously opened it. + // + /// This function throws if there was an error opening the database. No-op if libsession has + /// been compiled without database support. + void open_db(const std::string& path, const cleared_array<48>& raw_key); + + /// API: core/Core::pro_proof_is_revoked + /// + /// Check if the proof identified by its `gen_index_hash` is revoked with respect to the given + /// timestamp from the list of proofs stored in memory. If `gen_index_hash` does not exist this + /// function will always return `false`. + /// + /// Outputs: + /// - `bool` -- True if the proof was revoked, false otherwise. + bool pro_proof_is_revoked( + const array_uc32& gen_index_hash, + std::chrono::sys_time unix_ts) const; + + /// API: core/Core::pro_update_revocations + /// + /// Update the list of pro-revocations being managed by the core. This updates the in-memory + /// list as well as the copy stored in the database. If the `revocations_ticket` matches the + /// in-memory ticket, this is a no-op. + /// + /// If `db_conn` does not have an open connection then only the in-memory revocation list is + /// updated. + /// + /// Inputs: + /// - `revocations_ticket` -- Ticket that describes the version of the revocations. This value + /// comes alongside the revocation list when queried. If the ticket is the same as the ticket + /// stored in the core, this function no-ops (because the revocation list is the same in this + /// case). + /// - `revocations` -- List of Session Pro revocations to update in the core. Overwrites the + /// previous list stored in the core. + void pro_update_revocations( + uint32_t revocations_ticket, + std::span revocations); +}; +} // namespace session::core diff --git a/include/session/database/connection.h b/include/session/database/connection.h new file mode 100644 index 00000000..4a458545 --- /dev/null +++ b/include/session/database/connection.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +#include "../export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct session_pro_backend_pro_revocation_item; + +typedef struct session_database_connection session_database_connection; +struct session_database_connection { + uint64_t opaque; +}; + +typedef struct session_database_set_result session_database_set_result; +struct session_database_set_result { + session_c_result db; + int sql_return_code; + /// SQL's string-ified `sql_return_code` pointing to memory in the data segment. Should not be + /// modified and is valid for program lifetime. + const char* sql_error; +}; + +typedef struct session_database_get_pro_revocation_result + session_database_get_pro_revocation_result; +struct session_database_get_pro_revocation_result { + session_c_result db; + size_t count; +}; + +struct session_database_get_account { + bool found; + uint32_t db_id; + bytes64 long_term_privkey; +}; + +/// API: session_database_connection_open +/// +/// Open a connection to the DB specified at `path`. If this connection previously has an open +/// DB that connection is gracefully closed before opening up the newly requested one. +/// +/// This function returns an if the DB was not openable, if the `raw_key` was the incorrect key to +/// decrypt the DB or the contents of the DB were malformed. +/// +/// If the DB has never been initialised before, the DB is initialised with the required schema. +/// +/// Inputs: +/// - `conn` -- DB connection object that was zero-initialised or used previously +/// - `path` -- Path to the DB to open, this can be a URI or path on disk +/// - `raw_key` -- Encryption key to use to open the specified DB. If the DB does not exist then +/// the database will be created, encrypted with this key. +LIBSESSION_EXPORT session_c_result session_database_connection_open( + session_database_connection* conn, string8 path, span_u8 raw_key) NON_NULL_ARG(1); + +/// API: session_database_connection_close +/// +/// Close the DB connection which closes the underlying file descriptor referencing the database +/// +/// Inputs: +/// - `conn` -- DB connection object to close +LIBSESSION_EXPORT void session_database_connection_close(session_database_connection* conn); + +/// API: session_database_connection::get_account +/// +/// Get the Session account secrets stored in this database. If no account was initialised yet +/// then the output object's found flag is set to false. +/// +/// Outputs: +/// - `found` -- True if there was an account secret in the DB, false otherwise +/// - `db_id` -- Primary key of the row that the secret was retrieved from. 0 if `found` is +/// false +/// - `long_term_privkey` -- Session account's long term 64 byte libsodium-style private key. +/// This key is all 0s if `found` was false. +LIBSESSION_EXPORT session_database_get_account +session_database_connection_get_account(session_database_connection* conn); + +/// API: session_database_connection_set_account +/// +/// Sets the long-term 64 byte libsodium-style private key as the Session account's secret +/// associated with this database. This overwrites any pre-existing key, if any. +/// +/// This function errors if the key is incorrectly sized or if the DB insertion failed. +LIBSESSION_EXPORT session_c_result session_database_connection_set_account( + session_database_connection* conn, + void const* long_term_privkey, + size_t long_term_privkey_size); + +/// API: session_database_connection_set_pro_revocations +/// +/// Set the list of Session Pro revocations into the database associated with this connection +/// replacing the old revocations. This function is transactional, on failure changes to the +/// database are rolled back. +/// +/// Inputs: +/// - `ticket` -- Monotonic integer which is the version of the list, received by the Session +/// Pro Backend when syncing the revocation list. +/// - `revocations` -- The list of revocations to set +LIBSESSION_EXPORT session_database_set_result session_database_connection_set_pro_revocations( + session_database_connection* conn, + uint32_t ticket, + session_pro_backend_pro_revocation_item* revocations, + size_t revocations_len); + +/// API: database/get_pro_revocations_buffer +/// +/// Retrieve the Session Pro Backend revocation list given and output the rows into the given +/// `buf`. This function errors if SQLite returned an error +/// +/// Inputs: +/// - `buf` -- Buffer to write loaded revocations into. This can be nullptr in which case the +/// function returns the number of revocations currently in the DB. +/// - `buf_count` -- Size of the buffer and consequently the amount of revocations to load. This +/// can be 0 as well as setting `buf` to `nullptr` to make the function return the number of +/// revocations currently in the DB. +/// - `offset` -- Start retrieving revocation rows from this specified index of the list. Pass +/// in 0 to start from the beginning. +/// - `ticket` -- Retrieve the current ticket for the revocation list which represents the +/// current version of the list that has been synced from the Session Pro Backend. +/// +/// Outputs: +/// - `count` -- Number of revocation items read from the database. If the buffer was +/// insufficient sized to receive the rows, the return value is always capped to the size of +/// the buffer. If `buf` and `buf_count` are nullptr or 0 respectively, then value returned +/// is the amount of revocation items in the DB at the time of execution. +LIBSESSION_EXPORT session_database_get_pro_revocation_result +session_database_connection_get_pro_revocations_buffer( + session_database_connection* conn, + OPTIONAL session_pro_backend_pro_revocation_item* buf, + size_t buf_count, + size_t offset, + OPTIONAL uint32_t* ticket); +#ifdef __cplusplus +} +#endif diff --git a/include/session/database/connection.hpp b/include/session/database/connection.hpp new file mode 100644 index 00000000..b94b3fcd --- /dev/null +++ b/include/session/database/connection.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +/// Functions to interact with a SQLCipher database that maintains Libsession persistent state such +/// as the Session Pro revocation list. + +struct sqlite3; +struct sqlite3_stmt; + +namespace session::database { + +struct SetResult { + bool success; + int sql_return_code; + /// SQL's string-ified `sql_return_code` pointing to memory in the data segment. Should not be + /// modified and is valid for program lifetime. + const char* sql_error; +}; + +struct GetAccount { + bool found; + uint32_t db_id; + uc64 long_term_privkey; +}; + +/// The row from the runtime table which is the table housing global settings of the Session +/// database. There's only 1 row in the runtime table which gets extracted and filled out into this +/// struct. +struct Runtime { + int32_t id; + int32_t pro_revocations_ticket; +}; + +struct sqlite3_deleter { + void operator()(sqlite3* db) const noexcept; +}; + +struct Connection { + std::unique_ptr db_; + + /// API: database/Connection::open + /// + /// Open a connection to the DB specified at `path`. If this connection previously has an open + /// DB that connection is gracefully closed before opening up the newly requested one. If this + /// function fails to open the DB, the previous DB connection is untouched. + /// + /// This function throws an error if the DB was not openable, if the `raw_key` was the incorrect + /// key to decrypt the DB or the contents of the DB were malformed. + /// + /// If the DB has never been initialised before, the DB is initialised with the required schema. + /// + /// Inputs: + /// - `path` -- Path to the DB to open, this can be a URI or path on disk + /// - `raw_key` -- Encryption key to use to open the specified DB. If the DB does not exist then + /// the database will be created, encrypted with this key. + void open(const std::string& path, const cleared_array<48>& raw_key); + + /// API: database/Connection::close + /// + /// Close the database connection if it is open, no-op if the database is not open. This does + /// not need to be called unless you explicitly want to close the connection. On connection + /// destruction, the database closes itself. + void close(); + + /// API: database/Connection::exec + /// + /// Prepares a statement and executes it. Throws if SQLite returned an error + /// + /// Inputs: + /// - `sql` -- SQL statement to prepare and execute + void exec(const std::string& sql); + + /// API: database/Connection::query + /// + /// Prepares a statement, steps the statement, calls the provided `callback` and then finalizes + /// the statement once stepping no longer returns rows. Throws if SQLite returned an error + /// + /// Inputs: + /// - `sql` -- SQL statement to prepare + /// - `callback` -- User defined function to execute when a row is returned + void query(std::string_view sql, std::function callback); + + /// API: database/Connection::get_runtime + /// + /// Get the runtime row of the table which contains global metadata for the entire table. + /// There's only one runtime row per database. + /// + /// Outputs: + /// - `id` -- Row ID of the runtime row (essentially always 1 as there's only 1 runtime row) + /// - `pro_revocations_ticket` -- Current version of the pro revocations list that has been + /// synced from the Session Pro Backend. + Runtime get_runtime(); + + /// API: database/Connection::get_account + /// + /// Get the Session account secrets stored in this database. If no account was initialised yet + /// then the output object's found flag is set to false. + /// + /// Outputs: + /// - `found` -- True if there was an account secret in the DB, false otherwise + /// - `db_id` -- Primary key of the row that the secret was retrieved from. 0 if `found` is + /// false + /// - `long_term_privkey` -- Session account's long term 64 byte libsodium-style private key. + /// This key is all 0s if `found` was false. + GetAccount get_account(); + + /// API: database/Connection::set_account + /// + /// Sets the long-term 64 byte libsodium-style private key as the Session account's secret + /// associated with this database. This overwrites any pre-existing key, if any. + /// + /// This function throws if the key is incorrectly sized or if the DB insertion failed. + void set_account(std::span long_term_privkey); + + /// API: database/set_pro_revocations + /// + /// Set the list of Session Pro revocations into the database associated with this connection + /// replacing the old revocations. This function is transactional, on failure changes to the + /// database are rolled back. + /// + /// Inputs: + /// - `ticket` -- Monotonic integer which is the version of the list, received by the Session + /// Pro Backend when syncing the revocation list. + /// - `revocations` -- The list of revocations to set + /// + /// Outputs: + /// - `success` -- True if the add was successful, false otherwise. + /// - `return_code` -- The SQLite3 error code that caused the error if `success` was false + SetResult set_pro_revocations( + uint32_t ticket, std::span revocations) noexcept; + + /// API: database/Connection::get_pro_revocations_buffer + /// + /// Retrieve the Session Pro Backend revocation list given and output the rows into the given + /// `buf`. This function throws if SQLite returned an error + /// + /// Inputs: + /// - `buf` -- Buffer to write loaded revocations into. This can be nullptr in which case the + /// function returns the number of revocations currently in the DB. + /// - `buf_count` -- Size of the buffer and consequently the amount of revocations to load. This + /// can be 0 as well as setting `buf` to `nullptr` to make the function return the number of + /// revocations currently in the DB. + /// - `offset` -- Start retrieving revocation rows from this specified index of the list. Pass + /// in 0 to start from the beginning. + /// - `ticket` -- Retrieve the current ticket for the revocation list which represents the + /// current version of the list that has been synced from the Session Pro Backend. + /// + /// Outputs: + /// - `size_t` -- Number of revocation items read from the database. If the buffer was + /// insufficient sized to receive the rows, the return value is always capped to the size of + /// the buffer. If `buf` and `buf_count` are nullptr or 0 respectively, then value returned + /// is the amount of revocation items in the DB at the time of execution. + size_t get_pro_revocations_buffer( + OPTIONAL pro_backend::ProRevocationItem* buf, + size_t buf_count, + size_t offset, + OPTIONAL uint32_t* ticket); + + /// API: database/Connection::get_pro_revocations + /// + /// Retrieve the Session Pro Backend revocation list from the database. This function throws if + /// there was an allocation or SQLite returned an error + /// + /// Inputs: + /// - `ticket` -- Retrieve the current ticket for the revocation list which represents the + /// current version of the list that has been synced from the Session Pro Backend. + /// + /// Outputs: + /// - `std::vector` -- List of revocation items + std::vector get_pro_revocations(OPTIONAL uint32_t* ticket); +}; +} // namespace session::database diff --git a/include/session/pro_backend.hpp b/include/session/pro_backend.hpp index 6222d2a7..07aeb7b8 100644 --- a/include/session/pro_backend.hpp +++ b/include/session/pro_backend.hpp @@ -668,4 +668,7 @@ struct SetPaymentRefundRequestedResponse : public ResponseHeader { /// `errors` static SetPaymentRefundRequestedResponse parse(std::string_view json); }; +session_pro_backend_pro_revocation_item revocation_c_from_cpp(ProRevocationItem const& src); + +ProRevocationItem revocation_cpp_from_c(session_pro_backend_pro_revocation_item const& src); } // namespace session::pro_backend diff --git a/include/session/session_protocol.h b/include/session/session_protocol.h index 34efb16f..8e1061ac 100644 --- a/include/session/session_protocol.h +++ b/include/session/session_protocol.h @@ -139,8 +139,16 @@ typedef enum SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS { // See session::Pro typedef enum SESSION_PROTOCOL_DESTINATION_TYPE { // See session::DestinationType SESSION_PROTOCOL_DESTINATION_TYPE_SYNC_OR_1O1, SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, + + // Old-style community messages that is content encrypted using the blinding protocol or + // plaintext content respectively (inbox vs non-inbox). SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, + + // New-style community messages that are sent as envelopes, encrypted using the blinding + // protocol or plaintext content respectively (inbox vs non-inbox). + SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX, + SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY, } SESSION_PROTOCOL_DESTINATION_TYPE; typedef struct session_protocol_destination session_protocol_destination; @@ -709,6 +717,32 @@ session_protocol_encoded_for_destination session_protocol_encode_for_destination LIBSESSION_EXPORT void session_protocol_encode_for_destination_free( session_protocol_encoded_for_destination* encrypt); +/// API: session_protocol/decode_for_community_inbox +/// +/// Given a blinded encrypted content or envelope payload extract the content and any associated pro +/// metadata if there was any in the message. +/// +/// This function is the same as calling `decrypt_from_blinded_recipient` to decrypt the payload and +/// then decode with `decode_for_community`. See those functions for more information. On failure +/// this function throws as per `decrypt_from_blinded_recipient` +LIBSESSION_EXPORT session_protocol_decoded_community_message +session_protocol_decode_for_community_inbox( + const unsigned char* ed25519_privkey, + size_t ed25519_privkey_len, + const unsigned char* community_pubkey, + size_t community_pubkey_len, + const unsigned char* sender_id, + size_t sender_id_len, + const unsigned char* recipient_id, + size_t recipient_id_len, + const unsigned char* ciphertext, + size_t ciphertext_len, + uint64_t unix_ts_ms, + OPTIONAL const void* pro_backend_pubkey, + OPTIONAL size_t pro_backend_pubkey_len, + OPTIONAL char* error, + size_t error_len); + /// API: session_protocol/session_protocol_decode_envelope /// /// Given an envelope payload (i.e.: protobuf encoded stream of `WebsocketRequestMessage` which diff --git a/include/session/session_protocol.hpp b/include/session/session_protocol.hpp index 2ef515d5..bd5dcc18 100644 --- a/include/session/session_protocol.hpp +++ b/include/session/session_protocol.hpp @@ -213,6 +213,8 @@ enum class DestinationType { Group = SESSION_PROTOCOL_DESTINATION_TYPE_GROUP, CommunityInbox = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY_INBOX, Community = SESSION_PROTOCOL_DESTINATION_TYPE_COMMUNITY, + EnvelopeCommunityInbox = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX, + EnvelopeCommunity = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY, }; struct Destination { @@ -606,6 +608,23 @@ DecodedEnvelope decode_envelope( std::span envelope_payload, const array_uc32& pro_backend_pubkey); +/// API: session_protocol/decode_for_community_inbox +/// +/// Given a blinded encrypted content or envelope payload extract the plaintext to the content and +/// any associated pro metadata if there was any in the message. +/// +/// This function is the same as calling `decrypt_from_blinded_recipient` to decrypt the payload and +/// then decode with `decode_for_community`. See those functions for more information. On failure +/// this function throws as per `decrypt_from_blinded_recipient` +DecodedCommunityMessage decode_for_community_inbox( + std::span ed25519_privkey, + std::span community_pubkey, + std::span sender_id, + std::span recipient_id, + std::span ciphertext, + std::chrono::sys_time unix_ts, + const array_uc32& pro_backend_pubkey); + /// API: session_protocol/decode_for_community /// /// Given an unencrypted content or envelope payload extract the plaintext to the content and any diff --git a/include/session/types.h b/include/session/types.h index 5029cab3..59042b98 100644 --- a/include/session/types.h +++ b/include/session/types.h @@ -27,6 +27,14 @@ struct string8 { size_t size; }; +/// Generic function result structure +typedef struct session_result session_result; +struct session_c_result { + bool success; + char error[256]; + size_t error_count; +}; + #define string8_literal(literal) {(char*)literal, sizeof(literal) - 1} typedef struct bytes32 bytes32; @@ -44,6 +52,11 @@ struct bytes64 { uint8_t data[64]; }; +typedef struct bytes48 bytes48; +struct bytes48 { + uint8_t data[48]; +}; + /// Basic bump allocating arena typedef struct arena_t arena_t; struct arena_t { diff --git a/include/session/util.hpp b/include/session/util.hpp index 8ee05286..08b6fdc1 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -7,14 +7,13 @@ #include #include #include +#include #include #include #include #include #include -#include "types.hpp" - namespace session { using namespace oxenc; @@ -299,4 +298,17 @@ std::vector zstd_compress( /// then this returns nullopt if the decompressed size would exceed that limit. std::optional> zstd_decompress( std::span data, size_t max_size = 0); + +struct scope_exit { + explicit scope_exit(std::function func) : cleanup(func) {} + std::function cleanup; + ~scope_exit() { + if (cleanup) + cleanup(); + } +}; + +// Write the e.what() result into `result.error` +void write_exception_to_session_c_result(struct session_c_result* result, const std::string& what); + } // namespace session diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4354c189..30532713 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,12 +34,11 @@ macro(add_libsession_util_library name) list(APPEND export_targets ${name}) endmacro() - - add_libsession_util_library(util file.cpp logging.cpp util.cpp + types.cpp ) add_libsession_util_library(crypto @@ -55,7 +54,6 @@ add_libsession_util_library(crypto sodium_array.cpp xed25519.cpp pro_backend.cpp - types.cpp ) add_libsession_util_library(config @@ -79,8 +77,6 @@ add_libsession_util_library(config fields.cpp ) - - target_link_libraries(util PUBLIC common @@ -98,6 +94,24 @@ target_link_libraries(crypto libsession::protos ) +# NOTE: High level services/layers that use the primitives (cryptography, utils...) provided +# by libsession +add_libsession_util_library(services core.cpp) +target_link_libraries( + services + PUBLIC + crypto + PRIVATE + libsodium::sodium-internal +) + +if(ENABLE_DATABASE) + target_sources(services PUBLIC database/connection.cpp) + target_link_libraries(services PUBLIC sqlcipher::sqlcipher) +else() + target_compile_definitions(services PUBLIC DISABLE_SQLCIPHER) +endif() + target_link_libraries(config PUBLIC crypto diff --git a/src/core.cpp b/src/core.cpp new file mode 100644 index 00000000..3c6f64d7 --- /dev/null +++ b/src/core.cpp @@ -0,0 +1,209 @@ +#include +#include + +#include +#include +#include + +static auto logcat = oxen::log::Cat("core"); + +namespace { +enum class SaveToDB { No, Yes }; +void pro_update_revocations_internal( + uint32_t& core_revocations_ticket, + std::set& + core_revocations, +#if !defined(DISABLE_SQLCIPHER) + session::database::Connection& core_db_conn, +#endif + uint32_t revocations_ticket, + std::span revocations, + [[maybe_unused]] SaveToDB save_to_db) { + if (core_revocations_ticket == revocations_ticket) + return; + +#if !defined(DISABLE_SQLCIPHER) + if (core_db_conn.db_ && save_to_db == SaveToDB::Yes) { + session::database::SetResult set_result = + core_db_conn.set_pro_revocations(revocations_ticket, revocations); + + // There's not much we can do here for whatever reason it failed. The runtime cache is + // updated but not the DB. The DB is only for permanence of the list across restarts of + // libsession at which point, it will load from the DB, query the backend and notice the + // ticket is out of sync and try again. + if (!set_result.success) { + oxen::log::warning( + logcat, + "Failed to update SQL revocations from (items {}; ticket {}) -> (items {}; " + "ticket " + "{}): ({}) {}", + core_revocations.size(), + core_revocations_ticket, + revocations.size(), + revocations_ticket, + set_result.sql_return_code, + set_result.sql_error); + } + } +#endif + + // Currently we just dump the entire thing and re-write it, we don't expect this list to get big + core_revocations.clear(); + core_revocations.insert(revocations.begin(), revocations.end()); + core_revocations_ticket = revocations_ticket; +} +}; // namespace + +namespace session::core { +bool ProRevocationItemComparer::operator()( + const pro_backend::ProRevocationItem& lhs, + const pro_backend::ProRevocationItem& rhs) const noexcept { + bool result = lhs.gen_index_hash < rhs.gen_index_hash; + return result; +} + +void Core::open_db( + [[maybe_unused]] const std::string& path, + [[maybe_unused]] const cleared_array<48>& raw_key) { +#if !defined(DISABLE_SQLCIPHER) + std::lock_guard lock{shared_mutex_}; + // NOTE: Zero initialise everything + revocations_.clear(); + revocations_ticket_ = 0; + + // NOTE: Open the DB + db_conn_.open(path, raw_key); + + // NOTE: Load in the pro-revocations from the DB + uint32_t pro_revocations_ticket = 0; + std::vector pro_revocations = + db_conn_.get_pro_revocations(&pro_revocations_ticket); + pro_update_revocations_internal( + revocations_ticket_, + revocations_, + db_conn_, + pro_revocations_ticket, + pro_revocations, + SaveToDB::No); +#endif +} + +bool Core::pro_proof_is_revoked( + const array_uc32& gen_index_hash, + std::chrono::sys_time unix_ts) const { + bool result = false; + pro_backend::ProRevocationItem item = {}; + item.gen_index_hash = gen_index_hash; + + std::shared_lock lock{shared_mutex_}; + auto it = revocations_.find(item); + if (it != revocations_.end()) + result = unix_ts >= it->expiry_unix_ts; + return result; +} + +void Core::pro_update_revocations( + uint32_t revocations_ticket, + std::span revocations) { + std::lock_guard lock{shared_mutex_}; + pro_update_revocations_internal( + revocations_ticket_, + revocations_, +#if !defined(DISABLE_SQLCIPHER) + db_conn_, +#endif + revocations_ticket, + revocations, + SaveToDB::Yes); +} +}; // namespace session::core + +using namespace session::core; + +LIBSESSION_C_API void session_core_core_init(session_core_core* core) { + static_assert(sizeof(core->opaque) >= sizeof(Core)); + if (core) { + new (core->opaque) Core(); + } +} + +LIBSESSION_C_API void session_core_core_deinit(session_core_core* core) { + if (core) { + auto* core_cpp = reinterpret_cast(core->opaque); + if (core_cpp) { + core_cpp->~Core(); + memset(core->opaque, 0, sizeof(core->opaque)); + } + } +} + +LIBSESSION_C_API session_database_connection* session_core_core_db_conn(session_core_core* core) { + session_database_connection* result = nullptr; + auto* core_cpp = reinterpret_cast(core->opaque); +#if !defined(DISABLE_SQLCIPHER) + if (core_cpp->db_conn_.db_.get()) + result = reinterpret_cast(&core_cpp->db_conn_); +#endif + return result; +} + +LIBSESSION_C_API session_c_result +session_core_core_open_db(session_core_core* core, string8 path, span_u8 raw_key) { + auto* core_cpp = reinterpret_cast(core->opaque); + session::cleared_array<48> raw_key_cpp; + + session_c_result result = {}; + if (raw_key.size != raw_key_cpp.max_size()) { + result.error_count = snprintf_clamped( + result.error, + sizeof(result.error), + "Raw key must be %zu bytes, unable to open DB. Received: %zu", + raw_key.size, + raw_key_cpp.max_size()); + return result; + } + + try { + std::string path_cpp = std::string(path.data, path.size); + core_cpp->open_db(path_cpp, raw_key_cpp); + result.success = true; + } catch (const std::exception& e) { + session::write_exception_to_session_c_result(&result, e.what()); + } + return result; +} + +LIBSESSION_C_API bool session_core_core_pro_proof_is_revoked( + session_core_core* core, const bytes32* gen_index_hash, uint64_t unix_ts_ms) { + bool result = false; + auto* core_cpp = reinterpret_cast(core->opaque); + session::array_uc32 gen_index_hash_cpp = {}; + memcpy(gen_index_hash_cpp.data(), gen_index_hash->data, gen_index_hash_cpp.max_size()); + auto unix_ts = + std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); + result = core_cpp->pro_proof_is_revoked(gen_index_hash_cpp, unix_ts); + return result; +} + +LIBSESSION_C_API session_c_result session_core_core_pro_update_revocations( + session_core_core* core, + uint32_t revocations_ticket, + session_pro_backend_pro_revocation_item* revocations, + size_t revocations_count) { + session_c_result result = {}; + auto* core_cpp = reinterpret_cast(core->opaque); + try { + if (revocations_count && revocations) { + std::vector revocations_cpp; + revocations_cpp.resize(revocations_count); + for (size_t index = 0; index < revocations_count; index++) + revocations_cpp[index] = + session::pro_backend::revocation_cpp_from_c(revocations[index]); + core_cpp->pro_update_revocations(revocations_ticket, revocations_cpp); + } + result.success = true; + } catch (const std::exception& e) { + session::write_exception_to_session_c_result(&result, e.what()); + } + return result; +} diff --git a/src/database/connection.cpp b/src/database/connection.cpp new file mode 100644 index 00000000..d174f99e --- /dev/null +++ b/src/database/connection.cpp @@ -0,0 +1,493 @@ +#include "session/database/connection.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "session/database/connection.h" + +static auto logcat = oxen::log::Cat("database"); + +namespace { +void throw_sql_error(int sql_result, std::string_view error_prefix) { + std::string msg = fmt::format("{}: {}", error_prefix, sqlite3_errstr(sql_result)); + throw std::runtime_error(msg); +} + +int set_db_version_or_throw(sqlite3* db, uint8_t db_version) { + char sql[64]; + size_t sql_size = snprintf(sql, sizeof(sql), "PRAGMA user_version = %u", db_version); + assert(sql_size < sizeof(sql)); + int result = sqlite3_exec(db, sql, nullptr, nullptr, nullptr); + return result; +} +}; // namespace + +namespace session::database { + +void sqlite3_deleter::operator()(sqlite3* db) const noexcept { + sqlite3_close(db); +} + +void Connection::open(const std::string& path, const cleared_array<48>& raw_key) { + cleared_array<48> ZERO_RAW_KEY = {}; + assert(memcmp(raw_key.data(), ZERO_RAW_KEY.data(), raw_key.size()) != 0 && + "Raw key was not set"); + + // Open DB + sqlite3* db_ptr = nullptr; + int rc = sqlite3_open(path.c_str(), &db_ptr); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to open database"); + + // Replace the old connection w/ the new one (if there was one previously) + db_.reset(db_ptr); + + // According to the SQLCipher docs iOS needs the 'cipher_plaintext_header_size' value set to at + // least 32 as iOS extends special privileges to the database and needs this header to be in + // plaintext to determine the file type + // + // This keeps the first 32 bytes of the database unencrypted. iOS checks the headers of open + // file-descriptors as a heuristic to determine which processes can get elevated permissions or + // processes that can be culled. SQLite databases are one of those that get elevated. + // + // For more info, see: + // https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size + rc = sqlite3_exec( + db_.get(), "PRAGMA cipher_plaintext_header_size = 32", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to configure database"); + + // Set encryption key, this is the underlying function that "PRAGMA key = .." calls + std::string fmt_key = fmt::format("x'{}'", oxenc::to_hex(raw_key)); + rc = sqlite3_key(db_.get(), fmt_key.c_str(), fmt_key.size()); + sodium_zero_buffer(fmt_key.data(), fmt_key.size()); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to set encryption key"); + + // Verify the key works by reading the sqlite_master table + rc = sqlite3_exec(db_.get(), "SELECT COUNT(*) FROM sqlite_master", nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to decrypt database with the given encryption key"); + oxen::log::debug(logcat, "Opened database {} successfully", path); + + // Query the DB version + uint8_t curr_db_version = 0; + query("PRAGMA user_version", + [&](sqlite3_stmt* stmt) { curr_db_version = sqlite3_column_int(stmt, 0); }); + + // Version migrations + const uint8_t TARGET_DB_VERSION = 1; + if (curr_db_version == 0) { + std::string_view bootstrap_sql = R"( +CREATE TABLE IF NOT EXISTS pro_revocations ( + gen_index_hash BLOB PRIMARY KEY NOT NULL, + expiry_unix_ts_ms INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY NOT NULL, + long_term_privkey BLOB NOT NULL, -- 64 byte libsodium-style secret key + UNIQUE(long_term_privkey) +); + +CREATE TABLE IF NOT EXISTS runtime ( + id INTEGER PRIMARY KEY NOT NULL, + pro_revocations_ticket INTEGER NOT NULL +);)"; + // Create the initial DB tables + rc = sqlite3_exec(db_.get(), bootstrap_sql.data(), nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to bootstrap tables"); + + // Seed the runtime table + std::string_view seed_runtime_sql = + R"(INSERT INTO runtime (pro_revocations_ticket) VALUES (0))"; + rc = sqlite3_exec(db_.get(), seed_runtime_sql.data(), nullptr, nullptr, nullptr); + if (rc != SQLITE_OK) + throw_sql_error(rc, "Failed to seed the runtime table"); + + // Teleport to the target version + rc = set_db_version_or_throw(db_.get(), ++curr_db_version); + if (rc != SQLITE_OK) + throw_sql_error(rc, fmt::format("Failed to set DB version to {}", curr_db_version)); + } + + // Requery the DB version and ensure all version migrations have occurred + [[maybe_unused]] uint8_t final_version_in_db = 0; + query("PRAGMA user_version", [&final_version_in_db](sqlite3_stmt* stmt) { + final_version_in_db = sqlite3_column_int(stmt, 0); + }); + assert(final_version_in_db == TARGET_DB_VERSION); +} + +void Connection::close() { + db_.reset(); +} + +void Connection::exec(const std::string& sql) { + assert(db_); + char* error_msg = nullptr; + int rc = sqlite3_exec(db_.get(), sql.c_str(), nullptr, nullptr, &error_msg); + if (rc != SQLITE_OK) { + std::string error = error_msg ? error_msg : "unknown error"; + sqlite3_free(error_msg); + throw std::runtime_error("SQL execution failed: " + error); + } +} + +void Connection::query(std::string_view sql, std::function callback) { + assert(db_); + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &stmt, nullptr); + if (rc != SQLITE_OK) { + throw std::runtime_error( + fmt::format("Failed to prepare statement: {}", sqlite3_errmsg(db_.get()))); + } + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) + callback(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) + throw std::runtime_error( + fmt::format("Error executing query: {}", sqlite3_errmsg(db_.get()))); +} + +Runtime Connection::get_runtime() { + assert(db_); + Runtime result = {}; + std::string_view sql = R"(SELECT id, pro_revocations_ticket FROM runtime LIMIT 1)"; + query(sql, [&result](sqlite3_stmt* stmt) { + result.id = sqlite3_column_int(stmt, 0); + result.pro_revocations_ticket = sqlite3_column_int(stmt, 1); + }); + return result; +} + +GetAccount Connection::get_account() { + GetAccount result = {}; + sqlite3_stmt* stmt = nullptr; + query("SELECT id, long_term_privkey FROM accounts ORDER BY id ASC LIMIT 1", + [&result](sqlite3_stmt* stmt) { + int db_id = sqlite3_column_int(stmt, 0); + const void* privkey = sqlite3_column_blob(stmt, 1); + int privkey_size = sqlite3_column_bytes(stmt, 1); + if (privkey_size != result.long_term_privkey.max_size()) { + throw std::runtime_error(fmt::format( + "Privkey for account was not {}b was {}b", + result.long_term_privkey.max_size(), + privkey_size)); + } + result.found = true; + result.db_id = db_id; + std::memcpy(result.long_term_privkey.data(), privkey, privkey_size); + }); + return result; +} + +void Connection::set_account(std::span long_term_privkey) { + if (long_term_privkey.size() != crypto_sign_ed25519_SECRETKEYBYTES) { + throw std::invalid_argument(fmt::format( + "Privkey must be {}b, was {}b, unable to set account keys", + crypto_sign_ed25519_SECRETKEYBYTES, + long_term_privkey.size())); + } + + sqlite3_stmt* stmt = nullptr; + std::string_view sql = R"( +INSERT OR REPLACE INTO accounts (id, long_term_privkey) +VALUES (1, ?); +)"; + + int rc = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size() + 1, &stmt, nullptr); + if (rc == SQLITE_OK) + rc = sqlite3_bind_blob( + stmt, 1, long_term_privkey.data(), long_term_privkey.size(), nullptr); + if (rc == SQLITE_OK) + rc = sqlite3_step(stmt); + int finalize_rc = sqlite3_finalize(stmt); + if (rc == SQLITE_OK) + rc = finalize_rc; + if (rc != SQLITE_DONE) + throw_sql_error(rc, "Failed to update account keys"); +} + +SetResult Connection::set_pro_revocations( + uint32_t ticket, std::span revocations) noexcept { + assert(db_); + // The following consists of exception safe code so we do not need try catch and can trivially + // commit or rollback the at the end of the function. + exec("BEGIN DEFERRED TRANSACTION;"); + exec("DELETE FROM pro_revocations"); // Clear the table + + // Assign the pro-revocations + int rc = SQLITE_OK; + if (rc == SQLITE_OK || rc == SQLITE_DONE) { + sqlite3_stmt* stmt = nullptr; + std::string_view sql = R"( +INSERT INTO pro_revocations (gen_index_hash, expiry_unix_ts_ms) +VALUES (?, ?) +)"; + + rc = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size() + 1, &stmt, nullptr); + for (size_t index = 0; (rc == SQLITE_OK || rc == SQLITE_DONE) && index < revocations.size(); + index++) { + const auto& it = revocations[index]; + int bind = 0; + int64_t expiry = static_cast(it.expiry_unix_ts.time_since_epoch().count()); + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) + ? sqlite3_bind_blob( + stmt, + ++bind, + it.gen_index_hash.data(), + static_cast(it.gen_index_hash.size()), + nullptr) + : rc; + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_bind_int64(stmt, ++bind, expiry) + : rc; + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_step(stmt) : rc; + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_reset(stmt) : rc; + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_clear_bindings(stmt) : rc; + } + int finalize_rc = sqlite3_finalize(stmt); + if (rc == SQLITE_OK || rc == SQLITE_DONE) + rc = finalize_rc; + } + + // Update the ticket + if (rc == SQLITE_OK || rc == SQLITE_DONE) { + sqlite3_stmt* stmt = nullptr; + std::string_view sql = R"(UPDATE runtime SET pro_revocations_ticket = ?)"; + + int rc = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size() + 1, &stmt, nullptr); + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_bind_int(stmt, 1, ticket) : rc; + rc = (rc == SQLITE_OK || rc == SQLITE_DONE) ? sqlite3_step(stmt) : rc; + + int finalize_rc = sqlite3_finalize(stmt); + if (rc == SQLITE_OK || rc == SQLITE_DONE) + rc = finalize_rc; + } + + SetResult result = {}; + result.sql_return_code = rc; + result.sql_error = sqlite3_errstr(rc); + result.success = result.sql_return_code == SQLITE_OK || result.sql_return_code == SQLITE_DONE; + exec(result.success ? "COMMIT;" : "ROLLBACK;"); + return result; +} + +size_t Connection::get_pro_revocations_buffer( + pro_backend::ProRevocationItem* buf, size_t buf_count, size_t offset, uint32_t* ticket) { + assert(db_); + // Note this operation is not atomic, the collecting of revocations and the querying of the + // ticket happens in 2 separate read steps. This is probably not an issue as I expect the + // getting of revocations to only happen on startup where it'll get cached into runtime memory. + // Startup and initialisation of the libsession core is single threaded. + + // Count the number of rows + size_t result = 0; + query("SELECT COUNT(*) FROM pro_revocations", + [&](sqlite3_stmt* stmt) { result = sqlite3_column_int(stmt, 0); }); + + if (buf && buf_count) { + char sql[128]; + size_t sql_size = snprintf( + sql, + sizeof(sql), + R"( +SELECT gen_index_hash, expiry_unix_ts_ms +FROM pro_revocations +LIMIT %zu +OFFSET %zu +)", + buf_count, + offset); + assert(sql_size < sizeof(sql)); + + // Retrieve the rows + result = 0; + query(std::string_view(sql, sql_size), [&buf, buf_count, &result](sqlite3_stmt* stmt) { + pro_backend::ProRevocationItem& item = buf[result++]; + + // Copy out the gen index blob + const void* gen_index_blob = sqlite3_column_blob(stmt, 0); + int gen_index_hash_size = sqlite3_column_bytes(stmt, 0); + assert(gen_index_hash_size == 32); + std::memcpy( + item.gen_index_hash.data(), + gen_index_blob, + std::min(gen_index_hash_size, static_cast(item.gen_index_hash.size()))); + + // Copy out the expiry timestmap + auto expiry = std::chrono::milliseconds(sqlite3_column_int64(stmt, 1)); + item.expiry_unix_ts = std::chrono::sys_time(expiry); + }); + } + + // Retrieve the ticket + if (ticket) { + query("SELECT pro_revocations_ticket FROM runtime LIMIT 1", [&ticket](sqlite3_stmt* stmt) { + *ticket = static_cast(sqlite3_column_int(stmt, 0)); + }); + } + return result; +} + +std::vector Connection::get_pro_revocations(uint32_t* ticket) { + assert(db_); + std::vector result; + size_t size_req = get_pro_revocations_buffer(nullptr, 0, 0, ticket); + result.resize(size_req); + size_t items_read = get_pro_revocations_buffer(result.data(), result.size(), 0, ticket); + assert(items_read == size_req); + return result; +} +} // namespace session::database + +using namespace session::database; + +LIBSESSION_C_API session_c_result +session_database_connection_open(session_database_connection* conn, string8 path, span_u8 raw_key) { + session_c_result result = {}; + + static_assert( + sizeof(((session_database_connection*)0)->opaque) >= sizeof(Connection), + "C struct instantiates the C++ instance with an `opaque` buffer via placement new so " + "the capacity must be large enough to hold the `Connection` instance"); + + session_database_connection_close(conn); + Connection* conn_cpp = new (&conn->opaque) Connection(); + + session::cleared_array<48> raw_key_cpp; + if (raw_key.size != raw_key_cpp.max_size()) { + result.error_count = snprintf_clamped( + result.error, + sizeof(result.error), + "Raw key must be 48 bytes, received %zu", + raw_key.size); + return result; + } + + // Must be string because we need to guarantee that `path` was null-terminated for the SQL API. + std::string path_cpp = std::string(path.data, path.data + path.size); + memcpy(raw_key_cpp.data(), raw_key.data, raw_key.size); + + try { + conn_cpp->open(path_cpp, raw_key_cpp); + result.success = true; + } catch (const std::exception& e) { + session::write_exception_to_session_c_result(&result, e.what()); + } + + return result; +} + +LIBSESSION_C_API void session_database_connection_close(session_database_connection* conn) { + if (conn) { + auto* conn_cpp = reinterpret_cast(&conn->opaque); + if (conn_cpp) { + conn_cpp->close(); + memset(&conn->opaque, 0, sizeof(conn->opaque)); + } + } +} + +LIBSESSION_C_API session_database_get_account +session_database_connection_get_account(session_database_connection* conn) { + session_database_get_account result = {}; + if (conn) { + auto* conn_cpp = reinterpret_cast(&conn->opaque); + GetAccount cpp = conn_cpp->get_account(); + result.db_id = cpp.db_id; + result.found = cpp.found; + static_assert(sizeof(result.long_term_privkey.data) == cpp.long_term_privkey.max_size()); + std::memcpy( + result.long_term_privkey.data, + cpp.long_term_privkey.data(), + cpp.long_term_privkey.size()); + } + return result; +} + +LIBSESSION_C_API session_c_result session_database_connection_set_account( + session_database_connection* conn, + void const* long_term_privkey, + size_t long_term_privkey_size) { + session_c_result result = {}; + if (conn) { + auto* conn_cpp = reinterpret_cast(&conn->opaque); + auto long_term_privkey_cpp = std::span{ + reinterpret_cast(long_term_privkey), long_term_privkey_size}; + try { + conn_cpp->set_account(long_term_privkey_cpp); + result.success = true; + } catch (const std::exception& e) { + session::write_exception_to_session_c_result(&result, e.what()); + } + } + return result; +} + +LIBSESSION_C_API session_database_set_result session_database_connection_set_pro_revocations( + session_database_connection* conn, + uint32_t ticket, + session_pro_backend_pro_revocation_item* revocations, + size_t revocations_len) { + session_database_set_result result = {}; + auto* conn_cpp = reinterpret_cast(&conn->opaque); + try { + // Convert revocations to CPP instance + std::vector revocations_cpp; + revocations_cpp.reserve(revocations_len); + for (size_t index = 0; index < revocations_len; index++) { + const session_pro_backend_pro_revocation_item& src = revocations[index]; + session::pro_backend::ProRevocationItem& dest = revocations_cpp.emplace_back(); + dest = session::pro_backend::revocation_cpp_from_c(src); + } + + // Do the operation + SetResult result_cpp = conn_cpp->set_pro_revocations(ticket, revocations_cpp); + result.db.success = result_cpp.success; + result.sql_return_code = result_cpp.sql_return_code; + result.sql_error = result_cpp.sql_error; + } catch (const std::exception& e) { + session::write_exception_to_session_c_result(&result.db, e.what()); + } + + return result; +} + +LIBSESSION_C_API session_database_get_pro_revocation_result +session_database_connection_get_pro_revocations_buffer( + session_database_connection* conn, + OPTIONAL session_pro_backend_pro_revocation_item* buf, + size_t buf_count, + size_t offset, + OPTIONAL uint32_t* ticket) { + auto* conn_cpp = reinterpret_cast(&conn->opaque); + session_database_get_pro_revocation_result result = {}; + try { + std::vector buf_cpp; + if (buf && buf_count) + buf_cpp.resize(buf_count); + + result.count = + conn_cpp->get_pro_revocations_buffer(buf_cpp.data(), buf_count, offset, ticket); + buf_cpp.resize(result.count); + + if (buf) { + for (size_t index = 0; index < result.count; index++) + buf[index] = session::pro_backend::revocation_c_from_cpp(buf_cpp[index]); + } + + result.db.success = true; + } catch (std::exception& e) { + session::write_exception_to_session_c_result(&result.db, e.what()); + } + + return result; +} diff --git a/src/pro_backend.cpp b/src/pro_backend.cpp index ae6714df..9167a45a 100644 --- a/src/pro_backend.cpp +++ b/src/pro_backend.cpp @@ -776,6 +776,23 @@ GetProDetailsResponse GetProDetailsResponse::parse(std::string_view json) { return result; } +session_pro_backend_pro_revocation_item revocation_c_from_cpp(ProRevocationItem const& src) { + session_pro_backend_pro_revocation_item result = {}; + std::memcpy(result.gen_index_hash.data, src.gen_index_hash.data(), src.gen_index_hash.size()); + result.expiry_unix_ts_ms = std::chrono::duration_cast( + src.expiry_unix_ts.time_since_epoch()) + .count(); + return result; +} + +ProRevocationItem revocation_cpp_from_c(session_pro_backend_pro_revocation_item const& src) { + pro_backend::ProRevocationItem result = {}; + memcpy(result.gen_index_hash.data(), src.gen_index_hash.data, sizeof(src.gen_index_hash.data)); + result.expiry_unix_ts = std::chrono::sys_time( + std::chrono::milliseconds(src.expiry_unix_ts_ms)); + return result; +} + array_uc64 SetPaymentRefundRequestedRequest::build_sig( uint8_t version, std::span master_privkey, @@ -1407,11 +1424,7 @@ session_pro_backend_get_pro_revocations_response_parse(const char* json, size_t for (size_t index = 0; index < result.items_count; ++index) { const ProRevocationItem& src = cpp.items[index]; - session_pro_backend_pro_revocation_item& dest = result.items[index]; - std::memcpy(dest.gen_index_hash.data, src.gen_index_hash.data(), src.gen_index_hash.size()); - dest.expiry_unix_ts_ms = std::chrono::duration_cast( - src.expiry_unix_ts.time_since_epoch()) - .count(); + result.items[index] = revocation_c_from_cpp(src); } return result; } diff --git a/src/session_protocol.cpp b/src/session_protocol.cpp index aeabdee8..4d8bca23 100644 --- a/src/session_protocol.cpp +++ b/src/session_protocol.cpp @@ -445,6 +445,8 @@ static EncryptedForDestinationInternal encode_for_destination_internal( bool is_1o1 = dest_type == DestinationType::SyncOr1o1; bool is_community_inbox = dest_type == DestinationType::CommunityInbox; bool is_community = dest_type == DestinationType::Community; + bool is_community_envelope = dest_type == DestinationType::EnvelopeCommunityInbox || + dest_type == DestinationType::EnvelopeCommunityInbox; if (!is_community) { assert(ed25519_privkey.size() == crypto_sign_ed25519_SECRETKEYBYTES || ed25519_privkey.size() == crypto_sign_ed25519_SEEDBYTES); @@ -474,8 +476,12 @@ static EncryptedForDestinationInternal encode_for_destination_internal( EncryptedForDestinationInternal result = {}; switch (dest_type) { - case DestinationType::Group: /*FALLTHRU*/ + case DestinationType::EnvelopeCommunity: /*FALLTHRU*/ + case DestinationType::EnvelopeCommunityInbox: /*FALLTHRU*/ + case DestinationType::Group: /*FALLTHRU*/ case DestinationType::SyncOr1o1: { + assert(is_group || is_1o1 || is_community_envelope); + if (is_group && dest_group_ed25519_pubkey[0] != static_cast(SessionIDPrefix::group)) { // Legacy groups which have a 05 prefixed key @@ -484,14 +490,23 @@ static EncryptedForDestinationInternal encode_for_destination_internal( "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 + if (is_1o1) { + // 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 padded_payload = pad_message(content); + + // Encrypt the padded output tmp_content_buffer = encrypt_for_recipient_deterministic( ed25519_privkey, dest_recipient_pubkey, padded_payload); content = tmp_content_buffer; + } else if (dest_type == DestinationType::EnvelopeCommunityInbox) { + // For community messages we only pad the content if it's an inbox message (e.g. DMs + // require encryption in a community, so we pad to avoid leakage of metadata of the + // kind of message being sent being inferred from the size of the payload). + assert(is_community_envelope); + tmp_content_buffer = pad_message(content); + content = tmp_content_buffer; } // Create envelope @@ -499,9 +514,11 @@ static EncryptedForDestinationInternal encode_for_destination_internal( // 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); + (is_1o1 || is_community_envelope) + ? SessionProtos::Envelope_Type_SESSION_MESSAGE + : SessionProtos::Envelope_Type_CLOSED_GROUP_MESSAGE); + if (!is_community_envelope) + envelope.set_sourcedevice(1); envelope.set_timestamp(dest_sent_timestamp_ms.count()); envelope.set_content(content.data(), content.size()); @@ -540,7 +557,7 @@ static EncryptedForDestinationInternal encode_for_destination_internal( } else { result.ciphertext_cpp = std::move(ciphertext); } - } else { + } else if (is_1o1) { // 1o1, Wrap in websocket message WebSocketProtos::WebSocketMessage msg = {}; msg.set_type(WebSocketProtos::WebSocketMessage_Type::WebSocketMessage_Type_REQUEST); @@ -564,6 +581,35 @@ static EncryptedForDestinationInternal encode_for_destination_internal( result.ciphertext_cpp.data(), result.ciphertext_cpp.size()); } assert(serialized); + } else { + assert(is_community_envelope); + if (dest_type == DestinationType::EnvelopeCommunityInbox) { + std::string bytes = envelope.SerializeAsString(); + std::vector ciphertext = encrypt_for_blinded_recipient( + ed25519_privkey, + dest_community_inbox_server_pubkey, + dest_recipient_pubkey, // recipient blinded pubkey + to_span(bytes)); + + 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 { + [[maybe_unused]] bool serialized = false; + if (use_malloc == UseMalloc::Yes) { + result.ciphertext_c = span_u8_alloc_or_throw(envelope.ByteSizeLong()); + serialized = envelope.SerializeToArray( + result.ciphertext_c.data, result.ciphertext_c.size); + } else { + result.ciphertext_cpp.resize(envelope.ByteSizeLong()); + envelope.SerializeToArray( + result.ciphertext_cpp.data(), result.ciphertext_cpp.size()); + } + assert(serialized); + } } } break; @@ -573,19 +619,19 @@ static EncryptedForDestinationInternal encode_for_destination_internal( 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) + // community message types require it for old-style content messages) // 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. + // 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 = {}; @@ -608,9 +654,9 @@ static EncryptedForDestinationInternal encode_for_destination_internal( 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. + // 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( @@ -624,10 +670,6 @@ static EncryptedForDestinationInternal encode_for_destination_internal( content = tmp_content_buffer; } - // TODO: We don't need to actually pad the community message since that's unencrypted, - // there's no need to make the message sizes uniform but we need it for backwards - // compat. We can remove this eventually, first step is to unify the clients. - if (is_community_inbox) { std::vector ciphertext = encrypt_for_blinded_recipient( ed25519_privkey, @@ -868,6 +910,8 @@ DecodedEnvelope decode_envelope( static_assert(sizeof(result.envelope.pro_sig) == crypto_sign_ed25519_BYTES); std::memcpy(result.envelope.pro_sig.data(), pro_sig.data(), pro_sig.size()); + // We only care about verifying the pro-signature if the content has pro data populated, + // otherwise we assume that it's a dummy signature for preventing metadata leakage. if (content.has_promessage()) { if (!content.sigtimestamp()) throw std::runtime_error{fmt::format( @@ -1007,9 +1051,10 @@ DecodedCommunityMessage decode_for_community( 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{ - "Decoding community message failed, could not interpret blob as content or " - "envelope"}; + throw std::runtime_error{fmt::format( + "Decoding community message failed, could not interpret blob as {}: {}b", + result.envelope ? "envelope" : "content", + unpadded_content.size())}; // Extract the pro signature from content if it was present if (content.has_prosigforcommunitymessageonly()) { @@ -1120,6 +1165,21 @@ DecodedCommunityMessage decode_for_community( return result; } +DecodedCommunityMessage decode_for_community_inbox( + std::span ed25519_privkey, + std::span community_pubkey, + std::span sender_id, + std::span recipient_id, + std::span ciphertext, + std::chrono::sys_time unix_ts, + const array_uc32& pro_backend_pubkey) { + auto [decrypted_blob, session_id] = decrypt_from_blinded_recipient( + ed25519_privkey, community_pubkey, sender_id, recipient_id, ciphertext); + DecodedCommunityMessage result = + decode_for_community(decrypted_blob, unix_ts, pro_backend_pubkey); + return result; +} + void make_blake2b32_hasher( crypto_generichash_blake2b_state* hasher, std::string_view personalization) { assert(personalization.data() == nullptr || @@ -1458,6 +1518,85 @@ LIBSESSION_C_API void session_protocol_encode_for_destination_free( } } +static session_protocol_decoded_community_message +session_protocol_decoded_community_message_from_cpp(const DecodedCommunityMessage& decoded) { + session_protocol_decoded_community_message result = {}; + result.has_envelope = decoded.envelope.has_value(); + if (result.has_envelope) + result.envelope = envelope_from_cpp(*decoded.envelope); + result.content_plaintext = session::span_u8_copy_or_throw( + decoded.content_plaintext.data(), decoded.content_plaintext.size()); + result.has_pro = decoded.pro.has_value(); + if (decoded.pro_sig) + std::memcpy(result.pro_sig.data, decoded.pro_sig->data(), decoded.pro_sig->max_size()); + if (decoded.pro) + result.pro = decoded_pro_from_cpp(*decoded.pro); + result.success = true; + return result; +} + +LIBSESSION_C_API session_protocol_decoded_community_message +session_protocol_decode_for_community_inbox( + const unsigned char* ed25519_privkey, + size_t ed25519_privkey_len, + const unsigned char* community_pubkey, + size_t community_pubkey_len, + const unsigned char* sender_id, + size_t sender_id_len, + const unsigned char* recipient_id, + size_t recipient_id_len, + const unsigned char* ciphertext, + size_t ciphertext_len, + uint64_t unix_ts_ms, + OPTIONAL const void* pro_backend_pubkey, + OPTIONAL size_t pro_backend_pubkey_len, + OPTIONAL char* error, + size_t error_len) { + std::span ed25519_privkey_span = {ed25519_privkey, ed25519_privkey_len}; + std::span community_pubkey_span = {community_pubkey, community_pubkey_len}; + std::span sender_id_span = {sender_id, sender_id_len}; + std::span recipient_id_span = {recipient_id, recipient_id_len}; + std::span ciphertext_span = {ciphertext, ciphertext_len}; + auto unix_ts = + std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); + + session_protocol_decoded_community_message result = {}; + 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; + return result; + } + + try { + DecodedCommunityMessage decoded = decode_for_community_inbox( + ed25519_privkey_span, + community_pubkey_span, + sender_id_span, + recipient_id_span, + ciphertext_span, + unix_ts, + pro_backend_pubkey_cpp.data); + result = session_protocol_decoded_community_message_from_cpp(decoded); + } catch (const std::exception& e) { + std::string error_cpp = e.what(); + result.error_len_incl_null_terminator = snprintf_clamped( + error, + error_len, + "%.*s", + static_cast(error_cpp.size()), + error_cpp.data()) + + 1; + } + return result; +} + LIBSESSION_C_API session_protocol_decoded_envelope session_protocol_decode_envelope( const session_protocol_decode_envelope_keys* keys, @@ -1597,17 +1736,7 @@ session_protocol_decoded_community_message session_protocol_decode_for_community try { DecodedCommunityMessage decoded = decode_for_community( content_or_envelope_payload_span, unix_ts, pro_backend_pubkey_cpp.data); - result.has_envelope = decoded.envelope.has_value(); - if (result.has_envelope) - result.envelope = envelope_from_cpp(*decoded.envelope); - result.content_plaintext = session::span_u8_copy_or_throw( - decoded.content_plaintext.data(), decoded.content_plaintext.size()); - result.has_pro = decoded.pro.has_value(); - if (decoded.pro_sig) - std::memcpy(result.pro_sig.data, decoded.pro_sig->data(), decoded.pro_sig->max_size()); - if (decoded.pro) - result.pro = decoded_pro_from_cpp(*decoded.pro); - result.success = true; + result = session_protocol_decoded_community_message_from_cpp(decoded); } catch (const std::exception& e) { std::string error_cpp = e.what(); result.success = false; diff --git a/src/util.cpp b/src/util.cpp index a8b24360..8818788e 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -223,6 +224,14 @@ size_t utf16_count(std::span utf16_string) { return simdutf::count_utf16(utf16_string.data(), utf16_string.size()); } +void write_exception_to_session_c_result(struct session_c_result* result, const std::string& what) { + result->error_count = snprintf_clamped( + result->error, + sizeof(result->error), + "%.*s", + static_cast(what.size()), + what.data()); +} } // namespace session LIBSESSION_C_API size_t utf16_count_truncated_to_codepoints( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e0558c0..1f6e7094 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ set(LIB_SESSION_UTESTS_SOURCES test_blinding.cpp test_bt_merge.cpp test_bugs.cpp + test_core.cpp test_compression.cpp test_config_userprofile.cpp test_config_user_groups.cpp @@ -45,6 +46,7 @@ endif() add_library(test_libs INTERFACE) target_link_libraries(test_libs INTERFACE + libsession::services libsession::config libsodium::sodium-internal nlohmann_json::nlohmann_json diff --git a/tests/main.cpp b/tests/main.cpp index 76c4cc49..d982b075 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,7 +1,7 @@ #include #include -std::string g_test_pro_backend_dev_server_url = "http://127.0.0.1:5000"; +std::string g_test_pro_backend_dev_server_url = ""; int main(int argc, char* argv[]) { Catch::Session session; @@ -20,7 +20,8 @@ int main(int argc, char* argv[]) { "enable oxen log tracing of test cases/sections") | Opt(g_test_pro_backend_dev_server_url, "url")["--pro-backend-dev-server-url"]( "URL to a SESH_PRO_BACKEND_DEV=1 enabled Session Pro Backend server. Only " - "used if compiled with -D TEST_PRO_BACKEND_WITH_DEV_SERVER=1 support"); + "used if compiled with -D TEST_PRO_BACKEND_WITH_DEV_SERVER=1 support. These " + "tests are skipped if not specified."); session.cli(cli); @@ -47,5 +48,8 @@ int main(int argc, char* argv[]) { oxen::log::Cat("testcase"), test_case_tracing ? oxen::log::Level::trace : oxen::log::Level::off); + if (g_test_pro_backend_dev_server_url.empty()) + fprintf(stdout, "Skipping --pro-backend-dev-server-url tests, no URL specified.\n"); + return session.run(); } diff --git a/tests/test_core.cpp b/tests/test_core.cpp new file mode 100644 index 00000000..4559746f --- /dev/null +++ b/tests/test_core.cpp @@ -0,0 +1,190 @@ +#if !defined(DISABLE_SQLCIPHER) +#include +#include + +#include +#include +#include +#include +#include + +TEST_CASE("Core", "[core][database][pro][revocations]") { + session_core_core core = {}; + session_core_core_init(&core); + auto on_exit = session::scope_exit([&]() { session_core_core_deinit(&core); }); + + // Setup the encryption key + session::cleared_array<48> raw_key = {}; + randombytes_buf(raw_key.data(), raw_key.size()); + span_u8 raw_key_span = {raw_key.data(), raw_key.size()}; + + // Open the DB + string8 db_path = string8_literal("file::memory:?cache=shared"); + session_c_result open_db_result = session_core_core_open_db(&core, db_path, raw_key_span); + REQUIRE(open_db_result.success); + + session_database_connection* db = session_core_core_db_conn(&core); + auto* db_cpp = reinterpret_cast(&db->opaque); + + // Check runtime was seeded to ticket 0 + session::database::Runtime runtime = db_cpp->get_runtime(); + REQUIRE(runtime.id == 1); + REQUIRE(runtime.pro_revocations_ticket == 0); + + // Check that the DB has no revocations in it + uint32_t ticket = 0; + session_database_get_pro_revocation_result get_result = + session_database_connection_get_pro_revocations_buffer(db, nullptr, 0, 0, &ticket); + REQUIRE(get_result.db.success); + REQUIRE(get_result.count == 0); + + // Create the revocations we will put into the DB + uint64_t unix_ts_ms = 1698765432ULL * 1000; // Arbitrary timestamp + auto unix_ts = + std::chrono::sys_time(std::chrono::milliseconds(unix_ts_ms)); + + session_pro_backend_pro_revocation_item src_items[] = { + { + .gen_index_hash = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + .expiry_unix_ts_ms = static_cast(unix_ts.time_since_epoch().count()), + }, + { + .gen_index_hash = {0x33, 0xa1, 0xc4, 0x4e, 0x60, 0x94, 0x48, 0x8f, + 0x5c, 0xeb, 0xe2, 0x4b, 0xfc, 0xf9, 0x89, 0xda, + 0x07, 0xdd, 0xc4, 0x8d, 0xe2, 0xae, 0x86, 0x6c, + 0x8c, 0x78, 0xb9, 0x16, 0x60, 0xc8, 0x49, 0xf1}, + .expiry_unix_ts_ms = static_cast(unix_ts.time_since_epoch().count()), + }, + }; + + // Set the items in Core (and consequently, the DB) + session_c_result set_result = session_core_core_pro_update_revocations( + &core, 1 /*ticket*/, src_items, sizeof(src_items) / sizeof(src_items[0])); + INFO("Set w/ 2 items failed: " << set_result.error); + REQUIRE(set_result.success); + + // Check runtime ticket was changed to 1 + runtime = db_cpp->get_runtime(); + REQUIRE(runtime.id == 1); + REQUIRE(runtime.pro_revocations_ticket == 1); + + // Count the number of revocations in the DB (should be 2 as we've inserted them) + get_result = session_database_connection_get_pro_revocations_buffer(db, nullptr, 0, 0, &ticket); + REQUIRE(ticket == runtime.pro_revocations_ticket); + REQUIRE(get_result.db.success); + REQUIRE(get_result.count == 2); + + // Check that the revocations was in the DB + std::vector db_items = + db_cpp->get_pro_revocations(&ticket); + REQUIRE(ticket == 1); + REQUIRE(memcmp(src_items[0].gen_index_hash.data, + db_items[0].gen_index_hash.data(), + db_items[0].gen_index_hash.size()) == 0); + REQUIRE(src_items[0].expiry_unix_ts_ms == + db_items[0].expiry_unix_ts.time_since_epoch().count()); + + REQUIRE(memcmp(src_items[1].gen_index_hash.data, + db_items[1].gen_index_hash.data(), + db_items[1].gen_index_hash.size()) == 0); + REQUIRE(src_items[1].expiry_unix_ts_ms == + db_items[1].expiry_unix_ts.time_since_epoch().count()); + + // Delete the first item (src[0]) from the Core (and consequently the DB) + session_pro_backend_pro_revocation_item set_item = src_items[1]; + set_result = session_core_core_pro_update_revocations(&core, 2 /*ticket*/, &set_item, 1); + INFO("Set w/ 1 item failed: " << set_result.error); + REQUIRE(set_result.success); + + // Count the number of revocations in the DB (should be 1 as we've deleted one of them) + get_result = session_database_connection_get_pro_revocations_buffer(db, nullptr, 0, 0, &ticket); + REQUIRE(get_result.db.success); + REQUIRE(get_result.count == 1); + REQUIRE(ticket == 2); + + // Verify that the DB now has just the item at src[1] + session_pro_backend_pro_revocation_item db_items_after_delete[2]; + assert(sizeof(db_items_after_delete) / sizeof(db_items_after_delete[0]) >= get_result.count); + + session_database_get_pro_revocation_result db_items_after_delete_result = + session_database_connection_get_pro_revocations_buffer( + db, + db_items_after_delete, + sizeof(db_items_after_delete) / sizeof(db_items_after_delete[0]), + 0, + &ticket); + + REQUIRE(db_items_after_delete_result.db.success); + REQUIRE(db_items_after_delete_result.count == 1); + REQUIRE(memcmp(src_items[1].gen_index_hash.data, + db_items_after_delete[0].gen_index_hash.data, + sizeof(db_items_after_delete[0].gen_index_hash.data)) == 0); + REQUIRE(src_items[1].expiry_unix_ts_ms == db_items_after_delete[0].expiry_unix_ts_ms); + REQUIRE(ticket == 2); + + // Test a 2nd core, opening the DB that the 1st core created + session_core_core core_2nd = {}; + session_core_core_init(&core_2nd); + auto on_exit_2nd = session::scope_exit([&]() { session_core_core_deinit(&core_2nd); }); + session_c_result open_db_2nd = session_core_core_open_db(&core_2nd, db_path, raw_key_span); + REQUIRE(open_db_2nd.success); + + // Verify that the 2nd core loaded into memory the same contents as the 1st core + auto* core_1st_cpp = reinterpret_cast(core.opaque); + auto* core_2nd_cpp = reinterpret_cast(core_2nd.opaque); + REQUIRE(core_1st_cpp->revocations_ticket_ == core_2nd_cpp->revocations_ticket_); +} + +TEST_CASE("Core", "[core][database][pro][account]") { + session_core_core core = {}; + session_core_core_init(&core); + auto on_exit = session::scope_exit([&]() { session_core_core_deinit(&core); }); + + // Setup the encryption key + session::cleared_array<48> raw_key = {}; + randombytes_buf(raw_key.data(), raw_key.size()); + span_u8 raw_key_span = {raw_key.data(), raw_key.size()}; + + // Open the DB + string8 db_path = string8_literal(":memory:"); + session_c_result open_db_result = session_core_core_open_db(&core, db_path, raw_key_span); + REQUIRE(open_db_result.success); + + session_database_connection* db = session_core_core_db_conn(&core); + + // Try get an account before we added one + bytes64 zero_long_term_key = {}; + session_database_get_account get = session_database_connection_get_account(db); + REQUIRE_FALSE(get.found); + REQUIRE(get.db_id == 0); + REQUIRE(memcmp(get.long_term_privkey.data, + zero_long_term_key.data, + sizeof(zero_long_term_key.data)) == 0); + + // Set a 1 byte zero key for the account and check that it does not accept it + session_c_result c_result = + session_database_connection_set_account(db, zero_long_term_key.data, 1); + REQUIRE(!c_result.success); + REQUIRE(c_result.error_count > 0); + + // Generate a key and set it + bytes64 long_term_key = {}; + randombytes_buf(long_term_key.data, sizeof(long_term_key.data)); + c_result = session_database_connection_set_account( + db, long_term_key.data, sizeof(long_term_key.data)); + INFO(c_result.error); + REQUIRE(c_result.success); + REQUIRE(c_result.error_count == 0); + + // Try retrieving the account again + session_database_get_account get_again = session_database_connection_get_account(db); + REQUIRE(get_again.found); + REQUIRE(get_again.db_id == 1); + REQUIRE(memcmp(get_again.long_term_privkey.data, + long_term_key.data, + sizeof(long_term_key.data)) == 0); +} +#endif // !defined(DISABLE_SQLCIPHER) diff --git a/tests/test_pro_backend.cpp b/tests/test_pro_backend.cpp index 4a4f7240..cf7a6275 100644 --- a/tests/test_pro_backend.cpp +++ b/tests/test_pro_backend.cpp @@ -229,7 +229,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid request auto result = session_pro_backend_add_pro_payment_request_to_json(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + session::scope_exit result_free{ + [&]() { session_pro_backend_to_json_free(&result); }}; REQUIRE(result.success); REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); @@ -310,7 +311,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid request auto result = session_pro_backend_generate_pro_proof_request_to_json(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + session::scope_exit result_free{ + [&]() { session_pro_backend_to_json_free(&result); }}; REQUIRE(result.success); REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); @@ -367,7 +369,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid request auto result = session_pro_backend_get_pro_revocations_request_to_json(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + session::scope_exit result_free{ + [&]() { session_pro_backend_to_json_free(&result); }}; REQUIRE(result.success); REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); @@ -411,7 +414,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { // Valid request auto result = session_pro_backend_get_pro_details_request_to_json(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + session::scope_exit result_free{ + [&]() { session_pro_backend_to_json_free(&result); }}; REQUIRE(result.success); REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); @@ -471,7 +475,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( json.data(), json.size()); { - scope_exit result_free{[&]() { + session::scope_exit result_free{[&]() { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( &result); }}; @@ -528,7 +532,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { result = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( json.data(), json.size()); { - scope_exit result_free{[&]() { + session::scope_exit result_free{[&]() { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free( &result); }}; @@ -571,7 +575,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { auto result = session_pro_backend_get_pro_revocations_response_parse( json.data(), json.size()); { - scope_exit result_free{ + session::scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); @@ -598,7 +602,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { result = session_pro_backend_get_pro_revocations_response_parse( json.data(), json.size()); - scope_exit result_free{ + session::scope_exit result_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); @@ -652,7 +656,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { auto result = session_pro_backend_get_pro_details_response_parse(json.data(), json.size()); { - scope_exit result_free{ + session::scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) INFO(result.header.errors[index].data); @@ -702,7 +706,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { json = "{invalid}"; result = session_pro_backend_get_pro_details_response_parse(json.data(), json.size()); { - scope_exit result_free{ + session::scope_exit result_free{ [&]() { session_pro_backend_get_pro_details_response_free(&result); }}; REQUIRE(result.header.status != SESSION_PRO_BACKEND_STATUS_SUCCESS); REQUIRE(result.header.errors_count > 0); @@ -767,7 +771,8 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { auto result = session_pro_backend_set_payment_refund_requested_request_to_json(&request); { - scope_exit result_free{[&]() { session_pro_backend_to_json_free(&result); }}; + session::scope_exit result_free{ + [&]() { session_pro_backend_to_json_free(&result); }}; REQUIRE(result.success); REQUIRE(result.json.data != nullptr); REQUIRE(result.json.size > 0); @@ -814,7 +819,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { auto result = session_pro_backend_set_payment_refund_requested_response_parse( json.data(), json.size()); { - scope_exit result_free{[&]() { + session::scope_exit result_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) @@ -834,7 +839,7 @@ TEST_CASE("Pro Backend C API", "[pro_backend]") { { result = session_pro_backend_set_payment_refund_requested_response_parse( json.data(), json.size()); - scope_exit result_free{[&]() { + session::scope_exit result_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&result); }}; for (size_t index = 0; index < result.header.errors_count; index++) @@ -889,6 +894,10 @@ std::string curl_do_basic_blocking_post_request( } TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { + if (g_test_pro_backend_dev_server_url.empty()) { + return; + } + // Setup: Generate keys and payment token hash bytes32 master_pubkey = {}; bytes64 master_privkey = {}; @@ -903,16 +912,16 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Setup CURL curl_global_init(CURL_GLOBAL_DEFAULT); - scope_exit curl_cleanup{[&]() { curl_global_cleanup(); }}; + session::scope_exit curl_cleanup{[&]() { curl_global_cleanup(); }}; CURL* curl = curl_easy_init(); REQUIRE(curl); - scope_exit curl_free{[&]() { curl_easy_cleanup(curl); }}; + session::scope_exit curl_free{[&]() { curl_easy_cleanup(curl); }}; struct curl_slist* curl_headers = nullptr; curl_headers = curl_slist_append(curl_headers, "Content-Type: application/json"); REQUIRE(curl_headers); - scope_exit curl_headers_free{[&]() { curl_slist_free_all(curl_headers); }}; + session::scope_exit curl_headers_free{[&]() { curl_slist_free_all(curl_headers); }}; // Add pro payment session_protocol_pro_proof first_pro_proof = {}; @@ -961,7 +970,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_to_json request_json = session_pro_backend_add_pro_payment_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -974,7 +984,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { + session::scope_exit response_free{[&]() { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); }}; @@ -1020,7 +1030,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_to_json request_json = session_pro_backend_generate_pro_proof_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; // Do CURL request std::string response_json = curl_do_basic_blocking_post_request( @@ -1033,7 +1044,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { + session::scope_exit response_free{[&]() { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); }}; @@ -1080,7 +1091,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Do CURL request session_pro_backend_to_json request_json = session_pro_backend_get_pro_details_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; std::string response_json = curl_do_basic_blocking_post_request( curl, @@ -1092,7 +1104,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_get_pro_details_response response = session_pro_backend_get_pro_details_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{ + session::scope_exit response_free{ [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; // Verify the response @@ -1128,7 +1140,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { // Do CURL request session_pro_backend_to_json request_json = session_pro_backend_get_pro_details_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; std::string response_json = curl_do_basic_blocking_post_request( curl, @@ -1140,7 +1153,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_get_pro_details_response response = session_pro_backend_get_pro_details_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{ + session::scope_exit response_free{ [&]() { session_pro_backend_get_pro_details_response_free(&response); }}; for (size_t index = 0; index < response.header.errors_count; index++) { @@ -1205,7 +1218,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_to_json request_json = session_pro_backend_add_pro_payment_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1218,7 +1232,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_add_pro_payment_or_generate_pro_proof_response response = session_pro_backend_add_pro_payment_or_generate_pro_proof_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { + session::scope_exit response_free{[&]() { session_pro_backend_add_pro_payment_or_generate_pro_proof_response_free(&response); }}; @@ -1240,7 +1254,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_to_json request_json = session_pro_backend_get_pro_revocations_request_to_json(&request); - scope_exit request_json_free{[&]() { session_pro_backend_to_json_free(&request_json); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1253,7 +1268,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_get_pro_revocations_response response = session_pro_backend_get_pro_revocations_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{ + session::scope_exit response_free{ [&]() { session_pro_backend_get_pro_revocations_response_free(&response); }}; // Verify response @@ -1287,7 +1302,8 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { 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); }}; + session::scope_exit request_json_free{ + [&]() { session_pro_backend_to_json_free(&request_json); }}; // Do curl request std::string response_json = curl_do_basic_blocking_post_request( @@ -1300,7 +1316,7 @@ TEST_CASE("Pro Backend Dev Server", "[pro_backend][dev_server]") { session_pro_backend_set_payment_refund_requested_response response = session_pro_backend_set_payment_refund_requested_response_parse( response_json.data(), response_json.size()); - scope_exit response_free{[&]() { + session::scope_exit response_free{[&]() { session_pro_backend_set_payment_refund_requested_response_free(&response); }}; diff --git a/tests/test_session_protocol.cpp b/tests/test_session_protocol.cpp index 8b154c5f..4e65f11d 100644 --- a/tests/test_session_protocol.cpp +++ b/tests/test_session_protocol.cpp @@ -109,7 +109,7 @@ static SerialisedProtobufContentWithProForTesting build_protobuf_content_with_se TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { // Do tests that require no setup - SECTION("Ensure get pro fetaures detects large message") { + SECTION("Ensure get pro features detects large message") { // Try a message below the size threshold { auto msg = std::string(SESSION_PROTOCOL_PRO_STANDARD_CHARACTER_LIMIT, 'a'); @@ -847,36 +847,37 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); } - 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 = {}; - session_blind15_pk0.data[0] = 0x15; - session_blind15_key_pair( - keys.ed_sk0.data(), - community_pk.data(), - session_blind15_pk0.data + 1, - session_blind15_sk0.data); - - bytes32 session_blind15_sk1 = {}; - bytes33 session_blind15_pk1 = {}; - session_blind15_pk1.data[0] = 0x15; - session_blind15_key_pair( - keys.ed_sk1.data(), - community_pk.data(), - session_blind15_pk1.data + 1, - session_blind15_sk1.data); - - bytes33 recipient_pubkey = session_blind15_pk1; - bytes32 community_pubkey = {}; - std::memcpy(community_pubkey.data, community_pk.data(), community_pk.size()); + // NOTE: Setup some keys for community inbox testing + 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 = {}; + session_blind15_pk0.data[0] = 0x15; + session_blind15_key_pair( + keys.ed_sk0.data(), + community_pk.data(), + session_blind15_pk0.data + 1, + session_blind15_sk0.data); + + bytes32 session_blind15_sk1 = {}; + bytes33 session_blind15_pk1 = {}; + session_blind15_pk1.data[0] = 0x15; + session_blind15_key_pair( + keys.ed_sk1.data(), + community_pk.data(), + session_blind15_pk1.data + 1, + session_blind15_sk1.data); + + bytes33 recipient_pubkey = session_blind15_pk1; + bytes32 community_pubkey = {}; + std::memcpy(community_pubkey.data, community_pk.data(), community_pk.size()); + SECTION("Encode/decode for community inbox (content message)") { session_protocol_encoded_for_destination encoded = session_protocol_encode_for_community_inbox( protobuf_content.plaintext.data(), @@ -892,22 +893,178 @@ TEST_CASE("Session protocol helpers C API", "[session-protocol][helpers]") { sizeof(error)); scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; - 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}); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO(error); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(!decoded.has_pro); + } - 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(), + SECTION("Encode/decode for community inbox (content message+pro)") { + session_protocol_encoded_for_destination encoded = + session_protocol_encode_for_community_inbox( + protobuf_content.plaintext.data(), + protobuf_content.plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + timestamp_ms.time_since_epoch().count(), + &recipient_pubkey, + &community_pubkey, + user_pro_ed_sk.data(), + user_pro_ed_sk.size(), + error, + sizeof(error)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO(error); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(decoded.has_pro); + REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); + } + + SECTION("Encode/decode for community inbox (envelope)") { + // TODO: For these tests we call directly into the encode for destination function. The + // public helper-functions that wrap over this lower level function construct content + // community messages. This is the default behaviour before we transition to envelopes for + // community messages. + // + // To test envelope construction for communities we bypass that function as we need to set + // the type to Envelope community inbox. + session_protocol_destination dest = {}; + dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX; + dest.sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); + dest.recipient_pubkey = session_blind15_pk1; + dest.community_inbox_server_pubkey = community_pubkey; + + // Build content without pro attached + std::string plaintext; + { + SessionProtos::Content content = {}; + content.set_sigtimestamp(timestamp_ms.time_since_epoch().count()); + + SessionProtos::DataMessage* data = content.mutable_datamessage(); + data->set_body(std::string(data_body)); + plaintext = content.SerializeAsString(); + REQUIRE(plaintext.size() > data_body.size()); + } + + memset(error, 0, sizeof(error)); + session_protocol_encoded_for_destination encoded = session_protocol_encode_for_destination( + plaintext.data(), + plaintext.size(), + keys.ed_sk0.data(), + keys.ed_sk0.size(), + &dest, error, sizeof(error)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + REQUIRE(encoded.success); + + memset(error, 0, sizeof(error)); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + nullptr, + 0, + error, + sizeof(error)); + INFO("ERROR: " << error << ", cipher was: " << encoded.ciphertext.size << "b"); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; REQUIRE(!decoded.has_pro); } + + SECTION("Encode/decode for community inbox (envelope+pro)") { + session_protocol_destination dest = {}; + dest.type = SESSION_PROTOCOL_DESTINATION_TYPE_ENVELOPE_COMMUNITY_INBOX; + dest.pro_rotating_ed25519_privkey = user_pro_ed_sk.data(); + dest.pro_rotating_ed25519_privkey_len = user_pro_ed_sk.size(); + dest.sent_timestamp_ms = timestamp_ms.time_since_epoch().count(); + dest.recipient_pubkey = session_blind15_pk1; + dest.community_inbox_server_pubkey = community_pubkey; + + memset(error, 0, sizeof(error)); + session_protocol_encoded_for_destination encoded = 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)); + scope_exit encoded_free{[&]() { session_protocol_encode_for_destination_free(&encoded); }}; + REQUIRE(encoded.success); + + memset(error, 0, sizeof(error)); + session_protocol_decoded_community_message decoded = + session_protocol_decode_for_community_inbox( + keys.ed_sk1.data(), + keys.ed_sk1.size(), + community_pk.data(), + community_pk.size(), + /*sender*/ session_blind15_pk0.data, + sizeof(session_blind15_pk0.data), + /*recipient*/ session_blind15_pk1.data, + sizeof(session_blind15_pk1.data), + encoded.ciphertext.data, + encoded.ciphertext.size, + timestamp_ms.time_since_epoch().count(), + pro_backend_ed_pk.data(), + pro_backend_ed_pk.size(), + error, + sizeof(error)); + INFO("ERROR: " << error << ", cipher was: " << encoded.ciphertext.size << "b"); + REQUIRE(decoded.error_len_incl_null_terminator == 0); + REQUIRE(decoded.success); + scope_exit decoded_free{[&]() { session_protocol_decode_for_community_free(&decoded); }}; + REQUIRE(decoded.has_pro); + REQUIRE(decoded.pro.status == SESSION_PROTOCOL_PRO_STATUS_VALID); + } } diff --git a/tests/utils.hpp b/tests/utils.hpp index 4cd8f0c7..ce801905 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -190,12 +190,3 @@ static inline TestKeys get_deterministic_test_keys() { // clang-format on return result; } - -struct scope_exit { - explicit scope_exit(std::function func) : cleanup(func) {} - std::function cleanup; - ~scope_exit() { - if (cleanup) - cleanup(); - } -};