diff --git a/.github/cancelled_workflows/qodana_code_quality.yml b/.github/cancelled_workflows/qodana_code_quality.yml new file mode 100644 index 0000000..e7c9bfe --- /dev/null +++ b/.github/cancelled_workflows/qodana_code_quality.yml @@ -0,0 +1,25 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - dev + - main + +jobs: + qodana: + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.1 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index da14177..10a338f 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -3,158 +3,85 @@ name: CMake Build on: push: branches: [ "main", "dev" ] - pull_request: - branches: [ "main" ] jobs: build: runs-on: ${{ matrix.os }} + env: + GCC_MAJOR: 14 strategy: fail-fast: false matrix: - os: [ ubuntu-latest, macos-13 ] - build_type: [ Debug, Release ] + os: [ ubuntu-24.04, macos-latest ] + build_type: [ Debug ] c_compiler: [ clang ] include: - - os: macos-13 + - os: macos-latest c_compiler: clang - cpp_compiler: clang++ - env: - LDFLAGS=: "-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib" - CPPFLAGS: "-I/usr/local/opt/llvm/include I/usr/local/opt/llvm/include/c++/v1" - LD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" - DYLD_LIBRARY_PATH: "/usr/local/opt/llvm/lib" + cpp_compiler: clang++-18 -# - os: macos-13 -# c_compiler: gcc -# cpp_compiler: g++-13 -# -# - os: ubuntu-latest -# c_compiler: gcc -# cpp_compiler: g++-13 - - - os: ubuntu-latest + - os: ubuntu-24.04 c_compiler: clang - cpp_compiler: clang++-17 - - # Don't include the following configurations in the matrix - exclude: - - os: macos-13 - build_type: Debug + cpp_compiler: clang++-18 steps: - # Install dependencies: cmake, ninja, gcc, libgcrypt, openssl, readline, and libsodium - name: Install Dependencies - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE brew update - brew install llvm cmake ninja gcc libgcrypt openssl@3 readline libsodium - echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/usr/local/opt/gcc@13/bin:$PATH"' >> ~/.bash_profile - echo 'export PATH="/usr/local/opt/gcc@13/lib/gcc/13:$PATH"' >> ~/.bash_profile - - # - name: Install Dependencies -# if: matrix.os == 'ubuntu-latest' -# run: | -# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc -# sudo add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main" -# sudo add-apt-repository -y ppa:ubuntu-toolchain-r/ppa -# sudo apt update -# sudo apt install -y cmake ninja-build gcc-13 g++-13 clang-17 lldb-17 lld-17 libc++-17-dev libc++abi-17-dev \ -# libomp-17-dev libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev - + brew install ninja cmake git gcc@${{ env.GCC_MAJOR }} + brew reinstall llvm + echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/${{ env.GCC_MAJOR }}:$PATH"' >> ~/.bash_profile + . ~/.bash_profile - uses: actions/checkout@v4 - name: Set reusable strings id: strings shell: bash + working-directory: ${{ github.workspace }} run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - - # Build project + # Set the paths to the GCC include and lib directories on macOS + if [ "${{ matrix.os }}" == "macos-latest" ]; then + echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/current")" >> "$GITHUB_OUTPUT" + fi + + # Build the project - name: Build PrivacyShield - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | - sudo ./scripts/buildscript.sh - # OS=${{ matrix.os }} - # COMMAND="./scripts/install-blake3.sh ${{ matrix.c_compiler }}" - # if [ "$OS" == "macos-13" ]; then - # $COMMAND - # elif [ "$OS" == "ubuntu-latest" ]; then - # sudo $COMMAND - # fi - # + sudo ./scripts/build.sh - name: Install Blake3 - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: | - ./scripts/install-blake3.sh ${{ matrix.c_compiler }} + sudo ./scripts/install-blake3.sh ${{ matrix.c_compiler }} - name: Configure CMake - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: > - export LDFLAGS="-L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13"; - export CPPFLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22"; - export LD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; - export DYLD_LIBRARY_PATH="/usr/local/opt/gcc@13/lib/gcc/13"; - cmake -B ${{ steps.strings.outputs.build-output-dir }} - -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm/bin/clang++ - -DCMAKE_C_COMPILER=/usr/local/opt/llvm/bin/clang - -DCMAKE_CXX_FLAGS="-I/usr/local/opt/gcc@13/include/c++/13 -I/usr/local/opt/gcc@13/include/c++/13/x86_64-apple-darwin22 -L/usr/local/opt/gcc@13/lib/gcc/13 -Wl,-rpath,/usr/local/opt/gcc@13/lib/gcc/13 -stdlib=libstdc++" + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang + -DCMAKE_CXX_FLAGS="-stdlib++-isystem ${{ steps.strings.outputs.gcc-include-dir }} -cxx-isystem ${{ steps.strings.outputs.gcc-sys-include-dir }}" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L ${{ steps.strings.outputs.gcc-lib-dir }} -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja + # -DCMAKE_CXX_FLAGS="-stdlib=libstdc++ -stdlib++-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14 -cxx-isystem /opt/homebrew/Cellar/gcc/14.1.0_1/include/c++/14/aarch64-apple-darwin23 -L /opt/homebrew/Cellar/gcc/14.1.0_1/lib/gcc/14 -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" - name: Build - if: matrix.os == 'macos-13' + if: matrix.os == 'macos-latest' run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 -# -# - name: Test -# working-directory: ${{ steps.strings.outputs.build-output-dir }} -# # Execute tests defined by the CMake configuration -# run: ctest --build-config ${{ matrix.build_type }} -# - - name: Package - if: matrix.os == 'macos-13' && matrix.build_type == 'Release' - working-directory: ${{ steps.strings.outputs.build-output-dir }} - run: | - cpack -G DragNDrop - - name: Package - if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' + # Run Tests + - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} - run: | - sudo cpack - sudo chown -R $USER:$USER "${{ github.workspace }}/Packages" - - - name: Import GPG Key - if: matrix.build_type == 'Release' - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - passphrase: ${{ secrets.GPG_PASS }} - trust_level: 5 - - - name: Sign Package - if: matrix.build_type == 'Release' - working-directory: ${{ github.workspace }} - run: | - for file in Packages/*; do - gpg --batch --status-file ~/gpg_log.txt --passphrase ${{ secrets.GPG_PASS }} --default-key dr8co@duck.com \ - --pinentry-mode=loopback --detach-sign "$file" || (cat ~/gpg_log.txt && exit 1) - done -# -# # Upload the built artifacts - - name: Upload Artifacts - if: matrix.build_type == 'Release' - uses: actions/upload-artifact@v4 - with: - name: "${{ matrix.os }}-${{ matrix.build_type }}" - path: "${{ github.workspace }}/Packages" - overwrite: true - if-no-files-found: 'warn' - + run: ctest -j 4 diff --git a/.github/workflows/cpack-multi-platform.yml b/.github/workflows/cpack-multi-platform.yml new file mode 100644 index 0000000..c5d4608 --- /dev/null +++ b/.github/workflows/cpack-multi-platform.yml @@ -0,0 +1,121 @@ +name: CPack Multi-Platform + +on: + pull_request: + branches: [ "main" ] + +jobs: + build_then_package: + runs-on: ${{ matrix.os }} + env: + GCC_MAJOR: 14 + + strategy: + fail-fast: false + + matrix: + os: [ ubuntu-24.04, macos-latest ] + build_type: [ Release ] + c_compiler: [ clang ] + include: + - os: macos-latest + c_compiler: clang + cpp_compiler: clang++-18 + + - os: ubuntu-24.04 + c_compiler: clang + cpp_compiler: clang++-18 + + steps: + - name: Install Dependencies + if: matrix.os == 'macos-latest' + run: | + export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE + brew update + brew install ninja cmake git gcc@${{ env.GCC_MAJOR }} + brew reinstall llvm + echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/bin:$PATH"' >> ~/.bash_profile + echo 'export PATH="/opt/homebrew/opt/gcc/lib/gcc/${{ env.GCC_MAJOR }}:$PATH"' >> ~/.bash_profile + . ~/.bash_profile + + - uses: actions/checkout@v4 + + - name: Set reusable strings + id: strings + shell: bash + working-directory: ${{ github.workspace }} + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + # Set the paths to the GCC include and lib directories on macOS + if [ "${{ matrix.os }}" == "macos-latest" ]; then + echo "gcc-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}")" >> "$GITHUB_OUTPUT" + echo "gcc-sys-include-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/*/include/c++/${{ env.GCC_MAJOR }}/*-apple-darwin*")" >> "$GITHUB_OUTPUT" + echo "gcc-lib-dir=$(./scripts/search.sh "/opt/homebrew/Cellar/gcc/${{ env.GCC_MAJOR }}*/lib/gcc/current")" >> "$GITHUB_OUTPUT" + fi + + # Build the project + - name: Build PrivacyShield + if: matrix.os == 'ubuntu-24.04' + run: | + sudo ./scripts/build.sh + + - name: Install Blake3 + if: matrix.os == 'macos-latest' + run: | + sudo ./scripts/install-blake3.sh ${{ matrix.c_compiler }} + + - name: Configure CMake + if: matrix.os == 'macos-latest' + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ + -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang + -DCMAKE_CXX_FLAGS="-stdlib++-isystem ${{ steps.strings.outputs.gcc-include-dir }} -cxx-isystem ${{ steps.strings.outputs.gcc-sys-include-dir }}" + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libstdc++ -L ${{ steps.strings.outputs.gcc-lib-dir }} -Wl,-rpath,/opt/homebrew/opt/gcc/lib/gcc/current" + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -S ${{ github.workspace }} -G Ninja + + - name: Build + if: matrix.os == 'macos-latest' + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 + + - name: Package + if: matrix.os == 'macos-latest' && matrix.build_type == 'Release' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + cpack + + - name: Package + if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'Release' + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: | + sudo cpack + sudo chown -R $USER:$USER "${{ github.workspace }}/Packages" + + - name: Import GPG Key + if: matrix.build_type == 'Release' + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} + passphrase: ${{ secrets.GPG_PASS }} + trust_level: 5 + + - name: Sign Package + if: matrix.build_type == 'Release' + working-directory: ${{ github.workspace }} + run: | + for file in Packages/*; do + gpg --batch --status-file ~/gpg_log.txt --passphrase ${{ secrets.GPG_PASS }} --default-key dr8co@duck.com \ + --pinentry-mode=loopback --detach-sign "$file" || (cat ~/gpg_log.txt && exit 1) + done + + # Upload the built artifacts + - name: Upload Artifacts + if: matrix.build_type == 'Release' + uses: actions/upload-artifact@v4 + with: + name: "${{ matrix.os }}-${{ matrix.build_type }}" + path: "${{ github.workspace }}/Packages" + overwrite: true + if-no-files-found: 'warn' diff --git a/.gitignore b/.gitignore index a91432d..b664571 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ # .idea/modules # *.iml # *.ipr +.idea/**/*copilot* # CMake cmake-build-*/ diff --git a/.idea/misc.xml b/.idea/misc.xml index 79b3c94..0b76fe5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index aedd90d..a473ce9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,11 +18,10 @@ cmake_minimum_required(VERSION 3.28) project(privacyShield - VERSION 2.5.0 + VERSION 3.0.0 DESCRIPTION "A suite of tools for privacy and security" - LANGUAGES CXX) - -set(CMAKE_PROJECT_HOMEPAGE_URL "https://shield.boujee.tech") + HOMEPAGE_URL "https://shield.iandee.tech" + LANGUAGES C CXX) # C++23 support is required for this project set(CMAKE_CXX_STANDARD 23) @@ -39,44 +38,156 @@ if (NOT CMAKE_BUILD_TYPE) endif () # Set the path to additional CMake modules -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") + +# Options +include(CMakeDependentOption) +# GCC does not support all sanitizers +cmake_dependent_option(ENABLE_SANITIZERS + "Enable sanitizers (Ignored if not using Clang compiler)" OFF + "${CMAKE_CXX_COMPILER_ID} STREQUAL \"Clang\"" OFF) + +# Valgrind support +option(VALGRIND_BUILD "Build with Valgrind support" OFF) # Additional checks for the Debug build -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wpedantic") +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_options( + -Wall + -Wextra + -Werror + -Wpedantic + ) +endif () + +# Add the executable target +add_executable(privacyShield) + +# Add sources for the target +target_sources(privacyShield PRIVATE + src/encryption/encryptDecrypt.cpp + src/encryption/encryptFiles.cpp + src/encryption/encryptStrings.cpp + src/passwordManager/passwordManager.cpp + src/passwordManager/passwords.cpp + src/main.cpp +) + +# C++20 Modules +target_sources(privacyShield PRIVATE + FILE_SET CXX_MODULES FILES + src/duplicateFinder/duplicateFinder.cppm + src/encryption/cryptoCipher.cppm + src/encryption/encryption.cppm + src/fileShredder/fileShredder.cppm + src/passwordManager/FuzzyMatcher.cppm + src/passwordManager/passwordManager.cppm + src/privacyTracks/privacyTracks.cppm + src/utils/utils.cppm + src/secureAllocator.cppm + src/mimallocSTL.cppm +) + +# Sanitizers for debugging and testing +# Requires llvm-symbolizer and sanitizer libraries (asan, ubsan, msan, tsan) +if (ENABLE_SANITIZERS) + # Common flags for all sanitizers + set(sanitizer_common_flags "-fno-omit-frame-pointer -g -O1") + + # Address, leak, undefined, integer, nullability sanitizers + set(address_sanitizer_flags "-fsanitize=address,leak,undefined,integer,nullability") + + # Thread sanitizer, cannot be used with address sanitizer + set(thread_sanitizer_flags "-fsanitize=thread -fPIE") + + # Memory sanitizer, cannot be used with address sanitizer. + set(memory_sanitizer_flags "-fsanitize=memory -fPIE -fno-optimize-sibling-calls") + + # Add compile options + add_compile_options( + "SHELL:${sanitizer_common_flags}" + "SHELL:${address_sanitizer_flags}" + ) + + # Track mimalloc allocations for AddressSanitizer + set(MI_TRACK_ASAN ON) + + # Link the enabled sanitizers. + target_link_libraries(privacyShield PRIVATE asan ubsan) +endif () + +# Valgrind support +if (VALGRIND_BUILD) + add_compile_options(-g) # Valgrind requires debug symbols + # target_link_libraries(privacyShield PRIVATE valgrind) + set(MI_TRACK_VALGRIND ON) +endif () # Find the required packages find_package(OpenSSL REQUIRED) find_package(Sodium REQUIRED) -find_package(Readline REQUIRED) find_package(Gcrypt REQUIRED) -find_package(BLAKE3 REQUIRED) # See https://github.com/BLAKE3-team/BLAKE3 -# Add the executable target -add_executable(privacyShield) +find_package(BLAKE3 QUIET) # See https://github.com/BLAKE3-team/BLAKE3 -# Add sources for the target -file(GLOB_RECURSE PRIVACY_SHIELD_SOURCES - "${CMAKE_SOURCE_DIR}/src/*.cpp") +include(FetchContent) -target_sources(privacyShield PRIVATE ${PRIVACY_SHIELD_SOURCES}) +# Fetch BLAKE3 from GitHub if it is not found +if (NOT TARGET BLAKE3::blake3) + message(STATUS "BLAKE3 not found. Fetching from GitHub...") -# C++20 Modules -file(GLOB_RECURSE PRIVACY_SHIELD_MODULES - "${CMAKE_SOURCE_DIR}/src/*.cppm") + FetchContent_Declare( + blake3 + GIT_REPOSITORY https://github.com/BLAKE3-team/BLAKE3.git + GIT_TAG 454ee5a7c73583cb3060d1464a5d3a4e65f06062 + SOURCE_SUBDIR c + EXCLUDE_FROM_ALL + ) -target_sources(privacyShield - PRIVATE - FILE_SET CXX_MODULES FILES - ${PRIVACY_SHIELD_MODULES} + FetchContent_MakeAvailable(blake3) + target_include_directories(privacyShield PRIVATE "${blake3_SOURCE_DIR}") + +endif () + +# Mimalloc allocator +if (NOT TARGET mimalloc-static OR NOT TARGET mimalloc) + message(STATUS "mimalloc not found. Fetching from GitHub...") + + FetchContent_Declare( + mimalloc + GIT_REPOSITORY https://github.com/microsoft/mimalloc.git + GIT_TAG v2.1.7 + EXCLUDE_FROM_ALL + ) +endif () + +set(MI_BUILD_TESTS OFF) # Do not build tests + +FetchContent_MakeAvailable(mimalloc) +target_include_directories(privacyShield PRIVATE "${mimalloc_SOURCE_DIR}/include") +add_library(Mimalloc::mimalloc-static ALIAS mimalloc-static) +add_library(Mimalloc::mimalloc ALIAS mimalloc) + +# Fetch Isocline from GitHub +FetchContent_Declare( + isocline + GIT_REPOSITORY https://github.com/daanx/isocline.git + GIT_TAG c9310ae58941559d761fe5d2dd2713d245f18da6 + EXCLUDE_FROM_ALL ) +FetchContent_MakeAvailable(isocline) +target_include_directories(privacyShield PRIVATE "${isocline_SOURCE_DIR}/include") +add_library(ISOCline::isocline ALIAS isocline) # Link libraries -target_link_libraries(privacyShield - PRIVATE OpenSSL::Crypto - PRIVATE Readline::Readline - PRIVATE Sodium::sodium - PRIVATE Gcrypt::Gcrypt - PRIVATE BLAKE3::blake3) +target_link_libraries(privacyShield PRIVATE + OpenSSL::Crypto + Sodium::sodium + Gcrypt::Gcrypt + BLAKE3::blake3 + ISOCline::isocline + Mimalloc::mimalloc-static +) # Install the binary (optional), with 0755 permissions include(GNUInstallDirs) diff --git a/CMakeModules/FindReadline.cmake b/CMakeModules/FindReadline.cmake index b789688..a07eb80 100644 --- a/CMakeModules/FindReadline.cmake +++ b/CMakeModules/FindReadline.cmake @@ -48,8 +48,15 @@ if (READLINE_FOUND AND NOT APPLE) # Find the actual location of the Readline library file find_library(READLINE_LIBRARY - NAMES libreadline.so libreadline.dylib libreadline.a + NAMES libreadline.so libreadline.a HINTS ${READLINE_LIBRARY_DIRS} + PATHS + /usr/lib + /usr/lib/x86_64-linux-gnu + /opt/local/lib + /opt/homebrew/lib + /opt/homebrew/opt/readline/lib + /opt/homebrew/Cellar/readline/*/lib ) # Set the imported location dynamically @@ -69,7 +76,14 @@ if (NOT READLINE_FOUND AND APPLE) # Find library manually find_library(READLINE_LIBRARY REQUIRED NAMES libreadline.dylib libreadline.a - PATHS /usr/local/opt/readline/lib /usr/local/lib /opt/local/lib /usr/lib + PATHS + /usr/local/opt/readline/lib + /usr/local/lib + /opt/local/lib + /usr/lib + /opt/homebrew/lib + /opt/homebrew/opt/readline/lib + /opt/homebrew/Cellar/readline/*/lib NO_DEFAULT_PATH ) diff --git a/CMakeModules/Packing.cmake b/CMakeModules/Packing.cmake index 1b1cfe2..6c07e51 100644 --- a/CMakeModules/Packing.cmake +++ b/CMakeModules/Packing.cmake @@ -22,12 +22,13 @@ # Set the CPack variables set(CPACK_PACKAGE_NAME "PrivacyShield") set(CPACK_PACKAGE_VENDOR "Ian Duncan") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A suite of tools for privacy and security") -set(CPACK_PACKAGE_VERSION "2.5.0") -set(CPACK_PACKAGE_CONTACT "dr8co@duck.com") - -SET(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/Packages") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_CONTACT "Ian Duncan ") +set(CPACK_PACKAGE_HOMEPAGE_URL "${PROJECT_HOMEPAGE_URL}") +set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/Packages") +set(CPACK_STRIP_FILES YES) set(CPACK_SOURCE_IGNORE_FILES /.git /.idea @@ -56,11 +57,14 @@ set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) set(CPACK_RPM_FILE_NAME RPM-DEFAULT) -# Set the type of installer you want to generate -set(CPACK_GENERATOR "DEB;RPM") - -# Strip the executable from debug symbols -set(CPACK_STRIP_FILES YES) +# Set the package generator +if (APPLE) + set(CPACK_GENERATOR "TGZ;DragNDrop") +elseif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") + set(CPACK_GENERATOR "TGZ;DEB;RPM") +else () + set(CPACK_GENERATOR "TGZ") +endif () # Set the package dependencies set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.35), libstdc++6 (>= 13.2.0), openssl (>= 3.0.0), libsodium23 (>= 1.0.18), libreadline8 (>= 8.0), libgcrypt20 (>= 1.10.0), libgcc-s1 (>= 13.2.0)") @@ -76,4 +80,4 @@ set(CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE ON) set(CPACK_PACKAGE_CHECKSUM "SHA256") -include(CPack) \ No newline at end of file +include(CPack) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f49c21a..c238610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,8 @@ to help and details about how this project handles them. Please make sure to rea your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 +For security issues, please follow the instructions in the [Security](./SECURITY.md) section. + > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support > the project and show your appreciation, which we would also be very happy about: > @@ -90,7 +92,7 @@ following steps in advance to help us fix any potential bug as fast as possible. #### How Do I Submit a Good Bug Report? > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue -> tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . +> tracker, or elsewhere in public. Check the [Security](./SECURITY.md) section for more information. We use GitHub issues to track bugs and errors. If you run into an issue with the project: diff --git a/README.md b/README.md index 11ba8f9..1e15e06 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,24 @@ and are not saved in the shell command history. Any operation with any tool can be canceled at any time by pressing `Ctrl+C`, and confirming the cancellation. +**Note:**\ +The program uses ANSI escape codes for colors and formatting. If you experience issues with the colors, +you can disable them by setting the `NO_COLOR` environment variable to `true` (or `1`), +or by using the `--no-color` or `-nc` option. + +```bash +export NO_COLOR=true && privacyShield +``` + +or + +```bash +privacyShield --no-color +``` + +The program will automatically detect the `NO_COLOR` environment variable, and the terminal capabilities +to determine if colors should be used. + ### Password Manager The password manager requires a primary password to encrypt/decrypt your passwords. @@ -169,7 +187,7 @@ The process might be slow, and multithreading has been leveraged to speed up the The [Serpent cipher](https://en.wikipedia.org/wiki/Serpent_(cipher)) is used for the first step because it is a conservative and secure cipher with more rounds than [AES cipher](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) -(32 rounds vs 14 rounds, hence a larger security margin) that is resistant to cryptanalysis. +(32 rounds vs. 14 rounds, hence a larger security margin) that is resistant to cryptanalysis. The [counter mode (CTR)](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)) is used for it because it is a fast and secure mode that is resistant to padding oracle attacks. A non-deterministic random [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) @@ -332,14 +350,15 @@ operating system, such as [Linux](https://en.wikipedia.org/wiki/Linux), * A C++ compiler with [C++23](https://en.cppreference.com/w/cpp/23) support, and [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) support. For this project, [GCC 14](https://gcc.gnu.org/gcc-14/) (or newer), -or [LLVM Clang 17](https://clang.llvm.org/) (or newer) is required. +or [LLVM Clang 18](https://clang.llvm.org/) (or newer) is required. * [CMake](https://cmake.org/) 3.28+ * [Ninja](https://ninja-build.org/) 1.11+, or any other build system compatible with CMake and **C++20 Modules**. * [OpenSSL](https://www.openssl.org/) 3+ * [Sodium](https://libsodium.org/) 1.0.18+ * [GCrypt](https://gnupg.org/software/libgcrypt/index.html) 1.10+ -* [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 1.4+ (see the note below) -* [GNU Readline](https://tiswww.case.edu/php/chet/readline/rltop.html) 8+ +* [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) 1.4+ (Fetched automatically by CMake, if not already installed) +* [Isocline](https://github.com/daanx/isocline) (Fetched automatically by CMake) +* [Mimalloc](https://github.com/microsoft/mimalloc) 2.17+ (Fetched automatically by CMake) **Note:**\ This project utilizes the [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) feature, @@ -411,28 +430,58 @@ You can then run the program from the build directory: You can download a package for your platform from the [releases page](https://github.com/dr8co/PrivacyShield/releases). +The packages expect the dependencies to be installed on the system, +except the ones that are fetched automatically by CMake (they are statically linked to the executable). + The package will contain the built executable, and you can install it using the package manager of your platform. For the macOS package, you can simply drag the .dmg file to your Applications folder. +The current macOS package was built on macOS 14.5 arm64 (M1 chip), +and might not work on older versions of macOS. + For the Linux package, you can install the .deb or .rpm file using the package manager of your distribution.\ Internet connection might be required to install the dependencies. For instance, on Ubuntu, you can install the .deb file using the following command: ```bash -sudo dpkg -i privacyshield_2.5.0_amd64.deb # Replace with the actual file path +sudo dpkg -i privacyshield_3.0.0_amd64.deb # Replace with the actual file path # You can also use apt to install it: -sudo apt install ./privacyshield_2.5.0_amd64.deb # Replace with the actual file path +sudo apt install ./privacyshield_3.0.0_amd64.deb # Replace with the actual file path ``` On RPM-based distributions like Fedora, you can install the .rpm file using the following command: ```bash -sudo rpm -i privacyshield-2.5.0-1.x86_64.rpm # Replace with the actual file path +sudo rpm -i privacyshield-3.0.0-1.x86_64.rpm # Replace with the actual file path ``` The packages can be verified using the [GnuPG](https://gnupg.org/) signature files provided. +To verify the packages, first import the [public GPG key](./security/privacyShield_pub_key.asc) provided: + +```bash +gpg --import public_gpg_key.asc +``` + +The public key is provided in the [releases page](https://github.com/dr8co/PrivacyShield/releases) as well. +Then verify the package using the signature file (which can also be found on the +[releases page](https://github.com/dr8co/PrivacyShield/releases)): + +```bash +gpg --verify signatures/privacyshield_3.0.0_amd64.deb.sig privacyshield_3.0.0_amd64.deb +``` + +The verification succeeds if the output says +`gpg: Good signature from "Ian Duncan (Signing key for personal projects) ..."`. + +SHA256 checksums are also provided for the packages, and you can verify the integrity of the packages using them. + +```bash +shasum -a 256 -c privacyshield_3.0.0_amd64.deb.sha256 +# Or, if you have the sha256sum command available: +sha256sum -c privacyshield_3.0.0_amd64.deb.sha256 +``` #### Manual Installation @@ -473,6 +522,8 @@ There is no need to remember commands or arguments, as the CLI will guide you th To use the CLI, simply run the program by typing `privacyShield` in your terminal. +**Tab completion is supported** for most input fields. + ## Contributing Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, @@ -514,11 +565,15 @@ However, the feeling of empowered privacy protection is a strong possibility! ![""](./media/blank.svg) -[![Readline](./media/Heckert_GNU_white.svg)](https://tiswww.case.edu/php/chet/readline/rltop.html) +[![CMake](./media/Cmake.svg)](https://cmake.org/) ![""](./media/blank.svg) -[![CMake](./media/Cmake.svg)](https://cmake.org/) +[![Mimalloc](./media/mimalloc-logo.png)](https://github.com/microsoft/mimalloc) + +![""](./media/blank.svg) + +[![Isocline](./media/isocline.png)](https://github.com/daanx/isocline) ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c4acb41 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security + +We take the security of Privacy Shield seriously. +If you believe you have found a security vulnerability in the source code, +please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please send an encrypted email to [dr8co@duck.com](mailto:dr8co@duck.com). +Encrypt your message with our PGP key; it can be found [here](./security/privacyShield_pub_key.asc). + +Please include the requested information listed below (as much as you can provide) +to help us better understand the nature and scope of the possible issue: + +* Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. diff --git a/media/isocline.png b/media/isocline.png new file mode 100644 index 0000000..f602444 Binary files /dev/null and b/media/isocline.png differ diff --git a/media/mimalloc-logo.png b/media/mimalloc-logo.png new file mode 100644 index 0000000..32b0953 Binary files /dev/null and b/media/mimalloc-logo.png differ diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..52b97e8 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.recommended + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +bootstrap: sudo ./scripts/prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-clang:2024.1-eap diff --git a/scripts/build-functions.sh b/scripts/build-functions.sh new file mode 100755 index 0000000..172956a --- /dev/null +++ b/scripts/build-functions.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -e + +# This script is used to build the project on the Ubuntu Noble (24.04) distribution. +# It is not intended to be used on other distributions, and must be run from the project root. + +PARALLELISM_LEVEL=4 + +function check_root() { + # Root access is required to install the dependencies. + if [ "$EUID" -ne 0 ]; then + echo "Please run as root." + abort + fi +} + +function install_dependencies() { +# wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc +# add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" +# add-apt-repository -y ppa:ubuntu-toolchain-r/ppa + apt update + export NEEDRESTART_SUSPEND=1 + apt install -y wget unzip gcc-14 g++-14 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 libgcrypt20-dev openssl libsodium23 libsodium-dev + + # Install CMake 3.29.3 + if dpkg -s "cmake" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove cmake + fi + + wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local + + # Install Ninja 1.12 + if dpkg -s "ninja-build" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove ninja-build + fi + + wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" + unzip ninja-linux.zip -d /usr/local/bin +} + +function build_blake3() { + ./install-blake3.sh clang-18 +} + +function configure_cmake() { + cmake -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -G Ninja +} + +function build_project() { + cmake --build build --config Debug -j "$PARALLELISM_LEVEL" +} + +function abort() { + echo "An unexpected error occurred. Program aborted." + exit 1 +} + +function build_install_gcc_14() { + apt update + apt install -y software-properties-common build-essential wget libgmp-dev libmpfr-dev libmpc-dev + wget -q https://ftp.gnu.org/gnu/gcc/gcc-14.1.0/gcc-14.1.0.tar.xz + tar -xf gcc-14.1.0.tar.xz + cd gcc-14.1.0 || abort + ./contrib/download_prerequisites + mkdir build + cd build || abort + ../configure --enable-languages=c,c++ --disable-multilib + make -j "$PARALLELISM_LEVEL" + make install + update-alternatives --install /usr/bin/gcc gcc /usr/local/bin/gcc 60 --slave /usr/bin/g++ g++ /usr/local/bin/g++ + apt purge -y gcc cpp g++ +} + +trap "echo 'An unexpected error occurred. Program aborted.'" ERR diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..c0fdbe7 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Run from this directory +cd "${0%/*}" || abort + +# Include the build functions +. ./build-functions.sh + +# Root access is required to install the dependencies. +check_root + +# Install dependencies +install_dependencies + +echo "Ninja: $(ninja --version), CMake: $(cmake --version)" + +# Build and install BLAKE3 +build_blake3 + +# Configure CMake +cd .. || abort +configure_cmake + +# Build the project +build_project \ No newline at end of file diff --git a/scripts/buildscript.sh b/scripts/buildscript.sh deleted file mode 100755 index d1e3909..0000000 --- a/scripts/buildscript.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/env bash - -set -e - -# This script is used to build the project on the Ubuntu Jammy (22.04) distribution. -# It is not intended to be used on other distributions, and must be run from the project root. - -PARALLELISM_LEVEL=4 - -function check_root() { - # Root access is required to install the dependencies. - if [ "$EUID" -ne 0 ]; then - echo "Please run as root." - abort - fi -} - -function check_dependencies() { - for cmd in wget add-apt-repository; do - if ! command -v $cmd &>/dev/null; then - echo "$cmd could not be found" - exit - fi - done -} - -function install_dependencies() { - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - add-apt-repository -y "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" - add-apt-repository -y ppa:ubuntu-toolchain-r/ppa - apt update - apt install -y unzip gcc-13 g++-13 clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev clang-tools-18 libgcrypt20 openssl libreadline8 libsodium23 libsodium-dev - - # Install CMake 3.28.3 - if dpkg -s "cmake" >/dev/null 2>&1; then - apt remove -y --purge --auto-remove cmake - fi - - wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.28.3/cmake-3.28.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local - - # Install Ninja 1.11 - if dpkg -s "ninja-build" >/dev/null 2>&1; then - apt remove -y --purge --auto-remove ninja-build - fi - - wget -q "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip" - unzip ninja-linux.zip -d /usr/local/bin -} - -function build_blake3() { - ./install-blake3.sh clang-18 -} - -function configure_cmake() { - cmake -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -G Ninja -} - -function build_project() { - cmake --build build --config Debug -j "$PARALLELISM_LEVEL" -} - -main() { - trap "echo 'An unexpected error occurred. Program aborted.'" ERR - check_root - check_dependencies - cd "${0%/*}" || abort - install_dependencies - echo "Ninja: $(ninja --version), CMake: $(cmake --version)" - build_blake3 - cd .. || abort - configure_cmake - build_project -} - -main diff --git a/scripts/install-blake3.sh b/scripts/install-blake3.sh index 9e81789..cd740d9 100755 --- a/scripts/install-blake3.sh +++ b/scripts/install-blake3.sh @@ -19,7 +19,7 @@ detect_os() { # Function to check root access check_root() { - [[ "$CURRENT_OS" != "macos" && "$EUID" -ne 0 ]] && error_exit "This script must be run as root." + [[ "$CURRENT_OS" == "linux" && "$EUID" -ne 0 ]] && error_exit "This script must be run as root." } get_number_of_processors() { @@ -36,35 +36,17 @@ install_blake3() { cd ~ || error_exit "Failed to change to home directory." # Download BLAKE3 and extract to current directory - wget -qO- https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.5.0.tar.gz | tar -xz -C . + wget -qO- https://github.com/BLAKE3-team/BLAKE3/archive/refs/tags/1.5.1.tar.gz | tar -xz -C . - cd BLAKE3-1.5.0/c || error_exit "Failed to navigate to BLAKE3/c directory." + cd BLAKE3-1.5.1/c || error_exit "Failed to navigate to BLAKE3/c directory." - cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -G Ninja || error_exit "Failed to run cmake." + cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -DCMAKE_BUILD_TYPE=Release -G Ninja || error_exit "Failed to configure CMake." get_number_of_processors cmake --build build --config Release --target install -j "$NUMBER_OF_PROCESSORS" || error_exit "Failed to build and install." } -# Function to clone repository -clone_repo() { - git clone https://github.com/BLAKE3-team/BLAKE3.git || error_exit "Failed to clone BLAKE3 repository." -} - -# Function to build and install BLAKE3 -build_install() { - cd BLAKE3/c || error_exit "Failed to navigate to BLAKE3/c directory." - cmake -B build -DCMAKE_C_COMPILER="$C_COMPILER" -G Ninja || error_exit "Failed to run cmake." - get_number_of_processors - - cmake --build build --config Release --target install -j "$NUMBER_OF_PROCESSORS" || error_exit "Failed to build and install." - - # Cleanup - cd ../.. - rm -rf BLAKE3 -} - # Set C compiler C_COMPILER=${1:-gcc} @@ -75,8 +57,4 @@ cd "${0%/*}" || error_exit "Failed to change directory to script location." echo "Compiling BLAKE3 with $C_COMPILER compiler.." -# Call functions -# clone_repo -# build_install - install_blake3 diff --git a/scripts/prepare-qodana.sh b/scripts/prepare-qodana.sh new file mode 100755 index 0000000..96906cf --- /dev/null +++ b/scripts/prepare-qodana.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Run from this directory +cd "$(dirname "$0")" || (echo "Running from $(pwd)" && exit 1) + +# Include the build functions +. ./build-functions.sh + +# Root access is required to install the dependencies. +check_root + +# Build and install GCC 14 +build_install_gcc_14 + +# Install dependencies +apt remove -y --purge --auto-remove llvm-16-dev clang-16 clang-tidy-16 clang-format-16 lld-16 libc++-16-dev libc++abi-16-dev +apt update && apt install -y software-properties-common wget unzip build-essential openssl libsodium23 libsodium-dev libgcrypt20-dev +wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc +add-apt-repository -y "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" +apt update +export NEEDRESTART_SUSPEND=1 +apt install -y llvm-18-dev clang-18 lldb-18 lld-18 libc++-18-dev libc++abi-18-dev libllvmlibc-18-dev clang-tools-18 clang-tidy-18 clang-format-18 + +for f in /usr/lib/llvm-18/bin/*; do + ln -sf "$f" /usr/bin; +done + +# Install CMake 3.29.3 +if dpkg -s "cmake" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove cmake +fi + +wget -qO- "https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local + +# Install Ninja 1.12 +if dpkg -s "ninja-build" >/dev/null 2>&1; then + apt remove -y --purge --auto-remove ninja-build +fi + +wget -q "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip" +unzip ninja-linux.zip -d /usr/local/bin + +echo "Ninja: $(ninja --version), CMake: $(cmake --version)" + +# Build and install BLAKE3 +build_blake3 + +# Configure CMake +cd .. || abort +/usr/local/bin/cmake -S . -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja + diff --git a/scripts/search.py b/scripts/search.py new file mode 100755 index 0000000..433c486 --- /dev/null +++ b/scripts/search.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import sys +import glob + + +def search_filesystem(pattern: str) -> None: + """ + Search the filesystem for files or directories that match the pattern + :param pattern: The pattern to search for + :return: None + """ + # Find files or directories that match the pattern + matches = glob.glob(pattern, recursive=True) + + # Check if there are any matches + if not matches: + print(f"No matches found for pattern: {pattern}") + else: + # Sort matches in reverse order + matches.sort(reverse=True) + + # Select the first match (after sorting in reverse order) + print(matches[0]) + + +if __name__ == "__main__": + # Check if exactly one argument is provided + if len(sys.argv) != 2: + print("Usage: python3 search.py ") + sys.exit(1) + + search_filesystem(sys.argv[1]) diff --git a/scripts/search.sh b/scripts/search.sh new file mode 100755 index 0000000..b2848cc --- /dev/null +++ b/scripts/search.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# This script is used to search for files or directories that match a pattern. +# It searches recursively and returns the first match found. + +# Check if exactly one argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +pattern=$1 + +# Find files or directories that match the pattern and store them in an array +IFS=$'\n' read -d '' -r -a matches < <(find $(dirname "$pattern") -name "$(basename "$pattern")" | sort -r 2>/dev/null) + +# Check if any matches were found +if [ ${#matches[@]} -eq 0 ]; then + echo "No matches found." + exit 1 +else + # Print the first match + echo "${matches[0]}" +fi diff --git a/security/privacyShield_pub_key.asc b/security/privacyShield_pub_key.asc new file mode 100644 index 0000000..c9eab7c --- /dev/null +++ b/security/privacyShield_pub_key.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGXUykUBEAD0nkmT6SgkJnlXTx2aEJ23bl87TZR3gW3v+uNQn5ISaUw1WJtE +8XYYkibTBz88W8EeozXGfRn3UDs4g1UOvMcYlfibOxHUZm0MPrueJhFptaBs/x6h +Y0zaODlX3ZpM3ISGu1o4mwLTOhHhP+15XkbT+b7Cbf1HFKplPyp8TS/s0cUfSq5f +Thg8ngIbnkUp4S5cqKJMV3yyK0NyqYE8IJMNOMTot7szFA06phUp5XbOl+Jmhbr2 +I3YPWE/guFw0R4IX3klXOITLs5iJFkrLL5VbGseF0YQUkGVFTiTovuc8hcvmbS8x +mU+lH0IkRnye1F9fogymhuiX8vL5Gf9C6gNr0mpWxaHtFAkVqmchhmWWRn3RGkHM +6kdYrEj5yDrhwI8ZPVjh34XDQMg7pXIdiHZbTZMm/RA944VhdfjYs3VS/FZ3Oz9k +Rf5M830x9ifG28dCMiVFhYn5jm4hOmh6KrfPxK7Muf2XFJP6DiBNAT07LDq/DeG8 +qk/VALH6vKElFADVsrn2QVq8LmapmYwMl9l2Bmb6isWsuu+RUGft3MvwygUfve5b +9NQYITGGhmD/jH5IXiW/iU+6tIQx9UFaQTm9Plg4LChnVQpMqxp0YhX47QNZFyWB +qJ+N47DTGcjDENQcXTidu4kgV0jf26y2MQ8N0GvufJXvA3NuHpcfJ1e7uwARAQAB +tD9JYW4gRHVuY2FuIChTaWduaW5nIGtleSBmb3IgcGVyc29uYWwgcHJvamVjdHMp +IDxkcjhjb0BkdWNrLmNvbT6JAk4EEwEKADgWIQRkiIHdBB6Z7skF/YG9mRSEoI6b +xQUCZdTKRQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC9mRSEoI6bxXuU +D/0eYT2v5b8E6Ws8rNh0hzglzC9BiZDG2NWoaz1O26ixXPdzymYFB1xvNFS4UONP +1YDGGfFTBjhsseUwv11N6ZahwzGIkBnMUwH/Vbx2Rt4NTXkukfqeHaTM/jTPZUMn +OzBcECaE9Buph18c05tlXGb60+40R4zB/uTPys5qzuVCs0Us4X5CIt7GHC/hwJiW +hxEZyA/S0ardVyh1dQEpKV6PeWczwvfkqPBymsx5FLABC5N2epPq7OO6Uv/tMOfC +UrM/7z1CEY/vmHcD+fpfsXK4Y4ixaUrs8BJQtmaYrVSH+wHlmlNumudI6VtPU7i1 +egTpJ4ViidqzzZLL075hGCEczsp3EqtBKpik+ZpHXHSpMYeuLvqz20PAP3jAYNHV +k7vcv39mDsivf//GbfObQ43kKpWvfJ6lwb4EaopL8RLCFxGDrLM2bf1cb/WRULBw +YAgiF7gfYpe4rPUlp1/xFkdH5lBZay1sFN8rVu0wPwung7czQHypNo0jYrvHiufD +BDkr0sQznV6dKBY8uhMhkhiVhq2hast6I5rNkJSRx6ZpIROYVF5xnlAGaJ/C9SKK +zInHVJRWW19RNDfVj9Ld9O6fgsnGTOiJDSmH1J6jbdNoqafNc1EyXnYT0Dl5rQ0f +HDZMYssMgdIGIjh4gtNAuW/T8vE0rXl33+vwSvV7aJf1NLkCDQRl1MpFARAAv2hF +AbLa0Q7YUQXVma6Dr72ML8bdF1q1xXC1vD+L7Aka+TFjzHmgeQxmDsP9LvAAkRAg +K7XKLYJqT7nvHfr8g8flUiSImBRXHV7OBlVXiVmVJqKSMoFC6TOvGyrvlESDlBAB +SZayNTihRZz/fIdiahZ+Iblwp3/9ZwufAqPsYlgfL51SLUZZnohw2jzDbNTToVsk +W6qQKvjbDYOJk+/i4jNnmnoN3T39zRUqKiKudNHtspha424eRys6sBgTni4mSZ2b +8FpWmC+ou7Ichkb4i0RU1sS+LgnwzP9yic9qEQDzhLcBqcFiSWJV1gwyzE+gbc3i +KGAfXV6cIHpjA5ZrSoZyBIPsrDWHclWYJx7x4r39ddSORgOMCCtxiC57krjWh7oU +a/KlWZR3MjeLI+ipVT0ltZeT9FArZzeSLXEqW2ENGPk1fWv0SgwPStyQla5244aZ +BDaB3WUr4dVAdWWKEuxoPUQPwEVTgJKJ0/OtC2jPMbE9dFSBz1Iwh5+EuB7PLJTc +PoTb4vMLkFmJntWJIytPDcUgFIpf0Cld8nXICFHUp085zJo182KBkQQWr05SG8p3 +5nNuzRE3V00CURrH/SSEa1D3wNlnOYS6klGYzmGLBlb8EoZusb+Lsrd+tLIzqTdV +ciN+qUeEmPLCJEtRexaWVmbUfe7SGJH6VSB7JPEAEQEAAYkCNgQYAQoAIBYhBGSI +gd0EHpnuyQX9gb2ZFISgjpvFBQJl1MpFAhsMAAoJEL2ZFISgjpvFEA8P/2DBEln/ +BGBhMyhGMTuLqSb7FqG3yJLD7YTlM2MTGOHinCroOxI3UlS6PQew9o4DkJHaLQw+ +ho4qcIol7wISnRHE5fYABcmbdPR3wSIT/KQRdSTVSOkQDFPFGOPRfKeCmKP4npB/ +x8LHcoJwG3ab/2axHNkJLWrJwFY6iYxpYIW364v+uSJJq22z3SJ8bmL0JJjr6JPK +SU7Io4deRbnw7q0TNJqcgs4dzmIWaubaA1VdmYbZnq57F6WvTDVwz8vWT45sw4Sy +RML3B2UPxnvHS8sJygnM2Vo+yijU1sYm3yYYAfB81AlbrjGyuGhISR7jnFgAtnuD +HjpGXnDrYKcmjp+EpC88oJiIuuUD6E6AyIXGnXTs00fFCqhcXXwW6wRF7vzo47To +eApHCc0y9A+3ZEfmkpdIsxXdcQNf65Eh0XOB4tEYtmm2hPgjJ3T+1iZRYfsr7dkV +bE//dzBIom0VEr9I8vYlKsqlC0G98AplYvitS++4akCpYFuFXlcKlQErhSGL9qgo +34xrQv//salNPGzDWbOpCDGUQ8DV01FFBkneZIlrz85rR63SPi7mp7SHIW6mGxuA +ZSc8aL5X1m61d4uLTqYswRDJ1nE4ZT05Bk9bpDOzqbELBitU54oPCCNqWEAs+Usq +dhCOLSVqS8XZKOgkOO54MKbmb3YZv5m46DfY +=M91j +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/duplicateFinder/duplicateFinder.cppm b/src/duplicateFinder/duplicateFinder.cppm index 11d733c..8b882b6 100644 --- a/src/duplicateFinder/duplicateFinder.cppm +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -16,57 +16,64 @@ module; -#include +#include #include #include #include #include #include #include -#include #include +#include #include #include export module duplicateFinder; import utils; +import mimallocSTL; namespace fs = std::filesystem; -constexpr std::size_t CHUNK_SIZE = 4096; // Read and process files in chunks of 4 kB +constexpr std::size_t CHUNK_SIZE = 4096; ///< Read and process files in chunks of 4 kB /// \brief Represents a file by its path (canonical) and hash. struct FileInfo { - std::string path; // the path to the file. - std::string hash; // the file's BLAKE3 hash + miSTL::string path{}; ///< the path to the file. + miSTL::string hash{}; ///< the file's BLAKE3 hash }; /// \brief Calculates the 256-bit BLAKE3 hash of a file. /// \param filePath path to the file. /// \return Base64-encoded hash of the file. /// \throws std::runtime_error if the file cannot be opened. -std::string calculateBlake3(const std::string &filePath) { +miSTL::string calculateBlake3(const miSTL::string &filePath) { // Open the file - std::ifstream file(filePath, std::ios::binary); - if (!file) - throw std::runtime_error(std::format("Failed to open '{}' for hashing.", filePath)); + std::ifstream file(filePath.c_str(), std::ios::binary); + if (!file) { + if (std::error_code ec; fs::exists(filePath, ec)) + throw std::runtime_error(std::format("Failed to open '{}' for hashing.", filePath)); + + printColoredError('b', "{} ", filePath); + printColoredErrorln('r', "existed during scan but was not found during hashing."); + return ""; + } // Initialize the BLAKE3 hasher blake3_hasher hasher; blake3_hasher_init(&hasher); // Update the hasher with the file contents in chunks of 4 kB - std::vector buffer(CHUNK_SIZE); + std::array buffer{}; while (file.read(buffer.data(), CHUNK_SIZE)) blake3_hasher_update(&hasher, buffer.data(), CHUNK_SIZE); // Update the hasher with the last chunk of data - std::size_t remainingBytes = file.gcount(); + const std::size_t remainingBytes = file.gcount(); blake3_hasher_update(&hasher, buffer.data(), remainingBytes); // Finalize the hash calculation - std::vector digest(BLAKE3_OUT_LEN); + miSTL::vector digest(BLAKE3_OUT_LEN); blake3_hasher_finalize(&hasher, digest.data(), BLAKE3_OUT_LEN); return base64Encode(digest); @@ -75,104 +82,99 @@ std::string calculateBlake3(const std::string &filePath) { /// \brief handles file i/o errors during low-level file operations. /// \param filename path to the file on which an error occurred. inline void handleAccessError(const std::string_view filename) { - std::string errMsg; - errMsg.reserve(50); - - switch (errno) { - case EACCES: // Permission denied - errMsg = "You do not have permission to access this item"; - break; - case EEXIST: // File exists - errMsg = "already exists"; - break; - case EISDIR: // Is a directory - errMsg = "is a directory"; - break; - case ELOOP: // Too many symbolic links encountered - errMsg = "is a loop"; - break; - case ENAMETOOLONG: // The filename is too long - errMsg = "the path is too long"; - break; - case ENOENT: // No such file or directory - errMsg = "path does not exist"; - break; - case EROFS: // Read-only file system - errMsg = "the file system is read-only"; - break; - default: // Success (most likely) - return; - } + if (errno) { + printColoredError('r', "Skipping "); + printColoredError('c', "{}", filename); + printColoredErrorln('r', ": {}", std::strerror(errno)); - printColor(std::format("Skipping '{}': {}.", filename, errMsg), 'r', true, std::cerr); + errno = 0; + } } /// \brief recursively traverses a directory and collects file information. /// \param directoryPath the directory to process. -/// \param files a vector to store the information from the files found in the directory. -void traverseDirectory(const std::string_view directoryPath, std::vector &files) { +/// \param candidateDuplicates a vector to store the information from the files found in the directory. +std::size_t traverseDirectory(const fs::path &directoryPath, miSTL::vector &candidateDuplicates) { std::error_code ec; + // Number of files processed + std::size_t filesProcessed{0}; + + // Map to store file sizes and their corresponding paths + miSTL::unordered_map > sizeToFileMap; + for (const auto &entry: fs::recursive_directory_iterator(directoryPath, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink)) { + ++filesProcessed; if (entry.exists(ec)) { // In case of broken symlinks if (ec) { - printColor(std::format("Skipping '{}': {}.", - entry.path().string(), ec.message()), 'r', true, std::cerr); + printColoredError('r', "Skipping "); + printColoredError('c', "{}", entry.path().string()); + printColoredErrorln('r', ": {}", ec.message()); ec.clear(); continue; } // Make sure we can read the entry - if (isReadable(entry.path())) [[likely]] { + if (isReadable(entry.path().string().c_str())) [[likely]] { // process only regular files if (entry.is_regular_file()) [[likely]] { - FileInfo fileInfo; - - // Update the file details - fileInfo.path = entry.path().string(); - fileInfo.hash = ""; // the hash will be calculated later - files.emplace_back(fileInfo); - } else if (!entry.is_directory()) // Neither regular nor a directory - printColor(std::format("Skipping '{}': Not a regular file.", - entry.path().string()), 'r', true, std::cerr); + sizeToFileMap[fs::file_size(entry.path())].push_back(entry.path()); + } else if (!entry.is_directory()) { + // Neither regular nor a directory + printColoredError('r', "Skipping "); + printColoredError('c', "{}", entry.path().string()); + printColoredErrorln('r', ": Not a regular file.", ec.message()); + } } else handleAccessError(entry.path().string()); } } + candidateDuplicates.reserve(filesProcessed); + // Report files with the same sizes + for (auto &files: sizeToFileMap | std::views::values) { + if (files.size() > 1) { + for (const auto &file: files) { + candidateDuplicates.emplace_back(FileInfo{file.string().c_str(), ""}); + } + } + } + + return filesProcessed; } /// \brief calculates hashes for a range of files. /// \param files the files to process. /// \param start the index where processing starts. /// \param end the index where processing ends. -void calculateHashes(std::vector &files, const std::size_t start, const std::size_t end) { +void calculateHashes(miSTL::vector &files, const std::size_t start, const std::size_t end) { // Check if the range is valid if (start > end || end > files.size()) throw std::range_error("Invalid range."); // Calculate hashes for the files in the range for (std::size_t i = start; i < end; ++i) - files[i].hash = calculateBlake3(files[i].path); + files[i].hash = calculateBlake3(files[i].path).c_str(); } /// \brief finds duplicate files (by content) in a directory. /// \param directoryPath the directory to process. /// \return True if duplicates are found, else False. -std::size_t findDuplicates(const std::string_view directoryPath) { +std::size_t findDuplicates(const fs::path &directoryPath) { // Collect file information - std::vector files; - traverseDirectory(directoryPath, files); - const std::size_t filesProcessed = files.size(); - if (filesProcessed < 1) return 0; + miSTL::vector files; + const std::size_t filesProcessed = traverseDirectory(directoryPath, files); + const std::size_t numFiles = files.size(); + + if (filesProcessed < 2 || numFiles < 2) return 0; // Number of threads to use const unsigned int n{std::jthread::hardware_concurrency()}; const unsigned int numThreads{n ? n : 8}; // Use 8 threads if hardware_concurrency() fails // Divide the files among the threads - std::vector threads; - const std::size_t filesPerThread = filesProcessed / numThreads; + miSTL::vector threads; + const std::size_t filesPerThread = numFiles / numThreads; std::size_t start = 0; // Calculate the files' hashes in parallel @@ -187,7 +189,8 @@ std::size_t findDuplicates(const std::string_view directoryPath) { for (auto &thread: threads) thread.join(); // A hash map to map the files to their corresponding hashes - std::unordered_map > hashMap; + miSTL::unordered_map > hashMap; + hashMap.reserve(files.size()); // Iterate over files and identify duplicates for (const auto &[filePath, hash]: files) @@ -196,24 +199,24 @@ std::size_t findDuplicates(const std::string_view directoryPath) { std::size_t duplicatesSet{0}, numDuplicates{0}; // Display duplicate files - std::cout << "Duplicates found:" << std::endl; + std::println("Duplicates found:"); for (const auto &duplicates: hashMap | std::views::values) { if (duplicates.size() > 1) { ++duplicatesSet; // Show the duplicates in their sets - printColor("Duplicate files set ", 'c'); - printColor(duplicatesSet, 'g'); - printColor(":", 'c', true); + printColoredOutput('c', "Duplicate files set "); + printColoredOutput('g', "{}", duplicatesSet); + printColoredOutputln('c', ":"); for (const auto &filePath: duplicates) { - std::cout << " " << filePath << std::endl; ++numDuplicates; + std::println(" {}", filePath); } } } - printColor("\nFiles processed: ", 'c'); - printColor(filesProcessed, 'g', true); + printColoredOutput('c', "\nFiles processed: "); + printColoredOutputln('g', "{}", filesProcessed); return numDuplicates; } @@ -221,68 +224,64 @@ std::size_t findDuplicates(const std::string_view directoryPath) { /// \brief A simple duplicate file detective. export void duplicateFinder() { while (true) { - std::cout << "\n-------------------"; - printColor(" Duplicate Finder ", 'm'); - std::cout << "-------------------\n"; - printColor("1. Scan for duplicate files\n", 'g'); - printColor("2. Exit\n", 'r'); - std::cout << "--------------------------------------------------------" << std::endl; + std::print("\n-------------------"); + printColoredOutput('m', " Duplicate Finder "); + std::println("-------------------"); + printColoredOutputln('g', "1. Scan for duplicate files"); + printColoredOutputln('r', "2. Exit"); + std::println("--------------------------------------------------------"); - printColor("Enter your choice:", 'b'); + printColoredOutput('b', "Enter your choice:"); if (const int resp = getResponseInt(); resp == 1) { try { - printColor("Enter the path to the directory to scan:", 'b'); - std::string dirPath = getResponseStr(); - - if (const auto len = dirPath.size(); len > 1 && (dirPath.ends_with('/') || dirPath.ends_with('\\'))) - dirPath.erase(len - 1); + printColoredOutput('b', "Enter the path to the directory to scan:"); + fs::path dirPath = getFilesystemPath(); std::error_code ec; const fs::file_status fileStatus = fs::status(dirPath, ec); if (ec) { - printColor("Unable to determine ", 'y', false, std::cerr); - printColor(dirPath, 'b', false, std::cerr); + printColoredError('y', "Unable to determine "); + printColoredError('b', "{}", dirPath.string()); - printColor("'s status: ", 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "'s status: "); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } if (!exists(fileStatus)) { - printColor(dirPath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}", dirPath.string()); + printColoredErrorln('r', " does not exist."); continue; } if (!is_directory(fileStatus)) { - printColor(dirPath, 'c', false, std::cerr); - printColor(" is not a directory.", 'r', true, std::cerr); + printColoredError('c', "{}", dirPath.string()); + printColoredErrorln('r', " is not a directory."); continue; } if (fs::is_empty(dirPath, ec)) { if (ec) ec.clear(); else { - printColor("The directory is empty.", 'r', true, std::cerr); + printColoredErrorln('r', "The directory is empty."); continue; } } - printColor("Scanning ", 'c'); - printColor(fs::canonical(dirPath).string(), 'g'); - printColor(" ...", 'c', true); + printColoredOutput('c', "Scanning "); + printColoredOutput('g', "{}", fs::canonical(dirPath).string()); + printColoredOutputln('c', " ..."); const std::size_t duplicateFiles = findDuplicates(dirPath); - std::cout << "Duplicates " - << (duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found.") - << std::endl; + std::println("Duplicates {}", + duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found."); } catch (const std::exception &ex) { - printColor("An error occurred: ", 'y', false, std::cerr); - printColor(ex.what(), 'r', true, std::cerr); + printColoredError('y', "An error occurred: "); + printColoredErrorln('r', "{}", ex.what()); } } else if (resp == 2) break; else { - printColor("Invalid option!", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid option!"); } } } diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index ccdaa7d..f968b63 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -23,50 +23,19 @@ module; #include #include #include -#include #include #include +#include import utils; import secureAllocator; +import mimallocSTL; import passwordManager; module encryption; namespace fs = std::filesystem; -template -/// \brief A concept describing a type convertible and comparable with uintmax_t. -/// \tparam T - An integral type. -concept Num = std::integral && std::convertible_to && - std::equality_comparable_with; - - -/// \brief A class to make file sizes more readable. -/// \details Adapted from https://en.cppreference.com/w/cpp/filesystem/file_size -class FormatFileSize { -public: - explicit FormatFileSize(const Num auto &size) { - // Default negative values to zero - if (std::cmp_greater(size, size_)) - size_ = static_cast(size); - } - -private: - std::uintmax_t size_{0}; - - friend - std::ostream &operator<<(std::ostream &os, const FormatFileSize ffs) { - int i{}; - auto mantissa = static_cast(ffs.size_); - for (; mantissa >= 1024.; mantissa /= 1024., ++i) { - } - mantissa = std::ceil(mantissa * 10.) / 10.; - os << mantissa << "BKMGTPE"[i]; - return i == 0 ? os : os << "B (" << ffs.size_ << ')'; - } -}; - /// \brief Available encryption/decryption ciphers. enum class Algorithms : std::uint_fast8_t { AES = 1 << 0, @@ -91,13 +60,27 @@ constexpr struct { const gcry_cipher_algos Twofish = GCRY_CIPHER_TWOFISH; } AlgoSelection; + +/// \brief Formats a file size into a human-readable string. +/// \param size The file size as an unsigned integer. +/// \return A string representing the formatted file size. +miSTL::string formatFileSize(const std::uintmax_t &size) { + int i{}; + auto mantissa = static_cast(size); + for (; mantissa >= 1024.; mantissa /= 1024., ++i) { + } + mantissa = std::ceil(mantissa * 10.) / 10.; + miSTL::string result { std::to_string(mantissa) + "BKMGTPE"[i]}; + return i == 0 ? result : result + "B (" + std::to_string(size).c_str() + ')'; +} + /// \brief Checks for issues with the input file, that may hinder encryption/decryption. /// \param inFile the input file, to be encrypted/decrypted. /// \param mode the mode of operation: encryption or decryption. /// \throws std::invalid_argument if \p mode is invalid. /// \throws std::runtime_error if the input file does not exist, is a directory, /// is not a regular file, or is not readable. -inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { +void checkInputFile(const fs::path &inFile, const OperationMode &mode) { if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) throw std::invalid_argument("Invalid mode of operation."); @@ -111,7 +94,7 @@ inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { if (!is_regular_file(inFile)) { if (mode == OperationMode::Encryption) { // Encryption - std::cout << inFile.string() << " is not a regular file. \nDo you want to continue? (y/n): "; + std::print("{} is not a regular file.\nDo you want to continue? (y/n): ", inFile.string()); if (!validateYesNo()) throw std::runtime_error(std::format("{} is not a regular file.", inFile.string())); } else @@ -119,24 +102,22 @@ inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { std::format("{} is not a regular file.", inFile.string())); // Encrypted files are regular } // Check if the input file is readable - if (auto file = inFile.string(); !isReadable(file)) + if (auto file = inFile.string(); !isReadable(file.c_str())) throw std::runtime_error(std::format("{} is not readable.", file)); } /// \brief Creates non-existing parent directories for a file. /// \param filePath The file path for which the directory path needs to be created. /// \return True if the directory path is created successfully or already exists, false otherwise. -inline bool createPath(const fs::path &filePath) noexcept { +bool createPath(const fs::path &filePath) noexcept { if (filePath.string().empty()) return false; // Can't create empty paths std::error_code ec; - auto absolutePath = weakly_canonical(filePath, ec); if (ec) { absolutePath = filePath; ec.clear(); } - if (absolutePath.has_filename()) absolutePath.remove_filename(); @@ -167,15 +148,14 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope // If the output file is not specified, name it appropriately if (equivalent(fs::current_path(), outFile)) { outFile = inFile; - if (inFile.extension() == ".enc") { - outFile.replace_extension(""); - } else if (mode == OperationMode::Encryption) { - outFile += ".enc"; - } else { - outFile.replace_extension(""); - outFile += "_decrypted"; - outFile += inFile.extension(); - } + if (mode == OperationMode::Decryption) { + if (inFile.extension() == ".enc") + outFile.replace_extension(""); + else { + outFile += "_decrypted"; + outFile += inFile.extension(); + } + } else outFile += ".enc"; } else if (is_directory(outFile)) { // If the output file is a directory, rename it appropriately. if (mode == OperationMode::Encryption) { @@ -185,34 +165,34 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope } // If the output file exists, ask for confirmation for overwriting if (exists(outFile, ec)) { - printColor(canonical(outFile).string(), 'b', false, std::cerr); - printColor(" already exists. \nDo you want to overwrite it? (y/n): ", 'r', false, std::cerr); + printColoredError('b', "{}", canonical(outFile).string()); + printColoredError('r', " already exists.\nDo you want to overwrite it? (y/n): "); if (!validateYesNo()) throw std::runtime_error("Operation aborted."); // Determine if the output file can be written if it exists - if (auto file = weakly_canonical(outFile).string(); !(isWritable(file) && isReadable(file))) + if (auto file = weakly_canonical(outFile).string(); !(isWritable(file.c_str()) && isReadable(file.c_str()))) throw std::runtime_error(std::format("{} is not writable/readable.", file)); } } // Check if the input and output files are the same - if (equivalent(inFile, outFile)) - throw std::runtime_error("The input and output files are the same."); + if (std::error_code ec; exists(outFile, ec) && equivalent(inFile, outFile)) + throw std::runtime_error("The input and the output file both refer to the same object."); // Check if there is enough space on the disk to save the output file. const auto availableSpace = getAvailableSpace(weakly_canonical(outFile)); if (const auto fileSize = file_size(inFile); std::cmp_less(availableSpace, fileSize)) { - printColor("Not enough space to save ", 'r', false, std::cerr); - printColor(weakly_canonical(outFile).string(), 'c', true, std::cerr); + printColoredError('r', "Not enough space to save "); + printColoredError('c', "{}", weakly_canonical(outFile).string()); - printColor("Required: ", 'y', false, std::cerr); - printColor(FormatFileSize(fileSize), 'g', true, std::cerr); + printColoredError('y', "Required: "); + printColoredError('g', "{}", formatFileSize(fileSize)); - printColor("Available: ", 'y', false, std::cerr); - printColor(FormatFileSize(availableSpace), 'r', true, std::cerr); + printColoredError('y', "Available: "); + printColoredErrorln('r', "{}", formatFileSize(availableSpace)); - printColor("\nDo you still want to continue? (y/n):", 'b'); + printColoredOutput('b', "\nDo you still want to continue? (y/n):"); if (!validateYesNo()) throw std::runtime_error("Insufficient storage space."); } @@ -232,17 +212,17 @@ inline void copyLastWrite(const std::string_view srcFile, const std::string_view /// \param password the password to use for encryption/decryption. /// \param algo the algorithm to use for encryption/decryption. /// \param mode the mode of operation: encryption or decryption. -void fileEncryptionDecryption(const std::string &inputFileName, const std::string &outputFileName, +void fileEncryptionDecryption(const miSTL::string &inputFileName, const miSTL::string &outputFileName, const privacy::string &password, const Algorithms &algo, const OperationMode &mode) { // The mode must be valid: must be either encryption or decryption if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) [[unlikely]] { - printColor("Invalid mode of operation.", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid mode of operation."); return; } try { /// Encrypts/decrypts a file based on the passed mode and algorithm. - auto encryptDecrypt = [&](const std::string &algorithm) -> void { + auto encryptDecrypt = [&](const miSTL::string &algorithm) -> void { if (mode == OperationMode::Encryption) // Encryption encryptFile(inputFileName, outputFileName, password, algorithm); else // Decryption @@ -278,25 +258,25 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin // If we reach here, the operation was successful auto pre = mode == OperationMode::Encryption ? "En" : "De"; - printColor(std::format("{}cryption completed successfully. \n{}crypted file saved as ", pre, pre), 'g'); - printColor(outputFileName, 'b', true); + printColoredOutput('g', "{}cryption completed successfully.\n{}crypted file saved as ", pre, pre); + printColoredOutputln('b', "{}", outputFileName); // Preserve file permissions if (!copyFilePermissions(inputFileName, outputFileName)) [[unlikely]] - printColor(std::format("Check the permissions of the {}crypted file.", pre), 'm', true); + printColoredOutputln('m', "Check the permissions of the {}crypted file.", pre); // Try to preserve the time of last modification copyLastWrite(inputFileName, outputFileName); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); } } /// \brief Encrypts and decrypts files. void encryptDecrypt() { // I'm using hashmaps as an alternative to multiple if-else statements - const std::unordered_map algoChoice = { + const miSTL::unordered_map algoChoice = { {0, Algorithms::AES}, // Default {1, Algorithms::AES}, {2, Algorithms::Camellia}, @@ -305,7 +285,7 @@ void encryptDecrypt() { {5, Algorithms::Twofish} }; - const std::unordered_map algoDescription = { + const miSTL::unordered_map algoDescription = { {Algorithms::AES, "256-bit AES in CBC mode"}, {Algorithms::Camellia, "256-bit Camellia in CBC mode"}, {Algorithms::Aria, "256-bit Aria in CBC mode"}, @@ -314,18 +294,18 @@ void encryptDecrypt() { }; while (true) { - std::cout << "-------------"; - printColor(" file encryption/decryption utility ", 'c'); - std::cout << "-------------\n"; - printColor("1. Encrypt a file\n", 'g'); - printColor("2. Decrypt a file\n", 'm'); - printColor("3. Exit\n", 'r'); - std::cout << "--------------------------------------------------------------" << std::endl; + std::print("-------------"); + printColoredOutput('c', " file encryption/decryption utility "); + std::println("-------------"); + printColoredOutputln('g', "1. Encrypt a file"); + printColoredOutputln('m', "2. Decrypt a file"); + printColoredOutputln('r', "3. Exit"); + std::println("--------------------------------------------------------------"); if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { - std::string pre = choice == 1 ? "En" : "De"; // the prefix string - std::string pre_l{pre}; // the prefix in lowercase + miSTL::string pre = choice == 1 ? "En" : "De"; // the prefix string + miSTL::string pre_l{pre}; // the prefix in lowercase // Transform the prefix to lowercase std::ranges::transform(pre_l.begin(), pre_l.end(), pre_l.begin(), @@ -333,44 +313,36 @@ void encryptDecrypt() { return std::tolower(c); }); - printColor(std::format("Enter the path to the file to {}crypt:", pre_l), 'c', true); - std::string inputFile = getResponseStr(); - - // Remove the trailing directory separator - // ('\\' is considered as well in case the program is to be extended to Windows) - if ((inputFile.ends_with('/') || inputFile.ends_with('\\')) && inputFile.size() > 1) - inputFile.erase(inputFile.size() - 1); + printColoredOutputln('c', "Enter the path to the file to {}crypt:", pre_l); + fs::path inputPath = getFilesystemPath(); - fs::path inputPath(inputFile); if (!inputPath.is_absolute()) // The path should be absolute inputPath = fs::current_path() / inputPath; checkInputFile(inputPath, static_cast(choice)); - printColor(std::format("Enter the path to save the {}crypted file " - "\n(or leave it blank to save it in the same directory):", - pre_l), 'c', true); + printColoredOutputln('c', "Enter the path to save the {}crypted file" + "\n(or leave it blank to save it in the same directory):", pre_l); - fs::path outputPath{getResponseStr()}; + fs::path outputPath = getFilesystemPath(); if (!outputPath.is_absolute()) // If the path is not absolute outputPath = fs::current_path() / outputPath; checkOutputFile(inputPath, outputPath, static_cast(choice)); - std::cout << "Choose a cipher (All are 256-bit):\n"; - printColor("1. Advanced Encryption Standard (AES)\n", 'b'); - printColor("2. Camellia\n", 'c'); - printColor("3. Aria\n", 'g'); - printColor("4. Serpent\n", 'y'); - printColor("5. Twofish\n", 'm'); + std::println("Choose a cipher (All are 256-bit):"); + printColoredOutputln('b', "1. Advanced Encryption Standard (AES)"); + printColoredOutputln('c', "2. Camellia"); + printColoredOutputln('g', "3. Aria"); + printColoredOutputln('y', "4. Serpent"); + printColoredOutputln('m', "5. Twofish"); - std::cout << "Leave blank to use the default (AES)" << std::endl; + std::println("Leave blank to use the default (AES)"); int algo = getResponseInt(); if (algo < 0 || algo > 5) { // 0 is default (AES) - printColor("Invalid choice!", 'r', true, std::cerr); + printColoredErrorln('r', "Invalid choice!"); continue; } - const auto it = algoChoice.find(algo); auto cipher = it != algoChoice.end() ? it->second : Algorithms::AES; @@ -380,36 +352,33 @@ void encryptDecrypt() { if (choice == 1) { int tries{0}; while (password.empty() && ++tries < 3) { - printColor("Please avoid empty or weak passwords. Please try again.", 'r', true, std::cerr); + printColoredErrorln('r', "Please avoid empty or weak passwords. Please try again."); password = getSensitiveInfo("Enter the password: "); } - if (tries >= 3) throw std::runtime_error("Empty encryption password."); - const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; - - if (!verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, + if (const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; + !verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))) { - printColor("Passwords do not match.", 'r', true, std::cerr); + printColoredErrorln('r', "Passwords do not match."); continue; } } - printColor(std::format("{}crypting ", pre), 'g'); - printColor(canonical(inputPath).string(), 'b'); - printColor(" with ", 'g'); - printColor(algoDescription.find(cipher)->second, 'c'); - printColor("...", 'g', true); + printColoredOutput('g', "{}crypting '", pre); + printColoredOutput('m', "{}", canonical(inputPath).string()); + printColoredOutput('g', "' with "); + printColoredOutput('c', "{}", algoDescription.find(cipher)->second); + printColoredOutputln('g', "..."); - fileEncryptionDecryption(canonical(inputPath).string(), weakly_canonical(outputPath).string(), + fileEncryptionDecryption(canonical(inputPath).string().c_str(), weakly_canonical(outputPath).string().c_str(), password, cipher, static_cast(choice)); - std::cout << std::endl; + std::println(""); } catch (const std::exception &ex) { - printColor("Error: ", 'y', false, std::cerr); - printColor(ex.what(), 'r', true, std::cerr); - std::cerr << std::endl; + printColoredError('y', "Error: "); + printColoredErrorln('r', "{}", ex.what()); } } else if (choice == 3) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } } diff --git a/src/encryption/encryptFiles.cpp b/src/encryption/encryptFiles.cpp index 49fcadf..b45b0c4 100644 --- a/src/encryption/encryptFiles.cpp +++ b/src/encryption/encryptFiles.cpp @@ -30,12 +30,13 @@ module; import cryptoCipher; import secureAllocator; +import mimallocSTL; module encryption; -constexpr int MAX_KEY_SIZE = EVP_MAX_KEY_LENGTH; // For bounds checking -constexpr std::size_t CHUNK_SIZE = 4096; // Read/Write files in chunks of 4 kB -constexpr unsigned int PBKDF2_ITERATIONS = 100'000; // Iterations for PBKDF2 key derivation +constexpr int MAX_KEY_SIZE = EVP_MAX_KEY_LENGTH; ///< Maximum length of a key +constexpr std::streamsize CHUNK_SIZE = 4096; ///< Read/Write files in chunks of 4 kB +constexpr unsigned int PBKDF2_ITERATIONS = 100'000; ///< Iterations for PBKDF2 key derivation /// \brief Generates random bytes using a CSPRNG. @@ -45,7 +46,7 @@ privacy::vector generateSalt(const int saltSize) { std::mutex m; privacy::vector salt(saltSize); - if (std::scoped_lock lock(m); RAND_bytes(salt.data(), saltSize) != 1) { + if (std::scoped_lock lock(m); RAND_bytes(salt.data(), saltSize) != 1) { std::cerr << "Failed to seed OpenSSL's CSPRNG properly." "\nPlease check your system's randomness utilities." << std::endl; @@ -134,15 +135,15 @@ deriveKey(const privacy::string &password, const privacy::vector /// \details Encryption mode: CBC. /// \details Key derivation function: PBKDF2 with BLAKE2b512 as the digest function (salted). /// \details The IV is generated randomly with a CSPRNG and prepended to the encrypted file. -void encryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo) { +void encryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo) { // Open the input file for reading - std::ifstream inFile(inputFile, std::ios::binary); + std::ifstream inFile(inputFile.c_str(), std::ios::binary); if (!inFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFile)); // Open the output file for writing - std::ofstream outFile(outputFile, std::ios::binary | std::ios::trunc); + std::ofstream outFile(outputFile.c_str(), std::ios::binary | std::ios::trunc); if (!outFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFile)); @@ -184,33 +185,40 @@ void encryptFile(const std::string &inputFile, const std::string &outputFile, co outFile.write(reinterpret_cast(salt.data()), static_cast(salt.size())); outFile.write(reinterpret_cast(iv.data()), static_cast(iv.size())); - // Encrypt the file - std::vector inBuf(CHUNK_SIZE); - std::vector outBuf(CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH); - int bytesRead, bytesWritten; + // Buffers for file processing + unsigned char inBuf[CHUNK_SIZE]; + unsigned char outBuf[CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH]; + + // Lock the buffers + sodium_mlock(inBuf, CHUNK_SIZE); + sodium_mlock(outBuf, CHUNK_SIZE); - while (true) { + int bytesRead, bytesWritten; + // The encryption loop + while (!inFile.eof()) { // Read data from the file in chunks - inFile.read(reinterpret_cast(inBuf.data()), static_cast(inBuf.size())); + inFile.read(reinterpret_cast(inBuf), CHUNK_SIZE); bytesRead = static_cast(inFile.gcount()); - if (bytesRead <= 0) - break; - // Encrypt the data - if (EVP_EncryptUpdate(cipher.getCtx(), outBuf.data(), &bytesWritten, inBuf.data(), bytesRead) != 1) + // Encrypt the chunk + if (EVP_EncryptUpdate(cipher.getCtx(), outBuf, &bytesWritten, inBuf, bytesRead) != 1) throw std::runtime_error("Failed to encrypt the data."); // Write the ciphertext (the encrypted data) to the output file - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); // Ensure data is written immediately + outFile.write(reinterpret_cast(outBuf), bytesWritten); + // outFile.flush(); // Ensure data is written immediately } // Finalize the encryption operation - if (EVP_EncryptFinal_ex(cipher.getCtx(), outBuf.data(), &bytesWritten) != 1) + if (EVP_EncryptFinal_ex(cipher.getCtx(), outBuf, &bytesWritten) != 1) throw std::runtime_error("Failed to finalize encryption."); // Write the last chunk - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + + // Unlock and zeroize the buffers + sodium_munlock(inBuf, CHUNK_SIZE); + sodium_munlock(outBuf, CHUNK_SIZE); } /// \brief Decrypts a file encrypted by encryptFile() function. @@ -220,15 +228,15 @@ void encryptFile(const std::string &inputFile, const std::string &outputFile, co /// \param algo The cipher algorithm used to encrypt the file. /// /// \throws std::runtime_error if the decryption fails, and for other (documented) errors. -void decryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo) { +void decryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo) { // Open the input file for reading - std::ifstream inFile(inputFile, std::ios::binary); + std::ifstream inFile(inputFile.c_str(), std::ios::binary); if (!inFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFile)); // Open the output file for writing - std::ofstream outFile(outputFile, std::ios::binary | std::ios::trunc); + std::ofstream outFile(outputFile.c_str(), std::ios::binary | std::ios::trunc); if (!outFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFile)); @@ -278,33 +286,38 @@ void decryptFile(const std::string &inputFile, const std::string &outputFile, co // Set automatic padding handling EVP_CIPHER_CTX_set_padding(cipher.getCtx(), EVP_PADDING_PKCS7); - // Decrypt the file - privacy::vector inBuf(CHUNK_SIZE); - privacy::vector outBuf(CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH); + // Buffers for file processing + unsigned char inBuf[CHUNK_SIZE]; + unsigned char outBuf[CHUNK_SIZE + EVP_MAX_BLOCK_LENGTH]; + + // Lock the buffers + sodium_mlock(inBuf, CHUNK_SIZE); + sodium_mlock(outBuf, CHUNK_SIZE); int bytesRead, bytesWritten; - while (true) { + while (!inFile.eof()) { // Read the data in chunks - inFile.read(reinterpret_cast(inBuf.data()), static_cast(inBuf.size())); + inFile.read(reinterpret_cast(inBuf), CHUNK_SIZE); bytesRead = static_cast(inFile.gcount()); - if (bytesRead <= 0) - break; - // Decrypt the data - if (EVP_DecryptUpdate(cipher.getCtx(), outBuf.data(), &bytesWritten, inBuf.data(), bytesRead) != 1) + // Decrypt the chunk + if (EVP_DecryptUpdate(cipher.getCtx(), outBuf, &bytesWritten, inBuf, bytesRead) != 1) throw std::runtime_error("Failed to decrypt the data."); // Write the decrypted data to the output file - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + // outFile.flush(); } // Finalize the decryption operation - if (EVP_DecryptFinal_ex(cipher.getCtx(), outBuf.data(), &bytesWritten) != 1) + if (EVP_DecryptFinal_ex(cipher.getCtx(), outBuf, &bytesWritten) != 1) throw std::runtime_error("Failed to finalize decryption."); - outFile.write(reinterpret_cast(outBuf.data()), bytesWritten); - outFile.flush(); + outFile.write(reinterpret_cast(outBuf), bytesWritten); + + // Unlock and zeroize the buffers + sodium_munlock(inBuf, CHUNK_SIZE); + sodium_munlock(outBuf, CHUNK_SIZE); } /// \brief Throws a thread-safe Gcrypt error. @@ -331,15 +344,15 @@ inline void throwSafeError(const gcry_error_t &err, const std::string_view messa /// using PBKDF2 with BLAKE2b-512 as the hash function. /// \details The IV(nonce) is randomly generated and stored in the output file. void -encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, +encryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm) { // Open the input file for reading - std::ifstream inputFile(inputFilePath, std::ios::binary); + std::ifstream inputFile(inputFilePath.c_str(), std::ios::binary); if (!inputFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFilePath)); // Open the output file for writing - std::ofstream outputFile(outputFilePath, std::ios::binary | std::ios::trunc); + std::ofstream outputFile(outputFilePath.c_str(), std::ios::binary | std::ios::trunc); if (!outputFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFilePath)); @@ -383,22 +396,27 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o outputFile.write(reinterpret_cast(salt.data()), static_cast(salt.size())); outputFile.write(reinterpret_cast(ctr.data()), static_cast(ctr.size())); + unsigned char buffer[CHUNK_SIZE]; + // Lock the buffer + sodium_mlock(buffer, CHUNK_SIZE); + // Encrypt the file in chunks - privacy::vector buffer(CHUNK_SIZE); while (!inputFile.eof()) { - inputFile.read(reinterpret_cast(buffer.data()), CHUNK_SIZE); + inputFile.read(reinterpret_cast(buffer), CHUNK_SIZE); const auto bytesRead = inputFile.gcount(); // Encrypt the chunk - err = gcry_cipher_encrypt(cipherHandle, buffer.data(), buffer.size(), nullptr, 0); + err = gcry_cipher_encrypt(cipherHandle, buffer, CHUNK_SIZE, nullptr, 0); if (err) throwSafeError(err, "Failed to encrypt file"); // Write the encrypted chunk to the output file - outputFile.write(reinterpret_cast(buffer.data()), bytesRead); + outputFile.write(reinterpret_cast(buffer), bytesRead); } - // Clean up + // Release the handle gcry_cipher_close(cipherHandle); + // Unlock the buffer + sodium_munlock(buffer, CHUNK_SIZE); } /// \brief Decrypts a file encrypted by encryptFileWithMoreRounds() function. @@ -409,15 +427,15 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o /// /// \throws std::runtime_error if the decryption fails, and for other (documented) errors. void -decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, +decryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm) { // Open the input file for reading - std::ifstream inputFile(inputFilePath, std::ios::binary); + std::ifstream inputFile(inputFilePath.c_str(), std::ios::binary); if (!inputFile) throw std::runtime_error(std::format("Failed to open '{}' for reading.", inputFilePath)); // Open the output file for writing - std::ofstream outputFile(outputFilePath, std::ios::binary | std::ios::trunc); + std::ofstream outputFile(outputFilePath.c_str(), std::ios::binary | std::ios::trunc); if (!outputFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFilePath)); @@ -466,20 +484,25 @@ decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o if (err) throwSafeError(err, "Failed to set the decryption counter"); + unsigned char buffer[CHUNK_SIZE]; + // Lock the buffer + sodium_mlock(buffer, CHUNK_SIZE); + // Decrypt the file in chunks - privacy::vector buffer(CHUNK_SIZE); while (!inputFile.eof()) { - inputFile.read(reinterpret_cast(buffer.data()), CHUNK_SIZE); + inputFile.read(reinterpret_cast(buffer), CHUNK_SIZE); const auto bytesRead = inputFile.gcount(); // Decrypt the chunk in place - err = gcry_cipher_decrypt(cipherHandle, buffer.data(), buffer.size(), nullptr, 0); + err = gcry_cipher_decrypt(cipherHandle, buffer, CHUNK_SIZE, nullptr, 0); if (err) throwSafeError(err, "Failed to decrypt the ciphertext"); // Write the decrypted chunk to the output file - outputFile.write(reinterpret_cast(buffer.data()), bytesRead); + outputFile.write(reinterpret_cast(buffer), bytesRead); } // Release resources gcry_cipher_close(cipherHandle); + // Unlock the buffer + sodium_munlock(buffer, CHUNK_SIZE); } diff --git a/src/encryption/encryptStrings.cpp b/src/encryption/encryptStrings.cpp index d781706..b3b26f9 100644 --- a/src/encryption/encryptStrings.cpp +++ b/src/encryption/encryptStrings.cpp @@ -25,6 +25,7 @@ module; import utils; import secureAllocator; +import mimallocSTL; import cryptoCipher; module encryption; @@ -42,7 +43,7 @@ module encryption; /// \details The key is derived from the password using PBKDF2 with 100,000 rounds (salted). /// \details The IV is generated randomly using a CSPRNG and prepended to the ciphertext. privacy::string -encryptString(const privacy::string &plaintext, const privacy::string &password, const std::string &algo) { +encryptString(const privacy::string &plaintext, const privacy::string &password, const miSTL::string &algo) { CryptoCipher cipher; // Create the cipher context @@ -112,7 +113,7 @@ encryptString(const privacy::string &plaintext, const privacy::string &password, /// /// \throws std::runtime_error if the decryption operation fails. privacy::string -decryptString(const std::string_view encodedCiphertext, const privacy::string &password, const std::string &algo) { +decryptString(const std::string_view encodedCiphertext, const privacy::string &password, const miSTL::string &algo) { CryptoCipher cipher; // Create the cipher context @@ -134,7 +135,7 @@ decryptString(const std::string_view encodedCiphertext, const privacy::string &p privacy::vector encryptedText; // Base64 decode the encoded ciphertext - if (std::vector ciphertext = base64Decode(encodedCiphertext); + if (miSTL::vector ciphertext = base64Decode(encodedCiphertext); ciphertext.size() > static_cast(SALT_SIZE) + ivSize) [[likely]] { // Read the salt and IV from the ciphertext salt.assign(ciphertext.begin(), ciphertext.begin() + SALT_SIZE); @@ -277,7 +278,7 @@ decryptStringWithMoreRounds(const std::string_view encodedCiphertext, const priv privacy::vector encryptedText; // Base64-decode the encoded ciphertext - if (std::vector ciphertext = base64Decode(encodedCiphertext); + if (miSTL::vector ciphertext = base64Decode(encodedCiphertext); ciphertext.size() >= SALT_SIZE + ctrSize) [[likely]] { // Read the salt and the counter from the ciphertext salt.assign(ciphertext.begin(), ciphertext.begin() + SALT_SIZE); diff --git a/src/encryption/encryption.cppm b/src/encryption/encryption.cppm index fff6742..6d1284f 100644 --- a/src/encryption/encryption.cppm +++ b/src/encryption/encryption.cppm @@ -22,6 +22,7 @@ module; export module encryption; import secureAllocator; +import mimallocSTL; constexpr int SALT_SIZE = 32; // Default salt length (256 bits) constexpr int KEY_SIZE_256 = 32; // Default key size (256 bits) @@ -37,32 +38,32 @@ export { deriveKey(const privacy::string &password, const privacy::vector &salt, const int &keySize = KEY_SIZE_256); - void encryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + void encryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo = "AES-256-CBC"); void - encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, + encryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); - void decryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + void decryptFile(const miSTL::string &inputFile, const miSTL::string &outputFile, const privacy::string &password, + const miSTL::string &algo = "AES-256-CBC"); void - decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, + decryptFileWithMoreRounds(const miSTL::string &inputFilePath, const miSTL::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); privacy::string encryptString(const privacy::string &plaintext, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + const miSTL::string &algo = "AES-256-CBC"); privacy::string encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); privacy::string decryptString(std::string_view encodedCiphertext, const privacy::string &password, - const std::string &algo = "AES-256-CBC"); + const miSTL::string &algo = "AES-256-CBC"); privacy::string decryptStringWithMoreRounds(std::string_view encodedCiphertext, const privacy::string &password, const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); diff --git a/src/fileShredder/fileShredder.cppm b/src/fileShredder/fileShredder.cppm index 2541985..a866ace 100644 --- a/src/fileShredder/fileShredder.cppm +++ b/src/fileShredder/fileShredder.cppm @@ -17,7 +17,6 @@ module; #include -#include #include #include #include @@ -25,12 +24,15 @@ module; #include #include #include +#include #include +#include using StatType = struct stat; export module fileShredder; import utils; +import mimallocSTL; namespace fs = std::filesystem; constexpr std::streamoff BUFFER_SIZE = 4096; @@ -55,7 +57,7 @@ void overwriteRandom(std::ofstream &file, const std::size_t fileSize, const int // (Re)seed the Mersenne Twister engine in every pass std::mt19937_64 gen(rd()); - std::vector buffer(BUFFER_SIZE); + miSTL::vector buffer(BUFFER_SIZE); // Overwrite the file with random data for (std::size_t pos = 0; pos < fileSize; pos += BUFFER_SIZE) { @@ -90,7 +92,7 @@ void overwriteConstantByte(std::ofstream &file, T &byte, const auto &fileSize) { // seek to the beginning of the file file.seekp(0, std::ios::beg); - std::vector buffer(BUFFER_SIZE, byte); + miSTL::vector buffer(BUFFER_SIZE, byte); for (std::streamoff pos = 0; pos < fileSize; pos += BUFFER_SIZE) { if (pos + BUFFER_SIZE > fileSize) { @@ -124,10 +126,10 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { std::uniform_int_distribution numDist(minNameLength, maxNameLength); // Get the file extension using std::filesystem - const std::string fileExtension = fs::path(filename).extension().string(); + const miSTL::string fileExtension = fs::path(filename).extension().string().c_str(); // Generate a random name using the safe characters (Not exhaustive) - const std::string safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + constexpr std::string_view safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; std::uniform_int_distribution dist(0, safeChars.size() - 1); fs::path path(filename); @@ -141,7 +143,7 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { // Generate a random number of characters for the new name const int numChars = numDist(gen); - std::string newName; + miSTL::string newName; // Generate a random name for (int j = 0; j < numChars; ++j) newName += safeChars[dist(gen)]; @@ -161,9 +163,9 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { fs::remove(path, ec); if (ec) { - printColor("Failed to delete ", 'r', false, std::cerr); - printColor(filename, 'm', false, std::cerr); - printColor(std::format(": {}", ec.message()), 'r', true, std::cerr); + printColoredError('r', "Failed to delete "); + printColoredError('m', "{}", filename); + printColoredErrorln('r', ": {}", ec.message()); } } @@ -176,9 +178,9 @@ inline void renameAndRemove(const std::string_view filename, int numTimes = 1) { struct FileDescriptor { int fd{-1}; - explicit FileDescriptor(const std::string &filename) : fd(open(filename.c_str(), O_RDWR)) { + explicit FileDescriptor(const miSTL::string &filename) : fd(open(filename.c_str(), O_RDWR)) { if (fd == -1) - throw std::runtime_error("Failed to open file: " + filename + " (" + std::strerror(errno) + ")"); + throw std::runtime_error(std::format("Failed to open file: {} ({})", filename, std::strerror(errno))); } ~FileDescriptor() { if (fd != -1) close(fd); } @@ -202,7 +204,7 @@ struct FileStatInfo { /// \brief wipes the cluster tips of a file. /// \param fileName the path to the file to be wiped. /// \throws std::runtime_error if zeroing the cluster tips fails. -inline void wipeClusterTips(const std::string &fileName) { +inline void wipeClusterTips(const miSTL::string &fileName) { const FileDescriptor fileDescriptor(fileName); const FileStatInfo fileInformation(fileDescriptor.fd); @@ -213,14 +215,13 @@ inline void wipeClusterTips(const std::string &fileName) { if (clusterTipSize >= fileInformation.fileStat.st_size) { clusterTipSize = 0; } - // Seek to the end of the file if (lseek(fileDescriptor.fd, 0, SEEK_END) == -1) { throw std::runtime_error(std::format("Failed to seek to end of file: ({})", std::strerror(errno))); } // Write zeros to the cluster tip - const std::vector zeroBuffer(clusterTipSize, 0); + const miSTL::vector zeroBuffer(clusterTipSize, 0); if (write(fileDescriptor.fd, zeroBuffer.data(), zeroBuffer.size()) == static_cast(-1)) { throw std::runtime_error(std::format("Failed to write zeros: ({})", std::strerror(errno))); @@ -233,10 +234,10 @@ inline void wipeClusterTips(const std::string &fileName) { /// \param wipeClusterTip whether to wipe the cluster tips of the file. /// /// \throws std::runtime_error if the file cannot be opened. -void simpleShred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { - std::ofstream file(filename, std::ios::binary | std::ios::in); +void simpleShred(const miSTL::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { + std::ofstream file(filename.c_str(), std::ios::binary | std::ios::in); if (!file) - throw std::runtime_error("\nFailed to open file: " + filename); + throw std::runtime_error(std::format("\nFailed to open file: {}", filename)); std::error_code ec; // Read last write time @@ -268,10 +269,10 @@ void simpleShred(const std::string &filename, const int &nPasses = 3, const bool /// \param wipeClusterTip whether to wipe the cluster tips of the file. /// /// \throws std::runtime_error if the file cannot be opened, or if the number of passes is invalid. -void dod5220Shred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { - std::ofstream file(filename, std::ios::binary | std::ios::in); +void dod5220Shred(const miSTL::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { + std::ofstream file(filename.c_str(), std::ios::binary | std::ios::in); if (!file) - throw std::runtime_error("\nFailed to open file: " + filename); + throw std::runtime_error(std::format("\nFailed to open file: {}", filename)); std::error_code ec; // Read last write time @@ -321,10 +322,10 @@ void dod5220Shred(const std::string &filename, const int &nPasses = 3, const boo /// \enum ShredOptions /// \brief Represents the different shredding options. enum class ShredOptions : std::uint_fast8_t { - Simple = 1 << 0, // Simple overwrite with random bytes - Dod5220 = 1 << 1, // DoD 5220.22-M Standard algorithm - Dod5220_7 = 1 << 2, // DoD 5220.22-M Standard algorithm with 7 passes - WipeClusterTips = 1 << 3 // Wiping of the cluster tips + Simple = 1 << 0, ///< Simple overwrite with random bytes + Dod5220 = 1 << 1, ///< DoD 5220.22-M Standard algorithm + Dod5220_7 = 1 << 2, ///< DoD 5220.22-M Standard algorithm with 7 passes + WipeClusterTips = 1 << 3 ///< Wiping of the cluster tips }; /// \brief Adds write and write permissions to a file, if the user has authority. @@ -332,14 +333,14 @@ enum class ShredOptions : std::uint_fast8_t { /// \return True if the operation succeeds, else false. /// /// \details The actions of this function are similar to the unix command: -/// \code chmod ugo+rw fileName \endcode or \code chmod a+rw fileName \endcode +/// \code chmod ugo+rw fileName \endcode or \code chmod a+rw fileName \endcode. \n /// The read/write permissions are added for everyone. /// \note This function is meant for the file shredder ONLY, which might /// need to modify a file's permissions (if and only if it has to) to successfully shred it. /// /// \warning Modifying file permissions unnecessarily is a serious security risk, /// and this program doesn't take that for granted. -inline bool addReadWritePermissions(const std::string_view fileName) noexcept { +static inline bool addReadWritePermissions(const std::string_view fileName) noexcept { std::error_code ec; permissions(fileName, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write | fs::perms::others_read | fs::perms::others_write, @@ -359,21 +360,21 @@ inline bool addReadWritePermissions(const std::string_view fileName) noexcept { /// /// \warning If the filePath is a directory, then all its files and subdirectories /// are shredded without warning. -bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, const int &simplePasses = 3) { +bool shredFiles(const miSTL::string &filePath, const std::uint_fast8_t &options, const int &simplePasses = 3) { std::error_code ec; const fs::file_status fileStatus = fs::status(filePath, ec); if (ec) { - printColor("Unable to determine ", 'y', false, std::cerr); - printColor(filePath, 'b', false, std::cerr); + printColoredError('y', "Unable to determine "); + printColoredError('b', "{}", filePath); - printColor("'s status: ", 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "'s status: "); + printColoredErrorln('r', "{}", ec.message()); return false; } // Check if the file exists and is a regular file. if (!exists(fileStatus)) { - printColor(filePath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}", filePath); + printColoredErrorln('r', " does not exist."); return false; } // If the filepath is a directory, shred all the files in the directory and all its subdirectories @@ -381,8 +382,8 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c if (fs::is_empty(filePath, ec)) { if (ec) ec.clear(); else { - printColor(filePath, 'c'); - printColor(" is an empty directory.", 'y', true); + printColoredOutput('c', "{}", filePath); + printColoredOutputln('y', " is an empty directory."); return true; } } @@ -392,48 +393,48 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c for (const auto &entry: fs::recursive_directory_iterator(filePath)) { if (entry.exists(ec)) { if (ec) { - printColor(ec.message(), 'r', true, std::cerr); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } if (!is_directory(entry.status())) { - printColor("Shredding ", 'c'); - printColor(canonical(entry.path()).string(), 'b'); - printColor(" ...", 'c'); + printColoredOutput('c', "Shredding "); + printColoredOutput('b', "{}", canonical(entry.path()).string()); + printColoredOutput('c', " ..."); try { - const bool shredded = shredFiles(entry.path().string(), options); - printColor(shredded ? "\tshredded successfully." : "\tshredding failed.", shredded ? 'g' : 'r', - true); + const bool shredded = shredFiles(entry.path().string().c_str(), options); + printColoredOutputln(shredded ? 'g' : 'r', "{}", + shredded ? "\tshredded successfully." : "\tshredding failed."); ++(shredded ? numShredded : numNotShredded); } catch (const std::runtime_error &err) { - printColor("Shredding failed: ", 'y', false, std::cerr); - printColor(err.what(), 'r', true, std::cerr); + printColoredError('y', "Shredding failed: "); + printColoredErrorln('r', "{}", err.what()); } } } } if (numNotShredded == 0) // All files in the directory and all subdirectories were shredded successfully. remove_all(fs::canonical(filePath)); - else printColor("Failed to shred some files.", 'r', true, std::cerr); + else printColoredErrorln('r', "Failed to shred some files."); - std::cout << "\nProcessed " << numShredded + numNotShredded << " files." << std::endl; + std::println("\nProcessed {} files.", numShredded + numNotShredded); if (numShredded) { - printColor("Successfully shredded and deleted: ", 'g'); - printColor(numShredded, 'b', true); + printColoredOutput('g', "Successfully shredded and deleted: "); + printColoredOutputln('b', "{}", numShredded); } if (numNotShredded) { - printColor("Failed to shred ", 'r', false, std::cerr); - printColor(numNotShredded, 'b', false, std::cerr); - printColor(" files.", 'r', true, std::cerr); + printColoredError('r', "Failed to shred "); + printColoredError('b', "{}", numNotShredded); + printColoredErrorln('r', " files."); } return true; } if (!is_regular_file(fileStatus)) { - printColor(filePath, 'c', false, std::cerr); - printColor(" is not a regular file.", 'r', true, std::cerr); - printColor("Do you want to (try to) shred the file anyway? (y/n):", 'y', true); + printColoredError('c', "{}", filePath); + printColoredError('r', " is not a regular file."); + printColoredOutputln('y', "Do you want to (try to) shred the file anyway? (y/n):"); if (!validateYesNo()) return false; } @@ -441,8 +442,8 @@ bool shredFiles(const std::string &filePath, const std::uint_fast8_t &options, c // Check file permissions if (!isWritable(filePath) || !isReadable(filePath)) { if (!addReadWritePermissions(filePath)) { - printColor("\nInsufficient permissions to shred file: ", 'r', false, std::cerr); - printColor(filePath, 'c', true, std::cerr); + printColoredError('r', "\nInsufficient permissions to shred file: "); + printColoredErrorln('c', "{}", filePath); return false; } } @@ -472,11 +473,10 @@ export void fileShredder() { preferences |= std::to_underlying(ShredOptions::Simple) | wipeTips; } else if (moreChoices1 == 2) { // Configure shredding options - const int alg = getResponseInt("\nChoose a shredding algorithm:\n" + if (const int alg = getResponseInt("\nChoose a shredding algorithm:\n" "1. Overwrite with random bytes (default)\n" "2. 3-pass DoD 5220.22-M Standard algorithm\n" - "3. 7-pass DoD 5220.22-M Standard algorithm"); - if (alg == 1) { + "3. 7-pass DoD 5220.22-M Standard algorithm"); alg == 1) { preferences |= std::to_underlying(ShredOptions::Simple) | wipeTips; do { @@ -500,7 +500,7 @@ export void fileShredder() { } else if (simpleConfig == 4) { // Abort throw std::runtime_error("Operation aborted."); - } else printColor("Invalid option", 'r', true, std::cerr); + } else printColoredErrorln('r', "Invalid option"); } while (true); } else if (alg == 2 || alg == 3) { // DoD 5220.22-M Standard algorithms @@ -513,30 +513,26 @@ export void fileShredder() { }; while (true) { - printColor("\n------------------", 'g'); - printColor(" file shredder ", 'm'); - printColor("------------------", 'g', true); + printColoredOutput('g', "\n------------------"); + printColoredOutput('m', " file shredder "); + printColoredOutputln('g', "------------------"); - printColor("1. Shred a file", 'y', true); - printColor("2. Shred a directory", 'y', true); - printColor("3. Exit", 'r', true); + printColoredOutputln('y', "1. Shred a file"); + printColoredOutputln('y', "2. Shred a directory"); + printColoredOutputln('r', "3. Exit"); - printColor("---------------------------------------------------", 'g', true); + printColoredOutputln('g', "---------------------------------------------------"); if (const int choice = getResponseInt("Enter your choice: "); choice == 1 || choice == 2) { try { // Get the path to the file or directory to shred - std::string path = getResponseStr(std::format("Enter the path to the {} you would like to shred:", - choice == 1 ? "file" : "directory")); - - // Remove trailing slashes - if (const auto len = path.size(); len > 1 && (path.ends_with('/') || path.ends_with('\\'))) - path.erase(len - 1); + fs::path path = getFilesystemPath(std::format("Enter the path to the {} you would like to shred:", + choice == 1 ? "file" : "directory").c_str()); std::error_code ec; const fs::file_status fileStatus = fs::status(path, ec); if (ec) { - printColor(ec.message(), 'r', true, std::cerr); + printColoredErrorln('r', "{}", ec.message()); ec.clear(); continue; } @@ -545,23 +541,23 @@ export void fileShredder() { // Check if the file or directory exists if (!exists(fileStatus)) { - printColor(canonicalPath, 'c', false, std::cerr); - printColor(" does not exist.", 'r', true, std::cerr); + printColoredError('c', "{}", canonicalPath); + printColoredErrorln('r', " does not exist."); continue; } // If the path is a directory, shred all the files in the directory and all subdirectories (with confirmation) if (choice == 1 && isDir) { - printColor(canonicalPath, 'c'); - printColor(" is a directory.", 'r', true); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('r', " is a directory."); - printColor("Shred all files in '", 'y'); - printColor(canonicalPath, 'c'); - printColor("'\nand all its subdirectories? (y/n):", 'y', true); + printColoredOutput('y', "Shred all files in '"); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('y', "'\nand all its subdirectories? (y/n):"); if (!validateYesNo()) continue; } else if (choice == 2 && !isDir) { // If the path is a file, shred it without confirmation - printColor(canonicalPath, 'c'); - printColor(" is not a directory.", 'r', true); + printColoredOutput('c', "{}", canonicalPath); + printColoredOutputln('r', " is not a directory."); if (!validateYesNo("Shred it anyway? (y/n):")) continue; } std::uint_fast8_t preferences{0}; @@ -570,27 +566,27 @@ export void fileShredder() { try { selectPreferences(preferences, simpleNumPass); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); continue; } - printColor(std::format("The {} contents will be lost permanently.\nContinue? (y/n)", - isDir ? "directory's (and all its subdirectories')" : "file"), 'r', true); + printColoredOutputln('r', "The {} contents will be lost permanently.\nContinue? (y/n)", + isDir ? "directory's (and all its subdirectories')" : "file"); if (validateYesNo()) { std::cout << "Shredding '"; - printColor(canonicalPath, 'c'); + printColoredOutput('c', "{}", canonicalPath); std::cout << "'..." << std::endl; - const bool shredded = shredFiles(path, preferences, simpleNumPass); + const bool shredded = shredFiles(path.string().c_str(), preferences, simpleNumPass); if (!isDir) { - printColor(shredded ? "Successfully shredded " : "Failed to shred ", shredded ? 'g' : 'r', - false, shredded ? std::cout : std::cerr); - printColor(canonicalPath, 'c', true, shredded ? std::cout : std::cerr); + printColoredOutput(shredded ? 'g' : 'r', "{}", + shredded ? "Successfully shredded " : "Failed to shred "); + printColoredOutputln('c', "{}", canonicalPath); } } } catch (const std::exception &err) { - printColor(std::format("Error: {}", err.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", err.what()); } } else if (choice == 3) break; - else printColor("Invalid choice.", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice."); } } diff --git a/src/main.cpp b/src/main.cpp index 11a7953..217db68 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -29,37 +28,56 @@ import privacyTracks; import encryption; import passwordManager; import fileShredder; +import mimallocSTL; import utils; -constexpr const char *const MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; +constexpr auto MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; int main(const int argc, const char **argv) { // The program should be launched in interactive mode if (!isatty(STDIN_FILENO)) { if (errno == ENOTTY) { - printColor(std::format("{} is meant to be run interactively.", argv[0]), 'r', true, std::cerr); + printColoredErrorln('r', "{} is meant to be run interactively.", argv[0]); return 1; } } // Disable core dumping for security reasons if (constexpr rlimit coreLimit{0, 0}; setrlimit(RLIMIT_CORE, &coreLimit) != 0) { - printColor("Failed to disable core dumps.", 'r', true, std::cerr); + printColoredErrorln('r', "Failed to disable core dumps."); return 1; } - // No arguments required + // Configure the color output, if necessary + configureColor(); + + // Only the first argument is considered if (argc > 1) { - printColor("Ignoring extra arguments: ", 'y'); - for (int i = 1; i < argc; printColor(std::format("{} ", argv[i++]), 'r')) {} + // Disable color output if requested + if (std::string_view(argv[1]) == "--no-color" || std::string_view(argv[1]) == "-nc") { + configureColor(true); + } else { + printColoredError('y', "The option "); + printColoredError('r', "{} ", argv[1]); + printColoredErrorln('y', "is not recognized."); + + printColoredError('y', "Usage: "); + printColoredErrorln('r', "{} [--no-color | -nc]", argv[0]); + } + } + + if (argc > 2) { + printColoredOutput('y', "Ignoring extra arguments: ", 'y'); + for (int i = 2; i < argc; printColoredOutput('r', "{} ", argv[i++])) { + } std::cout << std::endl; } // Handle the keyboard interrupt (SIGINT) signal (i.e., Ctrl+C) struct sigaction act{}; act.sa_handler = [](int /* unused */) noexcept -> void { - printColor("Keyboard interrupt detected. Unsaved data might be lost if you quit now." - "\nDo you still want to quit? (y/n):", 'r'); + printColoredOutput('r', "Keyboard interrupt detected.\nUnsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); if (validateYesNo()) std::exit(1); }; @@ -97,22 +115,22 @@ int main(const int argc, const char **argv) { throw std::runtime_error("Failed to initialize libsodium."); // Display information about the program - printColor("\nPrivacy Shield 2.5.0\n", 'c'); - printColor("Copyright (C) 2024 Ian Duncan.\n", 'b'); + printColoredOutputln('c', "\nPrivacy Shield 3.0.0"); + printColoredOutputln('b', "Copyright (C) 2024 Ian Duncan."); - printColor("This program comes with ", 'g'); - printColor("ABSOLUTELY NO WARRANTY.", 'r'); + printColoredOutput('g', "This program comes with "); + printColoredOutputln('r', "ABSOLUTELY NO WARRANTY."); - printColor("\nThis is a free software; you are free to change and redistribute it\n" - "under the terms of the ", 'g'); - printColor("GNU General Public License v3 ", 'r'); - printColor("or later.", 'g'); + printColoredOutput('g', "This is a free software; you are free to change and redistribute it\n" + "under the terms of the "); + printColoredOutput('r', "GNU General Public License v3 "); + printColoredOutputln('g', "or later."); - printColor("\nFor more information, see ", 'g'); - printColor("https://www.gnu.org/licenses/gpl.html.\n", 'b', true); + printColoredOutput('g', "For more information, see "); + printColoredOutputln('b', "https://www.gnu.org/licenses/gpl.html."); // All the available tools - std::unordered_map > apps = { + miSTL::unordered_map > apps = { {1, passwordManager}, {2, encryptDecrypt}, {3, fileShredder}, @@ -122,14 +140,14 @@ int main(const int argc, const char **argv) { // Applications loop while (true) { - printColor("-------------------------------------\n", 'c'); - printColor("1. Manage passwords\n", 'b'); - printColor("2. Encrypt/decrypt files\n", 'g'); - printColor("3. Shred files\n", 'm'); - printColor("4. Clear browser privacy traces\n", 'y'); - printColor("5. Find duplicate files\n", 'b'); - printColor("6. Exit\n", 'r'); - printColor("-------------------------------------", 'c', true); + printColoredOutputln('c', "-------------------------------------"); + printColoredOutputln('b', "1. Manage passwords"); + printColoredOutputln('g', "2. Encrypt/decrypt files"); + printColoredOutputln('m', "3. Shred files"); + printColoredOutputln('y', "4. Clear browser privacy traces"); + printColoredOutputln('b', "5. Find duplicate files"); + printColoredOutputln('r', "6. Exit"); + printColoredOutputln('c', "-------------------------------------"); const int choice = getResponseInt("What would you like to do? (Enter 1 or 2, 3..)"); @@ -138,24 +156,23 @@ int main(const int argc, const char **argv) { iter->second(); else if (choice == 6) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } catch (const std::bad_function_call &bc) { // In case the std::function objects are called inappropriately - printColor(std::format("Bad function call: {}", bc.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Bad function call: {}", bc.what()); } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); } catch (...) { // All other exceptions, if any - printColor("An error occurred.", 'r', true, std::cerr); + printColoredErrorln('r', "An error occurred."); } } - return 0; } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); return 1; } catch (...) { - printColor("Something went wrong.", 'r', true, std::cerr); + printColoredErrorln('r', "Something went wrong."); return 1; } } diff --git a/src/mimallocSTL.cppm b/src/mimallocSTL.cppm new file mode 100644 index 0000000..249f1b7 --- /dev/null +++ b/src/mimallocSTL.cppm @@ -0,0 +1,310 @@ +// Privacy Shield: A Suite of Tools Designed to Facilitate Privacy Management. +// Copyright (C) 2024 Ian Duncan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://www.gnu.org/licenses. + +module; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __has_include +#if __has_include() +#include // for feature test macros +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#else // __has_include + +#if __cpp_lib_flat_set +#include +#endif + +#if __cpp_lib_flat_map +#include +#endif + +#if __cpp_lib_span +#include +#endif + +#if __cpp_lib_mdspan +#include +#endif + +#if __cpp_lib_syncbuf +#include +#endif + +#endif // __has_include + +export module mimallocSTL; + +export namespace miSTL { + /* ********** Sequence Containers ********** */ + // std::array doesn't need a custom allocator, we include it here for completeness + template + using array = std::array; + + // std::vector + template + using vector = std::vector >; + + // std::deque + template + using deque = std::deque >; + + // std::list + template + using list = std::list >; + + // std::forward_list + template + using forward_list = std::forward_list >; + + /* ********** Associative Containers ********** */ + // std::set + template > + using set = std::set >; + + // std::map + template< + class Key, + class T, + class Compare = std::less > + using map = std::map > >; + + // std::multiset + template< + class Key, + class Compare = std::less > + using multiset = std::multiset >; + + // std::multimap + template< + class Key, + class T, + class Compare = std::less > + using multimap = std::multimap > >; + + /* ********** Unordered Associative Containers ********** */ + // std::unordered_set + template< + class Key, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_set = std::unordered_set >; + + // std::unordered_map + template< + class Key, + class T, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_map = std::unordered_map > >; + + // std::unordered_multiset + template< + class Key, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_multiset = std::unordered_multiset >; + + // std::unordered_multimap + template< + class Key, + class T, + class Hash = std::hash, + class KeyEqual = std::equal_to > + using unordered_multimap = std::unordered_multimap > >; + + /* ********** Container Adaptors ********** */ + // std::stack + template > + using stack = std::stack; + + // std::queue + template > + using queue = std::queue; + + // std::priority_queue + template< + class T, + class Container = vector, + class Compare = std::less > + using priority_queue = std::priority_queue; + + // std::flat_set (C++23) +#if __cpp_lib_flat_set + template< + class Key, + class Compare = std::less, + class KeyContainer = vector > + using flat_set = std::flat_set; +#endif + + // std::flat_map (C++23) +#if __cpp_lib_flat_map + template< + class Key, + class T, + class Compare = std::less, + class KeyContainer = vector, + class MappedContainer = vector > + using flat_map = std::flat_map; +#endif + + // std::flat_multiset (C++23) +#if __cpp_lib_flat_multiset + template< + class Key, + class Compare = std::less, + class KeyContainer = vector > + using flat_multiset = std::flat_multiset; +#endif + + // std::flat_multimap (C++23) +#if __cpp_lib_flat_multimap + template< + class Key, + class T, + class Compare = std::less, + class KeyContainer = vector, + class MappedContainer = vector > + using flat_multimap = std::flat_multimap; +#endif + + /* ********** Views ********** */ + // views do not need custom allocators, they are included here for completeness + // std::span (C++20) +#if __cpp_lib_span + template + using span = std::span; +#endif + + // std::mdspan (C++23) +#if __cpp_lib_mdspan + template< + class T, + class Extents, + class LayoutPolicy = std::layout_right, + class AccessorPolicy = std::default_accessor > + using mdspan = std::mdspan; +#endif + + /* ********** Strings ********** */ + // std::basic_string + template > + using basic_string = std::basic_string >; + + // std::string + using string = basic_string; + + // std::string_view included for completeness, as it doesn't need a custom allocator + template > + using basic_string_view = std::basic_string_view; + using string_view = basic_string_view; + + /* ********** I/O Streams ********** */ + // std::basic_stringbuf + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_stringbuf = std::basic_stringbuf; + + // std::basic_istringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_istringstream = std::basic_istringstream; + + // std::basic_ostringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_ostringstream = std::basic_ostringstream; + + // std::basic_stringstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_stringstream = std::basic_stringstream; + + // std::basic_syncbuf + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_syncbuf = std::basic_syncbuf; + + // std::basic_osyncstream + template< + class CharT, + class Traits = std::char_traits, + class Allocator = mi_stl_allocator > + using basic_osyncstream = std::basic_osyncstream; + + // std::stringbuf + using stringbuf = basic_stringbuf; + + // std::istringstream + using istringstream = basic_istringstream; + + // std::stringstream + using stringstream = basic_stringstream; + + // std::ostringstream + using ostringstream = basic_ostringstream; + + // std::syncbuf + using syncbuf = basic_syncbuf; + + // std::osyncstream + using osyncstream = basic_osyncstream; +} // namespace miSTL diff --git a/src/passwordManager/FuzzyMatcher.cppm b/src/passwordManager/FuzzyMatcher.cppm index 56dacd9..6b748aa 100644 --- a/src/passwordManager/FuzzyMatcher.cppm +++ b/src/passwordManager/FuzzyMatcher.cppm @@ -22,6 +22,7 @@ module; export module FuzzyMatcher; import secureAllocator; +import mimallocSTL; template /// \brief A concept describing a range of strings. @@ -88,8 +89,8 @@ public: /// \param pattern the pattern to match. /// \param maxDistance the maximum Levenshtein Distance to consider a match. /// \return a vector of strings matching the pattern. - [[nodiscard]] std::vector fuzzyMatch(const std::string_view pattern, const int &maxDistance) const { - std::vector matches{}; + [[nodiscard]] miSTL::vector fuzzyMatch(const std::string_view pattern, const int &maxDistance) const { + miSTL::vector matches{}; matches.reserve(stringList.size()); // Worst case: every string in stringList is a match. // The maximum and minimum size of a string to be considered a match const auto maxSize{pattern.size() + maxDistance + 1}; @@ -110,7 +111,7 @@ public: private: - std::vector stringList{}; + miSTL::vector stringList{}; /// \brief Calculates the Levenshtein Distance between two strings. /// \param str1 the first string. @@ -122,7 +123,7 @@ private: const int m = static_cast(str1.length()); const int n = static_cast(str2.length()); - std::vector> dp(m + 1, std::vector(n + 1)); + miSTL::vector> dp(m + 1, miSTL::vector(n + 1)); // Initialize the first row and column for (int i = 0; i <= m; ++i) diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index ec55bc3..3ad0a7f 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -31,11 +31,12 @@ module; import utils; import FuzzyMatcher; import secureAllocator; +import mimallocSTL; module passwordManager; namespace fs = std::filesystem; -using string = std::string; +using string = miSTL::string; const string DefaultPasswordFile = getHomeDir() + "/.privacyShield/passwords"; /// \brief A binary predicate for searching, sorting, and deduplication of the password records, @@ -44,14 +45,10 @@ const string DefaultPasswordFile = getHomeDir() + "/.privacyShield/passwords"; /// \param rhs another record to be compared with lhs. /// \return true if lhs is less than (i.e. is ordered before) rhs, else false. bool constexpr comparator -// Avoid a compiler error on ignored scoped attribute directives (-Werror=attributes is enabled in debug config), -// while still encouraging both Clang and GCC compilers to inline the function. -#if __clang__ -[[clang::always_inline]] -#elif __GNUC__ +#if __clang__ || __GNUC__ [[gnu::always_inline]] #endif - (const auto &lhs, const auto &rhs) noexcept { +(const auto &lhs, const auto &rhs) noexcept { // Compare the site and username members of the tuples return std::tie(std::get<0>(lhs), std::get<1>(lhs)) < std::tie(std::get<0>(rhs), std::get<1>(rhs)); @@ -65,16 +62,16 @@ constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false if (!site.empty()) { // Skip blank entries std::cout << "Site/app: "; - printColor(site, 'c'); + printColoredOutput('c', "{}", site); } if (!username.empty()) { std::cout << "\nUsername: "; - printColor(username, 'b'); + printColoredOutput('b', "{}", username); } // Highlight a weak password std::cout << "\nPassword: "; - printColor(pass, isStrong ? 'g' : 'r', true); + printColoredOutputln(isStrong ? 'g' : 'r', "{}", pass); } /// \brief This function computes the strength of each password in the provided list of passwords. @@ -89,12 +86,10 @@ constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false /// /// \note This function is always inlined by the compiler. constexpr void computeStrengths -#if __clang__ -[[clang::always_inline]] -#elif __GNUC__ +#if __clang__ || __GNUC__ [[gnu::always_inline]] #endif - (const privacy::vector &passwords, std::vector &pwStrengths) { +(const privacy::vector &passwords, miSTL::vector &pwStrengths) { pwStrengths.resize(passwords.size()); for (std::size_t i = 0; i < passwords.size(); ++i) { pwStrengths[i] = isPasswordStrong(std::get<2>(passwords[i])); @@ -102,11 +97,11 @@ constexpr void computeStrengths } /// \brief Adds a new password to the saved records. -inline void addPassword(privacy::vector &passwords, std::vector &strengths) { +void addPassword(privacy::vector &passwords, miSTL::vector &strengths) { privacy::string site{getResponseStr("Enter the name of the site/app: ")}; // The site name must be non-empty if (site.empty()) { - printColor("\nThe site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe site/app name cannot be blank."); return; } privacy::string username{getResponseStr("Username (leave blank if N/A): ")}; @@ -120,8 +115,8 @@ inline void addPassword(privacy::vector &passwords, std::vecto // If the record already exists, ask the user if they want to update it bool update{false}; if (it != passwords.end() && std::get<0>(*it) == site && std::get<1>(*it) == username) { - printColor("\nA record with the same site and username already exists.", 'y', true); - printColor("Do you want to update it? (y/n): ", 'b'); + printColoredOutput('y', "\nA record with the same site and username already exists."); + printColoredOutput('b', "Do you want to update it? (y/n):"); update = validateYesNo(); if (!update) return; @@ -132,21 +127,21 @@ inline void addPassword(privacy::vector &passwords, std::vecto // The password can't be empty. Give the user 2 more tries to enter a non-empty password int attempts{0}; while (password.empty() && ++attempts < 3) { - printColor("Password can't be blank. Please try again: ", 'y'); + printColoredOutput('y', "Password can't be blank. Please try again: "); password = getSensitiveInfo(); } // If the password is still empty, return if (password.empty()) { - printColor("Password can't be blank. Try again later.", 'r', true, std::cerr); + printColoredErrorln('r', "Password can't be blank. Try again later."); return; } // Always warn on weak passwords if (!isPasswordStrong(password)) { - printColor( - "The password you entered is weak! A password should have at least 8 characters \nand include at least an " - "uppercase character, a lowercase, a punctuator, \nand a digit.", 'y', true); - printColor("Please consider using a stronger one.", 'r', true); + printColoredOutputln('y', + "The password you entered is weak! A password should have at least 8 characters \nand include at least an " + "uppercase character, a lowercase, a punctuator, \nand a digit."); + printColoredOutputln('r', "Please consider using a stronger one."); } // Update the record if it already exists, else add a new one @@ -154,7 +149,7 @@ inline void addPassword(privacy::vector &passwords, std::vecto std::get<2>(*it) = password; else passwords.emplace_back(site, username, password); - printColor(std::format("Password {} successfully.", update ? "updated" : "added"), 'g', true); + printColoredOutputln('g', "Password {} successfully.", update ? "updated" : "added"); // Entries should always be sorted std::ranges::sort(passwords, [](const auto &tuple1, const auto &tuple2) { @@ -166,53 +161,52 @@ inline void addPassword(privacy::vector &passwords, std::vecto } /// \brief Generates a random password. -inline void generatePassword(privacy::vector &, std::vector &) { +void generatePassword(privacy::vector &, miSTL::vector &) { int length = getResponseInt("Enter the length of the password to generate: "); int tries{0}; // The password must be at least 8 characters long while (length < 8 && ++tries < 3) { - printColor("A strong password should be at least 8 characters long.", 'y', true); - printColor(std::format("{}", tries == 2 ? "Last chance:" : "Please try again:"), - tries == 2 ? 'r' : 'y'); + printColoredOutputln('y', "A strong password should be at least 8 characters long."); + printColoredOutputln(tries == 2 ? 'r' : 'y', "{}", tries == 2 ? "Last chance:" : "Please try again:"); length = getResponseInt(); } // The password length must not exceed 256 characters if (length > 256) { - printColor("The password length cannot exceed 256 characters.", 'r', true, std::cerr); + printColoredErrorln('r', "The password length cannot exceed 256 characters."); return; } if (length < 8) return; - printColor("Generated password: ", 'c'); - printColor(generatePassword(length), 'g', true); + printColoredOutput('c', "Generated password: "); + printColoredOutputln('g', "{}", generatePassword(length)); } /// \brief Shows all saved passwords. -inline void viewAllPasswords(privacy::vector &passwords, std::vector &strengths) { +void viewAllPasswords(privacy::vector &passwords, miSTL::vector &strengths) { // Check if there are any passwords saved - if (auto &&constPasswordsRef = std::as_const(passwords); constPasswordsRef.empty()) { - printColor("You haven't saved any password yet.", 'r', true); + if (auto &&constPasswordsView = std::ranges::views::as_const(passwords); constPasswordsView.empty()) { + printColoredOutputln('r', "You haven't saved any password yet."); } else { std::cout << "All passwords: ("; - printColor("red is weak", 'r'); + printColoredOutput('r', "red is weak"); std::cout << ", "; - printColor("green is strong", 'g'); + printColoredOutput('g', "green is strong"); std::cout << ")" << std::endl; - printColor("-----------------------------------------------------", 'w', true); + printColoredOutputln('w', "-----------------------------------------------------"); // Print all the passwords - for (std::size_t i = 0; i < constPasswordsRef.size(); ++i) { - printPasswordDetails(constPasswordsRef[i], strengths[i]); - printColor("-----------------------------------------------------", 'w', true); + for (std::size_t i = 0; i < constPasswordsView.size(); ++i) { + printPasswordDetails(constPasswordsView[i], strengths[i]); + printColoredOutputln('w', "-----------------------------------------------------"); } } } /// \brief Handles fuzzy matching for update and deletion of passwords. -void checkFuzzyMatches(auto &iter, privacy::vector &records, privacy::string &query) { +void checkFuzzyMatches(auto &iter, privacy::vector &records, privacy::string &query) { // Fuzzy-match the query against the site names const FuzzyMatcher matcher(records | std::ranges::views::elements<0>); @@ -220,9 +214,9 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, p if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { const auto &match = fuzzyMatched.at(0); - printColor("Did you mean '", 'c'); - printColor(match, 'g'); - printColor("'? (y/n):", 'c'); + printColoredOutput('c', "Did you mean '"); + printColoredOutput('g', "{}", match); + printColoredOutput('c', "'? (y/n):"); if (validateYesNo()) { // Update the iterator @@ -230,30 +224,30 @@ void checkFuzzyMatches(auto &iter, privacy::vector &records, p [](const auto &lhs, const auto &rhs) noexcept -> bool { return comparator(lhs, rhs); }); - query = std::string{match}; + query = miSTL::string{match}; } } else if (!fuzzyMatched.empty()) { // multiple matches - printColor("Did you mean one of these?:", 'b', true); + printColoredOutputln('b', "Did you mean one of these?:"); // Print all the matches for (const auto &el: fuzzyMatched) { - printColor(el, 'g', true); - printColor("-----------------------------------------", 'b', true); + printColoredOutputln('g', "{}", el); + printColoredOutputln('b', "-----------------------------------------"); } } } /// \brief Updates a password record. -inline void updatePassword(privacy::vector &passwords, std::vector &strengths) { +void updatePassword(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) [[unlikely]] { // There is nothing to update - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string site{getResponseStr("Enter the name of the site/app to update: ")}; if (site.empty()) { - printColor("\nThe site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe site/app name cannot be blank."); return; } @@ -267,21 +261,19 @@ inline void updatePassword(privacy::vector &passwords, std::ve checkFuzzyMatches(it, passwords, site); // Extract all the accounts under the site - auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), - [](const auto &lhs, const auto &rhs) { - // this is consistent with the comparator() used to find the lower bound - return std::get<0>(lhs) < std::get<0>(rhs); - }); - - if (!matches.empty()) { + if (auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), + [](const auto &lhs, const auto &rhs) { + // this is consistent with the comparator() used to find the lower bound + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matches.empty()) { // site found if (matches.size() > 1) { // there are multiple accounts under the site std::cout << "Found the following usernames for " << std::quoted(site) << ":\n"; for (const auto &[_, username, pass]: matches) - printColor(username.empty() - ? "'' [no username, reply with a blank to select]" - : username, 'c', true); + printColoredOutputln('c', "{}", username.empty() + ? "'' [no username, reply with a blank to select]" + : username); privacy::string username{getResponseStr("\nEnter one of the above usernames to update:")}; @@ -292,10 +284,10 @@ inline void updatePassword(privacy::vector &passwords, std::ve }); // Exit if the entered username is incorrect if (it == matches.end() || std::get<1>(*it) != username) { - printColor("No such username as '", 'r', false, std::cerr); - printColor(username, 'y', false, std::cerr); - printColor("' under ", 'r', false, std::cerr); - printColor(site, 'c', true, std::cerr); + printColoredError('r', "No such username as '"); + printColoredError('y', "{}", username); + printColoredError('r', "' under "); + printColoredErrorln('c', "{}", site); return; } @@ -312,22 +304,20 @@ inline void updatePassword(privacy::vector &passwords, std::ve for (const auto &match: matches) { if (newUsername == std::get<1>(match)) { std::cerr << "Username already exists for this site. Try again later." << std::endl; - return; } } } - const privacy::string newPassword{ - getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") + getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") }; // Warn if the password is weak if (!newPassword.empty() && !isPasswordStrong(newPassword)) { - printColor( - "The password you entered is weak! A password should have at least 8 characters \nand include at least an " - "uppercase character, a lowercase, a punctuator, \nand a digit.", 'y', true); - printColor("Please consider using a stronger one.", 'r', true); + printColoredOutputln('y', + "The password you entered is weak! A password should have at least 8 characters \nand include at least an " + "uppercase character, a lowercase, a punctuator, \nand a digit."); + printColoredOutputln('r', "Please consider using a stronger one."); } // Update the record @@ -340,28 +330,28 @@ inline void updatePassword(privacy::vector &passwords, std::ve return comparator(tuple1, tuple2); }); - printColor("Password updated successfully.", 'g', true); + printColoredOutputln('g', "Password updated successfully."); // Recompute strengths computeStrengths(passwords, strengths); - } else printColor("Password not updated.", 'r', true, std::cerr); + } else printColoredErrorln('r', "Password not updated."); } else { - printColor("'", 'r', false, std::cerr); - printColor(site, 'c', false, std::cerr); - printColor("' was not found in the saved passwords.", 'r', true, std::cerr); + printColoredError('r', "'"); + printColoredError('c', "{}", site); + printColoredErrorln('r', "' was not found in the saved passwords."); } } /// \brief Deletes a password record. -inline void deletePassword(privacy::vector &passwords, std::vector &strengths) { +void deletePassword(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) { - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string site{getResponseStr("Enter the name of the site/app to delete: ")}; if (site.empty()) { - printColor("The site/app name cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "The site/app name cannot be blank."); return; } @@ -375,45 +365,45 @@ inline void deletePassword(privacy::vector &passwords, std::ve checkFuzzyMatches(it, passwords, site); // Extract all the accounts under the site - auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), - [](const auto &lhs, const auto &rhs) { - return std::get<0>(lhs) < std::get<0>(rhs); - }); - - if (!matches.empty()) { + if (auto matches = std::ranges::equal_range(it, passwords.end(), std::tie(site), + [](const auto &lhs, const auto &rhs) { + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matches.empty()) { // site found if (matches.size() > 1) { std::cout << "Found the following usernames for " << std::quoted(site) << ":\n"; for (const auto &[_, username, pass]: matches) - printColor(username.empty() - ? "'' [no username, reply with a blank to select]" - : username, 'c', true); + printColoredOutputln('c', "{}", username.empty() + ? "'' [no username, reply with a blank to select]" + : username); privacy::string username{ - getResponseStr("\nEnter one of the above usernames to delete (Enter \"All\" to delete all):")}; + getResponseStr("\nEnter one of the above usernames to delete (Enter \"All\" to delete all):") + }; // Update the iterator it = std::ranges::lower_bound(matches, std::tie(site, username, std::ignore), [](const auto &tuple1, const auto &tuple2) { return comparator(tuple1, tuple2); }); - if (it == matches.end() || std::get<1>(*it) != username) { // the entered username is incorrect + if (it == matches.end() || std::get<1>(*it) != username) { + // the entered username is incorrect // If the entered username is 'All', delete all the records under the site if (username == "All") { passwords.erase(matches.begin(), matches.end()); - printColor("All records under ", 'g'); - printColor(site, 'c'); - printColor(" deleted successfully.", 'g', true); + printColoredOutput('g', "All records under "); + printColoredOutput('c', "{}", site); + printColoredOutputln('g', " deleted successfully."); // Recompute strengths computeStrengths(passwords, strengths); return; } - printColor("No such username as '", 'r', false, std::cerr); - printColor(username, 'y', false, std::cerr); - printColor("' under ", 'r', false, std::cerr); - printColor(site, 'c', true, std::cerr); + printColoredError('r', "No such username as '"); + printColoredError('y', "{}", username); + printColoredError('r', "' under "); + printColoredErrorln('c', "{}", site); return; } @@ -430,83 +420,80 @@ inline void deletePassword(privacy::vector &passwords, std::ve return comparator(tuple1, tuple2); }); - printColor("Password record deleted successfully.", 'g', true); + printColoredOutputln('g', "Password record deleted successfully."); // Recompute strengths computeStrengths(passwords, strengths); } else { - printColor("'", 'r', false, std::cerr); - printColor(site, 'c', false, std::cerr); - printColor("' was not found in the saved passwords.", 'r', true, std::cerr); + printColoredError('r', "'"); + printColoredError('c', "{}", site); + printColoredErrorln('r', "' was not found in the saved passwords."); } } /// \brief Finds a password record. -inline void searchPasswords(privacy::vector &passwords, std::vector &) { +void searchPasswords(privacy::vector &passwords, miSTL::vector &) { if (passwords.empty()) [[unlikely]] { // There is nothing to search - printColor("No passwords saved yet.", 'r', true, std::cerr); + printColoredErrorln('r', "No passwords saved yet."); return; } privacy::string query{getResponseStr("Enter the name of the site/app: ")}; // The query must be non-empty if (query.empty()) { - printColor("\nThe search query cannot be blank.", 'r', true, std::cerr); + printColoredErrorln('r', "\nThe search query cannot be blank."); return; } // Use a const reference to protect the passwords from accidental modifications - auto &&constPasswordsRef = std::as_const(passwords); + auto &&constPasswordsView = std::ranges::views::as_const(passwords); // Look for partial and exact matches - auto matches = constPasswordsRef | std::ranges::views::filter([&query](const auto &vec) -> bool { + if (auto matches = constPasswordsView | std::ranges::views::filter([&query](const auto &vec) -> bool { return std::get<0>(vec).contains(query); - }); - - // Print all the matches - if (!matches.empty()) [[likely]] { + }); !matches.empty()) [[likely]] { + // Print all the matches std::cout << "All the matches:" << std::endl; - printColor("------------------------------------------------------", 'm', true); + printColoredOutputln('m', "------------------------------------------------------"); for (const auto &el: matches) { - printPasswordDetails(el); - printColor("------------------------------------------------------", 'm', true); + printPasswordDetails(el, isPasswordStrong(std::get<2>(el))); + printColoredOutputln('m', "------------------------------------------------------"); } } else { - printColor(std::format("No matches found for '{}'", query), 'r', true); + printColoredErrorln('r', "No matches found for '{}'", query); // Fuzzy-match the query against the site names - const FuzzyMatcher matcher(constPasswordsRef | std::ranges::views::elements<0>); + const FuzzyMatcher matcher(constPasswordsView | std::ranges::views::elements<0>); // If there is a single match, ask the user if they want to view it if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { const auto &match = fuzzyMatched.at(0); - printColor("Did you mean '", 'c'); - printColor(match, 'g'); - printColor("'? (y/n):", 'c'); + printColoredOutput('c', "Did you mean '"); + printColoredOutput('g', "{}", match); + printColoredOutput('c', "'? (y/n):"); if (validateYesNo()) { - auto matched = std::ranges::equal_range(constPasswordsRef, std::tie(match), - [](const auto &lhs, const auto &rhs) noexcept -> bool { - return std::get<0>(lhs) < std::get<0>(rhs); - }); // print all the records under the match - if (!matched.empty()) [[likely]] { - printColor("-----------------------------------------------------", 'w', true); + if (auto matched = std::ranges::equal_range(constPasswordsView, std::tie(match), + [](const auto &lhs, const auto &rhs) noexcept -> bool { + return std::get<0>(lhs) < std::get<0>(rhs); + }); !matched.empty()) [[likely]] { + printColoredOutputln('w', "-----------------------------------------------------"); for (const auto &pass: matched) { - printPasswordDetails(pass); - printColor("-----------------------------------------------------", 'w', true); + printPasswordDetails(pass, isPasswordStrong(std::get<2>(pass))); + printColoredOutputln('w', "-----------------------------------------------------"); } } - } else printColor("Sorry, '" + query + "' not found.", 'r', true); + } else printColoredErrorln('r', "Sorry, '{}' not found.", query); } else if (!fuzzyMatched.empty()) { // multiple matches - printColor("Did you mean one of these?:", 'b', true); + printColoredOutputln('b', "Did you mean one of these?:"); // Print all the matches for (const auto &el: fuzzyMatched) { - printColor(el, 'g', true); + printColoredOutput('g', "{}", el); std::cout << "---------------------------------------" << std::endl; } } @@ -514,13 +501,13 @@ inline void searchPasswords(privacy::vector &passwords, std::v } /// \brief Imports passwords from a csv file. -inline void importPasswords(privacy::vector &passwords, std::vector &strengths) { - const string fileName = getResponseStr("Enter the path to the csv file: "); +void importPasswords(privacy::vector &passwords, miSTL::vector &strengths) { + const fs::path fileName = getFilesystemPath("Enter the path to the csv file: "); - privacy::vector imports{importCsv(fileName)}; + privacy::vector imports{importCsv(fileName)}; if (imports.empty()) { - printColor("No passwords imported.", 'y', true); + printColoredOutputln('y', "No passwords imported."); return; } @@ -531,13 +518,13 @@ inline void importPasswords(privacy::vector &passwords, std::v // Remove duplicates from the imported passwords using the erase-remove idiom auto dups = std::ranges::unique(imports, [](const auto &lhs, const auto &rhs) noexcept -> bool { - // the binary predicate should check equivalence, not order + // The binary predicate should check equivalence, not order return std::tie(std::get<0>(lhs), std::get<1>(lhs)) == std::tie(std::get<0>(rhs), std::get<1>(rhs)); }); imports.erase(dups.begin(), dups.end()); // Check if the imported passwords already exist in the database by constructing their set intersection - privacy::vector duplicates; + privacy::vector duplicates; duplicates.reserve(imports.size()); // Find the passwords that already exist in the database @@ -546,19 +533,19 @@ inline void importPasswords(privacy::vector &passwords, std::v return comparator(pw1, pw2); }); - privacy::vector recordsUnion; + privacy::vector recordsUnion; recordsUnion.reserve(passwords.size() + imports.size()); bool overwrite{true}; // If there are duplicates, ask the user if they want to overwrite them if (!duplicates.empty()) { - printColor("Warning: The following passwords already exist in the database:", 'y', true); + printColoredOutputln('y', "Warning: The following passwords already exist in the database:"); for (const auto &duplicate: duplicates) { - printPasswordDetails(duplicate); - printColor("-------------------------------------------------", 'm', true); + printPasswordDetails(duplicate, isPasswordStrong(std::get<2>(duplicate))); + printColoredOutputln('m', "-------------------------------------------------"); } - printColor("Do you want to overwrite/update them? (y/n): ", 'b'); + printColoredOutput('b', "Do you want to overwrite/update them? (y/n): "); overwrite = validateYesNo(); } @@ -578,7 +565,7 @@ inline void importPasswords(privacy::vector &passwords, std::v return comparator(pw1, pw2); }); } else { - printColor("Warning: Duplicate passwords will not be imported.", 'y', true); + printColoredOutputln('y', "Warning: Duplicate passwords will not be imported."); // 'passwords' now come before 'imports', in accordance with the discussion in the previous branch. std::ranges::set_union(passwords, imports, std::back_inserter(recordsUnion), @@ -594,45 +581,45 @@ inline void importPasswords(privacy::vector &passwords, std::v computeStrengths(passwords, strengths); if (auto imported = overwrite ? imports.size() : passwords.size() - initSize; std::cmp_greater(imported, 0)) - printColor(std::format("Imported {} passwords successfully.", imported), 'g', true); - else printColor("Passwords not imported.", 'r', true); + printColoredOutputln('g', "Imported {} passwords successfully.", imported); + else printColoredErrorln('r', "Passwords not imported."); } /// \brief Exports passwords to a csv file. -inline void exportPasswords(privacy::vector &passwords, std::vector &) { - auto &&passwordsConstRef = std::as_const(passwords); +void exportPasswords(privacy::vector &passwords, miSTL::vector &) { + auto &&constPasswordsView = std::as_const(passwords); - if (passwordsConstRef.empty()) [[unlikely]] { - printColor("No passwords saved yet.", 'r', true, std::cerr); + if (constPasswordsView.empty()) [[unlikely]] { + printColoredOutputln('r', "No passwords saved yet."); return; } - const string fileName = getResponseStr("Enter the path to save the file (leave blank for default): "); + const fs::path fileName = getFilesystemPath("Enter the path to save the file (leave blank for default): "); // Export the passwords to a csv file - if (const bool exported = fileName.empty() ? exportCsv(passwordsConstRef) : exportCsv(passwordsConstRef, fileName); - exported) - [[likely]] - // Warn the user about the security risk - printColor("WARNING: The exported file contains all your passwords in plain text." - "\nPlease delete it securely after use.", 'r', true); - else printColor("Passwords not exported.", 'r', true, std::cerr); + if (const bool exported = fileName.string().empty() + ? exportCsv(constPasswordsView) + : exportCsv(constPasswordsView, fileName); exported) [[likely]] + // Warn the user about the security risk + printColoredOutputln('r', "WARNING: The exported file contains all your passwords in plain text." + "\nPlease delete it securely after use."); + else printColoredErrorln('r', "Passwords not exported."); } /// \brief Analyzes the saved passwords for weak passwords and password reuse. -inline void analyzePasswords(privacy::vector &passwords, std::vector &strengths) { +void analyzePasswords(privacy::vector &passwords, miSTL::vector &strengths) { if (passwords.empty()) { - printColor("No passwords to analyze.", 'r', true); + printColoredOutputln('r', "No passwords to analyze."); return; } const auto total = passwords.size(); - auto &&constPasswordsRef = std::as_const(passwords); + auto &&constPasswordsView = std::ranges::views::as_const(passwords); // Analyze the passwords std::cout << "Analyzing passwords..." << std::endl; // Scan for weak passwords - privacy::vector weakPasswords; + privacy::vector weakPasswords; weakPasswords.reserve(total); for (std::size_t i = 0; i < passwords.size(); ++i) { @@ -641,11 +628,8 @@ inline void analyzePasswords(privacy::vector &passwords, std:: } // Check for reused passwords - std::unordered_map > passwordMap; - for (const auto &record: constPasswordsRef) { - const auto &site = std::get<0>(record); - const auto &password = std::get<2>(record); - + miSTL::unordered_map > passwordMap; + for (const auto &[site, _, password]: constPasswordsView) { // Add the site to the set of sites that use the password passwordMap[password].insert(site); } @@ -653,19 +637,18 @@ inline void analyzePasswords(privacy::vector &passwords, std:: // Print the weak passwords auto weak{weakPasswords.size()}; if (!weakPasswords.empty()) [[likely]] { - printColor(std::format("Found {} account{} with weak passwords:", weak, weak == 1 ? "" : "s"), 'r', true); - printColor("------------------------------------------------------", 'r', true); + printColoredOutputln('r', "Found {} account{} with weak passwords:", weak, weak == 1 ? "" : "s"); + printColoredErrorln('r', "------------------------------------------------------"); for (const auto &password: weakPasswords) { printPasswordDetails(password); - printColor("------------------------------------------------------", 'r', true); + printColoredErrorln('r', "------------------------------------------------------"); } - printColor(std::format("Please change the weak passwords above. " - "\nYou can use the 'generate password' option to generate strong passwords.\n"), 'r', - true); - } else printColor("No weak passwords found. Keep it up!\n", 'g', true); + printColoredOutputln('r', "Please change the weak passwords above. " + "\nYou can use the 'generate password' option to generate strong passwords."); + } else printColoredOutputln('g', "No weak passwords found. Keep it up!"); // Find reused passwords - using PasswordSites = std::pair >; + using PasswordSites = std::pair >; std::multimap > countMap; for (const auto &[password, sites]: passwordMap) { @@ -677,11 +660,11 @@ inline void analyzePasswords(privacy::vector &passwords, std:: // Print reused passwords in descending order of counts std::size_t reused{0}; for (const auto &[count, password_sites]: countMap) { - printColor("Password '", 'y'); - printColor(password_sites.first, 'r'); - printColor(std::format("' is reused on {} sites:", count), 'y', true); + printColoredOutput('y', "Password '"); + printColoredOutput('r', "{}", password_sites.first); + printColoredOutputln('y', "' is reused on {} sites:", count); for (const auto &site: password_sites.second) - printColor(site + "\n", 'm'); + printColoredOutputln('m', "{}", site); std::cout << std::endl; ++reused; @@ -689,29 +672,28 @@ inline void analyzePasswords(privacy::vector &passwords, std:: // Print summary if (reused) { - printColor(std::format("{} password{} been reused.", reused, - reused == 1 ? " has" : "s have"), 'r', true); - } else printColor("Nice!! No password reuse detected.", 'g', true); + printColoredOutputln('r', "{} password{} been reused.", reused, + reused == 1 ? " has" : "s have"); + } else printColoredOutputln('g', "Nice!! No password reuse detected."); - printColor(std::format("{} use unique passwords to minimize the impact of their compromise.", - reused ? "Please" : "Always"), reused ? 'r' : 'c', true); + printColoredOutputln(reused ? 'r' : 'c', "{} use unique passwords to minimize the impact of their compromise.", + reused ? "Please" : "Always"); // Print the statistics std::cout << "\nTotal passwords: " << total << std::endl; if (weak > 0) [[likely]] { const char col{std::cmp_greater(weak, total / 4) ? 'r' : 'y'}; - printColor(std::format("{}% of your passwords are weak.", - std::round(static_cast(weak) / static_cast(total) * 100 * 100) / - 100), col, true); - } else printColor("All your passwords are strong. Keep it up!", 'g', true); + printColoredOutputln(col, "{}% of your passwords are weak.", + std::round(static_cast(weak) / static_cast(total) * 100 * 100) / 100); + } else printColoredOutputln('g', "All your passwords are strong. Keep it up!"); } /// \brief A simple, minimalistic password manager. /// \throws std::runtime_error if the primary password is incorrect after 3 attempts. void passwordManager() { privacy::string encryptionKey; - std::string passwordFile{DefaultPasswordFile}; + miSTL::string passwordFile{DefaultPasswordFile}; bool newSetup{false}; // Reserve 32 bytes for the primary key. @@ -730,11 +712,11 @@ void passwordManager() { newSetup = true; } else { // the user pointed us to an existing password records - passwordFile = path; + passwordFile = path.c_str(); } } - privacy::vector passwords; + privacy::vector passwords; if (!newSetup) { // preprocess the passwordFile @@ -745,19 +727,18 @@ void passwordManager() { // Get the primary password do { - encryptionKey = getSensitiveInfo("Enter the primary password: "); + encryptionKey = getSensitiveInfo("Enter your primary password: "); isCorrect = verifyPassword(encryptionKey, pwHash); if (!isCorrect && attempts < 2) - printColor("Wrong password, please try again.", 'r', true, std::cerr); + printColoredErrorln('r', "Wrong password, please try again."); } while (!isCorrect && ++attempts < 3); // If the password is still incorrect, exit - if (!isCorrect) { + if (!isCorrect) throw std::runtime_error("3 incorrect password attempts."); - } // Load the saved passwords - printColor("Decrypting passwords...", 'c', true); + printColoredOutputln('c', "Decrypting passwords..."); passwords = loadPasswords(passwordFile, encryptionKey); } @@ -767,22 +748,22 @@ void passwordManager() { }); // Assess the passwords' strength - std::vector passwordStrength(passwords.size(), false); + miSTL::vector passwordStrength(passwords.size(), false); for (std::size_t i = 0; i < passwords.size(); ++i) { passwordStrength[i] = isPasswordStrong(std::get<2>(passwords[i])); } // A map of choices and their corresponding functions - std::unordered_map &, std::vector &)> choices = { - {1, addPassword}, - {2, updatePassword}, - {3, deletePassword}, - {4, viewAllPasswords}, - {5, searchPasswords}, - {6, generatePassword}, - {7, analyzePasswords}, - {8, importPasswords}, - {9, exportPasswords} + miSTL::unordered_map &, miSTL::vector &)> choices = { + {1, addPassword}, + {2, updatePassword}, + {3, deletePassword}, + {4, viewAllPasswords}, + {5, searchPasswords}, + {6, generatePassword}, + {7, analyzePasswords}, + {8, importPasswords}, + {9, exportPasswords} }; // A fast, lightweight random number generator std::minstd_rand gen(std::random_device{}()); // seed the generator @@ -791,9 +772,12 @@ void passwordManager() { while (true) { // Colors to use for the menu constexpr auto colors = "rgbymcw"; - auto color = colors[dist(gen)]; + const auto color = colors[dist(gen)]; + + printColoredOutput(color, "----------------"); + printColoredOutput(colors[dist(gen)], " Password Manager "); + printColoredOutputln(color, "----------------"); - printColor("-------------------------------------------", color, true); std::cout << "1. Add new password\n"; std::cout << "2. Update password\n"; std::cout << "3. Delete password\n"; @@ -805,7 +789,7 @@ void passwordManager() { std::cout << "9. Export passwords\n"; std::cout << "10. Change the primary Password\n"; std::cout << "11. Save and Exit\n"; - printColor("-------------------------------------------", color, true); + printColoredOutputln(color, "----------------------------------------------"); try { int choice = getResponseInt("Enter your choice: "); @@ -813,14 +797,14 @@ void passwordManager() { if (auto iter = choices.find(choice); iter != choices.end()) iter->second(passwords, passwordStrength); else if (choice == 10) { - if (changeMasterPassword(encryptionKey)) - printColor("Master password changed successfully.", 'g', true); - else printColor("Master password not changed.", 'r', true, std::cerr); + if (changePrimaryPassword(encryptionKey)) + printColoredOutputln('g', "Primary password changed successfully."); + else printColoredErrorln('r', "Primary password not changed."); } else if (choice == 11) break; - else printColor("Invalid choice!", 'r', true, std::cerr); + else printColoredErrorln('r', "Invalid choice!"); } catch (const std::exception &ex) { - printColor(ex.what(), 'r', true, std::cerr); + printColoredErrorln('r', "{}", ex.what()); } catch (...) { throw std::runtime_error("An error occurred."); } } @@ -832,12 +816,12 @@ void passwordManager() { if (const auto home{getHomeDir()}; fs::exists(home)) fs::create_directory(home + "/.privacyShield", home, ec); if (ec) { - printColor(std::format("Failed to create '{}': ", DefaultPasswordFile), 'y', false, std::cerr); - printColor(ec.message(), 'r', true, std::cerr); + printColoredError('y', "Failed to create '{}': ", DefaultPasswordFile); + printColoredErrorln('r', "{}", ec.message()); } } // Save the passwords if (savePasswords(passwords, DefaultPasswordFile, encryptionKey)) - printColor("Passwords saved successfully.", 'g', true); - else printColor("Passwords not saved!", 'r', true, std::cerr); + printColoredOutputln('g', "Passwords saved successfully."); + else printColoredErrorln('r', "Passwords not saved!"); } diff --git a/src/passwordManager/passwordManager.cppm b/src/passwordManager/passwordManager.cppm index 048f440..b7ed75b 100644 --- a/src/passwordManager/passwordManager.cppm +++ b/src/passwordManager/passwordManager.cppm @@ -19,37 +19,40 @@ module; #include #include #include +#include export module passwordManager; import utils; import secureAllocator; +import mimallocSTL; using passwordRecords = std::tuple; -privacy::vector loadPasswords(std::string_view filePath, const privacy::string &decryptionKey); +privacy::vector loadPasswords(std::string_view filePath, const privacy::string &decryptionKey); -bool savePasswords(privacy::vector &passwords, std::string_view filePath, +bool savePasswords(privacy::vector &passwords, std::string_view filePath, const privacy::string &encryptionKey); bool isPasswordStrong(std::string_view password) noexcept; privacy::string generatePassword(int length); -bool changeMasterPassword(privacy::string &primaryPassword); +bool changePrimaryPassword(privacy::string &primaryPassword); -std::pair initialSetup() noexcept; +std::pair initialSetup() noexcept; privacy::string getHash(std::string_view filePath); -privacy::vector importCsv(const std::string &filePath); +privacy::vector importCsv(const std::filesystem::path &filePath); -bool exportCsv(const privacy::vector &records, std::string_view filePath = getHomeDir()); +bool exportCsv(const privacy::vector &records, const std::filesystem::path &filePath = getHomeDir()); export { privacy::string hashPassword(const privacy::string &password, const std::size_t &opsLimit = crypto_pwhash_OPSLIMIT_SENSITIVE, const std::size_t &memLimit = crypto_pwhash_MEMLIMIT_SENSITIVE); + void passwordManager(); bool verifyPassword(const privacy::string &password, const privacy::string &storedHash); diff --git a/src/passwordManager/passwords.cpp b/src/passwordManager/passwords.cpp index e5e5e82..9b4aa15 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -29,6 +29,7 @@ module; import utils; import encryption; import secureAllocator; +import mimallocSTL; module passwordManager; @@ -78,7 +79,7 @@ privacy::string generatePassword(const int length) { throw std::length_error("Password too long."); // generate from a set of printable ascii characters - const std::string characters = + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-=_~+[]{}<>"; // Seed the Mersenne Twister engine with a random source (ideally non-deterministic) @@ -100,8 +101,7 @@ privacy::string generatePassword(const int length) { for (int i = 0; i < length; ++i) password += characters[distribution(generator)]; - // If the length is >= 8, it is almost impossible that this loop is infinite, - // but let's handle that ultra-rare situation anyway + // Avoid an infinite loop } while (!isPasswordStrong(password) && ++trials <= maxTrials); return password; @@ -156,11 +156,11 @@ encryptDecryptRange(privacy::vector &passwords, const privacy:: for (std::size_t i = start; i < end; ++i) { std::get<2>(passwords[i]) = encrypt ? encryptStringWithMoreRounds(std::get<2>(passwords[i]), key) - : decryptStringWithMoreRounds(std::string{std::get<2>(passwords[i])}, + : decryptStringWithMoreRounds(miSTL::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); std::exit(1); } } @@ -184,18 +184,18 @@ encryptDecryptRangeAllFields(privacy::vector &passwords, const for (std::size_t i = start; i < end; ++i) { std::get<0>(passwords[i]) = encrypt ? encryptString(std::get<0>(passwords[i]), key) - : decryptString(std::string{std::get<0>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<0>(passwords[i])}, key); std::get<1>(passwords[i]) = encrypt ? encryptString(std::get<1>(passwords[i]), key) - : decryptString(std::string{std::get<1>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<1>(passwords[i])}, key); std::get<2>(passwords[i]) = encrypt ? encryptString(std::get<2>(passwords[i]), key) - : decryptString(std::string{std::get<2>(passwords[i])}, key); + : decryptString(miSTL::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { - printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); + printColoredErrorln('r', "Error: {}", ex.what()); std::exit(1); } } @@ -213,7 +213,7 @@ encryptDecryptConcurrently(privacy::vector &passwordEntries, co const unsigned int numThreads{std::jthread::hardware_concurrency() ? std::jthread::hardware_concurrency() : 8}; // Divide the password entries among threads - std::vector threads; + miSTL::vector threads; const std::size_t passPerThread = numPasswords / numThreads; std::size_t start = 0; @@ -260,7 +260,7 @@ inline void checkCommonErrors(const std::string_view path) { /// \return True, if successful. bool savePasswords(privacy::vector &passwords, const std::string_view filePath, const privacy::string &encryptionKey) { - auto tempFile = std::string{filePath} + "XXXXXX"; + auto tempFile = miSTL::string{filePath} + "XXXXXX"; // Create a temporary file // If the temporary file couldn't be created, use the original file path @@ -268,7 +268,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin tempFile = filePath; else close(tmpFileFd); // Close the file descriptor - std::ofstream file(tempFile, std::ios::trunc); + std::ofstream file(tempFile.c_str(), std::ios::trunc); if (!file) { try { checkCommonErrors(tempFile); @@ -284,7 +284,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin file << "PLEASE DO NOT EDIT THIS FILE!" << std::endl; file << hashPassword(encryptionKey) << std::endl; - printColor("Encrypting your passwords...", 'c', true); + printColoredOutputln('c', "Encrypting your passwords..."); // Encrypt the password field with Serpent encryptDecryptConcurrently(passwords, encryptionKey, true, false); @@ -304,7 +304,7 @@ bool savePasswords(privacy::vector &passwords, const std::strin // Rename the temporary file to the original file fs::rename(tempFile, filePath, ec); - if (ec) printColor(ec.message(), 'r', true, std::cerr); + if (ec) printColoredErrorln('r', "{}", ec.message()); return !ec; } @@ -368,14 +368,14 @@ privacy::vector loadPasswords(const std::string_view filePath, /// \brief Helps the user change the primary password. /// \param primaryPassword the current primary password. /// \return True if the password is changed successfully, else false. -bool changeMasterPassword(privacy::string &primaryPassword) { +bool changePrimaryPassword(privacy::string &primaryPassword) { const privacy::string oldPassword{getSensitiveInfo("Enter the current primary password: ")}; // Verify that the old password is correct - auto masterHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, - crypto_pwhash_MEMLIMIT_INTERACTIVE); - if (!verifyPassword(oldPassword, masterHash)) { + if (const auto primaryHash = hashPassword(primaryPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, + crypto_pwhash_MEMLIMIT_INTERACTIVE); + !verifyPassword(oldPassword, primaryHash)) { std::cerr << "Password verification failed." << std::endl; return false; } @@ -393,10 +393,9 @@ bool changeMasterPassword(privacy::string &primaryPassword) { return false; } - const privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; - // Verify that the new password is correct - if (!verifyPassword(newPassword2, hashPassword(newPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, + if (const privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; + !verifyPassword(newPassword2, hashPassword(newPassword, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))) { std::cerr << "Passwords do not match." << std::endl; @@ -409,8 +408,8 @@ bool changeMasterPassword(privacy::string &primaryPassword) { /// \brief Helps with the initial setup of the password manager. /// \return New primary password and/or path to the password file, whichever is applicable. -std::pair initialSetup() noexcept { - std::pair ret{"", ""}; // ret.first = path to file, ret.second = new primary password +std::pair initialSetup() noexcept { + std::pair ret{"", ""}; // ret.first = path to file, ret.second = new primary password std::cout << "Looks like you don't have any passwords saved yet." << std::endl; @@ -427,11 +426,12 @@ std::pair initialSetup() noexcept { int count{0}; while (!isPasswordStrong(pass) && ++count < 3) { const bool last{count == 2}; - printColor(last - ? "Last chance: " - : "Weak password! The password must be at least 8 characters long and include \nat least an" - " uppercase character, a lowercase, a punctuator, and a digit.", last ? 'r' : 'y', - !last); + printColoredOutput(last ? 'r' : 'y', "{}", + last + ? "Last chance: " + : "Weak password! The password must be at least 8 characters long and include \nat least an" + " uppercase character, a lowercase, a punctuator, and a digit."); + if (last) std::cout << std::endl; pass = getSensitiveInfo(last ? "" : "Please enter a stronger password: "); } @@ -454,13 +454,13 @@ std::pair initialSetup() noexcept { } if (resp == 2) { // Enter the path to an existing password file - std::string path = getResponseStr("Enter the path to the file: "); - if (!(fs::exists(path) && fs::is_regular_file(path))) { + fs::path path = getFilesystemPath("Enter the path to the file: "); + if (std::error_code ec; !(exists(path, ec) && is_regular_file(path, ec))) { std::cerr << "That file doesn't exist or is not a regular file." << std::endl; continue; } - ret.first = path; + ret.first = path.string().c_str(); break; } if (resp == 3) return ret; @@ -507,13 +507,13 @@ privacy::string getHash(const std::string_view filePath) { /// \brief Export the password records to a CSV file. /// \param records the password records to export. /// \param filePath the file to export to. -bool exportCsv(const privacy::vector &records, const std::string_view filePath) { - fs::path filepath(filePath); +bool exportCsv(const privacy::vector &records, const std::filesystem::path &filePath) { + fs::path filepath = filePath; std::error_code ec; // Check if the file path is valid - if (!fs::path(filepath).has_filename()) { - printColor(std::format("Invalid file path: {}", filePath), 'r', true, std::cerr); + if (!filepath.has_filename()) { + printColoredErrorln('r', "Invalid file path: {}", filePath.string()); return false; } @@ -528,7 +528,7 @@ bool exportCsv(const privacy::vector &records, const std::strin if (exists(filepath, ec)) { // Check if the file is a regular file if (!is_regular_file(filepath)) [[unlikely]] { - printColor(std::format("The destination file ({}) is not a regular file.", filePath), 'r', true, std::cerr); + printColoredErrorln('r', "The destination file ({}) is not a regular file.", filepath.string()); return false; } @@ -550,8 +550,7 @@ bool exportCsv(const privacy::vector &records, const std::strin // Open the file for writing std::ofstream file(filepath); if (!file) { - printColor(std::format("Failed to open the destination file ({}) for writing.", filePath), - 'r', true, std::cerr); + printColoredErrorln('r', "Failed to open the destination file ({}) for writing.", filepath.string()); return false; } @@ -564,14 +563,14 @@ bool exportCsv(const privacy::vector &records, const std::strin file.close(); // Notify the user that the export was successful - printColor("Export successful. The file was saved as ", 'g'); - printColor(filepath, 'c', true); + printColoredOutput('g', "Export successful. The file was saved as "); + printColoredOutputln('c', "{}", filepath.string()); return true; } /// \brief Trims space (whitespace) off the beginning and end of a string. /// \param str the string to trim. -inline void trim(std::string &str) { +inline void trim(miSTL::string &str) { constexpr std::string_view space = " \t\n\r\f\v"; // Trim the leading space @@ -589,24 +588,24 @@ inline void trim(std::string &str) { /// Non-compliant rows will be ignored entirely. /// /// \throws std::runtime_error if the file couldn't be opened for reading. -privacy::vector importCsv(const std::string &filePath) { +privacy::vector importCsv(const fs::path &filePath) { privacy::vector passwords; - checkCommonErrors(filePath); + checkCommonErrors(filePath.string()); bool hasHeader = validateYesNo("Does the file have a header? (Skip the first line?) (y/n): "); std::ifstream file(filePath); if (!file) - throw std::runtime_error(std::format("Failed to open the file ({}) for reading.", filePath)); + throw std::runtime_error(std::format("Failed to open the file ({}) for reading.", filePath.string())); privacy::string line, value; if (hasHeader) - std::getline, privacy::Allocator >(file, - line); // Read and discard the first line + // Read and discard the first line + std::getline, privacy::Allocator >(file, line); while (std::getline, privacy::Allocator >(file, line)) { privacy::istringstream iss(line); - privacy::vector tokens; + privacy::vector tokens; while (std::getline, privacy::Allocator >(iss, value, ',')) tokens.emplace_back(value); diff --git a/src/privacyTracks/privacyTracks.cppm b/src/privacyTracks/privacyTracks.cppm index 460b8b0..e061c6f 100644 --- a/src/privacyTracks/privacyTracks.cppm +++ b/src/privacyTracks/privacyTracks.cppm @@ -26,6 +26,7 @@ module; export module privacyTracks; import utils; +import mimallocSTL; namespace fs = std::filesystem; @@ -46,7 +47,7 @@ enum class Browser : std::uint_fast8_t { inline void handleFileError(std::error_code &ec, const std::string_view context = "", const std::string_view path = "") noexcept { if (ec) { - printColor(std::format("Error {} {}: {}", context, path, ec.message()), 'r', true, std::cerr); + printColoredErrorln('r', "Error {} {}: {}", context, path, ec.message()); ec.clear(); } } @@ -59,17 +60,17 @@ std::uint_fast8_t detectBrowsers(const std::string_view pathEnv) { // Check if the passed string is empty if (pathEnv.empty()) { - printColor("PATH environment variable not found.", 'r', true, std::cerr); + printColoredErrorln('r', "PATH environment variable not found."); return detectedBrowsers; } // Split the PATH variable into individual paths - std::string pathEnvStr{pathEnv}; - std::vector paths; + miSTL::string pathEnvStr{pathEnv}; + miSTL::vector paths; paths.reserve(256); std::size_t pos; - while ((pos = pathEnvStr.find(':')) != std::string::npos) { + while ((pos = pathEnvStr.find(':')) != miSTL::string::npos) { paths.emplace_back(pathEnvStr.substr(0, pos)); pathEnvStr.erase(0, pos + 1); } @@ -117,7 +118,7 @@ std::uint_fast8_t detectBrowsers() { if (const auto pathEnv = getEnv("PATH"); pathEnv) return detectBrowsers(*pathEnv); - printColor("PATH environment variable not found.", 'r', true, std::cerr); + printColoredErrorln('r', "PATH environment variable not found."); return 0; } @@ -126,14 +127,14 @@ std::uint_fast8_t detectBrowsers() { /// \return true if successful, false otherwise. bool clearFirefoxTracks(const std::string_view configDir) { if (!fs::exists(configDir)) { - printColor("Firefox config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Firefox config directory not found."); return false; } std::error_code ec; // Find all default profiles - std::vector defaultProfileDirs; + miSTL::vector defaultProfileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -147,7 +148,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { if (!defaultProfileDirs.empty()) { std::cout << "Deleting cookies and history for the following default profiles:" << std::endl; for (const auto &profile: defaultProfileDirs) { - printColor(profile.filename().string(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); // Clearing cookies fs::remove(profile / "cookies.sqlite", ec); handleFileError(ec, "deleting", (profile / "cookies.sqlite").string()); @@ -156,10 +157,10 @@ bool clearFirefoxTracks(const std::string_view configDir) { fs::remove(profile / "places.sqlite", ec); handleFileError(ec, "deleting", (profile / "places.sqlite").string()); } - } else printColor("No default profiles found.", 'r', true); + } else printColoredErrorln('r', "No default profiles found."); // Treat the other directories as profiles - std::vector profileDirs; + miSTL::vector profileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -191,7 +192,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { handleFileError(ec, "deleting", entry.path().string()); else { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; alreadyCounted = true; } @@ -199,7 +200,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { if (entry.path().filename() == "places.sqlite") { if (!alreadyCounted) { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; } fs::remove(entry.path(), ec); @@ -210,9 +211,11 @@ bool clearFirefoxTracks(const std::string_view configDir) { } } } - printColor(nonDefaultProfiles - ? std::format("Deleted cookies and history for {} non-default profiles.", nonDefaultProfiles) - : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", + nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } @@ -222,7 +225,7 @@ bool clearFirefoxTracks(const std::string_view configDir) { /// \return true if successful, false otherwise. bool clearChromiumTracks(const std::string_view configDir) { if (!fs::exists(configDir)) { - printColor("Config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Chromium config directory not found."); return false; } @@ -252,10 +255,10 @@ bool clearChromiumTracks(const std::string_view configDir) { // Clearing history fs::remove(defaultProfileDir / "History", ec); handleFileError(ec, "deleting", (defaultProfileDir / "History").string()); - } else printColor("Default profile directory not found.", 'r', true, std::cerr); + } else printColoredErrorln('r', "Default profile directory not found."); // Find other profile directories - std::vector profileDirs; + miSTL::vector profileDirs; for (const auto &entry: fs::directory_iterator(configDir, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", configDir); @@ -287,7 +290,7 @@ bool clearChromiumTracks(const std::string_view configDir) { handleFileError(ec, "deleting", entry.path().string()); else { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; alreadyCounted = true; } @@ -296,7 +299,7 @@ bool clearChromiumTracks(const std::string_view configDir) { if (entry.path().filename() == "History") { if (!alreadyCounted) { std::cout << "Found "; - printColor(profile.filename(), 'c', true); + printColoredOutputln('c', "{}", profile.filename().string()); ++nonDefaultProfiles; } fs::remove(entry.path(), ec); @@ -307,9 +310,11 @@ bool clearChromiumTracks(const std::string_view configDir) { } } } - printColor(nonDefaultProfiles - ? std::format("Deleted cookies and history for {} non-default profiles.", nonDefaultProfiles) - : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); + printColoredOutputln(nonDefaultProfiles ? 'g' : 'r', "{}", + nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found."); return true; } @@ -322,7 +327,7 @@ bool clearOperaTracks(const std::string_view profilePath) { // Check if the Opera config directory exists if (!fs::exists(profilePath)) { - printColor("Opera config directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Opera config directory not found."); return false; } @@ -334,7 +339,7 @@ bool clearOperaTracks(const std::string_view profilePath) { ec.clear(); fs::remove(fs::path{profilePath} / "cookies", ec); if (ec) { - handleFileError(ec, "deleting", std::string{profilePath} + "/cookies"); + handleFileError(ec, "deleting", miSTL::string{profilePath} + "/cookies"); ec.clear(); ret = false; // We don't to return yet, we want to try to clear history too } @@ -346,7 +351,7 @@ bool clearOperaTracks(const std::string_view profilePath) { ec.clear(); fs::remove(fs::path{profilePath} / "history", ec); if (ec) { - handleFileError(ec, "deleting", std::string{profilePath} + "/history"); + handleFileError(ec, "deleting", miSTL::string{profilePath} + "/history"); ec.clear(); return false; // No point in continuing } @@ -363,7 +368,7 @@ bool clearChromiumTracks() { #elif __APPLE__ return clearChromiumTracks(getHomeDir() + "/Library/Application Support/Chromium"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -376,7 +381,7 @@ bool clearChromeTracks() { #elif __APPLE__ return clearChromiumTracks(getHomeDir() + "/Library/Application Support/Google/Chrome"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -389,7 +394,7 @@ bool clearOperaTracks() { #elif __APPLE__ return clearOperaTracks(getHomeDir() + "/Library/Application Support/com.operasoftware.Opera"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -398,9 +403,9 @@ bool clearOperaTracks() { /// \return true if successful, false otherwise. bool clearSafariTracks() { #if __APPLE__ - const std::string cookiesPath = getHomeDir() + "/Library/Cookies"; + const miSTL::string cookiesPath = getHomeDir() + "/Library/Cookies"; if (!fs::exists(cookiesPath)) { - printColor("Safari cookies directory not found.", 'r', true, std::cerr); + printColoredErrorln('r', "Safari cookies directory not found."); return false; } @@ -414,9 +419,9 @@ bool clearSafariTracks() { } } - const std::string historyPath = getHomeDir() + "/Library/Safari"; + const miSTL::string historyPath = getHomeDir() + "/Library/Safari"; if (!fs::exists(historyPath)) { - printColor("Safari history directory not found.", 'r', true, std::cerr); + printColoredErrorln('c', "Safari history directory not found."); return false; } @@ -434,7 +439,8 @@ bool clearSafariTracks() { return true; #else - printColor("Safari is only available on macOS.", 'r', true, std::cerr); + printColoredErrorln('r', "Safari is only available on macOS."); + return false; #endif } @@ -447,7 +453,7 @@ bool clearFirefoxTracks() { #elif __APPLE__ return clearFirefoxTracks(getHomeDir() + "/Library/Application Support/Firefox"); #else - printColor("This OS is not supported at the moment.", 'r', true, std::cerr); + printColoredErrorln('r', "This OS is not supported at the moment."); return false; #endif } @@ -461,40 +467,41 @@ bool clearTracks(const std::uint_fast8_t &browsers) { if (browsers & std::to_underlying(Browser::Firefox)) { std::cout << "Clearing Firefox tracks..." << std::endl; - ret = clearFirefoxTracks(); - printColor(ret ? "Firefox tracks cleared successfully." : "Failed to clear Firefox tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearFirefoxTracks()) + printColoredOutputln('g', "Firefox tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Firefox tracks."); } if (browsers & std::to_underlying(Browser::Chrome)) { std::cout << "\nClearing Chrome tracks..." << std::endl; - ret = clearChromeTracks(); - printColor(ret ? "Chrome tracks cleared successfully." : "Failed to clear Chrome tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearChromeTracks()) + printColoredOutputln('g', "Chrome tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Chrome tracks."); } if (browsers & std::to_underlying(Browser::Chromium)) { std::cout << "\nClearing Chromium tracks..." << std::endl; - ret = clearChromiumTracks(); - printColor(ret ? "Chromium tracks cleared successfully." : "Failed to clear Chromium tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearChromiumTracks()) + printColoredOutputln('g', "Chromium tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Chromium tracks."); } if (browsers & std::to_underlying(Browser::Opera)) { std::cout << "\nClearing Opera tracks..." << std::endl; - ret = clearOperaTracks(); - printColor(ret ? "Opera tracks cleared successfully." : "Failed to clear Opera tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearOperaTracks()) + printColoredOutputln('g', "Opera tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Opera tracks."); } if (browsers & std::to_underlying(Browser::Safari)) { #if __APPLE__ std::cout << "Clearing Safari tracks..." << std::endl; - ret = clearSafariTracks(); - printColor(ret ? "Safari tracks cleared successfully." : "Failed to clear Safari tracks.", ret ? 'g' : 'r', - true, ret ? std::cout : std::cerr); + if (clearSafariTracks()) + printColoredOutputln('g', "Safari tracks cleared successfully."); + else printColoredErrorln('r', "Failed to clear Safari tracks."); + #else - printColor("\nSafari is only available on macOS.", 'r', true, std::cerr); + printColoredErrorln('r', "\nSafari is only available on macOS."); ret = false; #endif } @@ -509,31 +516,31 @@ export void clearPrivacyTracks() { const std::uint_fast8_t browsers = detectBrowsers(); if (browsers == 0) [[unlikely]] { - printColor("No supported browsers found.", 'r', true, std::cerr); + printColoredErrorln('r', "No supported browsers found."); return; } - printColor("Supported browsers found:", 'y', true); + printColoredOutputln('y', "Supported browsers found:"); if (browsers & std::to_underlying(Browser::Firefox)) - printColor("Firefox", 'c', true); + printColoredOutputln('c', "Firefox"); if (browsers & std::to_underlying(Browser::Chrome)) - printColor("Chrome", 'c', true); + printColoredOutputln('c', "Chrome"); if (browsers & std::to_underlying(Browser::Chromium)) - printColor("Chromium", 'c', true); + printColoredOutputln('c', "Chromium"); if (browsers & std::to_underlying(Browser::Opera)) - printColor("Opera", 'c', true); + printColoredOutputln('c', "Opera"); if (browsers & std::to_underlying(Browser::Safari)) - printColor("Safari", 'c', true); + printColoredOutputln('c', "Safari"); - printColor("\nAll the cookies and browsing history of the above browsers will be deleted.", 'r', true); - printColor("Continue? (y/n): ", 'c'); + printColoredOutputln('r', "\nAll the cookies and browsing history of the above browsers will be deleted."); + printColoredOutput('c', "Continue? (y/n): "); if (validateYesNo()) { const auto cleared{clearTracks(browsers)}; - printColor(cleared ? "\nAll tracks cleared successfully.\n" : "\nFailed to clear all tracks.\n", - cleared ? 'g' : 'r', true, cleared ? std::cout : std::cerr); - } else printColor("Aborted.", 'r', true); + printColoredOutputln(cleared ? 'g' : 'r', "{}", + cleared ? "\nAll tracks cleared successfully." : "\nFailed to clear all tracks."); + } else printColoredOutputln('r', "Aborted."); } diff --git a/src/secureAllocator.cppm b/src/secureAllocator.cppm index c1f30a3..f0a27e2 100644 --- a/src/secureAllocator.cppm +++ b/src/secureAllocator.cppm @@ -16,7 +16,6 @@ module; -#include #include #include #include @@ -25,7 +24,6 @@ module; export module secureAllocator; export namespace privacy { - template /// \class Allocator /// \brief Custom allocator for STL containers, which locks and zeroizes memory. @@ -33,7 +31,6 @@ export namespace privacy { /// \details Adapted from https://en.cppreference.com/w/cpp/named_req/Allocator class Allocator { public: - [[maybe_unused]] typedef T value_type; /// Default constructor @@ -47,15 +44,15 @@ export namespace privacy { /// Copy constructor template - constexpr explicit Allocator(const Allocator &) noexcept {} + constexpr explicit Allocator(const Allocator &) noexcept { + } /// Allocate memory - [[maybe_unused]] [[nodiscard]] constexpr T *allocate(std::size_t n) { + [[maybe_unused]] [[nodiscard]] constexpr T *allocate(const std::size_t n) { if (n > std::numeric_limits::max() / sizeof(T)) throw std::bad_array_new_length(); - if (auto p = static_cast(::operator new(n * sizeof(T)))) { - sodium_mlock(p, n * sizeof(T)); // Lock the allocated memory + if (auto p = static_cast(sodium_malloc(n * sizeof(T)))) { return p; } @@ -63,9 +60,8 @@ export namespace privacy { } /// Deallocate memory - [[maybe_unused]] constexpr void deallocate(T *p, std::size_t n) noexcept { - sodium_munlock(p, n * sizeof(T)); // Unlock and zeroize memory - ::operator delete(p); + [[maybe_unused]] static constexpr void deallocate(T *p, const std::size_t n [[maybe_unused]]) noexcept { + sodium_free(p); } }; @@ -85,8 +81,7 @@ export namespace privacy { using string = std::basic_string, Allocator >; template - using vector = std::vector>; + using vector = std::vector >; using istringstream = std::basic_istringstream, Allocator >; - -} // namespace privacy +} // namespace privacy diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp deleted file mode 100644 index 8edf6a9..0000000 --- a/src/utils/utils.cpp +++ /dev/null @@ -1,298 +0,0 @@ -// Privacy Shield: A Suite of Tools Designed to Facilitate Privacy Management. -// Copyright (C) 2024 Ian Duncan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see https://www.gnu.org/licenses. -module; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -module utils; - -import secureAllocator; - -constexpr int MAX_PASSPHRASE_LEN = 1024; // Maximum length of a passphrase - - -/// \brief Performs Base64 decoding of a string into binary data. -/// \param encodedData Base64 encoded string. -/// \return a vector of the decoded binary data. -/// \throws std::bad_alloc if memory allocation fails. -/// \throws std::runtime_error if the decoding operation fails. -std::vector base64Decode(const std::string_view encodedData) { - // Create a BIO object to decode the data - std::unique_ptr bio( - BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), &BIO_free_all); - if (bio == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Create a base64 BIO - BIO *b64 = BIO_new(BIO_f_base64()); - if (b64 == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Don't use newlines to flush buffer - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - - // Push the base64 BIO to the memory BIO - bio.reset(BIO_push(b64, bio.release())); // Transfer ownership to bio - - std::vector decodedData(encodedData.size()); - - // Decode the data - const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); - if (len < 0) - throw std::runtime_error("BIO_read() failed."); - - // Resize to the actual length of the decoded data - decodedData.resize(len); - - return decodedData; -} - -// This concept checks if the type provides the functionality of a string -template -concept StringLike = std::same_as >; - -/// \brief Trims space (whitespace) off the beginning and end of a string. -/// \param str the string to trim. -void stripString(StringLike auto &str) noexcept { - constexpr std::string_view space = " \t\n\r\f\v"; - - // Trim the leading space - str.erase(0, str.find_first_not_of(space)); - - // Trim the trailing space - str.erase(str.find_last_not_of(space) + 1); -} - -/// \brief Gets a response string from user input. -/// -/// This function prompts the user with the given prompt and reads a response string -/// from the standard input. -/// -/// \param prompt The prompt to display to the user. -/// \return The response string entered by the user if successful, else nullptr. -std::string getResponseStr(const std::string_view prompt) { - std::cout << prompt << std::endl; - char *tmp = readline("> "); - if (tmp == nullptr) return std::string{}; - - auto str = std::string{tmp}; - - // Trim leading and trailing spaces - stripString(str); - - // tmp must be freed - std::free(tmp); - - return str; -} - -/// \brief Captures the user's response while offering editing capabilities. -/// while the user is entering the data. -/// \param prompt the prompt displayed to the user for the input. -/// \return the user's input (an integer) on if it's convertible to integer, else 0. -int getResponseInt(const std::string_view prompt) { - // A lambda to convert a string to an integer - constexpr auto toInt = [](const std::string_view s) noexcept -> int { - int value; - return std::from_chars(s.begin(), s.end(), value).ec == std::errc{} ? value : 0; - }; - - return toInt(getResponseStr(prompt)); -} - -/// \brief Reads sensitive input from a terminal without echoing them. -/// \param prompt the prompt to display. -/// \return the user's input. -/// \throws std::bad_alloc if memory allocation fails. -/// \throws std::runtime_error if memory locking/unlocking fails. -privacy::string getSensitiveInfo(const std::string_view prompt) { - // Allocate a buffer for the password - auto *buffer = static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)); - if (buffer == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - // Lock the memory to prevent swapping - if (sodium_mlock(buffer, MAX_PASSPHRASE_LEN) == -1) { - sodium_free(buffer); - throw std::runtime_error("Failed to lock memory."); - } - - // Turn off terminal echoing - termios oldSettings{}, newSettings{}; - - tcgetattr(STDIN_FILENO, &oldSettings); - newSettings = oldSettings; - newSettings.c_lflag &= ~ECHO; - tcsetattr(STDIN_FILENO, TCSANOW, &newSettings); - - // Prompt the user for the password - std::cout << prompt; - - int index = 0; // current position in the buffer - char ch; - while (std::cin.get(ch) && ch != '\n') { - // check for backspace - if (ch == '\b') { - if (index > 0) { - --index; // move back one position in the buffer - } - } else { - // Check if buffer is not full - if (index < MAX_PASSPHRASE_LEN - 1) { - buffer[index++] = ch; - } - } - } - buffer[index] = '\0'; // Null-terminate the string - - // Restore terminal settings - tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); - - privacy::string passphrase{buffer}; - - // Unlock the memory - if (sodium_munlock(buffer, MAX_PASSPHRASE_LEN) == -1) - throw std::runtime_error("Failed to unlock memory."); - - // Free the buffer - sodium_free(buffer); - - // Trim leading and trailing spaces - stripString(passphrase); - - std::cout << std::endl; - - return passphrase; -} - -/// \brief Confirms a user's response to a yes/no (y/n) situation. -/// \param prompt The confirmation prompt. -/// \return True if the user confirms the action, else false. -bool validateYesNo(const std::string_view prompt) { - const std::string resp = getResponseStr(prompt); - if (resp.empty()) return false; - return std::tolower(resp.at(0)) == 'y'; -} - -/// \brief Checks if an existing file grants write permissions. -/// to the current user. -/// \param filename the path to the file. -/// \return true if the current user has write permissions, else false. -bool isWritable(const std::string &filename) { - return access(filename.c_str(), F_OK | W_OK) == 0; -} - -/// \brief Checks if an existing file grants read permissions. -/// to the current user. -/// \param filename the path to the file. -/// \return true if the current user has read permissions, else false. -bool isReadable(const std::string &filename) { - return access(filename.c_str(), F_OK | R_OK) == 0; -} - -/// \brief Checks the available space on disk. -/// \param path The path to check. -/// \return The available space in bytes. -/// -/// \warning This function does not throw, and returns 0 in case of an error. -/// \note This function is meant to be used to detect possible errors -/// early enough before file operations, and to warn the user to -/// check their filesystem storage space when it seems insufficient. -std::uintmax_t getAvailableSpace(const fs::path &path) noexcept { - fs::path filePath{path}; - - std::error_code ec; // For ignoring errors to avoid throwing - - // Find an existing component of the path - while ((!exists(filePath, ec)) && filePath.has_parent_path()) - filePath = filePath.parent_path(); - if (ec) ec.clear(); - - auto [capacity, free, available] = space(canonical(filePath, ec), ec); - - // Return 0 in case of an error - return std::cmp_less(available, 0) || std::cmp_equal(available, UINTMAX_MAX) ? 0 : available; -} - - -/// \brief Copies a file's permissions to another, replacing if necessary. -/// \param srcFile The source file. -/// \param destFile The destination file. -/// \return True if the operation is successful, else false. -/// -/// \note This function is only needed for the preservation of file permissions -/// during encryption and decryption. -bool copyFilePermissions(const std::string_view srcFile, const std::string_view destFile) noexcept { - std::error_code ec; - // Get the permissions of the input file - const auto permissions = fs::status(srcFile, ec).permissions(); - if (ec) return false; - - // Set the permissions to the output file - fs::permissions(destFile, permissions, fs::perm_options::replace, ec); - if (ec) return false; - - return true; -} - -/// \brief Gets the value of an environment variable. -/// \param var an environment variable to query. -/// \return the value of the environment variable if it exists, else nullopt (nothing). -/// \note The returned value MUST be checked before access. -std::optional getEnv(const char *const var) { - // Use secure_getenv() if available -#if _GNU_SOURCE - if (const char *value = secure_getenv(var)) - return value; -#else - if (const char *value = std::getenv(var)) - return value; -#endif - return std::nullopt; -} - -/// \brief Retrieves the user's home directory -/// \return The home directory read from {'HOME', 'USERPROFILE'} -/// environment variables, else the current working directory (or an empty -/// string if the current directory couldn't be determined). -std::string getHomeDir() noexcept { - std::error_code ec; - // Try to get the home directory from the environment variables - if (auto envHome = getEnv("HOME"); envHome) - return *envHome; - if (auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) - return *envUserProfile; - - // If the environment variables are not set, use the current working directory - std::cerr << "\nCouldn't find your home directory, using the current working directory instead.." << std::endl; - - std::string currentDir = std::filesystem::current_path(ec); - if (ec) std::cerr << ec.message() << std::endl; - - return currentDir; -} diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm index 42e34b8..bf425c6 100644 --- a/src/utils/utils.cppm +++ b/src/utils/utils.cppm @@ -16,34 +16,69 @@ module; +#include +#include +#include +#include #include #include #include -#include -#include #include #include +#include +#include +#include +#include export module utils; import secureAllocator; +import mimallocSTL; namespace fs = std::filesystem; -static const std::unordered_map COLOR = { - {'r', "\033[1;31m"}, // Red - {'g', "\033[1;32m"}, // Green - {'y', "\033[1;33m"}, // Yellow - {'b', "\033[1;34m"}, // Blue - {'m', "\033[1;35m"}, // Magenta - {'c', "\033[1;36m"}, // Cyan - {'w', "\033[1;37m"}, // White -}; +constexpr int MAX_PASSPHRASE_LEN = 1024; ///< Maximum length of a passphrase -template -// Describes a type that can be formatted to the output stream -concept PrintableToStream = requires(std::ostream &os, const T &t) { - os << t; + +/// \class ColorConfig +/// \brief A singleton class used to manage the color configuration of the terminal output. +/// This class encapsulates the \p suppressColor functionality, ensuring that there is only +/// one \p suppressColor instance throughout the application. +/// It provides methods to get and set the \p suppressColor value. +class ColorConfig { +public: + /// \brief Gets the instance of the \p ColorConfig singleton. + /// \return A reference to the singleton instance of the \p ColorConfig class. + static ColorConfig &getInstance() noexcept { + static ColorConfig instance; + return instance; + } + + // Delete the copy constructor and assignment operator + ColorConfig(ColorConfig const &) = delete; + + void operator=(ColorConfig const &) = delete; + + /// \brief Gets the \p suppressColor value. + /// \return The current value of the \p suppressColor variable. + [[nodiscard]] bool getSuppressColor() const noexcept { + return suppressColor; + } + + /// \brief Sets the \p suppressColor value. + /// \param value The new value for the \p suppressColor variable. + void setSuppressColor(const bool value) noexcept { + suppressColor = value; + } + +private: + /// \brief Private constructor for the \p ColorConfig class. + /// This constructor initializes the \p suppressColor variable to false. + ColorConfig() : suppressColor(false) { + } + + // The suppressColor value + bool suppressColor; }; template @@ -58,20 +93,115 @@ concept uCharVector = std::copy_constructible && requires(T t, unsigned char t.shrink_to_fit(); }; +/// \brief Returns the ANSI color code for the given character. +/// \param color The character representing the color. +/// \return The ANSI color code corresponding to the input character. +constexpr const char *getColorCode(const char color) noexcept { + switch (color) { + case 'r': // Red + return "\033[1;31m"; + case 'g': // Green + return "\033[1;32m"; + case 'y': // Yellow + return "\033[1;33m"; + case 'b': // Blue + return "\033[1;34m"; + case 'm': // Magenta + return "\033[1;35m"; + case 'c': // Cyan + return "\033[1;36m"; + case 'w': // White + return "\033[1;37m"; + default: // No color + return ""; + } +} + +/// \brief Completes a filename based on the user's input. +/// This function is used as a callback for the isocline library's readline function. +/// It provides filename completion when the user presses the Tab key. +/// \param cenv A pointer to the completion environment provided by readline. +/// \param input The user's input. +static void normal_completer(ic_completion_env_t *cenv, const char *input) { + ic_complete_filename(cenv, input, 0, nullptr, nullptr); +} + +/// \brief A null completer function for the isocline library's readline function. +/// This function is used as a callback for the isocline library's readline function. +/// It does not provide any completion, and is used when no completion is desired. +static void null_completer(ic_completion_env_t *, const char *) { +} + +/// \brief This concept checks if a type provides the functionality of a string +/// \tparam T The type to check. +template +concept StringLike = std::same_as >; + +/// \brief Trims space (whitespace) off the beginning and end of a string. +/// \param str the string to trim. +void stripString(StringLike auto &str) noexcept { + constexpr std::string_view space = " \t\n\r\f\v"; + + // Trim the leading space + str.erase(0, str.find_first_not_of(space)); + + // Trim the trailing space + str.erase(str.find_last_not_of(space) + 1); +} + export { + /// \brief Prints colored output to the console. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredOutput(const char color, std::format_string fmt, Args &&... args) { + // Print the output depending on the color configuration + if (ColorConfig::getInstance().getSuppressColor()) + std::cout << std::vformat(fmt.get(), std::make_format_args(args...)); + else std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; + } - /// \brief Prints colored text to a stream. - /// \param text the text to print. - /// \param color a character representing the desired color. - /// \param printNewLine a flag to indicate whether a newline should be printed after the text. - /// \param os the stream object to print to. - void printColor(const PrintableToStream auto &text, const char &color = 'w', const bool &printNewLine = false, - std::ostream &os = std::cout) { - // Print the text in the desired color - os << (COLOR.contains(color) ? COLOR.at(color) : "") << text << "\033[0m"; + /// \brief Prints colored output to the console and adds a newline at the end. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredOutputln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cout << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; + else + std::cout << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; + } + + /// \brief Prints colored error messages to the console. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredError(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)); + else std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m"; + } - // Print a newline if requested - if (printNewLine) os << std::endl; + /// \brief This function prints colored error messages to the console and adds a newline at the end. + /// \tparam Args Variadic template for all types of arguments that can be passed. + /// \param color The color code for the output. + /// \param fmt The format string for the output. + /// \param args The arguments to be printed. + template + void printColoredErrorln(const char color, std::format_string fmt, Args &&... args) { + if (ColorConfig::getInstance().getSuppressColor()) + std::cerr << std::vformat(fmt.get(), std::make_format_args(args...)) << std::endl; + else + std::cerr << getColorCode(color) << std::vformat(fmt.get(), std::make_format_args(args...)) << "\033[0m" << + std::endl; } /// \brief Performs Base64 encoding of binary data into a string. @@ -79,7 +209,7 @@ export { /// \return Base64-encoded string. /// \throws std::bad_alloc if memory allocation fails. /// \throws std::runtime_error if encoding fails. - std::string base64Encode(const uCharVector auto &input) { + miSTL::string base64Encode(const uCharVector auto &input) { // Create a BIO object to encode the data const std::unique_ptr b64(BIO_new(BIO_f_base64()), &BIO_free_all); if (b64 == nullptr) @@ -108,30 +238,299 @@ export { BIO_get_mem_ptr(b64.get(), &bufferPtr); // Create a string from the data - std::string encodedData(bufferPtr->data, bufferPtr->length); + miSTL::string encodedData(bufferPtr->data, bufferPtr->length); return encodedData; } - std::vector base64Decode(std::string_view encodedData); + /// \brief Performs Base64 decoding of a string into binary data. + /// \param encodedData Base64 encoded string. + /// \return a vector of the decoded binary data. + /// \throws std::bad_alloc if memory allocation fails. + /// \throws std::runtime_error if the decoding operation fails. + miSTL::vector base64Decode(const std::string_view encodedData) { + // Create a BIO object to decode the data + std::unique_ptr bio( + BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), &BIO_free_all); + if (bio == nullptr) + throw std::bad_alloc(); // Memory allocation failed + + // Create a base64 BIO + BIO *b64 = BIO_new(BIO_f_base64()); + if (b64 == nullptr) + throw std::bad_alloc(); // Memory allocation failed + + // Don't use newlines to flush buffer + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + + // Push the base64 BIO to the memory BIO + bio.reset(BIO_push(b64, bio.release())); // Transfer ownership to bio + + miSTL::vector decodedData(encodedData.size()); + + // Decode the data + const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); + if (len < 0) + throw std::runtime_error("BIO_read() failed."); + + // Resize to the actual length of the decoded data + decodedData.resize(len); + + return decodedData; + } + + bool validateYesNo(const char *prompt = ""); + + /// \brief Prompts the user for a filesystem path. + /// \param prompt The prompt to display to the user. + /// \return The filesystem path entered by the user if successful, else an empty path. + fs::path getFilesystemPath(const char *prompt = "") { + // Enable filename completion and automatic tab completion + ic_set_default_completer(normal_completer, nullptr); + ic_enable_auto_tab(true); + + // Display the prompt + std::puts(prompt); + // Read the input from the user + if (char *input = ic_readline("")) { + fs::path result(input); + // Free the input buffer + std::free(input); + return result; + } + // Handle Ctrl+C/D + printColoredError('r', "Input canceled. Unsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); + if (validateYesNo()) std::exit(1); + + return fs::path{}; + } + + /// \brief Gets a response string from user input. + /// This function prompts the user with the given prompt and reads a response string + /// from the standard input. + /// \param prompt The prompt to display to the user. + /// \return The response string entered by the user if successful, else an empty string. + miSTL::string getResponseStr(const char *prompt = "") { + // Disable completions + ic_set_default_completer(null_completer, nullptr); + + // Read the response from the user + std::puts(prompt); + if (char *input = ic_readline("")) { + miSTL::string result{input}; + ic_free(input); + stripString(result); + return result; + } + // Handle Ctrl+C/D + printColoredError('r', "Input canceled. Unsaved data might be lost if you quit now." + "\nDo you still want to quit? (y/n):"); + if (validateYesNo()) std::exit(1); - int getResponseInt(std::string_view prompt = ""); + return ""; + } - std::string getResponseStr(std::string_view prompt = ""); + /// \brief Captures the user's response while offering editing capabilities. + /// while the user is entering the data. + /// \param prompt the prompt displayed to the user for the input. + /// \return the user's input (an integer) on if it's convertible to integer, else 0. + int getResponseInt(const char *prompt = "") { + // A lambda to convert a string to an integer + constexpr auto toInt = [](const std::string_view s) noexcept -> int { + int value; + return std::from_chars(s.begin(), s.end(), value).ec == std::errc{} ? value : 0; + }; - bool isWritable(const std::string &filename); + return toInt(getResponseStr(prompt)); + } - bool isReadable(const std::string &filename); + /// \brief Reads sensitive input from a terminal without echoing them. + /// \param prompt the prompt to display. + /// \return the user's input. + /// \throws std::bad_alloc if memory allocation fails. + /// \throws std::runtime_error if memory locking/unlocking fails. + privacy::string getSensitiveInfo(const char *prompt = "") { + // A lambda to free memory allocated by sodium_malloc + auto deleter = [](char *ptr) noexcept -> void { + sodium_free(ptr); + }; - std::uintmax_t getAvailableSpace(const fs::path &path) noexcept; + // Allocate memory for the passphrase + const std::unique_ptr buffer(static_cast(sodium_malloc(MAX_PASSPHRASE_LEN)), + deleter); - bool copyFilePermissions(std::string_view srcFile, std::string_view destFile) noexcept; + if (!buffer) + throw std::bad_alloc(); // Memory allocation failed - privacy::string getSensitiveInfo(std::string_view prompt = ""); + // Lock the memory to prevent swapping + if (sodium_mlock(buffer.get(), MAX_PASSPHRASE_LEN) == -1) + throw std::runtime_error("Failed to lock memory."); - bool validateYesNo(std::string_view prompt = ""); + // Turn off terminal echoing + termios oldSettings{}, newSettings{}; - std::string getHomeDir() noexcept; + tcgetattr(STDIN_FILENO, &oldSettings); + newSettings = oldSettings; + newSettings.c_lflag &= ~ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &newSettings); + + // Prompt the user for the password + std::cout << prompt; + + int index = 0; // current position in the buffer + char ch; + while (std::cin.get(ch) && ch != '\n') { + // check for backspace + if (ch == '\b') { + if (index > 0) { + --index; // move back one position in the buffer + } + } else { + // Check if buffer is not full + if (index < MAX_PASSPHRASE_LEN - 1) { + buffer.get()[index++] = ch; + } + } + } + buffer.get()[index] = '\0'; // Null-terminate the string + + // Restore terminal settings + tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); + + privacy::string passphrase{buffer.get()}; + + // Unlock the memory + if (sodium_munlock(buffer.get(), MAX_PASSPHRASE_LEN) == -1) + throw std::runtime_error("Failed to unlock memory."); + + // Trim leading and trailing spaces + stripString(passphrase); + + std::cout << std::endl; + + return passphrase; + } + + /// \brief Checks if an existing file grants write permissions. + /// to the current user. + /// \param filename the path to the file. + /// \return true if the current user has write permissions, else false. + bool isWritable(const miSTL::string &filename) { + return access(filename.c_str(), F_OK | W_OK) == 0; + } - std::optional getEnv(const char *var); + /// \brief Checks if an existing file grants read permissions. + /// to the current user. + /// \param filename the path to the file. + /// \return true if the current user has read permissions, else false. + bool isReadable(const miSTL::string &filename) { + return access(filename.c_str(), F_OK | R_OK) == 0; + } + + /// \brief Checks the available space on disk. + /// \param path The path to check. + /// \return The available space in bytes. + /// + /// \warning This function does not throw, and returns 0 in case of an error. + /// \note This function is meant to be used to detect possible errors + /// early enough before file operations, and to warn the user to + /// check their filesystem storage space when it seems insufficient. + std::uintmax_t getAvailableSpace(const fs::path &path) noexcept { + fs::path filePath{path}; + + std::error_code ec; // For ignoring errors to avoid throwing + + // Find an existing component of the path + while ((!exists(filePath, ec)) && filePath.has_parent_path()) + filePath = filePath.parent_path(); + if (ec) ec.clear(); + + auto [capacity, free, available] = space(canonical(filePath, ec), ec); + + // Return 0 in case of an error + return std::cmp_less(available, 0) || std::cmp_equal(available, UINTMAX_MAX) ? 0 : available; + } + + /// \brief Copies a file's permissions to another, replacing if necessary. + /// \param srcFile The source file. + /// \param destFile The destination file. + /// \return True if the operation is successful, else false. + /// + /// \note This function is only needed for the preservation of file permissions + /// during encryption and decryption. + bool copyFilePermissions(const std::string_view srcFile, const std::string_view destFile) noexcept { + std::error_code ec; + // Get the permissions of the input file + const auto permissions = fs::status(srcFile, ec).permissions(); + if (ec) return false; + + // Set the permissions to the output file + fs::permissions(destFile, permissions, fs::perm_options::replace, ec); + if (ec) return false; + + return true; + } + + /// \brief Confirms a user's response to a yes/no (y/n) situation. + /// \param prompt The confirmation prompt. + /// \return True if the user confirms the action, else false. + bool validateYesNo(const char *prompt) { + const miSTL::string resp = getResponseStr(prompt); + if (resp.empty()) return false; + return std::tolower(resp.at(0)) == 'y'; + } + + /// \brief Gets the value of an environment variable. + /// \param var an environment variable to query. + /// \return the value of the environment variable if it exists, else nullopt (nothing). + /// \note The returned value MUST be checked before access. + std::optional getEnv(const char *const var) { + // Use secure_getenv() if available +#if _GNU_SOURCE + if (const char *value = secure_getenv(var)) + return value; +#else + if (const char *value = std::getenv(var)) + return value; +#endif + return std::nullopt; + } + + /// \brief Retrieves the user's home directory + /// \return The home directory read from {'HOME', 'USERPROFILE'} + /// environment variables, else the current working directory (or an empty + /// string if the current directory couldn't be determined). + miSTL::string getHomeDir() noexcept { + std::error_code ec; + // Try to get the home directory from the environment variables + if (const auto envHome = getEnv("HOME"); envHome) + return *envHome; + if (const auto envUserProfile = getEnv("USERPROFILE"); envUserProfile) + return *envUserProfile; + + // If the environment variables are not set, use the current working directory + std::cerr << "\nCouldn't find your home directory, using the current working directory instead.." << std::endl; + + miSTL::string currentDir = std::filesystem::current_path(ec).string().c_str(); + if (ec) std::cerr << ec.message() << std::endl; + + return currentDir; + } + + /// \brief Configures the color output of the terminal. + /// \param disable a flag to indicate whether color output should be disabled. + void configureColor(const bool disable = false) noexcept { + // Check if the user has requested no color + if (disable) { + ColorConfig::getInstance().setSuppressColor(true); + return; + } + // Process the environment variable to suppress color output + const auto noColorEnv = getEnv("NO_COLOR"); + const auto termEnv = getEnv("TERM"); + const bool suppressColor = noColorEnv.has_value() || + (termEnv.has_value() && (termEnv.value() == "dumb" || termEnv.value() == "emacs")); + ColorConfig::getInstance().setSuppressColor(suppressColor); + } } diff --git a/vcpkg.json b/vcpkg.json deleted file mode 100644 index dbefc28..0000000 --- a/vcpkg.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "privacyshield", - "version-string": "2.5.0", - "builtin-baseline": "7b5ca09708ae42dba9517d4e0a0c975d087f1061", - "dependencies": [ - { - "name": "libsodium", - "version>=": "1.0.18#9" - }, - { - "name": "openssl", - "version>=": "3.1.2#3" - }, - { - "name": "libgcrypt", - "version>=": "1.10.1#1" - }, - { - "name": "readline-unix", - "version>=": "8.2" - }, - { - "name": "readline-osx", - "version>=": "2020-01-04" - }, - { - "name": "blake3", - "version>=": "1.4.0" - } - ] -}