[
  {
    "path": ".github/actions/prepare-build/action.yaml",
    "content": "name: Install build prerequisites\n\ninputs:\n  runner:\n    description: The runner on which the action is being run\n    required: true\n  crypto:\n    description: The crypto library being used\n    required: true\n  cache-dir:\n    description: Where to put vcpkg cache\n    required: true\n  make-args:\n    description: Additional arguments to pass to make to configure the build\n    required: true\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Capture vcpkg revision for use in cache key\n      shell: bash\n      run: |\n        git -C cpp/vcpkg rev-parse HEAD > cpp/vcpkg_commit.txt\n\n    - name: Restore cache\n      id: cache-vcpkg-restore\n      uses: actions/cache/restore@v4\n      with:\n        path: ${{ inputs.cache-dir }}\n        key: vcpkg-${{ inputs.runner }}-${{ inputs.crypto }}-v03-${{ hashFiles('vcpkg_commit', 'cpp/vcpkg-alts/*') }}\n        restore-keys: |\n          vcpkg-${{ inputs.runner }}-${{ inputs.crypto }}\n          v02-vcpkg-${{ inputs.runner }}-${{ inputs.crypto }}\n\n    - name: vcpkg bootstrap (macOS/Linux)\n      if: ${{ runner.os == 'macOS' || runner.os == 'Linux' }}\n      shell: bash\n      run: |\n        ./cpp/vcpkg/bootstrap-vcpkg.sh\n\n    - name: vcpkg bootstrap (Windows)\n      if: ${{ runner.os == 'Windows' }}\n      shell: cmd\n      run: cpp\\vcpkg\\bootstrap-vcpkg.bat\n\n    - name: Install dependencies (macOS)\n      if: ${{ runner.os == 'macOS' }}\n      shell: bash\n      run: |\n        brew install go nasm\n         \n    - name: Set CC and CXX environment variables (macOS)\n      if: ${{ runner.os == 'macOS' }}\n      shell: bash\n      run: |\n        echo \"CC=$(brew --prefix llvm@18)/bin/clang\" >> $GITHUB_ENV\n        echo \"CXX=$(brew --prefix llvm@18)/bin/clang++\" >> $GITHUB_ENV\n\n    - name: Update dependencies (act)\n      if: ${{ runner.os == 'Linux' && env.ACT }}\n      shell: bash\n      run: |\n        sudo apt-get update\n\n    - name: Install dependencies (Ubuntu)\n      if: ${{ runner.os == 'Linux' }}\n      shell: bash\n      run: |\n        sudo apt-get install -y nasm\n\n    - name: Set BUILD_DIR environment variable\n      shell: bash\n      run: |\n        echo \"BUILD_DIR=${{ runner.temp }}/build\" >> $GITHUB_ENV\n\n    - name: Configure build\n      shell: bash\n      working-directory: ./cpp\n      run: make '${{ env.BUILD_DIR }}' BUILD_DIR='${{ env.BUILD_DIR }}' ${{ inputs.make-args }}\n\n    - name: Cache vckpg\n      if: steps.cache-vcpkg-restore.outputs.cache-hit != 'true'\n      uses: actions/cache/save@v4\n      with:\n        key: ${{ steps.cache-vcpkg-restore.outputs.cache-primary-key }}\n        path: ${{ inputs.cache-dir }}\n\n"
  },
  {
    "path": ".github/workflows/main.yaml",
    "content": "name: cpp\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"**\"]\n\nenv:\n  CMAKE_BUILD_PARALLEL_LEVEL: 3\n  CMAKE_TOOLCHAIN_FILE: ${{ github.workspace }}/cpp/vcpkg/scripts/buildsystems/vcpkg.cmake\n  VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg_cache,readwrite\n  VCPKG_CACHE_DIR: ${{ github.workspace }}/vcpkg_cache\n\ndefaults:\n  run:\n    working-directory: ./cpp\n    shell: bash\n\njobs:\n  debug-test:\n    strategy:\n      matrix:\n        runner: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest]\n        crypto: [openssl_3, openssl_1.1, boringssl]\n      fail-fast: false\n\n    runs-on: ${{matrix.runner}}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: \"recursive\"\n          fetch-depth: 0\n\n      - uses: ./.github/actions/prepare-build\n        with:\n          runner: ${{ matrix.runner }}\n          crypto: ${{ matrix.crypto }}\n          cache-dir: ${{ env.VCPKG_CACHE_DIR }}\n          make-args: BUILD_TYPE=Debug SSL=${{ matrix.crypto }} BUILD_SHARED_LIBS=ON TESTING=ON SANITIZERS=ON MSVC_RUNTIME_LIBRARY=MultiThreadedDebug\n\n      - name: build libdave\n        run: make dev-sanitizers BUILD_DIR='${{ env.BUILD_DIR }}'\n\n      - name: test\n        run: make dtest BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Debug\n\n      - name: test-capi\n        run: make dtest-capi BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Debug\n\n  release-build:\n    strategy:\n      matrix:\n        runner: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest]\n        crypto: [boringssl]\n      fail-fast: false\n\n    runs-on: ${{matrix.runner}}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: \"recursive\"\n          fetch-depth: 0\n\n      - uses: ./.github/actions/prepare-build\n        with:\n          runner: ${{ matrix.runner }}\n          crypto: ${{ matrix.crypto }}\n          cache-dir: ${{ env.VCPKG_CACHE_DIR }}\n          make-args: BUILD_TYPE=Release SSL=${{ matrix.crypto }} BUILD_SHARED_LIBS=ON TESTING=ON INSTALL_VCPKG_LICENSES=ON MSVC_RUNTIME_LIBRARY=MultiThreaded\n\n      - name: build libdave\n        run: make all BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Release\n\n      - name: run tests\n        if: ${{ runner.os != 'Windows' }}\n        run: make dtest BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Release\n\n      - name: run C api tests\n        run: make dtest-capi BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Release\n\n      - name: prepare artifacts (install)\n        run: make install BUILD_DIR='${{ env.BUILD_DIR }}' BUILD_TYPE=Release\n\n      - name: check licenses\n        run: ls -la '${{ env.BUILD_DIR }}/install/licenses' | grep -q \"boringssl\\|openssl\"\n\n      - name: upload build artifacts\n        uses: actions/upload-artifact@v6\n        if: ${{ github.event_name == 'push' }}\n        with:\n          path: ${{ env.BUILD_DIR }}/install\n          name: libdave-${{ runner.os }}-${{ runner.arch }}-${{ matrix.crypto }}\n          if-no-files-found: error"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"cpp/vcpkg\"]\n\tpath = cpp/vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "At this time we are not taking pull requests to this repository. We welcome reports and suggestions via Github Issues.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Discord\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "## libdave\n\nThis repository contains the JS and C++ libraries which together implement Discord's Audio & Video End-to-End Encryption (DAVE) protocol. These libraries are leveraged by Discord's native clients to support the DAVE protocol.\n\nThe DAVE protocol is described in detail in the [protocol whitepaper](https://github.com/discord/dave-protocol).\n\nSee the [cpp README](/cpp/README.md) or the [js README](/js/README.md) for information specific to each library."
  },
  {
    "path": "cpp/.clang-format",
    "content": "---\nAccessModifierOffset: -4\nAlignAfterOpenBracket: true\nAlignConsecutiveAssignments: false\nAlignConsecutiveDeclarations: false\nAlignEscapedNewlines: Left\nAlignOperands: false\nAlignTrailingComments: true\nAllowAllParametersOfDeclarationOnNextLine: false\nAllowShortBlocksOnASingleLine: false\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: InlineOnly\nAllowShortIfStatementsOnASingleLine: false\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: true\nBinPackArguments: false\nBinPackParameters: false\nBreakBeforeBinaryOperators: None\nBreakBeforeBraces: Stroustrup\nBreakBeforeInheritanceComma: true\nBreakBeforeTernaryOperators: true\nBreakConstructorInitializers: BeforeComma\nBreakStringLiterals: true\nColumnLimit: 100\nCommentPragmas: \"\"\nCompactNamespaces: false\nConstructorInitializerAllOnOneLineOrOnePerLine: false\nConstructorInitializerIndentWidth: 2\nContinuationIndentWidth: 2\nCpp11BracedListStyle: true\nDerivePointerAlignment: false\nDisableFormat: false\nFixNamespaceComments: true\nForEachMacros: []\nIndentCaseLabels: false\nIncludeBlocks: Preserve\nIncludeCategories:\n  - Regex: \"^<(W|w)indows.h>\"\n    Priority: 1\n  - Regex: \"^<\"\n    Priority: 2\n  - Regex: \".*\"\n    Priority: 3\nIncludeIsMainRegex: \"(_test|_win|_linux|_mac|_ios|_osx|_null)?$\"\nIndentPPDirectives: None\nIndentWidth: 4\nIndentWrappedFunctionNames: false\nKeepEmptyLinesAtTheStartOfBlocks: false\nMacroBlockBegin: \"\"\nMacroBlockEnd: \"\"\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: None\nPenaltyBreakAssignment: 0\nPenaltyBreakBeforeFirstCallParameter: 1\nPenaltyBreakComment: 300\nPenaltyBreakFirstLessLess: 120\nPenaltyBreakString: 1000\nPenaltyExcessCharacter: 1000000\nPenaltyReturnTypeOnItsOwnLine: 9999999\nPointerAlignment: Left\nReflowComments: true\nSortIncludes: true\nSortUsingDeclarations: true\nSpaceAfterCStyleCast: false\nSpaceAfterTemplateKeyword: true\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeParens: ControlStatements\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 1\nSpacesInAngles: false\nSpacesInCStyleCastParentheses: false\nSpacesInContainerLiterals: true\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStandard: Cpp11\nTabWidth: 4\nUseTab: Never\n"
  },
  {
    "path": "cpp/.gitignore",
    "content": "build/"
  },
  {
    "path": "cpp/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.20)\n\nproject(\n  libdave\n  VERSION 1.0\n  LANGUAGES CXX C\n)\n\noption(REQUIRE_BORINGSSL \"Require BoringSSL instead of OpenSSL\" OFF)\noption(TESTING \"Build tests\" OFF)\noption(PERSISTENT_KEYS \"Enable storage of persistent signature keys\" OFF)\noption(BUILD_SHARED_LIBS \"Build shared libraries\" OFF)\noption(ENABLE_SANITIZERS \"Enable address and undefined behavior sanitizers\" OFF)\noption(INSTALL_VCPKG_LICENSES \"Installs license files from vcpkg deps which require it\" OFF)\n\ninclude(CheckCXXCompilerFlag)\ninclude(CMakeFindDependencyMacro)\n\nset(CMAKE_CXX_STANDARD 17)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\nset(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n\nif (CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n  add_compile_options(-Wall -pedantic -Wextra -Werror -Wimplicit-int-conversion)\nelseif (CMAKE_CXX_COMPILER_ID MATCHES \"GNU\")\n  add_compile_options(-Wall -pedantic -Wextra -Werror)\nelseif(MSVC)\n  add_compile_options(/W4 /WX)\n  add_definitions(-DWINDOWS)\n\n  # MSVC helpfully recommends safer equivalents for things like\n  # getenv, but they are not portable.\n  add_definitions(-D_CRT_SECURE_NO_WARNINGS)\nendif()\n\n# Configure sanitizers\nif (ENABLE_SANITIZERS)\n  if (NOT ${CMAKE_BUILD_TYPE} STREQUAL \"Debug\")\n    message(FATAL_ERROR \"Sanitizers are only supported for Debug builds\")\n  endif()\n\n  if (CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" OR CMAKE_CXX_COMPILER_ID MATCHES \"GNU\")\n    set(SANITIZER_FLAGS \"-fsanitize=address,undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls\")\n    set(CMAKE_C_FLAGS \"${CMAKE_C_FLAGS} ${SANITIZER_FLAGS}\")\n    set(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} ${SANITIZER_FLAGS}\")\n    set(CMAKE_EXE_LINKER_FLAGS \"${CMAKE_EXE_LINKER_FLAGS} ${SANITIZER_FLAGS}\")\n    set(CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_SHARED_LINKER_FLAGS} ${SANITIZER_FLAGS}\")\n    message(STATUS \"Sanitizers enabled: address, undefined\")\n  elseif(MSVC)\n    set(SANITIZER_FLAGS \"/fsanitize=address /Zi\")\n    set(CMAKE_C_FLAGS \"${CMAKE_C_FLAGS} ${SANITIZER_FLAGS}\")\n    set(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} ${SANITIZER_FLAGS}\")\n    # Disable STL container annotations to avoid mismatch with dependencies built without ASAN\n    add_definitions(-D_DISABLE_STRING_ANNOTATION=1 -D_DISABLE_VECTOR_ANNOTATION=1)\n    # Find ASAN runtime DLL for copying to test directories\n    get_filename_component(COMPILER_DIR \"${CMAKE_CXX_COMPILER}\" DIRECTORY)\n    set(ASAN_RUNTIME_DLL \"${COMPILER_DIR}/clang_rt.asan_dynamic-x86_64.dll\" CACHE FILEPATH \"Path to ASAN runtime DLL\")\n    if(EXISTS \"${ASAN_RUNTIME_DLL}\")\n      message(STATUS \"ASAN runtime DLL: ${ASAN_RUNTIME_DLL}\")\n    else()\n      message(WARNING \"ASAN runtime DLL not found at ${ASAN_RUNTIME_DLL}\")\n    endif()\n    message(STATUS \"Sanitizers enabled: address\")\n  endif()\nendif()\n\nfind_package(OpenSSL REQUIRED)\nif (OPENSSL_FOUND)\n  find_path(BORINGSSL_INCLUDE_DIR openssl/is_boringssl.h HINTS ${OPENSSL_INCLUDE_DIR} NO_DEFAULT_PATH)\n\n  if (BORINGSSL_INCLUDE_DIR)\n    message(STATUS \"Found OpenSSL includes are for BoringSSL\")\n    \n    add_compile_definitions(WITH_BORINGSSL)\n\n    if (CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" OR CMAKE_CXX_COMPILER_ID MATCHES \"GNU\")\n      add_compile_options(-Wno-gnu-anonymous-struct -Wno-nested-anon-types)\n    endif ()\n\n    file(STRINGS \"${OPENSSL_INCLUDE_DIR}/openssl/crypto.h\" boringssl_version_str\n          REGEX \"^#[\\t ]*define[\\t ]+OPENSSL_VERSION_TEXT[\\t ]+\\\"OpenSSL ([0-9])+\\\\.([0-9])+\\\\.([0-9])+ .+\")\n    \n    string(REGEX REPLACE \"^.*OPENSSL_VERSION_TEXT[\\t ]+\\\"OpenSSL ([0-9]+\\\\.[0-9]+\\\\.[0-9])+ .+$\"\n            \"\\\\1\" OPENSSL_VERSION \"${boringssl_version_str}\")\n\n  elseif (REQUIRE_BORINGSSL)\n    message(FATAL_ERROR \"BoringSSL required but not found\")\n  endif ()\n\n  if (${OPENSSL_VERSION} VERSION_GREATER_EQUAL 3)\n    add_compile_definitions(WITH_OPENSSL3)\n  elseif(${OPENSSL_VERSION} VERSION_LESS 1.1.1)\n    message(FATAL_ERROR \"OpenSSL 1.1.1 or greater is required\")\n  endif()\n\n  message(STATUS \"OpenSSL Found: ${OPENSSL_VERSION}\")\n  message(STATUS \"OpenSSL Include: ${OPENSSL_INCLUDE_DIR}\")\n  message(STATUS \"OpenSSL Libraries: ${OPENSSL_LIBRARIES}\")\nelse()\n  message(FATAL_ERROR \"No OpenSSL library found\")\nendif()\n\nfind_package(nlohmann_json REQUIRED)\nfind_dependency(MLSPP REQUIRED)\n\nset(CMAKE_STATIC_LIBRARY_PREFIX \"\")\n\nSET(LIB_NAME ${PROJECT_NAME})\nfile(GLOB_RECURSE LIB_HEADERS CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h\" \"${CMAKE_CURRENT_SOURCE_DIR}/includes/*.h\")\nfile(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp\")\n\n# remove all of the persistent key files\nlist(FILTER LIB_SOURCES EXCLUDE REGEX \".*persisted_key.*\")\n\nif (PERSISTENT_KEYS)\n  # persistent keys enabled\n  list(APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/persisted_key_pair.cpp\")\n\n  if (APPLE)\n    # Apple has its own native and generic implementation, we just add the _apple.cpp file\n    list(APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/detail/persisted_key_pair_apple.cpp\")\n  else ()\n    # Other platforms share the generic implementation\n    list(APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/detail/persisted_key_pair_generic.cpp\")\n\n    if (WIN32)\n      # Windows has a native implementation\n      list(APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/detail/persisted_key_pair_win.cpp\")\n    else ()\n      # We don't have a native implementation, so we include the nullified native\n      list(APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/detail/persisted_key_pair_null.cpp\")\n    endif ()\n  endif ()\n  \nelse ()\n  # not using persistent keys, so we just need to add the null implementation\n  list (APPEND LIB_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/src/mls/persisted_key_pair_null.cpp\")\nendif ()\n\nif (NOT WIN32)\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*_win.cpp\")\nendif ()\n\nif (NOT APPLE)\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*_apple.cpp\")\nendif ()\n\nif (NOT DEFINED EMSCRIPTEN)\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*_wasm.cpp\")\nelse()\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*_capi.cpp\")\nendif()\n\nif (BORINGSSL_INCLUDE_DIR)\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*openssl_cryptor.*\")\nelse ()\n  list(FILTER LIB_SOURCES EXCLUDE REGEX \".*boringssl_cryptor.*\")\nendif()\n\nif (DEFINED EMSCRIPTEN)\n  add_executable(${LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES})\n\n  set(OPTIMIZATION \"-O3\")\n  set(CONFIG \"-sWASM=1 -sWASM_BIGINT -sENVIRONMENT=web -sMODULARIZE -sALLOW_MEMORY_GROWTH\")\n  set(EXPORTS \"-sEXPORT_ES6=1 -sEXPORT_NAME=DaveModuleFactory -sEXPORTED_RUNTIME_METHODS='[\\\"ccall\\\"]' -sEXPORTED_FUNCTIONS='[\\\"_malloc\\\", \\\"_free\\\"]'\")\n\n  set(COMPILE_FLAGS \"${OPTIMIZATION}\")\n  set(LINK_FLAGS \"${OPTIMIZATION} ${CONFIG} ${EXPORTS} -lembind --no-entry --whole-archive --emit-tsd libdave.d.ts\")\n\n  set_target_properties(${LIB_NAME} PROPERTIES COMPILE_FLAGS \"${COMPILE_FLAGS}\")\n  set_target_properties(${LIB_NAME} PROPERTIES LINK_FLAGS    \"${LINK_FLAGS}\")\nelse()\n  add_library(${LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES})\n\n  if (BUILD_SHARED_LIBS AND NOT WIN32)\n    # Whithout this the resulting file is called liblibdave.dylib\n    set_target_properties(${LIB_NAME} PROPERTIES OUTPUT_NAME dave)\n  endif()\nendif()\n\nif (TESTING)\n  add_subdirectory(test)\nendif()\n\ntarget_include_directories(\n  ${LIB_NAME}\n  PUBLIC\n    $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/includes>\n    $<INSTALL_INTERFACE:include>\n  PRIVATE\n    $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>\n)\n\ntarget_link_libraries(${LIB_NAME} PRIVATE OpenSSL::Crypto)\ntarget_link_libraries(${LIB_NAME} PRIVATE MLSPP::mlspp)\n\nif (APPLE AND PERSISTENT_KEYS)\n  target_link_libraries(${LIB_NAME} PUBLIC \"-framework CoreFoundation\" \"-framework Security\")\nendif()\n\nset(CMAKE_SKIP_INSTALL_ALL_DEPENDENCY ON)\n\ninstall(TARGETS ${LIB_NAME} INCLUDES DESTINATION \"include\")\ninstall(DIRECTORY ${PROJECT_SOURCE_DIR}/includes/ DESTINATION \"include\")\n\nif (INSTALL_VCPKG_LICENSES)\n  set(DEPS_NEEDING_LICENSE mlspp nlohmann-json)\n  if (BORINGSSL_INCLUDE_DIR)\n    list(APPEND DEPS_NEEDING_LICENSE boringssl)\n  else ()\n    list(APPEND DEPS_NEEDING_LICENSE openssl)\n  endif ()\n\n  foreach(DEP_NAME ${DEPS_NEEDING_LICENSE})\n    set(DEP_LICENSE_PATH \"${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/${DEP_NAME}\")\n    if (NOT EXISTS ${DEP_LICENSE_PATH})\n      message(ERROR \"Could not find license file for ${DEP_LICENSE_PATH}\")\n    endif()\n    \n    install(FILES ${DEP_LICENSE_PATH}/copyright DESTINATION \"licenses\" RENAME ${DEP_NAME})\n  endforeach()\nendif()\n\ninstall(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE DESTINATION \"licenses\" RENAME ${LIB_NAME})"
  },
  {
    "path": "cpp/Makefile",
    "content": "# Options\nBUILD_DIR=build\nINSTALL_DIR=${BUILD_DIR}/install\nSANITIZERS=OFF\nBUILD_SHARED_LIBS=OFF\nPERSISTENT_KEYS=OFF\nTESTING=OFF\nSSL=openssl_3\nINSTALL_VCPKG_LICENSES=OFF\n\n# Paths\nBORINGSSL_MANIFEST=vcpkg-alts/boringssl\nOPENSSL_1_1_MANIFEST=vcpkg-alts/openssl_1.1\nOPENSSL_3_MANIFEST=vcpkg-alts/openssl_3\nWASM_MANIFEST=vcpkg-alts/wasm\nTOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake\nEMSCRIPTEN_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake\n\nCLANG_FORMAT=clang-format -i -style=file:.clang-format\n\nDEFAULT_BUILD_TYPE=Debug\nBUILD_TYPE ?= $(DEFAULT_BUILD_TYPE)\nall shared install: DEFAULT_BUILD_TYPE=Release\n\nifeq ($(SSL), boringssl)\n\tSSL_MANIFEST=${BORINGSSL_MANIFEST}\nelse ifeq ($(SSL), openssl_1.1)\n\tSSL_MANIFEST=${OPENSSL_1_1_MANIFEST}\nelse ifeq ($(SSL), openssl_3)\n\tSSL_MANIFEST=${OPENSSL_3_MANIFEST}\nelse\n\t$(error Invalid SSL option: $(SSL))\nendif\n\nifeq ($(OS), Windows_NT)\n\tEXTRA_FLAGS=-DVCPKG_TARGET_TRIPLET=x64-windows-static \\\n\t\t-DVCPKG_TARGET_ARCHITECTURE=x86_64\n\tifeq ($(BUILD_TYPE), Debug)\n\t\tEXTRA_FLAGS+=-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON\n\tendif\n\tifdef MSVC_RUNTIME_LIBRARY\n\t\tEXTRA_FLAGS+=-DCMAKE_MSVC_RUNTIME_LIBRARY=${MSVC_RUNTIME_LIBRARY}\n\tendif\nendif\n\nall: ${BUILD_DIR}\n\tcmake --build ${BUILD_DIR} --target libdave --config ${BUILD_TYPE}\n\n${BUILD_DIR}: CMakeLists.txt test/CMakeLists.txt test/capi/CMakeLists.txt\n\tcmake -B${BUILD_DIR}  \\\n\t\t-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \\\n\t\t-DVCPKG_MANIFEST_DIR=${SSL_MANIFEST} \\\n\t\t-DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE} \\\n\t\t-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} \\\n\t\t-DENABLE_SANITIZERS=${SANITIZERS} \\\n\t\t-DPERSISTENT_KEYS=${PERSISTENT_KEYS} \\\n\t\t-DTESTING=${TESTING} \\\n\t\t-DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} \\\n\t\t-DINSTALL_VCPKG_LICENSES=${INSTALL_VCPKG_LICENSES} \\\n\t\t${EXTRA_FLAGS}\n\ninstall: ${BUILD_DIR}\n\tcmake --build ${BUILD_DIR} --target install --config ${BUILD_TYPE}\n\nshared:\n\t$(MAKE) all BUILD_SHARED_LIBS=ON\n\ndev:\n\t$(MAKE) all TESTING=ON BUILD_TYPE=$(BUILD_TYPE)\n\ndev-shared:\n\t$(MAKE) dev BUILD_SHARED_LIBS=ON\n\ndev-sanitizers:\n\t$(MAKE) dev SANITIZERS=ON\n\ndevB:\n\t# Like `dev`, but using OpenSSL 1.1\n\t$(MAKE) dev SSL=openssl_1.1\n\ndevC:\n\t# Like `dev`, but using BoringSSL\n\t$(MAKE) dev SSL=boringssl\n\nwasm: check-emsdk\n\temcmake cmake -B${BUILD_DIR} -DCMAKE_BUILD_TYPE=Release \\\n\t\t-DVCPKG_MANIFEST_DIR=${WASM_MANIFEST} \\\n\t\t-DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE} \\\n\t\t-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=${EMSCRIPTEN_TOOLCHAIN_FILE} \\\n\t\t-DVCPKG_TARGET_TRIPLET=wasm32-emscripten\n\tcmake --build ${BUILD_DIR} --target libdave --config ${BUILD_TYPE}\n\ncheck-emsdk:\n\t@if [ -z \"$$EMSDK\" ]; then \\\n\t\techo \"Error: EMSDK environment variable is not set\"; \\\n\t\techo \"Please set it to your emsdk installation directory\"; \\\n\t\techo \"Example: export EMSDK=/path/to/emsdk\"; \\\n\t\texit 1; \\\n\tfi\n\ntest: dev test/*\n\tcmake --build ${BUILD_DIR} --target libdave_test --config ${BUILD_TYPE}\n\ntest-capi: dev test/capi/*\n\tcmake --build ${BUILD_DIR} --target capi_test --config ${BUILD_TYPE}\n\ntest-sanitizers: dev-sanitizers test/*\n\tcmake --build ${BUILD_DIR} --target libdave_test --config ${BUILD_TYPE}\n\ntest-capi-sanitizers: dev-sanitizers test/capi/*\n\tcmake --build ${BUILD_DIR} --target capi_test --config ${BUILD_TYPE}\n\ndtest: test\nifeq ($(OS), Windows_NT)\n\t${BUILD_DIR}/test/${BUILD_TYPE}/libdave_test.exe\nelse\n\t${BUILD_DIR}/test/libdave_test\nendif\n\ndtest-capi: test-capi\nifeq ($(OS), Windows_NT)\n\t${BUILD_DIR}/test/capi/${BUILD_TYPE}/capi_test.exe\nelse\n\t${BUILD_DIR}/test/capi/capi_test\nendif\n\ndtest-sanitizers: test-sanitizers\nifeq ($(OS), Windows_NT)\n\t${BUILD_DIR}/test/${BUILD_TYPE}/libdave_test.exe\nelse\n\t${BUILD_DIR}/test/libdave_test\nendif\n\ndtest-capi-sanitizers: test-capi-sanitizers\nifeq ($(OS), Windows_NT)\n\t${BUILD_DIR}/test/capi/${BUILD_TYPE}/capi_test.exe\nelse\n\t${BUILD_DIR}/test/capi/capi_test\nendif\n\ndbtest: test\n\tlldb ${BUILD_DIR}/test/libdave_test\n\ndbtest-capi: test-capi\n\tlldb ${BUILD_DIR}/test/capi/capi_test\n\nctest: test\n\tcmake --build ${BUILD_DIR} --target test --config ${BUILD_TYPE}\n\nclean:\n\tcmake --build ${BUILD_DIR} --target clean\n\ncclean:\nifeq ($(OS), Windows_NT)\n\tif exist ${BUILD_DIR} rmdir /s /q ${BUILD_DIR}\nelse\n\trm -rf ${BUILD_DIR}\nendif\n\nformat:\n\tfind src -iname \"*.h\" -or -iname \"*.cpp\" -or -iname \"*.c\" | xargs ${CLANG_FORMAT}\n\tfind test -iname \"*.h\" -or -iname \"*.cpp\" -or -iname \"*.c\" | xargs ${CLANG_FORMAT}\n"
  },
  {
    "path": "cpp/README.md",
    "content": "## libdave C++\n\nContains the libdave C++ library, which handles the bulk of the DAVE protocol implementation for Discord's native clients.\n\n### Dependencies\n\n- [mlspp](https://github.com/cisco/mlspp)\n  - Configured with `-DMLS_CXX_NAMESPACE=\"mlspp\"` and `-DDISABLE_GREASE=ON`\n- One of the supported SSL backends:\n  - [OpenSSL 1.1 or 3.0](https://github.com/openssl/openssl)\n  - [boringssl](https://boringssl.googlesource.com/boringssl)\n\n#### Testing\n\n- [googletest](https://github.com/google/googletest)\n- [AFLplusplus](https://github.com/AFLplusplus/AFLplusplus)\n\n\n## Building\n\n### vcpkg\n\nMake sure the vcpkg submodule is up to date and initialized:\n```\ngit submodule update --recursive\n./vcpkg/bootstrap-vcpkg.sh\n```\n\n### Compiling\n\nFor a static library, run:\n```\nmake cclean\nmake\n```\n\nFor a shared library, run:\n```\nmake cclean\nmake shared\n```\n\n### SSL\n\nBy default the library builds with OpenSSL 3, however you can modify `VCPKG_MANIFEST_DIR` in the [Makefile](Makefile) to build with OpenSSL 1.1 or BoringSSL instead."
  },
  {
    "path": "cpp/afl-driver/src/main.cpp",
    "content": "#include <array>\n#include <cassert>\n#include <iostream>\n#include <memory>\n#include <unistd.h>\n\n#include <fuzzer/FuzzedDataProvider.h>\n\n#include \"common.h\"\n#include \"utils/array_view.h\"\n#include \"decryptor.h\"\n\nusing namespace discord::dave;\n\nextern \"C\" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)\n{\n    FuzzedDataProvider provider(data, size);\n    MediaType mediaType = static_cast<MediaType>(provider.ConsumeIntegralInRange(0, 2));\n    const auto InFrame = provider.ConsumeRemainingBytes<uint8_t>();\n\n    Decryptor decryptor;\n    const auto OutFrameSize = decryptor.GetMaxPlaintextByteSize(mediaType, InFrame.size());\n    auto outFrame = std::make_unique<uint8_t[]>(OutFrameSize);\n    [[maybe_unused]] auto res = decryptor.Decrypt(mediaType,\n                                                  MakeArrayView(InFrame.data(), InFrame.size()),\n                                                  MakeArrayView(outFrame.get(), OutFrameSize));\n    return 0;\n}\n"
  },
  {
    "path": "cpp/includes/dave/array_view.h",
    "content": "#pragma once\n\n#include <cassert>\n#include <cstddef>\n#include <vector>\n\nnamespace discord {\nnamespace dave {\n\ntemplate <typename T>\nclass ArrayView {\npublic:\n    ArrayView() = default;\n    ArrayView(T* data, size_t size)\n      : data_(data)\n      , size_(size)\n    {\n    }\n\n    size_t size() const { return size_; }\n    T* data() const { return data_; }\n\n    T* begin() const { return data_; }\n    T* end() const { return data_ + size_; }\n\nprivate:\n    T* data_ = nullptr;\n    size_t size_ = 0;\n};\n\ntemplate <typename T>\ninline ArrayView<T> MakeArrayView(T* data, size_t size)\n{\n    return ArrayView<T>(data, size);\n}\n\ntemplate <typename T>\ninline ArrayView<T> MakeArrayView(std::vector<T>& data)\n{\n    return ArrayView<T>(data.data(), data.size());\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/includes/dave/dave.h",
    "content": "/**\n * @file dave.h\n * @brief DAVE (Discord Audio/Video Encryption) C API\n *\n * This header provides the C API for end-to-end encryption of audio and video\n * streams using the DAVE protocol.\n *\n * All handles are opaque pointers that must be created and destroyed using\n * the corresponding API functions. Memory management rules:\n * - Handles from *Create functions must be freed with their *Destroy counterpart\n * - Output handles should be destroyed by the caller using the corresponding *Destroy function\n * - Functions do not take ownership of the input data unless otherwise specified\n * - Output byte arrays allocated by the library must be freed using daveFree()\n */\n\n#pragma once\n\n#include <stddef.h>\n#include <stdint.h>\n#include <stdbool.h>\n\n#if (defined(_WIN32) || defined(_WIN64))\n#define DAVE_EXPORT __declspec(dllexport)\n#else\n#define DAVE_EXPORT __attribute__((visibility(\"default\")))\n#endif\n\n#define DECLARE_OPAQUE_HANDLE(x) typedef struct x##_s* x\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n/** @brief DAVE session handle for managing group encryption state */\nDECLARE_OPAQUE_HANDLE(DAVESessionHandle);\n/** @brief Result handle from processing an MLS commit message */\nDECLARE_OPAQUE_HANDLE(DAVECommitResultHandle);\n/** @brief Result handle from processing an MLS welcome message */\nDECLARE_OPAQUE_HANDLE(DAVEWelcomeResultHandle);\n/** @brief Key ratchet handle for deriving encryption keys */\nDECLARE_OPAQUE_HANDLE(DAVEKeyRatchetHandle);\n/** @brief Media frame encryptor handle */\nDECLARE_OPAQUE_HANDLE(DAVEEncryptorHandle);\n/** @brief Media frame decryptor handle */\nDECLARE_OPAQUE_HANDLE(DAVEDecryptorHandle);\n\n/**\n * @brief Supported media codecs for encryption\n */\ntypedef enum {\n    DAVE_CODEC_UNKNOWN = 0, /**< Unknown or unspecified codec */\n    DAVE_CODEC_OPUS = 1,    /**< Opus audio codec */\n    DAVE_CODEC_VP8 = 2,     /**< VP8 video codec */\n    DAVE_CODEC_VP9 = 3,     /**< VP9 video codec */\n    DAVE_CODEC_H264 = 4,    /**< H.264/AVC video codec */\n    DAVE_CODEC_H265 = 5,    /**< H.265/HEVC video codec */\n    DAVE_CODEC_AV1 = 6      /**< AV1 video codec */\n} DAVECodec;\n\n/**\n * @brief Media stream type classification\n */\ntypedef enum {\n    DAVE_MEDIA_TYPE_AUDIO = 0, /**< Audio stream */\n    DAVE_MEDIA_TYPE_VIDEO = 1  /**< Video stream */\n} DAVEMediaType;\n\n/**\n * @brief Result codes returned by encryption operations\n */\ntypedef enum {\n    DAVE_ENCRYPTOR_RESULT_CODE_SUCCESS = 0,            /**< Encryption succeeded */\n    DAVE_ENCRYPTOR_RESULT_CODE_ENCRYPTION_FAILURE = 1, /**< Encryption failed */\n    DAVE_ENCRYPTOR_RESULT_CODE_MISSING_KEY_RATCHET = 2,/**< No key ratchet available */\n    DAVE_ENCRYPTOR_RESULT_CODE_MISSING_CRYPTOR = 3,    /**< Missing cryptographic context */\n    DAVE_ENCRYPTOR_RESULT_CODE_TOO_MANY_ATTEMPTS = 4,  /**< Too many attempts to encrypt the frame failed */\n} DAVEEncryptorResultCode;\n\n/**\n * @brief Result codes returned by decryption operations\n */\ntypedef enum {\n    DAVE_DECRYPTOR_RESULT_CODE_SUCCESS = 0,            /**< Decryption succeeded */\n    DAVE_DECRYPTOR_RESULT_CODE_DECRYPTION_FAILURE = 1, /**< Decryption failed */\n    DAVE_DECRYPTOR_RESULT_CODE_MISSING_KEY_RATCHET = 2,/**< No key ratchet available */\n    DAVE_DECRYPTOR_RESULT_CODE_INVALID_NONCE = 3,      /**< Invalid nonce in encrypted frame */\n    DAVE_DECRYPTOR_RESULT_CODE_MISSING_CRYPTOR = 4     /**< Missing cryptographic context */\n} DAVEDecryptorResultCode;\n\n/**\n * @brief Log message severity levels\n */\ntypedef enum {\n    DAVE_LOGGING_SEVERITY_VERBOSE = 0, /**< Verbose debug information */\n    DAVE_LOGGING_SEVERITY_INFO = 1,    /**< Informational messages */\n    DAVE_LOGGING_SEVERITY_WARNING = 2, /**< Warning messages */\n    DAVE_LOGGING_SEVERITY_ERROR = 3,   /**< Error messages */\n    DAVE_LOGGING_SEVERITY_NONE = 4     /**< Messages to be ignored */\n} DAVELoggingSeverity;\n\n/**\n * @brief Callback invoked when an MLS protocol failure occurs\n * @param source The source/component where the failure occurred\n * @param reason Human-readable failure reason\n * @param userData User-provided context pointer\n */\ntypedef void (*DAVEMLSFailureCallback)(const char* source, const char* reason, void* userData);\n\n/**\n * @brief Callback invoked with the computed pairwise fingerprint for identity verification\n * @param fingerprint Pointer to fingerprint bytes (freed by the library after the callback returns)\n * @param length Length of fingerprint in bytes\n * @param userData User-provided context pointer\n */\ntypedef void (*DAVEPairwiseFingerprintCallback)(const uint8_t* fingerprint, size_t length, void* userData);\n\n/**\n * @brief Callback invoked when the encryptor's protocol version changes\n * @param userData User-provided context pointer\n */\ntypedef void (*DAVEEncryptorProtocolVersionChangedCallback)(void* userData);\n\n/**\n * @brief Custom log sink callback for receiving library log messages\n * @param severity Log severity level\n * @param file Source file name where log originated (freed by the library after the callback returns)\n * @param line Line number in source file\n * @param message Log message text (freed by the library after the callback returns)\n */\ntypedef void (*DAVELogSinkCallback)(DAVELoggingSeverity severity,\n                                    const char* file,\n                                    int line,\n                                    const char* message);\n\n/**\n * @brief Statistics for encryption operations\n */\ntypedef struct DAVEEncryptorStats {\n    uint64_t passthroughCount;      /**< Frames passed through without encryption */\n    uint64_t encryptSuccessCount;   /**< Successful encryption count */\n    uint64_t encryptFailureCount;   /**< Failed encryption count */\n    uint64_t encryptDuration;       /**< Total encryption duration */\n    uint64_t encryptAttempts;       /**< Total encryption attempts */\n    uint64_t encryptMaxAttempts;    /**< Maximum retry attempts for a single frame */\n    uint64_t encryptMissingKeyCount;/**< Encryptions skipped due to missing key */\n} DAVEEncryptorStats;\n\n/**\n * @brief Statistics for decryption operations\n */\ntypedef struct DAVEDecryptorStats {\n    uint64_t passthroughCount;        /**< Frames passed through without decryption */\n    uint64_t decryptSuccessCount;     /**< Successful decryption count */\n    uint64_t decryptFailureCount;     /**< Failed decryption count */\n    uint64_t decryptDuration;         /**< Total decryption duration */\n    uint64_t decryptAttempts;         /**< Total decryption attempts */\n    uint64_t decryptMissingKeyCount;  /**< Decryptions failed due to missing key */\n    uint64_t decryptInvalidNonceCount;/**< Decryptions failed due to invalid nonce */\n} DAVEDecryptorStats;\n\n\n/*******************************************************************************\n * Version\n ******************************************************************************/\n\n/**\n * @brief Returns the maximum protocol version supported by this library\n * @return Maximum supported protocol version number\n */\nDAVE_EXPORT uint16_t daveMaxSupportedProtocolVersion(void);\n\n/*******************************************************************************\n * Memory Management\n ******************************************************************************/\n\n/**\n * @brief Frees memory allocated by the DAVE library\n *\n * Use this function to free any byte arrays or buffers allocated by DAVE API\n * functions.\n *\n * @param ptr Pointer to memory previously allocated by a DAVE API function.\n *            If NULL, this function does nothing.\n *\n * @note This function should be used to free output byte arrays from functions\n *       like daveSessionGetLastEpochAuthenticator, daveSessionGetMarshalledKeyPackage,\n *       daveCommitResultGetRosterMemberIds, etc. Do NOT use this to destroy handles;\n *       use the corresponding *Destroy functions instead.\n */\nDAVE_EXPORT void daveFree(void* ptr);\n\n/*******************************************************************************\n * Session Management\n ******************************************************************************/\n\n/**\n * @brief Creates a new DAVE session\n * @param context Currently unused platform-specific context pointer (can be NULL)\n * @param authSessionId String used to manage persistent key lifetimes (can be NULL)\n * @param callback Callback invoked on MLS failures\n * @param userData User data pointer passed to the callback\n * @return New session handle, or NULL on failure. Must be destroyed with daveSessionDestroy()\n */\nDAVE_EXPORT DAVESessionHandle daveSessionCreate(void* context,\n                                                const char* authSessionId,\n                                                DAVEMLSFailureCallback callback,\n                                                void* userData);\n\n/**\n * @brief Destroys a session and frees associated resources\n * @param session Session handle to destroy\n */\nDAVE_EXPORT void daveSessionDestroy(DAVESessionHandle session);\n\n/**\n * @brief Initializes a session with protocol version and group information\n * @param session Session handle\n * @param version Protocol version to use\n * @param groupId Group identifier common to all users in the group\n * @param selfUserId User ID of the local user\n */\nDAVE_EXPORT void daveSessionInit(DAVESessionHandle session,\n                                 uint16_t version,\n                                 uint64_t groupId,\n                                 const char* selfUserId);\n\n/**\n * @brief Resets the session state\n * @param session Session handle\n */\nDAVE_EXPORT void daveSessionReset(DAVESessionHandle session);\n\n/**\n * @brief Sets the protocol version for the session\n * @param session Session handle\n * @param version Protocol version to set\n */\nDAVE_EXPORT void daveSessionSetProtocolVersion(DAVESessionHandle session, uint16_t version);\n\n/**\n * @brief Gets the current protocol version of the session\n * @param session Session handle\n * @return Current protocol version\n */\nDAVE_EXPORT uint16_t daveSessionGetProtocolVersion(DAVESessionHandle session);\n\n/**\n * @brief Retrieves the authenticator from the last MLS epoch\n * @param session Session handle\n * @param[out] authenticator Output pointer to authenticator bytes (caller must free with daveFree)\n * @param[out] length Output pointer to authenticator length\n */\nDAVE_EXPORT void daveSessionGetLastEpochAuthenticator(DAVESessionHandle session,\n                                                      uint8_t** authenticator,\n                                                      size_t* length);\n\n/**\n * @brief Sets the external sender credentials for the session\n * @param session Session handle\n * @param externalSender External sender credential bytes\n * @param length Length of external sender data\n */\nDAVE_EXPORT void daveSessionSetExternalSender(DAVESessionHandle session,\n                                              const uint8_t* externalSender,\n                                              size_t length);\n\n/**\n * @brief Processes MLS proposals and generates commit/welcome messages\n * @param session Session handle\n * @param proposals Serialized proposal bytes\n * @param length Length of proposals\n * @param recognizedUserIds Array of recognized user ID strings\n * @param recognizedUserIdsLength Number of recognized user IDs\n * @param[out] commitWelcomeBytes Output buffer to commit/welcome message bytes (caller must free with daveFree)\n * @param[out] commitWelcomeBytesLength Output length of the commit/welcome message\n */\nDAVE_EXPORT void daveSessionProcessProposals(DAVESessionHandle session,\n                                             const uint8_t* proposals,\n                                             size_t length,\n                                             const char** recognizedUserIds,\n                                             size_t recognizedUserIdsLength,\n                                             uint8_t** commitWelcomeBytes,\n                                             size_t* commitWelcomeBytesLength);\n\n/**\n * @brief Processes an incoming MLS commit message\n * @param session Session handle\n * @param commit Serialized commit message bytes\n * @param length Length of commit message\n * @return Commit result handle. Must be destroyed with daveCommitResultDestroy()\n */\nDAVE_EXPORT DAVECommitResultHandle daveSessionProcessCommit(DAVESessionHandle session,\n                                                            const uint8_t* commit,\n                                                            size_t length);\n\n/**\n * @brief Processes an incoming MLS welcome message to join a group\n * @param session Session handle\n * @param welcome Serialized welcome message bytes\n * @param length Length of welcome message\n * @param recognizedUserIds Array of recognized user ID strings\n * @param recognizedUserIdsLength Number of recognized user IDs\n * @return Welcome result handle. Must be destroyed with daveWelcomeResultDestroy()\n */\nDAVE_EXPORT DAVEWelcomeResultHandle daveSessionProcessWelcome(DAVESessionHandle session,\n                                                              const uint8_t* welcome,\n                                                              size_t length,\n                                                              const char** recognizedUserIds,\n                                                              size_t recognizedUserIdsLength);\n\n/**\n * @brief Gets the marshalled MLS key package for this session\n * @param session Session handle\n * @param[out] keyPackage Output buffer to key package bytes (caller must free with daveFree)\n * @param[out] length Output length of the key package\n */\nDAVE_EXPORT void daveSessionGetMarshalledKeyPackage(DAVESessionHandle session,\n                                                    uint8_t** keyPackage,\n                                                    size_t* length);\n\n/**\n * @brief Gets a key ratchet for a specific user in the session\n * @param session Session handle\n * @param userId User ID to get key ratchet for\n * @return Key ratchet handle. Must be destroyed with daveKeyRatchetDestroy()\n */\nDAVE_EXPORT DAVEKeyRatchetHandle daveSessionGetKeyRatchet(DAVESessionHandle session,\n                                                          const char* userId);\n\n/**\n * @brief Computes a pairwise fingerprint for identity verification with another user\n * @param session Session handle\n * @param version Protocol version currently in use\n * @param userId User ID of the remote user to compute the fingerprint for\n * @param callback Callback to receive the fingerprint\n * @param userData User data passed to callback\n */\nDAVE_EXPORT void daveSessionGetPairwiseFingerprint(DAVESessionHandle session,\n                                                   uint16_t version,\n                                                   const char* userId,\n                                                   DAVEPairwiseFingerprintCallback callback,\n                                                   void* userData);\n\n\n/*******************************************************************************\n * Key Ratchet\n ******************************************************************************/\n\n/**\n * @brief Destroys a key ratchet and frees associated resources\n * @param keyRatchet Key ratchet handle to destroy\n */\nDAVE_EXPORT void daveKeyRatchetDestroy(DAVEKeyRatchetHandle keyRatchet);\n\n/*******************************************************************************\n * Commit Result\n ******************************************************************************/\n\n/**\n * @brief Checks if processing the commit failed\n * @param commitResultHandle Commit result handle\n * @return true if commit processing failed\n */\nDAVE_EXPORT bool daveCommitResultIsFailed(DAVECommitResultHandle commitResultHandle);\n\n/**\n * @brief Checks if the commit should be ignored\n * @param commitResultHandle Commit result handle\n * @return true if commit should be ignored\n */\nDAVE_EXPORT bool daveCommitResultIsIgnored(DAVECommitResultHandle commitResultHandle);\n\n/**\n * @brief Gets the list of member IDs in the roster after the commit\n * @param commitResultHandle Commit result handle\n * @param[out] rosterIds Output buffer to array of roster member IDs (caller must free with daveFree)\n * @param[out] rosterIdsLength Output length of the roster member IDs array\n */\nDAVE_EXPORT void daveCommitResultGetRosterMemberIds(DAVECommitResultHandle commitResultHandle,\n                                                    uint64_t** rosterIds,\n                                                    size_t* rosterIdsLength);\n\n/**\n * @brief Gets the signature for a specific roster member\n * @param commitResultHandle Commit result handle\n * @param rosterId Roster member ID\n * @param[out] signature Output buffer to signature bytes (caller must free with daveFree)\n * @param[out] signatureLength Output length of the signature\n */\nDAVE_EXPORT void daveCommitResultGetRosterMemberSignature(DAVECommitResultHandle commitResultHandle,\n                                                          uint64_t rosterId,\n                                                          uint8_t** signature,\n                                                          size_t* signatureLength);\n\n/**\n * @brief Destroys a commit result and frees associated resources\n * @param commitResultHandle Commit result handle to destroy\n */\nDAVE_EXPORT void daveCommitResultDestroy(DAVECommitResultHandle commitResultHandle);\n\n/*******************************************************************************\n * Welcome Result\n ******************************************************************************/\n\n/**\n * @brief Gets the list of member IDs in the roster from the welcome message\n * @param welcomeResultHandle Welcome result handle\n * @param[out] rosterIds Output buffer to array of roster member IDs (caller must free with daveFree)\n * @param[out] rosterIdsLength Output length of the roster member IDs array\n */\nDAVE_EXPORT void daveWelcomeResultGetRosterMemberIds(DAVEWelcomeResultHandle welcomeResultHandle,\n                                                     uint64_t** rosterIds,\n                                                     size_t* rosterIdsLength);\n\n/**\n * @brief Gets the signature for a specific roster member\n * @param welcomeResultHandle Welcome result handle\n * @param rosterId Roster member ID\n * @param[out] signature Output buffer to signature bytes (caller must free with daveFree)\n * @param[out] signatureLength Output length of the signature\n */\nDAVE_EXPORT void daveWelcomeResultGetRosterMemberSignature(DAVEWelcomeResultHandle welcomeResultHandle,\n                                                           uint64_t rosterId,\n                                                           uint8_t** signature,\n                                                           size_t* signatureLength);\n\n/**\n * @brief Destroys a welcome result and frees associated resources\n * @param welcomeResultHandle Welcome result handle to destroy\n */\nDAVE_EXPORT void daveWelcomeResultDestroy(DAVEWelcomeResultHandle welcomeResultHandle);\n\n/*******************************************************************************\n * Encryptor\n ******************************************************************************/\n\n/**\n * @brief Creates a new media frame encryptor\n * @return New encryptor handle. Must be destroyed with daveEncryptorDestroy()\n */\nDAVE_EXPORT DAVEEncryptorHandle daveEncryptorCreate(void);\n\n/**\n * @brief Destroys an encryptor and frees associated resources\n * @param encryptor Encryptor handle to destroy\n */\nDAVE_EXPORT void daveEncryptorDestroy(DAVEEncryptorHandle encryptor);\n\n/**\n * @brief Sets the key ratchet for encryption \n * @param encryptor Encryptor handle\n * @param keyRatchet Key ratchet to use for encryption (does *not* take ownership)\n */\nDAVE_EXPORT void daveEncryptorSetKeyRatchet(DAVEEncryptorHandle encryptor,\n                                            DAVEKeyRatchetHandle keyRatchet);\n\n/**\n * @brief Enables or disables passthrough mode (frames pass through unencrypted)\n * @param encryptor Encryptor handle\n * @param passthroughMode true to enable passthrough, false to encrypt\n */\nDAVE_EXPORT void daveEncryptorSetPassthroughMode(DAVEEncryptorHandle encryptor,\n                                                 bool passthroughMode);\n\n/**\n * @brief Associates an SSRC (Synchronization Source) with a specific codec\n * @param encryptor Encryptor handle\n * @param ssrc SSRC identifier\n * @param codecType Codec type for this SSRC\n */\nDAVE_EXPORT void daveEncryptorAssignSsrcToCodec(DAVEEncryptorHandle encryptor,\n                                                uint32_t ssrc,\n                                                DAVECodec codecType);\n\n/**\n * @brief Gets the current protocol version used by the encryptor\n * @param encryptor Encryptor handle\n * @return Protocol version number\n */\nDAVE_EXPORT uint16_t daveEncryptorGetProtocolVersion(DAVEEncryptorHandle encryptor);\n\n/**\n * @brief Calculates the maximum ciphertext size for a given plaintext frame size\n * @param encryptor Encryptor handle\n * @param mediaType Media type (audio or video)\n * @param frameSize Size of plaintext frame in bytes\n * @return Maximum possible ciphertext size in bytes\n */\nDAVE_EXPORT size_t daveEncryptorGetMaxCiphertextByteSize(DAVEEncryptorHandle encryptor,\n                                                         DAVEMediaType mediaType,\n                                                         size_t frameSize);\n\n/**\n * @brief Checks if the encryptor has a key ratchet\n * @param encryptor Encryptor handle\n * @return true if has key ratchet, false otherwise\n */\nDAVE_EXPORT bool daveEncryptorHasKeyRatchet(DAVEEncryptorHandle encryptor);\n\n/**\n * @brief Checks if the encryptor is in passthrough mode\n * @param encryptor Encryptor handle\n * @return true if in passthrough mode, false otherwise\n */\nDAVE_EXPORT bool daveEncryptorIsPassthroughMode(DAVEEncryptorHandle encryptor);\n\n/**\n * @brief Encrypts a media frame\n * @param encryptor Encryptor handle\n * @param mediaType Media type (audio or video)\n * @param ssrc SSRC of the stream\n * @param frame Pointer to plaintext frame data\n * @param frameLength Length of plaintext frame\n * @param[out] encryptedFrame Pointer to the output buffer the encrypted frame will be written to\n * @param encryptedFrameCapacity Capacity of the output buffer\n * @param[out] bytesWritten Number of bytes written to the output buffer\n * @return Result code indicating success or failure\n */\nDAVE_EXPORT DAVEEncryptorResultCode daveEncryptorEncrypt(DAVEEncryptorHandle encryptor,\n                                                         DAVEMediaType mediaType,\n                                                         uint32_t ssrc,\n                                                         const uint8_t* frame,\n                                                         size_t frameLength,\n                                                         uint8_t* encryptedFrame,\n                                                         size_t encryptedFrameCapacity,\n                                                         size_t* bytesWritten);\n\n/**\n * @brief Sets a callback to be notified when the protocol version changes\n * @param encryptor Encryptor handle\n * @param callback Callback function\n * @param userData User data passed to callback\n */\nDAVE_EXPORT void daveEncryptorSetProtocolVersionChangedCallback(\n    DAVEEncryptorHandle encryptor,\n    DAVEEncryptorProtocolVersionChangedCallback callback,\n    void* userData);\n\n/**\n * @brief Gets encryption statistics\n * @param encryptor Encryptor handle\n * @param mediaType Media type (audio or video)\n * @param[out] stats Pointer to the stats structure to be filled\n */\nDAVE_EXPORT void daveEncryptorGetStats(DAVEEncryptorHandle encryptor,\n                                       DAVEMediaType mediaType,\n                                       DAVEEncryptorStats* stats);\n\n\n\n\n/*******************************************************************************\n * Decryptor\n ******************************************************************************/\n\n/**\n * @brief Creates a new media frame decryptor\n * @return New decryptor handle. Must be destroyed with daveDecryptorDestroy()\n */\nDAVE_EXPORT DAVEDecryptorHandle daveDecryptorCreate(void);\n\n/**\n * @brief Destroys a decryptor and frees associated resources\n * @param decryptor Decryptor handle to destroy\n */\nDAVE_EXPORT void daveDecryptorDestroy(DAVEDecryptorHandle decryptor);\n\n/**\n * @brief Transitions the decryptor to use a new key ratchet\n * @param decryptor Decryptor handle\n * @param keyRatchet New key ratchet to transition to (does *not* take ownership)\n */\nDAVE_EXPORT void daveDecryptorTransitionToKeyRatchet(DAVEDecryptorHandle decryptor,\n                                                     DAVEKeyRatchetHandle keyRatchet);\n\n/**\n * @brief Transitions to or from passthrough mode\n * @param decryptor Decryptor handle\n * @param passthroughMode true to enable passthrough, false to decrypt\n */\nDAVE_EXPORT void daveDecryptorTransitionToPassthroughMode(DAVEDecryptorHandle decryptor,\n                                                          bool passthroughMode);\n\n/**\n * @brief Decrypts an encrypted media frame\n * @param decryptor Decryptor handle\n * @param mediaType Media type (audio or video)\n * @param encryptedFrame Pointer to the encrypted frame data\n * @param encryptedFrameLength Length of the encrypted frame\n * @param[out] frame Pointer to the output buffer the decrypted frame will be written to\n * @param frameCapacity Capacity of the output buffer\n * @param[out] bytesWritten Number of bytes written to the output buffer\n * @return Result code indicating success or failure\n */\nDAVE_EXPORT DAVEDecryptorResultCode daveDecryptorDecrypt(DAVEDecryptorHandle decryptor,\n                                                         DAVEMediaType mediaType,\n                                                         const uint8_t* encryptedFrame,\n                                                         size_t encryptedFrameLength,\n                                                         uint8_t* frame,\n                                                         size_t frameCapacity,\n                                                         size_t* bytesWritten);\n\n/**\n * @brief Calculates the maximum plaintext size for a given ciphertext frame size\n * @param decryptor Decryptor handle\n * @param mediaType Media type (audio or video)\n * @param encryptedFrameSize Size of encrypted frame in bytes\n * @return Maximum possible plaintext size in bytes\n */\nDAVE_EXPORT size_t daveDecryptorGetMaxPlaintextByteSize(DAVEDecryptorHandle decryptor,\n                                                        DAVEMediaType mediaType,\n                                                        size_t encryptedFrameSize);\n\n/**\n * @brief Gets decryption statistics\n * @param decryptor Decryptor handle\n * @param mediaType Media type (audio or video)\n * @param[out] stats Pointer to the stats structure to be filled\n */\nDAVE_EXPORT void daveDecryptorGetStats(DAVEDecryptorHandle decryptor,\n                                       DAVEMediaType mediaType,\n                                       DAVEDecryptorStats* stats);\n\n/*******************************************************************************\n * Logging\n ******************************************************************************/\n\n/**\n * @brief Sets a global callback for receiving log messages from the library\n * @param callback Log sink callback function\n */\nDAVE_EXPORT void daveSetLogSinkCallback(DAVELogSinkCallback callback);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "cpp/includes/dave/dave_interfaces.h",
    "content": "#pragma once\n\n#include <functional>\n#include <memory>\n#include <optional>\n#include <set>\n#include <string>\n#include <map>\n#include <variant>\n#include <chrono>\n#include <vector>\n\n#include <dave/array_view.h>\n#include <dave/dave.h>\n#include <dave/version.h>\n\nnamespace mlspp {\nnamespace bytes_ns {\nstruct bytes;\n}; // namespace bytes_ns\n\nstruct SignaturePrivateKey;\n} // namespace mlspp\n\n\nnamespace discord {\nnamespace dave {\n\nusing EncryptorStats = DAVEEncryptorStats;\nusing DecryptorStats = DAVEDecryptorStats;\nusing KeyGeneration = uint32_t;\nusing EncryptionKey = ::mlspp::bytes_ns::bytes;\n\nclass MlsKeyRatchet;\n\nenum MediaType : uint8_t { Audio, Video };\nenum Codec : uint8_t { Unknown, Opus, VP8, VP9, H264, H265, AV1 };\nenum LoggingSeverity {\n    LS_VERBOSE,\n    LS_INFO,\n    LS_WARNING,\n    LS_ERROR,\n    LS_NONE,\n};\n\n// Returned in std::variant when a message is hard-rejected and should trigger a reset\nstruct failed_t {};\n\n// Returned in std::variant when a message is soft-rejected and should not trigger a reset\nstruct ignored_t {};\n\n// Map of ID-key pairs.\n// In ProcessCommit, this lists IDs whose keys have been added, changed, or removed;\n// an empty value value means a key was removed.\nusing RosterMap = std::map<uint64_t, std::vector<uint8_t>>;\n\n// Return type for functions producing RosterMap or hard or soft failures\nusing RosterVariant = std::variant<failed_t, ignored_t, RosterMap>;\n\nconstexpr auto kDefaultTransitionDuration = std::chrono::seconds(10);\n\nclass IKeyRatchet {\npublic:\n    virtual ~IKeyRatchet() noexcept = default;\n    virtual EncryptionKey GetKey(KeyGeneration generation) noexcept = 0;\n    virtual void DeleteKey(KeyGeneration generation) noexcept = 0;\n};\n\nnamespace mls {\n\n#if defined(__ANDROID__)\ntypedef JNIEnv* KeyPairContextType;\n#else\ntypedef const char* KeyPairContextType;\n#endif\n\nclass ISession {\npublic:\n    virtual ~ISession() noexcept = default;\n\n    virtual void Init(ProtocolVersion version,\n                      uint64_t groupId,\n                      std::string const& selfUserId,\n                      std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey) noexcept = 0;\n    virtual void Reset() noexcept = 0;\n\n    virtual void SetProtocolVersion(ProtocolVersion version) noexcept = 0;\n    virtual ProtocolVersion GetProtocolVersion() const noexcept = 0;\n\n    virtual std::vector<uint8_t> GetLastEpochAuthenticator() const noexcept = 0;\n\n    virtual void SetExternalSender(std::vector<uint8_t> const& externalSenderPackage) noexcept = 0;\n\n    virtual std::optional<std::vector<uint8_t>> ProcessProposals(\n      std::vector<uint8_t> proposals,\n      std::set<std::string> const& recognizedUserIDs) noexcept = 0;\n\n    virtual RosterVariant ProcessCommit(std::vector<uint8_t> commit) noexcept = 0;\n\n    virtual std::optional<RosterMap> ProcessWelcome(\n      std::vector<uint8_t> welcome,\n      std::set<std::string> const& recognizedUserIDs) noexcept = 0;\n\n    virtual std::vector<uint8_t> GetMarshalledKeyPackage() noexcept = 0;\n\n    virtual std::unique_ptr<IKeyRatchet> GetKeyRatchet(\n      std::string const& userId) const noexcept = 0;\n\n    using PairwiseFingerprintCallback = std::function<void(std::vector<uint8_t> const&)>;\n    virtual void GetPairwiseFingerprint(uint16_t version,\n                                        std::string const& userId,\n                                        PairwiseFingerprintCallback callback) const noexcept = 0;\n};\n\nusing MLSFailureCallback = std::function<void(std::string const&, std::string const&)>;\nstd::unique_ptr<ISession> CreateSession(KeyPairContextType context,\n                                                    std::string authSessionId,\n                                                    MLSFailureCallback callback) noexcept;\n\n} // namespace mls\n\nclass IEncryptor {\npublic:\n    enum ResultCode {\n        Success,\n        EncryptionFailure,\n        MissingKeyRatchet,\n        MissingCryptor,\n        TooManyAttempts,\n    };\n\n    virtual ~IEncryptor() = default;\n\n    virtual void SetKeyRatchet(std::unique_ptr<IKeyRatchet> keyRatchet) = 0;\n    virtual void SetPassthroughMode(bool passthroughMode) = 0;\n\n    virtual bool HasKeyRatchet() const = 0;\n    virtual bool IsPassthroughMode() const = 0;\n\n    virtual void AssignSsrcToCodec(uint32_t ssrc, Codec codecType) = 0;\n    virtual Codec CodecForSsrc(uint32_t ssrc) = 0;\n\n    virtual ResultCode Encrypt(MediaType mediaType,\n                               uint32_t ssrc,\n                               ArrayView<const uint8_t> frame,\n                               ArrayView<uint8_t> encryptedFrame,\n                               size_t* bytesWritten) = 0;\n\n    virtual size_t GetMaxCiphertextByteSize(MediaType mediaType, size_t frameSize) = 0;\n    virtual EncryptorStats GetStats(MediaType mediaType) const = 0;\n\n    using ProtocolVersionChangedCallback = std::function<void()>;\n    virtual void SetProtocolVersionChangedCallback(ProtocolVersionChangedCallback callback) = 0;\n\n    virtual ProtocolVersion GetProtocolVersion() const = 0;\n};\n\nstd::unique_ptr<IEncryptor> CreateEncryptor();\n\nclass IDecryptor {\npublic:\n    using Duration = std::chrono::seconds;\n    enum ResultCode {\n        Success,\n        DecryptionFailure,\n        MissingKeyRatchet,\n        InvalidNonce,\n        MissingCryptor,\n    };\n\n    virtual ~IDecryptor() = default;\n\n    virtual void TransitionToKeyRatchet(std::unique_ptr<IKeyRatchet> keyRatchet,\n                                        Duration transitionExpiry = kDefaultTransitionDuration) = 0;\n    virtual void TransitionToPassthroughMode(\n      bool passthroughMode,\n      Duration transitionExpiry = kDefaultTransitionDuration) = 0;\n\n    virtual ResultCode Decrypt(MediaType mediaType,\n                               ArrayView<const uint8_t> encryptedFrame,\n                               ArrayView<uint8_t> frame,\n                               size_t* bytesWritten) = 0;\n\n    virtual size_t GetMaxPlaintextByteSize(MediaType mediaType, size_t encryptedFrameSize) = 0;\n    virtual DecryptorStats GetStats(MediaType mediaType) const = 0;\n};\n\nstd::unique_ptr<IDecryptor> CreateDecryptor();\n\nstatic_assert(DAVE_CODEC_UNKNOWN == static_cast<int>(Codec::Unknown));\nstatic_assert(DAVE_CODEC_OPUS == static_cast<int>(Codec::Opus));\nstatic_assert(DAVE_CODEC_VP8 == static_cast<int>(Codec::VP8));\nstatic_assert(DAVE_CODEC_VP9 == static_cast<int>(Codec::VP9));\nstatic_assert(DAVE_CODEC_H264 == static_cast<int>(Codec::H264));\nstatic_assert(DAVE_CODEC_H265 == static_cast<int>(Codec::H265));\nstatic_assert(DAVE_CODEC_AV1 == static_cast<int>(Codec::AV1));\nstatic_assert(DAVE_MEDIA_TYPE_AUDIO == static_cast<int>(MediaType::Audio));\nstatic_assert(DAVE_MEDIA_TYPE_VIDEO == static_cast<int>(MediaType::Video));\nstatic_assert(DAVE_ENCRYPTOR_RESULT_CODE_SUCCESS == static_cast<int>(IEncryptor::Success));\nstatic_assert(DAVE_ENCRYPTOR_RESULT_CODE_ENCRYPTION_FAILURE ==\n              static_cast<int>(IEncryptor::EncryptionFailure));\nstatic_assert(DAVE_ENCRYPTOR_RESULT_CODE_MISSING_KEY_RATCHET ==\n              static_cast<int>(IEncryptor::MissingKeyRatchet));\nstatic_assert(DAVE_ENCRYPTOR_RESULT_CODE_MISSING_CRYPTOR ==\n              static_cast<int>(IEncryptor::MissingCryptor));\nstatic_assert(DAVE_ENCRYPTOR_RESULT_CODE_TOO_MANY_ATTEMPTS ==\n              static_cast<int>(IEncryptor::TooManyAttempts));\nstatic_assert(DAVE_DECRYPTOR_RESULT_CODE_SUCCESS == static_cast<int>(IDecryptor::Success));\nstatic_assert(DAVE_DECRYPTOR_RESULT_CODE_DECRYPTION_FAILURE ==\n              static_cast<int>(IDecryptor::DecryptionFailure));\nstatic_assert(DAVE_DECRYPTOR_RESULT_CODE_MISSING_KEY_RATCHET ==\n              static_cast<int>(IDecryptor::MissingKeyRatchet));\nstatic_assert(DAVE_DECRYPTOR_RESULT_CODE_INVALID_NONCE ==\n              static_cast<int>(IDecryptor::InvalidNonce));\nstatic_assert(DAVE_DECRYPTOR_RESULT_CODE_MISSING_CRYPTOR ==\n              static_cast<int>(IDecryptor::MissingCryptor));\nstatic_assert(DAVE_LOGGING_SEVERITY_VERBOSE == static_cast<int>(LoggingSeverity::LS_VERBOSE));\nstatic_assert(DAVE_LOGGING_SEVERITY_INFO == static_cast<int>(LoggingSeverity::LS_INFO));\nstatic_assert(DAVE_LOGGING_SEVERITY_WARNING == static_cast<int>(LoggingSeverity::LS_WARNING));\nstatic_assert(DAVE_LOGGING_SEVERITY_ERROR == static_cast<int>(LoggingSeverity::LS_ERROR));\nstatic_assert(DAVE_LOGGING_SEVERITY_NONE == static_cast<int>(LoggingSeverity::LS_NONE));\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/includes/dave/logger.h",
    "content": "#pragma once\n\n#include <sstream>\n\n#include <dave/dave_interfaces.h>\n\n#if !defined(DISCORD_LOG)\n#define DISCORD_LOG_FILE_LINE(sev, file, line) ::discord::dave::LogStreamer(sev, file, line)\n#define DISCORD_LOG(sev) DISCORD_LOG_FILE_LINE(::discord::dave::sev, __FILE__, __LINE__)\n#endif\nnamespace discord {\nnamespace dave {\n\nusing LogSink = void (*)(LoggingSeverity severity,\n                         const char* file,\n                         int line,\n                         const std::string& message);\nvoid SetLogSink(LogSink sink);\n\nclass LogStreamer {\npublic:\n    LogStreamer(LoggingSeverity severity, const char* file, int line);\n    ~LogStreamer();\n\n    template <typename T>\n    LogStreamer& operator<<(const T& value)\n    {\n        stream_ << value;\n        return *this;\n    }\n\nprivate:\n    LoggingSeverity severity_;\n    const char* file_;\n    int line_;\n    std::ostringstream stream_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/includes/dave/version.h",
    "content": "#pragma once\n\n#include <stdint.h>\n\n#include <dave/dave.h>\n\nnamespace discord {\nnamespace dave {\n\nusing ProtocolVersion = uint16_t;\nusing SignatureVersion = uint8_t;\n\nProtocolVersion MaxSupportedProtocolVersion();\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/bindings_capi.cpp",
    "content": "#include <dave/dave_interfaces.h>\n\n#include <atomic>\n#include <cstdint>\n#include <cstring>\n\n#include <mls/messages.h>\n\n#include <dave/logger.h>\n#include <dave/version.h>\n\n#include \"mls_key_ratchet.h\"\n\n#define ARG_CHECK(arg)                                \\\n    if (arg == nullptr) {                             \\\n        fprintf(stderr, \"ERROR: %s is null\\n\", #arg); \\\n        assert(false);                                \\\n        return;                                       \\\n    }\n\n#define ARG_CHECK_RET(arg, ret)                       \\\n    if (arg == nullptr) {                             \\\n        fprintf(stderr, \"ERROR: %s is null\\n\", #arg); \\\n        assert(false);                                \\\n        return ret;                                   \\\n    }\n\nstd::unique_ptr<discord::dave::MlsKeyRatchet> CopyKeyRatchet(DAVEKeyRatchetHandle keyRatchet)\n{\n    auto mlsKeyRatchet = reinterpret_cast<discord::dave::MlsKeyRatchet*>(keyRatchet);\n    if (!mlsKeyRatchet) {\n        return nullptr;\n    }\n\n    auto hashRatchet = mlsKeyRatchet->GetHashRatchet();\n    auto cipherSuite = hashRatchet.suite;\n    auto baseSecret = hashRatchet.next_secret;\n\n    return std::make_unique<discord::dave::MlsKeyRatchet>(cipherSuite, std::move(baseSecret));\n}\n\nvoid CopyVectorToOutputBuffer(std::vector<uint8_t> const& vector, uint8_t** data, size_t* length)\n{\n    if (data == nullptr || length == nullptr) {\n        return;\n    }\n\n    if (vector.empty()) {\n        *data = nullptr;\n        *length = 0;\n        return;\n    }\n\n    *data = reinterpret_cast<uint8_t*>(malloc(vector.size()));\n    memcpy(*data, vector.data(), vector.size());\n    *length = vector.size();\n}\n\nvoid GetRosterMemberIds(const discord::dave::RosterMap& rosterMap,\n                        uint64_t** rosterIds,\n                        size_t* rosterIdsLength)\n{\n    *rosterIdsLength = rosterMap.size();\n    *rosterIds = reinterpret_cast<uint64_t*>(malloc(*rosterIdsLength * sizeof(uint64_t)));\n    size_t i = 0;\n    for (const auto& [key, value] : rosterMap) {\n        (*rosterIds)[i++] = key;\n    }\n}\n\nvoid GetRosterMemberSignature(const discord::dave::RosterMap& rosterMap,\n                              uint64_t rosterId,\n                              uint8_t** signature,\n                              size_t* signatureLength)\n{\n    CopyVectorToOutputBuffer(rosterMap.at(rosterId), signature, signatureLength);\n}\n\nuint16_t daveMaxSupportedProtocolVersion(void)\n{\n    return discord::dave::MaxSupportedProtocolVersion();\n}\n\nvoid daveFree(void* ptr)\n{\n    free(ptr);\n}\n\nDAVESessionHandle daveSessionCreate(void* context,\n                                    const char* authSessionId,\n                                    DAVEMLSFailureCallback callback,\n                                    void* userData)\n{\n    discord::dave::mls::MLSFailureCallback mlsFailureCallback;\n    if (callback != nullptr) {\n        mlsFailureCallback = [callback, userData](std::string source, std::string reason) {\n            callback(source.c_str(), reason.c_str(), userData);\n        };\n    };\n\n    auto contextType = static_cast<discord::dave::mls::KeyPairContextType>(context);\n    auto authSessionIdStr = authSessionId ? std::string(authSessionId) : std::string();\n    auto session =\n      discord::dave::mls::CreateSession(contextType, authSessionIdStr, mlsFailureCallback);\n    return reinterpret_cast<DAVESessionHandle>(session.release());\n}\n\nvoid daveSessionDestroy(DAVESessionHandle sessionHandle)\n{\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    delete session;\n}\n\nvoid daveSessionInit(DAVESessionHandle sessionHandle,\n                     uint16_t version,\n                     uint64_t groupId,\n                     const char* selfUserId)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto selfUserIdStr = selfUserId ? std::string(selfUserId) : std::string();\n    std::shared_ptr<::mlspp::SignaturePrivateKey> transientKey;\n    session->Init(version, groupId, selfUserIdStr, transientKey);\n}\n\nvoid daveSessionReset(DAVESessionHandle sessionHandle)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    session->Reset();\n}\n\nvoid daveSessionSetProtocolVersion(DAVESessionHandle sessionHandle, uint16_t version)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    session->SetProtocolVersion(version);\n}\n\nuint16_t daveSessionGetProtocolVersion(DAVESessionHandle sessionHandle)\n{\n    ARG_CHECK_RET(sessionHandle, 0);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    return session->GetProtocolVersion();\n}\n\nvoid daveSessionGetLastEpochAuthenticator(DAVESessionHandle sessionHandle,\n                                          uint8_t** authenticator,\n                                          size_t* length)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto lastEpochAuthenticator = session->GetLastEpochAuthenticator();\n    CopyVectorToOutputBuffer(lastEpochAuthenticator, authenticator, length);\n}\n\nvoid daveSessionSetExternalSender(DAVESessionHandle sessionHandle,\n                                  const uint8_t* externalSender,\n                                  size_t length)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto externalSenderVec = std::vector<uint8_t>(externalSender, externalSender + length);\n    session->SetExternalSender(externalSenderVec);\n}\n\nvoid daveSessionProcessProposals(DAVESessionHandle sessionHandle,\n                                 const uint8_t* proposals,\n                                 size_t length,\n                                 const char** recognizedUserIds,\n                                 size_t recognizedUserIdsLength,\n                                 uint8_t** commitWelcomeBytes,\n                                 size_t* commitWelcomeBytesLength)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto proposalsVec = std::vector<uint8_t>(proposals, proposals + length);\n    auto recognizedUserIdsSet =\n      std::set<std::string>(recognizedUserIds, recognizedUserIds + recognizedUserIdsLength);\n    auto result =\n      session->ProcessProposals(std::move(proposalsVec), std::move(recognizedUserIdsSet));\n\n    if (result) {\n        CopyVectorToOutputBuffer(*result, commitWelcomeBytes, commitWelcomeBytesLength);\n    }\n}\n\nDAVECommitResultHandle daveSessionProcessCommit(DAVESessionHandle sessionHandle,\n                                                const uint8_t* commit,\n                                                size_t length)\n{\n    ARG_CHECK_RET(sessionHandle, nullptr);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto commitVec = std::vector<uint8_t>(commit, commit + length);\n\n    auto rosterVariant = session->ProcessCommit(std::move(commitVec));\n\n    auto rosterVariantPtr = new discord::dave::RosterVariant(std::move(rosterVariant));\n    return reinterpret_cast<DAVECommitResultHandle>(rosterVariantPtr);\n}\n\nDAVEWelcomeResultHandle daveSessionProcessWelcome(DAVESessionHandle sessionHandle,\n                                                  const uint8_t* welcome,\n                                                  size_t length,\n                                                  const char** recognizedUserIds,\n                                                  size_t recognizedUserIdsLength)\n{\n    ARG_CHECK_RET(sessionHandle, nullptr);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto welcomeVec = std::vector<uint8_t>(welcome, welcome + length);\n    auto recognizedUserIdsSet =\n      std::set<std::string>(recognizedUserIds, recognizedUserIds + recognizedUserIdsLength);\n\n    auto result = session->ProcessWelcome(std::move(welcomeVec), std::move(recognizedUserIdsSet));\n    if (!result) {\n        return nullptr;\n    }\n\n    auto rosterMapPtr = new discord::dave::RosterMap(std::move(*result));\n    return reinterpret_cast<DAVEWelcomeResultHandle>(rosterMapPtr);\n}\n\nvoid daveSessionGetMarshalledKeyPackage(DAVESessionHandle sessionHandle,\n                                        uint8_t** keyPackage,\n                                        size_t* length)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto keyPackageVec = session->GetMarshalledKeyPackage();\n    CopyVectorToOutputBuffer(keyPackageVec, keyPackage, length);\n}\n\nDAVEKeyRatchetHandle daveSessionGetKeyRatchet(DAVESessionHandle sessionHandle, const char* userId)\n{\n    ARG_CHECK_RET(sessionHandle, nullptr);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto userIdStr = userId ? std::string(userId) : std::string();\n    auto keyRatchetPtr = session->GetKeyRatchet(userIdStr);\n    return reinterpret_cast<DAVEKeyRatchetHandle>(keyRatchetPtr.release());\n}\n\nvoid daveSessionGetPairwiseFingerprint(DAVESessionHandle sessionHandle,\n                                       uint16_t version,\n                                       const char* userId,\n                                       DAVEPairwiseFingerprintCallback callback,\n                                       void* userData)\n{\n    ARG_CHECK(sessionHandle);\n    auto session = reinterpret_cast<discord::dave::mls::ISession*>(sessionHandle);\n    auto userIdStr = userId ? std::string(userId) : std::string();\n    session->GetPairwiseFingerprint(\n      version, userIdStr, [callback, userData](std::vector<uint8_t> const& fingerprint) {\n          callback(fingerprint.data(), fingerprint.size(), userData);\n      });\n}\n\nvoid daveKeyRatchetDestroy(DAVEKeyRatchetHandle keyRatchet)\n{\n    delete reinterpret_cast<discord::dave::MlsKeyRatchet*>(keyRatchet);\n}\n\nbool daveCommitResultIsFailed(DAVECommitResultHandle commitResultHandle)\n{\n    ARG_CHECK_RET(commitResultHandle, false);\n    auto commitResult = reinterpret_cast<discord::dave::RosterVariant*>(commitResultHandle);\n    return std::holds_alternative<discord::dave::failed_t>(*commitResult);\n}\n\nbool daveCommitResultIsIgnored(DAVECommitResultHandle commitResultHandle)\n{\n    ARG_CHECK_RET(commitResultHandle, false);\n    auto commitResult = reinterpret_cast<discord::dave::RosterVariant*>(commitResultHandle);\n    return std::holds_alternative<discord::dave::ignored_t>(*commitResult);\n}\n\nvoid daveCommitResultGetRosterMemberIds(DAVECommitResultHandle commitResultHandle,\n                                        uint64_t** rosterIds,\n                                        size_t* rosterIdsLength)\n{\n    ARG_CHECK(commitResultHandle);\n    auto commitResult = reinterpret_cast<discord::dave::RosterVariant*>(commitResultHandle);\n    if (!std::holds_alternative<discord::dave::RosterMap>(*commitResult)) {\n        *rosterIds = nullptr;\n        *rosterIdsLength = 0;\n        return;\n    }\n    GetRosterMemberIds(\n      std::get<discord::dave::RosterMap>(*commitResult), rosterIds, rosterIdsLength);\n}\n\nvoid daveCommitResultGetRosterMemberSignature(DAVECommitResultHandle commitResultHandle,\n                                              uint64_t rosterId,\n                                              uint8_t** signature,\n                                              size_t* signatureLength)\n{\n    ARG_CHECK(commitResultHandle);\n    auto commitResult = reinterpret_cast<discord::dave::RosterVariant*>(commitResultHandle);\n    if (!std::holds_alternative<discord::dave::RosterMap>(*commitResult)) {\n        *signature = nullptr;\n        *signatureLength = 0;\n        return;\n    }\n    GetRosterMemberSignature(\n      std::get<discord::dave::RosterMap>(*commitResult), rosterId, signature, signatureLength);\n}\n\nvoid daveCommitResultDestroy(DAVECommitResultHandle commitResultHandle)\n{\n    auto commitResult = reinterpret_cast<discord::dave::RosterVariant*>(commitResultHandle);\n    delete commitResult;\n}\n\nvoid daveWelcomeResultGetRosterMemberIds(DAVEWelcomeResultHandle welcomeResultHandle,\n                                         uint64_t** rosterIds,\n                                         size_t* rosterIdsLength)\n{\n    ARG_CHECK(welcomeResultHandle);\n    auto welcomeResult = reinterpret_cast<discord::dave::RosterMap*>(welcomeResultHandle);\n    GetRosterMemberIds(*welcomeResult, rosterIds, rosterIdsLength);\n}\n\nvoid daveWelcomeResultGetRosterMemberSignature(DAVEWelcomeResultHandle welcomeResultHandle,\n                                               uint64_t rosterId,\n                                               uint8_t** signature,\n                                               size_t* signatureLength)\n{\n    ARG_CHECK(welcomeResultHandle);\n    auto welcomeResult = reinterpret_cast<discord::dave::RosterMap*>(welcomeResultHandle);\n    GetRosterMemberSignature(*welcomeResult, rosterId, signature, signatureLength);\n}\n\nvoid daveWelcomeResultDestroy(DAVEWelcomeResultHandle welcomeResultHandle)\n{\n    auto welcomeResult = reinterpret_cast<discord::dave::RosterMap*>(welcomeResultHandle);\n    delete welcomeResult;\n}\n\nDAVEEncryptorHandle daveEncryptorCreate()\n{\n    auto encryptor = discord::dave::CreateEncryptor();\n    return reinterpret_cast<DAVEEncryptorHandle>(encryptor.release());\n}\n\nvoid daveEncryptorDestroy(DAVEEncryptorHandle encryptorHandle)\n{\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    delete encryptor;\n}\n\nvoid daveEncryptorSetKeyRatchet(DAVEEncryptorHandle encryptorHandle,\n                                DAVEKeyRatchetHandle keyRatchet)\n{\n    ARG_CHECK(encryptorHandle);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    auto keyRatchetCopy = CopyKeyRatchet(keyRatchet);\n    encryptor->SetKeyRatchet(std::move(keyRatchetCopy));\n}\n\nvoid daveEncryptorSetPassthroughMode(DAVEEncryptorHandle encryptorHandle, bool passthroughMode)\n{\n    ARG_CHECK(encryptorHandle);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    encryptor->SetPassthroughMode(passthroughMode);\n}\n\nvoid daveEncryptorAssignSsrcToCodec(DAVEEncryptorHandle encryptorHandle,\n                                    uint32_t ssrc,\n                                    DAVECodec codecType)\n{\n    ARG_CHECK(encryptorHandle);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    encryptor->AssignSsrcToCodec(ssrc, static_cast<discord::dave::Codec>(codecType));\n}\n\nuint16_t daveEncryptorGetProtocolVersion(DAVEEncryptorHandle encryptorHandle)\n{\n    ARG_CHECK_RET(encryptorHandle, 0);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    return encryptor->GetProtocolVersion();\n}\n\nsize_t daveEncryptorGetMaxCiphertextByteSize(DAVEEncryptorHandle encryptorHandle,\n                                             DAVEMediaType mediaType,\n                                             size_t frameSize)\n{\n    ARG_CHECK_RET(encryptorHandle, 0);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    return encryptor->GetMaxCiphertextByteSize(static_cast<discord::dave::MediaType>(mediaType),\n                                               frameSize);\n}\n\nbool daveEncryptorHasKeyRatchet(DAVEEncryptorHandle encryptorHandle)\n{\n    ARG_CHECK_RET(encryptorHandle, false);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    return encryptor->HasKeyRatchet();\n}\n\nbool daveEncryptorIsPassthroughMode(DAVEEncryptorHandle encryptorHandle)\n{\n    ARG_CHECK_RET(encryptorHandle, false);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    return encryptor->IsPassthroughMode();\n}\n\nDAVEEncryptorResultCode daveEncryptorEncrypt(DAVEEncryptorHandle encryptorHandle,\n                                             DAVEMediaType mediaType,\n                                             uint32_t ssrc,\n                                             const uint8_t* frame,\n                                             size_t frameLength,\n                                             uint8_t* encryptedFrame,\n                                             size_t encryptedFrameCapacity,\n                                             size_t* bytesWritten)\n{\n    ARG_CHECK_RET(encryptorHandle, DAVE_ENCRYPTOR_RESULT_CODE_ENCRYPTION_FAILURE);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    auto frameView = discord::dave::MakeArrayView(frame, frameLength);\n    auto encryptedFrameView = discord::dave::MakeArrayView(encryptedFrame, encryptedFrameCapacity);\n    auto result = encryptor->Encrypt(static_cast<discord::dave::MediaType>(mediaType),\n                                     ssrc,\n                                     frameView,\n                                     encryptedFrameView,\n                                     bytesWritten);\n    return static_cast<DAVEEncryptorResultCode>(result);\n}\n\nvoid daveEncryptorSetProtocolVersionChangedCallback(\n  DAVEEncryptorHandle encryptorHandle,\n  DAVEEncryptorProtocolVersionChangedCallback callback,\n  void* userData)\n{\n    ARG_CHECK(encryptorHandle);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    encryptor->SetProtocolVersionChangedCallback([callback, userData]() { callback(userData); });\n}\n\nvoid daveEncryptorGetStats(DAVEEncryptorHandle encryptorHandle,\n                           DAVEMediaType mediaType,\n                           DAVEEncryptorStats* stats)\n{\n    ARG_CHECK(encryptorHandle);\n    auto encryptor = reinterpret_cast<discord::dave::IEncryptor*>(encryptorHandle);\n    *stats = encryptor->GetStats(static_cast<discord::dave::MediaType>(mediaType));\n}\n\nDAVEDecryptorHandle daveDecryptorCreate()\n{\n    auto decryptor = discord::dave::CreateDecryptor();\n    return reinterpret_cast<DAVEDecryptorHandle>(decryptor.release());\n}\n\nvoid daveDecryptorDestroy(DAVEDecryptorHandle decryptorHandle)\n{\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    delete decryptor;\n}\n\nvoid daveDecryptorTransitionToKeyRatchet(DAVEDecryptorHandle decryptorHandle,\n                                         DAVEKeyRatchetHandle keyRatchet)\n{\n    ARG_CHECK(decryptorHandle);\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    auto keyRatchetCopy = CopyKeyRatchet(keyRatchet);\n    decryptor->TransitionToKeyRatchet(std::move(keyRatchetCopy));\n}\n\nvoid daveDecryptorTransitionToPassthroughMode(DAVEDecryptorHandle decryptorHandle,\n                                              bool passthroughMode)\n{\n    ARG_CHECK(decryptorHandle);\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    decryptor->TransitionToPassthroughMode(passthroughMode);\n}\n\nDAVEDecryptorResultCode daveDecryptorDecrypt(DAVEDecryptorHandle decryptorHandle,\n                                             DAVEMediaType mediaType,\n                                             const uint8_t* encryptedFrame,\n                                             size_t encryptedFrameLength,\n                                             uint8_t* frame,\n                                             size_t frameCapacity,\n                                             size_t* bytesWritten)\n{\n    ARG_CHECK_RET(decryptorHandle, DAVE_DECRYPTOR_RESULT_CODE_DECRYPTION_FAILURE);\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    auto encryptedFrameView = discord::dave::MakeArrayView(encryptedFrame, encryptedFrameLength);\n    auto frameView = discord::dave::MakeArrayView(frame, frameCapacity);\n    auto result = decryptor->Decrypt(static_cast<discord::dave::MediaType>(mediaType),\n                                     encryptedFrameView,\n                                     frameView,\n                                     bytesWritten);\n    return static_cast<DAVEDecryptorResultCode>(result);\n}\n\nsize_t daveDecryptorGetMaxPlaintextByteSize(DAVEDecryptorHandle decryptorHandle,\n                                            DAVEMediaType mediaType,\n                                            size_t encryptedFrameSize)\n{\n    ARG_CHECK_RET(decryptorHandle, 0);\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    return decryptor->GetMaxPlaintextByteSize(static_cast<discord::dave::MediaType>(mediaType),\n                                              encryptedFrameSize);\n}\n\nvoid daveDecryptorGetStats(DAVEDecryptorHandle decryptorHandle,\n                           DAVEMediaType mediaType,\n                           DAVEDecryptorStats* stats)\n{\n    ARG_CHECK(decryptorHandle);\n    auto decryptor = reinterpret_cast<discord::dave::IDecryptor*>(decryptorHandle);\n    *stats = decryptor->GetStats(static_cast<discord::dave::MediaType>(mediaType));\n}\n\nstatic std::atomic<DAVELogSinkCallback> gLogSinkCallback{nullptr};\n\nvoid LogSinkCallback(discord::dave::LoggingSeverity severity,\n                     const char* file,\n                     int line,\n                     const std::string& message)\n{\n    auto callback = gLogSinkCallback.load();\n    if (callback) {\n        callback(static_cast<DAVELoggingSeverity>(severity), file, line, message.c_str());\n    }\n}\n\nvoid daveSetLogSinkCallback(DAVELogSinkCallback callback)\n{\n    gLogSinkCallback.store(callback);\n    discord::dave::SetLogSink(callback ? LogSinkCallback : nullptr);\n}\n"
  },
  {
    "path": "cpp/src/bindings_wasm.cpp",
    "content": "#include <cstdint>\n#include <memory>\n#include <set>\n#include <vector>\n\n#include <emscripten.h>\n#include <emscripten/bind.h>\n#include <emscripten/val.h>\n\n#include <mls/crypto.h>\n#include <mls/key_schedule.h>\n\n#include <dave/logger.h>\n#include <dave/version.h>\n\n#include \"common.h\"\n#include \"decryptor.h\"\n#include \"encryptor.h\"\n#include \"mls/parameters.h\"\n#include \"mls/session.h\"\n#include \"mls_key_ratchet.h\"\n\nusing namespace emscripten;\n\nnamespace discord {\nnamespace dave {\n\nval ToOwnedTypedArray(const uint8_t* data, size_t size)\n{\n    val array = val::array();\n    for (size_t i = 0; i < size; i++) {\n        array.call<void>(\"push\", data[i]);\n    }\n    return array;\n}\n\nval ToOwnedTypedArray(const ::mlspp::bytes_ns::bytes& data)\n{\n    return ToOwnedTypedArray(data.data(), data.size());\n}\n\nval ToOwnedTypedArray(const std::vector<uint8_t>& data)\n{\n    return ToOwnedTypedArray(data.data(), data.size());\n}\n\nval MlsKeyRatchetToJS(std::unique_ptr<MlsKeyRatchet> keyRatchet)\n{\n    if (!keyRatchet) {\n        return val::null();\n    }\n\n    auto hashRatchet = keyRatchet->GetHashRatchet();\n\n    auto value = val::object();\n    value.set(\"cipherSuite\", static_cast<uint16_t>(hashRatchet.suite.cipher_suite()));\n    value.set(\"baseSecret\", ToOwnedTypedArray(hashRatchet.next_secret));\n\n    return value;\n}\n\nstd::unique_ptr<MlsKeyRatchet> MlsKeyRatchetFromJS(val keyRatchet)\n{\n    if (keyRatchet.isNull()) {\n        return nullptr;\n    }\n\n    auto cipherSuite = ::mlspp::CipherSuite(\n      static_cast<::mlspp::CipherSuite::ID>(keyRatchet[\"cipherSuite\"].as<uint16_t>()));\n    auto baseSecret = emscripten::convertJSArrayToNumberVector<uint8_t>(keyRatchet[\"baseSecret\"]);\n    auto baseSecretBytes = ::mlspp::bytes_ns::bytes(baseSecret);\n\n    return std::make_unique<MlsKeyRatchet>(cipherSuite, baseSecretBytes);\n}\n\nnamespace mls {\n\nclass TransientKeys {\npublic:\n    std::shared_ptr<::mlspp::SignaturePrivateKey> GetTransientPrivateKey(ProtocolVersion version)\n    {\n        auto it = keys_.find(version);\n        if (it == keys_.end()) {\n            auto ciphersuite = CiphersuiteForProtocolVersion(version);\n            auto key = std::make_shared<::mlspp::SignaturePrivateKey>(\n              ::mlspp::SignaturePrivateKey::generate(ciphersuite));\n            it = keys_.emplace(version, key).first;\n        }\n        return it->second;\n    }\n\n    void Clear() { keys_.clear(); }\n\nprivate:\n    std::map<ProtocolVersion, std::shared_ptr<::mlspp::SignaturePrivateKey>> keys_;\n};\n\nclass SessionWrapper {\npublic:\n    SessionWrapper(std::string ctx, std::string authSessionId, val callback)\n    {\n        session_ = std::make_unique<Session>(\n          ctx.c_str(), authSessionId, [callback](std::string source, std::string message) {\n              callback(source, message);\n          });\n    }\n\n    void Init(ProtocolVersion version,\n              uint64_t groupId,\n              std::string const& selfUserId,\n              std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey)\n    {\n        session_->Init(version, groupId, selfUserId, transientKey);\n    }\n\n    void Reset() { session_->Reset(); }\n\n    void SetProtocolVersion(ProtocolVersion version) { session_->SetProtocolVersion(version); }\n\n    ProtocolVersion GetProtocolVersion() { return session_->GetProtocolVersion(); }\n\n    val GetLastEpochAuthenticator()\n    {\n        return ToOwnedTypedArray(session_->GetLastEpochAuthenticator());\n    }\n\n    void SetExternalSender(val externalSender)\n    {\n        if (!externalSender.isNull()) {\n            std::vector<uint8_t> externalSenderVec =\n              emscripten::convertJSArrayToNumberVector<uint8_t>(externalSender);\n            session_->SetExternalSender(externalSenderVec);\n        }\n        else {\n            DISCORD_LOG(LS_ERROR) << \"External sender is null\";\n        }\n    }\n\n    val ProcessProposals(val proposals, val recognizedUserIDs)\n    {\n        std::vector<uint8_t> proposalsVec =\n          emscripten::convertJSArrayToNumberVector<uint8_t>(proposals);\n        auto recognizedUserIDsVec = emscripten::vecFromJSArray<std::string>(recognizedUserIDs);\n        auto recognizedUserIDsSet =\n          std::set<std::string>(recognizedUserIDsVec.begin(), recognizedUserIDsVec.end());\n\n        auto bytes = session_->ProcessProposals(proposalsVec, recognizedUserIDsSet);\n        if (!bytes) {\n            return val::null();\n        }\n        return ToOwnedTypedArray(*bytes);\n    }\n\n    val ProcessCommit(val commit)\n    {\n        std::vector<uint8_t> commitVec = emscripten::convertJSArrayToNumberVector<uint8_t>(commit);\n        auto processedCommit = session_->ProcessCommit(commitVec);\n\n        auto failed = std::holds_alternative<dave::failed_t>(processedCommit);\n        auto ignored = std::holds_alternative<dave::ignored_t>(processedCommit);\n        auto rosterUpdate = GetOptional<dave::RosterMap>(std::move(processedCommit));\n\n        val result = val::object();\n        result.set(\"failed\", failed);\n        result.set(\"ignored\", ignored);\n\n        val rosterObj = val::null();\n        if (rosterUpdate) {\n            rosterObj = val::object();\n            for (const auto& [key, value] : *rosterUpdate) {\n                rosterObj.set(key, ToOwnedTypedArray(value));\n            }\n        }\n        result.set(\"rosterUpdate\", rosterObj);\n        return result;\n    }\n\n    val ProcessWelcome(val welcome, val recognizedUserIDs)\n    {\n        auto welcomeVec = emscripten::convertJSArrayToNumberVector<uint8_t>(welcome);\n        auto recognizedUserIDsVec = emscripten::vecFromJSArray<std::string>(recognizedUserIDs);\n        auto recognizedUserIDsSet =\n          std::set<std::string>(recognizedUserIDsVec.begin(), recognizedUserIDsVec.end());\n        auto roster = session_->ProcessWelcome(welcomeVec, recognizedUserIDsSet);\n        if (!roster) {\n            return val::null();\n        }\n\n        val rosterObj = val::object();\n        for (const auto& [key, value] : *roster) {\n            rosterObj.set(key, ToOwnedTypedArray(value));\n        }\n\n        return rosterObj;\n    }\n\n    val GetMarshalledKeyPackage() { return ToOwnedTypedArray(session_->GetMarshalledKeyPackage()); }\n\n    val GetKeyRatchet(std::string const& userId)\n    {\n        auto keyRatchet = session_->GetKeyRatchet(userId);\n        auto mlsKeyRatchet =\n          std::unique_ptr<MlsKeyRatchet>(static_cast<MlsKeyRatchet*>(keyRatchet.release()));\n        return MlsKeyRatchetToJS(std::move(mlsKeyRatchet));\n    }\n\nprivate:\n    std::unique_ptr<mls::Session> session_;\n};\n\n} // namespace mls\n\nclass EncryptorWrapper {\npublic:\n    EncryptorWrapper() { encryptor_ = std::make_unique<Encryptor>(); }\n\n    void SetKeyRatchet(val keyRatchet)\n    {\n        encryptor_->SetKeyRatchet(MlsKeyRatchetFromJS(keyRatchet));\n    }\n\n    void SetPassthroughMode(bool passthroughMode)\n    {\n        encryptor_->SetPassthroughMode(passthroughMode);\n    }\n\n    void AssignSsrcToCodec(uint32_t ssrc, Codec codecType)\n    {\n        encryptor_->AssignSsrcToCodec(ssrc, codecType);\n    }\n\n    ProtocolVersion GetProtocolVersion() { return encryptor_->GetProtocolVersion(); }\n\n    size_t GetMaxCiphertextByteSize(MediaType mediaType, size_t plaintextByteSize)\n    {\n        return encryptor_->GetMaxCiphertextByteSize(mediaType, plaintextByteSize);\n    }\n\n    size_t Encrypt(MediaType mediaType,\n                   uint32_t ssrc,\n                   int framePtr,\n                   size_t frameLength,\n                   size_t frameCapacity)\n    {\n        auto frame = reinterpret_cast<uint8_t*>(framePtr);\n\n        auto frameView = MakeArrayView(const_cast<const uint8_t*>(frame), frameLength);\n        auto encryptedFrameMaxSize = GetMaxCiphertextByteSize(mediaType, frameLength);\n        if (frameCapacity < encryptedFrameMaxSize) {\n            DISCORD_LOG(LS_ERROR) << \"Frame capacity is less than the maximum ciphertext size\";\n            return 0;\n        }\n        auto encryptedFrameView = MakeArrayView(frame, encryptedFrameMaxSize);\n\n        size_t bytesWritten = 0;\n        auto result =\n          encryptor_->Encrypt(mediaType, ssrc, frameView, encryptedFrameView, &bytesWritten);\n\n        if (result != 0) {\n            return 0;\n        }\n\n        return bytesWritten;\n    }\n\n    void SetProtocolVersionChangedCallback(val callback)\n    {\n        encryptor_->SetProtocolVersionChangedCallback([callback]() { callback(); });\n    }\n\nprivate:\n    std::unique_ptr<Encryptor> encryptor_;\n};\n\nclass DecryptorWrapper {\npublic:\n    DecryptorWrapper() { decryptor_ = std::make_unique<Decryptor>(); }\n\n    void TransitionToKeyRatchet(val keyRatchet)\n    {\n        decryptor_->TransitionToKeyRatchet(MlsKeyRatchetFromJS(keyRatchet));\n    }\n\n    void TransitionToPassthroughMode(bool passthroughMode)\n    {\n        decryptor_->TransitionToPassthroughMode(passthroughMode);\n    }\n\n    size_t GetMaxPlaintextByteSize(MediaType mediaType, size_t ciphertextByteSize)\n    {\n        return decryptor_->GetMaxPlaintextByteSize(mediaType, ciphertextByteSize);\n    }\n\n    size_t Decrypt(MediaType mediaType, int framePtr, size_t frameLength, size_t frameCapacity)\n    {\n        auto frame = reinterpret_cast<uint8_t*>(framePtr);\n        auto frameView = MakeArrayView(const_cast<const uint8_t*>(frame), frameLength);\n        auto maxPlaintextByteSize = decryptor_->GetMaxPlaintextByteSize(mediaType, frameLength);\n\n        if (frameCapacity < maxPlaintextByteSize) {\n            DISCORD_LOG(LS_ERROR) << \"Frame capacity is less than the maximum plaintext size\";\n            return 0;\n        }\n        auto plaintextView = MakeArrayView(frame, maxPlaintextByteSize);\n\n        size_t bytesWritten = 0;\n        auto result = decryptor_->Decrypt(mediaType, frameView, plaintextView, &bytesWritten);\n        if (result != Decryptor::ResultCode::Success) {\n            return 0;\n        }\n        return bytesWritten;\n    }\n\nprivate:\n    std::unique_ptr<Decryptor> decryptor_;\n};\n\n} // namespace dave\n} // namespace discord\n\nEMSCRIPTEN_BINDINGS(dave)\n{\n    constant(\"kInitTransitionId\", discord::dave::kInitTransitionId);\n    constant(\"kDisabledVersion\", discord::dave::kDisabledVersion);\n\n    enum_<discord::dave::MediaType>(\"MediaType\")\n      .value(\"Audio\", discord::dave::MediaType::Audio)\n      .value(\"Video\", discord::dave::MediaType::Video);\n\n    enum_<discord::dave::Codec>(\"Codec\")\n      .value(\"Unknown\", discord::dave::Codec::Unknown)\n      .value(\"Opus\", discord::dave::Codec::Opus)\n      .value(\"VP8\", discord::dave::Codec::VP8)\n      .value(\"VP9\", discord::dave::Codec::VP9)\n      .value(\"H264\", discord::dave::Codec::H264)\n      .value(\"H265\", discord::dave::Codec::H265)\n      .value(\"AV1\", discord::dave::Codec::AV1);\n\n    function(\"MaxSupportedProtocolVersion\", &discord::dave::MaxSupportedProtocolVersion);\n\n    class_<::mlspp::SignaturePrivateKey>(\"SignaturePrivateKey\")\n      .smart_ptr<std::shared_ptr<::mlspp::SignaturePrivateKey>>(\"SignaturePrivateKeyPtr\");\n\n    class_<discord::dave::mls::TransientKeys>(\"TransientKeys\")\n      .constructor<>()\n      .function(\"GetTransientPrivateKey\",\n                &discord::dave::mls::TransientKeys::GetTransientPrivateKey)\n      .function(\"Clear\", &discord::dave::mls::TransientKeys::Clear);\n\n    class_<discord::dave::mls::SessionWrapper>(\"Session\")\n      .constructor<std::string, std::string, val>()\n      .function(\"Init\", &discord::dave::mls::SessionWrapper::Init)\n      .function(\"Reset\", &discord::dave::mls::SessionWrapper::Reset)\n      .function(\"SetProtocolVersion\", &discord::dave::mls::SessionWrapper::SetProtocolVersion)\n      .function(\"GetProtocolVersion\", &discord::dave::mls::SessionWrapper::GetProtocolVersion)\n      .function(\"GetLastEpochAuthenticator\",\n                &discord::dave::mls::SessionWrapper::GetLastEpochAuthenticator)\n      .function(\"SetExternalSender\", &discord::dave::mls::SessionWrapper::SetExternalSender)\n      .function(\"ProcessProposals\", &discord::dave::mls::SessionWrapper::ProcessProposals)\n      .function(\"ProcessCommit\", &discord::dave::mls::SessionWrapper::ProcessCommit)\n      .function(\"ProcessWelcome\", &discord::dave::mls::SessionWrapper::ProcessWelcome)\n      .function(\"GetMarshalledKeyPackage\",\n                &discord::dave::mls::SessionWrapper::GetMarshalledKeyPackage)\n      .function(\"GetKeyRatchet\", &discord::dave::mls::SessionWrapper::GetKeyRatchet);\n\n    class_<discord::dave::EncryptorWrapper>(\"Encryptor\")\n      .constructor<>()\n      .function(\"SetKeyRatchet\", &discord::dave::EncryptorWrapper::SetKeyRatchet)\n      .function(\"SetPassthroughMode\", &discord::dave::EncryptorWrapper::SetPassthroughMode)\n      .function(\"AssignSsrcToCodec\", &discord::dave::EncryptorWrapper::AssignSsrcToCodec)\n      .function(\"GetProtocolVersion\", &discord::dave::EncryptorWrapper::GetProtocolVersion)\n      .function(\"GetMaxCiphertextByteSize\",\n                &discord::dave::EncryptorWrapper::GetMaxCiphertextByteSize)\n      .function(\n        \"Encrypt\", &discord::dave::EncryptorWrapper::Encrypt, emscripten::allow_raw_pointers())\n      .function(\"SetProtocolVersionChangedCallback\",\n                &discord::dave::EncryptorWrapper::SetProtocolVersionChangedCallback);\n\n    class_<discord::dave::DecryptorWrapper>(\"Decryptor\")\n      .constructor<>()\n      .function(\"TransitionToKeyRatchet\", &discord::dave::DecryptorWrapper::TransitionToKeyRatchet)\n      .function(\"TransitionToPassthroughMode\",\n                &discord::dave::DecryptorWrapper::TransitionToPassthroughMode)\n      .function(\"GetMaxPlaintextByteSize\",\n                &discord::dave::DecryptorWrapper::GetMaxPlaintextByteSize)\n      .function(\n        \"Decrypt\", &discord::dave::DecryptorWrapper::Decrypt, emscripten::allow_raw_pointers());\n}\n"
  },
  {
    "path": "cpp/src/boringssl_cryptor.cpp",
    "content": "#include \"boringssl_cryptor.h\"\n\n#include <openssl/err.h>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\n\nvoid PrintSSLErrors()\n{\n    ERR_print_errors_cb(\n      [](const char* str, size_t len, [[maybe_unused]] void* ctx) {\n          DISCORD_LOG(LS_ERROR) << std::string(str, len);\n          return 1;\n      },\n      nullptr);\n}\n\nBoringSSLCryptor::BoringSSLCryptor(const EncryptionKey& encryptionKey)\n{\n    EVP_AEAD_CTX_zero(&cipherCtx_);\n\n    auto initResult = EVP_AEAD_CTX_init(&cipherCtx_,\n                                        EVP_aead_aes_128_gcm(),\n                                        encryptionKey.data(),\n                                        encryptionKey.size(),\n                                        kAesGcm128TruncatedTagBytes,\n                                        nullptr);\n\n    if (initResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to initialize AEAD context\";\n        PrintSSLErrors();\n    }\n}\n\nBoringSSLCryptor::~BoringSSLCryptor()\n{\n    EVP_AEAD_CTX_cleanup(&cipherCtx_);\n}\n\nbool BoringSSLCryptor::Encrypt(ArrayView<uint8_t> ciphertextBufferOut,\n                               ArrayView<const uint8_t> plaintextBuffer,\n                               ArrayView<const uint8_t> nonceBuffer,\n                               ArrayView<const uint8_t> additionalData,\n                               ArrayView<uint8_t> tagBufferOut)\n{\n    if (cipherCtx_.aead == nullptr) {\n        DISCORD_LOG(LS_ERROR) << \"Encrypt: AEAD context is not initialized\";\n        return false;\n    }\n\n    size_t tagSizeOut;\n    auto encryptResult = EVP_AEAD_CTX_seal_scatter(&cipherCtx_,\n                                                   ciphertextBufferOut.data(),\n                                                   tagBufferOut.data(),\n                                                   &tagSizeOut,\n                                                   kAesGcm128TruncatedTagBytes,\n                                                   nonceBuffer.data(),\n                                                   kAesGcm128NonceBytes,\n                                                   plaintextBuffer.data(),\n                                                   plaintextBuffer.size(),\n                                                   nullptr,\n                                                   0,\n                                                   additionalData.data(),\n                                                   additionalData.size());\n    if (encryptResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to encrypt data\";\n        PrintSSLErrors();\n    }\n\n    return encryptResult == 1;\n}\n\nbool BoringSSLCryptor::Decrypt(ArrayView<uint8_t> plaintextBufferOut,\n                               ArrayView<const uint8_t> ciphertextBuffer,\n                               ArrayView<const uint8_t> tagBuffer,\n                               ArrayView<const uint8_t> nonceBuffer,\n                               ArrayView<const uint8_t> additionalData)\n{\n    if (cipherCtx_.aead == nullptr) {\n        DISCORD_LOG(LS_ERROR) << \"Decrypt: AEAD context is not initialized\";\n        return false;\n    }\n\n    auto decryptResult = EVP_AEAD_CTX_open_gather(&cipherCtx_,\n                                                  plaintextBufferOut.data(),\n                                                  nonceBuffer.data(),\n                                                  kAesGcm128NonceBytes,\n                                                  ciphertextBuffer.data(),\n                                                  ciphertextBuffer.size(),\n                                                  tagBuffer.data(),\n                                                  kAesGcm128TruncatedTagBytes,\n                                                  additionalData.data(),\n                                                  additionalData.size());\n\n    return decryptResult == 1;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/boringssl_cryptor.h",
    "content": "#pragma once\n\n#include <openssl/aead.h>\n\n#include \"cryptor.h\"\n\nnamespace discord {\nnamespace dave {\n\nclass BoringSSLCryptor : public ICryptor {\npublic:\n    BoringSSLCryptor(const EncryptionKey& encryptionKey);\n    ~BoringSSLCryptor();\n\n    bool IsValid() const { return cipherCtx_.aead != nullptr; }\n\n    bool Encrypt(ArrayView<uint8_t> ciphertextBufferOut,\n                 ArrayView<const uint8_t> plaintextBuffer,\n                 ArrayView<const uint8_t> nonceBuffer,\n                 ArrayView<const uint8_t> additionalData,\n                 ArrayView<uint8_t> tagBufferOut) override;\n    bool Decrypt(ArrayView<uint8_t> plaintextBufferOut,\n                 ArrayView<const uint8_t> ciphertextBuffer,\n                 ArrayView<const uint8_t> tagBuffer,\n                 ArrayView<const uint8_t> nonceBuffer,\n                 ArrayView<const uint8_t> additionalData) override;\n\nprivate:\n    EVP_AEAD_CTX cipherCtx_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/codec_utils.cpp",
    "content": "#include \"codec_utils.h\"\n\n#include <cassert>\n#include <limits>\n#include <optional>\n\n#include <dave/logger.h>\n\n#include \"common.h\"\n#include \"utils/leb128.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace codec_utils {\n\nUnencryptedFrameHeaderSize BytesCoveringH264PPS(const uint8_t* payload,\n                                                const uint64_t sizeRemaining)\n{\n    // the payload starts with three exponential golomb encoded values\n    // (first_mb_in_slice, sps_id, pps_id)\n    // the depacketizer needs the pps_id unencrypted\n    // and the payload has RBSP encoding that we need to work around\n\n    constexpr uint8_t kEmulationPreventionByte = 0x03;\n\n    uint64_t payloadBitIndex = 0;\n    auto zeroBitCount = 0;\n    auto parsedExpGolombValues = 0;\n\n    while (payloadBitIndex < sizeRemaining * 8 && parsedExpGolombValues < 3) {\n        auto bitIndex = payloadBitIndex % 8;\n        auto byteIndex = payloadBitIndex / 8;\n        auto payloadByte = payload[byteIndex];\n\n        // if we're starting a new byte\n        // check if this is an emulation prevention byte\n        // which we skip over\n        if (bitIndex == 0) {\n            if (byteIndex >= 2 && payloadByte == kEmulationPreventionByte &&\n                payload[byteIndex - 1] == 0 && payload[byteIndex - 2] == 0) {\n                payloadBitIndex += 8;\n                continue;\n            }\n        }\n\n        if ((payloadByte & (1 << (7 - bitIndex))) == 0) {\n            // still in the run of leading zero bits\n            ++zeroBitCount;\n            ++payloadBitIndex;\n\n            if (zeroBitCount >= 32) {\n                assert(false && \"Unexpectedly large exponential golomb encoded value\");\n                return 0;\n            }\n        }\n        else {\n            // we hit a one\n            // skip forward the number of bits dictated by the leading number of zeroes\n            parsedExpGolombValues += 1;\n            payloadBitIndex += 1 + zeroBitCount;\n            zeroBitCount = 0;\n        }\n    }\n\n    // return the number of bytes that covers the last exp golomb encoded value\n    auto result = (payloadBitIndex / 8) + 1;\n    if (result > std::numeric_limits<UnencryptedFrameHeaderSize>::max()) {\n        DISCORD_LOG(LS_WARNING)\n          << \"BytesCoveringH264PPS result cannot fit in UnencryptedFrameHeaderSize\";\n        return 0;\n    }\n    else {\n        return static_cast<UnencryptedFrameHeaderSize>(result);\n    }\n}\n\nconst uint8_t kH26XNaluLongStartCode[] = {0, 0, 0, 1};\nconstexpr uint8_t kH26XNaluShortStartSequenceSize = 3;\n\nusing IndexStartCodeSizePair = std::pair<size_t, size_t>;\n\nstd::optional<IndexStartCodeSizePair> FindNextH26XNaluIndex(const uint8_t* buffer,\n                                                            const size_t bufferSize,\n                                                            const size_t searchStartIndex = 0)\n{\n    constexpr uint8_t kH26XStartCodeHighestPossibleValue = 1;\n    constexpr uint8_t kH26XStartCodeEndByteValue = 1;\n    constexpr uint8_t kH26XStartCodeLeadingBytesValue = 0;\n\n    if (bufferSize < kH26XNaluShortStartSequenceSize) {\n        return std::nullopt;\n    }\n\n    // look for NAL unit 3 or 4 byte start code\n    for (size_t i = searchStartIndex; i < bufferSize - kH26XNaluShortStartSequenceSize;) {\n        if (buffer[i + 2] > kH26XStartCodeHighestPossibleValue) {\n            // third byte is not 0 or 1, can't be a start code\n            i += kH26XNaluShortStartSequenceSize;\n        }\n        else if (buffer[i + 2] == kH26XStartCodeEndByteValue) {\n            // third byte matches the start code end byte, might be a start code sequence\n            if (buffer[i + 1] == kH26XStartCodeLeadingBytesValue &&\n                buffer[i] == kH26XStartCodeLeadingBytesValue) {\n                // confirmed start sequence {0, 0, 1}\n                auto nalUnitStartIndex = i + kH26XNaluShortStartSequenceSize;\n\n                if (i >= 1 && buffer[i - 1] == kH26XStartCodeLeadingBytesValue) {\n                    // 4 byte start code\n                    return std::optional<IndexStartCodeSizePair>({nalUnitStartIndex, 4});\n                }\n                else {\n                    // 3 byte start code\n                    return std::optional<IndexStartCodeSizePair>({nalUnitStartIndex, 3});\n                }\n            }\n\n            i += kH26XNaluShortStartSequenceSize;\n        }\n        else {\n            // third byte is 0, might be a four byte start code\n            ++i;\n        }\n    }\n\n    return std::nullopt;\n}\n\nbool ProcessFrameOpus(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    processor.AddEncryptedBytes(frame.data(), frame.size());\n    return true;\n}\n\nbool ProcessFrameVp8(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    constexpr uint8_t kVP8KeyFrameUnencryptedBytes = 10;\n    constexpr uint8_t kVP8DeltaFrameUnencryptedBytes = 1;\n\n    // parse the VP8 payload header to determine if it's a key frame\n    // https://datatracker.ietf.org/doc/html/rfc7741#section-4.3\n\n    // 0 1 2 3 4 5 6 7\n    // +-+-+-+-+-+-+-+-+\n    // |Size0|H| VER |P|\n    // +-+-+-+-+-+-+-+-+\n    // P is an inverse key frame flag\n\n    // if this is a key frame the depacketizer will read 10 bytes into the payload header\n    // if this is a delta frame the depacketizer only needs the first byte of the payload\n    // header (since that's where the key frame flag is)\n\n    size_t unencryptedHeaderBytes = 0;\n    if ((frame.data()[0] & 0x01) == 0) {\n        unencryptedHeaderBytes = kVP8KeyFrameUnencryptedBytes;\n    }\n    else {\n        unencryptedHeaderBytes = kVP8DeltaFrameUnencryptedBytes;\n    }\n\n    processor.AddUnencryptedBytes(frame.data(), unencryptedHeaderBytes);\n    processor.AddEncryptedBytes(frame.data() + unencryptedHeaderBytes,\n                                frame.size() - unencryptedHeaderBytes);\n    return true;\n}\n\nbool ProcessFrameVp9(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    // payload descriptor is unencrypted in each packet\n    // and includes all information the depacketizer needs\n    processor.AddEncryptedBytes(frame.data(), frame.size());\n    return true;\n}\n\nbool ProcessFrameH264(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    // minimize the amount of unencrypted header data for H264 depending on the NAL unit\n    // type from WebRTC, see: src/modules/rtp_rtcp/source/rtp_format_h264.cc\n    // src/common_video/h264/h264_common.cc\n    // src/modules/rtp_rtcp/source/video_rtp_depacketizer_h264.cc\n\n    // constexpr uint8_t kH264SBit = 0x80;\n    constexpr uint8_t kH264NalHeaderTypeMask = 0x1F;\n    constexpr uint8_t kH264NalTypeSlice = 1;\n    constexpr uint8_t kH264NalTypeIdr = 5;\n    constexpr uint8_t kH264NalUnitHeaderSize = 1;\n\n    // this frame can be packetized as a STAP-A or a FU-A\n    // so we need to look at the first NAL units to determine how many bytes\n    // the packetizer/depacketizer will need into the payload\n    if (frame.size() < kH26XNaluShortStartSequenceSize + kH264NalUnitHeaderSize) {\n        assert(false && \"H264 frame is too small to contain a NAL unit\");\n        DISCORD_LOG(LS_WARNING) << \"H264 frame is too small to contain a NAL unit\";\n        return false;\n    }\n\n    auto naluIndexPair = FindNextH26XNaluIndex(frame.data(), frame.size());\n    while (naluIndexPair && naluIndexPair->first < frame.size() - 1) {\n        auto [nalUnitStartIndex, startCodeSize] = *naluIndexPair;\n\n        auto nalType = frame.data()[nalUnitStartIndex] & kH264NalHeaderTypeMask;\n\n        // copy the start code and then the NAL unit\n\n        // Because WebRTC will convert them all start codes to 4-byte on the receiver side\n        // always write a long start code and then the NAL unit\n        processor.AddUnencryptedBytes(kH26XNaluLongStartCode, sizeof(kH26XNaluLongStartCode));\n\n        auto nextNaluIndexPair =\n          FindNextH26XNaluIndex(frame.data(), frame.size(), nalUnitStartIndex);\n        auto nextNaluStart = nextNaluIndexPair.has_value()\n          ? nextNaluIndexPair->first - nextNaluIndexPair->second\n          : frame.size();\n\n        if (nalType == kH264NalTypeSlice || nalType == kH264NalTypeIdr) {\n            // once we've hit a slice or an IDR\n            // we just need to cover getting to the PPS ID\n            auto nalUnitPayloadStart = nalUnitStartIndex + kH264NalUnitHeaderSize;\n            auto nalUnitPPSBytes = BytesCoveringH264PPS(frame.data() + nalUnitPayloadStart,\n                                                        frame.size() - nalUnitPayloadStart);\n\n            processor.AddUnencryptedBytes(frame.data() + nalUnitStartIndex,\n                                          kH264NalUnitHeaderSize + nalUnitPPSBytes);\n            processor.AddEncryptedBytes(\n              frame.data() + nalUnitStartIndex + kH264NalUnitHeaderSize + nalUnitPPSBytes,\n              nextNaluStart - nalUnitStartIndex - kH264NalUnitHeaderSize - nalUnitPPSBytes);\n        }\n        else {\n            // copy the whole NAL unit\n            processor.AddUnencryptedBytes(frame.data() + nalUnitStartIndex,\n                                          nextNaluStart - nalUnitStartIndex);\n        }\n\n        naluIndexPair = nextNaluIndexPair;\n    }\n\n    return true;\n}\n\nbool ProcessFrameH265(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    // minimize the amount of unencrypted header data for H265 depending on the NAL unit\n    // type from WebRTC, see: src/modules/rtp_rtcp/source/rtp_format_h265.cc\n    // src/common_video/h265/h265_common.cc\n    // src/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc\n\n    constexpr uint8_t kH265NalHeaderTypeMask = 0x7E;\n    constexpr uint8_t kH265NalTypeVclCutoff = 32;\n    constexpr uint8_t kH265NalUnitHeaderSize = 2;\n\n    // this frame can be packetized as a STAP-A or a FU-A\n    // so we need to look at the first NAL units to determine how many bytes\n    // the packetizer/depacketizer will need into the payload\n    if (frame.size() < kH26XNaluShortStartSequenceSize + kH265NalUnitHeaderSize) {\n        assert(false && \"H265 frame is too small to contain a NAL unit\");\n        DISCORD_LOG(LS_WARNING) << \"H265 frame is too small to contain a NAL unit\";\n        return false;\n    }\n\n    // look for NAL unit 3 or 4 byte start code\n    auto naluIndexPair = FindNextH26XNaluIndex(frame.data(), frame.size());\n    while (naluIndexPair && naluIndexPair->first < frame.size() - 1) {\n        auto [nalUnitStartIndex, startCodeSize] = *naluIndexPair;\n\n        uint8_t nalType = (frame.data()[nalUnitStartIndex] & kH265NalHeaderTypeMask) >> 1;\n\n        // copy the start code and then the NAL unit\n\n        // Because WebRTC will convert them all start codes to 4-byte on the receiver side\n        // always write a long start code and then the NAL unit\n        processor.AddUnencryptedBytes(kH26XNaluLongStartCode, sizeof(kH26XNaluLongStartCode));\n\n        auto nextNaluIndexPair =\n          FindNextH26XNaluIndex(frame.data(), frame.size(), nalUnitStartIndex);\n        auto nextNaluStart = nextNaluIndexPair.has_value()\n          ? nextNaluIndexPair->first - nextNaluIndexPair->second\n          : frame.size();\n\n        if (nalType < kH265NalTypeVclCutoff) {\n            // found a VCL NAL, encrypt the payload only\n            processor.AddUnencryptedBytes(frame.data() + nalUnitStartIndex, kH265NalUnitHeaderSize);\n            processor.AddEncryptedBytes(frame.data() + nalUnitStartIndex + kH265NalUnitHeaderSize,\n                                        nextNaluStart - nalUnitStartIndex - kH265NalUnitHeaderSize);\n        }\n        else {\n            // copy the whole NAL unit\n            processor.AddUnencryptedBytes(frame.data() + nalUnitStartIndex,\n                                          nextNaluStart - nalUnitStartIndex);\n        }\n\n        naluIndexPair = nextNaluIndexPair;\n    }\n\n    return true;\n}\n\nbool ProcessFrameAv1(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame)\n{\n    constexpr uint8_t kAv1ObuHeaderHasExtensionMask = 0b0'0000'100;\n    constexpr uint8_t kAv1ObuHeaderHasSizeMask = 0b0'0000'010;\n    constexpr uint8_t kAv1ObuHeaderTypeMask = 0b0'1111'000;\n    constexpr uint8_t kObuTypeTemporalDelimiter = 2;\n    constexpr uint8_t kObuTypeTileList = 8;\n    constexpr uint8_t kObuTypePadding = 15;\n    constexpr uint8_t kObuExtensionSizeBytes = 1;\n\n    size_t i = 0;\n    while (i < frame.size()) {\n        // Read the OBU header.\n        size_t obuHeaderIndex = i;\n        uint8_t obuHeader = frame.data()[obuHeaderIndex];\n        i += sizeof(obuHeader);\n\n        bool obuHasExtension = obuHeader & kAv1ObuHeaderHasExtensionMask;\n        bool obuHasSize = obuHeader & kAv1ObuHeaderHasSizeMask;\n        int obuType = (obuHeader & kAv1ObuHeaderTypeMask) >> 3;\n\n        if (obuHasExtension) {\n            // Skip extension byte\n            i += kObuExtensionSizeBytes;\n        }\n\n        if (i >= frame.size()) {\n            // Malformed frame\n            assert(false && \"Malformed AV1 frame: header overflows frame\");\n            DISCORD_LOG(LS_WARNING) << \"Malformed AV1 frame: header overflows frame\";\n            return false;\n        }\n\n        size_t obuPayloadSize = 0;\n        if (obuHasSize) {\n            // Read payload size\n            const uint8_t* start = frame.data() + i;\n            const uint8_t* ptr = start;\n            obuPayloadSize = ReadLeb128(ptr, frame.end());\n            if (!ptr) {\n                // Malformed frame\n                assert(false && \"Malformed AV1 frame: invalid LEB128 size\");\n                DISCORD_LOG(LS_WARNING) << \"Malformed AV1 frame: invalid LEB128 size\";\n                return false;\n            }\n            i += ptr - start;\n        }\n        else {\n            // If the size is not present, the OBU extends to the end of the frame.\n            obuPayloadSize = frame.size() - i;\n        }\n\n        const auto obuPayloadIndex = i;\n\n        if (i + obuPayloadSize > frame.size()) {\n            // Malformed frame\n            assert(false && \"Malformed AV1 frame: payload overflows frame\");\n            DISCORD_LOG(LS_WARNING) << \"Malformed AV1 frame: payload overflows frame\";\n            return false;\n        }\n\n        i += obuPayloadSize;\n\n        // We only copy the OBUs that will not get dropped by the packetizer\n        if (obuType != kObuTypeTemporalDelimiter && obuType != kObuTypeTileList &&\n            obuType != kObuTypePadding) {\n            // if this is the last OBU, we may need to flip the \"has size\" bit\n            // which allows us to append necessary protocol data to the frame\n            bool rewrittenWithoutSize = false;\n\n            if (i == frame.size() && obuHasSize) {\n                // Flip the \"has size\" bit\n                obuHeader &= ~kAv1ObuHeaderHasSizeMask;\n                rewrittenWithoutSize = true;\n            }\n\n            // write the OBU header unencrypted\n            processor.AddUnencryptedBytes(&obuHeader, sizeof(obuHeader));\n            if (obuHasExtension) {\n                // write the extension byte unencrypted\n                processor.AddUnencryptedBytes(frame.data() + obuHeaderIndex + sizeof(obuHeader),\n                                              kObuExtensionSizeBytes);\n            }\n\n            // write the OBU payload size unencrypted if it was present and we didn't rewrite\n            // without it\n            if (obuHasSize && !rewrittenWithoutSize) {\n                // The AMD AV1 encoder may pad LEB128 encoded sizes with a zero byte which the\n                // webrtc packetizer removes. To prevent the packetizer from changing the frame,\n                // we sanitize the size by re-writing it ourselves\n                uint8_t leb128Buffer[Leb128MaxSize];\n                size_t additionalBytesToWrite = WriteLeb128(obuPayloadSize, leb128Buffer);\n                processor.AddUnencryptedBytes(leb128Buffer, additionalBytesToWrite);\n            }\n\n            // add the OBU payload, encrypted\n            processor.AddEncryptedBytes(frame.data() + obuPayloadIndex, obuPayloadSize);\n        }\n    }\n\n    return true;\n}\n\nbool ValidateEncryptedFrame(OutboundFrameProcessor& processor, ArrayView<uint8_t> frame)\n{\n    auto codec = processor.GetCodec();\n    if (codec != Codec::H264 && codec != Codec::H265) {\n        return true;\n    }\n\n    static_assert(kH26XNaluShortStartSequenceSize - 1 >= 0, \"Padding will overflow!\");\n    constexpr size_t Padding = kH26XNaluShortStartSequenceSize - 1;\n\n    const auto& unencryptedRanges = processor.GetUnencryptedRanges();\n\n    // H264 and H265 ciphertexts cannot contain a 3 or 4 byte start code {0, 0, 1}\n    // otherwise the packetizer gets confused\n    // and the frame we get on the decryption side will be shifted and fail to decrypt\n    size_t encryptedSectionStart = 0;\n    for (auto& range : unencryptedRanges) {\n        if (encryptedSectionStart == range.offset) {\n            encryptedSectionStart += range.size;\n            continue;\n        }\n\n        auto start = encryptedSectionStart - std::min(encryptedSectionStart, size_t{Padding});\n        auto end = std::min(range.offset + Padding, frame.size());\n        if (FindNextH26XNaluIndex(frame.data() + start, end - start)) {\n            return false;\n        }\n\n        encryptedSectionStart = range.offset + range.size;\n    }\n\n    if (encryptedSectionStart == frame.size()) {\n        return true;\n    }\n\n    auto start = encryptedSectionStart - std::min(encryptedSectionStart, size_t{Padding});\n    auto end = frame.size();\n    if (FindNextH26XNaluIndex(frame.data() + start, end - start)) {\n        return false;\n    }\n\n    return true;\n}\n\n} // namespace codec_utils\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/codec_utils.h",
    "content": "#pragma once\n\n#include <dave/array_view.h>\n\n#include \"common.h\"\n#include \"frame_processors.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace codec_utils {\n\nbool ProcessFrameOpus(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\nbool ProcessFrameVp8(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\nbool ProcessFrameVp9(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\nbool ProcessFrameH264(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\nbool ProcessFrameH265(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\nbool ProcessFrameAv1(OutboundFrameProcessor& processor, ArrayView<const uint8_t> frame);\n\nbool ValidateEncryptedFrame(OutboundFrameProcessor& processor, ArrayView<uint8_t> frame);\n\n} // namespace codec_utils\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/common.h",
    "content": "#pragma once\n\n#include <array>\n#include <chrono>\n#include <map>\n#include <optional>\n#include <string>\n#include <variant>\n#include <vector>\n\n#include <dave/version.h>\n\nnamespace discord {\nnamespace dave {\n\nusing UnencryptedFrameHeaderSize = uint16_t;\nusing TruncatedSyncNonce = uint32_t;\nusing MagicMarker = uint16_t;\nusing TransitionId = uint16_t;\nusing SupplementalBytesSize = uint8_t;\n\nconstexpr MagicMarker kMarkerBytes = 0xFAFA;\n\n// Layout constants\nconstexpr size_t kAesGcm128KeyBytes = 16;\nconstexpr size_t kAesGcm128NonceBytes = 12;\nconstexpr size_t kAesGcm128TruncatedSyncNonceBytes = 4;\nconstexpr size_t kAesGcm128TruncatedSyncNonceOffset =\n  kAesGcm128NonceBytes - kAesGcm128TruncatedSyncNonceBytes;\nconstexpr size_t kAesGcm128TruncatedTagBytes = 8;\nconstexpr size_t kRatchetGenerationBytes = 1;\nconstexpr size_t kRatchetGenerationShiftBits =\n  8 * (kAesGcm128TruncatedSyncNonceBytes - kRatchetGenerationBytes);\nconstexpr size_t kSupplementalBytes =\n  kAesGcm128TruncatedTagBytes + sizeof(SupplementalBytesSize) + sizeof(MagicMarker);\nconstexpr size_t kTransformPaddingBytes = 64;\n\n// Timing constants\nconstexpr auto kCryptorExpiry = std::chrono::seconds(10);\n\n// Behavior constants\nconstexpr auto kInitTransitionId = 0;\nconstexpr auto kDisabledVersion = 0;\nconstexpr auto kMaxGenerationGap = 250;\nconstexpr auto kMaxMissingNonces = 1000;\nconstexpr auto kGenerationWrap = 1 << (8 * kRatchetGenerationBytes);\nconstexpr auto kMaxFramesPerSecond = 50 + 2 * 60; // 50 audio frames + 2 * 60fps video streams\nconstexpr std::array<uint8_t, 3> kOpusSilencePacket = {0xF8, 0xFF, 0xFE};\n\n// Utility routine for variant return types\ntemplate <class T, class V>\ninline std::optional<T> GetOptional(V&& variant)\n{\n    if (auto map = std::get_if<T>(&variant)) {\n        if constexpr (std::is_rvalue_reference_v<decltype(variant)>) {\n            return std::move(*map);\n        }\n        else {\n            return *map;\n        }\n    }\n    else {\n        return std::nullopt;\n    }\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/cryptor.cpp",
    "content": "#include \"cryptor.h\"\n\n#ifdef WITH_BORINGSSL\n#include \"boringssl_cryptor.h\"\n#else\n#include \"openssl_cryptor.h\"\n#endif\n\nnamespace discord {\nnamespace dave {\n\nstd::unique_ptr<ICryptor> CreateCryptor(const EncryptionKey& encryptionKey)\n{\n#ifdef WITH_BORINGSSL\n    auto cryptor = std::make_unique<BoringSSLCryptor>(encryptionKey);\n#else\n    auto cryptor = std::make_unique<OpenSSLCryptor>(encryptionKey);\n#endif\n\n    return cryptor->IsValid() ? std::move(cryptor) : nullptr;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/cryptor.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include <dave/array_view.h>\n#include <dave/dave_interfaces.h>\n\nnamespace discord {\nnamespace dave {\n\nclass ICryptor {\npublic:\n    virtual ~ICryptor() = default;\n\n    virtual bool Encrypt(ArrayView<uint8_t> ciphertextBufferOut,\n                         ArrayView<const uint8_t> plaintextBuffer,\n                         ArrayView<const uint8_t> nonceBuffer,\n                         ArrayView<const uint8_t> additionalData,\n                         ArrayView<uint8_t> tagBufferOut) = 0;\n    virtual bool Decrypt(ArrayView<uint8_t> plaintextBufferOut,\n                         ArrayView<const uint8_t> ciphertextBuffer,\n                         ArrayView<const uint8_t> tagBuffer,\n                         ArrayView<const uint8_t> nonceBuffer,\n                         ArrayView<const uint8_t> additionalData) = 0;\n};\n\nstd::unique_ptr<ICryptor> CreateCryptor(const EncryptionKey& encryptionKey);\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/cryptor_manager.cpp",
    "content": "#include \"cryptor_manager.h\"\n\n#include <limits>\n\n#include <dave/logger.h>\n\n#include <bytes/bytes.h>\n\nusing namespace std::chrono_literals;\n\nnamespace discord {\nnamespace dave {\n\nKeyGeneration ComputeWrappedGeneration(KeyGeneration oldest, KeyGeneration generation)\n{\n    // Assume generation is greater than or equal to oldest, this may be wrong in a few cases but\n    // will be caught by the max generation gap check.\n    auto remainder = oldest % kGenerationWrap;\n    auto factor = oldest / kGenerationWrap + (generation < remainder ? 1 : 0);\n    return factor * kGenerationWrap + generation;\n}\n\nBigNonce ComputeWrappedBigNonce(KeyGeneration generation, TruncatedSyncNonce nonce)\n{\n    // Remove the generation bits from the nonce\n    auto maskedNonce = nonce & ((1 << kRatchetGenerationShiftBits) - 1);\n    // Add the wrapped generation bits back in\n    return static_cast<BigNonce>(generation) << kRatchetGenerationShiftBits | maskedNonce;\n}\n\nCryptorManager::CryptorManager(const IClock& clock, std::unique_ptr<IKeyRatchet> keyRatchet)\n  : clock_(clock)\n  , keyRatchet_(std::move(keyRatchet))\n  , ratchetCreation_(clock.Now())\n  , ratchetExpiry_(TimePoint::max())\n{\n}\n\nbool CryptorManager::CanProcessNonce(KeyGeneration generation, TruncatedSyncNonce nonce) const\n{\n    if (!newestProcessedNonce_) {\n        return true;\n    }\n\n    auto bigNonce = ComputeWrappedBigNonce(generation, nonce);\n    return bigNonce > *newestProcessedNonce_ ||\n      std::find(missingNonces_.rbegin(), missingNonces_.rend(), bigNonce) != missingNonces_.rend();\n}\n\nICryptor* CryptorManager::GetCryptor(KeyGeneration generation)\n{\n    CleanupExpiredCryptors();\n\n    if (generation < oldestGeneration_) {\n        DISCORD_LOG(LS_INFO) << \"Received frame with old generation: \" << generation\n                             << \", oldest generation: \" << oldestGeneration_;\n        return nullptr;\n    }\n\n    if (generation > newestGeneration_ + kMaxGenerationGap) {\n        DISCORD_LOG(LS_INFO) << \"Received frame with future generation: \" << generation\n                             << \", newest generation: \" << newestGeneration_;\n        return nullptr;\n    }\n\n    auto ratchetLifetimeSec =\n      std::chrono::duration_cast<std::chrono::seconds>(clock_.Now() - ratchetCreation_).count();\n    auto maxLifetimeFrames = kMaxFramesPerSecond * ratchetLifetimeSec;\n    auto maxLifetimeGenerations = maxLifetimeFrames >> kRatchetGenerationShiftBits;\n    if (generation > maxLifetimeGenerations) {\n        DISCORD_LOG(LS_INFO) << \"Received frame with generation \" << generation\n                             << \" beyond ratchet max lifetime generations: \"\n                             << maxLifetimeGenerations\n                             << \", ratchet lifetime: \" << ratchetLifetimeSec << \"s\";\n        return nullptr;\n    }\n\n    auto it = cryptors_.find(generation);\n    if (it == cryptors_.end()) {\n        // We don't have a cryptor for this generation, create one\n        std::tie(it, std::ignore) = cryptors_.emplace(generation, MakeExpiringCryptor(generation));\n    }\n\n    // Return a non-owning pointer to the cryptor\n    auto& [cryptor, expiry] = it->second;\n    return cryptor.get();\n}\n\nvoid CryptorManager::ReportCryptorSuccess(KeyGeneration generation, TruncatedSyncNonce nonce)\n{\n    auto bigNonce = ComputeWrappedBigNonce(generation, nonce);\n\n    // Add any missing nonces to the queue\n    if (!newestProcessedNonce_) {\n        newestProcessedNonce_ = bigNonce;\n    }\n    else if (bigNonce > *newestProcessedNonce_) {\n        auto missingNonces =\n          std::min(bigNonce - *newestProcessedNonce_ - 1, static_cast<uint64_t>(kMaxMissingNonces));\n\n        while (!missingNonces_.empty() &&\n               missingNonces_.size() + missingNonces > kMaxMissingNonces) {\n            missingNonces_.pop_front();\n        }\n\n        for (auto i = bigNonce - missingNonces; i < bigNonce; ++i) {\n            missingNonces_.push_back(i);\n        }\n\n        // Update the newest processed nonce\n        newestProcessedNonce_ = bigNonce;\n    }\n    else {\n        auto it = std::find(missingNonces_.begin(), missingNonces_.end(), bigNonce);\n        if (it != missingNonces_.end()) {\n            missingNonces_.erase(it);\n        }\n    }\n\n    if (generation <= newestGeneration_ || cryptors_.find(generation) == cryptors_.end()) {\n        return;\n    }\n    DISCORD_LOG(LS_INFO) << \"Reporting cryptor success, generation: \" << generation;\n    newestGeneration_ = generation;\n\n    // Update the expiry time for all old cryptors\n    const auto expiryTime = clock_.Now() + kCryptorExpiry;\n    for (auto& [gen, cryptor] : cryptors_) {\n        if (gen < newestGeneration_) {\n            DISCORD_LOG(LS_INFO) << \"Updating expiry for cryptor, generation: \" << gen;\n            cryptor.expiry = std::min(cryptor.expiry, expiryTime);\n        }\n    }\n}\n\nKeyGeneration CryptorManager::ComputeWrappedGeneration(KeyGeneration generation) const\n{\n    return ::discord::dave::ComputeWrappedGeneration(oldestGeneration_, generation);\n}\n\nCryptorManager::ExpiringCryptor CryptorManager::MakeExpiringCryptor(KeyGeneration generation)\n{\n    // Get the new key from the ratchet\n    auto encryptionKey = keyRatchet_->GetKey(generation);\n    auto expiryTime = TimePoint::max();\n\n    // If we got frames out of order, we might have to create a cryptor for an old generation\n    // In that case, create it with a non-infinite expiry time as we have already transitioned\n    // to a newer generation\n    if (generation < newestGeneration_) {\n        DISCORD_LOG(LS_INFO) << \"Creating cryptor for old generation: \" << generation;\n        expiryTime = clock_.Now() + kCryptorExpiry;\n    }\n    else {\n        DISCORD_LOG(LS_INFO) << \"Creating cryptor for new generation: \" << generation;\n    }\n\n    return {CreateCryptor(encryptionKey), expiryTime};\n}\n\nvoid CryptorManager::CleanupExpiredCryptors()\n{\n    for (auto it = cryptors_.begin(); it != cryptors_.end();) {\n        auto& [generation, cryptor] = *it;\n\n        bool expired = cryptor.expiry < clock_.Now();\n        if (expired) {\n            DISCORD_LOG(LS_INFO) << \"Removing expired cryptor, generation: \" << generation;\n        }\n\n        it = expired ? cryptors_.erase(it) : ++it;\n    }\n\n    while (oldestGeneration_ < newestGeneration_ &&\n           cryptors_.find(oldestGeneration_) == cryptors_.end()) {\n        DISCORD_LOG(LS_INFO) << \"Deleting key for old generation: \" << oldestGeneration_;\n        keyRatchet_->DeleteKey(oldestGeneration_);\n        ++oldestGeneration_;\n    }\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/cryptor_manager.h",
    "content": "#pragma once\n\n#include <deque>\n#include <memory>\n#include <optional>\n#include <unordered_map>\n\n#include \"common.h\"\n#include \"cryptor.h\"\n#include \"utils/clock.h\"\n\nnamespace discord {\nnamespace dave {\n\nKeyGeneration ComputeWrappedGeneration(KeyGeneration oldest, KeyGeneration generation);\n\nusing BigNonce = uint64_t;\nBigNonce ComputeWrappedBigNonce(KeyGeneration generation, TruncatedSyncNonce nonce);\n\nclass CryptorManager {\npublic:\n    using TimePoint = typename IClock::TimePoint;\n\n    CryptorManager(const IClock& clock, std::unique_ptr<IKeyRatchet> keyRatchet);\n\n    void UpdateExpiry(TimePoint expiry) { ratchetExpiry_ = expiry; }\n    bool IsExpired() const { return clock_.Now() > ratchetExpiry_; }\n\n    bool CanProcessNonce(KeyGeneration generation, TruncatedSyncNonce nonce) const;\n    KeyGeneration ComputeWrappedGeneration(KeyGeneration generation) const;\n\n    ICryptor* GetCryptor(KeyGeneration generation);\n    void ReportCryptorSuccess(KeyGeneration generation, TruncatedSyncNonce nonce);\n\nprivate:\n    struct ExpiringCryptor {\n        std::unique_ptr<ICryptor> cryptor;\n        TimePoint expiry;\n    };\n\n    ExpiringCryptor MakeExpiringCryptor(KeyGeneration generation);\n    void CleanupExpiredCryptors();\n\n    const IClock& clock_;\n    std::unique_ptr<IKeyRatchet> keyRatchet_;\n    std::unordered_map<KeyGeneration, ExpiringCryptor> cryptors_;\n\n    TimePoint ratchetCreation_;\n    TimePoint ratchetExpiry_;\n    KeyGeneration oldestGeneration_{0};\n    KeyGeneration newestGeneration_{0};\n\n    std::optional<BigNonce> newestProcessedNonce_;\n    std::deque<BigNonce> missingNonces_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/decryptor.cpp",
    "content": "#include \"decryptor.h\"\n\n#include <cstring>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n\n#include \"common.h\"\n#include \"utils/leb128.h\"\n#include \"utils/scope_exit.h\"\n\nusing namespace std::chrono_literals;\n\nnamespace discord {\nnamespace dave {\n\nconstexpr auto kStatsInterval = 10s;\n\nstd::unique_ptr<IDecryptor> CreateDecryptor()\n{\n    return std::make_unique<Decryptor>();\n}\n\nvoid Decryptor::TransitionToKeyRatchet(std::unique_ptr<IKeyRatchet> keyRatchet,\n                                       Duration transitionExpiry)\n{\n    DISCORD_LOG(LS_INFO) << \"Transitioning to new key ratchet: \" << keyRatchet.get()\n                         << \", expiry: \" << transitionExpiry.count();\n\n    // Update the expiry time for all existing cryptor managers\n    UpdateCryptorManagerExpiry(transitionExpiry);\n\n    if (keyRatchet) {\n        cryptorManagers_.emplace_back(clock_, std::move(keyRatchet));\n    }\n}\n\nvoid Decryptor::TransitionToPassthroughMode(bool passthroughMode, Duration transitionExpiry)\n{\n    if (passthroughMode) {\n        allowPassThroughUntil_ = TimePoint::max();\n    }\n    else {\n        // Update the pass through mode expiry\n        auto maxExpiry = clock_.Now() + transitionExpiry;\n        allowPassThroughUntil_ = std::min(allowPassThroughUntil_, maxExpiry);\n    }\n}\n\nDecryptor::ResultCode Decryptor::Decrypt(MediaType mediaType,\n                                         ArrayView<const uint8_t> encryptedFrame,\n                                         ArrayView<uint8_t> frame,\n                                         size_t* bytesWritten)\n{\n    if (mediaType != Audio && mediaType != Video) {\n        DISCORD_LOG(LS_WARNING) << \"Decrypt failed, invalid media type: \"\n                                << static_cast<int>(mediaType);\n        *bytesWritten = 0;\n        return ResultCode::DecryptionFailure;\n    }\n    auto& stats = stats_[mediaType];\n\n    auto start = clock_.Now();\n\n    auto localFrame = GetOrCreateFrameProcessor();\n    ScopeExit cleanup([&] { ReturnFrameProcessor(std::move(localFrame)); });\n\n    // Skip decrypting for silence frames\n    if (mediaType == Audio && encryptedFrame.size() == kOpusSilencePacket.size() &&\n        memcmp(encryptedFrame.data(), kOpusSilencePacket.data(), kOpusSilencePacket.size()) == 0) {\n        DISCORD_LOG(LS_VERBOSE) << \"Decrypt skipping silence of size: \" << encryptedFrame.size();\n        auto copySize = std::min(frame.size(), encryptedFrame.size());\n        if (encryptedFrame.data() != frame.data()) {\n            memcpy(frame.data(), encryptedFrame.data(), copySize);\n        }\n        *bytesWritten = copySize;\n        return ResultCode::Success;\n    }\n\n    // Remove any expired cryptor manager\n    CleanupExpiredCryptorManagers();\n\n    // Process the incoming frame\n    // This will check whether it looks like a valid encrypted frame\n    // and if so it will parse it into its different components\n    localFrame->ParseFrame(encryptedFrame);\n\n    // If the frame is not encrypted and we can pass it through, do it\n    bool canUsePassThrough = allowPassThroughUntil_ > start;\n    if (!localFrame->IsEncrypted() && canUsePassThrough) {\n        auto copySize = std::min(frame.size(), encryptedFrame.size());\n        if (encryptedFrame.data() != frame.data()) {\n            memcpy(frame.data(), encryptedFrame.data(), copySize);\n        }\n        stats_[mediaType].passthroughCount++;\n        *bytesWritten = copySize;\n        return ResultCode::Success;\n    }\n\n    // If the frame is not encrypted and we can't pass it through, fail\n    if (!localFrame->IsEncrypted()) {\n        DISCORD_LOG(LS_INFO)\n          << \"Decrypt failed, frame is not encrypted and pass through is disabled\";\n        stats_[mediaType].decryptFailureCount++;\n        *bytesWritten = 0;\n        return ResultCode::DecryptionFailure;\n    }\n\n    // Try and decrypt with each valid cryptor\n    // reverse iterate to try the newest cryptors first\n    auto result = ResultCode::MissingKeyRatchet;\n    for (auto it = cryptorManagers_.rbegin(); it != cryptorManagers_.rend(); ++it) {\n        auto& cryptorManager = *it;\n        result = DecryptImpl(cryptorManager, mediaType, *localFrame);\n        if (result == ResultCode::Success) {\n            break;\n        }\n    }\n\n    size_t reconstructedFrameSize = 0;\n    if (result == ResultCode::Success) {\n        stats.decryptSuccessCount++;\n        reconstructedFrameSize = localFrame->ReconstructFrame(frame);\n    }\n    else {\n        stats.decryptFailureCount++;\n        DISCORD_LOG(LS_WARNING) << \"Decrypt failed, no valid cryptor found, type: \"\n                                << (mediaType ? \"video\" : \"audio\")\n                                << \", encrypted frame size: \" << encryptedFrame.size()\n                                << \", plaintext frame size: \" << frame.size()\n                                << \", number of cryptor managers: \" << cryptorManagers_.size()\n                                << \", pass through enabled: \" << (canUsePassThrough ? \"yes\" : \"no\");\n\n        if (result == ResultCode::InvalidNonce) {\n            stats.decryptInvalidNonceCount++;\n        }\n        else if (result == ResultCode::MissingKeyRatchet) {\n            stats.decryptMissingKeyCount++;\n        }\n    }\n\n    auto end = clock_.Now();\n    if (end > lastStatsTime_ + kStatsInterval) {\n        lastStatsTime_ = end;\n        DISCORD_LOG(LS_INFO) << \"Decrypted audio: \" << stats_[Audio].decryptSuccessCount\n                             << \", video: \" << stats_[Video].decryptSuccessCount\n                             << \". Failed audio: \" << stats_[Audio].decryptFailureCount\n                             << \", video: \" << stats_[Video].decryptFailureCount;\n    }\n    stats.decryptDuration +=\n      std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();\n\n    *bytesWritten = reconstructedFrameSize;\n    return result;\n}\n\nDecryptor::ResultCode Decryptor::DecryptImpl(CryptorManager& cryptorManager,\n                                             MediaType mediaType,\n                                             InboundFrameProcessor& encryptedFrame)\n{\n    auto tag = encryptedFrame.GetTag();\n    auto truncatedNonce = encryptedFrame.GetTruncatedNonce();\n\n    auto authenticatedData = encryptedFrame.GetAuthenticatedData();\n    auto ciphertext = encryptedFrame.GetCiphertext();\n    auto plaintext = encryptedFrame.GetPlaintext();\n\n    // expand the truncated nonce to the full sized one needed for decryption\n    auto nonceBuffer = std::array<uint8_t, kAesGcm128NonceBytes>();\n    memcpy(nonceBuffer.data() + kAesGcm128TruncatedSyncNonceOffset,\n           &truncatedNonce,\n           kAesGcm128TruncatedSyncNonceBytes);\n\n    auto nonceBufferView = MakeArrayView<const uint8_t>(nonceBuffer.data(), nonceBuffer.size());\n\n    auto generation =\n      cryptorManager.ComputeWrappedGeneration(truncatedNonce >> kRatchetGenerationShiftBits);\n\n    if (!cryptorManager.CanProcessNonce(generation, truncatedNonce)) {\n        DISCORD_LOG(LS_INFO) << \"Decrypt failed, cannot process nonce: \" << truncatedNonce;\n        return ResultCode::InvalidNonce;\n    }\n\n    // Get the cryptor for this generation\n    ICryptor* cryptor = cryptorManager.GetCryptor(generation);\n\n    if (!cryptor) {\n        DISCORD_LOG(LS_INFO) << \"Decrypt failed, no cryptor found for generation: \" << generation;\n        return ResultCode::MissingCryptor;\n    }\n\n    // perform the decryption\n    bool success = cryptor->Decrypt(plaintext, ciphertext, tag, nonceBufferView, authenticatedData);\n    stats_[mediaType].decryptAttempts++;\n\n    if (success) {\n        cryptorManager.ReportCryptorSuccess(generation, truncatedNonce);\n    }\n\n    return success ? ResultCode::Success : ResultCode::DecryptionFailure;\n}\n\nsize_t Decryptor::GetMaxPlaintextByteSize([[maybe_unused]] MediaType mediaType,\n                                          size_t encryptedFrameSize)\n{\n    return encryptedFrameSize;\n}\n\nvoid Decryptor::UpdateCryptorManagerExpiry(Duration expiry)\n{\n    auto maxExpiryTime = clock_.Now() + expiry;\n    for (auto& cryptorManager : cryptorManagers_) {\n        cryptorManager.UpdateExpiry(maxExpiryTime);\n    }\n}\n\nvoid Decryptor::CleanupExpiredCryptorManagers()\n{\n    while (!cryptorManagers_.empty() && cryptorManagers_.front().IsExpired()) {\n        DISCORD_LOG(LS_INFO) << \"Removing expired cryptor manager.\";\n        cryptorManagers_.pop_front();\n    }\n}\n\nstd::unique_ptr<InboundFrameProcessor> Decryptor::GetOrCreateFrameProcessor()\n{\n    std::lock_guard<std::mutex> lock(frameProcessorsMutex_);\n    if (frameProcessors_.empty()) {\n        return std::make_unique<InboundFrameProcessor>();\n    }\n    auto frameProcessor = std::move(frameProcessors_.back());\n    frameProcessors_.pop_back();\n    return frameProcessor;\n}\n\nvoid Decryptor::ReturnFrameProcessor(std::unique_ptr<InboundFrameProcessor> frameProcessor)\n{\n    std::lock_guard<std::mutex> lock(frameProcessorsMutex_);\n    frameProcessors_.push_back(std::move(frameProcessor));\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/decryptor.h",
    "content": "#pragma once\n\n#include <array>\n#include <deque>\n#include <functional>\n#include <memory>\n#include <mutex>\n#include <vector>\n\n#include <dave/dave_interfaces.h>\n#include <dave/version.h>\n\n#include \"codec_utils.h\"\n#include \"common.h\"\n#include \"cryptor.h\"\n#include \"cryptor_manager.h\"\n#include \"frame_processors.h\"\n#include \"utils/clock.h\"\n\nnamespace discord {\nnamespace dave {\n\nclass IKeyRatchet;\n\nclass Decryptor final : public IDecryptor {\npublic:\n    using Duration = std::chrono::seconds;\n\n    virtual ~Decryptor() noexcept = default;\n\n    virtual void TransitionToKeyRatchet(\n      std::unique_ptr<IKeyRatchet> keyRatchet,\n      Duration transitionExpiry = kDefaultTransitionDuration) override;\n    virtual void TransitionToPassthroughMode(\n      bool passthroughMode,\n      Duration transitionExpiry = kDefaultTransitionDuration) override;\n\n    virtual ResultCode Decrypt(MediaType mediaType,\n                               ArrayView<const uint8_t> encryptedFrame,\n                               ArrayView<uint8_t> frame,\n                               size_t* bytesWritten) override;\n\n    virtual size_t GetMaxPlaintextByteSize(MediaType mediaType, size_t encryptedFrameSize) override;\n    virtual DecryptorStats GetStats(MediaType mediaType) const override\n    {\n        return stats_[mediaType];\n    }\n\nprivate:\n    using TimePoint = IClock::TimePoint;\n\n    Decryptor::ResultCode DecryptImpl(CryptorManager& cryptor,\n                                      MediaType mediaType,\n                                      InboundFrameProcessor& encryptedFrame);\n\n    void UpdateCryptorManagerExpiry(Duration expiry);\n    void CleanupExpiredCryptorManagers();\n\n    std::unique_ptr<InboundFrameProcessor> GetOrCreateFrameProcessor();\n    void ReturnFrameProcessor(std::unique_ptr<InboundFrameProcessor> frameProcessor);\n\n    Clock clock_;\n    std::deque<CryptorManager> cryptorManagers_;\n\n    std::mutex frameProcessorsMutex_;\n    std::vector<std::unique_ptr<InboundFrameProcessor>> frameProcessors_;\n\n    TimePoint allowPassThroughUntil_{TimePoint::min()};\n\n    TimePoint lastStatsTime_{TimePoint::min()};\n    std::array<DecryptorStats, 2> stats_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/encryptor.cpp",
    "content": "#include \"encryptor.h\"\n\n#include <algorithm>\n#include <cstring>\n\n#include <bytes/bytes.h>\n#include <dave/array_view.h>\n#include <dave/logger.h>\n\n#include \"codec_utils.h\"\n#include \"common.h\"\n#include \"cryptor_manager.h\"\n#include \"utils/leb128.h\"\n#include \"utils/scope_exit.h\"\n\nusing namespace std::chrono_literals;\n\nnamespace discord {\nnamespace dave {\n\nconstexpr auto kStatsInterval = 10s;\n\nstd::unique_ptr<IEncryptor> CreateEncryptor()\n{\n    return std::make_unique<Encryptor>();\n}\n\nvoid Encryptor::SetKeyRatchet(std::unique_ptr<IKeyRatchet> keyRatchet)\n{\n    std::lock_guard<std::mutex> lock(keyGenMutex_);\n    keyRatchet_ = std::move(keyRatchet);\n    cryptor_ = nullptr;\n    currentKeyGeneration_ = 0;\n    truncatedNonce_ = 0;\n}\n\nvoid Encryptor::SetPassthroughMode(bool passthroughMode)\n{\n    passthroughMode_ = passthroughMode;\n    UpdateCurrentProtocolVersion(passthroughMode ? 0 : MaxSupportedProtocolVersion());\n}\n\nEncryptor::ResultCode Encryptor::Encrypt(MediaType mediaType,\n                                         uint32_t ssrc,\n                                         ArrayView<const uint8_t> frame,\n                                         ArrayView<uint8_t> encryptedFrame,\n                                         size_t* bytesWritten)\n{\n    if (mediaType != Audio && mediaType != Video) {\n        DISCORD_LOG(LS_WARNING) << \"Encrypt failed, invalid media type: \"\n                                << static_cast<int>(mediaType);\n        return ResultCode::EncryptionFailure;\n    }\n    auto& stats = stats_[mediaType];\n\n    if (passthroughMode_) {\n        // Pass frame through without encrypting\n        auto copySize = std::min(encryptedFrame.size(), frame.size());\n        memcpy(encryptedFrame.data(), frame.data(), copySize);\n        *bytesWritten = copySize;\n        stats.passthroughCount++;\n        return ResultCode::Success;\n    }\n\n    {\n        std::lock_guard<std::mutex> lock(keyGenMutex_);\n        if (!keyRatchet_) {\n            stats.encryptFailureCount++;\n            stats.encryptMissingKeyCount++;\n            return ResultCode::MissingKeyRatchet;\n        }\n    }\n\n    auto start = std::chrono::steady_clock::now();\n    auto result = ResultCode::Success;\n\n    // write the codec identifier\n    auto codec = CodecForSsrc(ssrc);\n\n    auto frameProcessor = GetOrCreateFrameProcessor();\n    ScopeExit cleanup([&] { ReturnFrameProcessor(std::move(frameProcessor)); });\n\n    frameProcessor->ProcessFrame(frame, codec);\n\n    const auto& unencryptedBytes = frameProcessor->GetUnencryptedBytes();\n    const auto& encryptedBytes = frameProcessor->GetEncryptedBytes();\n    auto& ciphertextBytes = frameProcessor->GetCiphertextBytes();\n\n    const auto& unencryptedRanges = frameProcessor->GetUnencryptedRanges();\n    auto unencryptedRangesSize = UnencryptedRangesSize(unencryptedRanges);\n\n    auto additionalData = MakeArrayView(unencryptedBytes.data(), unencryptedBytes.size());\n    auto plaintextBuffer = MakeArrayView(encryptedBytes.data(), encryptedBytes.size());\n    auto ciphertextBuffer = MakeArrayView(ciphertextBytes.data(), ciphertextBytes.size());\n\n    auto frameSize = encryptedBytes.size() + unencryptedBytes.size();\n    auto tagBuffer = MakeArrayView(encryptedFrame.data() + frameSize, kAesGcm128TruncatedTagBytes);\n\n    auto nonceBuffer = std::array<uint8_t, kAesGcm128NonceBytes>();\n    auto nonceBufferView = MakeArrayView<const uint8_t>(nonceBuffer.data(), nonceBuffer.size());\n\n    constexpr auto MAX_CIPHERTEXT_VALIDATION_RETRIES = 10;\n\n    // some codecs (e.g. H26X) have packetizers that cannot handle specific byte sequences\n    // so we attempt up to MAX_CIPHERTEXT_VALIDATION_RETRIES to encrypt the frame\n    // calling into codec utils to validate the ciphertext + supplemental section\n    // and re-rolling the truncated nonce if it fails\n\n    // the nonce increment will definitely change the ciphertext and the tag\n    // incrementing the nonce will also change the appropriate bytes\n    // in the tail end of the nonce\n    // which can remove start codes from the last 1 or 2 bytes of the nonce\n    // and the two bytes of the unencrypted header bytes\n    for (auto attempt = 1; attempt <= MAX_CIPHERTEXT_VALIDATION_RETRIES; ++attempt) {\n        auto [cryptor, truncatedNonce] = GetNextCryptorAndNonce();\n\n        if (!cryptor) {\n            stats.encryptMissingKeyCount++;\n            result = ResultCode::MissingCryptor;\n            break;\n        }\n\n        // write the truncated nonce to our temporary full nonce array\n        // (since the encryption call expects a full size nonce)\n        memcpy(nonceBuffer.data() + kAesGcm128TruncatedSyncNonceOffset,\n               &truncatedNonce,\n               kAesGcm128TruncatedSyncNonceBytes);\n\n        // encrypt the plaintext, adding the unencrypted header to the tag\n        bool success = cryptor->Encrypt(\n          ciphertextBuffer, plaintextBuffer, nonceBufferView, additionalData, tagBuffer);\n\n        stats.encryptAttempts++;\n        stats.encryptMaxAttempts = std::max(stats.encryptMaxAttempts, (uint64_t)attempt);\n\n        if (!success) {\n            assert(false && \"Failed to encrypt frame\");\n            result = ResultCode::EncryptionFailure;\n            break;\n        }\n\n        auto reconstructedFrameSize = frameProcessor->ReconstructFrame(encryptedFrame);\n        assert(reconstructedFrameSize == frameSize && \"Failed to reconstruct frame\");\n\n        auto nonceSize = Leb128Size(truncatedNonce);\n\n        auto truncatedNonceBuffer = MakeArrayView(tagBuffer.end(), nonceSize);\n        auto unencryptedRangesBuffer =\n          MakeArrayView(truncatedNonceBuffer.end(), unencryptedRangesSize);\n        auto supplementalBytesBuffer =\n          MakeArrayView(unencryptedRangesBuffer.end(), sizeof(SupplementalBytesSize));\n        auto markerBytesBuffer = MakeArrayView(supplementalBytesBuffer.end(), sizeof(MagicMarker));\n\n        // write the nonce\n        auto res = WriteLeb128(truncatedNonce, truncatedNonceBuffer.begin());\n        if (res != nonceSize) {\n            assert(false && \"Failed to write truncated nonce\");\n            result = ResultCode::EncryptionFailure;\n            break;\n        }\n\n        // write the unencrypted ranges\n        res = SerializeUnencryptedRanges(\n          unencryptedRanges, unencryptedRangesBuffer.begin(), unencryptedRangesBuffer.size());\n        if (res != unencryptedRangesSize) {\n            assert(false && \"Failed to write unencrypted ranges\");\n            result = ResultCode::EncryptionFailure;\n            break;\n        }\n\n        // write the supplemental bytes size\n        uint64_t supplementalBytesLarge = kSupplementalBytes + nonceSize + unencryptedRangesSize;\n\n        if (supplementalBytesLarge > std::numeric_limits<SupplementalBytesSize>::max()) {\n            assert(false && \"Supplemental bytes size too large\");\n            result = ResultCode::EncryptionFailure;\n            break;\n        }\n\n        SupplementalBytesSize supplementalBytes =\n          static_cast<SupplementalBytesSize>(supplementalBytesLarge);\n        memcpy(supplementalBytesBuffer.data(), &supplementalBytes, sizeof(SupplementalBytesSize));\n\n        // write the marker bytes, ends the frame\n        memcpy(markerBytesBuffer.data(), &kMarkerBytes, sizeof(MagicMarker));\n\n        auto encryptedFrameBytes = reconstructedFrameSize + kAesGcm128TruncatedTagBytes +\n          nonceSize + unencryptedRangesSize + sizeof(SupplementalBytesSize) + sizeof(MagicMarker);\n\n        if (codec_utils::ValidateEncryptedFrame(\n              *frameProcessor, MakeArrayView(encryptedFrame.data(), encryptedFrameBytes))) {\n            *bytesWritten = encryptedFrameBytes;\n            break;\n        }\n        else if (attempt >= MAX_CIPHERTEXT_VALIDATION_RETRIES) {\n            assert(false && \"Failed to validate encrypted section for codec\");\n            result = ResultCode::TooManyAttempts;\n            break;\n        }\n    }\n\n    auto now = std::chrono::steady_clock::now();\n    stats.encryptDuration +=\n      std::chrono::duration_cast<std::chrono::microseconds>(now - start).count();\n    if (result == ResultCode::Success) {\n        stats.encryptSuccessCount++;\n    }\n    else {\n        stats.encryptFailureCount++;\n    }\n\n    if (now > lastStatsTime_ + kStatsInterval) {\n        lastStatsTime_ = now;\n        DISCORD_LOG(LS_INFO) << \"Encrypted audio: \" << stats_[Audio].encryptSuccessCount\n                             << \", video: \" << stats_[Video].encryptSuccessCount\n                             << \". Failed audio: \" << stats_[Audio].encryptFailureCount\n                             << \", video: \" << stats_[Video].encryptFailureCount;\n        DISCORD_LOG(LS_INFO) << \"Last encrypted frame, type: \"\n                             << (mediaType == Audio ? \"audio\" : \"video\") << \", ssrc: \" << ssrc\n                             << \", size: \" << frame.size();\n    }\n\n    return result;\n}\n\nsize_t Encryptor::GetMaxCiphertextByteSize([[maybe_unused]] MediaType mediaType, size_t frameSize)\n{\n    return frameSize + kSupplementalBytes + kTransformPaddingBytes;\n}\n\nvoid Encryptor::AssignSsrcToCodec(uint32_t ssrc, Codec codecType)\n{\n    auto existingCodecIt = std::find_if(\n      ssrcCodecPairs_.begin(), ssrcCodecPairs_.end(), [ssrc](const SsrcCodecPair& pair) {\n          return pair.first == ssrc;\n      });\n\n    if (existingCodecIt == ssrcCodecPairs_.end()) {\n        ssrcCodecPairs_.emplace_back(ssrc, codecType);\n    }\n    else {\n        existingCodecIt->second = codecType;\n    }\n}\n\nCodec Encryptor::CodecForSsrc(uint32_t ssrc)\n{\n    auto existingCodecIt = std::find_if(\n      ssrcCodecPairs_.begin(), ssrcCodecPairs_.end(), [ssrc](const SsrcCodecPair& pair) {\n          return pair.first == ssrc;\n      });\n\n    if (existingCodecIt != ssrcCodecPairs_.end()) {\n        return existingCodecIt->second;\n    }\n    else {\n        return Codec::Unknown;\n    }\n}\n\nstd::unique_ptr<OutboundFrameProcessor> Encryptor::GetOrCreateFrameProcessor()\n{\n    std::lock_guard<std::mutex> lock(frameProcessorsMutex_);\n    if (frameProcessors_.empty()) {\n        return std::make_unique<OutboundFrameProcessor>();\n    }\n    auto frameProcessor = std::move(frameProcessors_.back());\n    frameProcessors_.pop_back();\n    return frameProcessor;\n}\n\nvoid Encryptor::ReturnFrameProcessor(std::unique_ptr<OutboundFrameProcessor> frameProcessor)\n{\n    std::lock_guard<std::mutex> lock(frameProcessorsMutex_);\n    frameProcessors_.push_back(std::move(frameProcessor));\n}\n\nEncryptor::CryptorAndNonce Encryptor::GetNextCryptorAndNonce()\n{\n    std::lock_guard<std::mutex> lock(keyGenMutex_);\n    if (!keyRatchet_) {\n        return {nullptr, 0};\n    }\n\n    auto generation = ComputeWrappedGeneration(currentKeyGeneration_,\n                                               ++truncatedNonce_ >> kRatchetGenerationShiftBits);\n\n    if (generation != currentKeyGeneration_ || !cryptor_) {\n        currentKeyGeneration_ = generation;\n\n        auto encryptionKey = keyRatchet_->GetKey(currentKeyGeneration_);\n        cryptor_ = CreateCryptor(encryptionKey);\n    }\n\n    return {cryptor_, truncatedNonce_};\n}\n\nvoid Encryptor::UpdateCurrentProtocolVersion(ProtocolVersion version)\n{\n    if (version == currentProtocolVersion_) {\n        return;\n    }\n\n    currentProtocolVersion_ = version;\n    if (protocolVersionChangedCallback_) {\n        protocolVersionChangedCallback_();\n    }\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/encryptor.h",
    "content": "#pragma once\n\n#include <array>\n#include <atomic>\n#include <chrono>\n#include <functional>\n#include <memory>\n#include <mutex>\n#include <vector>\n\n#include <dave/dave_interfaces.h>\n#include <dave/version.h>\n\n#include \"codec_utils.h\"\n#include \"common.h\"\n#include \"cryptor.h\"\n#include \"frame_processors.h\"\n\nnamespace discord {\nnamespace dave {\n\nclass Encryptor final : public IEncryptor {\npublic:\n    virtual ~Encryptor() noexcept = default;\n\n    virtual void SetKeyRatchet(std::unique_ptr<IKeyRatchet> keyRatchet) override;\n    virtual void SetPassthroughMode(bool passthroughMode) override;\n\n    virtual bool HasKeyRatchet() const override { return keyRatchet_ != nullptr; }\n    virtual bool IsPassthroughMode() const override { return passthroughMode_; }\n\n    virtual void AssignSsrcToCodec(uint32_t ssrc, Codec codecType) override;\n    virtual Codec CodecForSsrc(uint32_t ssrc) override;\n\n    virtual ResultCode Encrypt(MediaType mediaType,\n                               uint32_t ssrc,\n                               ArrayView<const uint8_t> frame,\n                               ArrayView<uint8_t> encryptedFrame,\n                               size_t* bytesWritten) override;\n\n    virtual size_t GetMaxCiphertextByteSize(MediaType mediaType, size_t frameSize) override;\n    virtual EncryptorStats GetStats(MediaType mediaType) const override\n    {\n        return stats_[mediaType];\n    }\n\n    using ProtocolVersionChangedCallback = std::function<void()>;\n    virtual void SetProtocolVersionChangedCallback(ProtocolVersionChangedCallback callback) override\n    {\n        protocolVersionChangedCallback_ = std::move(callback);\n    }\n    virtual ProtocolVersion GetProtocolVersion() const override { return currentProtocolVersion_; }\n\nprivate:\n    std::unique_ptr<OutboundFrameProcessor> GetOrCreateFrameProcessor();\n    void ReturnFrameProcessor(std::unique_ptr<OutboundFrameProcessor> frameProcessor);\n\n    using CryptorAndNonce = std::pair<std::shared_ptr<ICryptor>, TruncatedSyncNonce>;\n    CryptorAndNonce GetNextCryptorAndNonce();\n\n    void UpdateCurrentProtocolVersion(ProtocolVersion version);\n\n    std::atomic_bool passthroughMode_{false};\n\n    std::mutex keyGenMutex_;\n    std::unique_ptr<IKeyRatchet> keyRatchet_;\n    std::shared_ptr<ICryptor> cryptor_;\n    KeyGeneration currentKeyGeneration_{0};\n    TruncatedSyncNonce truncatedNonce_{0};\n\n    std::mutex frameProcessorsMutex_;\n    std::vector<std::unique_ptr<OutboundFrameProcessor>> frameProcessors_;\n\n    using SsrcCodecPair = std::pair<uint32_t, Codec>;\n    std::vector<SsrcCodecPair> ssrcCodecPairs_;\n\n    using TimePoint = std::chrono::time_point<std::chrono::steady_clock>;\n    TimePoint lastStatsTime_{TimePoint::min()};\n    std::array<EncryptorStats, 2> stats_;\n\n    ProtocolVersionChangedCallback protocolVersionChangedCallback_;\n    ProtocolVersion currentProtocolVersion_{MaxSupportedProtocolVersion()};\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/frame_processors.cpp",
    "content": "#include \"frame_processors.h\"\n\n#include <cassert>\n#include <cstring>\n#include <limits>\n#include <optional>\n\n#include <dave/array_view.h>\n#include <dave/logger.h>\n\n#include \"codec_utils.h\"\n#include \"utils/leb128.h\"\n\n#if defined(_MSC_VER)\n#include <intrin.h>\n#endif\n\nnamespace discord {\nnamespace dave {\n\nstd::pair<bool, size_t> OverflowAdd(size_t a, size_t b)\n{\n    size_t res;\n#if defined(_MSC_VER) && defined(_M_X64)\n    bool didOverflow = _addcarry_u64(0, a, b, &res);\n#elif defined(_MSC_VER) && defined(_M_IX86)\n    bool didOverflow = _addcarry_u32(0, a, b, &res);\n#else\n    bool didOverflow = __builtin_add_overflow(a, b, &res);\n#endif\n    return {didOverflow, res};\n}\n\nuint8_t UnencryptedRangesSize(const Ranges& unencryptedRanges)\n{\n    size_t size = 0;\n    for (const auto& range : unencryptedRanges) {\n        size += Leb128Size(range.offset);\n        size += Leb128Size(range.size);\n    }\n    assert(size <= std::numeric_limits<uint8_t>::max() &&\n           \"Unencrypted ranges size exceeds 255 bytes\");\n    return static_cast<uint8_t>(size);\n}\n\nuint8_t SerializeUnencryptedRanges(const Ranges& unencryptedRanges,\n                                   uint8_t* buffer,\n                                   size_t bufferSize)\n{\n    auto writeAt = buffer;\n    auto end = buffer + bufferSize;\n    for (const auto& range : unencryptedRanges) {\n        auto rangeSize = Leb128Size(range.offset) + Leb128Size(range.size);\n        if (rangeSize > static_cast<size_t>(end - writeAt)) {\n            assert(false && \"Buffer is too small to serialize unencrypted ranges\");\n            break;\n        }\n\n        writeAt += WriteLeb128(range.offset, writeAt);\n        writeAt += WriteLeb128(range.size, writeAt);\n    }\n\n    assert(writeAt >= buffer);\n    return static_cast<uint8_t>(writeAt - buffer);\n}\n\nuint8_t DeserializeUnencryptedRanges(const uint8_t*& readAt,\n                                     const uint8_t bufferSize,\n                                     Ranges& unencryptedRanges)\n{\n    auto start = readAt;\n    auto end = readAt + bufferSize;\n    while (readAt < end) {\n        size_t offset = ReadLeb128(readAt, end);\n        if (readAt == nullptr) {\n            break;\n        }\n\n        size_t size = ReadLeb128(readAt, end);\n        if (readAt == nullptr) {\n            break;\n        }\n        unencryptedRanges.push_back({offset, size});\n    }\n\n    if (readAt != end) {\n        DISCORD_LOG(LS_WARNING) << \"Failed to deserialize unencrypted ranges\";\n        unencryptedRanges.clear();\n        readAt = nullptr;\n        return 0;\n    }\n\n    return static_cast<uint8_t>(readAt - start);\n}\n\nbool ValidateUnencryptedRanges(const Ranges& unencryptedRanges, size_t frameSize)\n{\n    if (unencryptedRanges.empty()) {\n        return true;\n    }\n\n    // validate that the ranges are in order and don't overlap\n    for (auto i = 0u; i < unencryptedRanges.size(); ++i) {\n        auto current = unencryptedRanges[i];\n        // The current range should not overflow into the next range\n        // or if it is the last range, the end of the frame\n        auto maxEnd =\n          i + 1 < unencryptedRanges.size() ? unencryptedRanges[i + 1].offset : frameSize;\n\n        auto [didOverflow, currentEnd] = OverflowAdd(current.offset, current.size);\n        if (didOverflow || currentEnd > maxEnd) {\n            DISCORD_LOG(LS_WARNING)\n              << \"Unencrypted range may overlap or be out of order: current offset: \"\n              << current.offset << \", current size: \" << current.size << \", maximum end: \" << maxEnd\n              << \", frame size: \" << frameSize;\n            return false;\n        }\n    }\n\n    return true;\n}\n\nsize_t Reconstruct(Ranges ranges,\n                   const std::vector<uint8_t>& rangeBytes,\n                   const std::vector<uint8_t>& otherBytes,\n                   const ArrayView<uint8_t>& output)\n{\n    size_t frameIndex = 0;\n    size_t rangeBytesIndex = 0;\n    size_t otherBytesIndex = 0;\n\n    const auto CopyRangeBytes = [&](size_t size) {\n        assert(rangeBytesIndex + size <= rangeBytes.size());\n        assert(frameIndex + size <= output.size());\n        if ((rangeBytes.size() - rangeBytesIndex < size) || (output.size() - frameIndex < size)) {\n            return;\n        }\n        memcpy(output.data() + frameIndex, rangeBytes.data() + rangeBytesIndex, size);\n        rangeBytesIndex += size;\n        frameIndex += size;\n    };\n\n    const auto CopyOtherBytes = [&](size_t size) {\n        assert(otherBytesIndex + size <= otherBytes.size());\n        assert(frameIndex + size <= output.size());\n        if ((otherBytes.size() - otherBytesIndex < size) || (output.size() - frameIndex < size)) {\n            return;\n        }\n        memcpy(output.data() + frameIndex, otherBytes.data() + otherBytesIndex, size);\n        otherBytesIndex += size;\n        frameIndex += size;\n    };\n\n    for (const auto& range : ranges) {\n        if (range.offset > frameIndex) {\n            CopyOtherBytes(range.offset - frameIndex);\n        }\n\n        CopyRangeBytes(range.size);\n    }\n\n    if (otherBytesIndex < otherBytes.size()) {\n        CopyOtherBytes(otherBytes.size() - otherBytesIndex);\n    }\n\n    assert(rangeBytesIndex == rangeBytes.size());\n    assert(otherBytesIndex == otherBytes.size());\n    assert(frameIndex <= output.size());\n\n    return frameIndex;\n}\n\nvoid InboundFrameProcessor::Clear()\n{\n    isEncrypted_ = false;\n    originalSize_ = 0;\n    truncatedNonce_ = std::numeric_limits<TruncatedSyncNonce>::max();\n    unencryptedRanges_.clear();\n    authenticated_.clear();\n    ciphertext_.clear();\n    plaintext_.clear();\n}\n\nvoid InboundFrameProcessor::ParseFrame(ArrayView<const uint8_t> frame)\n{\n    Clear();\n\n    constexpr auto MinSupplementalBytesSize =\n      kAesGcm128TruncatedTagBytes + sizeof(SupplementalBytesSize) + sizeof(MagicMarker);\n    if (frame.size() < MinSupplementalBytesSize) {\n        DISCORD_LOG(LS_WARNING) << \"Encrypted frame is too small to contain min supplemental bytes\";\n        return;\n    }\n\n    // Check the frame ends with the magic marker\n    auto magicMarkerBuffer = frame.end() - sizeof(MagicMarker);\n    if (memcmp(magicMarkerBuffer, &kMarkerBytes, sizeof(MagicMarker)) != 0) {\n        return;\n    }\n\n    // Read the supplemental bytes size\n    SupplementalBytesSize supplementalBytesSize;\n    auto supplementalBytesSizeBuffer = magicMarkerBuffer - sizeof(SupplementalBytesSize);\n    assert(frame.begin() <= supplementalBytesSizeBuffer &&\n           supplementalBytesSizeBuffer <= frame.end());\n    memcpy(&supplementalBytesSize, supplementalBytesSizeBuffer, sizeof(SupplementalBytesSize));\n\n    // Check the frame is large enough to contain the supplemental bytes\n    if (frame.size() < supplementalBytesSize) {\n        DISCORD_LOG(LS_WARNING) << \"Encrypted frame is too small to contain supplemental bytes\";\n        return;\n    }\n\n    // Check that supplemental bytes size is large enough to contain the supplemental bytes\n    if (supplementalBytesSize < MinSupplementalBytesSize) {\n        DISCORD_LOG(LS_WARNING)\n          << \"Supplemental bytes size is too small to contain supplemental bytes\";\n        return;\n    }\n\n    auto supplementalBytesBuffer = frame.end() - supplementalBytesSize;\n    assert(frame.begin() <= supplementalBytesBuffer && supplementalBytesBuffer <= frame.end());\n\n    // Read the tag\n    tag_ = MakeArrayView(supplementalBytesBuffer, kAesGcm128TruncatedTagBytes);\n\n    // Read the nonce\n    auto nonceBuffer = supplementalBytesBuffer + kAesGcm128TruncatedTagBytes;\n    assert(frame.begin() <= nonceBuffer && nonceBuffer <= frame.end());\n    auto readAt = nonceBuffer;\n    auto end = supplementalBytesSizeBuffer;\n    truncatedNonce_ = static_cast<uint32_t>(ReadLeb128(readAt, end));\n    if (readAt == nullptr) {\n        DISCORD_LOG(LS_WARNING) << \"Failed to read truncated nonce\";\n        return;\n    }\n\n    // Read the unencrypted ranges\n    assert(nonceBuffer <= readAt && readAt <= end &&\n           end - readAt <= std::numeric_limits<uint8_t>::max());\n    auto unencryptedRangesSize = static_cast<uint8_t>(end - readAt);\n\n    DeserializeUnencryptedRanges(readAt, unencryptedRangesSize, unencryptedRanges_);\n    if (readAt == nullptr) {\n        DISCORD_LOG(LS_WARNING) << \"Failed to read unencrypted ranges\";\n        return;\n    }\n\n    if (!ValidateUnencryptedRanges(unencryptedRanges_, frame.size())) {\n        DISCORD_LOG(LS_WARNING) << \"Invalid unencrypted ranges\";\n        return;\n    }\n\n    // This is overly aggressive but will keep reallocations to a minimum\n    authenticated_.reserve(frame.size());\n    ciphertext_.reserve(frame.size());\n    plaintext_.reserve(frame.size());\n\n    originalSize_ = frame.size();\n\n    // Split the frame into authenticated and ciphertext bytes\n    size_t frameIndex = 0;\n    for (const auto& range : unencryptedRanges_) {\n        auto encryptedBytes = range.offset - frameIndex;\n        if (encryptedBytes > 0) {\n            assert(frameIndex + encryptedBytes <= frame.size());\n            AddCiphertextBytes(frame.data() + frameIndex, encryptedBytes);\n        }\n\n        assert(range.offset + range.size <= frame.size());\n        AddAuthenticatedBytes(frame.data() + range.offset, range.size);\n        frameIndex = range.offset + range.size;\n    }\n    auto actualFrameSize = frame.size() - supplementalBytesSize;\n    if (frameIndex < actualFrameSize) {\n        AddCiphertextBytes(frame.data() + frameIndex, actualFrameSize - frameIndex);\n    }\n\n    // Make sure the plaintext buffer is the same size as the ciphertext buffer\n    plaintext_.resize(ciphertext_.size());\n\n    // We've successfully parsed the frame\n    // Mark the frame as encrypted\n    isEncrypted_ = true;\n}\n\nsize_t InboundFrameProcessor::ReconstructFrame(ArrayView<uint8_t> frame) const\n{\n    if (!isEncrypted_) {\n        DISCORD_LOG(LS_WARNING) << \"Cannot reconstruct an invalid encrypted frame\";\n        return 0;\n    }\n\n    if (authenticated_.size() + plaintext_.size() > frame.size()) {\n        DISCORD_LOG(LS_WARNING) << \"Frame is too small to contain the decrypted frame\";\n        return 0;\n    }\n\n    return Reconstruct(unencryptedRanges_, authenticated_, plaintext_, frame);\n}\n\nvoid InboundFrameProcessor::AddAuthenticatedBytes(const uint8_t* data, size_t size)\n{\n    authenticated_.resize(authenticated_.size() + size);\n    memcpy(authenticated_.data() + authenticated_.size() - size, data, size);\n}\n\nvoid InboundFrameProcessor::AddCiphertextBytes(const uint8_t* data, size_t size)\n{\n    ciphertext_.resize(ciphertext_.size() + size);\n    memcpy(ciphertext_.data() + ciphertext_.size() - size, data, size);\n}\n\nvoid OutboundFrameProcessor::Reset()\n{\n    codec_ = Codec::Unknown;\n    frameIndex_ = 0;\n    unencryptedBytes_.clear();\n    encryptedBytes_.clear();\n    unencryptedRanges_.clear();\n}\n\nvoid OutboundFrameProcessor::ProcessFrame(ArrayView<const uint8_t> frame, Codec codec)\n{\n    Reset();\n\n    codec_ = codec;\n    unencryptedBytes_.reserve(frame.size());\n    encryptedBytes_.reserve(frame.size());\n\n    bool success = false;\n    switch (codec) {\n    case Codec::Opus:\n        success = codec_utils::ProcessFrameOpus(*this, frame);\n        break;\n    case Codec::VP8:\n        success = codec_utils::ProcessFrameVp8(*this, frame);\n        break;\n    case Codec::VP9:\n        success = codec_utils::ProcessFrameVp9(*this, frame);\n        break;\n    case Codec::H264:\n        success = codec_utils::ProcessFrameH264(*this, frame);\n        break;\n    case Codec::H265:\n        success = codec_utils::ProcessFrameH265(*this, frame);\n        break;\n    case Codec::AV1:\n        success = codec_utils::ProcessFrameAv1(*this, frame);\n        break;\n    default:\n        assert(false && \"Unsupported codec for frame encryption\");\n        break;\n    }\n\n    if (!success) {\n        frameIndex_ = 0;\n        unencryptedBytes_.clear();\n        encryptedBytes_.clear();\n        unencryptedRanges_.clear();\n        AddEncryptedBytes(frame.data(), frame.size());\n    }\n\n    ciphertextBytes_.resize(encryptedBytes_.size());\n}\n\nsize_t OutboundFrameProcessor::ReconstructFrame(ArrayView<uint8_t> frame)\n{\n    if (unencryptedBytes_.size() + ciphertextBytes_.size() > frame.size()) {\n        DISCORD_LOG(LS_WARNING) << \"Frame is too small to contain the encrypted frame\";\n        return 0;\n    }\n\n    return Reconstruct(unencryptedRanges_, unencryptedBytes_, ciphertextBytes_, frame);\n}\n\nvoid OutboundFrameProcessor::AddUnencryptedBytes(const uint8_t* bytes, size_t size)\n{\n    if (!unencryptedRanges_.empty() &&\n        unencryptedRanges_.back().offset + unencryptedRanges_.back().size == frameIndex_) {\n        // extend the last range\n        unencryptedRanges_.back().size += size;\n    }\n    else {\n        // add a new range (offset, size)\n        unencryptedRanges_.push_back({frameIndex_, size});\n    }\n\n    unencryptedBytes_.resize(unencryptedBytes_.size() + size);\n    memcpy(unencryptedBytes_.data() + unencryptedBytes_.size() - size, bytes, size);\n    frameIndex_ += size;\n}\n\nvoid OutboundFrameProcessor::AddEncryptedBytes(const uint8_t* bytes, size_t size)\n{\n    encryptedBytes_.resize(encryptedBytes_.size() + size);\n    memcpy(encryptedBytes_.data() + encryptedBytes_.size() - size, bytes, size);\n    frameIndex_ += size;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/frame_processors.h",
    "content": "#pragma once\n\n#include <cstdint>\n#include <string>\n#include <utility>\n#include <vector>\n\n#include <dave/array_view.h>\n#include <dave/dave_interfaces.h>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\n\nstruct Range {\n    size_t offset;\n    size_t size;\n};\nusing Ranges = std::vector<Range>;\n\nuint8_t UnencryptedRangesSize(const Ranges& unencryptedRanges);\nuint8_t SerializeUnencryptedRanges(const Ranges& unencryptedRanges,\n                                   uint8_t* buffer,\n                                   size_t bufferSize);\nuint8_t DeserializeUnencryptedRanges(const uint8_t*& buffer,\n                                     const uint8_t bufferSize,\n                                     Ranges& unencryptedRanges);\nbool ValidateUnencryptedRanges(const Ranges& unencryptedRanges, size_t frameSize);\n\nclass InboundFrameProcessor {\npublic:\n    void ParseFrame(ArrayView<const uint8_t> frame);\n    size_t ReconstructFrame(ArrayView<uint8_t> frame) const;\n\n    bool IsEncrypted() const { return isEncrypted_; }\n    size_t Size() const { return originalSize_; }\n    void Clear();\n\n    ArrayView<const uint8_t> GetTag() const { return tag_; }\n    TruncatedSyncNonce GetTruncatedNonce() const { return truncatedNonce_; }\n    ArrayView<const uint8_t> GetAuthenticatedData() const\n    {\n        return MakeArrayView(authenticated_.data(), authenticated_.size());\n    }\n    ArrayView<const uint8_t> GetCiphertext() const\n    {\n        return MakeArrayView(ciphertext_.data(), ciphertext_.size());\n    }\n    ArrayView<uint8_t> GetPlaintext() { return MakeArrayView(plaintext_); }\n\nprivate:\n    void AddAuthenticatedBytes(const uint8_t* data, size_t size);\n    void AddCiphertextBytes(const uint8_t* data, size_t size);\n\n    bool isEncrypted_{false};\n    size_t originalSize_{0};\n    ArrayView<const uint8_t> tag_;\n    TruncatedSyncNonce truncatedNonce_;\n    Ranges unencryptedRanges_;\n    std::vector<uint8_t> authenticated_;\n    std::vector<uint8_t> ciphertext_;\n    std::vector<uint8_t> plaintext_;\n};\n\nclass OutboundFrameProcessor {\npublic:\n    void ProcessFrame(ArrayView<const uint8_t> frame, Codec codec);\n    size_t ReconstructFrame(ArrayView<uint8_t> frame);\n\n    Codec GetCodec() const { return codec_; }\n    const std::vector<uint8_t>& GetUnencryptedBytes() const { return unencryptedBytes_; }\n    const std::vector<uint8_t>& GetEncryptedBytes() const { return encryptedBytes_; }\n    std::vector<uint8_t>& GetCiphertextBytes() { return ciphertextBytes_; }\n    const Ranges& GetUnencryptedRanges() const { return unencryptedRanges_; }\n\n    void Reset();\n    void AddUnencryptedBytes(const uint8_t* bytes, size_t size);\n    void AddEncryptedBytes(const uint8_t* bytes, size_t size);\n\nprivate:\n    Codec codec_{Codec::Unknown};\n    size_t frameIndex_{0};\n    std::vector<uint8_t> unencryptedBytes_;\n    std::vector<uint8_t> encryptedBytes_;\n    std::vector<uint8_t> ciphertextBytes_;\n    Ranges unencryptedRanges_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/key_ratchet.h",
    "content": "#pragma once\n\n#include <memory>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/logger.cpp",
    "content": "#include <dave/logger.h>\n\n#include <atomic>\n#include <cstring>\n#include <iostream>\n\nnamespace discord {\nnamespace dave {\n\nstd::atomic<LogSink> gLogSink = nullptr;\n\nvoid SetLogSink(LogSink sink)\n{\n    gLogSink = sink;\n}\n\nLogStreamer::LogStreamer(LoggingSeverity severity, const char* file, int line)\n  : severity_(severity)\n  , file_(file)\n  , line_(line)\n{\n}\n\nLogStreamer::~LogStreamer()\n{\n    std::string logLine = stream_.str();\n    if (logLine.empty()) {\n        return;\n    }\n\n    auto sink = gLogSink.load();\n    if (sink) {\n        sink(severity_, file_, line_, logLine);\n        return;\n    }\n\n    switch (severity_) {\n    case LS_VERBOSE:\n    case LS_INFO:\n    case LS_WARNING:\n    case LS_ERROR: {\n        const char* file = file_;\n        if (auto separator = strrchr(file, '/')) {\n            file = separator + 1;\n        }\n        std::cout << \"(\" << file << \":\" << line_ << \") \" << logLine << std::endl;\n        break;\n    }\n    case LS_NONE:\n        break;\n    }\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/detail/persisted_key_pair.h",
    "content": "#pragma once\n\n#include <memory>\n#include <string>\n\n#include <mls/crypto.h>\n\n#include \"mls/persisted_key_pair.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\nnamespace detail {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetNativePersistedKeyPair(KeyPairContextType ctx,\n                                                                        const std::string& keyID,\n                                                                        ::mlspp::CipherSuite suite,\n                                                                        bool& supported);\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetGenericPersistedKeyPair(\n  KeyPairContextType ctx,\n  const std::string& keyID,\n  ::mlspp::CipherSuite suite);\n\nbool DeleteNativePersistedKeyPair(KeyPairContextType ctx, const std::string& keyID);\nbool DeleteGenericPersistedKeyPair(KeyPairContextType ctx, const std::string& keyID);\n\n} // namespace detail\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/detail/persisted_key_pair_apple.cpp",
    "content": "#include \"mls/detail/persisted_key_pair.h\"\n\n#include <cassert>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <mutex>\n#include <sstream>\n#include <string>\n\n#include <CoreFoundation/CoreFoundation.h>\n#include <Security/Security.h>\n#include <unistd.h>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n#include <mls/crypto.h>\n\n#include \"mls/parameters.h\"\n\nstatic const CFStringRef KeyServiceLabel = CFSTR(\"Discord Secure Frames Key\");\nstatic const std::string KeyLabelPrefix = \"Discord Secure Frames Key: \";\nstatic const std::string KeyTagPrefix = \"discord-secure-frames-key-\";\n\n#ifdef KEYCHAIN_ACCESS_GROUP_ID_SYMBOL\nextern CFStringRef KEYCHAIN_ACCESS_GROUP_ID_SYMBOL;\n#endif\n\nstatic void AddAccessGroup([[maybe_unused]] CFMutableDictionaryRef dict)\n{\n#ifdef KEYCHAIN_ACCESS_GROUP_ID_SYMBOL\n    CFDictionaryAddValue(dict, kSecAttrAccessGroup, KEYCHAIN_ACCESS_GROUP_ID_SYMBOL);\n#elif defined(KEYCHAIN_ACCESS_GROUP_ID)\n    CFDictionaryAddValue(dict, kSecAttrAccessGroup, CFSTR(#KEYCHAIN_ACCESS_GROUP_ID));\n#endif\n}\n\ntemplate <class T = CFTypeRef>\nstruct ScopedCFTypeRef {\n    ScopedCFTypeRef() = default;\n    ScopedCFTypeRef(T ref)\n      : ref_(ref)\n    {\n    }\n    ScopedCFTypeRef(ScopedCFTypeRef& other)\n      : ref_(other.ref_)\n    {\n        if (ref_) {\n            CFRetain(ref_);\n        }\n    }\n    ScopedCFTypeRef(ScopedCFTypeRef&& other)\n      : ref_(std::exchange(other.ref_, nullptr))\n    {\n    }\n\n    ~ScopedCFTypeRef() { release(); }\n\n    ScopedCFTypeRef& operator=(T ref)\n    {\n        release();\n        ref_ = ref;\n        return *this;\n    }\n\n    void release()\n    {\n        if (ref_) {\n            CFRelease(ref_);\n        }\n        ref_ = nullptr;\n    }\n\n    T& get() { return ref_; }\n\n    T* getPtr() { return &ref_; }\n    CFTypeRef* getGenericPtr() { return (CFTypeRef*)getPtr(); }\n\n    operator T&() { return get(); }\n\n    explicit operator bool() { return ref_ != nullptr; }\n\n    T ref_ = nullptr;\n};\n\nstatic std::string ConvertCFString(CFStringRef string)\n{\n    if (const char* str = CFStringGetCStringPtr(string, kCFStringEncodingUTF8)) {\n        return str;\n    }\n\n    CFIndex len = CFStringGetLength(string);\n    std::string ret(CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF8), 0);\n\n    CFStringGetBytes(string,\n                     CFRangeMake(0, len),\n                     kCFStringEncodingUTF8,\n                     '?',\n                     false,\n                     (UInt8*)ret.data(),\n                     ret.size(),\n                     &len);\n\n    ret.resize(len);\n\n    return ret;\n}\n\nstatic std::string SecStatusToString(OSStatus status)\n{\n    std::string ret = std::to_string(status);\n\n    if (__builtin_available(macOS 10.3, iOS 11.3, *)) {\n        ScopedCFTypeRef string = SecCopyErrorMessageString(status, NULL);\n        if (string) {\n            ret += \" (\";\n            ret += ConvertCFString(string);\n            ret += \")\";\n        }\n    }\n\n    return ret;\n}\n\nstatic std::string ErrorToString(CFErrorRef error)\n{\n    if (!error) {\n        return \"(null)\";\n    }\n\n    if (__builtin_available(macOS 10.3, iOS 11.3, *)) {\n        CFIndex status = CFErrorGetCode(error);\n        ScopedCFTypeRef string = CFErrorCopyFailureReason(error);\n        if (string) {\n            std::string ret = std::to_string(status);\n\n            ret += \" (\";\n            ret += ConvertCFString(string);\n            ret += \")\";\n\n            return ret;\n        }\n    }\n\n    if (ScopedCFTypeRef string = CFErrorCopyDescription(error)) {\n        return ConvertCFString(string);\n    }\n\n    return \"(unknown)\";\n}\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\nnamespace detail {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetNativePersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  const std::string& id,\n  ::mlspp::CipherSuite suite,\n  bool& supported)\n{\n    std::shared_ptr<::mlspp::SignaturePrivateKey> ret;\n\n    CFStringRef keyType = nullptr;\n    int keySize = 0;\n    std::function<bytes(CFDataRef)> convertKey;\n\n    ScopedCFTypeRef query = CFDictionaryCreateMutable(\n      NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n    CFDictionaryAddValue(query, kSecReturnRef, kCFBooleanTrue);\n    CFDictionaryAddValue(query, kSecUseAuthenticationUI, kSecUseAuthenticationUISkip);\n    AddAccessGroup(query);\n\n    auto suiteId = suite.cipher_suite();\n    switch (suiteId) {\n    case ::mlspp::CipherSuite::ID::P256_AES128GCM_SHA256_P256:\n    case ::mlspp::CipherSuite::ID::P384_AES256GCM_SHA384_P384:\n    case ::mlspp::CipherSuite::ID::P521_AES256GCM_SHA512_P521:\n        supported = true;\n        keyType = kSecAttrKeyTypeECSECPrimeRandom;\n        if (suiteId == ::mlspp::CipherSuite::ID::P521_AES256GCM_SHA512_P521) {\n            keySize = 521;\n        }\n        else if (suiteId == ::mlspp::CipherSuite::ID::P384_AES256GCM_SHA384_P384) {\n            keySize = 384;\n        }\n        else {\n            keySize = 256;\n        }\n        convertKey = [keySize](CFDataRef data) {\n            // https://developer.apple.com/documentation/security/1643698-seckeycopyexternalrepresentation\n            // Input has a 1-byte header (always 0x04, per ANSI X9.63), followed by 3\n            // keySize-bit left-padded byte-aligned big-endian integers: X, Y, and K.\n            // X and Y are the public key (represented as the coordinates);\n            // K is the private key.\n            bytes ret;\n            constexpr size_t HeaderSize = 1;\n            constexpr size_t ValueCount = 3;\n            constexpr size_t PublicValues = 2;\n            constexpr uint8_t HeaderByte = 0x04;\n\n            // Convert keySize from bits to bytes (rounding up)\n            CFIndex byteLen = (keySize + 7) / 8;\n\n            CFIndex len = CFDataGetLength(data);\n            if (len < 0 || (size_t)len < HeaderSize + ValueCount * byteLen) {\n                DISCORD_LOG(LS_ERROR)\n                  << \"Exported key blob too small in GetPersistedKeyPair/convertKey: \" << len;\n                return ret;\n            }\n\n            const uint8_t* ptr = CFDataGetBytePtr(data);\n            if (ptr[0] != HeaderByte) {\n                DISCORD_LOG(LS_ERROR)\n                  << \"Exported key blob has unexpected format in GetPersistedKeyPair/convertKey: \"\n                  << ptr[0];\n                return ret;\n            }\n\n            // Skip header, X, and Y, and extract K.\n            ptr += HeaderSize + PublicValues * byteLen;\n            ret.as_vec().assign(ptr, ptr + byteLen);\n\n            return ret;\n        };\n        break;\n    default:\n        // Other suites will need to store keys as generic data items\n        return nullptr;\n    }\n\n    assert(keyType && keySize && convertKey);\n\n    ScopedCFTypeRef<CFNumberRef> sizeRef = CFNumberCreate(NULL, kCFNumberIntType, &keySize);\n\n    std::string labelString = KeyLabelPrefix + id;\n    std::string tagString = KeyTagPrefix + id;\n    ScopedCFTypeRef labelStringRef =\n      CFStringCreateWithCString(NULL, labelString.c_str(), kCFStringEncodingUTF8);\n    ScopedCFTypeRef tagDataRef =\n      CFDataCreate(NULL, (const UInt8*)tagString.c_str(), tagString.size());\n\n    CFDictionaryAddValue(query, kSecClass, kSecClassKey);\n    CFDictionaryAddValue(query, kSecAttrKeyType, keyType);\n    CFDictionaryAddValue(query, kSecAttrApplicationTag, tagDataRef);\n    CFDictionaryAddValue(query, kSecAttrCanSign, kCFBooleanTrue);\n\n    ScopedCFTypeRef<CFErrorRef> cfError;\n    ScopedCFTypeRef<SecKeyRef> key;\n\n    // If we get errSecMissingEntitlement, try again with the file-based keychain\n    constexpr int AttemptCount = 2;\n    for (int attempt = 0; attempt < AttemptCount && !key; attempt++) {\n        cfError.release();\n\n        CFBooleanRef useDataProtection = attempt == 0 ? kCFBooleanTrue : kCFBooleanFalse;\n        if (__builtin_available(macOS 10.15, *)) {\n            CFDictionarySetValue(query, kSecUseDataProtectionKeychain, useDataProtection);\n        }\n        else if (attempt == 1) {\n            return nullptr;\n        }\n\n        OSStatus status = SecItemCopyMatching(query, key.getGenericPtr());\n\n        if (status == errSecSuccess) {\n            ScopedCFTypeRef updateQuery = CFDictionaryCreateMutableCopy(NULL, 0, query);\n            ScopedCFTypeRef updateAttrs = CFDictionaryCreateMutable(\n              NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n            CFDictionaryRemoveValue(updateQuery, kSecReturnRef);\n            CFDictionaryAddValue(\n              updateAttrs, kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly);\n\n            // Best effort\n            OSStatus updateStatus = SecItemUpdate(query, updateAttrs);\n            DISCORD_LOG(LS_INFO) << \"Attempted to update permissions on existing key: \"\n                                 << SecStatusToString(updateStatus);\n        }\n\n        if (status == errSecItemNotFound) {\n            DISCORD_LOG(LS_INFO) << \"Item not found in GetPersistedKeyPair; generating new: \"\n                                 << SecStatusToString(status);\n\n            ScopedCFTypeRef params = CFDictionaryCreateMutable(\n              NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n            AddAccessGroup(params);\n            CFDictionaryAddValue(params, kSecAttrKeyType, keyType);\n            CFDictionaryAddValue(params, kSecAttrKeySizeInBits, sizeRef);\n            CFDictionaryAddValue(params, kSecAttrCanEncrypt, kCFBooleanFalse);\n            CFDictionaryAddValue(params, kSecAttrCanDecrypt, kCFBooleanFalse);\n            CFDictionaryAddValue(params, kSecAttrCanWrap, kCFBooleanFalse);\n            CFDictionaryAddValue(params, kSecAttrCanUnwrap, kCFBooleanFalse);\n            CFDictionaryAddValue(\n              params, kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly);\n            if (__builtin_available(macOS 10.15, *)) {\n                CFDictionaryAddValue(params, kSecUseDataProtectionKeychain, useDataProtection);\n            }\n\n            ScopedCFTypeRef privParams = CFDictionaryCreateMutable(\n              NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n            CFDictionaryAddValue(privParams, kSecAttrIsPermanent, kCFBooleanTrue);\n            CFDictionaryAddValue(privParams, kSecAttrLabel, labelStringRef);\n            CFDictionaryAddValue(privParams, kSecAttrApplicationTag, tagDataRef);\n\n            CFDictionaryAddValue(params, kSecPrivateKeyAttrs, privParams);\n\n            key = SecKeyCreateRandomKey(params, cfError.getPtr());\n\n            if (!key || cfError) {\n                DISCORD_LOG(LS_WARNING)\n                  << \"Failed to create key in GetPersistedKeyPair: \" << ErrorToString(cfError);\n\n                if (!cfError || CFErrorGetCode(cfError) != errSecMissingEntitlement) {\n                    return nullptr;\n                }\n\n                key.release();\n            }\n        }\n        else if (status != 0 || !key) {\n            DISCORD_LOG(LS_WARNING)\n              << \"Item not found GetPersistedKeyPair: \" << SecStatusToString(status);\n            if (status != errSecMissingEntitlement) {\n                return nullptr;\n            }\n        }\n    }\n\n    if (!key) {\n        return nullptr;\n    }\n\n    ScopedCFTypeRef data = SecKeyCopyExternalRepresentation(key, cfError.getPtr());\n    if (!data) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to export key in GetPersistedKeyPair: \"\n                              << ErrorToString(cfError);\n        return nullptr;\n    }\n\n    bytes converted = convertKey(data);\n    if (converted.empty()) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to convert key in GetPersistedKeyPair\";\n        return nullptr;\n    }\n\n    return std::make_shared<::mlspp::SignaturePrivateKey>(\n      ::mlspp::SignaturePrivateKey::parse(suite, converted));\n}\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetGenericPersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  const std::string& id,\n  ::mlspp::CipherSuite suite)\n{\n    ::mlspp::SignaturePrivateKey ret;\n\n    ScopedCFTypeRef query = CFDictionaryCreateMutable(\n      NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n    ScopedCFTypeRef accountString =\n      CFStringCreateWithCString(NULL, id.c_str(), kCFStringEncodingUTF8);\n    CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue);\n    CFDictionaryAddValue(query, kSecUseAuthenticationUI, kSecUseAuthenticationUISkip);\n    CFDictionaryAddValue(query, kSecAttrService, KeyServiceLabel);\n    CFDictionaryAddValue(query, kSecAttrAccount, accountString);\n    CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword);\n    AddAccessGroup(query);\n\n    // If we get errSecMissingEntitlement, try again with the file-based keychain\n    constexpr int AttemptCount = 2;\n    for (int attempt = 0; attempt < AttemptCount && ret.public_key.data.empty(); attempt++) {\n        if (__builtin_available(macOS 10.15, *)) {\n            CFDictionarySetValue(query,\n                                 kSecUseDataProtectionKeychain,\n                                 attempt == 0 ? kCFBooleanTrue : kCFBooleanFalse);\n        }\n        else if (attempt == 1) {\n            return nullptr;\n        }\n\n        ScopedCFTypeRef<CFDataRef> result;\n        OSStatus status = SecItemCopyMatching(query, result.getGenericPtr());\n\n        std::string curstr;\n        if (status == 0 && result) {\n            curstr.assign((char*)CFDataGetBytePtr(result), CFDataGetLength(result));\n\n            try {\n                ret = ::mlspp::SignaturePrivateKey::from_jwk(suite, curstr);\n            }\n            catch (std::exception& ex) {\n                DISCORD_LOG(LS_WARNING)\n                  << \"Failed to parse key in GetPersistedKeyPair: \" << ex.what();\n                return nullptr;\n            }\n        }\n        else if (status == errSecItemNotFound) {\n            DISCORD_LOG(LS_INFO) << \"Did not receive item in GetPersistedKeyPair; generating new: \"\n                                 << SecStatusToString(status);\n\n            ret = ::mlspp::SignaturePrivateKey::generate(suite);\n\n            std::string newstr = ret.to_jwk(suite);\n\n            ScopedCFTypeRef data =\n              CFDataCreate(NULL, (const UInt8*)newstr.c_str(), newstr.length());\n\n            CFDictionaryRemoveValue(query, kSecReturnData);\n            CFDictionaryAddValue(query, kSecValueData, data);\n\n            status = SecItemAdd(query, nullptr);\n            if (status) {\n                DISCORD_LOG(LS_WARNING) << \"Failed to create keychain item in GetPersistedKeyPair: \"\n                                        << SecStatusToString(status);\n\n                if (status != errSecMissingEntitlement) {\n                    return nullptr;\n                }\n\n                ret = ::mlspp::SignaturePrivateKey();\n            }\n        }\n        else {\n            DISCORD_LOG(LS_WARNING)\n              << \"Failed to retrieve item in GetPersistedKeyPair: \" << SecStatusToString(status);\n            if (status != errSecMissingEntitlement) {\n                return nullptr;\n            }\n        }\n    }\n\n    if (!ret.public_key.data.empty()) {\n        return std::make_shared<::mlspp::SignaturePrivateKey>(std::move(ret));\n    }\n    else {\n        return nullptr;\n    }\n}\n\nstatic bool DeleteWithQuery(CFMutableDictionaryRef query)\n{\n#if !TARGET_OS_IPHONE\n    if (__builtin_available(macOS 10.15, *)) {\n        CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue);\n    }\n#endif\n\n    auto ret = SecItemDelete(query);\n\n#if !TARGET_OS_IPHONE\n    if (__builtin_available(macOS 10.15, *)) {\n        if (ret == errSecMissingEntitlement) {\n            CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanFalse);\n            ret = SecItemDelete(query);\n        }\n    }\n#endif\n\n    return ret == errSecSuccess;\n}\n\nbool DeleteNativePersistedKeyPair([[maybe_unused]] KeyPairContextType ctx, const std::string& id)\n{\n    std::string tagString = KeyTagPrefix + id;\n    ScopedCFTypeRef tagDataRef =\n      CFDataCreate(NULL, (const UInt8*)tagString.c_str(), tagString.size());\n\n    ScopedCFTypeRef query = CFDictionaryCreateMutable(\n      NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n    CFDictionaryAddValue(query, kSecClass, kSecClassKey);\n    CFDictionaryAddValue(query, kSecAttrApplicationTag, tagDataRef);\n    AddAccessGroup(query);\n\n    return DeleteWithQuery(query);\n}\n\nbool DeleteGenericPersistedKeyPair([[maybe_unused]] KeyPairContextType ctx, const std::string& id)\n{\n    ScopedCFTypeRef accountString =\n      CFStringCreateWithCString(NULL, id.c_str(), kCFStringEncodingUTF8);\n\n    ScopedCFTypeRef query = CFDictionaryCreateMutable(\n      NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);\n\n    CFDictionaryAddValue(query, kSecAttrService, KeyServiceLabel);\n    CFDictionaryAddValue(query, kSecAttrAccount, accountString);\n    CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword);\n    AddAccessGroup(query);\n\n    return DeleteWithQuery(query);\n}\n\n} // namespace detail\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/detail/persisted_key_pair_generic.cpp",
    "content": "#include \"mls/detail/persisted_key_pair.h\"\n\n#include <cassert>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <mutex>\n#include <sstream>\n#include <string>\n\n#ifdef _WIN32\n#include <io.h>\n#else\n#include <sys/stat.h>\n#include <unistd.h>\n#endif\n#include <fcntl.h>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n#include <mls/crypto.h>\n\n#include \"mls/parameters.h\"\n\nstatic const std::string_view KeyStorageDir = \"Discord Key Storage\";\n\nstatic std::filesystem::path GetKeyStorageDirectory()\n{\n    std::filesystem::path dir;\n\n#if defined(__ANDROID__)\n    dir = std::filesystem::path(\"/data/data\");\n\n    {\n        std::ifstream idFile(\"/proc/self/cmdline\", std::ios_base::in);\n        std::string appId;\n        std::getline(idFile, appId, '\\0');\n        dir /= appId;\n    }\n#else // __ANDROID__\n#if defined(_WIN32)\n    if (const wchar_t* appdata = _wgetenv(L\"LOCALAPPDATA\")) {\n        dir = std::filesystem::path(appdata);\n    }\n#else  // _WIN32\n    if (const char* xdg = getenv(\"XDG_CONFIG_HOME\")) {\n        dir = std::filesystem::path(xdg);\n    }\n    else if (const char* home = getenv(\"HOME\")) {\n        dir = std::filesystem::path(home);\n        dir /= \".config\";\n    }\n#endif // !_WIN32\n    else {\n        return dir;\n    }\n#endif // !__ANDROID__\n\n    return dir / KeyStorageDir;\n}\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\nnamespace detail {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetGenericPersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  const std::string& id,\n  ::mlspp::CipherSuite suite)\n{\n    ::mlspp::SignaturePrivateKey ret;\n    std::string curstr;\n    std::filesystem::path dir = GetKeyStorageDirectory();\n\n    if (dir.empty()) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to determine key storage directory in GetPersistedKeyPair\";\n        return nullptr;\n    }\n\n    std::error_code errc;\n    std::filesystem::create_directories(dir, errc);\n    if (errc) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to create key storage directory in GetPersistedKeyPair: \"\n                              << errc;\n        return nullptr;\n    }\n\n    std::filesystem::path file = dir / (id + \".key\");\n\n    if (std::filesystem::exists(file)) {\n        std::ifstream ifs(file, std::ios_base::in | std::ios_base::binary);\n        if (!ifs) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to open key in GetPersistedKeyPair\";\n            return nullptr;\n        }\n\n        curstr = (std::stringstream() << ifs.rdbuf()).str();\n        if (!ifs) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to read key in GetPersistedKeyPair\";\n            return nullptr;\n        }\n\n        try {\n            ret = ::mlspp::SignaturePrivateKey::from_jwk(suite, curstr);\n        }\n        catch (std::exception& ex) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to parse key in GetPersistedKeyPair: \" << ex.what();\n            return nullptr;\n        }\n    }\n    else {\n        ret = ::mlspp::SignaturePrivateKey::generate(suite);\n\n        std::string newstr = ret.to_jwk(suite);\n\n        std::filesystem::path tmpfile = file;\n        tmpfile += \".tmp\";\n\n#ifdef _WIN32\n        int fd = _wopen(tmpfile.c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC, _S_IREAD | _S_IWRITE);\n#else\n        int fd = open(tmpfile.c_str(),\n                      O_WRONLY | O_CLOEXEC | O_NOFOLLOW | O_CREAT | O_TRUNC,\n                      S_IRUSR | S_IWUSR);\n#endif\n        if (fd < 0) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to open output file in GetPersistedKeyPair: \" << errno\n                                  << \"(\" << tmpfile << \")\";\n            return nullptr;\n        }\n\n#ifdef _WIN32\n        int wret = _write(fd, newstr.c_str(), static_cast<unsigned int>(newstr.size()));\n        _close(fd);\n#else\n        ssize_t wret = write(fd, newstr.c_str(), newstr.size());\n        close(fd);\n#endif\n        if (wret < 0 || (size_t)wret != newstr.size()) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to write output file in GetPersistedKeyPair: \"\n                                  << errno;\n            return nullptr;\n        }\n\n        std::filesystem::rename(tmpfile, file, errc);\n        if (errc) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to rename output file in GetPersistedKeyPair: \"\n                                  << errc;\n            return nullptr;\n        }\n    }\n\n    if (!ret.public_key.data.empty()) {\n        return std::make_shared<::mlspp::SignaturePrivateKey>(std::move(ret));\n    }\n    else {\n        return nullptr;\n    }\n}\n\nbool DeleteGenericPersistedKeyPair([[maybe_unused]] KeyPairContextType ctx, const std::string& id)\n{\n    std::error_code errc;\n    std::filesystem::path dir = GetKeyStorageDirectory();\n    if (dir.empty()) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to determine key storage directory in GetPersistedKeyPair\";\n        return false;\n    }\n\n    std::filesystem::path file = dir / (id + \".key\");\n\n    return std::filesystem::remove(file, errc);\n}\n\n} // namespace detail\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/detail/persisted_key_pair_null.cpp",
    "content": "#include \"mls/detail/persisted_key_pair.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\nnamespace detail {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetNativePersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  [[maybe_unused]] const std::string& keyID,\n  [[maybe_unused]] ::mlspp::CipherSuite suite,\n  bool& supported)\n{\n    supported = false;\n    return nullptr;\n}\n\nbool DeleteNativePersistedKeyPair([[maybe_unused]] KeyPairContextType ctx,\n                                  [[maybe_unused]] const std::string& keyID)\n{\n    return false;\n}\n\n} // namespace detail\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/detail/persisted_key_pair_win.cpp",
    "content": "#include \"mls/detail/persisted_key_pair.h\"\n\n#include <cassert>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <mutex>\n#include <sstream>\n#include <string>\n\n#ifndef SECURITY_WIN32\n#define SECURITY_WIN32 1\n#endif\n#include <Windows.h>\n#include <Bcrypt.h>\n#include <Security.h>\n#include <ncrypt.h>\n#include <wincrypt.h>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n#include <mls/crypto.h>\n\n#include \"mls/parameters.h\"\n\nstatic const std::string KeyTagPrefix = \"discord-secure-frames-key-\";\n\ntemplate <class T = NCRYPT_HANDLE>\nstruct ScopedNCryptHandle {\n    ScopedNCryptHandle() = default;\n    ScopedNCryptHandle(T handle)\n      : handle_(handle)\n    {\n    }\n    ScopedNCryptHandle(const ScopedNCryptHandle& other) = delete;\n    ScopedNCryptHandle(ScopedNCryptHandle&& other)\n      : handle_(std::exchange(other.handle_, T()))\n    {\n    }\n\n    ~ScopedNCryptHandle() { finalize(); }\n\n    ScopedNCryptHandle& operator=(T handle)\n    {\n        finalize();\n        handle_ = handle;\n        return *this;\n    }\n\n    T release() { return std::exchange(handle_, T()); }\n\n    void finalize()\n    {\n        if (auto handle = release()) {\n            NCryptFreeObject(handle);\n        }\n    }\n\n    T& get() { return handle_; }\n\n    T* getPtr() { return &handle_; }\n\n    operator T&() { return get(); }\n\n    explicit operator bool() { return handle_ != T(); }\n\n    T handle_ = T();\n};\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\nnamespace detail {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetNativePersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  const std::string& id,\n  ::mlspp::CipherSuite suite,\n  bool& supported)\n{\n    LPCWSTR keyType = nullptr;\n    ULONG keyBlobMagic = 0;\n    std::function<bool(bytes&)> convertBlob;\n\n    auto suiteId = suite.cipher_suite();\n    switch (suiteId) {\n    case ::mlspp::CipherSuite::ID::P256_AES128GCM_SHA256_P256:\n    case ::mlspp::CipherSuite::ID::P384_AES256GCM_SHA384_P384:\n    case ::mlspp::CipherSuite::ID::P521_AES256GCM_SHA512_P521:\n        supported = true;\n        if (suiteId == ::mlspp::CipherSuite::ID::P521_AES256GCM_SHA512_P521) {\n            keyType = BCRYPT_ECDSA_P521_ALGORITHM;\n            keyBlobMagic = BCRYPT_ECDSA_PRIVATE_P521_MAGIC;\n        }\n        else if (suiteId == ::mlspp::CipherSuite::ID::P384_AES256GCM_SHA384_P384) {\n            keyType = BCRYPT_ECDSA_P384_ALGORITHM;\n            keyBlobMagic = BCRYPT_ECDSA_PRIVATE_P384_MAGIC;\n        }\n        else {\n            keyType = BCRYPT_ECDSA_P256_ALGORITHM;\n            keyBlobMagic = BCRYPT_ECDSA_PRIVATE_P256_MAGIC;\n        }\n\n        convertBlob = [](bytes& blob) {\n            // https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_ecckey_blob\n            // Input has an PBCRYPT_ECCKEY_BLOB header, followed by 3 cbKey-byte big-endian\n            // integers: X, Y, and d. X and Y are the public key (represented as the coordinates);\n            // d is the private key.\n            constexpr size_t ValueCount = 3;\n            constexpr size_t PublicValues = 2;\n\n            if (blob.size() < sizeof(BCRYPT_ECCKEY_BLOB)) {\n                DISCORD_LOG(LS_ERROR)\n                  << \"Exported key blob too small in GetPersistedKeyPair/convertBlob: \"\n                  << blob.size();\n                return false;\n            }\n\n            PBCRYPT_ECCKEY_BLOB header = (PBCRYPT_ECCKEY_BLOB)blob.data();\n            ULONG keySize = header->cbKey;\n            if (blob.size() < sizeof(BCRYPT_ECCKEY_BLOB) + keySize * ValueCount) {\n                DISCORD_LOG(LS_ERROR)\n                  << \"Exported key blob too small in GetPersistedKeyPair/convertBlob: \"\n                  << blob.size();\n                return false;\n            }\n            blob.resize(sizeof(BCRYPT_ECCKEY_BLOB) + keySize * ValueCount);\n            blob.as_vec().erase(blob.begin(),\n                                blob.begin() + sizeof(BCRYPT_ECCKEY_BLOB) + keySize * PublicValues);\n            return true;\n        };\n        break;\n    default:\n        // Other suites will need to store keys as JWK files on disk\n        return nullptr;\n    }\n\n    assert(keyType && keyBlobMagic && convertBlob);\n\n    ScopedNCryptHandle<NCRYPT_PROV_HANDLE> provider;\n    SECURITY_STATUS status =\n      NCryptOpenStorageProvider(provider.getPtr(), MS_KEY_STORAGE_PROVIDER, 0);\n    if (status != ERROR_SUCCESS) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to open storage provider in GetPersistedKeyPair: \"\n                              << status;\n        return nullptr;\n    }\n\n    std::filesystem::path keyName = KeyTagPrefix + id;\n\n    ScopedNCryptHandle<NCRYPT_KEY_HANDLE> key;\n    status =\n      NCryptOpenKey(provider, key.getPtr(), keyName.c_str(), AT_SIGNATURE, NCRYPT_SILENT_FLAG);\n\n    if (status == NTE_BAD_KEYSET) {\n        DISCORD_LOG(LS_INFO) << \"No key found in GetPersistedKeyPair; generating new\";\n\n        status = NCryptCreatePersistedKey(\n          provider, key.getPtr(), keyType, keyName.c_str(), AT_SIGNATURE, 0);\n        if (status != ERROR_SUCCESS) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to create key in GetPersistedKeyPair: \" << status;\n            return nullptr;\n        }\n\n        DWORD exportPolicyValue = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;\n        status = NCryptSetProperty(key,\n                                   NCRYPT_EXPORT_POLICY_PROPERTY,\n                                   (PBYTE)&exportPolicyValue,\n                                   sizeof(exportPolicyValue),\n                                   NCRYPT_PERSIST_FLAG | NCRYPT_SILENT_FLAG);\n        if (status != ERROR_SUCCESS) {\n            DISCORD_LOG(LS_ERROR)\n              << \"Failed to configure key export policy in GetPersistedKeyPair: \" << status;\n            return nullptr;\n        }\n\n        // struct {\n        //     DWORD   dwVersion;\n        //     DWORD   dwFlags;\n        //     LPCWSTR pszCreationTitle;\n        //     LPCWSTR pszFriendlyName;\n        //     LPCWSTR pszDescription;\n        // } NCRYPT_UI_POLICY;\n\n        NCRYPT_UI_POLICY uiPolicyValue = {1, 0, nullptr, nullptr, nullptr};\n        status = NCryptSetProperty(key,\n                                   NCRYPT_UI_POLICY_PROPERTY,\n                                   (PBYTE)&uiPolicyValue,\n                                   sizeof(uiPolicyValue),\n                                   NCRYPT_PERSIST_FLAG | NCRYPT_SILENT_FLAG);\n        if (status != ERROR_SUCCESS) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to configure key UI policy in GetPersistedKeyPair: \"\n                                  << status;\n            return nullptr;\n        }\n\n        status = NCryptFinalizeKey(key, NCRYPT_SILENT_FLAG);\n        if (status != ERROR_SUCCESS) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to finalize key in GetPersistedKeyPair: \" << status;\n            return nullptr;\n        }\n    }\n    else if (status != ERROR_SUCCESS) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to open key in GetPersistedKeyPair: \" << status;\n        return nullptr;\n    }\n\n    DWORD keySize = 0;\n    status = NCryptExportKey(\n      key, NULL, BCRYPT_PRIVATE_KEY_BLOB, NULL, NULL, 0, &keySize, NCRYPT_SILENT_FLAG);\n    if (status != ERROR_SUCCESS) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to size key in GetPersistedKeyPair: \" << status;\n        return nullptr;\n    }\n\n    bytes keyData(keySize);\n\n    status = NCryptExportKey(key,\n                             NULL,\n                             BCRYPT_PRIVATE_KEY_BLOB,\n                             NULL,\n                             keyData.data(),\n                             keySize,\n                             &keySize,\n                             NCRYPT_SILENT_FLAG);\n    if (status != ERROR_SUCCESS) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to export key in GetPersistedKeyPair: \" << status;\n        return nullptr;\n    }\n\n    if (keyData.size() < sizeof(BCRYPT_KEY_BLOB)) {\n        DISCORD_LOG(LS_ERROR) << \"Exported key blob too small in GetPersistedKeyPair/convertBlob: \"\n                              << keyData.size();\n        return nullptr;\n    }\n\n    BCRYPT_KEY_BLOB* header = (BCRYPT_KEY_BLOB*)keyData.data();\n    if (header->Magic != keyBlobMagic) {\n        DISCORD_LOG(LS_ERROR) << \"Exported key blob has unexpected magic in GetPersistedKeyPair: \"\n                              << header->Magic;\n        return nullptr;\n    }\n\n    if (!convertBlob(keyData)) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to convert key in GetPersistedKeyPair\";\n        return nullptr;\n    }\n\n    return std::make_shared<::mlspp::SignaturePrivateKey>(\n      ::mlspp::SignaturePrivateKey::parse(suite, keyData));\n}\n\nbool DeleteNativePersistedKeyPair([[maybe_unused]] KeyPairContextType ctx, const std::string& id)\n{\n    ScopedNCryptHandle<NCRYPT_PROV_HANDLE> provider;\n    SECURITY_STATUS status =\n      NCryptOpenStorageProvider(provider.getPtr(), MS_KEY_STORAGE_PROVIDER, 0);\n    if (status != ERROR_SUCCESS) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to open storage provider in DeletePersistedKeyPair: \"\n                              << status;\n        return false;\n    }\n\n    std::filesystem::path keyName = KeyTagPrefix + id;\n\n    ScopedNCryptHandle<NCRYPT_KEY_HANDLE> key;\n    status =\n      NCryptOpenKey(provider, key.getPtr(), keyName.c_str(), AT_SIGNATURE, NCRYPT_SILENT_FLAG);\n    if (status != ERROR_SUCCESS) {\n        return false;\n    }\n\n    auto ret = NCryptDeleteKey(key, NCRYPT_SILENT_FLAG);\n    if (ret == ERROR_SUCCESS) {\n        // If NCryptDeleteKey succeeds, it frees the handle, so our wrapper shouldn't also do so.\n        key.release();\n        return true;\n    }\n    else {\n        return false;\n    }\n}\n\n} // namespace detail\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/parameters.cpp",
    "content": "#include \"parameters.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::CipherSuite::ID CiphersuiteIDForProtocolVersion(\n  [[maybe_unused]] ProtocolVersion version) noexcept\n{\n    return ::mlspp::CipherSuite::ID::P256_AES128GCM_SHA256_P256;\n}\n\n::mlspp::CipherSuite CiphersuiteForProtocolVersion(ProtocolVersion version) noexcept\n{\n    return ::mlspp::CipherSuite{CiphersuiteIDForProtocolVersion(version)};\n}\n\n::mlspp::CipherSuite::ID CiphersuiteIDForSignatureVersion(\n  [[maybe_unused]] SignatureVersion version) noexcept\n{\n    return ::mlspp::CipherSuite::ID::P256_AES128GCM_SHA256_P256;\n}\n\n::mlspp::CipherSuite CiphersuiteForSignatureVersion(SignatureVersion version) noexcept\n{\n    return ::mlspp::CipherSuite{CiphersuiteIDForProtocolVersion(version)};\n}\n\n::mlspp::Capabilities LeafNodeCapabilitiesForProtocolVersion(ProtocolVersion version) noexcept\n{\n    auto capabilities = ::mlspp::Capabilities::create_default();\n\n    capabilities.cipher_suites = {CiphersuiteIDForProtocolVersion(version)};\n    capabilities.credentials = {::mlspp::CredentialType::basic};\n\n    return capabilities;\n}\n\n::mlspp::ExtensionList LeafNodeExtensionsForProtocolVersion(\n  [[maybe_unused]] ProtocolVersion version) noexcept\n{\n    return ::mlspp::ExtensionList{};\n}\n\n::mlspp::ExtensionList GroupExtensionsForProtocolVersion(\n  [[maybe_unused]] ProtocolVersion version,\n  const ::mlspp::ExternalSender& externalSender) noexcept\n{\n    auto extensionList = ::mlspp::ExtensionList{};\n\n    extensionList.add(::mlspp::ExternalSendersExtension{{\n      {externalSender.signature_key, externalSender.credential},\n    }});\n\n    return extensionList;\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/parameters.h",
    "content": "#pragma once\n\n#include <dave/version.h>\n#include <mls/core_types.h>\n#include <mls/crypto.h>\n#include <mls/messages.h>\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::CipherSuite::ID CiphersuiteIDForProtocolVersion(ProtocolVersion version) noexcept;\n::mlspp::CipherSuite CiphersuiteForProtocolVersion(ProtocolVersion version) noexcept;\n::mlspp::CipherSuite::ID CiphersuiteIDForSignatureVersion(SignatureVersion version) noexcept;\n::mlspp::CipherSuite CiphersuiteForSignatureVersion(SignatureVersion version) noexcept;\n::mlspp::Capabilities LeafNodeCapabilitiesForProtocolVersion(ProtocolVersion version) noexcept;\n::mlspp::ExtensionList LeafNodeExtensionsForProtocolVersion(ProtocolVersion version) noexcept;\n::mlspp::ExtensionList GroupExtensionsForProtocolVersion(\n  ProtocolVersion version,\n  const ::mlspp::ExternalSender& externalSender) noexcept;\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/persisted_key_pair.cpp",
    "content": "#include \"mls/detail/persisted_key_pair.h\"\n\n#include <cassert>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <mutex>\n#include <sstream>\n#include <string>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n#include <mls/crypto.h>\n\n#include \"mls/parameters.h\"\n\nstatic const std::string SelfSignatureLabel = \"DiscordSelfSignature\";\n\nstatic std::string MakeKeyID(const std::string& sessionID, ::mlspp::CipherSuite suite)\n{\n    return sessionID + \"-\" + std::to_string((uint16_t)suite.cipher_suite()) + \"-\" +\n      std::to_string(discord::dave::mls::KeyVersion);\n}\n\nstatic std::mutex mtx;\nstatic std::map<std::string, std::shared_ptr<::mlspp::SignaturePrivateKey>> map;\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\nstatic std::shared_ptr<::mlspp::SignaturePrivateKey> GetPersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType ctx,\n  const std::string& sessionID,\n  ::mlspp::CipherSuite suite)\n{\n    std::lock_guard lk(mtx);\n\n    std::string id = MakeKeyID(sessionID, suite);\n\n    if (auto it = map.find(id); it != map.end()) {\n        return it->second;\n    }\n\n    std::shared_ptr<::mlspp::SignaturePrivateKey> ret;\n\n    bool supported = false;\n    ret = ::discord::dave::mls::detail::GetNativePersistedKeyPair(ctx, id, suite, supported);\n\n    if (!ret && supported) {\n        // Do not fall back on the generic route if we error here\n        DISCORD_LOG(LS_ERROR) << \"Encountered error in native key handling in GetPersistedKeyPair\";\n        return nullptr;\n    }\n\n    if (!ret) {\n        ret = ::discord::dave::mls::detail::GetGenericPersistedKeyPair(ctx, id, suite);\n    }\n\n    if (!ret) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to get key in GetPersistedKeyPair\";\n        return nullptr;\n    }\n\n    map.emplace(id, ret);\n\n    return ret;\n}\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetPersistedKeyPair(KeyPairContextType ctx,\n                                                                  const std::string& sessionID,\n                                                                  ProtocolVersion version)\n{\n    return GetPersistedKeyPair(ctx, sessionID, CiphersuiteForProtocolVersion(version));\n}\n\nKeyAndSelfSignature GetPersistedPublicKey(KeyPairContextType ctx,\n                                          const std::string& sessionID,\n                                          SignatureVersion version)\n{\n    auto suite = CiphersuiteForSignatureVersion(version);\n\n    auto pair = GetPersistedKeyPair(ctx, sessionID, suite);\n\n    if (!pair) {\n        return {};\n    }\n\n    bytes sign_data = from_ascii(sessionID + \":\") + pair->public_key.data;\n\n    return {\n      pair->public_key.data.as_vec(),\n      std::move(pair->sign(suite, SelfSignatureLabel, sign_data).as_vec()),\n    };\n}\n\nbool DeletePersistedKeyPair([[maybe_unused]] KeyPairContextType ctx,\n                            const std::string& sessionID,\n                            SignatureVersion version)\n{\n    std::string id = MakeKeyID(sessionID, CiphersuiteForSignatureVersion(version));\n\n    std::lock_guard lk(mtx);\n\n    map.erase(id);\n\n    bool native = ::discord::dave::mls::detail::DeleteNativePersistedKeyPair(ctx, id);\n    bool generic = ::discord::dave::mls::detail::DeleteGenericPersistedKeyPair(ctx, id);\n\n    return native || generic;\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/persisted_key_pair.h",
    "content": "#pragma once\n\n#include <memory>\n#include <string>\n#include <vector>\n\n#ifdef __ANDROID__\n#include <jni.h>\n#endif\n\n#include <dave/dave_interfaces.h>\n#include <dave/version.h>\n\nnamespace mlspp {\nstruct SignaturePrivateKey;\n};\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetPersistedKeyPair(KeyPairContextType ctx,\n                                                                  const std::string& sessionID,\n                                                                  ProtocolVersion version);\n\nstruct KeyAndSelfSignature {\n    std::vector<uint8_t> key;\n    std::vector<uint8_t> signature;\n};\n\nKeyAndSelfSignature GetPersistedPublicKey(KeyPairContextType ctx,\n                                          const std::string& sessionID,\n                                          SignatureVersion version);\n\nbool DeletePersistedKeyPair(KeyPairContextType ctx,\n                            const std::string& sessionID,\n                            SignatureVersion version);\n\nconstexpr unsigned KeyVersion = 1;\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/persisted_key_pair_null.cpp",
    "content": "#include \"persisted_key_pair.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\nstd::shared_ptr<::mlspp::SignaturePrivateKey> GetPersistedKeyPair(\n  [[maybe_unused]] KeyPairContextType,\n  [[maybe_unused]] const std::string&,\n  [[maybe_unused]] ProtocolVersion)\n{\n    return nullptr;\n}\n\nbool DeletePersistedKeyPair([[maybe_unused]] KeyPairContextType,\n                            [[maybe_unused]] const std::string&,\n                            [[maybe_unused]] SignatureVersion)\n{\n    return false;\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/session.cpp",
    "content": "#include \"session.h\"\n\n#include <cstring>\n#include <thread>\n#include <vector>\n\n#include <dave/logger.h>\n#include <hpke/random.h>\n#include <hpke/signature.h>\n#include <mls/crypto.h>\n#include <mls/messages.h>\n#include <mls/state.h>\n\n#include \"common.h\"\n#include \"mls/parameters.h\"\n#include \"mls/persisted_key_pair.h\"\n#include \"mls/user_credential.h\"\n#include \"mls/util.h\"\n#include \"mls_key_ratchet.h\"\n\n#include \"openssl/evp.h\"\n\n#define TRACK_MLS_ERROR(reason)                      \\\n    if (onMLSFailureCallback_) {                     \\\n        onMLSFailureCallback_(__FUNCTION__, reason); \\\n    }\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\nstruct QueuedProposal {\n    ::mlspp::ValidatedContent content;\n    ::mlspp::bytes_ns::bytes ref;\n};\n\nstd::unique_ptr<ISession> CreateSession(KeyPairContextType context,\n                                        std::string authSessionId,\n                                        MLSFailureCallback callback) noexcept\n{\n    return std::make_unique<Session>(context, authSessionId, callback);\n}\n\nSession::Session(KeyPairContextType context,\n                 std::string authSessionId,\n                 MLSFailureCallback callback) noexcept\n  : signingKeyId_(authSessionId)\n  , keyPairContext_(context)\n  , onMLSFailureCallback_(std::move(callback))\n{\n    DISCORD_LOG(LS_INFO) << \"Creating a new MLS session\";\n}\n\nSession::~Session() noexcept = default;\n\nvoid Session::Init(ProtocolVersion protocolVersion,\n                   uint64_t groupId,\n                   std::string const& selfUserId,\n                   std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey) noexcept\n{\n    Reset();\n\n    selfUserId_ = selfUserId;\n\n    DISCORD_LOG(LS_INFO) << \"Initializing MLS session with protocol version \" << protocolVersion\n                         << \" and group ID \" << groupId;\n    protocolVersion_ = protocolVersion;\n    groupId_ = std::move(BigEndianBytesFrom(groupId).as_vec());\n\n    InitLeafNode(selfUserId, transientKey);\n\n    if (externalSender_) {\n        CreatePendingGroup();\n    }\n    else {\n        DISCORD_LOG(LS_INFO) << \"Waiting for external sender to create a pending group\";\n    }\n}\n\nvoid Session::Reset() noexcept\n{\n    DISCORD_LOG(LS_INFO) << \"Resetting MLS session\";\n\n    ClearPendingState();\n\n    currentState_.reset();\n    outboundCachedGroupState_.reset();\n\n    protocolVersion_ = 0;\n    groupId_.clear();\n}\n\nvoid Session::SetProtocolVersion(ProtocolVersion version) noexcept\n{\n    if (version != protocolVersion_) {\n        // when we need to retain backwards compatibility\n        // there may be some changes to the MLS objects required here\n        // until then we can just update the stored version\n        protocolVersion_ = version;\n    }\n}\n\nstd::vector<uint8_t> Session::GetLastEpochAuthenticator() const noexcept\n{\n    if (!currentState_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot get epoch authenticator without an established MLS group\";\n        return {};\n    }\n\n    return std::move(currentState_->epoch_authenticator().as_vec());\n}\n\nvoid Session::SetExternalSender(const std::vector<uint8_t>& marshalledExternalSender) noexcept\ntry {\n    if (currentState_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot set external sender after joining/creating an MLS group\";\n        return;\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Unmarshalling MLS external sender\";\n\n    DISCORD_LOG(LS_INFO) << \"Sender: \" << ::mlspp::bytes_ns::bytes(marshalledExternalSender);\n\n    externalSender_ = std::make_unique<::mlspp::ExternalSender>(\n      ::mlspp::tls::get<::mlspp::ExternalSender>(marshalledExternalSender));\n\n    if (!groupId_.empty()) {\n        CreatePendingGroup();\n    }\n    else {\n        DISCORD_LOG(LS_INFO) << \"Waiting for group ID to create a pending group\";\n    }\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to unmarshal external sender: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return;\n}\n\nstd::optional<std::vector<uint8_t>> Session::ProcessProposals(\n  std::vector<uint8_t> proposals,\n  std::set<std::string> const& recognizedUserIDs) noexcept\ntry {\n    if (!pendingGroupState_ && !currentState_) {\n        DISCORD_LOG(LS_ERROR)\n          << \"Cannot process proposals without any pending or established MLS group state\";\n        return std::nullopt;\n    }\n\n    if (!stateWithProposals_) {\n        stateWithProposals_ = std::make_unique<::mlspp::State>(\n          pendingGroupState_ ? *pendingGroupState_ : *currentState_);\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Processing MLS proposals message of \" << proposals.size() << \" bytes\";\n\n    DISCORD_LOG(LS_INFO) << \"Proposals: \" << ::mlspp::bytes_ns::bytes(proposals);\n\n    ::mlspp::tls::istream inStream(proposals);\n\n    bool isRevoke = false;\n    inStream >> isRevoke;\n\n    DISCORD_LOG(LS_INFO) << \"Revoking: \" << isRevoke;\n\n    const auto suite = stateWithProposals_->cipher_suite();\n\n    if (isRevoke) {\n        std::vector<::mlspp::bytes_ns::bytes> refs;\n        inStream >> refs;\n\n        for (const auto& ref : refs) {\n            bool found = false;\n            for (auto it = proposalQueue_.begin(); it != proposalQueue_.end(); it++) {\n                if (it->ref == ref) {\n                    found = true;\n                    proposalQueue_.erase(it);\n                    break;\n                }\n            }\n\n            if (!found) {\n                DISCORD_LOG(LS_ERROR) << \"Cannot revoke unrecognized proposal ref\";\n                TRACK_MLS_ERROR(\"Unrecognized proposal revocation\");\n                return std::nullopt;\n            }\n        }\n\n        stateWithProposals_ = std::make_unique<::mlspp::State>(\n          pendingGroupState_ ? *pendingGroupState_ : *currentState_);\n\n        for (auto& prop : proposalQueue_) {\n            // success will queue the proposal, failure will throw\n            stateWithProposals_->handle(prop.content);\n        }\n    }\n    else {\n        std::vector<::mlspp::MLSMessage> messages;\n        inStream >> messages;\n\n        for (const auto& proposalMessage : messages) {\n            auto validatedMessage = stateWithProposals_->unwrap(proposalMessage);\n\n            if (!ValidateProposalMessage(validatedMessage.authenticated_content(),\n                                         *stateWithProposals_,\n                                         recognizedUserIDs)) {\n                return std::nullopt;\n            }\n\n            // success will queue the proposal, failure will throw\n            stateWithProposals_->handle(validatedMessage);\n\n            auto ref = suite.ref(validatedMessage.authenticated_content());\n\n            proposalQueue_.push_back({\n              std::move(validatedMessage),\n              std::move(ref),\n            });\n        }\n    }\n\n    // generate a commit\n    auto commitSecret = ::mlspp::hpke::random_bytes(suite.secret_size());\n\n    auto commitOpts = ::mlspp::CommitOpts{\n      {},    // no extra proposals\n      true,  // inline tree in welcome\n      false, // do not force path\n      {}     // default leaf node options\n    };\n\n    auto [commitMessage, welcomeMessage, newState] =\n      stateWithProposals_->commit(commitSecret, commitOpts, {});\n\n    DISCORD_LOG(LS_INFO)\n      << \"Prepared commit/welcome/next state for MLS group from received proposals\";\n\n    // combine the commit and welcome messages into a single buffer\n    auto outStream = ::mlspp::tls::ostream();\n    outStream << commitMessage;\n\n    // keep a copy of the commit, we can check incoming pending group commit later for a match\n    pendingGroupCommit_ = std::make_unique<::mlspp::MLSMessage>(std::move(commitMessage));\n\n    // if there were any add proposals in this commit, then we also include the welcome message\n    if (welcomeMessage.secrets.size() > 0) {\n        outStream << welcomeMessage;\n    }\n\n    // cache the outbound state in case we're the winning sender\n    outboundCachedGroupState_ = std::make_unique<::mlspp::State>(std::move(newState));\n\n    DISCORD_LOG(LS_INFO) << \"Output: \" << ::mlspp::bytes_ns::bytes(outStream.bytes());\n\n    return outStream.bytes();\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to parse MLS proposals: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return std::nullopt;\n}\n\nbool Session::IsRecognizedUserID(const ::mlspp::Credential& cred,\n                                 std::set<std::string> const& recognizedUserIDs) const\n{\n    std::string uid = UserCredentialToString(cred, protocolVersion_);\n    if (uid.empty()) {\n        DISCORD_LOG(LS_ERROR) << \"Attempted to verify credential of unexpected type\";\n        return false;\n    }\n\n    if (recognizedUserIDs.find(uid) == recognizedUserIDs.end()) {\n        DISCORD_LOG(LS_ERROR) << \"Attempted to verify credential for unrecognized user ID: \" << uid;\n        return false;\n    }\n\n    return true;\n}\n\nbool Session::ValidateProposalMessage(::mlspp::AuthenticatedContent const& message,\n                                      ::mlspp::State const& targetState,\n                                      std::set<std::string> const& recognizedUserIDs) const\n{\n    if (message.wire_format != ::mlspp::WireFormat::mls_public_message) {\n        DISCORD_LOG(LS_ERROR) << \"MLS proposal message must be PublicMessage\";\n        TRACK_MLS_ERROR(\"Invalid proposal wire format\");\n        return false;\n    }\n\n    if (message.content.epoch != targetState.epoch()) {\n        DISCORD_LOG(LS_ERROR) << \"MLS proposal message must be for current epoch (\"\n                              << message.content.epoch << \" != \" << targetState.epoch() << \")\";\n        TRACK_MLS_ERROR(\"Proposal epoch mismatch\");\n        return false;\n    }\n\n    if (message.content.content_type() != ::mlspp::ContentType::proposal) {\n        DISCORD_LOG(LS_ERROR) << \"ProcessProposals called with non-proposal message\";\n        TRACK_MLS_ERROR(\"Unexpected message type\");\n        return false;\n    }\n\n    if (message.content.sender.sender_type() != ::mlspp::SenderType::external) {\n        DISCORD_LOG(LS_ERROR) << \"MLS proposal must be from external sender\";\n        TRACK_MLS_ERROR(\"Unexpected proposal sender type\");\n        return false;\n    }\n\n    const auto& proposal = ::mlspp::tls::var::get<::mlspp::Proposal>(message.content.content);\n    switch (proposal.proposal_type()) {\n    case ::mlspp::ProposalType::add: {\n        const auto& credential =\n          ::mlspp::tls::var::get<::mlspp::Add>(proposal.content).key_package.leaf_node.credential;\n        if (!IsRecognizedUserID(credential, recognizedUserIDs)) {\n            DISCORD_LOG(LS_ERROR) << \"MLS add proposal must be for recognized user\";\n            TRACK_MLS_ERROR(\"Unexpected user ID in add proposal\");\n            return false;\n        }\n        break;\n    }\n    case ::mlspp::ProposalType::remove:\n        // Remove proposals are always allowed (mlspp will validate that it's a recognized user)\n        break;\n    default:\n        DISCORD_LOG(LS_ERROR) << \"MLS proposal must be add or remove\";\n        TRACK_MLS_ERROR(\"Unexpected proposal type\");\n        return false;\n    }\n\n    return true;\n}\n\nbool Session::CanProcessCommit(const ::mlspp::MLSMessage& commit) noexcept\n{\n    if (!stateWithProposals_) {\n        return false;\n    }\n\n    if (commit.group_id() != groupId_) {\n        DISCORD_LOG(LS_ERROR) << \"MLS commit message was for unexpected group\";\n        return false;\n    }\n\n    return true;\n}\n\nRosterVariant Session::ProcessCommit(std::vector<uint8_t> commit) noexcept\ntry {\n    DISCORD_LOG(LS_INFO) << \"Processing commit\";\n    DISCORD_LOG(LS_INFO) << \"Commit: \" << ::mlspp::bytes_ns::bytes(commit);\n\n    auto commitMessage = ::mlspp::tls::get<::mlspp::MLSMessage>(commit);\n\n    if (!CanProcessCommit(commitMessage)) {\n        DISCORD_LOG(LS_ERROR) << \"ProcessCommit called with unprocessable MLS commit\";\n        return ignored_t{};\n    }\n\n    // in case we're the sender of this commit\n    // we need to pull the cached state from our outbound cache\n    std::optional<::mlspp::State> optionalCachedState = std::nullopt;\n    if (outboundCachedGroupState_) {\n        optionalCachedState = *(outboundCachedGroupState_.get());\n    }\n\n    auto validatedMessage = stateWithProposals_->unwrap(commitMessage);\n    const auto& authenticatedContent = validatedMessage.authenticated_content();\n\n    if (authenticatedContent.wire_format != ::mlspp::WireFormat::mls_public_message) {\n        throw std::invalid_argument(\"Invalid commit wire format\");\n    }\n\n    if (authenticatedContent.content.epoch != stateWithProposals_->epoch()) {\n        throw std::invalid_argument(\"Commit epoch mismatch\");\n    }\n\n    if (authenticatedContent.content.content_type() != ::mlspp::ContentType::commit) {\n        throw std::invalid_argument(\"Unexpected message type\");\n    }\n\n    if (authenticatedContent.content.sender.sender_type() != ::mlspp::SenderType::member) {\n        throw std::invalid_argument(\"Unexpected commit sender type\");\n    }\n\n    const auto& commitContent =\n      ::mlspp::tls::var::get<::mlspp::Commit>(authenticatedContent.content.content);\n\n    for (const auto& proposalOrRef : commitContent.proposals) {\n        if (::mlspp::tls::variant<::mlspp::ProposalOrRefType>::type(proposalOrRef.content) !=\n            ::mlspp::ProposalOrRefType::reference) {\n            throw std::invalid_argument(\"Unexpected non-ref proposal\");\n        }\n    }\n\n    auto newState = stateWithProposals_->handle(validatedMessage, optionalCachedState);\n\n    if (!newState) {\n        DISCORD_LOG(LS_ERROR) << \"MLS commit handling did not produce a new state\";\n        return failed_t{};\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Successfully processed MLS commit, updating state; our leaf index is \"\n                         << newState->index().val << \"; current epoch is \" << newState->epoch();\n\n    RosterMap ret = ReplaceState(std::make_unique<::mlspp::State>(std::move(*newState)));\n\n    // reset the outbound cached group since we handled the commit for this epoch\n    outboundCachedGroupState_.reset();\n\n    ClearPendingState();\n\n    return ret;\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to process MLS commit: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return failed_t{};\n}\n\nstd::optional<RosterMap> Session::ProcessWelcome(\n  std::vector<uint8_t> welcome,\n  std::set<std::string> const& recognizedUserIDs) noexcept\ntry {\n    if (!HasCryptographicStateForWelcome()) {\n        DISCORD_LOG(LS_ERROR) << \"Missing local cyrpto state necessary to process MLS welcome\";\n        return std::nullopt;\n    }\n\n    if (!externalSender_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot process MLS welcome without an external sender\";\n        return std::nullopt;\n    }\n\n    if (currentState_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot process MLS welcome after joining/creating an MLS group\";\n        return std::nullopt;\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Processing welcome: \" << ::mlspp::bytes_ns::bytes(welcome);\n\n    // unmarshal the incoming welcome\n    auto unmarshalledWelcome = ::mlspp::tls::get<::mlspp::Welcome>(welcome);\n\n    // construct the state from the unmarshalled welcome\n    auto newState = std::make_unique<::mlspp::State>(\n      *joinInitPrivateKey_,\n      *selfHPKEPrivateKey_,\n      *selfSigPrivateKey_,\n      *joinKeyPackage_,\n      unmarshalledWelcome,\n      std::nullopt,\n      std::map<::mlspp::bytes_ns::bytes, ::mlspp::bytes_ns::bytes>());\n\n    // perform application-level verification of the new state\n    if (!VerifyWelcomeState(*newState, recognizedUserIDs)) {\n        DISCORD_LOG(LS_ERROR) << \"Group received in MLS welcome is not valid\";\n\n        return std::nullopt;\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Successfully welcomed to MLS Group, our leaf index is \"\n                         << newState->index().val << \"; current epoch is \" << newState->epoch();\n\n    // make the verified state our new (and only) state\n    RosterMap ret = ReplaceState(std::move(newState));\n\n    // clear out any pending state for creating/joining a group\n    ClearPendingState();\n\n    return ret;\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to create group state from MLS welcome: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return std::nullopt;\n}\n\nRosterMap Session::ReplaceState(std::unique_ptr<::mlspp::State>&& state)\n{\n    RosterMap newRoster;\n    for (const ::mlspp::LeafNode& node : state->roster()) {\n        if (node.credential.type() != ::mlspp::CredentialType::basic) {\n            throw std::invalid_argument(\"Unexpected credential type in roster\");\n        }\n\n        const auto& cred = node.credential.template get<::mlspp::BasicCredential>();\n\n        if (newRoster.emplace(FromBigEndianBytes(cred.identity), node.signature_key.data.as_vec())\n              .second != true) {\n            throw std::invalid_argument(\"Duplicate identity in roster\");\n        }\n    }\n\n    RosterMap changeMap;\n\n    std::set_difference(newRoster.begin(),\n                        newRoster.end(),\n                        roster_.begin(),\n                        roster_.end(),\n                        std::inserter(changeMap, changeMap.end()));\n\n    struct MissingItemWrapper {\n        RosterMap& changeMap_;\n\n        using iterator = RosterMap::iterator;\n        using const_iterator = RosterMap::const_iterator;\n        using value_type = RosterMap::value_type;\n\n        iterator insert(const_iterator it, const value_type& value)\n        {\n            return changeMap_.try_emplace(it, value.first, std::vector<uint8_t>{});\n        }\n\n        iterator begin() { return changeMap_.begin(); }\n\n        iterator end() { return changeMap_.end(); }\n    };\n\n    MissingItemWrapper wrapper{changeMap};\n\n    std::set_difference(roster_.begin(),\n                        roster_.end(),\n                        newRoster.begin(),\n                        newRoster.end(),\n                        std::inserter(wrapper, wrapper.end()));\n\n    roster_ = std::move(newRoster);\n    currentState_ = std::move(state);\n\n    return changeMap;\n}\n\nbool Session::HasCryptographicStateForWelcome() const noexcept\n{\n    return joinKeyPackage_ && joinInitPrivateKey_ && selfSigPrivateKey_ && selfHPKEPrivateKey_;\n}\n\nbool Session::VerifyWelcomeState(::mlspp::State const& state,\n                                 std::set<std::string> const& recognizedUserIDs) const\n{\n    if (!externalSender_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot verify MLS welcome without an external sender\";\n        TRACK_MLS_ERROR(\"Missing external sender when processing Welcome\");\n        return false;\n    }\n\n    auto ext = state.extensions().template find<mlspp::ExternalSendersExtension>();\n    if (!ext) {\n        DISCORD_LOG(LS_ERROR) << \"MLS welcome missing external senders extension\";\n        TRACK_MLS_ERROR(\"Welcome message missing external sender extension\");\n        return false;\n    }\n\n    if (ext->senders.size() != 1) {\n        DISCORD_LOG(LS_ERROR) << \"MLS welcome lists unexpected number of external senders: \"\n                              << ext->senders.size();\n        TRACK_MLS_ERROR(\"Welcome message lists unexpected external sender count\");\n        return false;\n    }\n\n    if (ext->senders.front() != *externalSender_) {\n        DISCORD_LOG(LS_ERROR) << \"MLS welcome lists unexpected external sender\";\n        TRACK_MLS_ERROR(\"Welcome message lists unexpected external sender\");\n        return false;\n    }\n\n    // TODO: Until we leverage revocation in the protocol\n    // if we re-enable this change we will refuse welcome messages\n    // because someone was previously supposed to be added but disconnected\n    // before all in-flight proposals were handled.\n\n    for (const auto& leaf : state.roster()) {\n        if (!IsRecognizedUserID(leaf.credential, recognizedUserIDs)) {\n            DISCORD_LOG(LS_ERROR) << \"MLS welcome lists unrecognized user ID\";\n            // TRACK_MLS_ERROR(\"Welcome message lists unrecognized user ID\");\n            // return false;\n        }\n    }\n\n    return true;\n}\n\nvoid Session::InitLeafNode(std::string const& selfUserId,\n                           std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey) noexcept\ntry {\n    auto ciphersuite = CiphersuiteForProtocolVersion(protocolVersion_);\n\n    if (!transientKey) {\n        if (!signingKeyId_.empty()) {\n            transientKey = GetPersistedKeyPair(keyPairContext_, signingKeyId_, protocolVersion_);\n            if (!transientKey) {\n                DISCORD_LOG(LS_ERROR) << \"Did not receive MLS signature private key from \"\n                                         \"GetPersistedKeyPair; aborting\";\n                return;\n            }\n        }\n        else {\n            transientKey = std::make_shared<::mlspp::SignaturePrivateKey>(\n              ::mlspp::SignaturePrivateKey::generate(ciphersuite));\n        }\n    }\n\n    selfSigPrivateKey_ = transientKey;\n\n    auto selfCredential = CreateUserCredential(selfUserId, protocolVersion_);\n\n    selfHPKEPrivateKey_ =\n      std::make_unique<::mlspp::HPKEPrivateKey>(::mlspp::HPKEPrivateKey::generate(ciphersuite));\n\n    selfLeafNode_ =\n      std::make_unique<::mlspp::LeafNode>(ciphersuite,\n                                          selfHPKEPrivateKey_->public_key,\n                                          selfSigPrivateKey_->public_key,\n                                          std::move(selfCredential),\n                                          LeafNodeCapabilitiesForProtocolVersion(protocolVersion_),\n                                          ::mlspp::Lifetime::create_default(),\n                                          LeafNodeExtensionsForProtocolVersion(protocolVersion_),\n                                          *selfSigPrivateKey_);\n\n    DISCORD_LOG(LS_INFO) << \"Created MLS leaf node\";\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_INFO) << \"Failed to initialize MLS leaf node: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n}\n\nvoid Session::ResetJoinKeyPackage() noexcept\ntry {\n    if (!selfLeafNode_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot initialize join key package without a leaf node\";\n        return;\n    }\n\n    auto ciphersuite = CiphersuiteForProtocolVersion(protocolVersion_);\n\n    joinInitPrivateKey_ =\n      std::make_unique<::mlspp::HPKEPrivateKey>(::mlspp::HPKEPrivateKey::generate(ciphersuite));\n\n    joinKeyPackage_ =\n      std::make_unique<::mlspp::KeyPackage>(ciphersuite,\n                                            joinInitPrivateKey_->public_key,\n                                            *selfLeafNode_,\n                                            LeafNodeExtensionsForProtocolVersion(protocolVersion_),\n                                            *selfSigPrivateKey_);\n\n    DISCORD_LOG(LS_INFO) << \"Generated key package: \"\n                         << ::mlspp::bytes_ns::bytes(::mlspp::tls::marshal(*joinKeyPackage_));\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to initialize join key package: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n}\n\nvoid Session::CreatePendingGroup() noexcept\ntry {\n    if (groupId_.empty()) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot create MLS group without a group ID\";\n        return;\n    }\n\n    if (!externalSender_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot create MLS group without ExternalSender\";\n        return;\n    }\n\n    if (!selfLeafNode_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot create MLS group without self leaf node\";\n        return;\n    }\n\n    DISCORD_LOG(LS_INFO) << \"Creating a pending MLS group\";\n\n    auto ciphersuite = CiphersuiteForProtocolVersion(protocolVersion_);\n\n    pendingGroupState_ = std::make_unique<::mlspp::State>(\n      groupId_,\n      ciphersuite,\n      *selfHPKEPrivateKey_,\n      *selfSigPrivateKey_,\n      *selfLeafNode_,\n      GroupExtensionsForProtocolVersion(protocolVersion_, *externalSender_));\n\n    DISCORD_LOG(LS_INFO) << \"Created a pending MLS group\";\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to create MLS group: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return;\n}\n\nstd::vector<uint8_t> Session::GetMarshalledKeyPackage() noexcept\ntry {\n    // key packages are not meant to be re-used\n    // so every time the client asks for a key package we create a new one\n    ResetJoinKeyPackage();\n\n    if (!joinKeyPackage_) {\n        DISCORD_LOG(LS_ERROR) << \"Cannot marshal an uninitialized key package\";\n        return {};\n    }\n\n    return ::mlspp::tls::marshal(*joinKeyPackage_);\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to marshal join key package: \" << e.what();\n    TRACK_MLS_ERROR(e.what());\n    return {};\n}\n\nstd::unique_ptr<IKeyRatchet> Session::GetKeyRatchet(std::string const& userId) const noexcept\n{\n    if (!currentState_) {\n        DISCORD_LOG(LS_INFO) << \"Cannot get key ratchet without an established MLS group\";\n        return nullptr;\n    }\n\n    // change the string user ID to a little endian 64 bit user ID\n    auto u64userId = strtoull(userId.c_str(), nullptr, 10);\n    auto userIdBytes = ::mlspp::bytes_ns::bytes(sizeof(u64userId));\n    memcpy(userIdBytes.data(), &u64userId, sizeof(u64userId));\n\n    // generate the base secret for the hash ratchet\n    auto baseSecret =\n      currentState_->do_export(Session::USER_MEDIA_KEY_BASE_LABEL, userIdBytes, kAesGcm128KeyBytes);\n\n    // this assumes the MLS ciphersuite produces a kAesGcm128KeyBytes sized key\n    // would need to be updated to a different ciphersuite if there's a future mismatch\n    return std::make_unique<MlsKeyRatchet>(currentState_->cipher_suite(), std::move(baseSecret));\n}\n\nvoid Session::GetPairwiseFingerprint(uint16_t version,\n                                     std::string const& userId,\n                                     PairwiseFingerprintCallback callback) const noexcept\ntry {\n    if (!currentState_ || !selfSigPrivateKey_) {\n        throw std::invalid_argument(\"No established MLS group\");\n    }\n\n    uint64_t u64RemoteUserId = strtoull(userId.c_str(), nullptr, 10);\n    uint64_t u64SelfUserId = strtoull(selfUserId_.c_str(), nullptr, 10);\n\n    auto it = roster_.find(u64RemoteUserId);\n    if (it == roster_.end()) {\n        throw std::invalid_argument(\"Unknown user ID: \" + userId);\n    }\n\n    ::mlspp::tls::ostream toHash1;\n    ::mlspp::tls::ostream toHash2;\n\n    toHash1 << version;\n    toHash1.write_raw(it->second);\n    toHash1 << u64RemoteUserId;\n\n    toHash2 << version;\n    toHash2.write_raw(selfSigPrivateKey_->public_key.data);\n    toHash2 << u64SelfUserId;\n\n    std::vector<std::vector<uint8_t>> keyData = {\n      toHash1.bytes(),\n      toHash2.bytes(),\n    };\n\n    std::sort(keyData.begin(), keyData.end());\n\n    std::thread([callback = std::move(callback),\n                 data = ::mlspp::bytes_ns::bytes(std::move(keyData[0])) + keyData[1]] {\n        static constexpr uint8_t salt[] = {\n          0x24,\n          0xca,\n          0xb1,\n          0x7a,\n          0x7a,\n          0xf8,\n          0xec,\n          0x2b,\n          0x82,\n          0xb4,\n          0x12,\n          0xb9,\n          0x2d,\n          0xab,\n          0x19,\n          0x2e,\n        };\n\n        constexpr uint64_t N = 16384, r = 8, p = 2, max_mem = 32 * 1024 * 1024;\n        constexpr size_t hash_len = 64;\n\n        std::vector<uint8_t> out(hash_len);\n\n        int ret = EVP_PBE_scrypt((const char*)data.data(),\n                                 data.size(),\n                                 salt,\n                                 sizeof(salt),\n                                 N,\n                                 r,\n                                 p,\n                                 max_mem,\n                                 out.data(),\n                                 out.size());\n\n        if (ret == 1) {\n            callback(out);\n        }\n        else {\n            callback({});\n        }\n    }).detach();\n}\ncatch (const std::exception& e) {\n    DISCORD_LOG(LS_ERROR) << \"Failed to generate pairwise fingerprint: \" << e.what();\n    callback({});\n}\n\nvoid Session::ClearPendingState()\n{\n    pendingGroupState_.reset();\n    pendingGroupCommit_.reset();\n\n    joinInitPrivateKey_.reset();\n    joinKeyPackage_.reset();\n\n    selfHPKEPrivateKey_.reset();\n\n    selfLeafNode_.reset();\n\n    stateWithProposals_.reset();\n    proposalQueue_.clear();\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/session.h",
    "content": "#pragma once\n\n#include <functional>\n#include <list>\n#include <memory>\n#include <optional>\n#include <set>\n#include <string>\n#include <vector>\n\n#include <dave/dave_interfaces.h>\n#include <dave/version.h>\n\n#include \"mls/persisted_key_pair.h\"\n#include \"mls_key_ratchet.h\"\n\nnamespace mlspp {\nstruct AuthenticatedContent;\nstruct Credential;\nstruct ExternalSender;\nstruct HPKEPrivateKey;\nstruct KeyPackage;\nstruct LeafNode;\nstruct MLSMessage;\nstruct SignaturePrivateKey;\nclass State;\n} // namespace mlspp\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\nstruct QueuedProposal;\n\nclass Session final : public ISession {\npublic:\n    Session(KeyPairContextType context,\n            std::string authSessionId,\n            MLSFailureCallback callback) noexcept;\n\n    virtual ~Session() noexcept;\n\n    virtual void Init(\n      ProtocolVersion version,\n      uint64_t groupId,\n      std::string const& selfUserId,\n      std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey) noexcept override;\n    virtual void Reset() noexcept override;\n\n    virtual void SetProtocolVersion(ProtocolVersion version) noexcept override;\n    virtual ProtocolVersion GetProtocolVersion() const noexcept override\n    {\n        return protocolVersion_;\n    }\n\n    virtual std::vector<uint8_t> GetLastEpochAuthenticator() const noexcept override;\n\n    virtual void SetExternalSender(\n      std::vector<uint8_t> const& externalSenderPackage) noexcept override;\n\n    virtual std::optional<std::vector<uint8_t>> ProcessProposals(\n      std::vector<uint8_t> proposals,\n      std::set<std::string> const& recognizedUserIDs) noexcept override;\n\n    virtual RosterVariant ProcessCommit(std::vector<uint8_t> commit) noexcept override;\n\n    virtual std::optional<RosterMap> ProcessWelcome(\n      std::vector<uint8_t> welcome,\n      std::set<std::string> const& recognizedUserIDs) noexcept override;\n\n    virtual std::vector<uint8_t> GetMarshalledKeyPackage() noexcept override;\n\n    virtual std::unique_ptr<IKeyRatchet> GetKeyRatchet(\n      std::string const& userId) const noexcept override;\n\n    using PairwiseFingerprintCallback = std::function<void(std::vector<uint8_t> const&)>;\n\n    virtual void GetPairwiseFingerprint(\n      uint16_t version,\n      std::string const& userId,\n      PairwiseFingerprintCallback callback) const noexcept override;\n\nprivate:\n    void InitLeafNode(std::string const& selfUserId,\n                      std::shared_ptr<::mlspp::SignaturePrivateKey>& transientKey) noexcept;\n    void ResetJoinKeyPackage() noexcept;\n\n    void CreatePendingGroup() noexcept;\n\n    bool HasCryptographicStateForWelcome() const noexcept;\n\n    bool IsRecognizedUserID(const ::mlspp::Credential& cred,\n                            std::set<std::string> const& recognizedUserIDs) const;\n    bool ValidateProposalMessage(::mlspp::AuthenticatedContent const& message,\n                                 ::mlspp::State const& targetState,\n                                 std::set<std::string> const& recognizedUserIDs) const;\n    bool VerifyWelcomeState(::mlspp::State const& state,\n                            std::set<std::string> const& recognizedUserIDs) const;\n\n    bool CanProcessCommit(const ::mlspp::MLSMessage& commit) noexcept;\n\n    RosterMap ReplaceState(std::unique_ptr<::mlspp::State>&& state);\n\n    void ClearPendingState();\n\n    inline static const std::string USER_MEDIA_KEY_BASE_LABEL = \"Discord Secure Frames v0\";\n\n    ProtocolVersion protocolVersion_;\n    std::vector<uint8_t> groupId_;\n    std::string signingKeyId_;\n    std::string selfUserId_;\n    KeyPairContextType keyPairContext_{nullptr};\n\n    std::unique_ptr<::mlspp::LeafNode> selfLeafNode_;\n    std::shared_ptr<::mlspp::SignaturePrivateKey> selfSigPrivateKey_;\n    std::unique_ptr<::mlspp::HPKEPrivateKey> selfHPKEPrivateKey_;\n\n    std::unique_ptr<::mlspp::HPKEPrivateKey> joinInitPrivateKey_;\n    std::unique_ptr<::mlspp::KeyPackage> joinKeyPackage_;\n\n    std::unique_ptr<::mlspp::ExternalSender> externalSender_;\n\n    std::unique_ptr<::mlspp::State> pendingGroupState_;\n    std::unique_ptr<::mlspp::MLSMessage> pendingGroupCommit_;\n\n    std::unique_ptr<::mlspp::State> outboundCachedGroupState_;\n\n    std::unique_ptr<::mlspp::State> currentState_;\n    RosterMap roster_;\n\n    std::unique_ptr<::mlspp::State> stateWithProposals_;\n    std::list<QueuedProposal> proposalQueue_;\n\n    MLSFailureCallback onMLSFailureCallback_{};\n};\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/user_credential.cpp",
    "content": "#include \"user_credential.h\"\n\n#include <string>\n\n#include \"mls/util.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::Credential CreateUserCredential(const std::string& userId,\n                                         [[maybe_unused]] ProtocolVersion version)\n{\n    // convert the string user ID to a big endian uint64_t\n    auto userID = std::stoull(userId);\n    auto credentialBytes = BigEndianBytesFrom(userID);\n\n    return ::mlspp::Credential::basic(credentialBytes);\n}\n\nstd::string UserCredentialToString(const ::mlspp::Credential& cred,\n                                   [[maybe_unused]] ProtocolVersion version)\n{\n    if (cred.type() != ::mlspp::CredentialType::basic) {\n        return \"\";\n    }\n\n    const auto& basic = cred.template get<::mlspp::BasicCredential>();\n\n    auto uidVal = FromBigEndianBytes(basic.identity);\n\n    return std::to_string(uidVal);\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/user_credential.h",
    "content": "#pragma once\n\n#include <string>\n\n#include <mls/credential.h>\n\n#include <dave/version.h>\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::Credential CreateUserCredential(const std::string& userId, ProtocolVersion version);\nstd::string UserCredentialToString(const ::mlspp::Credential& cred, ProtocolVersion version);\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/util.cpp",
    "content": "#include \"util.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::bytes_ns::bytes BigEndianBytesFrom(uint64_t value) noexcept\n{\n    auto buffer = ::mlspp::bytes_ns::bytes();\n    buffer.reserve(sizeof(value));\n\n    for (int i = sizeof(value) - 1; i >= 0; --i) {\n        buffer.push_back(static_cast<uint8_t>(value >> (i * 8)));\n    }\n\n    return buffer;\n}\n\nuint64_t FromBigEndianBytes(const ::mlspp::bytes_ns::bytes& buffer) noexcept\n{\n    uint64_t val = 0;\n\n    if (buffer.size() <= sizeof(val)) {\n        for (uint8_t byte : buffer) {\n            val = (val << 8) | byte;\n        }\n    }\n\n    return val;\n}\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls/util.h",
    "content": "#pragma once\n\n#include <string>\n\n#include <bytes/bytes.h>\n\nnamespace discord {\nnamespace dave {\nnamespace mls {\n\n::mlspp::bytes_ns::bytes BigEndianBytesFrom(uint64_t value) noexcept;\nuint64_t FromBigEndianBytes(const ::mlspp::bytes_ns::bytes& value) noexcept;\n\n} // namespace mls\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls_key_ratchet.cpp",
    "content": "#include \"mls_key_ratchet.h\"\n\n#include <cassert>\n\n#include <dave/logger.h>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\n\nMlsKeyRatchet::MlsKeyRatchet(::mlspp::CipherSuite suite, bytes baseSecret) noexcept\n  : hashRatchet_(suite, std::move(baseSecret))\n{\n}\n\nMlsKeyRatchet::~MlsKeyRatchet() noexcept = default;\n\nEncryptionKey MlsKeyRatchet::GetKey(KeyGeneration generation) noexcept\n{\n    DISCORD_LOG(LS_INFO) << \"Retrieving key for generation \" << generation << \" from HashRatchet\";\n\n    try {\n        auto keyAndNonce = hashRatchet_.get(generation);\n        assert(keyAndNonce.key.size() >= kAesGcm128KeyBytes);\n        return std::move(keyAndNonce.key.as_vec());\n    }\n    catch (const std::exception& e) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to retrieve key for generation \" << generation << \": \"\n                              << e.what();\n        return {};\n    }\n}\n\nvoid MlsKeyRatchet::DeleteKey(KeyGeneration generation) noexcept\n{\n    hashRatchet_.erase(generation);\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/mls_key_ratchet.h",
    "content": "#pragma once\n\n#include <bytes/bytes.h>\n#include <mls/key_schedule.h>\n\n#include <dave/dave_interfaces.h>\n\nnamespace discord {\nnamespace dave {\n\nclass MlsKeyRatchet : public IKeyRatchet {\npublic:\n    MlsKeyRatchet(::mlspp::CipherSuite suite, bytes baseSecret) noexcept;\n    ~MlsKeyRatchet() noexcept override;\n\n    EncryptionKey GetKey(KeyGeneration generation) noexcept override;\n    void DeleteKey(KeyGeneration generation) noexcept override;\n\n    const ::mlspp::HashRatchet& GetHashRatchet() const noexcept { return hashRatchet_; }\n\nprivate:\n    ::mlspp::HashRatchet hashRatchet_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/openssl_cryptor.cpp",
    "content": "#include \"openssl_cryptor.h\"\n\n#include <openssl/err.h>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\n\nvoid PrintSSLErrors()\n{\n    ERR_print_errors_cb(\n      [](const char* str, size_t len, [[maybe_unused]] void* ctx) {\n          DISCORD_LOG(LS_ERROR) << std::string(str, len);\n          return 1;\n      },\n      nullptr);\n}\n\nOpenSSLCryptor::OpenSSLCryptor(const EncryptionKey& encryptionKey)\n{\n    if (!cipherCtx_) {\n        cipherCtx_ = EVP_CIPHER_CTX_new();\n    }\n    else {\n        EVP_CIPHER_CTX_reset(cipherCtx_);\n    }\n\n    auto initResult =\n      EVP_CipherInit_ex(cipherCtx_, EVP_aes_128_gcm(), nullptr, encryptionKey.data(), nullptr, 0);\n\n    if (initResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to initialize AEAD context\";\n        PrintSSLErrors();\n    }\n}\n\nOpenSSLCryptor::~OpenSSLCryptor()\n{\n    EVP_CIPHER_CTX_free(cipherCtx_);\n}\n\nbool OpenSSLCryptor::Encrypt(ArrayView<uint8_t> ciphertextBufferOut,\n                             ArrayView<const uint8_t> plaintextBuffer,\n                             ArrayView<const uint8_t> nonceBuffer,\n                             ArrayView<const uint8_t> additionalData,\n                             ArrayView<uint8_t> tagBufferOut)\n{\n    if (!cipherCtx_) {\n        DISCORD_LOG(LS_ERROR) << \"Encrypt: AEAD context is not initialized\";\n        return false;\n    }\n\n    auto contextResult =\n      EVP_EncryptInit_ex(cipherCtx_, nullptr, nullptr, nullptr, nonceBuffer.data());\n    if (contextResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to set nonce for encryption\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    int ciphertextOutSize = 0;\n\n    if (additionalData.size() > 0) {\n        if (additionalData.size() > std::numeric_limits<int>::max()) {\n            DISCORD_LOG(LS_ERROR) << \"Additional data size exceeds the maximum supported size\";\n            return false;\n        }\n\n        auto aadResult = EVP_EncryptUpdate(cipherCtx_,\n                                           nullptr,\n                                           &ciphertextOutSize,\n                                           additionalData.data(),\n                                           static_cast<int>(additionalData.size()));\n\n        if (aadResult != 1) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to update encryption with additional data\";\n            PrintSSLErrors();\n            return false;\n        }\n    }\n\n    if (plaintextBuffer.size() > std::numeric_limits<int>::max()) {\n        DISCORD_LOG(LS_ERROR) << \"Plaintext buffer size exceeds the maximum supported size\";\n        return false;\n    }\n\n    auto updateResult = EVP_EncryptUpdate(cipherCtx_,\n                                          ciphertextBufferOut.data(),\n                                          &ciphertextOutSize,\n                                          plaintextBuffer.data(),\n                                          static_cast<int>(plaintextBuffer.size()));\n\n    if (updateResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to encrypt plaintext\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    auto finalizeResult =\n      EVP_EncryptFinal_ex(cipherCtx_, ciphertextBufferOut.data(), &ciphertextOutSize);\n    if (finalizeResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to finalize encryption\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    auto tagResult = EVP_CIPHER_CTX_ctrl(\n      cipherCtx_, EVP_CTRL_GCM_GET_TAG, kAesGcm128TruncatedTagBytes, tagBufferOut.data());\n    if (tagResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to get truncated authentication tag\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    return true;\n}\n\nbool OpenSSLCryptor::Decrypt(ArrayView<uint8_t> plaintextBufferOut,\n                             ArrayView<const uint8_t> ciphertextBuffer,\n                             ArrayView<const uint8_t> tagBuffer,\n                             ArrayView<const uint8_t> nonceBuffer,\n                             ArrayView<const uint8_t> additionalData)\n{\n    if (!cipherCtx_) {\n        DISCORD_LOG(LS_ERROR) << \"Decrypt: AEAD context is not initialized\";\n        return false;\n    }\n\n    auto contextResult =\n      EVP_DecryptInit_ex(cipherCtx_, nullptr, nullptr, nullptr, nonceBuffer.data());\n    if (contextResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to set nonce for decryption\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    int plaintextOutSize = 0;\n\n    if (additionalData.size() > 0) {\n        if (additionalData.size() > std::numeric_limits<int>::max()) {\n            DISCORD_LOG(LS_ERROR) << \"Additional data size exceeds the maximum supported size\";\n            return false;\n        }\n\n        auto aadResult = EVP_DecryptUpdate(cipherCtx_,\n                                           nullptr,\n                                           &plaintextOutSize,\n                                           additionalData.data(),\n                                           static_cast<int>(additionalData.size()));\n\n        if (aadResult != 1) {\n            DISCORD_LOG(LS_ERROR) << \"Failed to update decryption with additional data\";\n            PrintSSLErrors();\n            return false;\n        }\n    }\n\n    if (ciphertextBuffer.size() > std::numeric_limits<int>::max()) {\n        DISCORD_LOG(LS_ERROR) << \"Ciphertext buffer size exceeds the maximum supported size\";\n        return false;\n    }\n\n    auto updateResult = EVP_DecryptUpdate(cipherCtx_,\n                                          plaintextBufferOut.data(),\n                                          &plaintextOutSize,\n                                          ciphertextBuffer.data(),\n                                          static_cast<int>(ciphertextBuffer.size()));\n    if (updateResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to decrypt ciphertext\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    // make a copy of the tag since the interface expects a const tag for decryption\n    std::vector<uint8_t> tagBufferCopy(tagBuffer.begin(), tagBuffer.end());\n\n    auto tagResult = EVP_CIPHER_CTX_ctrl(\n      cipherCtx_, EVP_CTRL_GCM_SET_TAG, kAesGcm128TruncatedTagBytes, tagBufferCopy.data());\n    if (tagResult != 1) {\n        DISCORD_LOG(LS_ERROR)\n          << \"Failed to set expected truncated authentication tag for decryption\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    auto finalizeResult =\n      EVP_DecryptFinal_ex(cipherCtx_, plaintextBufferOut.data(), &plaintextOutSize);\n    if (finalizeResult != 1) {\n        DISCORD_LOG(LS_ERROR) << \"Failed to finalize decryption\";\n        PrintSSLErrors();\n        return false;\n    }\n\n    return true;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/openssl_cryptor.h",
    "content": "#pragma once\n\n#include <openssl/evp.h>\n\n#include \"cryptor.h\"\n\nnamespace discord {\nnamespace dave {\n\nclass OpenSSLCryptor : public ICryptor {\npublic:\n    OpenSSLCryptor(const EncryptionKey& encryptionKey);\n    ~OpenSSLCryptor();\n\n    bool IsValid() const { return cipherCtx_ != nullptr; }\n\n    bool Encrypt(ArrayView<uint8_t> ciphertextBufferOut,\n                 ArrayView<const uint8_t> plaintextBuffer,\n                 ArrayView<const uint8_t> nonceBuffer,\n                 ArrayView<const uint8_t> additionalData,\n                 ArrayView<uint8_t> tagBufferOut) override;\n    bool Decrypt(ArrayView<uint8_t> plaintextBufferOut,\n                 ArrayView<const uint8_t> ciphertextBuffer,\n                 ArrayView<const uint8_t> tagBuffer,\n                 ArrayView<const uint8_t> nonceBuffer,\n                 ArrayView<const uint8_t> additionalData) override;\n\nprivate:\n    EVP_CIPHER_CTX* cipherCtx_ = nullptr;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/utils/clock.h",
    "content": "#pragma once\n\n#include <chrono>\n\nnamespace discord {\nnamespace dave {\n\nclass IClock {\npublic:\n    using BaseClock = std::chrono::steady_clock;\n    using TimePoint = BaseClock::time_point;\n    using Duration = BaseClock::duration;\n\n    virtual ~IClock() = default;\n    virtual TimePoint Now() const = 0;\n};\n\nclass Clock : public IClock {\npublic:\n    TimePoint Now() const override { return BaseClock::now(); }\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/utils/leb128.cpp",
    "content": "\n#include \"leb128.h\"\n\n// The following code was copied from the webrtc source code:\n// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/rtp_rtcp/source/leb128.cc\n\nnamespace discord {\nnamespace dave {\n\nsize_t Leb128Size(uint64_t value)\n{\n    int size = 0;\n    while (value >= 0x80) {\n        ++size;\n        value >>= 7;\n    }\n    return size + 1;\n}\n\nuint64_t ReadLeb128(const uint8_t*& readAt, const uint8_t* end)\n{\n    uint64_t value = 0;\n    int fillBits = 0;\n    while (readAt != end && fillBits < 64 - 7) {\n        uint8_t leb128Byte = *readAt;\n        value |= uint64_t{leb128Byte & 0x7Fu} << fillBits;\n        ++readAt;\n        fillBits += 7;\n        if ((leb128Byte & 0x80) == 0) {\n            return value;\n        }\n    }\n    // Read 9 bytes and didn't find the terminator byte. Check if 10th byte\n    // is that terminator, however to fit result into uint64_t it may carry only\n    // single bit.\n    if (readAt != end && *readAt <= 1) {\n        value |= uint64_t{*readAt} << fillBits;\n        ++readAt;\n        return value;\n    }\n    // Failed to find terminator leb128 byte.\n    readAt = nullptr;\n    return 0;\n}\n\nsize_t WriteLeb128(uint64_t value, uint8_t* buffer)\n{\n    int size = 0;\n    while (value >= 0x80) {\n        buffer[size] = 0x80 | (value & 0x7F);\n        ++size;\n        value >>= 7;\n    }\n    buffer[size] = static_cast<uint8_t>(value);\n    ++size;\n    return size;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/utils/leb128.h",
    "content": "#pragma once\n\n#include <cstddef>\n#include <cstdint>\n\nnamespace discord {\nnamespace dave {\n\nconstexpr size_t Leb128MaxSize = 10;\n\n// Returns number of bytes needed to store `value` in leb128 format.\nsize_t Leb128Size(uint64_t value);\n\n// Reads leb128 encoded value and advance read_at by number of bytes consumed.\n// Sets read_at to nullptr on error.\nuint64_t ReadLeb128(const uint8_t*& readAt, const uint8_t* end);\n\n// Encodes `value` in leb128 format. Assumes buffer has size of at least\n// Leb128Size(value). Returns number of bytes consumed.\nsize_t WriteLeb128(uint64_t value, uint8_t* buffer);\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/utils/scope_exit.h",
    "content": "#pragma once\n\n#include <algorithm>\n#include <functional>\n#include <utility>\n\nnamespace discord {\nnamespace dave {\n\nclass [[nodiscard]] ScopeExit final {\npublic:\n    template <typename Cleanup>\n    explicit ScopeExit(Cleanup&& cleanup)\n      : cleanup_{std::forward<Cleanup>(cleanup)}\n    {\n    }\n\n    ScopeExit(ScopeExit&& rhs)\n      : cleanup_{std::move(rhs.cleanup_)}\n    {\n        rhs.cleanup_ = nullptr;\n    }\n\n    ~ScopeExit()\n    {\n        if (cleanup_) {\n            cleanup_();\n        }\n    }\n\n    ScopeExit& operator=(ScopeExit&& rhs)\n    {\n        cleanup_ = std::move(rhs.cleanup_);\n        rhs.cleanup_ = nullptr;\n        return *this;\n    }\n\n    void Dismiss() { cleanup_ = nullptr; }\n\nprivate:\n    ScopeExit(ScopeExit const&) = delete;\n    ScopeExit& operator=(ScopeExit const&) = delete;\n\n    std::function<void()> cleanup_;\n};\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/src/version.cpp",
    "content": "#include <dave/version.h>\n\nnamespace discord {\nnamespace dave {\n\nconstexpr ProtocolVersion CurrentDaveProtocolVersion = 1;\n\nProtocolVersion MaxSupportedProtocolVersion()\n{\n    return CurrentDaveProtocolVersion;\n}\n\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/CMakeLists.txt",
    "content": "enable_testing()\n\nfind_package(GTest CONFIG REQUIRED)\n\nSET(TEST_APP_NAME \"libdave_test\")\n\nfile(GLOB_RECURSE TEST_HEADERS CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/*.h\")\nfile(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/*.cpp\")\n\nadd_executable(${TEST_APP_NAME} ${TEST_HEADERS} ${TEST_SOURCES})\nadd_dependencies(${TEST_APP_NAME} ${LIB_NAME})\ntarget_include_directories(${TEST_APP_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src)\n\ntarget_link_libraries(libdave_test PRIVATE ${LIB_NAME} GTest::gtest_main GTest::gmock MLSPP::bytes MLSPP::mlspp)\n\nadd_test(NAME ${TEST_APP_NAME} COMMAND ${TEST_APP_NAME})\n\nif(WIN32 AND BUILD_SHARED_LIBS)\n    add_custom_command(TARGET ${TEST_APP_NAME} POST_BUILD\n        COMMAND ${CMAKE_COMMAND} -E copy_if_different\n            $<TARGET_FILE:${LIB_NAME}>\n            $<TARGET_FILE_DIR:${TEST_APP_NAME}>\n        COMMENT \"Copying ${LIB_NAME}.dll to test directory\"\n    )\nendif()\n\nif(WIN32 AND ENABLE_SANITIZERS)\n    if (NOT EXISTS \"${ASAN_RUNTIME_DLL}\")\n        message(FATAL_ERROR \"ASAN runtime DLL not found at ${ASAN_RUNTIME_DLL}\")\n    endif()\n\n    add_custom_command(TARGET ${TEST_APP_NAME} POST_BUILD\n        COMMAND ${CMAKE_COMMAND} -E copy_if_different\n            \"${ASAN_RUNTIME_DLL}\"\n            $<TARGET_FILE_DIR:${TEST_APP_NAME}>\n        COMMENT \"Copying ASAN runtime DLL to test directory\"\n    )\nendif()\n\nadd_subdirectory(capi)"
  },
  {
    "path": "cpp/test/capi/CMakeLists.txt",
    "content": "set(CMAKE_C_STANDARD 11)\nset(CMAKE_C_STANDARD_REQUIRED ON)\n\n# Small wrapper library for the external sender\nSET(EXTERNAL_SENDER_WRAPPER_LIB \"external_sender\")\nSET(EXTERNAL_SENDER_WRAPPER_SOURCES \"${CMAKE_CURRENT_SOURCE_DIR}/../external_sender.cpp\" \"${CMAKE_CURRENT_SOURCE_DIR}/external_sender_wrapper.cpp\")\nSET(EXTERNAL_SENDER_WRAPPER_HEADERS \"${CMAKE_CURRENT_SOURCE_DIR}/../external_sender.h\" \"${CMAKE_CURRENT_SOURCE_DIR}/external_sender_wrapper.h\")\nadd_library(${EXTERNAL_SENDER_WRAPPER_LIB} ${EXTERNAL_SENDER_WRAPPER_SOURCES} ${EXTERNAL_SENDER_WRAPPER_HEADERS})\ntarget_include_directories(${EXTERNAL_SENDER_WRAPPER_LIB} PRIVATE ${PROJECT_SOURCE_DIR}/includes ${PROJECT_SOURCE_DIR}/src)\ntarget_link_libraries(${EXTERNAL_SENDER_WRAPPER_LIB} PRIVATE ${LIB_NAME} MLSPP::bytes MLSPP::mlspp)\n\n\nSET(TEST_APP_NAME \"capi_test\")\n\nfile(GLOB_RECURSE TEST_HEADERS CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/*.h\")\nfile(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS \"${CMAKE_CURRENT_SOURCE_DIR}/*.c\")\n\nadd_executable(${TEST_APP_NAME} ${TEST_HEADERS} ${TEST_SOURCES})\nadd_dependencies(${TEST_APP_NAME} ${LIB_NAME} ${EXTERNAL_SENDER_WRAPPER_LIB})\ntarget_include_directories(${TEST_APP_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/includes)\n\nif (BUILD_SHARED_LIBS)\n    set(LINKER_LANG C)\nelse()\n    set(LINKER_LANG CXX)\nendif()\n\nset_target_properties(${TEST_APP_NAME} PROPERTIES LINKER_LANGUAGE ${LINKER_LANG})\n\ntarget_link_libraries(${TEST_APP_NAME} PRIVATE ${LIB_NAME} ${EXTERNAL_SENDER_WRAPPER_LIB})\n\nif(WIN32 AND BUILD_SHARED_LIBS)\n    add_custom_command(TARGET ${TEST_APP_NAME} POST_BUILD\n        COMMAND ${CMAKE_COMMAND} -E copy_if_different\n            $<TARGET_FILE:${LIB_NAME}>\n            $<TARGET_FILE_DIR:${TEST_APP_NAME}>\n        COMMENT \"Copying ${LIB_NAME}.dll to test directory\"\n    )\nendif()\n\nif(WIN32 AND ENABLE_SANITIZERS)\n    if (NOT EXISTS \"${ASAN_RUNTIME_DLL}\")\n        message(FATAL_ERROR \"ASAN runtime DLL not found at ${ASAN_RUNTIME_DLL}\")\n    endif()\n    \n    add_custom_command(TARGET ${TEST_APP_NAME} POST_BUILD\n        COMMAND ${CMAKE_COMMAND} -E copy_if_different\n            \"${ASAN_RUNTIME_DLL}\"\n            $<TARGET_FILE_DIR:${TEST_APP_NAME}>\n        COMMENT \"Copying ASAN runtime DLL to test directory\"\n    )\nendif()\n\nadd_test(NAME ${TEST_APP_NAME} COMMAND ${TEST_APP_NAME})\n"
  },
  {
    "path": "cpp/test/capi/basic_tests.c",
    "content": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#ifdef _WIN32\n#define WIN32_LEAN_AND_MEAN\n#include <windows.h>\ntypedef CRITICAL_SECTION mutex_t;\ntypedef CONDITION_VARIABLE cond_t;\n#else\n#include <pthread.h>\n#include <unistd.h>\ntypedef pthread_mutex_t mutex_t;\ntypedef pthread_cond_t cond_t;\n#endif\n\n#include <dave/dave.h>\n\n#include \"external_sender_wrapper.h\"\n#include \"test_helpers.h\"\n\n#define RUN_TEST(test)                    \\\n    do {                                  \\\n        printf(\"Running %s...\\n\", #test); \\\n        if (test()) {                     \\\n            printf(\"  PASSED\\n\");         \\\n            passed++;                     \\\n        }                                 \\\n        else {                            \\\n            printf(\"  FAILED\\n\");         \\\n            failed++;                     \\\n        }                                 \\\n    } while (0)\n\nstatic int TestEncryptorCreateDestroy(void)\n{\n    DAVEEncryptorHandle encryptor = daveEncryptorCreate();\n    TEST_ASSERT(encryptor != NULL, \"Failed to create encryptor\");\n\n    daveEncryptorDestroy(encryptor);\n\n    return 1;\n}\n\nstatic int TestDecryptorCreateDestroy(void)\n{\n    DAVEDecryptorHandle decryptor = daveDecryptorCreate();\n    TEST_ASSERT(decryptor != NULL, \"Failed to create decryptor\");\n\n    daveDecryptorDestroy(decryptor);\n\n    return 1;\n}\n\nstatic int TestMaxProtocolVersion(void)\n{\n    uint16_t maxProtocolVersion = daveMaxSupportedProtocolVersion();\n    TEST_ASSERT_EQ(maxProtocolVersion, 1, \"Max protocol version should be 1\");\n\n    return 1;\n}\n\nstatic int TestEncryptorPassthrough(void)\n{\n    DAVEEncryptorHandle encryptor = daveEncryptorCreate();\n    TEST_ASSERT(encryptor != NULL, \"Failed to create encryptor\");\n\n    TEST_ASSERT_EQ(daveEncryptorHasKeyRatchet(encryptor), false, \"Encryptor should not have a key ratchet\");\n    TEST_ASSERT_EQ(daveEncryptorIsPassthroughMode(encryptor), false, \"Encryptor should not be in passthrough mode\");\n\n    // Set passthrough mode\n    daveEncryptorSetPassthroughMode(encryptor, true);\n\n    TEST_ASSERT_EQ(daveEncryptorIsPassthroughMode(encryptor), true, \"Encryptor should be in passthrough mode\");\n    TEST_ASSERT_EQ(daveEncryptorHasKeyRatchet(encryptor), false, \"Encryptor should not have a key ratchet\");\n\n    daveEncryptorAssignSsrcToCodec(encryptor, 0, DAVE_CODEC_OPUS);\n\n    // Create test data\n    const char* hexData = \"0dc5aedd5bdc3f20be5697e54dd1f437\";\n    size_t inputDataLength = 0;\n    uint8_t* inputData = GetBufferFromHex(hexData, &inputDataLength);\n    TEST_ASSERT(inputData != NULL, \"Failed to get input data\");\n\n    // Allocate output buffer\n    size_t outputDataLength =\n      daveEncryptorGetMaxCiphertextByteSize(encryptor, DAVE_MEDIA_TYPE_AUDIO, inputDataLength);\n    uint8_t* outputData = (uint8_t*)malloc(outputDataLength);\n\n    size_t bytesWritten = 0;\n\n    // Encrypt in passthrough mode\n    DAVEEncryptorResultCode result = daveEncryptorEncrypt(encryptor,\n                                                          DAVE_MEDIA_TYPE_AUDIO,\n                                                          0,\n                                                          inputData,\n                                                          inputDataLength,\n                                                          outputData,\n                                                          outputDataLength,\n                                                          &bytesWritten);\n\n    TEST_ASSERT_EQ(result, DAVE_ENCRYPTOR_RESULT_CODE_SUCCESS, \"Encryption should succeed\");\n    TEST_ASSERT_EQ(bytesWritten, inputDataLength, \"Bytes written should match input length\");\n    TEST_ASSERT(memcmp(inputData, outputData, inputDataLength) == 0,\n                \"Output should match input in passthrough mode\");\n\n    // Cleanup\n    free(inputData);\n    free(outputData);\n    daveEncryptorDestroy(encryptor);\n\n    return 1;\n}\n\nstatic int TestDecryptorPassthrough(void)\n{\n    DAVEDecryptorHandle decryptor = daveDecryptorCreate();\n    TEST_ASSERT(decryptor != NULL, \"Decryptor should be created\");\n\n    // Set passthrough mode\n    daveDecryptorTransitionToPassthroughMode(decryptor, 1);\n\n    // Create test data\n    const char* hexData = \"0dc5aedd5bdc3f20be5697e54dd1f437\";\n    size_t inputDataLength = 0;\n    uint8_t* inputData = GetBufferFromHex(hexData, &inputDataLength);\n    TEST_ASSERT(inputData != NULL, \"Input data should be allocated\");\n\n    // Allocate output buffer\n    size_t outputDataLength =\n      daveDecryptorGetMaxPlaintextByteSize(decryptor, DAVE_MEDIA_TYPE_AUDIO, inputDataLength);\n    uint8_t* outputData = (uint8_t*)malloc(outputDataLength);\n    size_t bytesWritten = 0;\n\n    // Decrypt in passthrough mode\n    DAVEDecryptorResultCode result = daveDecryptorDecrypt(decryptor,\n                                                          DAVE_MEDIA_TYPE_AUDIO,\n                                                          inputData,\n                                                          inputDataLength,\n                                                          outputData,\n                                                          outputDataLength,\n                                                          &bytesWritten);\n\n    TEST_ASSERT_EQ(result, DAVE_DECRYPTOR_RESULT_CODE_SUCCESS, \"Decryption should succeed\");\n    TEST_ASSERT_EQ(bytesWritten, inputDataLength, \"Bytes written should match input length\");\n    TEST_ASSERT(memcmp(inputData, outputData, inputDataLength) == 0,\n                \"Output should match input in passthrough mode\");\n\n    // Cleanup\n    free(inputData);\n    free(outputData);\n    daveDecryptorDestroy(decryptor);\n\n    return 1;\n}\n\nstatic int TestPassthroughInOutBuffer(void)\n{\n    const char* RandomBytes =\n      \"0dc5aedd5bdc3f20be5697e54dd1f437b896a36f858c6f20bbd69e2a493ca170c4f0c1b9acd4\"\n      \"9d324b92afa788d09b12b29115a2feb3552b60fff983234a6c9608af3933683efc6b0f5579a9\";\n\n    size_t incomingFrameLength = 0;\n    uint8_t* incomingFrame = GetBufferFromHex(RandomBytes, &incomingFrameLength);\n    TEST_ASSERT(incomingFrame != NULL, \"Failed to allocate incoming frame\");\n\n    uint8_t* frameCopy = (uint8_t*)malloc(incomingFrameLength);\n    TEST_ASSERT(frameCopy != NULL, \"Failed to allocate frame copy\");\n    memcpy(frameCopy, incomingFrame, incomingFrameLength);\n\n    // Encryptor test\n    DAVEEncryptorHandle encryptor = daveEncryptorCreate();\n    TEST_ASSERT(encryptor != NULL, \"Failed to create encryptor\");\n    daveEncryptorAssignSsrcToCodec(encryptor, 0, DAVE_CODEC_OPUS);\n    daveEncryptorSetPassthroughMode(encryptor, true);\n\n    size_t bytesWritten = 0;\n    DAVEEncryptorResultCode encryptResult = daveEncryptorEncrypt(encryptor,\n                                                                 DAVE_MEDIA_TYPE_AUDIO,\n                                                                 0,\n                                                                 incomingFrame,\n                                                                 incomingFrameLength,\n                                                                 incomingFrame,\n                                                                 incomingFrameLength,\n                                                                 &bytesWritten);\n\n    TEST_ASSERT_EQ(encryptResult, DAVE_ENCRYPTOR_RESULT_CODE_SUCCESS, \"Encryption should succeed\");\n    TEST_ASSERT_EQ(bytesWritten, incomingFrameLength, \"Bytes written should match input length\");\n    TEST_ASSERT(memcmp(incomingFrame, frameCopy, bytesWritten) == 0,\n                \"Encrypted data should match input in passthrough mode\");\n\n    // Decryptor test\n    DAVEDecryptorHandle decryptor = daveDecryptorCreate();\n    TEST_ASSERT(decryptor != NULL, \"Failed to create decryptor\");\n    daveDecryptorTransitionToPassthroughMode(decryptor, true);\n\n    bytesWritten = 0;\n    DAVEDecryptorResultCode decryptResult = daveDecryptorDecrypt(decryptor,\n                                                                 DAVE_MEDIA_TYPE_AUDIO,\n                                                                 incomingFrame,\n                                                                 incomingFrameLength,\n                                                                 incomingFrame,\n                                                                 incomingFrameLength,\n                                                                 &bytesWritten);\n\n    TEST_ASSERT_EQ(decryptResult, DAVE_DECRYPTOR_RESULT_CODE_SUCCESS, \"Decryption should succeed\");\n    TEST_ASSERT_EQ(bytesWritten, incomingFrameLength, \"Bytes written should match input length\");\n    TEST_ASSERT(memcmp(incomingFrame, frameCopy, bytesWritten) == 0,\n                \"Decrypted data should match input in passthrough mode\");\n\n    // Cleanup\n    free(incomingFrame);\n    free(frameCopy);\n    daveEncryptorDestroy(encryptor);\n    daveDecryptorDestroy(decryptor);\n\n    return 1;\n}\n\nstatic int TestPassthroughTwoBuffers(void)\n{\n    const char* RandomBytes =\n      \"0dc5aedd5bdc3f20be5697e54dd1f437b896a36f858c6f20bbd69e2a493ca170c4f0c1b9acd4\"\n      \"9d324b92afa788d09b12b29115a2feb3552b60fff983234a6c9608af3933683efc6b0f5579a9\";\n\n    size_t incomingFrameLength = 0;\n    uint8_t* incomingFrame = GetBufferFromHex(RandomBytes, &incomingFrameLength);\n    TEST_ASSERT(incomingFrame != NULL, \"Failed to allocate incoming frame\");\n\n    uint8_t* encryptedFrame = (uint8_t*)malloc(incomingFrameLength * 2);\n    TEST_ASSERT(encryptedFrame != NULL, \"Failed to allocate encrypted frame\");\n\n    uint8_t* decryptedFrame = (uint8_t*)malloc(incomingFrameLength);\n    TEST_ASSERT(decryptedFrame != NULL, \"Failed to allocate decrypted frame\");\n\n    // Encryptor test\n    DAVEEncryptorHandle encryptor = daveEncryptorCreate();\n    TEST_ASSERT(encryptor != NULL, \"Failed to create encryptor\");\n    daveEncryptorAssignSsrcToCodec(encryptor, 0, DAVE_CODEC_OPUS);\n    daveEncryptorSetPassthroughMode(encryptor, true);\n\n    size_t bytesWritten = 0;\n    DAVEEncryptorResultCode encryptResult = daveEncryptorEncrypt(encryptor,\n                                                                 DAVE_MEDIA_TYPE_AUDIO,\n                                                                 0,\n                                                                 incomingFrame,\n                                                                 incomingFrameLength,\n                                                                 encryptedFrame,\n                                                                 incomingFrameLength * 2,\n                                                                 &bytesWritten);\n\n    TEST_ASSERT_EQ(encryptResult, DAVE_ENCRYPTOR_RESULT_CODE_SUCCESS, \"Encryption should succeed\");\n    TEST_ASSERT_EQ(bytesWritten, incomingFrameLength, \"Bytes written should match input length\");\n    TEST_ASSERT(memcmp(incomingFrame, encryptedFrame, bytesWritten) == 0,\n                \"Encrypted data should match input in passthrough mode\");\n\n    // Decryptor test\n    DAVEDecryptorHandle decryptor = daveDecryptorCreate();\n    TEST_ASSERT(decryptor != NULL, \"Failed to create decryptor\");\n    daveDecryptorTransitionToPassthroughMode(decryptor, true);\n\n    size_t bytesDecrypted = 0;\n    DAVEDecryptorResultCode decryptResult = daveDecryptorDecrypt(decryptor,\n                                                                 DAVE_MEDIA_TYPE_AUDIO,\n                                                                 encryptedFrame,\n                                                                 bytesWritten,\n                                                                 decryptedFrame,\n                                                                 incomingFrameLength,\n                                                                 &bytesDecrypted);\n\n    TEST_ASSERT_EQ(decryptResult, DAVE_DECRYPTOR_RESULT_CODE_SUCCESS, \"Decryption should succeed\");\n    TEST_ASSERT_EQ(\n      bytesDecrypted, incomingFrameLength, \"Bytes decrypted should match input length\");\n    TEST_ASSERT(memcmp(encryptedFrame, decryptedFrame, bytesDecrypted) == 0,\n                \"Decrypted data should match encrypted data\");\n\n    // Cleanup\n    free(incomingFrame);\n    free(encryptedFrame);\n    free(decryptedFrame);\n    daveEncryptorDestroy(encryptor);\n    daveDecryptorDestroy(decryptor);\n\n    return 1;\n}\n\nstatic void TestSessionFailureCallback(const char* source, const char* reason, void* userData)\n{\n    (void)userData;\n    printf(\"Session failure: %s: %s\\n\", source, reason);\n}\n\ntypedef struct {\n    mutex_t mutex;\n    cond_t cond;\n    uint8_t* pairwiseFingerprint;\n    size_t pairwiseFingerprintLength;\n} PairwiseFingerprintData;\n\nstatic void PairwiseFingerprintDataInit(PairwiseFingerprintData* data)\n{\n#ifdef _WIN32\n    InitializeCriticalSection(&data->mutex);\n    InitializeConditionVariable(&data->cond);\n#else\n    pthread_mutex_init(&data->mutex, NULL);\n    pthread_cond_init(&data->cond, NULL);\n#endif\n    data->pairwiseFingerprint = NULL;\n    data->pairwiseFingerprintLength = 0;\n}\n\nstatic void PairwiseFingerprintDataDestroy(PairwiseFingerprintData* data)\n{\n#ifdef _WIN32\n    DeleteCriticalSection(&data->mutex);\n    // CONDITION_VARIABLE does not need cleanup\n#else\n    pthread_mutex_destroy(&data->mutex);\n    pthread_cond_destroy(&data->cond);\n#endif\n    free(data->pairwiseFingerprint);\n    data->pairwiseFingerprint = NULL;\n    data->pairwiseFingerprintLength = 0;\n}\n\nstatic void PairwiseFingerprintDataWait(PairwiseFingerprintData* data)\n{\n#ifdef _WIN32\n    EnterCriticalSection(&data->mutex);\n    if (data->pairwiseFingerprint == NULL) {\n        SleepConditionVariableCS(&data->cond, &data->mutex, INFINITE);\n    }\n    LeaveCriticalSection(&data->mutex);\n#else\n    pthread_mutex_lock(&data->mutex);\n    if (data->pairwiseFingerprint == NULL) {\n        pthread_cond_wait(&data->cond, &data->mutex);\n    }\n    pthread_mutex_unlock(&data->mutex);\n#endif\n}\n\nstatic void PairwiseFingerprintCallback(const uint8_t* pairwiseFingerprint,\n                                        size_t pairwiseFingerprintLength,\n                                        void* userData)\n{\n    if (userData == NULL) {\n        return;\n    }\n    PairwiseFingerprintData* data = (PairwiseFingerprintData*)userData;\n#ifdef _WIN32\n    EnterCriticalSection(&data->mutex);\n    data->pairwiseFingerprint = (uint8_t*)malloc(pairwiseFingerprintLength);\n    memcpy(data->pairwiseFingerprint, pairwiseFingerprint, pairwiseFingerprintLength);\n    data->pairwiseFingerprintLength = pairwiseFingerprintLength;\n    WakeConditionVariable(&data->cond);\n    LeaveCriticalSection(&data->mutex);\n#else\n    pthread_mutex_lock(&data->mutex);\n    data->pairwiseFingerprint = (uint8_t*)malloc(pairwiseFingerprintLength);\n    memcpy(data->pairwiseFingerprint, pairwiseFingerprint, pairwiseFingerprintLength);\n    data->pairwiseFingerprintLength = pairwiseFingerprintLength;\n    pthread_cond_signal(&data->cond);\n    pthread_mutex_unlock(&data->mutex);\n#endif\n}\n\nstatic int TestSession(void)\n{\n    uint64_t groupId = 1234567890;\n    const char* userA = \"1234123412341234\";\n    const char* userB = \"5678567856785678\";\n\n    printf(\"Creating external sender\\n\");\n    DAVEExternalSenderHandle externalSender = daveExternalSenderCreate(groupId);\n    TEST_ASSERT(externalSender != NULL, \"Failed to create external sender\");\n\n    // Create sessions\n    printf(\"Creating sessions\\n\");\n    DAVESessionHandle sessionA = daveSessionCreate(NULL, NULL, TestSessionFailureCallback, NULL);\n    DAVESessionHandle sessionB = daveSessionCreate(NULL, NULL, TestSessionFailureCallback, NULL);\n    TEST_ASSERT(sessionA != NULL, \"Failed to create session\");\n    TEST_ASSERT(sessionB != NULL, \"Failed to create session\");\n\n    // Set external sender\n    printf(\"Setting external sender\\n\");\n    uint8_t* marshalledExternalSender = NULL;\n    size_t marshalledExternalSenderLength = 0;\n    daveExternalSenderGetMarshalledExternalSender(\n      externalSender, &marshalledExternalSender, &marshalledExternalSenderLength);\n    TEST_ASSERT(marshalledExternalSender != NULL, \"Failed to get marshalled external sender\");\n    daveSessionSetExternalSender(\n      sessionA, marshalledExternalSender, marshalledExternalSenderLength);\n    daveSessionSetExternalSender(\n      sessionB, marshalledExternalSender, marshalledExternalSenderLength);\n    daveFree(marshalledExternalSender);\n\n    // Init sessions\n    daveSessionInit(sessionA, 1, groupId, userA);\n    daveSessionInit(sessionB, 1, groupId, userB);\n    TEST_ASSERT_EQ(daveSessionGetProtocolVersion(sessionA), 1, \"Protocol version should be 1\");\n    TEST_ASSERT_EQ(daveSessionGetProtocolVersion(sessionB), 1, \"Protocol version should be 1\");\n\n    // Get key packages\n    printf(\"Getting key packages\\n\");\n    uint8_t* keyPackageA = NULL;\n    size_t keyPackageALength = 0;\n    daveSessionGetMarshalledKeyPackage(sessionA, &keyPackageA, &keyPackageALength);\n    TEST_ASSERT(keyPackageA != NULL, \"Failed to get key package\");\n\n    uint8_t* keyPackageB = NULL;\n    size_t keyPackageBLength = 0;\n    daveSessionGetMarshalledKeyPackage(sessionB, &keyPackageB, &keyPackageBLength);\n    TEST_ASSERT(keyPackageB != NULL, \"Failed to get key package\");\n\n    // Make add proposal for user B\n    printf(\"Proposing add\\n\");\n    uint8_t* proposal = NULL;\n    size_t proposalLength = 0;\n    daveExternalSenderProposeAdd(\n      externalSender, 0, keyPackageB, keyPackageBLength, &proposal, &proposalLength);\n    TEST_ASSERT(proposal != NULL, \"Failed to propose add user B\");\n    daveFree(keyPackageA);\n    daveFree(keyPackageB);\n\n    // Process proposal of user B\n    printf(\"Processing proposals\\n\");\n    uint8_t* commitWelcome = NULL;\n    size_t commitWelcomeLength = 0;\n    const char* recognizedUserIds[] = {userA, userB};\n    daveSessionProcessProposals(sessionA,\n                                proposal,\n                                proposalLength,\n                                recognizedUserIds,\n                                2,\n                                &commitWelcome,\n                                &commitWelcomeLength);\n    TEST_ASSERT(commitWelcome != NULL, \"Failed to process proposals\");\n    daveFree(proposal);\n\n    // Split commit welcome\n    printf(\"Splitting commit welcome\\n\");\n    uint8_t* commit = NULL;\n    size_t commitLength = 0;\n    uint8_t* welcome = NULL;\n    size_t welcomeLength = 0;\n    daveExternalSenderSplitCommitWelcome(externalSender,\n                                         commitWelcome,\n                                         commitWelcomeLength,\n                                         &commit,\n                                         &commitLength,\n                                         &welcome,\n                                         &welcomeLength);\n    TEST_ASSERT(commit != NULL, \"Failed to split commit welcome\");\n    TEST_ASSERT(welcome != NULL, \"Failed to split commit welcome\");\n    daveFree(commitWelcome);\n\n    // Process commit generated by user A\n    printf(\"Processing commit welcome\\n\");\n    DAVECommitResultHandle commitResult = daveSessionProcessCommit(sessionA, commit, commitLength);\n    DAVEWelcomeResultHandle welcomeResult =\n      daveSessionProcessWelcome(sessionB, welcome, welcomeLength, recognizedUserIds, 2);\n    daveFree(commit);\n    daveFree(welcome);\n\n    // Check commit welcome results\n    printf(\"Checking commit welcome results\\n\");\n    TEST_ASSERT_EQ(daveCommitResultIsFailed(commitResult), false, \"Commit should not be failed\");\n    TEST_ASSERT_EQ(daveCommitResultIsIgnored(commitResult), false, \"Commit should not be ignored\");\n    uint64_t* rosterIds = NULL;\n    size_t rosterIdsLength = 0;\n    daveCommitResultGetRosterMemberIds(commitResult, &rosterIds, &rosterIdsLength);\n    TEST_ASSERT(rosterIds != NULL, \"Failed to get roster member ids\");\n    TEST_ASSERT_EQ(rosterIdsLength, 2, \"Roster member ids length should be 2\");\n    TEST_ASSERT(rosterIds[0] == 1234123412341234, \"Roster member id should be user A\");\n    TEST_ASSERT(rosterIds[1] == 5678567856785678, \"Roster member id should be user B\");\n    daveFree(rosterIds);\n    daveWelcomeResultGetRosterMemberIds(welcomeResult, &rosterIds, &rosterIdsLength);\n    TEST_ASSERT(rosterIds != NULL, \"Failed to get roster member ids\");\n    TEST_ASSERT_EQ(rosterIdsLength, 2, \"Roster member ids length should be 2\");\n    TEST_ASSERT(rosterIds[0] == 1234123412341234, \"Roster member id should be user A\");\n    TEST_ASSERT(rosterIds[1] == 5678567856785678, \"Roster member id should be user B\");\n\n    uint8_t* signature = NULL;\n    size_t signatureLength = 0;\n    daveCommitResultGetRosterMemberSignature(\n      commitResult, rosterIds[0], &signature, &signatureLength);\n    TEST_ASSERT(signature != NULL, \"Failed to get signature\");\n    TEST_ASSERT(signatureLength > 0, \"Signature length should be greater than 0\");\n    daveFree(signature);\n    daveCommitResultGetRosterMemberSignature(\n      commitResult, rosterIds[1], &signature, &signatureLength);\n    TEST_ASSERT(signature != NULL, \"Failed to get signature\");\n    TEST_ASSERT(signatureLength > 0, \"Signature length should be greater than 0\");\n    daveFree(signature);\n    daveWelcomeResultGetRosterMemberSignature(\n      welcomeResult, rosterIds[0], &signature, &signatureLength);\n    TEST_ASSERT(signature != NULL, \"Failed to get signature\");\n    TEST_ASSERT(signatureLength > 0, \"Signature length should be greater than 0\");\n    daveFree(signature);\n    daveWelcomeResultGetRosterMemberSignature(\n      welcomeResult, rosterIds[1], &signature, &signatureLength);\n    TEST_ASSERT(signature != NULL, \"Failed to get signature\");\n    TEST_ASSERT(signatureLength > 0, \"Signature length should be greater than 0\");\n    daveFree(signature);\n\n    daveFree(rosterIds);\n    daveCommitResultDestroy(commitResult);\n    daveWelcomeResultDestroy(welcomeResult);\n\n    // Match authenticators\n    printf(\"Matching authenticators\\n\");\n    uint8_t* authenticatorA = NULL;\n    size_t authenticatorALength = 0;\n    daveSessionGetLastEpochAuthenticator(sessionA, &authenticatorA, &authenticatorALength);\n    TEST_ASSERT(authenticatorA != NULL, \"Failed to get authenticator\");\n    uint8_t* authenticatorB = NULL;\n    size_t authenticatorBLength = 0;\n    daveSessionGetLastEpochAuthenticator(sessionB, &authenticatorB, &authenticatorBLength);\n    TEST_ASSERT(authenticatorB != NULL, \"Failed to get authenticator\");\n    TEST_ASSERT(memcmp(authenticatorA, authenticatorB, authenticatorALength) == 0,\n                \"Authenticators should match\");\n    daveFree(authenticatorA);\n    daveFree(authenticatorB);\n\n    // Get pairwise fingerprints\n    printf(\"Matching pairwise fingerprints\\n\");\n    PairwiseFingerprintData pairwiseFingerprintDataA;\n    PairwiseFingerprintDataInit(&pairwiseFingerprintDataA);\n    PairwiseFingerprintData pairwiseFingerprintDataB;\n    PairwiseFingerprintDataInit(&pairwiseFingerprintDataB);\n    daveSessionGetPairwiseFingerprint(\n      sessionA, 1, userB, &PairwiseFingerprintCallback, &pairwiseFingerprintDataA);\n    daveSessionGetPairwiseFingerprint(\n      sessionB, 1, userA, &PairwiseFingerprintCallback, &pairwiseFingerprintDataB);\n    PairwiseFingerprintDataWait(&pairwiseFingerprintDataA);\n    PairwiseFingerprintDataWait(&pairwiseFingerprintDataB);\n    TEST_ASSERT(pairwiseFingerprintDataA.pairwiseFingerprintLength ==\n                  pairwiseFingerprintDataB.pairwiseFingerprintLength,\n                \"Pairwise fingerprint lengths should match\");\n    TEST_ASSERT(memcmp(pairwiseFingerprintDataA.pairwiseFingerprint,\n                       pairwiseFingerprintDataB.pairwiseFingerprint,\n                       pairwiseFingerprintDataA.pairwiseFingerprintLength) == 0,\n                \"Pairwise fingerprint should match\");\n    PairwiseFingerprintDataDestroy(&pairwiseFingerprintDataA);\n    PairwiseFingerprintDataDestroy(&pairwiseFingerprintDataB);\n\n    // Get key ratchets\n    printf(\"Getting key ratchets\\n\");\n    DAVEKeyRatchetHandle keyRatchetA = daveSessionGetKeyRatchet(sessionA, userA);\n    DAVEKeyRatchetHandle keyRatchetB = daveSessionGetKeyRatchet(sessionB, userA);\n    TEST_ASSERT(keyRatchetA != NULL, \"Failed to get key ratchet\");\n    TEST_ASSERT(keyRatchetB != NULL, \"Failed to get key ratchet\");\n\n    // Setup encryptor\n    printf(\"Setting up encryptor\\n\");\n    DAVEEncryptorHandle encryptorA = daveEncryptorCreate();\n    daveEncryptorAssignSsrcToCodec(encryptorA, 0, DAVE_CODEC_OPUS);\n    daveEncryptorSetPassthroughMode(encryptorA, false);\n    daveEncryptorSetKeyRatchet(encryptorA, keyRatchetA);\n    daveKeyRatchetDestroy(keyRatchetA);\n\n    TEST_ASSERT_EQ(daveEncryptorHasKeyRatchet(encryptorA), true, \"Encryptor should have a key ratchet\");\n    TEST_ASSERT_EQ(daveEncryptorIsPassthroughMode(encryptorA), false, \"Encryptor should not be in passthrough mode\");\n\n    // Setup decryptor\n    printf(\"Setting up decryptor\\n\");\n    DAVEDecryptorHandle decryptorA = daveDecryptorCreate();\n    daveDecryptorTransitionToPassthroughMode(decryptorA, false);\n    daveDecryptorTransitionToKeyRatchet(decryptorA, keyRatchetB);\n    daveKeyRatchetDestroy(keyRatchetB);\n\n    // Create test data\n    printf(\"Creating test data\\n\");\n    const char* hexData = \"0dc5aedd5bdc3f20be5697e54dd1f437\";\n    size_t inputDataLength = 0;\n    uint8_t* inputData = GetBufferFromHex(hexData, &inputDataLength);\n    TEST_ASSERT(inputData != NULL, \"Failed to get input data\");\n\n    // Encrypt data\n    printf(\"Encrypting data\\n\");\n    size_t encryptedFrameLength =\n      daveEncryptorGetMaxCiphertextByteSize(encryptorA, DAVE_MEDIA_TYPE_AUDIO, inputDataLength);\n    uint8_t* encryptedFrame = (uint8_t*)malloc(encryptedFrameLength);\n    daveEncryptorEncrypt(encryptorA,\n                         DAVE_MEDIA_TYPE_AUDIO,\n                         0,\n                         inputData,\n                         inputDataLength,\n                         encryptedFrame,\n                         encryptedFrameLength,\n                         &encryptedFrameLength);\n    TEST_ASSERT(encryptedFrame != NULL, \"Failed to encrypt data\");\n    TEST_ASSERT(encryptedFrameLength > inputDataLength,\n                \"Encrypted data length should be greater than input data length\");\n    TEST_ASSERT(memcmp(inputData, encryptedFrame, inputDataLength) != 0,\n                \"Encrypted data should not match input data\");\n\n    // Decrypt data\n    printf(\"Decrypting data\\n\");\n    size_t decryptedFrameLength =\n      daveDecryptorGetMaxPlaintextByteSize(decryptorA, DAVE_MEDIA_TYPE_AUDIO, encryptedFrameLength);\n    uint8_t* decryptedFrame = (uint8_t*)malloc(decryptedFrameLength);\n    daveDecryptorDecrypt(decryptorA,\n                         DAVE_MEDIA_TYPE_AUDIO,\n                         encryptedFrame,\n                         encryptedFrameLength,\n                         decryptedFrame,\n                         decryptedFrameLength,\n                         &decryptedFrameLength);\n    TEST_ASSERT(decryptedFrame != NULL, \"Failed to decrypt data\");\n    TEST_ASSERT_EQ(decryptedFrameLength,\n                   inputDataLength,\n                   \"Decrypted data length should be equal to input data length\");\n    TEST_ASSERT(memcmp(inputData, decryptedFrame, inputDataLength) == 0,\n                \"Decrypted data should match input data\");\n\n    // Check encryptor stats\n    printf(\"Checking encryptor stats\\n\");\n    DAVEEncryptorStats encryptorStats;\n    daveEncryptorGetStats(encryptorA, DAVE_MEDIA_TYPE_AUDIO, &encryptorStats);\n    TEST_ASSERT_EQ(encryptorStats.encryptSuccessCount,\n                   1,\n                   \"Encryptor should have at least one successful encryption\");\n    TEST_ASSERT_EQ(\n      encryptorStats.encryptFailureCount, 0, \"Encryptor should have no failed encryptions\");\n    TEST_ASSERT(encryptorStats.encryptDuration > 0, \"Encryptor should have a duration\");\n    TEST_ASSERT_EQ(\n      encryptorStats.encryptAttempts, 1, \"Encryptor should have at least one encryption attempt\");\n    TEST_ASSERT_EQ(encryptorStats.encryptMaxAttempts,\n                   1,\n                   \"Encryptor should have a maximum number of encryption attempts\");\n    TEST_ASSERT_EQ(\n      encryptorStats.encryptMissingKeyCount, 0, \"Encryptor should have no missing keys\");\n\n    // Check decryptor stats\n    printf(\"Checking decryptor stats\\n\");\n    DAVEDecryptorStats decryptorStats;\n    daveDecryptorGetStats(decryptorA, DAVE_MEDIA_TYPE_AUDIO, &decryptorStats);\n    TEST_ASSERT_EQ(decryptorStats.decryptSuccessCount,\n                   1,\n                   \"Decryptor should have at least one successful decryption\");\n    TEST_ASSERT_EQ(\n      decryptorStats.decryptFailureCount, 0, \"Decryptor should have no failed decryptions\");\n    TEST_ASSERT(decryptorStats.decryptDuration > 0, \"Decryptor should have a duration\");\n    TEST_ASSERT_EQ(\n      decryptorStats.decryptAttempts, 1, \"Decryptor should have at least one decryption attempt\");\n    TEST_ASSERT_EQ(\n      decryptorStats.decryptMissingKeyCount, 0, \"Decryptor should have no missing keys\");\n    TEST_ASSERT_EQ(\n      decryptorStats.decryptInvalidNonceCount, 0, \"Decryptor should have no invalid nonces\");\n\n    // Clean up\n    printf(\"Cleaning up\\n\");\n    free(inputData);\n    free(encryptedFrame);\n    free(decryptedFrame);\n    daveEncryptorDestroy(encryptorA);\n    daveDecryptorDestroy(decryptorA);\n    daveSessionDestroy(sessionA);\n    daveSessionDestroy(sessionB);\n    daveExternalSenderDestroy(externalSender);\n\n    return 1;\n}\n\nstatic int TestExceptions(void)\n{\n    printf(\"Testing exception catching\\n\");\n    DAVESessionHandle session = daveSessionCreate(NULL, NULL, TestSessionFailureCallback, NULL);\n    TEST_ASSERT(session != NULL, \"Failed to create session\");\n\n\n    PairwiseFingerprintData pairwiseFingerprintData;\n    PairwiseFingerprintDataInit(&pairwiseFingerprintData);\n    daveSessionGetPairwiseFingerprint(\n      session, 1, \"1234123412341234\", &PairwiseFingerprintCallback, &pairwiseFingerprintData);\n    PairwiseFingerprintDataWait(&pairwiseFingerprintData);\n    TEST_ASSERT_EQ(pairwiseFingerprintData.pairwiseFingerprintLength, 0,\n                   \"Expected empty fingerprint when exception is caught\");\n\n    PairwiseFingerprintDataDestroy(&pairwiseFingerprintData);\n    daveSessionDestroy(session);\n\n    return 1;\n}\n\nint main(void)\n{\n    int passed = 0;\n    int failed = 0;\n\n    printf(\"\\n=== Running C API Tests ===\\n\\n\");\n\n    RUN_TEST(TestEncryptorCreateDestroy);\n    RUN_TEST(TestDecryptorCreateDestroy);\n    RUN_TEST(TestMaxProtocolVersion);\n    RUN_TEST(TestEncryptorPassthrough);\n    RUN_TEST(TestDecryptorPassthrough);\n    RUN_TEST(TestPassthroughInOutBuffer);\n    RUN_TEST(TestPassthroughTwoBuffers);\n    RUN_TEST(TestSession);\n    RUN_TEST(TestExceptions);\n\n    printf(\"\\n=== Test Results ===\\n\");\n    printf(\"Passed: %d\\n\", passed);\n    printf(\"Failed: %d\\n\", failed);\n    printf(\"Total:  %d\\n\", passed + failed);\n\n    return (failed == 0) ? 0 : 1;\n}\n"
  },
  {
    "path": "cpp/test/capi/external_sender_wrapper.cpp",
    "content": "#include \"external_sender_wrapper.h\"\n\n#include <cstring>\n#include <vector>\n\n#include \"../external_sender.h\"\n\nnamespace {\n\nvoid CopyVectorToOutputBuffer(std::vector<uint8_t> const& vector, uint8_t** data, size_t* length)\n{\n    if (data == nullptr || length == nullptr) {\n        return;\n    }\n\n    if (vector.empty()) {\n        *data = nullptr;\n        *length = 0;\n        return;\n    }\n\n    *data = reinterpret_cast<uint8_t*>(malloc(vector.size()));\n    memcpy(*data, vector.data(), vector.size());\n    *length = vector.size();\n}\n\n} // anonymous namespace\n\nDAVEExternalSenderHandle daveExternalSenderCreate(uint64_t groupId)\n{\n    auto protocolVersion = daveMaxSupportedProtocolVersion();\n    auto externalSender =\n      std::make_unique<discord::dave::test::ExternalSender>(protocolVersion, groupId);\n    return reinterpret_cast<DAVEExternalSenderHandle>(externalSender.release());\n}\n\nvoid daveExternalSenderDestroy(DAVEExternalSenderHandle externalSenderHandle)\n{\n    auto externalSender =\n      reinterpret_cast<discord::dave::test::ExternalSender*>(externalSenderHandle);\n    delete externalSender;\n}\n\nvoid daveExternalSenderGetMarshalledExternalSender(DAVEExternalSenderHandle externalSenderHandle,\n                                                   uint8_t** marshalledExternalSender,\n                                                   size_t* length)\n{\n    auto externalSender =\n      reinterpret_cast<discord::dave::test::ExternalSender*>(externalSenderHandle);\n    auto externalSenderVec = externalSender->GetMarshalledExternalSender();\n    CopyVectorToOutputBuffer(externalSenderVec, marshalledExternalSender, length);\n}\n\nvoid daveExternalSenderProposeAdd(DAVEExternalSenderHandle externalSenderHandle,\n                                  uint32_t epoch,\n                                  uint8_t* keyPackage,\n                                  size_t keyPackageLength,\n                                  uint8_t** proposal,\n                                  size_t* proposalLength)\n{\n    auto externalSender =\n      reinterpret_cast<discord::dave::test::ExternalSender*>(externalSenderHandle);\n    auto keyPackageVec = std::vector<uint8_t>(keyPackage, keyPackage + keyPackageLength);\n    auto result = externalSender->ProposeAdd(epoch, std::move(keyPackageVec));\n    CopyVectorToOutputBuffer(result, proposal, proposalLength);\n}\n\nvoid daveExternalSenderSplitCommitWelcome(DAVEExternalSenderHandle externalSenderHandle,\n                                          uint8_t* commitWelcome,\n                                          size_t commitWelcomeLength,\n                                          uint8_t** commit,\n                                          size_t* commitLength,\n                                          uint8_t** welcome,\n                                          size_t* welcomeLength)\n{\n    auto externalSender =\n      reinterpret_cast<discord::dave::test::ExternalSender*>(externalSenderHandle);\n    auto commitWelcomeVec =\n      std::vector<uint8_t>(commitWelcome, commitWelcome + commitWelcomeLength);\n    auto [commitBytes, welcomeBytes] =\n      externalSender->SplitCommitWelcome(std::move(commitWelcomeVec));\n    CopyVectorToOutputBuffer(commitBytes, commit, commitLength);\n    CopyVectorToOutputBuffer(welcomeBytes, welcome, welcomeLength);\n}"
  },
  {
    "path": "cpp/test/capi/external_sender_wrapper.h",
    "content": "\n#include <dave/dave.h>\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nDECLARE_OPAQUE_HANDLE(DAVEExternalSenderHandle);\n\nDAVE_EXPORT DAVEExternalSenderHandle daveExternalSenderCreate(uint64_t groupId);\nDAVE_EXPORT void daveExternalSenderDestroy(DAVEExternalSenderHandle externalSender);\nDAVE_EXPORT void daveExternalSenderGetMarshalledExternalSender(\n  DAVEExternalSenderHandle externalSender,\n  uint8_t** marshalledExternalSender,\n  size_t* length);\nDAVE_EXPORT void daveExternalSenderProposeAdd(DAVEExternalSenderHandle externalSender,\n                                              uint32_t epoch,\n                                              uint8_t* keyPackage,\n                                              size_t keyPackageLength,\n                                              uint8_t** proposal,\n                                              size_t* proposalLength);\nDAVE_EXPORT void daveExternalSenderSplitCommitWelcome(DAVEExternalSenderHandle externalSender,\n                                                      uint8_t* commitWelcome,\n                                                      size_t commitWelcomeLength,\n                                                      uint8_t** commit,\n                                                      size_t* commitLength,\n                                                      uint8_t** welcome,\n                                                      size_t* welcomeLength);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "cpp/test/capi/test_helpers.c",
    "content": "#include \"test_helpers.h\"\n\n#include <stdlib.h>\n#include <string.h>\n\nstatic int HexDigitToValue(char c)\n{\n    if (c >= '0' && c <= '9') {\n        return c - '0';\n    }\n    if (c >= 'a' && c <= 'f') {\n        return c - 'a' + 10;\n    }\n    if (c >= 'A' && c <= 'F') {\n        return c - 'A' + 10;\n    }\n    return -1;\n}\n\nuint8_t* GetBufferFromHex(const char* hex, size_t* outLength)\n{\n    if (!hex || !outLength) {\n        return NULL;\n    }\n\n    size_t hexLength = strlen(hex);\n    if (hexLength % 2 != 0) {\n        *outLength = 0;\n        return NULL;\n    }\n\n    size_t bufferLength = hexLength / 2;\n    uint8_t* buffer = (uint8_t*)malloc(bufferLength);\n    if (!buffer) {\n        *outLength = 0;\n        return NULL;\n    }\n\n    for (size_t i = 0; i < hexLength; i += 2) {\n        int high = HexDigitToValue(hex[i]);\n        int low = HexDigitToValue(hex[i + 1]);\n\n        if (high < 0 || low < 0) {\n            free(buffer);\n            *outLength = 0;\n            return NULL;\n        }\n\n        buffer[i / 2] = (uint8_t)((high << 4) | low);\n    }\n\n    *outLength = bufferLength;\n    return buffer;\n}\n"
  },
  {
    "path": "cpp/test/capi/test_helpers.h",
    "content": "#ifndef TEST_HELPERS_H\n#define TEST_HELPERS_H\n\n#include <stddef.h>\n#include <stdint.h>\n\n#define TEST_ASSERT(condition, message)                                              \\\n    do {                                                                             \\\n        if (!(condition)) {                                                          \\\n            fprintf(stderr, \"FAILED: %s (at %s:%d)\\n\", message, __FILE__, __LINE__); \\\n            return 0;                                                                \\\n        }                                                                            \\\n    } while (0)\n\n#define TEST_ASSERT_EQ(a, b, message)                                    \\\n    do {                                                                 \\\n        if ((a) != (b)) {                                                \\\n            fprintf(stderr,                                              \\\n                    \"FAILED: %s - expected %lld, got %lld (at %s:%d)\\n\", \\\n                    message,                                             \\\n                    (long long)(b),                                      \\\n                    (long long)(a),                                      \\\n                    __FILE__,                                            \\\n                    __LINE__);                                           \\\n            return 0;                                                    \\\n        }                                                                \\\n    } while (0)\n\nuint8_t* GetBufferFromHex(const char* hex, size_t* outLength);\n\n#endif /* TEST_HELPERS_H */\n"
  },
  {
    "path": "cpp/test/codec_utils_tests.cpp",
    "content": "#include <vector>\n#include \"gtest/gtest.h\"\n\n#include <dave/array_view.h>\n\n#include \"codec_utils.h\"\n#include \"decryptor.h\"\n#include \"encryptor.h\"\n#include \"frame_processors.h\"\n\n#include \"dave_test.h\"\n#include \"static_key_ratchet.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nTEST_F(DaveTests, RandomOpusFrame)\n{\n    constexpr std::string_view randomBytes =\n      \"0dc5aedd5bdc3f20be5697e54dd1f437b896a36f858c6f20bbd69e2a493ca170c4f0c1b9acd4\"\n      \"9d324b92afa788d09b12b29115a2feb3552b60fff983234a6c9608af3933683efc6b0f5579a9\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(randomBytes);\n\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::Opus);\n    auto& unencryptedBytes = frameProcessor.GetUnencryptedBytes();\n    auto& encryptedBytes = frameProcessor.GetEncryptedBytes();\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(incomingFrame.size(), 76u);\n    EXPECT_EQ(unencryptedBytes.size(), 0u);\n    EXPECT_EQ(encryptedBytes.size(), incomingFrame.size());\n    EXPECT_EQ(unencryptedRanges.size(), 0u);\n}\n\nTEST_F(DaveTests, SplitReconstruct)\n{\n    std::string randomBytes =\n      \"0dc5aedd5bdc3f20be5697e54dd1f437b896a36f858c6f20bbd69e2a493ca170c4f0c1b9acd4\"\n      \"9d324b92afa788d09b12b29115a2feb3552b60fff983234a6c9608af3933683efc6b0f5579a9\"\n      \"0000000000000000 00 000a 140a 280a 3c0a 14 fafa\";\n    randomBytes.erase(std::remove(randomBytes.begin(), randomBytes.end(), ' '), randomBytes.end());\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(randomBytes);\n\n    auto reconstructedFrame = std::make_unique<uint8_t[]>(incomingFrame.size());\n\n    InboundFrameProcessor frameProcessor;\n\n    frameProcessor.ParseFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()));\n    memcpy(frameProcessor.GetPlaintext().data(),\n           frameProcessor.GetCiphertext().data(),\n           frameProcessor.GetCiphertext().size());\n    auto bytesWritten = frameProcessor.ReconstructFrame(\n      MakeArrayView<uint8_t>(reconstructedFrame.get(), incomingFrame.size()));\n\n    EXPECT_EQ(bytesWritten, 76u);\n    EXPECT_EQ(memcmp(incomingFrame.data(), reconstructedFrame.get(), bytesWritten), 0);\n}\n\nTEST_F(DaveTests, H264SliceOneByteExpGolomb)\n{\n    // start code, nal unit header\n    // 3 exponential golomb values (first_mb_in_slice, slice_type, pic_parameter_set_id)\n    // then slice payloads\n    constexpr std::string_view kH264SliceHex = \"0000000161e0fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264SliceHex);\n\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 6u);\n}\n\nTEST_F(DaveTests, H264ShortIDROneByteExpGolomb)\n{\n    // SPS NAL UNIT, PPS NAL UNIT, then IDR NAL Unit\n    // for IDR: nal unit header, then 3 exponential golomb values (first_mb_in_slice, slice_type,\n    // pic_parameter_set_id) then IDR payloads\n    constexpr std::string_view kH264ShortIDR =\n      \"000000016742c00d8c8d40d0fbc900f08846a00000000168ce3c800000000165b8fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264ShortIDR);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 33u);\n}\n\nTEST_F(DaveTests, H264ShortIDRTwoByteExpGolomb)\n{\n    // SPS NAL UNIT, PPS NAL UNIT, then IDR NAL Unit\n    // for IDR: nal unit header, then 3 exponential golomb values (first_mb_in_slice, slice_type,\n    // pic_parameter_set_id) then IDR payloads\n    constexpr std::string_view kH264ShortIDR =\n      \"000000016742c00d8c8d40d0fbc900f08846a00000000168ce3c8000000001654760fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264ShortIDR);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 34u);\n}\n\nTEST_F(DaveTests, H264LongIDROneByteExpGolomb)\n{\n    // SPS NAL UNIT, PPS NAL UNIT, SEI NAL unit, then IDR NAL Unit\n    // which has nal unit header,\n    // then 3 exponential golomb values (first_mb_in_slice, slice_type, pic_parameter_set_id)\n    // then IDR payloads\n    constexpr std::string_view kH264LongIDR =\n      \"00000001274d0033ab402802dd00da08846a000000000128ee3c800000000106051a47564adc5c4c433f94efc511\"\n      \"3cd143a801ffccccff020004ca90800000000125b8fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264LongIDR);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 67u);\n}\n\nTEST_F(DaveTests, H264LongIDRTwoByteExpGolomb)\n{\n    // SPS NAL UNIT, PPS NAL UNIT, SEI NAL unit, then IDR NAL Unit\n    // which has nal unit header, then 3 exponential golomb values\n    // (first_mb_in_slice, slice_type, pic_parameter_set_id) then IDR payloads\n    constexpr std::string_view kH264LongIDR =\n      \"00000001274d0033ab402802dd00da08846a000000000128ee3c800000000106051a47564adc5c4c433f94efc5\"\n      \"11\"\n      \"3cd143a801ffccccff020004ca908000000001254760fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264LongIDR);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 68u);\n}\n\nTEST_F(DaveTests, H264EmulationPreventionInEarlyExpGolomb)\n{\n    constexpr std::string_view kH264SliceHex = \"00000001610000038000e0fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264SliceHex);\n\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 11u);\n}\n\nTEST_F(DaveTests, H264ThreeByteShortCodeExtension)\n{\n    constexpr std::string_view kH264MixedShortCodes =\n      \"000000012764001fac2b602802dd8088000003000800000301b46d0e1970\"\n      \"00000128ee3cb0000001258880ababab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264MixedShortCodes);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 45u);\n\n    auto bytesToEncrypt = frameProcessor.GetEncryptedBytes();\n    auto encryptedBytes = frameProcessor.GetCiphertextBytes();\n    EXPECT_EQ(bytesToEncrypt.size(), encryptedBytes.size());\n    memcpy(encryptedFrame.get(), bytesToEncrypt.data(), bytesToEncrypt.size());\n\n    frameProcessor.ReconstructFrame(MakeArrayView<uint8_t>(\n      encryptedFrame.get(), bytesToEncrypt.size() + frameProcessor.GetUnencryptedBytes().size()));\n\n    constexpr std::string_view kExpectedUnencryptedHeaderHex =\n      \"000000012764001fac2b602802dd8088000003000800000301b46d0e19700000000128ee3cb000000001258880\";\n    auto expectedUnencryptedHeader = GetBufferFromHex(kExpectedUnencryptedHeaderHex);\n\n    auto compareResultExpected = memcmp(\n      encryptedFrame.get(), expectedUnencryptedHeader.data(), expectedUnencryptedHeader.size());\n\n    EXPECT_EQ(compareResultExpected, 0);\n}\n\nTEST_F(DaveTests, H264TwoSliceTest)\n{\n    // start code, nal unit header\n    // 3 exponential golomb values (first_mb_in_slice, slice_type, pic_parameter_set_id)\n    // then slice payload\n    // and repeated again\n    constexpr std::string_view kH264TwoSliceHex = \"0000000161e0fafafa0000000161e0fafafa\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH264TwoSliceHex);\n\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H264);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 2u);\n    EXPECT_EQ(unencryptedRanges[0].offset, 0u);\n    EXPECT_EQ(unencryptedRanges[0].size, 6u);\n    EXPECT_EQ(unencryptedRanges[1].offset, 9u);\n    EXPECT_EQ(unencryptedRanges[1].size, 6u);\n}\n\nTEST_F(DaveTests, H265IdrSlice)\n{\n    constexpr std::string_view kH265IdrSliceHex =\n      \"0000000140010c01ffff016000000300b0000003000003005d17024\"\n      \"000000001420101016000000300b0000003000003005da00280802d16205ee45914bff2e7f13fa2\"\n      \"000000014401c072f05324000000014e01051a47564adc5c4c433f94efc5113cd143a803ee0000ee02001fc8b88\"\n      \"0000000012801abab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH265IdrSliceHex);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H265);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 119u);\n}\n\nTEST_F(DaveTests, H265TsaSlice)\n{\n    constexpr std::string_view kH265TsaSliceHex = \"000000010201abab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH265TsaSliceHex);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H265);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 6u);\n}\n\nTEST_F(DaveTests, H265SimpleThreeByteCodeExtension)\n{\n    constexpr std::string_view kH265TsaSliceHexShort = \"0000010201abab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH265TsaSliceHexShort);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H265);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 6u);\n}\n\nTEST_F(DaveTests, H265MultipleThreeByteCodeExtensions)\n{\n    constexpr std::string_view kH265IdrSliceHex =\n      \"00000140010c01ffff016000000300b0000003000003005d17024\"\n      \"0000001420101016000000300b0000003000003005da00280802d16205ee45914bff2e7f13fa2\"\n      \"000000014401c072f05324000000014e01051a47564adc5c4c433f94efc5113cd143a803ee0000ee02001fc8b88\"\n      \"00000012801abab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH265IdrSliceHex);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H265);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 1u);\n    EXPECT_EQ(unencryptedRanges.front().offset, 0u);\n    EXPECT_EQ(unencryptedRanges.front().size, 119u);\n}\n\nTEST_F(DaveTests, H265TwoIdrSlice)\n{\n    constexpr std::string_view kH265TwoIdrSliceHex = \"0000010201abab0000010201abab\";\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(kH265TwoIdrSliceHex);\n    auto encryptedFrame = std::make_unique<uint8_t[]>(incomingFrame.size() * 2);\n\n    OutboundFrameProcessor frameProcessor;\n    frameProcessor.ProcessFrame(\n      MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size()), Codec::H265);\n\n    auto unencryptedRanges = frameProcessor.GetUnencryptedRanges();\n\n    EXPECT_EQ(unencryptedRanges.size(), 2u);\n    EXPECT_EQ(unencryptedRanges[0].offset, 0u);\n    EXPECT_EQ(unencryptedRanges[0].size, 6u);\n    EXPECT_EQ(unencryptedRanges[1].offset, 8u);\n    EXPECT_EQ(unencryptedRanges[1].size, 6u);\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/cryptor_manager_tests.cpp",
    "content": "#include <gmock/gmock.h>\n#include <gtest/gtest.h>\n\n#include <bytes/bytes.h>\n#include <limits>\n\n#include \"common.h\"\n#include \"cryptor_manager.h\"\n#include \"utils/clock.h\"\n\n#include \"dave_test.h\"\n#include \"static_key_ratchet.h\"\n\nusing namespace testing;\nusing namespace std::chrono_literals;\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\n// Gap can't be larger than the amount of bits allocated for it if we want to handle wraparound\n// correctly\nstatic_assert(kMaxGenerationGap < kGenerationWrap, \"Gap can't be larger than wraparound value\");\n\nclass MockKeyRatchet : public IKeyRatchet {\npublic:\n    MockKeyRatchet()\n    {\n        ON_CALL(*this, GetKey).WillByDefault([](KeyGeneration generation) {\n            auto userId = std::string(\"12345678901234567890\");\n            return MakeStaticSenderKey(userId + std::to_string(generation));\n        });\n    }\n    MOCK_METHOD(EncryptionKey, GetKey, (KeyGeneration generation), (override, noexcept));\n    MOCK_METHOD(void, DeleteKey, (KeyGeneration generation), (override, noexcept));\n};\n\nclass MockClock : public IClock {\npublic:\n    TimePoint Now() const override { return now_; }\n\n    void SetNow(TimePoint now) { now_ = now; }\n    void Advance(Duration duration) { now_ += duration; }\n\nprivate:\n    TimePoint now_{std::chrono::steady_clock::now()};\n};\n\nTEST_F(DaveTests, CryptorManagerCheckMaxGap)\n{\n    auto mockKeyRatchet = std::make_unique<MockKeyRatchet>();\n    EXPECT_CALL(*mockKeyRatchet, GetKey(0));\n    EXPECT_CALL(*mockKeyRatchet, GetKey(kMaxGenerationGap));\n    EXPECT_CALL(*mockKeyRatchet, GetKey(kMaxGenerationGap + 1));\n\n    MockClock clock;\n    CryptorManager cryptorManager{clock, std::move(mockKeyRatchet)};\n    // Give plenty of room to not trigger the max lifetime generations check\n    clock.Advance(kMaxGenerationGap * 48h);\n\n    auto cryptor = cryptorManager.GetCryptor(0);\n    EXPECT_NE(cryptor, nullptr);\n    EXPECT_EQ(cryptorManager.GetCryptor(0), cryptor);\n    EXPECT_NE(cryptorManager.GetCryptor(kMaxGenerationGap), nullptr);\n    EXPECT_EQ(cryptorManager.GetCryptor(kMaxGenerationGap + 1), nullptr);\n    cryptorManager.ReportCryptorSuccess(\n      kMaxGenerationGap,\n      static_cast<TruncatedSyncNonce>(kMaxGenerationGap << kRatchetGenerationShiftBits));\n    EXPECT_NE(cryptorManager.GetCryptor(kMaxGenerationGap + 1), nullptr);\n}\n\nTEST_F(DaveTests, CryptorManagerCheckExpiry)\n{\n    auto mockKeyRatchet = std::make_unique<MockKeyRatchet>();\n    EXPECT_CALL(*mockKeyRatchet, GetKey(0));\n    EXPECT_CALL(*mockKeyRatchet, GetKey(1));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(0));\n\n    MockClock clock;\n    CryptorManager cryptorManager{clock, std::move(mockKeyRatchet)};\n    EXPECT_NE(cryptorManager.GetCryptor(0), nullptr);\n    clock.Advance(1000000h);\n    EXPECT_NE(cryptorManager.GetCryptor(0), nullptr);\n    EXPECT_NE(cryptorManager.GetCryptor(1), nullptr);\n    cryptorManager.ReportCryptorSuccess(1, 1 << kRatchetGenerationShiftBits);\n    clock.Advance(kCryptorExpiry - 1us);\n    EXPECT_NE(cryptorManager.GetCryptor(0), nullptr);\n    clock.Advance(2us);\n    EXPECT_EQ(cryptorManager.GetCryptor(0), nullptr);\n}\n\nTEST_F(DaveTests, CryptorManagerDeleteOldKeys)\n{\n    auto mockKeyRatchet = std::make_unique<MockKeyRatchet>();\n    EXPECT_CALL(*mockKeyRatchet, GetKey(0));\n    EXPECT_CALL(*mockKeyRatchet, GetKey(5));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(0));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(1));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(2));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(3));\n    EXPECT_CALL(*mockKeyRatchet, DeleteKey(4));\n\n    MockClock clock;\n    CryptorManager cryptorManager{clock, std::move(mockKeyRatchet)};\n    // Give plenty of room to not trigger the max lifetime generations check\n    clock.Advance(kMaxGenerationGap * 48h);\n\n    EXPECT_NE(cryptorManager.GetCryptor(0), nullptr);\n    EXPECT_NE(cryptorManager.GetCryptor(5), nullptr);\n    cryptorManager.ReportCryptorSuccess(5, 5 << kRatchetGenerationShiftBits);\n    clock.Advance(kCryptorExpiry + 1us);\n    EXPECT_NE(cryptorManager.GetCryptor(5), nullptr);\n}\n\nTEST_F(DaveTests, CryptorManagerGenerationWrap)\n{\n    EXPECT_EQ(ComputeWrappedGeneration(0, 0), KeyGeneration{0});\n    EXPECT_EQ(ComputeWrappedGeneration(0, 1), KeyGeneration{1});\n    EXPECT_EQ(ComputeWrappedGeneration(0, 250), KeyGeneration{250});\n\n    EXPECT_EQ(ComputeWrappedGeneration(11 * kGenerationWrap + 42, 42),\n              KeyGeneration{11 * kGenerationWrap + 42});\n    EXPECT_EQ(ComputeWrappedGeneration(11 * kGenerationWrap + 42, 50),\n              KeyGeneration{11 * kGenerationWrap + 50});\n    EXPECT_EQ(ComputeWrappedGeneration(11 * kGenerationWrap + 42, 10),\n              KeyGeneration{12 * kGenerationWrap + 10});\n}\n\nTEST_F(DaveTests, CryptorManagerBigNonce)\n{\n    EXPECT_EQ(ComputeWrappedBigNonce(0, 0), 0u);\n    EXPECT_EQ(ComputeWrappedBigNonce(0, 1), 1u);\n    EXPECT_EQ(ComputeWrappedBigNonce(0, 250), 250u);\n\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 10), 11 << kRatchetGenerationShiftBits | 10u);\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 42), 11 << kRatchetGenerationShiftBits | 42u);\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 50), 11 << kRatchetGenerationShiftBits | 50u);\n\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 2 << kRatchetGenerationShiftBits | 34),\n              11 << kRatchetGenerationShiftBits | 34u);\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 37 << kRatchetGenerationShiftBits | 139),\n              11 << kRatchetGenerationShiftBits | 139u);\n    EXPECT_EQ(ComputeWrappedBigNonce(11, 89 << kRatchetGenerationShiftBits | 294),\n              11 << kRatchetGenerationShiftBits | 294u);\n}\n\nTEST_F(DaveTests, CryptorManagerNoReprocess)\n{\n    auto mockKeyRatchet = std::make_unique<MockKeyRatchet>();\n    EXPECT_CALL(*mockKeyRatchet, GetKey(0));\n\n    MockClock clock;\n    CryptorManager cryptorManager{clock, std::move(mockKeyRatchet)};\n    // Give plenty of room to not trigger the max lifetime generations check\n    clock.Advance(kMaxGenerationGap * 48h);\n\n    auto cryptor = cryptorManager.GetCryptor(0);\n    EXPECT_NE(cryptor, nullptr);\n\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 0));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 1));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 2));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 3));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, std::numeric_limits<TruncatedSyncNonce>::max()));\n    cryptorManager.ReportCryptorSuccess(0, 0);\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 0));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 1));\n    cryptorManager.ReportCryptorSuccess(0, 1);\n    cryptorManager.ReportCryptorSuccess(0, 2);\n    cryptorManager.ReportCryptorSuccess(0, 5);\n    cryptorManager.ReportCryptorSuccess(0, 7);\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 0));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 1));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 2));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 5));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 7));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 3));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 4));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 6));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 8));\n    cryptorManager.ReportCryptorSuccess(0, 4);\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 3));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 4));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 6));\n    cryptorManager.ReportCryptorSuccess(0, 6);\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 3));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 6));\n    cryptorManager.ReportCryptorSuccess(0, 10 + kMaxMissingNonces);\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 3));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 7));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 8));\n    EXPECT_FALSE(cryptorManager.CanProcessNonce(0, 9));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 10));\n    EXPECT_TRUE(cryptorManager.CanProcessNonce(0, 11));\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/cryptor_tests.cpp",
    "content": "#include <gtest/gtest.h>\n\n#include \"decryptor.h\"\n#include \"encryptor.h\"\n#include \"frame_processors.h\"\n\n#include \"dave_test.h\"\n#include \"static_key_ratchet.h\"\n\nusing namespace testing;\nusing namespace std::chrono_literals;\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nconstexpr std::string_view RandomBytes =\n  \"0dc5aedd5bdc3f20be5697e54dd1f437b896a36f858c6f20bbd69e2a493ca170c4f0c1b9acd4\"\n  \"9d324b92afa788d09b12b29115a2feb3552b60fff983234a6c9608af3933683efc6b0f5579a9\";\n\nTEST_F(DaveTests, PassthroughInOutBuffer)\n{\n    auto incomingFrame = GetBufferFromHex(RandomBytes);\n    auto frameCopy = incomingFrame;\n\n    auto frameViewIn = MakeArrayView<const uint8_t>(incomingFrame.data(), incomingFrame.size());\n    auto frameViewOut = MakeArrayView<uint8_t>(incomingFrame.data(), incomingFrame.size());\n\n    EXPECT_NE(incomingFrame.data(), frameCopy.data());\n\n    Encryptor encryptor;\n    encryptor.AssignSsrcToCodec(0, Codec::Opus);\n    encryptor.SetPassthroughMode(true);\n\n    size_t bytesWritten = 0;\n    auto encryptResult =\n      encryptor.Encrypt(MediaType::Audio, 0, frameViewIn, frameViewOut, &bytesWritten);\n\n    EXPECT_EQ(encryptResult, 0);\n    EXPECT_EQ(bytesWritten, frameCopy.size());\n    EXPECT_EQ(memcmp(incomingFrame.data(), frameCopy.data(), bytesWritten), 0);\n\n    Decryptor decryptor;\n    decryptor.TransitionToPassthroughMode(true, 0s);\n\n    bytesWritten = 0;\n    auto decryptResult =\n      decryptor.Decrypt(MediaType::Audio, frameViewIn, frameViewOut, &bytesWritten);\n\n    EXPECT_EQ(decryptResult, Decryptor::ResultCode::Success);\n    EXPECT_EQ(bytesWritten, frameCopy.size());\n    EXPECT_EQ(memcmp(incomingFrame.data(), frameCopy.data(), bytesWritten), 0);\n}\n\nTEST_F(DaveTests, PassthroughTwoBuffers)\n{\n    auto incomingFrame = GetBufferFromHex(RandomBytes);\n    auto encryptedFrame = std::vector<uint8_t>(incomingFrame.size() * 2);\n    auto decryptedFrame = std::vector<uint8_t>(incomingFrame.size());\n\n    Encryptor encryptor;\n    encryptor.AssignSsrcToCodec(0, Codec::Opus);\n    encryptor.SetPassthroughMode(true);\n\n    size_t bytesWritten = 0;\n    auto encryptResult = encryptor.Encrypt(MediaType::Audio,\n                                           0,\n                                           {incomingFrame.data(), incomingFrame.size()},\n                                           {encryptedFrame.data(), encryptedFrame.size()},\n                                           &bytesWritten);\n\n    EXPECT_EQ(encryptResult, 0);\n    EXPECT_EQ(bytesWritten, incomingFrame.size());\n    EXPECT_EQ(memcmp(incomingFrame.data(), encryptedFrame.data(), bytesWritten), 0);\n\n    Decryptor decryptor;\n    decryptor.TransitionToPassthroughMode(true, 0s);\n\n    size_t bytesDecrypted = 0;\n    auto decryptResult = decryptor.Decrypt(MediaType::Audio,\n                                           {encryptedFrame.data(), bytesWritten},\n                                           {decryptedFrame.data(), decryptedFrame.size()},\n                                           &bytesDecrypted);\n\n    EXPECT_EQ(decryptResult, Decryptor::ResultCode::Success);\n    EXPECT_EQ(bytesDecrypted, incomingFrame.size());\n    EXPECT_EQ(memcmp(encryptedFrame.data(), decryptedFrame.data(), decryptResult), 0);\n}\n\nTEST_F(DaveTests, SilencePacketPassthrough)\n{\n    const std::vector<uint8_t> WorkerSilencePacket = {248, 255, 254};\n\n    Decryptor decryptor;\n    decryptor.TransitionToKeyRatchet(std::make_unique<StaticKeyRatchet>(\"0123456789876543210\"), 0s);\n\n    auto decryptedFrame = std::vector<uint8_t>(WorkerSilencePacket.size());\n    size_t bytesWritten = 0;\n    auto decryptResult = decryptor.Decrypt(MediaType::Audio,\n                                           {WorkerSilencePacket.data(), WorkerSilencePacket.size()},\n                                           {decryptedFrame.data(), decryptedFrame.size()},\n                                           &bytesWritten);\n\n    EXPECT_EQ(decryptResult, Decryptor::ResultCode::Success);\n    EXPECT_EQ(bytesWritten, WorkerSilencePacket.size());\n    EXPECT_EQ(memcmp(WorkerSilencePacket.data(), decryptedFrame.data(), decryptResult), 0);\n}\n\nTEST_F(DaveTests, RandomOpusFrameEncryptDecrypt)\n{\n    Encryptor encryptor;\n    Decryptor decryptor;\n\n    // set static key ratchet for testing\n    encryptor.SetKeyRatchet(std::make_unique<StaticKeyRatchet>(\"0123456789876543210\"));\n    decryptor.TransitionToKeyRatchet(std::make_unique<StaticKeyRatchet>(\"0123456789876543210\"), 0s);\n\n    // load the hex encoded sample frame to a buffer\n    auto incomingFrame = GetBufferFromHex(RandomBytes);\n    auto encryptedFrame = std::vector<uint8_t>(incomingFrame.size() * 2);\n    auto decryptedFrame = std::vector<uint8_t>(incomingFrame.size());\n\n    for (size_t i = 0; i < 1; i++) {\n        // encrypt frame\n        size_t bytesWritten = 0;\n        encryptor.AssignSsrcToCodec(0, Codec::Opus);\n        auto encryptResult = encryptor.Encrypt(MediaType::Audio,\n                                               0,\n                                               {incomingFrame.data(), incomingFrame.size()},\n                                               {encryptedFrame.data(), encryptedFrame.size()},\n                                               &bytesWritten);\n\n        EXPECT_EQ(encryptResult, 0);\n        EXPECT_GE(bytesWritten, incomingFrame.size());\n\n        // decrypt frame\n        size_t bytesDecrypted = 0;\n        auto decryptResult = decryptor.Decrypt(MediaType::Audio,\n                                               {encryptedFrame.data(), bytesWritten},\n                                               {decryptedFrame.data(), decryptedFrame.size()},\n                                               &bytesDecrypted);\n        EXPECT_EQ(decryptResult, Decryptor::ResultCode::Success);\n        EXPECT_EQ(bytesDecrypted, incomingFrame.size());\n        EXPECT_EQ(memcmp(incomingFrame.data(), decryptedFrame.data(), incomingFrame.size()), 0);\n    }\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/dave_test.cpp",
    "content": "#include \"dave_test.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nstd::vector<uint8_t> GetBufferFromHex(const std::string_view& hex)\n{\n    auto hexLength = hex.length();\n\n    if (hexLength % 2 != 0) {\n        return {};\n    }\n\n    auto buffer = std::vector<uint8_t>(hexLength / 2);\n\n    for (unsigned int i = 0; i < hexLength; i += 2) {\n        auto byte = std::string(hex.substr(i, 2));\n        buffer[i / 2] = static_cast<uint8_t>(std::stoi(byte, nullptr, 16));\n    }\n\n    return buffer;\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/dave_test.h",
    "content": "\n#include \"gtest/gtest.h\"\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nstd::vector<uint8_t> GetBufferFromHex(const std::string_view& hex);\n\nclass DaveTests : public ::testing::Test {\npublic:\n    void SetUp() override {}\n\n    void TearDown() override {}\n};\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/external_sender.cpp",
    "content": "#include \"external_sender.h\"\n\n#include \"mls/parameters.h\"\n#include \"mls/util.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\n::mlspp::bytes_ns::bytes BigEndianBytesFrom(uint64_t value) noexcept\n{\n    auto buffer = ::mlspp::bytes_ns::bytes();\n    buffer.reserve(sizeof(value));\n\n    for (int i = sizeof(value) - 1; i >= 0; --i) {\n        buffer.push_back(static_cast<uint8_t>(value >> (i * 8)));\n    }\n\n    return buffer;\n}\n\n::mlspp::CipherSuite CiphersuiteForProtocolVersion(\n  [[maybe_unused]] ProtocolVersion version) noexcept\n{\n    return ::mlspp::CipherSuite{::mlspp::CipherSuite::ID::P256_AES128GCM_SHA256_P256};\n}\n\nExternalSender::ExternalSender(discord::dave::ProtocolVersion protocolVersion, uint64_t groupId)\n{\n    ciphersuite_ = CiphersuiteForProtocolVersion(protocolVersion);\n    groupId_ = std::move(BigEndianBytesFrom(groupId).as_vec());\n    signaturePrivateKey_ = std::make_shared<::mlspp::SignaturePrivateKey>(\n      ::mlspp::SignaturePrivateKey::generate(ciphersuite_));\n    externalSender_.signature_key = signaturePrivateKey_->public_key;\n    externalSender_.credential = ::mlspp::Credential::basic({0x00, 0x01, 0x01, 0x00});\n}\n\nstd::vector<uint8_t> ExternalSender::GetMarshalledExternalSender()\n{\n    return ::mlspp::tls::marshal(externalSender_);\n}\n\nstd::vector<uint8_t> ExternalSender::ProposeAdd(uint32_t epoch,\n                                                std::vector<uint8_t> const& keyPackage)\n{\n    const auto keyPackageBytes = ::mlspp::bytes_ns::bytes(keyPackage);\n    auto proposal =\n      ::mlspp::Proposal{::mlspp::Add{{::mlspp::tls::get<::mlspp::KeyPackage>(keyPackageBytes)}}};\n    auto message = ::mlspp::external_proposal(\n      ciphersuite_, groupId_, epoch, proposal, signerIndex_, *signaturePrivateKey_);\n\n    bool isRevoke = false;\n    ::mlspp::tls::ostream out;\n    out << isRevoke;\n    out << std::vector<::mlspp::MLSMessage>{message};\n    return out.bytes();\n}\n\nstd::pair<std::vector<uint8_t>, std::vector<uint8_t>> ExternalSender::SplitCommitWelcome(\n  std::vector<uint8_t> const& commitWelcome)\n{\n    auto commitWelcomeBytes = ::mlspp::bytes_ns::bytes(commitWelcome);\n    ::mlspp::tls::istream in(commitWelcomeBytes);\n    ::mlspp::MLSMessage commitMessage;\n    ::mlspp::Welcome welcomeMessage;\n    in >> commitMessage;\n    in >> welcomeMessage;\n\n    ::mlspp::tls::ostream commitOut;\n    commitOut << commitMessage;\n    auto commitBytes = commitOut.bytes();\n    ::mlspp::tls::ostream welcomeOut;\n    welcomeOut << welcomeMessage;\n    auto welcomeBytes = welcomeOut.bytes();\n    return std::make_pair(commitBytes, welcomeBytes);\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord"
  },
  {
    "path": "cpp/test/external_sender.h",
    "content": "#pragma once\n\n#include <dave/version.h>\n#include <mls/messages.h>\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nclass ExternalSender {\npublic:\n    ExternalSender(discord::dave::ProtocolVersion protocolVersion, uint64_t groupId);\n\n    std::vector<uint8_t> GetMarshalledExternalSender();\n    std::vector<uint8_t> ProposeAdd(uint32_t epoch, std::vector<uint8_t> const& keyPackage);\n    std::pair<std::vector<uint8_t>, std::vector<uint8_t>> SplitCommitWelcome(\n      std::vector<uint8_t> const& commitWelcome);\n\nprivate:\n    uint32_t signerIndex_{0};\n    ::mlspp::CipherSuite ciphersuite_;\n    ::mlspp::bytes_ns::bytes groupId_;\n    std::shared_ptr<::mlspp::SignaturePrivateKey> signaturePrivateKey_;\n    ::mlspp::ExternalSender externalSender_;\n};\n\n} // namespace test\n} // namespace dave\n} // namespace discord"
  },
  {
    "path": "cpp/test/static_key_ratchet.cpp",
    "content": "#include \"static_key_ratchet.h\"\n\n#include <algorithm>\n\n#include <bytes/bytes.h>\n#include <dave/logger.h>\n\n#include \"common.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nEncryptionKey MakeStaticSenderKey(const std::string& userID)\n{\n    auto u64userID = strtoull(userID.c_str(), nullptr, 10);\n    return MakeStaticSenderKey(u64userID);\n}\n\nEncryptionKey MakeStaticSenderKey(uint64_t u64userID)\n{\n    static_assert(kAesGcm128KeyBytes == 2 * sizeof(u64userID));\n    EncryptionKey senderKey(kAesGcm128KeyBytes);\n    const uint8_t* bytePtr = reinterpret_cast<const uint8_t*>(&u64userID);\n    std::copy_n(bytePtr, sizeof(u64userID), senderKey.begin());\n    std::copy_n(bytePtr, sizeof(u64userID), senderKey.begin() + sizeof(u64userID));\n    return senderKey;\n}\n\nStaticKeyRatchet::StaticKeyRatchet(const std::string& userId) noexcept\n  : u64userID_(strtoull(userId.c_str(), nullptr, 10))\n{\n}\n\nEncryptionKey StaticKeyRatchet::GetKey(KeyGeneration generation) noexcept\n{\n    DISCORD_LOG(LS_INFO) << \"Retrieving static key for generation \" << generation << \" for user \"\n                         << u64userID_;\n    return MakeStaticSenderKey(u64userID_);\n}\n\nvoid StaticKeyRatchet::DeleteKey([[maybe_unused]] KeyGeneration generation) noexcept\n{\n    // noop\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/static_key_ratchet.h",
    "content": "#pragma once\n\n#include <string>\n\n#include <dave/dave_interfaces.h>\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\nEncryptionKey MakeStaticSenderKey(const std::string& userID);\nEncryptionKey MakeStaticSenderKey(uint64_t u64userID);\n\nclass StaticKeyRatchet : public IKeyRatchet {\npublic:\n    StaticKeyRatchet(const std::string& userId) noexcept;\n    ~StaticKeyRatchet() noexcept override = default;\n\n    EncryptionKey GetKey(KeyGeneration generation) noexcept override;\n    void DeleteKey(KeyGeneration generation) noexcept override;\n\nprivate:\n    uint64_t u64userID_;\n};\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/test/xssl_cryptor_tests.cpp",
    "content": "#include \"gtest/gtest.h\"\n\n#include <bytes/bytes.h>\n\n#ifdef WITH_BORINGSSL\n#include \"boringssl_cryptor.h\"\n#else\n#include \"openssl_cryptor.h\"\n#endif\n\n#include \"dave_test.h\"\n#include \"static_key_ratchet.h\"\n\nnamespace discord {\nnamespace dave {\nnamespace test {\n\n#ifdef WITH_BORINGSSL\nusing CryptorVariant = BoringSSLCryptor;\n#else\nusing CryptorVariant = OpenSSLCryptor;\n#endif\n\nTEST_F(DaveTests, XSSLEncryptDecrypt)\n{\n    constexpr size_t PLAINTEXT_SIZE = 1024;\n    auto plaintextBufferIn = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto plaintextBufferOut = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto nonceBuffer = std::vector<uint8_t>(kAesGcm128NonceBytes, 0);\n    auto tagBuffer = std::vector<uint8_t>(kAesGcm128TruncatedTagBytes, 0);\n\n    auto plaintextIn =\n      MakeArrayView<const uint8_t>(plaintextBufferIn.data(), plaintextBufferIn.size());\n    auto additionalData =\n      MakeArrayView<const uint8_t>(additionalDataBuffer.data(), additionalDataBuffer.size());\n    auto plaintextOut =\n      MakeArrayView<uint8_t>(plaintextBufferOut.data(), plaintextBufferOut.size());\n    auto ciphertextOut = MakeArrayView<uint8_t>(ciphertextBuffer.data(), ciphertextBuffer.size());\n    auto ciphertextIn =\n      MakeArrayView<const uint8_t>(ciphertextBuffer.data(), ciphertextBuffer.size());\n    auto nonce = MakeArrayView<const uint8_t>(nonceBuffer.data(), nonceBuffer.size());\n    auto tagOut = MakeArrayView<uint8_t>(tagBuffer.data(), tagBuffer.size());\n    auto tagIn = MakeArrayView<const uint8_t>(tagBuffer.data(), tagBuffer.size());\n\n    CryptorVariant cryptor(MakeStaticSenderKey(\"12345678901234567890\"));\n\n    EXPECT_TRUE(cryptor.Encrypt(ciphertextOut, plaintextIn, nonce, additionalData, tagOut));\n\n    // The ciphertext should not be the same as the plaintext\n    EXPECT_FALSE(memcmp(plaintextBufferIn.data(), ciphertextBuffer.data(), PLAINTEXT_SIZE) == 0);\n\n    EXPECT_TRUE(cryptor.Decrypt(plaintextOut, ciphertextIn, tagIn, nonce, additionalData));\n\n    // The plaintext should be the same as the original plaintext\n    EXPECT_TRUE(memcmp(plaintextBufferIn.data(), plaintextBufferOut.data(), PLAINTEXT_SIZE) == 0);\n}\n\nTEST_F(DaveTests, XSSLAdditionalDataAuth)\n{\n    constexpr size_t PLAINTEXT_SIZE = 1024;\n    auto plaintextBufferIn = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto plaintextBufferOut = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto nonceBuffer = std::vector<uint8_t>(kAesGcm128NonceBytes, 0);\n    auto tagBuffer = std::vector<uint8_t>(kAesGcm128TruncatedTagBytes, 0);\n\n    auto plaintextIn =\n      MakeArrayView<const uint8_t>(plaintextBufferIn.data(), plaintextBufferIn.size());\n    auto additionalData =\n      MakeArrayView<const uint8_t>(additionalDataBuffer.data(), additionalDataBuffer.size());\n    auto plaintextOut =\n      MakeArrayView<uint8_t>(plaintextBufferOut.data(), plaintextBufferOut.size());\n    auto ciphertextOut = MakeArrayView<uint8_t>(ciphertextBuffer.data(), ciphertextBuffer.size());\n    auto ciphertextIn =\n      MakeArrayView<const uint8_t>(ciphertextBuffer.data(), ciphertextBuffer.size());\n    auto nonce = MakeArrayView<const uint8_t>(nonceBuffer.data(), nonceBuffer.size());\n    auto tagOut = MakeArrayView<uint8_t>(tagBuffer.data(), tagBuffer.size());\n    auto tagIn = MakeArrayView<const uint8_t>(tagBuffer.data(), tagBuffer.size());\n\n    CryptorVariant cryptor(MakeStaticSenderKey(\"12345678901234567890\"));\n\n    EXPECT_TRUE(cryptor.Encrypt(ciphertextOut, plaintextIn, nonce, additionalData, tagOut));\n\n    // We modify the additional data before decryption\n    additionalDataBuffer[0] = 1;\n\n    EXPECT_FALSE(cryptor.Decrypt(plaintextOut, ciphertextIn, tagIn, nonce, additionalData));\n}\n\nTEST_F(DaveTests, XSSLKeyDiff)\n{\n    constexpr size_t PLAINTEXT_SIZE = 1024;\n    auto plaintextBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto plaintextBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto nonceBuffer = std::vector<uint8_t>(kAesGcm128NonceBytes, 0);\n    auto tagBuffer = std::vector<uint8_t>(kAesGcm128TruncatedTagBytes, 0);\n\n    auto plaintext1 =\n      MakeArrayView<const uint8_t>(plaintextBuffer1.data(), plaintextBuffer1.size());\n    auto additionalData1 =\n      MakeArrayView<const uint8_t>(additionalDataBuffer1.data(), additionalDataBuffer1.size());\n    auto plaintext2 =\n      MakeArrayView<const uint8_t>(plaintextBuffer2.data(), plaintextBuffer2.size());\n    auto additionalData2 =\n      MakeArrayView<const uint8_t>(additionalDataBuffer2.data(), additionalDataBuffer2.size());\n    auto ciphertext1 = MakeArrayView<uint8_t>(ciphertextBuffer1.data(), ciphertextBuffer1.size());\n    auto ciphertext2 = MakeArrayView<uint8_t>(ciphertextBuffer2.data(), ciphertextBuffer2.size());\n    auto nonce = MakeArrayView<const uint8_t>(nonceBuffer.data(), nonceBuffer.size());\n    auto tag = MakeArrayView<uint8_t>(tagBuffer.data(), tagBuffer.size());\n\n    CryptorVariant cryptor1(MakeStaticSenderKey(\"12345678901234567890\"));\n    CryptorVariant cryptor2(MakeStaticSenderKey(\"09876543210987654321\"));\n\n    EXPECT_TRUE(cryptor1.Encrypt(ciphertext1, plaintext1, nonce, additionalData1, tag));\n    EXPECT_TRUE(cryptor2.Encrypt(ciphertext2, plaintext2, nonce, additionalData2, tag));\n\n    EXPECT_FALSE(memcmp(ciphertextBuffer1.data(), ciphertextBuffer2.data(), PLAINTEXT_SIZE) == 0);\n}\n\nTEST_F(DaveTests, XSSLNonceDiff)\n{\n    constexpr size_t PLAINTEXT_SIZE = 1024;\n    auto plaintextBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto plaintextBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto additionalDataBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer1 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto ciphertextBuffer2 = std::vector<uint8_t>(PLAINTEXT_SIZE, 0);\n    auto nonceBuffer1 = std::vector<uint8_t>(kAesGcm128NonceBytes, 0);\n    auto nonceBuffer2 = std::vector<uint8_t>(kAesGcm128NonceBytes, 1);\n    auto tagBuffer = std::vector<uint8_t>(kAesGcm128TruncatedTagBytes, 0);\n\n    auto plaintext1 =\n      MakeArrayView<const uint8_t>(plaintextBuffer1.data(), plaintextBuffer1.size());\n    auto additionalData1 =\n      MakeArrayView<const uint8_t>(additionalDataBuffer1.data(), additionalDataBuffer1.size());\n    auto plaintext2 =\n      MakeArrayView<const uint8_t>(plaintextBuffer2.data(), plaintextBuffer2.size());\n    auto additionalData2 =\n      MakeArrayView<const uint8_t>(additionalDataBuffer2.data(), additionalDataBuffer2.size());\n    auto ciphertext1 = MakeArrayView<uint8_t>(ciphertextBuffer1.data(), ciphertextBuffer1.size());\n    auto ciphertext2 = MakeArrayView<uint8_t>(ciphertextBuffer2.data(), ciphertextBuffer2.size());\n    auto nonce1 = MakeArrayView<const uint8_t>(nonceBuffer1.data(), nonceBuffer1.size());\n    auto nonce2 = MakeArrayView<const uint8_t>(nonceBuffer2.data(), nonceBuffer2.size());\n    auto tag = MakeArrayView<uint8_t>(tagBuffer.data(), tagBuffer.size());\n\n    CryptorVariant cryptor(MakeStaticSenderKey(\"12345678901234567890\"));\n\n    EXPECT_TRUE(cryptor.Encrypt(ciphertext1, plaintext1, nonce1, additionalData1, tag));\n    EXPECT_TRUE(cryptor.Encrypt(ciphertext2, plaintext2, nonce2, additionalData2, tag));\n\n    EXPECT_FALSE(memcmp(ciphertextBuffer1.data(), ciphertextBuffer2.data(), PLAINTEXT_SIZE) == 0);\n}\n\n} // namespace test\n} // namespace dave\n} // namespace discord\n"
  },
  {
    "path": "cpp/vcpkg-alts/boringssl/overlay-ports/mlspp/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO cisco/mlspp\n    REF \"${VERSION}\"\n    SHA512 5d37631e2c47daae1133ef074e60cc09ca2d395f9e11c416f829060e374051cf219d2d7fe98dae49d1d045292e07d6a09f4814a5f16e6cc05e67e7cd96f146c4\n)\n\nif(VCPKG_TARGET_IS_OSX AND EXISTS \"/usr/local/include/openssl/\")\n    set(VCPKG_INCLUDE_OVERRIDE \"-DCMAKE_CXX_FLAGS=-I${CURRENT_INSTALLED_DIR}/include\")\nendif()\n\nvcpkg_cmake_configure(\n    SOURCE_PATH \"${SOURCE_PATH}\"\n    OPTIONS \n        ${VCPKG_INCLUDE_OVERRIDE}\n        -DDISABLE_GREASE=ON \n        -DVCPKG_MANIFEST_DIR=\"alternatives/boringssl\"\n        -DMLS_CXX_NAMESPACE=\"mlspp\"\n)\n\nvcpkg_cmake_install()\n\nvcpkg_cmake_config_fixup(PACKAGE_NAME \"MLSPP\" CONFIG_PATH \"share/MLSPP\")\n"
  },
  {
    "path": "cpp/vcpkg-alts/boringssl/overlay-ports/mlspp/vcpkg.json",
    "content": "{\n  \"name\": \"mlspp\",\n  \"version-string\": \"1cc50a124a3bc4e143a787ec934280dc70c1034d\",\n  \"description\": \"Cisco MLS C++ library\",\n  \"dependencies\": [\n    {\n      \"name\": \"boringssl\",\n      \"version>=\": \"2023-10-13\"\n    },\n    \"nlohmann-json\",\n    {\n      \"name\": \"vcpkg-cmake\",\n      \"host\": true\n    },\n    {\n      \"name\": \"vcpkg-cmake-config\",\n      \"host\": true\n    }\n  ],\n  \"builtin-baseline\": \"eb33d2f7583405fca184bcdf7fdd5828ec88ac05\"\n}\n\n"
  },
  {
    "path": "cpp/vcpkg-alts/boringssl/vcpkg.json",
    "content": "{\n  \"name\": \"libdave\",\n  \"license\": \"MIT\",\n  \"dependencies\": [\n    {\n      \"name\": \"boringssl\",\n      \"version>=\": \"2023-10-13\"\n    },\n    \"gtest\",\n    \"mlspp\"\n  ],\n  \"builtin-baseline\": \"7adc2e4d49e8d0efc07a369079faa6bc3dbb90f3\",\n  \"vcpkg-configuration\": {\n    \"overlay-ports\": [\n      \"./overlay-ports\"\n    ]\n  }\n}\n"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_1.1/overlay-ports/mlspp/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO cisco/mlspp\n    REF \"${VERSION}\"\n    SHA512 5d37631e2c47daae1133ef074e60cc09ca2d395f9e11c416f829060e374051cf219d2d7fe98dae49d1d045292e07d6a09f4814a5f16e6cc05e67e7cd96f146c4\n)\n\nif(VCPKG_TARGET_IS_OSX AND EXISTS \"/usr/local/include/openssl/\")\n    set(VCPKG_INCLUDE_OVERRIDE \"-DCMAKE_CXX_FLAGS=-I${CURRENT_INSTALLED_DIR}/include\")\nendif()\n\nvcpkg_cmake_configure(\n    SOURCE_PATH \"${SOURCE_PATH}\"\n    OPTIONS\n        ${VCPKG_INCLUDE_OVERRIDE}\n        -DDISABLE_GREASE=ON \n        -DVCPKG_MANIFEST_DIR=\"alternatives/openssl_1.1\"\n        -DMLS_CXX_NAMESPACE=\"mlspp\"\n)\n\nvcpkg_cmake_install()\n\nvcpkg_cmake_config_fixup(PACKAGE_NAME \"MLSPP\" CONFIG_PATH \"share/MLSPP\")"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_1.1/overlay-ports/mlspp/vcpkg.json",
    "content": "{\n  \"name\": \"mlspp\",\n  \"version-string\": \"1cc50a124a3bc4e143a787ec934280dc70c1034d\",\n  \"description\": \"Cisco MLS C++ library\",\n  \"dependencies\": [\n    {\n      \"name\": \"openssl\",\n      \"version>=\": \"1.1.1n\"\n    },\n    \"catch2\",\n    \"nlohmann-json\",\n    {\n      \"name\": \"vcpkg-cmake\",\n      \"host\": true\n    },\n    {\n      \"name\": \"vcpkg-cmake-config\",\n      \"host\": true\n    }\n  ],\n  \"builtin-baseline\": \"eb33d2f7583405fca184bcdf7fdd5828ec88ac05\",\n  \"overrides\": [\n    {\n      \"name\": \"openssl\",\n      \"version-string\": \"1.1.1n\"\n    }\n  ]\n}"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_1.1/vcpkg.json",
    "content": "{\n  \"name\": \"libdave\",\n  \"license\": \"MIT\",\n  \"dependencies\": [\n    {\n      \"name\": \"openssl\",\n      \"version>=\": \"1.1.1n\"\n    },\n    \"gtest\",\n    \"mlspp\"\n  ],\n  \"builtin-baseline\": \"d07689ef165f033de5c0710e4f67c193a85373e1\",\n  \"vcpkg-configuration\": {\n    \"overlay-ports\": [\n      \"./overlay-ports\"\n    ]\n  },\n  \"overrides\": [\n    {\n      \"name\": \"openssl\",\n      \"version-string\": \"1.1.1n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_3/overlay-ports/mlspp/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO cisco/mlspp\n    REF \"${VERSION}\"\n    SHA512 5d37631e2c47daae1133ef074e60cc09ca2d395f9e11c416f829060e374051cf219d2d7fe98dae49d1d045292e07d6a09f4814a5f16e6cc05e67e7cd96f146c4\n)\n\nif(VCPKG_TARGET_IS_OSX AND EXISTS \"/usr/local/include/openssl/\")\n    set(VCPKG_INCLUDE_OVERRIDE \"-DCMAKE_CXX_FLAGS=-I${CURRENT_INSTALLED_DIR}/include\")\nendif()\n\nvcpkg_cmake_configure(\n    SOURCE_PATH \"${SOURCE_PATH}\"\n    OPTIONS \n        ${VCPKG_INCLUDE_OVERRIDE}\n        -DDISABLE_GREASE=ON \n        -DVCPKG_MANIFEST_DIR=\"alternatives/openssl_3\"\n        -DMLS_CXX_NAMESPACE=\"mlspp\"\n)\n\nvcpkg_cmake_install()\n\nvcpkg_cmake_config_fixup(PACKAGE_NAME \"MLSPP\" CONFIG_PATH \"share/MLSPP\")"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_3/overlay-ports/mlspp/vcpkg.json",
    "content": "{\n    \"name\": \"mlspp\",\n    \"version-string\": \"1cc50a124a3bc4e143a787ec934280dc70c1034d\",\n    \"description\": \"Cisco MLS C++ library\",\n    \"dependencies\": [\n      {\n        \"name\": \"openssl\",\n        \"version>=\": \"3.0.7\"\n      },\n      \"catch2\",\n      \"nlohmann-json\",\n      {\n        \"name\": \"vcpkg-cmake\",\n        \"host\": true\n      },\n      {\n        \"name\": \"vcpkg-cmake-config\",\n        \"host\": true\n      }\n    ],\n    \"builtin-baseline\": \"eb33d2f7583405fca184bcdf7fdd5828ec88ac05\",\n    \"overrides\": [\n      {\n        \"name\": \"openssl\",\n        \"version\": \"3.0.7\"\n      }\n    ]\n  }"
  },
  {
    "path": "cpp/vcpkg-alts/openssl_3/vcpkg.json",
    "content": "{\n  \"name\": \"libdave\",\n  \"license\": \"MIT\",\n  \"dependencies\": [\n    {\n      \"name\": \"openssl\",\n      \"version>=\": \"3.0.7\"\n    },\n    \"gtest\",\n    \"mlspp\"\n  ],\n  \"builtin-baseline\": \"d07689ef165f033de5c0710e4f67c193a85373e1\",\n  \"vcpkg-configuration\": {\n    \"overlay-ports\": [\n      \"./overlay-ports\"\n    ]\n  },\n  \"overrides\": [\n    {\n      \"name\": \"openssl\",\n      \"version\": \"3.0.7\"\n    }\n  ]\n}\n"
  },
  {
    "path": "cpp/vcpkg-alts/wasm/overlay-ports/mlspp/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO cisco/mlspp\n    REF \"${VERSION}\"\n    SHA512 5d37631e2c47daae1133ef074e60cc09ca2d395f9e11c416f829060e374051cf219d2d7fe98dae49d1d045292e07d6a09f4814a5f16e6cc05e67e7cd96f146c4\n)\n\nif(VCPKG_TARGET_IS_EMSCRIPTEN)\n    set(VCPKG_C_FLAGS \"${VCPKG_C_FLAGS} -s WASM=1\")\n    set(VCPKG_CXX_FLAGS \"${VCPKG_CXX_FLAGS} -s WASM=1\")\n    set(VCPKG_LINKER_FLAGS \"${VCPKG_LINKER_FLAGS} -s WASM=1 -s ALLOW_MEMORY_GROWTH=1\")\nendif()\n\nif(VCPKG_TARGET_IS_OSX AND EXISTS \"/usr/local/include/openssl/\")\n    set(VCPKG_INCLUDE_OVERRIDE \"-DCMAKE_CXX_FLAGS=-I${CURRENT_INSTALLED_DIR}/include\")\nendif()\n\nvcpkg_cmake_configure(\n    SOURCE_PATH \"${SOURCE_PATH}\"\n    OPTIONS \n        ${VCPKG_INCLUDE_OVERRIDE}\n        -DDISABLE_GREASE=ON \n        -DVCPKG_MANIFEST_DIR=\"alternatives/openssl_3\"\n        -DMLS_CXX_NAMESPACE=\"mlspp\"\n)\n\nvcpkg_cmake_install()\n\nvcpkg_cmake_config_fixup(PACKAGE_NAME \"MLSPP\" CONFIG_PATH \"share/MLSPP\")"
  },
  {
    "path": "cpp/vcpkg-alts/wasm/overlay-ports/mlspp/vcpkg.json",
    "content": "{\n  \"name\": \"mlspp\",\n  \"version-string\": \"1cc50a124a3bc4e143a787ec934280dc70c1034d\",\n  \"description\": \"Cisco MLS C++ library\",\n  \"dependencies\": [\n    {\n      \"name\": \"openssl\",\n      \"version>=\": \"3.0.7\"\n    },\n    \"nlohmann-json\",\n    {\n      \"name\": \"vcpkg-cmake\",\n      \"host\": true\n    },\n    {\n      \"name\": \"vcpkg-cmake-config\",\n      \"host\": true\n    }\n  ],\n  \"builtin-baseline\": \"eb33d2f7583405fca184bcdf7fdd5828ec88ac05\"\n}\n"
  },
  {
    "path": "cpp/vcpkg-alts/wasm/vcpkg.json",
    "content": "{\n  \"name\": \"libdave\",\n  \"license\": \"MIT\",\n  \"dependencies\": [\n    {\n      \"name\": \"openssl\",\n      \"version>=\": \"3.0.7\"\n    },\n    \"gtest\",\n    \"mlspp\"\n  ],\n  \"builtin-baseline\": \"7adc2e4d49e8d0efc07a369079faa6bc3dbb90f3\",\n  \"vcpkg-configuration\": {\n    \"overlay-ports\": [\n      \"./overlay-ports\"\n    ]\n  }\n}\n"
  },
  {
    "path": "js/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# Source maps\n*.wasm.map\n"
  },
  {
    "path": "js/.npmrc",
    "content": "node-linker = hoisted\n"
  },
  {
    "path": "js/README.md",
    "content": "## libdave JS\n\nContains the package @discordapp/libdave. This is leveraged by Discord clients to enable out-of-band verifications of DAVE protocol call members and the MLS epoch authenticator.\n\n### Testing\n\nTesting uses [Jest](https://jestjs.io/). You can run the tests with `pnpm jest`.\n\n### Dependencies\n\n- [@noble/hashes](https://github.com/paulmillr/noble-hashes)\n- [base64-js](https://www.npmjs.com/package/base64-js)"
  },
  {
    "path": "js/__tests__/DisplayableCode-test.ts",
    "content": "import {describe, expect, test} from '@jest/globals';\nimport {generateDisplayableCode} from '../src/DisplayableCode';\n\ndescribe('DisplayableCode', () => {\n  test('expectedOutput', () => {\n    const shortData = new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd, 0xee]);\n    expect(generateDisplayableCode(shortData, 5, 5)).toBe('05870');\n\n    const longDataBuffer = Buffer.from('aabbccddeebbccddeeffccddeeffaaddeeffaabbeeffaabbccffaabbccdd', 'hex');\n    const longData = Uint8Array.from(longDataBuffer);\n    expect(generateDisplayableCode(longData, 30, 5)).toBe('058708105556138052119572494877');\n  });\n\n  test('expectedFailure', () => {\n    const tooShortData = new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd]);\n    expect(() => {\n      generateDisplayableCode(tooShortData, 5, 5);\n    }).toThrow();\n\n    const goodData = new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd]);\n    expect(() => {\n      generateDisplayableCode(goodData, 4, 3);\n    }).toThrow();\n\n    const randomData = new Uint8Array(1024);\n    globalThis.crypto.getRandomValues(randomData);\n    expect(() => {\n      generateDisplayableCode(randomData, 1024, 11);\n    }).toThrow();\n  });\n});\n"
  },
  {
    "path": "js/__tests__/KeyFingerprint-test.ts",
    "content": "import {describe, expect, test} from '@jest/globals';\nimport {generateKeyFingerprint} from '../src/KeyFingerprint';\n\ndescribe('KeyFingerprint', () => {\n  test('expectedOutput', async () => {\n    const shortData = new Uint8Array(33);\n    expect((await generateKeyFingerprint(0, shortData, '1234')).join('')).toBe(\n      '000000000000000000000000000000000000000004210',\n    );\n\n    const longData = new Uint8Array(65);\n    expect((await generateKeyFingerprint(0, longData, '12345678')).join('')).toBe(\n      '0000000000000000000000000000000000000000000000000000000000000000000000001889778',\n    );\n  });\n\n  test('expectedFailure', async () => {\n    const data = new Uint8Array(33);\n    await expect(generateKeyFingerprint(1, data, '1234')).rejects.toThrow();\n\n    await expect(generateKeyFingerprint(0, data, 'abcd')).rejects.toThrow();\n\n    await expect(generateKeyFingerprint(0, new Uint8Array(0), '1234')).rejects.toThrow();\n  });\n});\n"
  },
  {
    "path": "js/__tests__/KeySerialization-test.ts",
    "content": "import {describe, expect, test} from '@jest/globals';\nimport {serializeKey} from '../src/KeySerialization';\n\ndescribe('KeySerialization', () => {\n  test('expectedOutput', async () => {\n    const zeroData = new Uint8Array(6);\n    expect(serializeKey(zeroData)).toBe('AAAAAAAA');\n\n    const moreData = new Uint8Array([0, 1, 0xff, 0x7f, 0x80]);\n    expect(serializeKey(moreData)).toBe('AAH/f4A=');\n  });\n});\n"
  },
  {
    "path": "js/__tests__/PairwiseFingerprint-test.ts",
    "content": "import {describe, expect, test} from '@jest/globals';\nimport {generatePairwiseFingerprint} from '../src/PairwiseFingerprint';\n\ndescribe('PairwiseFingerprint', () => {\n  test('expectedOutput', async () => {\n    const data1 = new Uint8Array(33);\n    const data2 = new Uint8Array(65);\n    expect(generatePairwiseFingerprint(0, data1, '1234', data2, '5678')).resolves.toEqual(\n      new Uint8Array([\n        133, 129, 241, 44, 36, 135, 79, 195, 27, 28, 151, 69, 124, 197, 189, 41, 192, 7, 16, 45, 79, 247, 138, 58, 126,\n        161, 178, 136, 12, 109, 96, 164, 169, 92, 2, 232, 136, 174, 74, 156, 173, 144, 191, 184, 34, 45, 242, 136, 41,\n        133, 14, 158, 119, 79, 204, 48, 6, 220, 121, 6, 242, 11, 164, 60,\n      ]),\n    );\n  });\n\n  test('badSort', async () => {\n    const data1 = new Uint8Array([0, 100]);\n    const data2 = new Uint8Array([0, 20]);\n    expect(generatePairwiseFingerprint(0, data1, '1', data2, '2')).resolves.toEqual(\n      new Uint8Array([\n        141, 169, 194, 143, 22, 72, 22, 245, 13, 140, 66, 228, 159, 195, 101, 106, 119, 240, 69, 191, 178, 227, 194,\n        126, 162, 255, 222, 148, 138, 5, 33, 215, 240, 167, 234, 245, 149, 182, 46, 20, 4, 83, 191, 31, 165, 74, 253,\n        165, 199, 16, 29, 71, 193, 205, 169, 154, 255, 154, 34, 30, 94, 171, 247, 43,\n      ]),\n    );\n  });\n\n  test('expectedFailure', async () => {\n    const data = new Uint8Array(33);\n    await expect(generatePairwiseFingerprint(1, data, '1234', data, '5678')).rejects.toThrow();\n\n    await expect(generatePairwiseFingerprint(0, data, 'abcd', data, '5678')).rejects.toThrow();\n\n    await expect(generatePairwiseFingerprint(0, new Uint8Array(0), '1234', data, '5678')).rejects.toThrow();\n  });\n});\n"
  },
  {
    "path": "js/jest-setup.js",
    "content": "const crypto = require('crypto');\n\nfunction convertAlgorithm(name) {\n  switch (name) {\n    case 'SHA-512':\n      return 'sha512';\n    default:\n      return name;\n  }\n}\n\nObject.defineProperty(globalThis, 'crypto', {\n  value: {\n    getRandomValues: (arr) => crypto.randomBytes(arr.length),\n    subtle: {\n      digest: (algorithm, data) => {\n        return crypto.hash(convertAlgorithm(algorithm), data, 'buffer').buffer;\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "js/jest.config.js",
    "content": "/**\n * For a detailed explanation regarding each configuration property, visit:\n * https://jestjs.io/docs/configuration\n */\n\n/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n  roots: ['<rootDir>/src', '<rootDir>/__tests__'],\n  setupFiles: ['<rootDir>/jest-setup.js'],\n  transform: {\n    '^.+\\\\.tsx?$': ['ts-jest', {\n      tsconfig: 'tsconfig.json',\n    }],\n  },\n};\n"
  },
  {
    "path": "js/package.json",
    "content": "{\n  \"name\": \"@discordapp/libdave\",\n  \"description\": \"Discord's DAVE library for end-to-end encryption\",\n  \"license\": \"MIT\",\n  \"version\": \"1.0.1\",\n  \"private\": true,\n  \"main\": \"src/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./src/index.ts\"\n    },\n    \"./wasm\": {\n      \"import\": \"./src/wasm.ts\"\n    }\n  },\n  \"files\": [\n    \"src/DisplayableCode.ts\",\n    \"src/index.ts\",\n    \"src/KeyFingerprint.ts\",\n    \"src/KeySerialization.ts\",\n    \"src/PairwiseFingerprint.ts\",\n    \"src/wasm.ts\",\n    \"wasm/libdave.js\",\n    \"wasm/libdave.d.ts\",\n    \"wasm/libdave.wasm\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build:watch\": \"tsc --watch\",\n    \"build\": \"tsc\",\n    \"clean\": \"rm -rf dist\",\n    \"prepublishOnly\": \"npm run clean && npm run build\"\n  },\n  \"devDependencies\": {\n    \"@jest/globals\": \"^30.2.0\",\n    \"jest\": \"^30.2.0\",\n    \"ts-jest\": \"^29.4.6\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"dependencies\": {\n    \"@noble/hashes\": \"1.5.0\",\n    \"base64-js\": \"1.5.1\"\n  },\n  \"bundledDependencies\": true\n}\n"
  },
  {
    "path": "js/src/DisplayableCode.ts",
    "content": "const MAX_GROUP_SIZE = 8;\n\nexport function generateDisplayableCode(data: Uint8Array, desiredLength: number, groupSize: number): string {\n  if (data.byteLength < desiredLength) {\n    throw new Error('data.byteLength must be greater than or equal to desiredLength');\n  }\n\n  if (desiredLength % groupSize !== 0) {\n    throw new Error('desiredLength must be a multiple of groupSize');\n  }\n\n  if (groupSize > MAX_GROUP_SIZE) {\n    throw new Error(`groupSize must be less than or equal to ${MAX_GROUP_SIZE}`);\n  }\n\n  const groupModulus = BigInt(10 ** groupSize);\n\n  let result = '';\n\n  for (let i = 0; i < desiredLength; i += groupSize) {\n    let groupValue = BigInt(0);\n\n    for (let j = groupSize; j > 0; --j) {\n      const nextByte = data[i + (groupSize - j)];\n      if (nextByte === undefined) {\n        throw new Error('Out of bounds access from data array');\n      }\n\n      groupValue = (groupValue << 8n) | BigInt(nextByte);\n    }\n\n    groupValue %= groupModulus;\n\n    result += groupValue.toString().padStart(groupSize, '0');\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "js/src/KeyFingerprint.ts",
    "content": "const VERSION_LEN = 2;\nconst UID_LEN = 8;\n\nexport async function generateKeyFingerprint(version: number, key: Uint8Array, userId: string): Promise<Uint8Array> {\n  if (version !== 0) {\n    throw new Error('unsupported fingerprint format version');\n  }\n\n  if (key.byteLength === 0) {\n    throw new Error('zero-length key');\n  }\n\n  if (userId.length === 0) {\n    throw new Error('zero-length user ID');\n  }\n\n  const userIdInt = BigInt(userId);\n  if (userIdInt < 0n || userIdInt >= 2n ** 64n) {\n    throw new Error('user ID out of range');\n  }\n\n  let lbuf = new Uint8Array(VERSION_LEN + key.byteLength + UID_LEN);\n  lbuf.set(key, VERSION_LEN);\n\n  const dv = new DataView(lbuf.buffer);\n  dv.setUint16(0, version);\n  dv.setBigUint64(VERSION_LEN + key.byteLength, userIdInt);\n\n  return lbuf;\n}\n"
  },
  {
    "path": "js/src/KeySerialization.ts",
    "content": "import base64 from 'base64-js';\n\nexport function serializeKey(data: Uint8Array): string {\n  return base64.fromByteArray(data);\n}\n"
  },
  {
    "path": "js/src/PairwiseFingerprint.ts",
    "content": "import {generateKeyFingerprint} from './KeyFingerprint';\nimport {scryptAsync} from '@noble/hashes/scrypt';\n\nconst salt = Uint8Array.of(\n  0x24,\n  0xca,\n  0xb1,\n  0x7a,\n  0x7a,\n  0xf8,\n  0xec,\n  0x2b,\n  0x82,\n  0xb4,\n  0x12,\n  0xb9,\n  0x2d,\n  0xab,\n  0x19,\n  0x2e,\n);\nconst scryptParams = {\n  N: 16384,\n  r: 8,\n  p: 2,\n  dkLen: 64,\n};\n\nfunction compareArrays(a: Uint8Array, b: Uint8Array) {\n  for (let i = 0; i < a.length && i < b.length; i++) {\n    if (a[i] != b[i]) return a[i]! - b[i]!;\n  }\n\n  return a.length - b.length;\n}\n\nexport async function generatePairwiseFingerprint(\n  version: number,\n  keyA: Uint8Array,\n  userIdA: string,\n  keyB: Uint8Array,\n  userIdB: string,\n): Promise<Uint8Array> {\n  const fingerprints = await Promise.all([\n    generateKeyFingerprint(version, keyA, userIdA),\n    generateKeyFingerprint(version, keyB, userIdB),\n  ]);\n\n  fingerprints.sort(compareArrays);\n\n  const input = new Uint8Array(fingerprints[0].byteLength + fingerprints[1].byteLength);\n  input.set(fingerprints[0], 0);\n  input.set(fingerprints[1], fingerprints[0].byteLength);\n\n  const ret = await scryptAsync(input, salt, scryptParams);\n\n  return new Uint8Array(ret);\n}\n"
  },
  {
    "path": "js/src/index.ts",
    "content": "export {generateDisplayableCode} from './DisplayableCode';\nexport {generateKeyFingerprint} from './KeyFingerprint';\nexport {generatePairwiseFingerprint} from './PairwiseFingerprint';\nexport {serializeKey} from './KeySerialization';\n"
  },
  {
    "path": "js/src/wasm.ts",
    "content": "export {default as DaveModuleFactory} from '../wasm/libdave';\nexport type {MainModule as DaveModule} from '../wasm/libdave';\nexport * from '../wasm/libdave';\n"
  },
  {
    "path": "js/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"target\": \"es2022\",\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"moduleDetection\": \"force\",\n    \"isolatedModules\": true,\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitOverride\": true,\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"outDir\": \"dist\",\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"lib\": [\"es2022\", \"dom\", \"dom.iterable\"]\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\", \"__tests__/**/*\"]\n}"
  },
  {
    "path": "js/wasm/.gitignore",
    "content": "libdave.*\n"
  },
  {
    "path": "samples/typescript/DaveSessionManager.ts",
    "content": "import type {Session, TransientKeys, DaveModule, SignaturePrivateKey} from '@discordapp/libdave/wasm';\n\nconst MLS_NEW_GROUP_EXPECTED_EPOCH = '1';\nconst DAVE_PROTOCOL_INIT_TRANSITION_ID = 0;\n\nexport class DaveSessionManager {\n  private readonly dave: DaveModule;\n  private readonly transientKeys: TransientKeys | null;\n  private readonly mlsSession: Session;\n\n  private readonly selfUserId: string;\n  private readonly groupId: string;\n  private readonly recognizedUserIds: Set<string> = new Set();\n  private readonly daveProtocolTransitions: Map<number, number> = new Map();\n  private latestPreparedTransitionVersion: number = 0;\n\n  constructor(dave: DaveModule, transientKeys: TransientKeys | null, selfUserId: string, groupId: string) {\n    this.dave = dave;\n    this.transientKeys = transientKeys;\n    this.selfUserId = selfUserId;\n    this.groupId = groupId;\n\n    // These are only used with persistent key storage and can be ignored most of the time\n    const context = '';\n    const authSessionId = '';\n\n    this.mlsSession = new dave.Session(context, authSessionId, (source: string, reason: string) => {\n      console.error(`MLS failure: ${source} ${reason}`);\n    });\n  }\n\n  // Add an allowed user to the connection\n  public createUser(userId: string) {\n    this.recognizedUserIds.add(userId);\n    this._setupKeyRatchetForUser(userId, this.latestPreparedTransitionVersion);\n  }\n\n  // Remove an allowed user from the connection\n  public destroyUser(userId: string) {\n    this.recognizedUserIds.delete(userId);\n    // TODO: Signal the relevant media code that a user has left the call and the associated Encryptor/Decryptor should be destroyed\n  }\n\n  // Incoming Voice Gateway Requests\n\n  // Opcode SELECT_PROTOCOL_ACK (1)\n  public onSelectProtocolAck(protocolVersion: number) {\n    this._handleDaveProtocolInit(protocolVersion);\n  }\n\n  // Opcode DAVE_PROTOCOL_PREPARE_TRANSITION (21)\n  public onDaveProtocolPrepareTransition(transitionId: number, protocolVersion: number) {\n    this._prepareDaveProtocolRatchets(transitionId, protocolVersion);\n    this._maybeSendDaveProtocolReadyForTransition(transitionId);\n  }\n\n  // Opcode DAVE_PROTOCOL_EXECUTE_TRANSITION (22)\n  public onDaveProtocolExecuteTransition(transitionId: number) {\n    this._handleDaveProtocolExecuteTransition(transitionId);\n  }\n\n  // Opcode DAVE_PROTOCOL_PREPARE_EPOCH (24)\n  public onDaveProtocolPrepareEpoch(epoch: string, protocolVersion: number) {\n    this._handleDaveProtocolPrepareEpoch(epoch, protocolVersion, this.groupId);\n\n    if (epoch === MLS_NEW_GROUP_EXPECTED_EPOCH) {\n      this._sendMLSKeyPackage();\n    }\n  }\n\n  // Opcode MLS_EXTERNAL_SENDER_PACKAGE (25)\n  public onDaveProtocolMLSExternalSenderPackage(externalSenderPackage: ArrayBuffer) {\n    this.mlsSession.SetExternalSender(externalSenderPackage);\n  }\n\n  // Opcode MLS_PROPOSALS (27)\n  public onMLSProposals(proposals: ArrayBuffer) {\n    const commitWelcome = this.mlsSession.ProcessProposals(proposals, this._getRecognizedUserIDs());\n    if (commitWelcome) {\n      this._sendMLSCommitWelcome(commitWelcome);\n    }\n  }\n\n  // Opcode MLS_PREPARE_COMMIT_TRANSITION (29)\n  public onMLSPrepareCommitTransition(transitionId: number, commit: ArrayBuffer) {\n    const processedCommit = this.mlsSession.ProcessCommit(commit);\n    const joinedGroup = processedCommit.rosterUpdate != null;\n\n    if (processedCommit.ignored) {\n      return;\n    }\n\n    if (joinedGroup) {\n      this._prepareDaveProtocolRatchets(transitionId, this.mlsSession.GetProtocolVersion());\n      this._maybeSendDaveProtocolReadyForTransition(transitionId);\n    } else {\n      this._flagMLSInvalidCommitWelcome(transitionId);\n      this._handleDaveProtocolInit(this.mlsSession.GetProtocolVersion());\n    }\n  }\n\n  // Opcode MLS_WELCOME (30)\n  public onMLSWelcome(transitionId: number, welcome: ArrayBuffer) {\n    const roster = this.mlsSession.ProcessWelcome(welcome, this._getRecognizedUserIDs());\n    const joinedGroup = roster != null;\n\n    if (joinedGroup) {\n      this._prepareDaveProtocolRatchets(transitionId, this.mlsSession.GetProtocolVersion());\n      this._maybeSendDaveProtocolReadyForTransition(transitionId);\n    } else {\n      this._flagMLSInvalidCommitWelcome(transitionId);\n      this._sendMLSKeyPackage();\n    }\n  }\n\n  // Outgoing Voice Gateway Responses\n\n  // Opcode MLS_KEY_PACKAGE (26)\n  private _sendMLSKeyPackage() {\n    const _keyPackage = this.mlsSession.GetMarshalledKeyPackage();\n    // TODO: Send keyPackage to the voice gateway using the MLS_KEY_PACKAGE (26) opcode\n  }\n\n  // Opcode DAVE_PROTOCOL_READY_FOR_TRANSITION (23)\n  private _maybeSendDaveProtocolReadyForTransition(transitionId: number) {\n    if (transitionId !== DAVE_PROTOCOL_INIT_TRANSITION_ID) {\n      // TODO: Send the transition ready message to the voice gateway using the DAVE_PROTOCOL_READY_FOR_TRANSITION (23) opcode\n    }\n  }\n\n  // Opcode MLS_COMMIT_WELCOME (28)\n  private _sendMLSCommitWelcome(commitWelcomeMessage: ArrayBuffer) {\n    // TODO: Send the commit welcome message to the voice gateway using the MLS_COMMIT_WELCOME (28) opcode\n  }\n\n  // Opcode MLS_INVALID_COMMIT_WELCOME (31)\n  private _flagMLSInvalidCommitWelcome(transitionId: number) {\n    // TODO: Send the invalid commit welcome message to the voice gateway using the MLS_INVALID_COMMIT_WELCOME (31) opcode\n  }\n\n  // Internal methods\n\n  private _setupKeyRatchetForUser(userId: string, protocolVersion: number) {\n    const keyRatchet = this._makeUserKeyRatchet(userId, protocolVersion);\n    // TODO: Signal the relevant media code that a key ratchet has changed and the associated Encryptor/Decryptor needs to be updated\n  }\n\n  private _handleDaveProtocolInit(protocolVersion: number) {\n    if (protocolVersion > 0) {\n      this._handleDaveProtocolPrepareEpoch(MLS_NEW_GROUP_EXPECTED_EPOCH, protocolVersion, this.groupId);\n      this._sendMLSKeyPackage();\n    } else {\n      this._prepareDaveProtocolRatchets(DAVE_PROTOCOL_INIT_TRANSITION_ID, protocolVersion);\n      this._handleDaveProtocolExecuteTransition(DAVE_PROTOCOL_INIT_TRANSITION_ID);\n    }\n  }\n\n  private _handleDaveProtocolPrepareEpoch(epoch: string, protocolVersion: number, groupId: string): void {\n    if (epoch === MLS_NEW_GROUP_EXPECTED_EPOCH) {\n      let privateKey: SignaturePrivateKey | null = null;\n      if (this.transientKeys != null) {\n        privateKey = this.transientKeys.GetTransientPrivateKey(protocolVersion);\n      }\n\n      this.mlsSession.Init(protocolVersion, BigInt(groupId), this.selfUserId, privateKey);\n    }\n  }\n\n  private _handleDaveProtocolExecuteTransition(transitionID: number): void {\n    if (!this.daveProtocolTransitions.has(transitionID)) {\n      return;\n    }\n\n    const protocolVersion = this.daveProtocolTransitions.get(transitionID)!;\n    this.daveProtocolTransitions.delete(transitionID);\n\n    if (protocolVersion === this.dave.kDisabledVersion) {\n      this.mlsSession.Reset();\n    }\n\n    this._setupKeyRatchetForUser(this.selfUserId, protocolVersion);\n  }\n\n  private _getRecognizedUserIDs(): string[] {\n    return Array.from(this.recognizedUserIds).concat([this.selfUserId]);\n  }\n\n  private _makeUserKeyRatchet(userId: string, protocolVersion: number): any {\n    if (protocolVersion === this.dave.kDisabledVersion) {\n      return null;\n    }\n\n    return this.mlsSession.GetKeyRatchet(userId);\n  }\n\n  private _prepareDaveProtocolRatchets(transitionID: number, protocolVersion: number): void {\n    for (const userId of this._getRecognizedUserIDs()) {\n      if (userId === this.selfUserId) {\n        continue;\n      }\n\n      this._setupKeyRatchetForUser(userId, protocolVersion);\n    }\n\n    if (transitionID === this.dave.kInitTransitionId) {\n      this._setupKeyRatchetForUser(this.selfUserId, protocolVersion);\n    } else {\n      this.daveProtocolTransitions.set(transitionID, protocolVersion);\n    }\n\n    this.latestPreparedTransitionVersion = protocolVersion;\n  }\n}\n"
  },
  {
    "path": "samples/typescript/README.md",
    "content": "# TypeScript DAVE Session Manager\n\nThis directory contains an example implementation of a TypeScript class capable of handling Voice Gateway events for DAVE (Discord Audio Video Encryption) support.\n\n## Overview\n\nThe `DaveSessionManager` class can handle and respond to voice gateway events concerning DAVE.\n\n## Features\n\n- Voice Gateway event handling\n- DAVE key ratchet generation\n\n## Usage\n\n1. **Initialize**: Create a `DaveSessionManager` object per selfUserId/groupId pair and call the relevant method upon receiving an opcode from the Voice Gateway.\n\n2. **Implementation**: Look for `TODO`s and make sure to implement the relevant networking or media code.\n\n3. **Key Updates**: Whenever you are signaled that a new key ratchet is available for a given user, make sure to update the associated encryptor/decryptor."
  }
]