diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 47d7617..a7d5184 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -11,25 +11,29 @@ jobs: runs-on: ${{ matrix.os }} strategy: - fail-fast: true + fail-fast: false matrix: os: [ ubuntu-latest, macos-13 ] build_type: [ Debug, Release ] - c_compiler: [ gcc, clang ] + c_compiler: [ clang ] include: - os: macos-13 - c_compiler: gcc - cpp_compiler: g++-13 + 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" - - LD_LIBRARY_PATH="/usr/local/opt/llvm/lib" - - DYLD_LIBRARY_PATH="/usr/local/opt/llvm/lib" - - - os: ubuntu-latest - c_compiler: gcc - cpp_compiler: g++-13 + 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" + +# - os: macos-13 +# c_compiler: gcc +# cpp_compiler: g++-13 +# +# - os: ubuntu-latest +# c_compiler: gcc +# cpp_compiler: g++-13 - os: ubuntu-latest c_compiler: clang @@ -38,7 +42,7 @@ jobs: # Don't include the following configurations in the matrix exclude: - os: macos-13 - c_compiler: clang # LLVM Clang17+ required, and building it from the source will take longer + build_type: Debug steps: # Install dependencies: cmake, ninja, gcc, libgcrypt, openssl, readline, and libsodium @@ -46,67 +50,109 @@ jobs: if: matrix.os == 'macos-13' run: | brew update - brew install cmake ninja gcc libgcrypt openssl@3 readline libsodium + brew install llvm cmake ninja gcc libgcrypt openssl@3 readline libsodium gnupg pinentry 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 + # - 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 - uses: actions/checkout@v4 - # Install BLAKE3 - - name: Build BLAKE3 - run: | - 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 - - name: Set reusable strings id: strings shell: bash run: | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + # Build project + - name: Build PrivacyShield + if: matrix.os == 'ubuntu-latest' + 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 + # + + - name: Install Blake3 + if: matrix.os == 'macos-13' + run: | + ./scripts/install-blake3.sh ${{ matrix.c_compiler }} + - name: Configure CMake + if: matrix.os == 'macos-13' 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=${{ matrix.cpp_compiler }} - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -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_BUILD_TYPE=${{ matrix.build_type }} -S ${{ github.workspace }} -G Ninja - name: Build + if: matrix.os == 'macos-13' run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} -j 4 - - - name: Test +# +# - 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 }} - # Execute tests defined by the CMake configuration - run: ctest --build-config ${{ matrix.build_type }} - - # Add execution permission to the binary and tar it + run: | + cpack -G DragNDrop + - name: Package + if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Release' working-directory: ${{ steps.strings.outputs.build-output-dir }} run: | - chmod +x privacyShield - tar -czvf privacyShield.tar.gz privacyShield - - # Upload the built artifacts + 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 }}.tar.gz" - path: "${{ steps.strings.outputs.build-output-dir }}/privacyShield.tar.gz" + name: "${{ matrix.os }}-${{ matrix.build_type }}" + path: "${{ github.workspace }}/Packages" overwrite: true - + if-no-files-found: 'warn' diff --git a/.gitignore b/.gitignore index 2d5600b..a91432d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea/**/dictionaries .idea/**/shelf .idea/**/Project_Default.xml +.idea/**/editor.xml # AWS User-specific .idea/**/aws.xml @@ -617,3 +618,5 @@ Network Trash Folder Temporary Items .apdisk +# Generated packages +Packages/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 012dcbf..2636b37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,19 +14,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see https://www.gnu.org/licenses. -cmake_minimum_required(VERSION 3.25) +# CMake 3.28+ is required for C++20 modules +cmake_minimum_required(VERSION 3.28) project(privacyShield - VERSION 1.0.0 + VERSION 2.0.0 DESCRIPTION "A suite of tools for privacy and security" LANGUAGES CXX) -# If the user is not on a unix-like system, fail the build +set(CMAKE_PROJECT_HOMEPAGE_URL "https://shield.boujee.tech") + +# C++23 support is required for this project +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# If the target system is not UNIX, fail the build if (NOT UNIX) message(FATAL_ERROR "This project is only supported on unix-like systems.") endif () -# If the user does not specify a build type, default to Release +# If the build type is not specified, default to Release if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif () @@ -34,14 +41,10 @@ endif () # Set the path to additional CMake modules set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules") -# C++23 is required for this project -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Additional checks for the Debug build +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -Wpedantic") -# Check for extra compiler warnings and errors in debug config -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -pedantic") - -# Find dependencies +# Find the required packages find_package(OpenSSL REQUIRED) find_package(Sodium REQUIRED) find_package(Readline REQUIRED) @@ -53,12 +56,21 @@ add_executable(privacyShield) # Add sources for the target file(GLOB_RECURSE PRIVACY_SHIELD_SOURCES - "${CMAKE_SOURCE_DIR}/src/*.cpp" - "${CMAKE_SOURCE_DIR}/src/*.hpp") + "${CMAKE_SOURCE_DIR}/src/*.cpp") target_sources(privacyShield PRIVATE ${PRIVACY_SHIELD_SOURCES}) -# Link dependencies +# C++20 Modules +file(GLOB_RECURSE PRIVACY_SHIELD_MODULES + "${CMAKE_SOURCE_DIR}/src/*.cppm") + +target_sources(privacyShield + PRIVATE + FILE_SET CXX_MODULES FILES + ${PRIVACY_SHIELD_MODULES} +) + +# Link libraries target_link_libraries(privacyShield PRIVATE OpenSSL::Crypto PRIVATE Readline::Readline @@ -66,8 +78,11 @@ target_link_libraries(privacyShield PRIVATE Gcrypt::Gcrypt PRIVATE BLAKE3::blake3) -# Install the binary (optional) +# Install the binary (optional), with 0755 permissions +include(GNUInstallDirs) install(TARGETS privacyShield - DESTINATION bin + DESTINATION ${CMAKE_INSTALL_BINDIR} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) + +include(Packing) diff --git a/CMakeModules/FindGcrypt.cmake b/CMakeModules/FindGcrypt.cmake index e39f7af..d932043 100644 --- a/CMakeModules/FindGcrypt.cmake +++ b/CMakeModules/FindGcrypt.cmake @@ -16,19 +16,26 @@ # ################################################################################# # -# A CMake module to find a local installation of Gcrypt +# A CMake module to find a local installation of Gcrypt and Gpg-error -# This module sets the following variables: +# This module sets the following variables for Gcrypt: # GCRYPT_FOUND - True if Gcrypt is found # GCRYPT_INCLUDE_DIR - Include directories for Gcrypt # GCRYPT_LIBRARIES - Linker flags for Gcrypt # GCRYPT_VERSION - Version of Gcrypt -# This module also provides the imported target Gcrypt::Gcrypt +# This module also sets the following variables for Gpg-error: +# GPG_ERROR_FOUND - True if Gpg-error is found +# GPG_ERROR_INCLUDE_DIR - Include directories for Gpg-error +# GPG_ERROR_LIBRARIES - Linker flags for Gpg-error +# GPG_ERROR_VERSION - Version of Gpg-error -# Find the pkg-config package for Gcrypt +# This module also provides the imported targets Gcrypt::Gcrypt and Gcrypt::Gpg_error + +# Find the pkg-config package for Gcrypt & Gpg-error find_package(PkgConfig REQUIRED) pkg_check_modules(GCRYPT REQUIRED libgcrypt) +pkg_check_modules(GPG_ERROR gpg-error) # Set the Gcrypt variables set(GCRYPT_FOUND TRUE) @@ -38,27 +45,50 @@ set(GCRYPT_VERSION ${GCRYPT_VERSION}) # Provide imported target for Gcrypt add_library(Gcrypt::Gcrypt UNKNOWN IMPORTED) + set_target_properties(Gcrypt::Gcrypt PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GCRYPT_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "${GCRYPT_LIBRARIES}" -) + INTERFACE_LINK_LIBRARIES "${GCRYPT_LIBRARIES}") + +# Set the Gpg-error variables +add_library(Gcrypt::Gpg_error UNKNOWN IMPORTED) +set_target_properties(Gcrypt::Gpg_error PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${GPG_ERROR_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${GPG_ERROR_LIBRARIES}") # Find the actual location of the Gcrypt library file find_library(GCRYPT_LIBRARY NAMES libgcrypt.so libgcrypt.dylib libgcrypt.a - HINTS ${GCRYPT_LIBRARY_DIRS} -) + HINTS ${GCRYPT_LIBRARY_DIRS}) + +# Find the actual location of the Gpg-error library file +find_library(GPG_ERROR_LIBRARY + NAMES libgpg-error.so libgpg-error.dylib libgpg-error.a + HINTS ${GPG_ERROR_LIBRARY_DIRS}) # Set the imported location dynamically if (GCRYPT_LIBRARY) set_target_properties(Gcrypt::Gcrypt PROPERTIES - IMPORTED_LOCATION "${GCRYPT_LIBRARY}" - ) + IMPORTED_LOCATION "${GCRYPT_LIBRARY}") else () message(FATAL_ERROR "Gcrypt library not found") endif () + +# Set the imported location dynamically +if (GPG_ERROR_LIBRARY) + set_target_properties(Gcrypt::Gpg_error PROPERTIES + IMPORTED_LOCATION "${GPG_ERROR_LIBRARY}") +else () + message(FATAL_ERROR "Gpg-error library not found") +endif () + # Print Gcrypt information message(STATUS "Found Gcrypt ${GCRYPT_VERSION}") message(STATUS "Gcrypt include directories: ${GCRYPT_INCLUDE_DIR}") message(STATUS "Gcrypt libraries: ${GCRYPT_LIBRARIES}") + +# Print Gpg-error information +message(STATUS "Found Gpg-error ${GPG_ERROR_VERSION}") +message(STATUS "Gpg-error include directories: ${GPG_ERROR_INCLUDE_DIR}") +message(STATUS "Gpg-error libraries: ${GPG_ERROR_LIBRARIES}") diff --git a/CMakeModules/Packing.cmake b/CMakeModules/Packing.cmake new file mode 100644 index 0000000..738d300 --- /dev/null +++ b/CMakeModules/Packing.cmake @@ -0,0 +1,79 @@ +# 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. +# +################################################################################# +# +# A CMake module to generate a package for Privacy Shield +# + +# 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.0.0") +set(CPACK_PACKAGE_CONTACT "dr8co@duck.com") + +SET(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/Packages") + +set(CPACK_SOURCE_IGNORE_FILES + /.git + /.idea + /.github + /.vscode + /.cache + /build + /cmake-build-* + /CMakeFiles + /CMakeScripts + /CMakeModules + /CMakeLists.txt.user + /CMakeCache.txt + /CTestTestfile.cmake + /Makefile + /Makefile.in + /CPackConfig.cmake + /CPackSourceConfig.cmake + /CPackSourceConfig.cmake + /CPack +) + +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") +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 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)") +set(CPACK_RPM_PACKAGE_REQUIRES "libc6 >= 2.35, libstdc++ >= 13.2.0, openssl >= 3.0.0, libsodium >= 1.0.18, readline >= 8.0, libgcrypt >= 1.10.0, libgcc >= 13.2.0") + +set(CPACK_RPM_PACKAGE_LICENSE "GPLv3") + +# Set the section of the package +set(CPACK_DEBIAN_PACKAGE_SECTION "utils") + +# Use the resource file for the license +set(CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE ON) + +set(CPACK_PACKAGE_CHECKSUM "SHA512") + +include(CPack) \ No newline at end of file diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE diff --git a/README.md b/README.md index e0304f9..a74b0ff 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,15 @@ These tools include: * [File Shredder](#file-shredder) * [Browser Privacy Tracks Cleaner](#browser-privacy-tracks-cleaner) * [Duplicate File detector](#file-deduplicator) -* [Getting Started](#getting-started) +* [Building and Installation](#building-and-installation) * [Prerequisites](#prerequisites) - * [Installation/Building](#installationbuilding) + * [Building from Source](#building-from-source) + * [Installation](#installation) + * [Pre-built Packages](#pre-built-packages) + * [Manual Installation](#manual-installation) * [Uninstallation](#uninstallation) + * [Package Managers](#package-managers) + * [Manual](#manual) * [Usage](#usage) * [Command Line Interface](#command-line-interface) * [Contributing](#contributing) @@ -107,7 +112,7 @@ If you find any security vulnerabilities, please report them, or better yet, sub * **Safe** – Privacy Shield supports safe operations, with support for cancellation and error handling. * **Free** – Privacy Shield is free and open-source software, licensed under the GNU General Public License v3.0. -See [LICENSE](./LICENSE.md) for more information. +See [LICENSE](./LICENSE) for more information. * **No Ads**, **Tracking**, and **Telemetry** – Privacy Shield is free of all these things. @@ -317,7 +322,7 @@ BLAKE3 is a fast and secure hash function that is resistant to extension attacks The hashes are computed in parallel using multiple threads to speed up the process. -## Getting Started +## Building and Installation ### Prerequisites @@ -326,17 +331,34 @@ operating system, such as [Linux](https://en.wikipedia.org/wiki/Linux), [BSD](https://en.wikipedia.org/wiki/Berkeley_Software_Distribution), or [macOS](https://en.wikipedia.org/wiki/MacOS). * 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-13/) (or newer), +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. * [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. +* [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+ -### IMPORTANT: A note on installing/building BLAKE3 +**Note:**\ +This project utilizes the [C++20 Modules](https://en.cppreference.com/w/cpp/language/modules) feature, +which is not yet fully supported by all C++ compilers. + +The selected C++ compiler must support C++20 Modules, and must provide a way to discover +the import graph dependencies. + +Only [LLVM Clang](https://clang.llvm.org/) 17+ (16 has an issue in CMake, +where CMake extensions have to be turned off to use modules) +support this feature via the `clang-scan-deps` tool +at the time of writing. + +GCC 14 is expected to support this feature as well. + +Also, a generator that supports C++20 Modules is required. +[Ninja](https://ninja-build.org/) 1.11+ is recommended. + +#### IMPORTANT: A note on installing/building BLAKE3 BLAKE3 is not available in the package repositories of most Linux distributions, so you will need to build it from the source and install it manually. @@ -348,7 +370,7 @@ Another option is to install BLAKE3 using a package manager, such as [Vcpkg](htt **Installing with the script is recommended, as it will also set up the required CMake module for BLAKE3 automatically.** -### Installation/Building +### Building from Source After installing the prerequisites, you can install Privacy Shield by following the steps below: @@ -356,44 +378,90 @@ After installing the prerequisites, you can install Privacy Shield by following # Clone the repository (or download the source code) git clone https://github.com/dr8co/PrivacyShield.git +# Build from the main branch +git checkout main + # Change directory to the project root cd PrivacyShield # Configure the project -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-13 -G Ninja +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++-18 -G Ninja # Build the project cmake --build build --config Release -j 4 # Install the program (optional) -cmake --install build --config Release # Might need sudo +cmake --install build # Might need sudo ``` -Remember to replace `g++-13` with your C++ compiler, and `4` with the number of CPU cores you have (for faster builds).\ +Remember to replace `clang++-18` with your C++ compiler, and `4` with the number of CPU cores you have (for faster builds).\ Also, replace `Ninja` with your build system if you are not using Ninja. -You can also install to a custom location by adding `-DCMAKE_INSTALL_PREFIX=/path/to/install` to the `configure` step. - -If you have installed Privacy Shield, you can run the program by typing `privacyShield` in your terminal -(if you've installed to a custom location, -you will need to add that installation location to your `PATH` environment variable) - -If not, you can run the compiled target directly: +You can then run the program from the build directory: ```bash # Assuming you are in the project root ./build/privacyShield ``` +### Installation + +#### Pre-built Packages + +You can download a package for your platform from the +[releases page](https://github.com/dr8co/PrivacyShield/releases). + +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. + +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.0.0_amd64.deb # Replace with the actual file path +# You can also use apt to install it: +sudo apt install ./privacyshield_2.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.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. + +#### Manual Installation + +After building from the source, you can install the built executable to a custom location by adding +`-DCMAKE_INSTALL_PREFIX=/path/to/install` option to the configuration step when building from the source. + +The program can then be run by typing `privacyShield` in your terminal +(if you've installed to a custom location, you might need to add that installation location to your `PATH` +environment variable) + ### Uninstallation -If you have installed Privacy Shield, you can uninstall it by running the following command: +#### Package Managers + +The package manager used to install the package can be used to uninstall it. + +The specific commands to uninstall the package will depend on the package manager and the platform. + +#### Manual + +If you have installed Privacy Shield manually, you can uninstall it by running the following command: ```bash # From the project root, run: xargs rm -f < build/install_manifest.txt # Might need sudo ``` +Or, just remove everything listed in the `build/install_manifest.txt` file. + ## Usage ### Command Line Interface @@ -456,6 +524,6 @@ However, the feeling of empowered privacy protection is a strong possibility! [![GPLv3](./media/gpl-v3-logo.png)](https://www.gnu.org/licenses/gpl-3.0.en.html) -This project is licensed under the GNU GPLv3 License—see the [LICENSE](./LICENSE.md) file for details. +This project is licensed under the GNU GPLv3 License—see the [LICENSE](./LICENSE) file for details. All third party libraries are licensed under their respective licenses. diff --git a/scripts/buildscript.sh b/scripts/buildscript.sh index 7e31ad5..1e2050c 100755 --- a/scripts/buildscript.sh +++ b/scripts/buildscript.sh @@ -5,7 +5,6 @@ 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. -REQUIRED_PACKAGES=(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) PARALLELISM_LEVEL=4 function check_root() { @@ -17,7 +16,7 @@ function check_root() { } function check_dependencies() { - for cmd in wget add-apt-repository cmake; do + for cmd in wget add-apt-repository; do if ! command -v $cmd &>/dev/null; then echo "$cmd could not be found" exit @@ -27,25 +26,33 @@ function check_dependencies() { 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-17 main" + 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 pinentry-tty gpg - for package in "${REQUIRED_PACKAGES[@]}"; do - if ! dpkg -s "$package" >/dev/null 2>&1; then - apt install -y "$package" - else - echo "$package is already installed" - fi - done + # 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-17 + ./install-blake3.sh clang-18 } function configure_cmake() { - cmake -B build -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DCMAKE_BUILD_TYPE=Debug -G Ninja + cmake -B build -DCMAKE_C_COMPILER=clang-18 -DCMAKE_CXX_COMPILER=clang++-18 -DCMAKE_BUILD_TYPE=Debug -G Ninja } function build_project() { @@ -58,7 +65,9 @@ main() { check_dependencies cd "${0%/*}" || abort install_dependencies + echo "Ninja: $(ninja --version), CMake: $(cmake --version)" build_blake3 + cd .. || abort configure_cmake build_project } diff --git a/scripts/install-blake3.sh b/scripts/install-blake3.sh index 676b0eb..9e81789 100755 --- a/scripts/install-blake3.sh +++ b/scripts/install-blake3.sh @@ -30,6 +30,23 @@ get_number_of_processors() { esac } +# Function to download and install BLAKE3 +install_blake3() { + # change to home directory + 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 . + + cd BLAKE3-1.5.0/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." + +} + # Function to clone repository clone_repo() { git clone https://github.com/BLAKE3-team/BLAKE3.git || error_exit "Failed to clone BLAKE3 repository." @@ -59,5 +76,7 @@ cd "${0%/*}" || error_exit "Failed to change directory to script location." echo "Compiling BLAKE3 with $C_COMPILER compiler.." # Call functions -clone_repo -build_install +# clone_repo +# build_install + +install_blake3 diff --git a/src/duplicateFinder/duplicateFinder.cpp b/src/duplicateFinder/duplicateFinder.cppm similarity index 83% rename from src/duplicateFinder/duplicateFinder.cpp rename to src/duplicateFinder/duplicateFinder.cppm index 15f01a0..d431287 100644 --- a/src/duplicateFinder/duplicateFinder.cpp +++ b/src/duplicateFinder/duplicateFinder.cppm @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "duplicateFinder.hpp" +module; + #include #include #include @@ -26,10 +26,14 @@ #include #include #include +#include + +export module duplicateFinder; +import utils; 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. @@ -74,28 +78,28 @@ inline void handleAccessError(const std::string &filename) { errMsg.reserve(50); switch (errno) { - case EACCES: // Permission denied + case EACCES: // Permission denied errMsg = "You do not have permission to access this item"; break; - case EEXIST: // File exists + case EEXIST: // File exists errMsg = "already exists"; break; - case EISDIR: // Is a directory + case EISDIR: // Is a directory errMsg = "is a directory"; break; - case ELOOP: // Too many symbolic links encountered + case ELOOP: // Too many symbolic links encountered errMsg = "is a loop"; break; - case ENAMETOOLONG: // The filename is too long + case ENAMETOOLONG: // The filename is too long errMsg = "the path is too long"; break; - case ENOENT: // No such file or directory + case ENOENT: // No such file or directory errMsg = "path does not exist"; break; - case EROFS: // Read-only file system + case EROFS: // Read-only file system errMsg = "the file system is read-only"; break; - default: // Success (most likely) + default: // Success (most likely) return; } @@ -111,7 +115,8 @@ void traverseDirectory(const std::string &directoryPath, std::vector & for (const auto &entry: fs::recursive_directory_iterator(directoryPath, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink)) { - if (entry.exists(ec)) { // In case of broken symlinks + if (entry.exists(ec)) { + // In case of broken symlinks if (ec) { printColor(std::format("Skipping '{}': {}.", entry.path().string(), ec.message()), 'r', true, std::cerr); @@ -120,20 +125,17 @@ void traverseDirectory(const std::string &directoryPath, std::vector & } // Make sure we can read the entry if (isReadable(entry.path())) [[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 + 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); - } else handleAccessError(entry.path().string()); } } @@ -143,7 +145,7 @@ void traverseDirectory(const std::string &directoryPath, std::vector & /// \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, std::size_t start, std::size_t end) { +void calculateHashes(std::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."); @@ -164,16 +166,16 @@ std::size_t findDuplicates(const std::string &directoryPath) { // Collect file information std::vector files; traverseDirectory(directoryPath, files); - std::size_t filesProcessed = files.size(); + const std::size_t filesProcessed = files.size(); if (filesProcessed < 1) return 0; // Number of threads to use - unsigned int n{std::jthread::hardware_concurrency()}; + 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; - std::size_t filesPerThread = filesProcessed / numThreads; + const std::size_t filesPerThread = filesProcessed / numThreads; std::size_t start = 0; // Calculate the files' hashes in parallel @@ -189,13 +191,10 @@ std::size_t findDuplicates(const std::string &directoryPath) { thread.join(); } // A hash map to map the files to their corresponding hashes - std::unordered_map> hashMap; + std::unordered_map > hashMap; // Iterate over files and identify duplicates - for (const auto &fileInfo: files) { - const std::string &hash = fileInfo.hash; - const std::string &filePath = fileInfo.path; - + for (const auto &[filePath, hash]: files) { hashMap[hash].push_back(filePath); } @@ -203,9 +202,7 @@ std::size_t findDuplicates(const std::string &directoryPath) { // Display duplicate files std::cout << "Duplicates found:" << std::endl; - for (const auto &pair: hashMap) { - const std::vector &duplicates = pair.second; - + for (const auto &duplicates: hashMap | std::views::values) { if (duplicates.size() > 1) { ++duplicatesSet; @@ -227,7 +224,7 @@ std::size_t findDuplicates(const std::string &directoryPath) { } /// \brief A simple duplicate file detective. -void duplicateFinder() { +export void duplicateFinder() { while (true) { std::cout << "\n-------------------"; printColor(" Duplicate Finder ", 'm'); @@ -237,18 +234,17 @@ void duplicateFinder() { std::cout << "--------------------------------------------------------" << std::endl; printColor("Enter your choice:", 'b'); - int resp = getResponseInt(); - if (resp == 1) { + if (const int resp = getResponseInt(); resp == 1) { try { printColor("Enter the path to the directory to scan:", 'b'); std::string dirPath = getResponseStr(); - if (auto len = dirPath.size(); len > 1 && (dirPath.ends_with('/') || dirPath.ends_with('\\'))) + if (const auto len = dirPath.size(); len > 1 && (dirPath.ends_with('/') || dirPath.ends_with('\\'))) dirPath.erase(len - 1); std::error_code ec; - fs::file_status fileStatus = fs::status(dirPath, 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); @@ -259,12 +255,12 @@ void duplicateFinder() { ec.clear(); continue; } - if (!fs::exists(fileStatus)) { + if (!exists(fileStatus)) { printColor(dirPath, 'c', false, std::cerr); printColor(" does not exist.", 'r', true, std::cerr); continue; } - if (!fs::is_directory(fileStatus)) { + if (!is_directory(fileStatus)) { printColor(dirPath, 'c', false, std::cerr); printColor(" is not a directory.", 'r', true, std::cerr); continue; @@ -280,23 +276,18 @@ void duplicateFinder() { printColor("Scanning ", 'c'); printColor(fs::canonical(dirPath).string(), 'g'); printColor(" ...", 'c', true); - std::size_t duplicateFiles = findDuplicates(dirPath); + const std::size_t duplicateFiles = findDuplicates(dirPath); std::cout << "Duplicates " - << (duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found.") - << std::endl; - + << (duplicateFiles > 0 ? "found: " + std::to_string(duplicateFiles) : "not found.") + << std::endl; } catch (const std::exception &ex) { printColor("An error occurred: ", 'y', false, std::cerr); printColor(ex.what(), 'r', true, std::cerr); - continue; } - } else if (resp == 2) break; else { printColor("Invalid option!", 'r', true, std::cerr); - continue; } } - } diff --git a/src/duplicateFinder/duplicateFinder.hpp b/src/duplicateFinder/duplicateFinder.hpp deleted file mode 100644 index 70a52b5..0000000 --- a/src/duplicateFinder/duplicateFinder.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void duplicateFinder(); diff --git a/src/encryption/cryptoCipher.hpp b/src/encryption/cryptoCipher.cppm similarity index 97% rename from src/encryption/cryptoCipher.hpp rename to src/encryption/cryptoCipher.cppm index 9a8b61b..3670818 100644 --- a/src/encryption/cryptoCipher.hpp +++ b/src/encryption/cryptoCipher.cppm @@ -13,13 +13,14 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. - -#pragma once +module; #include +export module cryptoCipher; + /// \brief A class wrapper for OpenSSL cipher implementations and contexts. -class CryptoCipher { +export class CryptoCipher final { public: // Default constructor constexpr CryptoCipher() noexcept = default; @@ -98,7 +99,7 @@ class CryptoCipher { } // Destructor for cleanup - virtual ~CryptoCipher() { + ~CryptoCipher() { EVP_CIPHER_free(cipher); EVP_CIPHER_CTX_free(ctx); } diff --git a/src/encryption/encryptDecrypt.cpp b/src/encryption/encryptDecrypt.cpp index 23a7a5f..b276c90 100644 --- a/src/encryption/encryptDecrypt.cpp +++ b/src/encryption/encryptDecrypt.cpp @@ -14,15 +14,26 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "../passwordManager/passwords.hpp" -#include "encryptDecrypt.hpp" +module; + #include #include #include #include #include #include +#include +#include +#include +#include + +import utils; +import secureAllocator; +import passwordManager; + +module encryption; + +namespace fs = std::filesystem; template /// \brief A concept describing a type convertible and comparable with uintmax_t. @@ -45,10 +56,11 @@ class FormatFileSize { std::uintmax_t size_{0}; friend - std::ostream &operator<<(std::ostream &os, FormatFileSize ffs) { + std::ostream &operator<<(std::ostream &os, const FormatFileSize ffs) { int i{}; auto mantissa = static_cast(ffs.size_); - for (; mantissa >= 1024.; mantissa /= 1024., ++i) {} + for (; mantissa >= 1024.; mantissa /= 1024., ++i) { + } mantissa = std::ceil(mantissa * 10.) / 10.; os << mantissa << "BKMGTPE"[i]; return i == 0 ? os : os << "B (" << ffs.size_ << ')'; @@ -56,8 +68,7 @@ class FormatFileSize { }; /// \brief Available encryption/decryption ciphers. -enum class Algorithms : const -unsigned int { +enum class Algorithms : unsigned int { AES = 1 << 0, Camellia = 1 << 1, Aria = 1 << 2, @@ -66,14 +77,13 @@ unsigned int { }; /// \brief Operation modes: encryption or decryption. -enum class OperationMode : const -int { +enum class OperationMode : unsigned int { Encryption = 1, Decryption = 2 }; /// \brief An anonymous struct to aid algorithm selection. -static const struct { +constexpr struct { const char *const AES = "AES-256-CBC"; const char *const Camellia = "CAMELLIA-256-CBC"; const char *const Aria = "ARIA-256-CBC"; @@ -84,38 +94,39 @@ static const struct { /// \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. -inline void checkInputFile(fs::path &inFile, const OperationMode &mode) { +inline void checkInputFile(const fs::path &inFile, const OperationMode &mode) { if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) throw std::invalid_argument("Invalid mode of operation."); // Ensure the input file exists and is not a directory - if (!fs::exists(inFile)) + if (!exists(inFile)) throw std::runtime_error(std::format("'{}' does not exist.", inFile.string())); - if (fs::is_directory(inFile)) + if (is_directory(inFile)) throw std::runtime_error(std::format("'{}' is a directory.", inFile.string())); // Check if the input file is a regular file and ask for confirmation if it is not - if (!fs::is_regular_file(inFile)) { - if (mode == OperationMode::Encryption) { // Encryption + 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): "; if (!validateYesNo()) throw std::runtime_error(std::format("{} is not a regular file.", inFile.string())); } else throw std::runtime_error( - std::format("{} is not a regular file.", inFile.string())); // Encrypted files are regular + 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)) throw std::runtime_error(std::format("{} is not readable.", file)); } -/// \brief Creates the directory path for a given file path if it does not exist. +/// \brief Creates the directory path for a given file path if it does not exist. /// \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 { std::error_code ec; - auto absolutePath = fs::weakly_canonical(filePath, ec); + auto absolutePath = weakly_canonical(filePath, ec); if (ec) { absolutePath = filePath; ec.clear(); @@ -127,10 +138,10 @@ inline bool createPath(const fs::path &filePath) noexcept { if (exists(absolutePath, ec)) { if (is_directory(absolutePath, ec) || is_regular_file(absolutePath, ec)) return true; - else return false; + return false; } - return fs::create_directories(absolutePath, ec); + return create_directories(absolutePath, ec); } /// \brief Checks for issues with the output file, that may hinder encryption/decryption. @@ -138,8 +149,6 @@ inline bool createPath(const fs::path &filePath) noexcept { /// \param outFile the output file, to be saved. /// \param mode the mode of operation: encryption or decryption. inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const OperationMode &mode) { - std::error_code ec; - if (mode != OperationMode::Encryption && mode != OperationMode::Decryption) throw std::invalid_argument("Invalid mode of operation."); @@ -147,7 +156,7 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope throw std::runtime_error("Unable to create destination directory."); // Check if the output file is a directory, and rename it appropriately if so - if (fs::is_directory(outFile)) { + if (is_directory(outFile)) { if (mode == OperationMode::Encryption) { outFile /= inFile.filename(); outFile += ".enc"; @@ -157,11 +166,11 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope // If the output file is not specified, name it appropriately if (outFile.string().empty()) { outFile = inFile; - if (inFile.extension() == ".enc") + if (inFile.extension() == ".enc") { outFile.replace_extension(""); - else if (mode == OperationMode::Encryption) + } else if (mode == OperationMode::Encryption) { outFile += ".enc"; - else if (mode == OperationMode::Decryption) { + } else { outFile.replace_extension(""); outFile += "_decrypted"; outFile += inFile.extension(); @@ -169,23 +178,22 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope } // If the output file exists, ask for confirmation for overwriting - if (fs::exists(outFile, ec)) { - printColor(fs::canonical(outFile).string(), 'b', false, std::cerr); + if (std::error_code ec; 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); if (!validateYesNo()) throw std::runtime_error("Operation aborted."); // Determine if the output file can be written if it exists - if (auto file = fs::weakly_canonical(outFile).string(); !(isWritable(file) && isReadable(file))) + if (auto file = weakly_canonical(outFile).string(); !(isWritable(file) && isReadable(file))) throw std::runtime_error(std::format("{} is not writable/readable.", file)); } // Check if there is enough space on the disk to save the output file. const auto availableSpace = getAvailableSpace(outFile); - const auto fileSize = fs::file_size(inFile); - if (std::cmp_less(availableSpace, fileSize)) { + if (const auto fileSize = file_size(inFile); std::cmp_less(availableSpace, fileSize)) { printColor("Not enough space to save ", 'r', false, std::cerr); - printColor(fs::weakly_canonical(outFile).string(), 'c', true, std::cerr); + printColor(weakly_canonical(outFile).string(), 'c', true, std::cerr); printColor("Required: ", 'y', false, std::cerr); printColor(FormatFileSize(fileSize), 'g', true, std::cerr); @@ -204,7 +212,7 @@ inline void checkOutputFile(const fs::path &inFile, fs::path &outFile, const Ope /// \param destFile the destination file. inline void copyLastWrite(const std::string &srcFile, const std::string &destFile) noexcept { std::error_code ec; - fs::last_write_time(destFile, fs::last_write_time(srcFile, ec), ec); + last_write_time(destFile, fs::last_write_time(srcFile, ec), ec); } /// \brief Encrypts/Decrypts a file. @@ -214,7 +222,7 @@ inline void copyLastWrite(const std::string &srcFile, const std::string &destFil /// \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, - const privacy::string &password, unsigned int algo, OperationMode mode) { + const privacy::string &password, const unsigned int &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); @@ -226,16 +234,16 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin auto encryptDecrypt = [&](const std::string &algorithm) -> void { if (mode == OperationMode::Encryption) // Encryption encryptFile(inputFileName, outputFileName, password, algorithm); - else // Decryption + else // Decryption decryptFile(inputFileName, outputFileName, password, algorithm); }; /// Encrypts/decrypts a file using a cipher with more rounds. - auto encryptDecryptMoreRounds = [&](const gcry_cipher_algos &algo) -> void { - if (mode == OperationMode::Encryption) // Encryption - encryptFileWithMoreRounds(inputFileName, outputFileName, password, algo); - else // Decryption - decryptFileWithMoreRounds(inputFileName, outputFileName, password, algo); + auto encryptDecryptMoreRounds = [&](const gcry_cipher_algos &algorithm) -> void { + if (mode == OperationMode::Encryption) // Encryption + encryptFileWithMoreRounds(inputFileName, outputFileName, password, algorithm); + else // Decryption + decryptFileWithMoreRounds(inputFileName, outputFileName, password, algorithm); }; // Encrypt/decrypt with the specified algorithm @@ -262,7 +270,6 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin // 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); } @@ -271,21 +278,21 @@ void fileEncryptionDecryption(const std::string &inputFileName, const std::strin /// \brief Encrypts and decrypts files. void encryptDecrypt() { // I'm using hashmaps as an alternative to multiple if-else statements - std::unordered_map algoChoice = { - {0, Algorithms::AES}, // Default - {1, Algorithms::AES}, - {2, Algorithms::Camellia}, - {3, Algorithms::Aria}, - {4, Algorithms::Serpent}, - {5, Algorithms::Twofish} + const std::unordered_map algoChoice = { + {0, Algorithms::AES}, // Default + {1, Algorithms::AES}, + {2, Algorithms::Camellia}, + {3, Algorithms::Aria}, + {4, Algorithms::Serpent}, + {5, Algorithms::Twofish} }; - std::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"}, - {Algorithms::Serpent, "256-bit Serpent in CTR mode"}, - {Algorithms::Twofish, "256-bit Twofish in CTR mode"} + const std::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"}, + {Algorithms::Serpent, "256-bit Serpent in CTR mode"}, + {Algorithms::Twofish, "256-bit Twofish in CTR mode"} }; while (true) { @@ -297,17 +304,16 @@ void encryptDecrypt() { printColor("3. Exit\n", 'r'); std::cout << "--------------------------------------------------------------" << std::endl; - int choice = getResponseInt("Enter your choice: "); - - if (choice == 1 || choice == 2) { + 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 // Transform the prefix to lowercase - std::ranges::transform(pre_l.begin(), pre_l.end(), pre_l.begin(), [](unsigned char c) -> unsigned char { - return std::tolower(c); - }); + std::ranges::transform(pre_l.begin(), pre_l.end(), pre_l.begin(), + [](const unsigned char c) -> unsigned char { + return std::tolower(c); + }); printColor(std::format("Enter the path to the file to {}crypt:", pre_l), 'c', true); std::string inputFile = getResponseStr(); @@ -337,7 +343,8 @@ void encryptDecrypt() { std::cout << "Leave blank to use the default (AES)" << std::endl; int algo = getResponseInt(); - if (algo < 0 || algo > 5) { // 0 is default (AES) + if (algo < 0 || algo > 5) { + // 0 is default (AES) printColor("Invalid choice!", 'r', true, std::cerr); continue; } @@ -358,7 +365,7 @@ void encryptDecrypt() { if (tries >= 3) throw std::runtime_error("Empty encryption password."); - privacy::string password2{getSensitiveInfo("Enter the password again: ")}; + const privacy::string password2{getSensitiveInfo("Enter the password again: ")}; if (!verifyPassword(password2, hashPassword(password, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))) { @@ -367,22 +374,19 @@ void encryptDecrypt() { } } printColor(std::format("{}crypting ", pre), 'g'); - printColor(fs::canonical(inputPath).string(), 'b'); + printColor(canonical(inputPath).string(), 'b'); printColor(" with ", 'g'); printColor(algoDescription.find(cipher)->second, 'c'); printColor("...", 'g', true); - fileEncryptionDecryption(fs::canonical(inputPath).string(), fs::weakly_canonical(outputPath).string(), + fileEncryptionDecryption(canonical(inputPath).string(), weakly_canonical(outputPath).string(), password, static_cast(cipher), static_cast(choice)); std::cout << std::endl; - } catch (const std::exception &ex) { printColor("Error: ", 'y', false, std::cerr); printColor(ex.what(), 'r', true, std::cerr); std::cerr << std::endl; - continue; } - } else if (choice == 3) break; else printColor("Invalid choice!", 'r', true, std::cerr); } diff --git a/src/encryption/encryptDecrypt.hpp b/src/encryption/encryptDecrypt.hpp deleted file mode 100644 index 97cf2af..0000000 --- a/src/encryption/encryptDecrypt.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "../secureAllocator.hpp" -#include -#include - -extern const int SALT_SIZE; -extern const int KEY_SIZE_256; - -// OpenSSL's library context and property query string -extern OSSL_LIB_CTX *libContext; -extern const char *propertyQuery; - -privacy::vector generateSalt(int saltSize); - -privacy::vector -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 -encryptFileWithMoreRounds(const std::string &inputFile, const std::string &outputFile, 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 -decryptFileWithMoreRounds(const std::string &inputFile, const std::string &outputFile, 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"); - -privacy::string encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::string &password, - const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); - -privacy::string -decryptString(const std::string &ciphertext, const privacy::string &password, const std::string &algo = "AES-256-CBC"); - -privacy::string decryptStringWithMoreRounds(const std::string &ciphertext, const privacy::string &password, - const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); - -void encryptDecrypt(); diff --git a/src/encryption/encryptDecryptFiles.cpp b/src/encryption/encryptFiles.cpp similarity index 93% rename from src/encryption/encryptDecryptFiles.cpp rename to src/encryption/encryptFiles.cpp index 72e288f..826f223 100644 --- a/src/encryption/encryptDecryptFiles.cpp +++ b/src/encryption/encryptFiles.cpp @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "cryptoCipher.hpp" -#include "encryptDecrypt.hpp" +module; + #include #include #include @@ -25,30 +25,31 @@ #include #include #include +#include +#include + +import cryptoCipher; +import secureAllocator; -// OpenSSL's library context and property query string -OSSL_LIB_CTX *libContext = nullptr; -const char *propertyQuery = nullptr; +module encryption; -constexpr int SALT_SIZE = 32; // Default salt length (256 bits) -constexpr int KEY_SIZE_256 = 32; // Default key size (256 bits) -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 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 /// \brief Generates random bytes using a CSPRNG. /// \param saltSize number of bytes of salt to generate. /// \return the generated salt as a vector. -privacy::vector generateSalt(int saltSize) { +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) { std::cerr << "Failed to seed OpenSSL's CSPRNG properly." - "\nPlease check your system's randomness utilities." << std::endl; + "\nPlease check your system's randomness utilities." << std::endl; - randombytes_buf(salt.data(), salt.size()); // Use Sodium's random generator as a backup + randombytes_buf(salt.data(), salt.size()); // Use Sodium's random generator as a backup } return salt; } @@ -88,17 +89,20 @@ deriveKey(const privacy::string &password, const privacy::vector /// ************* Set the required parameters ************* // Set the password to derive the key from - *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, (void *) password.data(), password.size()); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, const_cast(password.data()), + password.size()); // Set the salt - *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, (void *) salt.data(), salt.size()); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, const_cast(salt.data()), + salt.size()); // Set the number of iterations unsigned int iterations{PBKDF2_ITERATIONS}; *p++ = OSSL_PARAM_construct_uint(OSSL_KDF_PARAM_ITER, &iterations); // Use BLAKE2b512 as the hash function for the digest - *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, (char *) "BLAKE2B512", 0); + char hashFn[] = "BLAKE2B512"; + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, hashFn, 0); // Enable SP800-132 compliance checks (iterations >= 1000, salt >= 128 bits, key >= 112 bits) int pkcs5 = 0; @@ -119,6 +123,7 @@ deriveKey(const privacy::string &password, const privacy::vector /// \param inputFile The file to be encrypted. /// \param outputFile The file to store the encrypted content. /// \param password The password used to encrypt the file. +/// \param algo The cipher algorithm to use. /// /// \details Available ciphers: AES-256, Camellia-256, and Aria-256. /// \details Encryption mode: CBC. @@ -207,6 +212,7 @@ void encryptFile(const std::string &inputFile, const std::string &outputFile, co /// \param inputFile The file to be decrypted. /// \param outputFile The file to store the decrypted content. /// \param password The password used to decrypt the file. +/// \param algo The cipher algorithm used to encrypt the file. void decryptFile(const std::string &inputFile, const std::string &outputFile, const privacy::string &password, const std::string &algo) { // Open the input file for reading @@ -297,7 +303,7 @@ void decryptFile(const std::string &inputFile, const std::string &outputFile, co /// \brief Throws a thread-safe Gcrypt error. /// \param err Gcrypt error value. /// \param message the error message. -inline void throwSafeError(gcry_error_t &err, const std::string &message) { +inline void throwSafeError(const gcry_error_t &err, const std::string &message) { std::mutex m; std::scoped_lock locker(m); throw std::runtime_error(std::format("{}: {}", message, gcry_strerror(err))); @@ -307,6 +313,7 @@ inline void throwSafeError(gcry_error_t &err, const std::string &message) { /// \param inputFilePath the file to be encrypted. /// \param outputFilePath the file to save the ciphertext to. /// \param password the password used to encrypt the file. +/// \param algorithm the cipher algorithm to use. /// /// \details Available ciphers: Serpent-256 and Twofish-256. /// \details Encryption mode: Counter (CTR). @@ -326,7 +333,7 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o if (!outputFile) throw std::runtime_error(std::format("Failed to open '{}' for writing.", outputFilePath)); - gcry_error_t err; // error tracker + gcry_error_t err; // error tracker // Set up the encryption context gcry_cipher_hd_t cipherHandle; @@ -340,7 +347,7 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o // Default the key size to 256 bits if the previous call failed if (keySize == 0) keySize = KEY_SIZE_256; - if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length + if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length // Generate a random salt, and a random counter privacy::vector salt = generateSalt(SALT_SIZE); @@ -388,6 +395,7 @@ encryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &o /// \param inputFilePath The file to be decrypted. /// \param outputFilePath The file to store the decrypted content. /// \param password The password used to decrypt the file. +/// \param algorithm The cipher algorithm used to encrypt the file. void decryptFileWithMoreRounds(const std::string &inputFilePath, const std::string &outputFilePath, const privacy::string &password, const gcry_cipher_algos &algorithm) { diff --git a/src/encryption/encryptDecryptStrings.cpp b/src/encryption/encryptStrings.cpp similarity index 89% rename from src/encryption/encryptDecryptStrings.cpp rename to src/encryption/encryptStrings.cpp index 3f96f7d..a522f2c 100644 --- a/src/encryption/encryptDecryptStrings.cpp +++ b/src/encryption/encryptStrings.cpp @@ -14,16 +14,25 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "cryptoCipher.hpp" -#include "encryptDecrypt.hpp" +module; + #include #include #include +#include +#include +#include + +import utils; +import secureAllocator; +import cryptoCipher; + +module encryption; /// \brief Encrypts a string using symmetric unauthenticated encryption. /// \param plaintext The string to be encrypted. /// \param password The string to be used to derive the encryption key. +/// \param algo The cipher to be used for encryption. /// \return Base64-encoded ciphertext (the encrypted data). /// /// \details Available ciphers: AES-256, Camellia-256, and Aria-256. @@ -55,7 +64,7 @@ encryptString(const privacy::string &plaintext, const privacy::string &password, // Derive the encryption key using the generated salt privacy::vector key = deriveKey(password, salt, keySize); - int block_size = EVP_CIPHER_get_block_size(cipher.getCipher()); + const int block_size = EVP_CIPHER_get_block_size(cipher.getCipher()); privacy::vector ciphertext(plaintext.size() + block_size); int ciphertextLength = 0; @@ -96,6 +105,7 @@ encryptString(const privacy::string &plaintext, const privacy::string &password, /// \brief Decrypts a string encrypted with the encryptString() function. /// \param encodedCiphertext Base64-encoded ciphertext to be decrypted. /// \param password The string to be used to derive the decryption key. +/// \param algo The cipher used for encryption. /// \return The decrypted string (the plaintext). privacy::string decryptString(const std::string &encodedCiphertext, const privacy::string &password, const std::string &algo) { @@ -120,9 +130,8 @@ decryptString(const std::string &encodedCiphertext, const privacy::string &passw privacy::vector encryptedText; // Base64 decode the encoded ciphertext - std::vector ciphertext = base64Decode(encodedCiphertext); - - if (ciphertext.size() > (static_cast(SALT_SIZE) + ivSize)) [[likely]] { + if (std::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); iv.assign(ciphertext.begin() + SALT_SIZE, ciphertext.begin() + SALT_SIZE + ivSize); @@ -134,7 +143,7 @@ decryptString(const std::string &encodedCiphertext, const privacy::string &passw // Derive the decryption key from the password, and the salt privacy::vector key = deriveKey(password, salt, keySize); - int block_size = EVP_CIPHER_get_block_size(cipher.getCipher()); + const int block_size = EVP_CIPHER_get_block_size(cipher.getCipher()); privacy::vector plaintext(encryptedText.size() + block_size); int plaintextLength = 0; @@ -164,7 +173,7 @@ decryptString(const std::string &encodedCiphertext, const privacy::string &passw return decryptedText; } -inline void throwSafeError(gcry_error_t &err, const std::string &message) { +inline void throwSafeError(const gcry_error_t &err, const std::string &message) { std::mutex m; std::scoped_lock locker(m); throw std::runtime_error(std::format("{}: {}", message, gcry_strerror(err))); @@ -173,6 +182,7 @@ inline void throwSafeError(gcry_error_t &err, const std::string &message) { /// \brief Encrypts a string with ciphers with more rounds. /// \param plaintext The string to be encrypted. /// \param password The string to be used to derive the encryption key. +/// \param algorithm The cipher to be used for encryption. /// \return Base64-encoded ciphertext (the encrypted data). /// /// \details Available ciphers: Serpent-256 and Twofish-256. @@ -182,11 +192,9 @@ inline void throwSafeError(gcry_error_t &err, const std::string &message) { privacy::string encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::string &password, const gcry_cipher_algos &algorithm) { - gcry_error_t err; // error tracker - // Set up the encryption context gcry_cipher_hd_t cipherHandle; - err = gcry_cipher_open(&cipherHandle, algorithm, GCRY_CIPHER_MODE_CTR, GCRY_CIPHER_SECURE); + gcry_error_t err = gcry_cipher_open(&cipherHandle, algorithm, GCRY_CIPHER_MODE_CTR, GCRY_CIPHER_SECURE); if (err) throwSafeError(err, "Failed to create the encryption cipher context"); @@ -196,7 +204,7 @@ encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::str // Default the key size to 256 bits if the previous call failed if (keySize == 0) keySize = KEY_SIZE_256; - if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length + if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length // Generate a random salt, and a random counter privacy::vector salt = generateSalt(SALT_SIZE); @@ -243,6 +251,7 @@ encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::str /// \brief Decrypts a string encrypted by encryptStringWithMoreRounds() function. /// \param encodedCiphertext Base64-encoded ciphertext to be decrypted. /// \param password The string to be used to derive the decryption key. +/// \param algorithm The cipher used for encryption. /// \return The decrypted string (the plaintext). privacy::string decryptStringWithMoreRounds(const std::string &encodedCiphertext, const privacy::string &password, @@ -253,16 +262,15 @@ decryptStringWithMoreRounds(const std::string &encodedCiphertext, const privacy: // Default the key size to 256 bits if the previous call failed if (keySize == 0) keySize = KEY_SIZE_256; - if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length + if (ctrSize == 0) ctrSize = 16; // Default the counter size to 128 bits if we can't get the block length privacy::vector salt(SALT_SIZE); privacy::vector ctr(ctrSize); privacy::vector encryptedText; // Base64-decode the encoded ciphertext - std::vector ciphertext = base64Decode(encodedCiphertext); - - if (ciphertext.size() >= SALT_SIZE + ctrSize) [[likely]] { + if (std::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); ctr.assign(ciphertext.begin() + SALT_SIZE, ciphertext.begin() + SALT_SIZE + static_cast(ctrSize)); @@ -271,15 +279,14 @@ decryptStringWithMoreRounds(const std::string &encodedCiphertext, const privacy: } else throw std::runtime_error("Invalid ciphertext."); - std::size_t encryptedTextSize = encryptedText.size(); + const std::size_t encryptedTextSize = encryptedText.size(); // Derive the key privacy::vector key = deriveKey(password, salt, static_cast(keySize)); // Set up the decryption context - gcry_error_t err; gcry_cipher_hd_t cipherHandle; - err = gcry_cipher_open(&cipherHandle, algorithm, GCRY_CIPHER_MODE_CTR, GCRY_CIPHER_SECURE); + gcry_error_t err = gcry_cipher_open(&cipherHandle, algorithm, GCRY_CIPHER_MODE_CTR, GCRY_CIPHER_SECURE); if (err) throwSafeError(err, "Failed to create the decryption cipher context"); diff --git a/src/encryption/encryption.cppm b/src/encryption/encryption.cppm new file mode 100644 index 0000000..495c924 --- /dev/null +++ b/src/encryption/encryption.cppm @@ -0,0 +1,71 @@ +// 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 + +export module encryption; +import secureAllocator; + +constexpr int SALT_SIZE = 32; // Default salt length (256 bits) +constexpr int KEY_SIZE_256 = 32; // Default key size (256 bits) + +// OpenSSL's library context and property query string +OSSL_LIB_CTX *libContext = nullptr; +const char *propertyQuery = nullptr; + +export { + privacy::vector generateSalt(int saltSize); + + privacy::vector + 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 + encryptFileWithMoreRounds(const std::string &inputFilePath, const std::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 + decryptFileWithMoreRounds(const std::string &inputFilePath, const std::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"); + + privacy::string encryptStringWithMoreRounds(const privacy::string &plaintext, const privacy::string &password, + const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); + + privacy::string + decryptString(const std::string &encodedCiphertext, const privacy::string &password, + const std::string &algo = "AES-256-CBC"); + + privacy::string decryptStringWithMoreRounds(const std::string &encodedCiphertext, const privacy::string &password, + const gcry_cipher_algos &algorithm = GCRY_CIPHER_SERPENT256); + + void encryptDecrypt(); +} diff --git a/src/fileShredder/shredFiles.cpp b/src/fileShredder/fileShredder.cppm similarity index 78% rename from src/fileShredder/shredFiles.cpp rename to src/fileShredder/fileShredder.cppm index 40893d0..8cbcfad 100644 --- a/src/fileShredder/shredFiles.cpp +++ b/src/fileShredder/fileShredder.cppm @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "shredFiles.hpp" +module; + #include #include #include @@ -26,14 +26,20 @@ #include #include +using StatType = struct stat; + +export module fileShredder; +import utils; + namespace fs = std::filesystem; constexpr std::streamoff BUFFER_SIZE = 4096; + /// \brief overwrites a file with random bytes. /// \param file output file stream object. /// \param fileSize the size of the file in bytes. /// \param nPasses the number of passes to overwrite the file. -void overwriteRandom(std::ofstream &file, const std::size_t fileSize, int nPasses = 1) { +void overwriteRandom(std::ofstream &file, const std::size_t fileSize, const int nPasses = 1) { if (!file.is_open()) throw std::runtime_error("File not open."); // Instantiate the random number generator std::random_device rd; @@ -70,7 +76,7 @@ void overwriteRandom(std::ofstream &file, const std::size_t fileSize, int nPasse /// \brief overwrites a file wih a constant byte. /// \tparam T type of the byte. -/// \param filename the path to the file to be overwritten. +/// \param file output file stream object to overwrite. /// \param byte the byte to overwrite the file with. /// \param fileSize the size of the file in bytes. template @@ -95,13 +101,13 @@ void overwriteConstantByte(std::ofstream &file, T &byte, const auto &fileSize) { /// \param filename the path to the file to be renamed. /// \param numTimes the number of times to rename the file. inline void renameAndRemove(const std::string &filename, int numTimes = 1) { - constexpr int maxTries = 10; // max number of trials to rename the file - constexpr int minNameLength = 3; // min length of the random name - constexpr int maxNameLength = 16; // max length of the random name + constexpr int maxTries = 10; // max number of trials to rename the file + constexpr int minNameLength = 3; // min length of the random name + constexpr int maxNameLength = 16; // max length of the random name // Check if the number of times is valid if (numTimes < 1) return; - else if (numTimes > maxTries) numTimes = maxTries; + if (numTimes > maxTries) numTimes = maxTries; // Create an instance of the random device for generating secure random numbers std::random_device rd; @@ -113,7 +119,7 @@ inline void renameAndRemove(const std::string &filename, int numTimes = 1) { std::uniform_int_distribution numDist(minNameLength, maxNameLength); // Get the file extension using std::filesystem - std::string fileExtension = fs::path(filename).extension().string(); + const std::string fileExtension = fs::path(filename).extension().string(); // Generate a random name using the safe characters (Not exhaustive) const std::string safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; @@ -124,11 +130,11 @@ inline void renameAndRemove(const std::string &filename, int numTimes = 1) { // (Try to) rename the file numTimes times for (int i = 0; i < numTimes; ++i) { - if (i >= maxTries) break; // Give up after 10 tries - fs::path tmpPath = path; // Track renaming + if (i >= maxTries) break; // Give up after 10 tries + fs::path tmpPath = path; // Track renaming // Generate a random number of characters for the new name - int numChars = numDist(gen); + const int numChars = numDist(gen); std::string newName; // Generate a random name @@ -138,14 +144,13 @@ inline void renameAndRemove(const std::string &filename, int numTimes = 1) { path.replace_filename(newName); // Rename the file if it doesn't exist to avoid overwriting existing files - if (!fs::exists(path)) { + if (!exists(path)) { fs::rename(tmpPath, path, ec); // Try again if there was an error if (ec) { ++numTimes; ec.clear(); } - } else ++numTimes; // Try again, the file already exists } @@ -181,9 +186,9 @@ struct FileDescriptor { /// for a given file descriptor. It provides a simple way to access file attributes such as /// file size, permissions, and timestamps. struct FileStatInfo { - struct stat fileStat{}; + StatType fileStat{}; - explicit FileStatInfo(int &fileDescriptor) { + explicit FileStatInfo(const int &fileDescriptor) { if (fstat(fileDescriptor, &fileStat) == -1) throw std::runtime_error(std::format("Failed to get file size: ({})", std::strerror(errno))); } @@ -192,8 +197,8 @@ struct FileStatInfo { /// \brief wipes the cluster tips of a file. /// \param fileName the path to the file to be wiped. inline void wipeClusterTips(const std::string &fileName) { - FileDescriptor fileDescriptor(fileName); - FileStatInfo fileInformation(fileDescriptor.fd); + const FileDescriptor fileDescriptor(fileName); + const FileStatInfo fileInformation(fileDescriptor.fd); // Calculate the size of the cluster tip auto clusterTipSize = fileInformation.fileStat.st_blksize - @@ -209,29 +214,30 @@ inline void wipeClusterTips(const std::string &fileName) { } // Write zeros to the cluster tip - std::vector zeroBuffer(clusterTipSize, 0); - auto bytesWritten = write(fileDescriptor.fd, zeroBuffer.data(), zeroBuffer.size()); + const std::vector zeroBuffer(clusterTipSize, 0); - if (bytesWritten == static_cast(-1)) { + if (write(fileDescriptor.fd, zeroBuffer.data(), zeroBuffer.size()) == static_cast(-1)) { throw std::runtime_error(std::format("Failed to write zeros: ({})", std::strerror(errno))); } } /// \brief shreds a file by overwriting it with random bytes. /// \param filename path to the file being overwritten. -void simpleShred(const std::string &filename, const int &nPasses = 3, bool wipeClusterTip = false) { +/// \param nPasses the number of passes to overwrite the file. +/// \param wipeClusterTip whether to wipe the cluster tips of the file. +void simpleShred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { std::ofstream file(filename, std::ios::binary | std::ios::in); if (!file) throw std::runtime_error("\nFailed to open file: " + filename); std::error_code ec; // Read last write time - auto initTime = fs::last_write_time(filename, ec); + const auto initTime = fs::last_write_time(filename, ec); if (ec) ec.clear(); // Get the file size file.seekp(0, std::ios::end); - std::streamoff fileSize = file.tellp(); + const std::streamoff fileSize = file.tellp(); file.seekp(0, std::ios::beg); // Shred the file @@ -241,7 +247,7 @@ void simpleShred(const std::string &filename, const int &nPasses = 3, bool wipeC if (wipeClusterTip) wipeClusterTips(filename); // Restore last write time - fs::last_write_time(filename, initTime, ec); + last_write_time(filename, initTime, ec); // Rename and remove the file renameAndRemove(filename, 3); @@ -250,14 +256,16 @@ void simpleShred(const std::string &filename, const int &nPasses = 3, bool wipeC /// \brief shreds a file using a simple version of /// The U.S Department of Defence (DoD) 5220.22-M Standard algorithm. /// \param filename - the path to the file to be shred. -void dod5220Shred(const std::string &filename, const int &nPasses = 3, bool wipeClusterTip = false) { +/// \param nPasses the number of passes to overwrite the file. +/// \param wipeClusterTip whether to wipe the cluster tips of the file. +void dod5220Shred(const std::string &filename, const int &nPasses = 3, const bool wipeClusterTip = false) { std::ofstream file(filename, std::ios::binary | std::ios::in); if (!file) throw std::runtime_error("\nFailed to open file: " + filename); std::error_code ec; // Read last write time - auto initTime = fs::last_write_time(filename, ec); + const auto initTime = fs::last_write_time(filename, ec); if (ec) ec.clear(); // Get the file size @@ -293,7 +301,7 @@ void dod5220Shred(const std::string &filename, const int &nPasses = 3, bool wipe if (wipeClusterTip) wipeClusterTips(filename); // Restore last write time - fs::last_write_time(filename, initTime, ec); + last_write_time(filename, initTime, ec); if (ec) ec.clear(); // Rename and remove the file @@ -301,12 +309,11 @@ void dod5220Shred(const std::string &filename, const int &nPasses = 3, bool wipe } /// \brief Represents the different shredding options. -enum class shredOptions : const -unsigned int { - 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 +enum class shredOptions : unsigned int { + 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. @@ -323,9 +330,9 @@ unsigned int { /// and this program doesn't take that for granted. inline bool addReadWritePermissions(const std::string &fileName) noexcept { std::error_code ec; - fs::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, - fs::perm_options::add, 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, + fs::perm_options::add, ec); return !ec; } @@ -340,7 +347,7 @@ inline bool addReadWritePermissions(const std::string &fileName) noexcept { /// are shredded without warning. bool shredFiles(const std::string &filePath, const unsigned int &options, const int &simplePasses = 3) { std::error_code ec; - fs::file_status fileStatus = fs::status(filePath, 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); @@ -350,12 +357,13 @@ bool shredFiles(const std::string &filePath, const unsigned int &options, const return false; } // Check if the file exists and is a regular file. - if (!fs::exists(fileStatus)) { + if (!exists(fileStatus)) { printColor(filePath, 'c', false, std::cerr); printColor(" does not exist.", 'r', true, std::cerr); return false; - }// If the filepath is a directory, shred all the files in the directory and all its subdirectories - else if (fs::is_directory(fileStatus)) { + } + // If the filepath is a directory, shred all the files in the directory and all its subdirectories + if (is_directory(fileStatus)) { if (fs::is_empty(filePath, ec)) { if (ec) ec.clear(); else { @@ -374,17 +382,16 @@ bool shredFiles(const std::string &filePath, const unsigned int &options, const ec.clear(); continue; } - if (!fs::is_directory(entry.status())) { + if (!is_directory(entry.status())) { printColor("Shredding ", 'c'); - printColor(fs::canonical(entry.path()).string(), 'b'); + printColor(canonical(entry.path()).string(), 'b'); printColor(" ...", 'c'); try { - bool shredded = shredFiles(entry.path(), options); + const bool shredded = shredFiles(entry.path(), options); printColor(shredded ? "\tshredded successfully." : "\tshredding failed.", shredded ? 'g' : 'r', true); ++(shredded ? numShredded : numNotShredded); - } catch (const std::runtime_error &err) { printColor("Shredding failed: ", 'y', false, std::cerr); printColor(err.what(), 'r', true, std::cerr); @@ -393,7 +400,7 @@ bool shredFiles(const std::string &filePath, const unsigned int &options, const } } if (numNotShredded == 0) // All files in the directory and all subdirectories were shredded successfully. - fs::remove_all(fs::canonical(filePath)); + remove_all(fs::canonical(filePath)); else printColor("Failed to shred some files.", 'r', true, std::cerr); std::cout << "\nProcessed " << numShredded + numNotShredded << " files." << std::endl; @@ -408,7 +415,8 @@ bool shredFiles(const std::string &filePath, const unsigned int &options, const } return true; - } else if (!fs::is_regular_file(fileStatus)) { + } + 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); @@ -437,58 +445,55 @@ bool shredFiles(const std::string &filePath, const unsigned int &options, const } /// \brief A simple file shredder. -void fileShredder() { - /// \brief Configures the shredding options. +export void fileShredder() { + // Configures the shredding options. auto selectPreferences = [](unsigned int &preferences, int &simpleNumPass) { - int moreChoices1 = getResponseInt("\n1. Continue with default shredding options\n" - "2. Configure shredding options"); + const int moreChoices1 = getResponseInt("\n1. Continue with default shredding options\n" + "2. Configure shredding options"); unsigned const int &wipeTips = static_cast(shredOptions::WipeClusterTips); if (moreChoices1 == 1) { + // Default options: simple shredding with random bytes, and wipe cluster tips. preferences |= static_cast(shredOptions::Simple) | wipeTips; } else if (moreChoices1 == 2) { - 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"); + // Configure shredding options + 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) { preferences |= static_cast(shredOptions::Simple) | wipeTips; - int simpleConfig{0}; do { - simpleConfig = getResponseInt("\n1. Continue\n" - "2. Change the number of passes (default is 3)\n" - "3. Configure wiping of cluster tips (enabled by default)\n" - "4. Abort"); - if (simpleConfig == 1) { - break; - } else if (simpleConfig == 2) { + const int simpleConfig = getResponseInt("\n1. Continue\n" + "2. Change the number of passes (default is 3)\n" + "3. Configure wiping of cluster tips (enabled by default)\n" + "4. Abort"); + + if (simpleConfig == 1) break; // Continue + if (simpleConfig == 2) { + // Change the number of passes simpleNumPass = getResponseInt( - "How many times would you like to overwrite? (3 times is recommended.)"); - - if (simpleNumPass > 10) - throw std::length_error("Too many passes."); - else if (simpleNumPass < 1) throw std::length_error("Number of passes should be at least 1."); + "How many times would you like to overwrite? (3 times is recommended.)"); + if (simpleNumPass > 10) throw std::length_error("Too many passes."); + if (simpleNumPass < 1) throw std::length_error("Number of passes should be at least 1."); } else if (simpleConfig == 3) { + // Configure wiping of cluster tips preferences = (preferences & ~wipeTips) | (-validateYesNo("Wipe cluster tips? (Recommended) (y/n):") & wipeTips); } else if (simpleConfig == 4) { + // Abort throw std::runtime_error("Operation aborted."); - } else { - printColor("Invalid option", 'r', true, std::cerr); - continue; - } + } else printColor("Invalid option", 'r', true, std::cerr); } while (true); - } else if (alg == 2 || alg == 3) { + // DoD 5220.22-M Standard algorithms preferences |= static_cast(alg == 2 ? shredOptions::Dod5220 : shredOptions::Dod5220_7); preferences = (preferences & ~wipeTips) | (-validateYesNo("Wipe cluster tips? (Recommended) (y/n):") & wipeTips); - } else throw std::invalid_argument("Invalid option"); - } else throw std::invalid_argument("Invalid option"); }; @@ -503,31 +508,34 @@ void fileShredder() { printColor("---------------------------------------------------", 'g', true); - int choice = getResponseInt("Enter your choice: "); - - if (choice == 1 || choice == 2) { + 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")); - if (auto len = path.size(); len > 1 && (path.ends_with('/') || path.ends_with('\\'))) + // Remove trailing slashes + if (const auto len = path.size(); len > 1 && (path.ends_with('/') || path.ends_with('\\'))) path.erase(len - 1); std::error_code ec; - fs::file_status fileStatus = fs::status(path, ec); + const fs::file_status fileStatus = fs::status(path, ec); if (ec) { printColor(ec.message(), 'r', true, std::cerr); ec.clear(); continue; } - bool isDir{fs::is_directory(fileStatus)}; - auto canonicalPath = fs::exists(fileStatus) ? fs::canonical(path).string() : path; + const bool isDir{is_directory(fileStatus)}; + auto canonicalPath = fs::weakly_canonical(path).string(); - if (!fs::exists(fileStatus)) { + // Check if the file or directory exists + if (!exists(fileStatus)) { printColor(canonicalPath, 'c', false, std::cerr); printColor(" does not exist.", 'r', true, std::cerr); continue; - } else if (choice == 1 && isDir) { + } + // 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); @@ -536,12 +544,14 @@ void fileShredder() { printColor("'\nand all its subdirectories? (y/n):", 'y', true); 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); if (!validateYesNo("Shred it anyway? (y/n):")) continue; } unsigned int preferences{0}; int simpleNumPass{3}; + // Select shredding preferences try { selectPreferences(preferences, simpleNumPass); } catch (const std::exception &ex) { @@ -555,7 +565,7 @@ void fileShredder() { std::cout << "Shredding '"; printColor(canonicalPath, 'c'); std::cout << "'..." << std::endl; - bool shredded = shredFiles(path, preferences, simpleNumPass); + const bool shredded = shredFiles(path, preferences, simpleNumPass); if (!isDir) { printColor(shredded ? "Successfully shredded " : "Failed to shred ", shredded ? 'g' : 'r', false, shredded ? std::cout : std::cerr); @@ -564,12 +574,8 @@ void fileShredder() { } } catch (const std::exception &err) { printColor(std::format("Error: {}", err.what()), 'r', true, std::cerr); - continue; } - } else if (choice == 3) break; - else { - printColor("Invalid choice.", 'r', true, std::cerr); - } + else printColor("Invalid choice.", 'r', true, std::cerr); } } diff --git a/src/fileShredder/shredFiles.hpp b/src/fileShredder/shredFiles.hpp deleted file mode 100644 index d29f1b6..0000000 --- a/src/fileShredder/shredFiles.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void fileShredder(); diff --git a/src/main.cpp b/src/main.cpp index 67b39a2..a8259ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,11 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "duplicateFinder/duplicateFinder.hpp" -#include "encryption/encryptDecrypt.hpp" -#include "fileShredder/shredFiles.hpp" -#include "passwordManager/passwords.hpp" -#include "privacyTracks/privacyTracks.hpp" #include #include #include @@ -27,11 +22,19 @@ #include #include #include +#include + +import duplicateFinder; +import privacyTracks; +import encryption; +import passwordManager; +import fileShredder; +import utils; constexpr const char *const MINIMUM_LIBGCRYPT_VERSION = "1.10.0"; -int main(int argc, char **argv) { +int main(const int argc, const char **argv) { // The program should be launched in interactive mode if (!isatty(STDIN_FILENO)) { if (errno == ENOTTY) { @@ -40,8 +43,7 @@ int main(int argc, char **argv) { } } // Disable core dumping for security reasons - rlimit coreLimit{0, 0}; - if (setrlimit(RLIMIT_CORE, &coreLimit) != 0) { + if (constexpr rlimit coreLimit{0, 0}; setrlimit(RLIMIT_CORE, &coreLimit) != 0) { printColor("Failed to disable core dumps.", 'r', true, std::cerr); return 1; } @@ -95,7 +97,7 @@ int main(int argc, char **argv) { throw std::runtime_error("Failed to initialize libsodium."); // Display information about the program - printColor("\nPrivacy Shield 1.0.0\n", 'c'); + printColor("\nPrivacy Shield 2.0.0\n", 'c'); printColor("Copyright (C) 2024 Ian Duncan.\n", 'b'); printColor("This program comes with ", 'g'); @@ -110,12 +112,12 @@ int main(int argc, char **argv) { printColor("https://www.gnu.org/licenses/gpl.html.\n", 'b', true); // All the available tools - std::unordered_map> apps = { - {1, passwordManager}, - {2, encryptDecrypt}, - {3, fileShredder}, - {4, clearPrivacyTracks}, - {5, duplicateFinder} + std::unordered_map > apps = { + {1, passwordManager}, + {2, encryptDecrypt}, + {3, fileShredder}, + {4, clearPrivacyTracks}, + {5, duplicateFinder} }; // Applications loop @@ -129,40 +131,31 @@ int main(int argc, char **argv) { printColor("6. Exit\n", 'r'); printColor("-------------------------------------", 'c', true); - int choice = getResponseInt("What would you like to do? (Enter 1 or 2, 3..)"); + const int choice = getResponseInt("What would you like to do? (Enter 1 or 2, 3..)"); try { - auto iter = apps.find(choice); - - if (iter != apps.end()) + if (const auto iter = apps.find(choice); iter != apps.end()) iter->second(); else if (choice == 6) break; else printColor("Invalid choice!", 'r', true, std::cerr); - - } catch (const std::bad_function_call &bc) { // In case the std::function objects are called inappropriately + } 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); - continue; - } catch (const std::exception &ex) { printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); - continue; - - } catch (...) { // All other exceptions, if any + } catch (...) { + // All other exceptions, if any printColor("An error occurred.", 'r', true, std::cerr); - continue; } } return 0; - } catch (const std::exception &ex) { printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); return 1; - } catch (...) { printColor("Something went wrong.", 'r', true, std::cerr); return 1; } - } diff --git a/src/passwordManager/FuzzyMatcher.hpp b/src/passwordManager/FuzzyMatcher.cppm similarity index 90% rename from src/passwordManager/FuzzyMatcher.hpp rename to src/passwordManager/FuzzyMatcher.cppm index f09ac34..3af8a48 100644 --- a/src/passwordManager/FuzzyMatcher.hpp +++ b/src/passwordManager/FuzzyMatcher.cppm @@ -14,12 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#pragma once +module; -#include "../secureAllocator.hpp" #include #include +export module FuzzyMatcher; + +import secureAllocator; + template /// \brief A concept describing a range of strings. /// @tparam T string type. @@ -28,7 +31,7 @@ concept StringRange = std::ranges::input_range && /// \brief A simple case insensitive fuzzy matcher. -class FuzzyMatcher { +export class FuzzyMatcher final { public: /// Default constructor constexpr FuzzyMatcher() noexcept = default; @@ -85,16 +88,16 @@ class FuzzyMatcher { /// \param pattern the pattern to match. /// \param maxDistance the maximum Levenshtein Distance to consider a match. /// \return a vector of strings matching the pattern. - std::vector fuzzyMatch(const privacy::string &pattern, const int &maxDistance) { + [[nodiscard]] std::vector fuzzyMatch(const privacy::string &pattern, const int &maxDistance) const { std::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 - auto maxSize{pattern.size() + maxDistance + 1}; - auto minSize{pattern.size() - (maxDistance + 1)}; + const auto maxSize{pattern.size() + maxDistance + 1}; + const auto minSize{pattern.size() - (maxDistance + 1)}; // Iterate over the string list and find matches for (const auto &str: stringList) - if (auto size{str.size()}; size <= maxSize && size >= minSize && + if (const auto size{str.size()}; size <= maxSize && size >= minSize && levenshteinDistance(pattern, str) <= maxDistance) matches.emplace_back(str); @@ -103,7 +106,7 @@ class FuzzyMatcher { } /// Default destructor - virtual ~FuzzyMatcher() noexcept = default; + ~FuzzyMatcher() noexcept = default; private: @@ -116,8 +119,8 @@ class FuzzyMatcher { /// \note The Levenshtein distance calculated by this function is case insensitive, /// i.e the strings are converted to lowercase when calculating the edit distance. constexpr static int levenshteinDistance(const privacy::string &str1, const privacy::string &str2) { - int m = static_cast(str1.length()); - int n = static_cast(str2.length()); + const int m = static_cast(str1.length()); + const int n = static_cast(str2.length()); std::vector> dp(m + 1, std::vector(n + 1)); diff --git a/src/passwordManager/passwordManager.cpp b/src/passwordManager/passwordManager.cpp index 6b3955b..7e32786 100644 --- a/src/passwordManager/passwordManager.cpp +++ b/src/passwordManager/passwordManager.cpp @@ -14,9 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "FuzzyMatcher.hpp" -#include "passwords.hpp" +module; + #include #include #include @@ -27,6 +26,13 @@ #include #include #include +#include + +import utils; +import FuzzyMatcher; +import secureAllocator; + +module passwordManager; namespace fs = std::filesystem; using string = std::string; @@ -37,7 +43,7 @@ const string DefaultPasswordFile = getHomeDir() + "/.privacyShield/passwords"; /// \param lhs a password record tuple. /// \param rhs another record to be compared with lhs. /// \return true if lhs is less than (i.e. is ordered before) rhs, else false. -inline bool comparator +bool constexpr comparator // Avoid a gcc 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__ is checked first since Clang might also define __GNUC__, but GCC never defines __clang__. @@ -45,17 +51,19 @@ inline bool comparator #elif __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)) < nullptr; + return std::tie(std::get<0>(lhs), std::get<1>(lhs)) < + std::tie(std::get<0>(rhs), std::get<1>(rhs)); } /// \brief Prints the details of a password record. /// \param pw a password tuple. -inline constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false) noexcept { +/// \param isStrong a boolean value indicating if the password is strong or not. +constexpr void printPasswordDetails(const auto &pw, const bool &isStrong = false) noexcept { const auto &[site, username, pass]{pw}; - if (!site.empty()) { // Skip blank entries + if (!site.empty()) { + // Skip blank entries std::cout << "Site/app: "; printColor(site, 'c'); } @@ -67,7 +75,6 @@ inline constexpr void printPasswordDetails(const auto &pw, const bool &isStrong // Highlight a weak password std::cout << "\nPassword: "; printColor(pass, isStrong ? 'g' : 'r', true); - } /// \brief This function computes the strength of each password in the provided list of passwords. @@ -81,13 +88,13 @@ inline constexpr void printPasswordDetails(const auto &pw, const bool &isStrong /// in the passwords vector. It is resized to match the size of the passwords vector. /// /// \note This function is always inlined by the compiler. -inline constexpr void computeStrengths +constexpr void computeStrengths #if __clang__ [[clang::always_inline]] #elif __GNUC__ [[gnu::always_inline]] #endif - (const privacy::vector &passwords, std::vector &pwStrengths) { +(const privacy::vector &passwords, std::vector &pwStrengths) { pwStrengths.resize(passwords.size()); for (std::size_t i = 0; i < passwords.size(); ++i) { pwStrengths[i] = isPasswordStrong(std::get<2>(passwords[i])); @@ -105,10 +112,10 @@ inline void addPassword(privacy::vector &passwords, std::vector privacy::string username{getResponseStr("Username (leave blank if N/A): ")}; // Check if the record already exists in the database - auto it = std::ranges::lower_bound(passwords, std::tie(site, username, std::ignore), - [](const auto &tuple1, const auto &tuple2) { - return comparator(tuple1, tuple2); - }); + const auto it = std::ranges::lower_bound(passwords, std::tie(site, username, std::ignore), + [](const auto &tuple1, const auto &tuple2) { + return comparator(tuple1, tuple2); + }); // If the record already exists, ask the user if they want to update it bool update{false}; @@ -137,8 +144,8 @@ inline void addPassword(privacy::vector &passwords, std::vector // Always warn on weak passwords if (!isPasswordStrong(password)) { printColor( - "Weak password! A password should have at least 8 characters and include \nat least an " - "uppercase character, a lowercase, a punctuator, and a digit.", 'y', true); + "Weak password! A password should have at least 8 characters and include \nat least an " + "uppercase character, a lowercase, a punctuator, and a digit.", 'y', true); printColor("Please consider using a stronger one.", 'r', true); } @@ -178,14 +185,9 @@ inline void generatePassword(privacy::vector &, std::vector &passwords, std::vector &strengths) { - // We mustn't modify the password records in this function - auto &&constPasswordsRef = std::as_const(passwords); - // Check if there are any passwords saved - if (constPasswordsRef.empty()) { + if (auto &&constPasswordsRef = std::as_const(passwords); constPasswordsRef.empty()) { printColor("You haven't saved any password yet.", 'r', true); - return; - } else { std::cout << "All passwords: ("; printColor("red is weak", 'r'); @@ -204,13 +206,12 @@ inline void viewAllPasswords(privacy::vector &passwords, std::v } /// \brief Handles fuzzy matching for update and deletion of passwords. -inline 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 - FuzzyMatcher matcher(records | std::ranges::views::elements<0>); - auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; + const FuzzyMatcher matcher(records | std::ranges::views::elements<0>); // If there is a single match, ask the user if they want to update the query - if (fuzzyMatched.size() == 1) { + if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { const auto &match = fuzzyMatched.at(0); printColor("Did you mean '", 'c'); @@ -223,10 +224,11 @@ inline void checkFuzzyMatches(auto &iter, privacy::vector &reco [](const auto &lhs, const auto &rhs) noexcept -> bool { return comparator(lhs, rhs); }); - query = std::string{match}; // string constructed because 'match' is a reference and 'query' outlives it. + query = std::string{match}; + // string constructed because 'match' is a reference and 'query' outlives it. } - - } else if (!fuzzyMatched.empty()) { // multiple matches + } else if (!fuzzyMatched.empty()) { + // multiple matches printColor("Did you mean one of these?:", 'b', true); // Print all the matches for (const auto &el: fuzzyMatched) { @@ -238,7 +240,8 @@ inline void checkFuzzyMatches(auto &iter, privacy::vector &reco /// \brief Updates a password record. inline void updatePassword(privacy::vector &passwords, std::vector &strengths) { - if (passwords.empty()) [[unlikely]] { // There is nothing to update + if (passwords.empty()) [[unlikely]] { + // There is nothing to update printColor("No passwords saved yet.", 'r', true, std::cerr); return; } @@ -265,12 +268,15 @@ inline void updatePassword(privacy::vector &passwords, std::vec return std::get<0>(lhs) < std::get<0>(rhs); }); - if (!matches.empty()) { // site found - if (matches.size() > 1) { // there are multiple accounts under the site + if (!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); + printColor(username.empty() + ? "'' [no username, reply with a blank to select]" + : username, 'c', true); privacy::string username{getResponseStr("\nEnter one of the above usernames to update:")}; @@ -292,7 +298,7 @@ inline void updatePassword(privacy::vector &passwords, std::vec // Update the required fields privacy::string newUsername; - bool updateUsername{validateYesNo("Do you want to change the username? (y/n):")}; + const bool updateUsername{validateYesNo("Do you want to change the username? (y/n):")}; if (updateUsername) { newUsername = getResponseStr("Enter the new username (Leave blank to delete the current one):"); @@ -307,13 +313,15 @@ inline void updatePassword(privacy::vector &passwords, std::vec } } - privacy::string newPassword{getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ")}; + const privacy::string newPassword{ + getSensitiveInfo("Enter the new password (Leave blank to keep the current one): ") + }; // Warn if the password is weak if (!newPassword.empty() && !isPasswordStrong(newPassword)) { printColor( - "Weak password! A password should have at least 8 characters and include \nat least an" - " uppercase character, a lowercase, a punctuator, and a digit.", 'y', true); + "Weak password! A password should have at least 8 characters and include \nat least an" + " uppercase character, a lowercase, a punctuator, and a digit.", 'y', true); printColor("Please consider using a stronger one.", 'r', true); } @@ -332,7 +340,6 @@ inline void updatePassword(privacy::vector &passwords, std::vec // Recompute strengths computeStrengths(passwords, strengths); } else printColor("Password not updated.", 'r', true, std::cerr); - } else { printColor("'", 'r', false, std::cerr); printColor(site, 'c', false, std::cerr); @@ -368,12 +375,14 @@ inline void deletePassword(privacy::vector &passwords, std::vec return std::get<0>(lhs) < std::get<0>(rhs); }); - if (!matches.empty()) { // site found + if (!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); + printColor(username.empty() + ? "'' [no username, reply with a blank to select]" + : username, 'c', true); privacy::string username{getResponseStr("\nEnter one of the above usernames to delete:")}; @@ -407,7 +416,6 @@ inline void deletePassword(privacy::vector &passwords, std::vec // Recompute strengths computeStrengths(passwords, strengths); - } else { printColor("'", 'r', false, std::cerr); printColor(site, 'c', false, std::cerr); @@ -417,7 +425,8 @@ inline void deletePassword(privacy::vector &passwords, std::vec /// \brief Finds a password record. inline void searchPasswords(privacy::vector &passwords, std::vector &) { - if (passwords.empty()) [[unlikely]] { // There is nothing to search + if (passwords.empty()) [[unlikely]] { + // There is nothing to search printColor("No passwords saved yet.", 'r', true, std::cerr); return; } @@ -450,11 +459,10 @@ inline void searchPasswords(privacy::vector &passwords, std::ve printColor(std::format("No matches found for '{}'", query), 'r', true); // Fuzzy-match the query against the site names - FuzzyMatcher matcher(constPasswordsRef | std::ranges::views::elements<0>); - auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; + const FuzzyMatcher matcher(constPasswordsRef | std::ranges::views::elements<0>); // If there is a single match, ask the user if they want to view it - if (fuzzyMatched.size() == 1) { + if (const auto fuzzyMatched{matcher.fuzzyMatch(query, 2)}; fuzzyMatched.size() == 1) { const auto &match = fuzzyMatched.at(0); printColor("Did you mean '", 'c'); @@ -474,10 +482,9 @@ inline void searchPasswords(privacy::vector &passwords, std::ve printColor("-----------------------------------------------------", 'w', true); } } - } else printColor("Sorry, '" + query + "' not found.", 'r', true); - - } else if (!fuzzyMatched.empty()) { // multiple matches + } else if (!fuzzyMatched.empty()) { + // multiple matches printColor("Did you mean one of these?:", 'b', true); // Print all the matches for (const auto &el: fuzzyMatched) { @@ -486,12 +493,11 @@ inline void searchPasswords(privacy::vector &passwords, std::ve } } } - } /// \brief Imports passwords from a csv file. inline void importPasswords(privacy::vector &passwords, std::vector &strengths) { - string fileName = getResponseStr("Enter the path to the csv file: "); + const string fileName = getResponseStr("Enter the path to the csv file: "); privacy::vector imports{importCsv(fileName)}; @@ -538,7 +544,7 @@ inline void importPasswords(privacy::vector &passwords, std::ve overwrite = validateYesNo(); } - std::size_t initSize{passwords.size()}; + const std::size_t initSize{passwords.size()}; if (overwrite) { // According to an unofficial language reference, https://en.cppreference.com/w/cpp/algorithm/ranges/set_union, @@ -548,7 +554,7 @@ inline void importPasswords(privacy::vector &passwords, std::ve // also preserving order. // So, if a record exists in both 'imports' and 'passwords' (it is guaranteed here that such a record // can be found only once in each range, as both have been deduplicated), - // then with 'imports' as the first argument, only 'imports'' version will be copied to the result. + // then with 'imports' as the first argument, only 'imports' version will be copied to the result. std::ranges::set_union(imports, passwords, std::back_inserter(recordsUnion), [](const auto &pw1, const auto &pw2) { return comparator(pw1, pw2); @@ -556,22 +562,20 @@ inline void importPasswords(privacy::vector &passwords, std::ve } else { printColor("Warning: Duplicate passwords will not be imported.", 'y', true); - // 'passwords' now come before 'imports,' in accordance with the discussion in the previous branch. + // 'passwords' now come before 'imports', in accordance with the discussion in the previous branch. std::ranges::set_union(passwords, imports, std::back_inserter(recordsUnion), [](const auto &pw1, const auto &pw2) { return comparator(pw1, pw2); }); } - // Reassign the records - passwords.assign(recordsUnion.begin(), recordsUnion.end()); + // Update the passwords + passwords = std::move(recordsUnion); // Recompute strengths computeStrengths(passwords, strengths); - auto imported = overwrite ? imports.size() : passwords.size() - initSize; - - if (std::cmp_greater(imported, 0)) + 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); } @@ -584,13 +588,11 @@ inline void exportPasswords(privacy::vector &passwords, std::ve printColor("No passwords saved yet.", 'r', true, std::cerr); return; } - string fileName = getResponseStr("Enter the path to save the file (leave blank for default): "); + const string fileName = getResponseStr("Enter the path to save the file (leave blank for default): "); // Export the passwords to a csv file - bool exported = fileName.empty() ? exportCsv(passwordsConstRef) : exportCsv(passwordsConstRef, - fileName); - - if (exported) + 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." @@ -605,7 +607,7 @@ inline void analyzePasswords(privacy::vector &passwords, std::v return; } - auto total = passwords.size(); + const auto total = passwords.size(); auto &&constPasswordsRef = std::as_const(passwords); // Analyze the passwords @@ -621,7 +623,7 @@ inline void analyzePasswords(privacy::vector &passwords, std::v } // Check for reused passwords - std::unordered_map> passwordMap; + std::unordered_map > passwordMap; for (const auto &record: constPasswordsRef) { const auto &site = std::get<0>(record); const auto &password = std::get<2>(record); @@ -632,7 +634,7 @@ inline void analyzePasswords(privacy::vector &passwords, std::v // Print the weak passwords auto weak{weakPasswords.size()}; - if (!weakPasswords.empty())[[likely]] { + if (!weakPasswords.empty()) [[likely]] { printColor(std::format("Found {} account{} with weak passwords:", weak, weak == 1 ? "" : "s"), 'r', true); printColor("------------------------------------------------------", 'r', true); for (const auto &password: weakPasswords) { @@ -640,18 +642,17 @@ inline void analyzePasswords(privacy::vector &passwords, std::v printColor("------------------------------------------------------", 'r', true); } printColor(std::format("Please change the weak passwords above. " - "\nYou can use the 'generate password' option to generate strong passwords.\n"), 'r', + "\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); // Find reused passwords - using PasswordSites = std::pair>; - std::multimap> countMap; + using PasswordSites = std::pair >; + std::multimap > countMap; - for (const auto &entry: passwordMap) { - const auto &sites = entry.second; + for (const auto &[password, sites]: passwordMap) { if (const auto &x = sites.size(); x > 1) { - countMap.insert(std::make_pair(x, PasswordSites(entry.first, sites))); + countMap.insert(std::make_pair(x, PasswordSites(password, sites))); } } @@ -679,8 +680,8 @@ inline void analyzePasswords(privacy::vector &passwords, std::v // Print the statistics std::cout << "\nTotal passwords: " << total << std::endl; - if (weak > 0)[[likely]] { - char col{std::cmp_greater(weak, total / 4) ? 'r' : 'y'}; + 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) / @@ -701,13 +702,15 @@ void passwordManager() { if (!fs::exists(passwordFile) || !fs::is_regular_file(passwordFile) || fs::is_empty(passwordFile)) { auto [path, pass] = initialSetup(); - if (path.empty() && pass.empty()) { // user exited - return; - } else if (path.empty()) [[likely]] { // user provided a new primary password - encryptionKey = pass; + // If both path and pass are empty, the user wants to exit + if (path.empty() && pass.empty()) return; + // The user provided a new primary password + if (path.empty()) [[likely]] { + encryptionKey = pass; newSetup = true; - } else { // the user pointed us to an existing password records + } else { + // the user pointed us to an existing password records passwordFile = path; } } @@ -716,7 +719,7 @@ void passwordManager() { if (!newSetup) { // preprocess the passwordFile - privacy::string pwHash = getHash(passwordFile); + const privacy::string pwHash = getHash(passwordFile); int attempts{0}; bool isCorrect; @@ -727,7 +730,6 @@ void passwordManager() { isCorrect = verifyPassword(encryptionKey, pwHash); if (!isCorrect && attempts < 2) printColor("Wrong password, please try again.", 'r', true, std::cerr); - } while (!isCorrect && ++attempts < 3); // If the password is still incorrect, exit @@ -753,25 +755,26 @@ void passwordManager() { // 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} + {1, addPassword}, + {2, updatePassword}, + {3, deletePassword}, + {4, viewAllPasswords}, + {5, searchPasswords}, + {6, generatePassword}, + {7, analyzePasswords}, + {8, importPasswords}, + {9, exportPasswords} }; - // A string of colors to choose from - constexpr auto colors = "rgbymcw"; std::random_device rd; // get a random number from hardware std::mt19937 gen(rd()); // seed the generator std::uniform_int_distribution dist(0, 6); // define the range while (true) { + // Colors to use for the menu + constexpr auto colors = "rgbymcw"; auto color = colors[dist(gen)]; + printColor("-------------------------------------------", color, true); std::cout << "1. Add new password\n"; std::cout << "2. Update password\n"; @@ -789,9 +792,7 @@ void passwordManager() { try { int choice = getResponseInt("Enter your choice: "); - auto iter = choices.find(choice); - - if (iter != choices.end()) + if (auto iter = choices.find(choice); iter != choices.end()) iter->second(passwords, passwordStrength); else if (choice == 10) { if (changeMasterPassword(encryptionKey)) @@ -800,11 +801,8 @@ void passwordManager() { } else if (choice == 11) break; else printColor("Invalid choice!", 'r', true, std::cerr); - } catch (const std::exception &ex) { printColor(ex.what(), 'r', true, std::cerr); - continue; - } catch (...) { throw std::runtime_error("An error occurred."); } } @@ -813,7 +811,7 @@ void passwordManager() { // Create the password file if it doesn't exist if (!fs::exists(DefaultPasswordFile)) { std::error_code ec; - if (auto home{getHomeDir()}; fs::exists(home)) + 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); @@ -824,5 +822,4 @@ void passwordManager() { if (savePasswords(passwords, DefaultPasswordFile, encryptionKey)) printColor("Passwords saved successfully.", 'g', true); else printColor("Passwords not saved!", 'r', true, std::cerr); - } diff --git a/src/passwordManager/passwordManager.cppm b/src/passwordManager/passwordManager.cppm new file mode 100644 index 0000000..c2b9bb1 --- /dev/null +++ b/src/passwordManager/passwordManager.cppm @@ -0,0 +1,56 @@ +// 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 + +export module passwordManager; + +import utils; +import secureAllocator; + +using passwordRecords = std::tuple; + +privacy::vector loadPasswords(const std::string &filePath, const privacy::string &decryptionKey); + +bool savePasswords(privacy::vector &passwords, const std::string &filePath, + const privacy::string &encryptionKey); + +bool isPasswordStrong(const privacy::string &password) noexcept; + +privacy::string generatePassword(const int &length); + +bool changeMasterPassword(privacy::string &primaryPassword); + +std::pair initialSetup() noexcept; + +privacy::string getHash(const std::string &filePath); + +privacy::vector importCsv(const std::string &filePath); + +bool exportCsv(const privacy::vector &records, const std::string &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 60a3626..29d393f 100644 --- a/src/passwordManager/passwords.cpp +++ b/src/passwordManager/passwords.cpp @@ -14,9 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../encryption/encryptDecrypt.hpp" -#include "../utils/utils.hpp" -#include "passwords.hpp" +module; + #include #include #include @@ -25,6 +24,13 @@ #include #include #include +#include + +import utils; +import encryption; +import secureAllocator; + +module passwordManager; namespace fs = std::filesystem; @@ -42,7 +48,7 @@ bool isPasswordStrong(const privacy::string &password) noexcept { bool hasDigit = false; bool hasPunctuation = false; - for (char ch: password) { + for (const char ch: password) { if (std::isupper(ch)) hasUppercase = true; else if (std::islower(ch)) @@ -63,7 +69,7 @@ bool isPasswordStrong(const privacy::string &password) noexcept { /// \brief Generates a random password. /// \param length the length of the password. /// \return a random password. -privacy::string generatePassword(int length) { +privacy::string generatePassword(const int &length) { // a password shouldn't be too short, nor too long if (length < 8) throw std::length_error("Password too short."); @@ -71,7 +77,8 @@ privacy::string generatePassword(int length) { throw std::length_error("Password too long."); // generate from a set of printable ascii characters - const std::string characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-=_~+[]{}<>"; + const std::string characters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-=_~+[]{}<>"; // Seed the Mersenne Twister engine with a random source (ideally non-deterministic) std::random_device rd; @@ -83,11 +90,12 @@ privacy::string generatePassword(int length) { privacy::string password; password.reserve(length); - int trials{0}, maxTrials{100}; // 100 trials to generate a strong password is generous enough + int trials{0}; + constexpr int maxTrials{100}; // 100 trials to generate a strong password is generous enough // Generate a strong password by default do { - password.clear(); // empty the password to reset it to it's initial state + password.clear(); // empty the password to reset it to it's initial state for (int i = 0; i < length; ++i) password += characters[distribution(generator)]; @@ -109,8 +117,8 @@ hashPassword(const privacy::string &password, const std::size_t &opsLimit, const std::array hashedPassword{}; if (crypto_pwhash_str - (hashedPassword.data(), password.c_str(), password.size(), - opsLimit, memLimit) != 0) { + (hashedPassword.data(), password.c_str(), password.size(), + opsLimit, memLimit) != 0) { throw std::runtime_error("Out of memory for password hashing."); } @@ -134,8 +142,8 @@ bool verifyPassword(const privacy::string &password, const privacy::string &stor /// \param end the end index. /// \param encrypt A boolean value indicating whether to encrypt or decrypt. void -encryptDecryptRange(privacy::vector &passwords, const privacy::string &key, std::size_t start, - std::size_t end, bool encrypt = false) { +encryptDecryptRange(privacy::vector &passwords, const privacy::string &key, const std::size_t &start, + const std::size_t &end, const bool &encrypt = false) { // Check for invalid range if (start > end || end > passwords.size()) throw std::range_error("Invalid range."); @@ -143,9 +151,10 @@ encryptDecryptRange(privacy::vector &passwords, const privacy:: try { // Encrypt/decrypt the password field 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])}, - key); + std::get<2>(passwords[i]) = encrypt + ? encryptStringWithMoreRounds(std::get<2>(passwords[i]), key) + : decryptStringWithMoreRounds(std::string{std::get<2>(passwords[i])}, + key); } } catch (const std::exception &ex) { printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); @@ -160,22 +169,26 @@ encryptDecryptRange(privacy::vector &passwords, const privacy:: /// \param end the end index. /// \param encrypt A boolean value indicating whether to encrypt or decrypt. void -encryptDecryptRangeAllFields(privacy::vector &passwords, const privacy::string &key, std::size_t start, - std::size_t end, bool encrypt = false) { +encryptDecryptRangeAllFields(privacy::vector &passwords, const privacy::string &key, + const std::size_t &start, + const std::size_t &end, const bool &encrypt = false) { if (start > end || end > passwords.size()) throw std::range_error("Invalid range."); try { // Encrypt/decrypt all fields 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); + std::get<0>(passwords[i]) = encrypt + ? encryptString(std::get<0>(passwords[i]), key) + : decryptString(std::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); + std::get<1>(passwords[i]) = encrypt + ? encryptString(std::get<1>(passwords[i]), key) + : decryptString(std::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); + std::get<2>(passwords[i]) = encrypt + ? encryptString(std::get<2>(passwords[i]), key) + : decryptString(std::string{std::get<2>(passwords[i])}, key); } } catch (const std::exception &ex) { printColor(std::format("Error: {}", ex.what()), 'r', true, std::cerr); @@ -189,14 +202,15 @@ encryptDecryptRangeAllFields(privacy::vector &passwords, const /// \param encrypt A boolean value indicating whether to encrypt or decrypt. /// \param allFields A boolean value indicating whether to encrypt/decrypt all fields or just the password field. void -encryptDecryptConcurrently(privacy::vector &passwordEntries, const privacy::string &key, bool encrypt, - bool allFields) { - std::size_t numPasswords = passwordEntries.size(); +encryptDecryptConcurrently(privacy::vector &passwordEntries, const privacy::string &key, + const bool &encrypt, + const bool &allFields) { + const std::size_t numPasswords = passwordEntries.size(); const unsigned int numThreads{std::jthread::hardware_concurrency() ? std::jthread::hardware_concurrency() : 8}; // Divide the password entries among threads std::vector threads; - std::size_t passPerThread = numPasswords / numThreads; + const std::size_t passPerThread = numPasswords / numThreads; std::size_t start = 0; // encrypt/decrypt passwords in parallel @@ -219,19 +233,19 @@ encryptDecryptConcurrently(privacy::vector &passwordEntries, co /// \brief Checks for common errors when reading/writing to a file. /// \param path the path to the file. -inline void checkCommonErrors(const std::string &path) { +inline void checkCommonErrors(std::string_view path) { std::error_code ec; - fs::file_status fileStatus = fs::status(path, ec); + const fs::file_status fileStatus = fs::status(path, ec); if (ec) throw std::runtime_error(std::format("Could not determine {}'s status: {}.", path, ec.message())); - if (!fs::exists(fileStatus)) + if (!exists(fileStatus)) throw std::runtime_error(std::format("The password file ({}) does not exist.", path)); - if (fs::is_directory(fileStatus)) + if (is_directory(fileStatus)) throw std::runtime_error(std::format("The path '{}' is a directory.", path)); - if (!fs::is_regular_file(fileStatus)) + if (!is_regular_file(fileStatus)) throw std::runtime_error(std::format("The password file ({}) is not a regular file.", path)); } @@ -242,14 +256,11 @@ inline void checkCommonErrors(const std::string &path) { /// \return True, if successful. bool savePasswords(privacy::vector &passwords, const std::string &filePath, const privacy::string &encryptionKey) { - std::string tempFile = filePath + "XXXXXX"; // Create a temporary file - int tmpFileFd = mkstemp(tempFile.data()); - // If the temporary file couldn't be created, use the original file path - if (tmpFileFd == -1) + if (int tmpFileFd = mkstemp(tempFile.data()); tmpFileFd == -1) tempFile = filePath; else close(tmpFileFd); // Close the file descriptor @@ -259,8 +270,8 @@ bool savePasswords(privacy::vector &passwords, const std::strin checkCommonErrors(tempFile); } catch (const std::exception &ex) { std::cerr << ex.what() << std::endl; - - } catch (...) {} // Ignore any other exceptions as we're about to exit anyway + } catch (...) { + } // Ignore any other exceptions as we're about to exit anyway std::cerr << std::format("Failed to open the password file ({}) for writing.\n", tempFile); return false; @@ -317,16 +328,16 @@ privacy::vector loadPasswords(const std::string &filePath, cons throw std::runtime_error(std::format("Failed to open the password file ({}) for reading.", filePath)); privacy::string line; - line.reserve(4096); // Pre-allocate space for efficiency + line.reserve(4096); // Pre-allocate space for efficiency // Read and discard the first line - std::getline, privacy::Allocator>(file, line); + std::getline, privacy::Allocator >(file, line); // Read and discard the second line too - std::getline, privacy::Allocator>(file, line); + std::getline, privacy::Allocator >(file, line); // Read and process the file line by line - while (std::getline, privacy::Allocator>(file, line)) { + while (std::getline, privacy::Allocator >(file, line)) { std::size_t firstDelimiterPos = line.find(':'); std::size_t secondDelimiterPos = line.find(':', firstDelimiterPos + 1); @@ -357,7 +368,7 @@ privacy::vector loadPasswords(const std::string &filePath, cons /// \param primaryPassword the current primary password. /// \return True if the password is changed successfully, else false. bool changeMasterPassword(privacy::string &primaryPassword) { - privacy::string oldPassword{getSensitiveInfo("Enter the current primary password: ")}; + 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, @@ -372,7 +383,7 @@ bool changeMasterPassword(privacy::string &primaryPassword) { while (!isPasswordStrong(newPassword) && ++count < 3) { std::cerr << "Weak password! Password should have at least 8 characters and include uppercase letters,\n" - "lowercase letters, special characters and digits" << std::endl; + "lowercase letters, special characters and digits" << std::endl; newPassword = getSensitiveInfo("Please enter a stronger password: "); } @@ -381,7 +392,7 @@ bool changeMasterPassword(privacy::string &primaryPassword) { return false; } - privacy::string newPassword2{getSensitiveInfo("Enter the new primary password again: ")}; + 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, @@ -403,21 +414,23 @@ std::pair initialSetup() noexcept { std::cout << "Looks like you don't have any passwords saved yet." << std::endl; while (true) { - - int resp = getResponseInt( - "1. Initial setup. (Select if you haven't used this program to manage credentials before).\n" - "2. Enter the path to an existing password file (previously created by this program).\n" - "3. Exit.\n" - "select 1, 2, or 3: "); - if (resp == 1) { // Initial setup + const int resp = getResponseInt( + "1. Initial setup. (Select if you haven't used this program to manage credentials before).\n" + "2. Enter the path to an existing password file (previously created by this program).\n" + "3. Exit.\n" + "select 1, 2, or 3: "); + if (resp == 1) { + // Initial setup privacy::string pass{getSensitiveInfo("Enter a new primary password: ")}; int count{0}; while (!isPasswordStrong(pass) && ++count < 3) { - 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); + 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); pass = getSensitiveInfo(last ? "" : "Please enter a stronger password: "); } @@ -428,17 +441,18 @@ std::pair initialSetup() noexcept { const auto hash = hashPassword(pass, crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE); - privacy::string pass2{getSensitiveInfo("Enter the password again: ")}; - if (!verifyPassword(pass2, hash)) { + if (const privacy::string pass2{getSensitiveInfo("Enter the password again: ")}; !verifyPassword( + pass2, hash)) { std::cerr << "Password mismatch!" << std::endl; continue; } ret.second = pass; break; - - } else if (resp == 2) { // Enter the path to an existing password file + } + 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))) { std::cerr << "That file doesn't exist or is not a regular file." << std::endl; @@ -447,14 +461,14 @@ std::pair initialSetup() noexcept { ret.first = path; break; - - } else if (resp == 3) { + } + if (resp == 3) { return ret; - } else { // Invalid choice - std::cerr << "Invalid choice. Try again" << std::endl; - continue; } + // Invalid choice + std::cerr << "Invalid choice. Try again" << std::endl; } + return ret; } @@ -474,16 +488,16 @@ privacy::string getHash(const std::string &filePath) { privacy::string pwHash; // Read and discard the first line ('PLEASE DO NOT EDIT THIS FILE') - std::getline, privacy::Allocator>(passFileStream, pwHash); + std::getline, privacy::Allocator >(passFileStream, pwHash); // The hash is on the second line - std::getline, privacy::Allocator>(passFileStream, pwHash); + std::getline, privacy::Allocator >(passFileStream, pwHash); passFileStream.close(); if (pwHash.empty()) throw std::runtime_error("The password hash is empty."); - if (!pwHash.contains("argon")) // Just making sure the read content is the hash we need + if (!pwHash.contains("argon")) // Just making sure the read content is the hash we need throw std::runtime_error("Invalid password hash in the password file."); return pwHash; @@ -503,29 +517,27 @@ bool exportCsv(const privacy::vector &records, const std::strin } // Check if the file path is a directory - if (fs::is_directory(filepath, ec)) { + if (is_directory(filepath, ec)) { // If the file path is a directory, append the default file name to it filepath /= "credentials.csv"; } if (ec) ec.clear(); // Don't throw yet, try other checks // Check if the file already exists - if (fs::exists(filepath, ec)) { + if (exists(filepath, ec)) { // Check if the file is a regular file - if (!fs::is_regular_file(filepath)) - [[unlikely]] { + if (!is_regular_file(filepath)) [[unlikely]] { printColor(std::format("The destination file ({}) is not a regular file.", filePath), 'r', true, std::cerr); return false; } if (!validateYesNo("The destination file already exists. Do you want to overwrite it? (y/n):")) return false; - else { - fs::remove(filepath, ec); - if (ec) { - std::cerr << "Error removing " << filepath << ": " << ec.message() << std::endl; - ec.clear(); - } + + fs::remove(filepath, ec); + if (ec) { + std::cerr << "Error removing " << filepath << ": " << ec.message() << std::endl; + ec.clear(); } } if (ec) ec.clear(); @@ -561,11 +573,11 @@ bool exportCsv(const privacy::vector &records, const std::strin inline void trim(std::string &str) { // Trim the leading space (my IDE finds the w-word offensive) std::input_iterator auto it = std::ranges::find_if_not(str.begin(), str.end(), - [](char c) { return std::isspace(c); }); + [](const char c) { return std::isspace(c); }); str.erase(str.begin(), it); // Trim the trailing space - it = std::ranges::find_if_not(str.rbegin(), str.rend(), [](char c) { return std::isspace(c); }).base(); + it = std::ranges::find_if_not(str.rbegin(), str.rend(), [](const char c) { return std::isspace(c); }).base(); str.erase(it, str.end()); } @@ -587,14 +599,14 @@ privacy::vector importCsv(const std::string &filePath) { privacy::string line, value; if (hasHeader) - std::getline, privacy::Allocator>(file, - line); // Read and discard the first line + std::getline, privacy::Allocator >(file, + line); // Read and discard the first line - while (std::getline, privacy::Allocator>(file, line)) { + while (std::getline, privacy::Allocator >(file, line)) { privacy::istringstream iss(line); privacy::vector tokens; - while (std::getline, privacy::Allocator>(iss, value, ',')) + while (std::getline, privacy::Allocator >(iss, value, ',')) tokens.emplace_back(value); // Trim leading and trailing space from the tokens, including tabs and newlines, if any @@ -605,7 +617,7 @@ privacy::vector importCsv(const std::string &filePath) { // Skip empty passwords if (tokens[2].empty()) { std::cerr << std::format("Empty password for {}. Entry skipped.\n", tokens[0]); - } else if (!(tokens[0].empty() && tokens[1].empty())) // Both site & username can't be empty + } else if (!(tokens[0].empty() && tokens[1].empty())) // Both site & username can't be empty passwords.emplace_back(tokens[0], tokens[1], tokens[2]); } else std::cerr << std::format("Invalid entry skipped: {}\n", line); diff --git a/src/passwordManager/passwords.hpp b/src/passwordManager/passwords.hpp deleted file mode 100644 index c46aaf9..0000000 --- a/src/passwordManager/passwords.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "../utils/utils.hpp" -#include "../secureAllocator.hpp" - -using passwordRecords = std::tuple; - -privacy::vector loadPasswords(const std::string &filePath, const privacy::string &decryptionKey); - -bool savePasswords(privacy::vector &passwords, const std::string &filePath, - const privacy::string &encryptionKey); - -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); - -bool isPasswordStrong(const privacy::string &password) noexcept; - -privacy::string generatePassword(int length); - -bool verifyPassword(const privacy::string &password, const privacy::string &storedHash); - -void passwordManager(); - -bool changeMasterPassword(privacy::string &primaryPassword); - -std::pair initialSetup() noexcept; - -privacy::string getHash(const std::string &filePath); - -privacy::vector importCsv(const std::string &filePath); - -bool exportCsv(const privacy::vector &records, const std::string &filePath = getHomeDir()); - -void -encryptDecryptConcurrently(privacy::vector &passwordEntries, const privacy::string &key, - bool encrypt = true, bool allFields = false); diff --git a/src/privacyTracks/privacyTracks.cpp b/src/privacyTracks/privacyTracks.cppm similarity index 89% rename from src/privacyTracks/privacyTracks.cpp rename to src/privacyTracks/privacyTracks.cppm index 18955f5..7706420 100644 --- a/src/privacyTracks/privacyTracks.cpp +++ b/src/privacyTracks/privacyTracks.cppm @@ -14,20 +14,23 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#include "../utils/utils.hpp" -#include "privacyTracks.hpp" +module; + #include #include #include #include #include +export module privacyTracks; + +import utils; + namespace fs = std::filesystem; /// \brief Represents different browsers in a system. -enum class Browser : const -unsigned int { +enum class Browser : unsigned int { Firefox = 1 << 0, Chrome = 1 << 1, Chromium = 1 << 2, @@ -82,7 +85,7 @@ unsigned int detectBrowsers(const std::string &pathEnv) { handleFileError(ec, "reading", entry.path()); // Skip broken symlinks - if (fs::exists(entry.status())) { + if (exists(entry.status())) { // Check for the existence of the browser executable if (auto executable = entry.path().filename().string(); !entry.is_directory() && entry.exists()) { if (executable == "firefox") @@ -107,12 +110,11 @@ unsigned int detectBrowsers(const std::string &pathEnv) { /// \details This function uses the PATH environment variable to detect browsers. /// \note Only stable versions of browsers are detected. unsigned int detectBrowsers() { - if (auto pathEnv = getEnv("PATH"); pathEnv) + if (const auto pathEnv = getEnv("PATH"); pathEnv) return detectBrowsers(*pathEnv); - else { - printColor("PATH environment variable not found.", 'r', true, std::cerr); - return 0; - } + + printColor("PATH environment variable not found.", 'r', true, std::cerr); + return 0; } /// \brief Clears Firefox cookies and history. @@ -131,7 +133,7 @@ bool clearFirefoxTracks(const std::string &configDir) { 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); - if (fs::exists(entry.status())) { // skip broken symlinks + if (exists(entry.status())) { if (entry.is_directory() && entry.path().filename().string().contains(".default")) defaultProfileDirs.emplace_back(entry.path()); } @@ -149,7 +151,6 @@ bool clearFirefoxTracks(const std::string &configDir) { // Clearing history fs::remove(profile / "places.sqlite", ec); handleFileError(ec, "deleting", profile / "places.sqlite"); - } } else printColor("No default profiles found.", 'r', true); @@ -158,7 +159,8 @@ bool clearFirefoxTracks(const std::string &configDir) { 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); - if (fs::exists(entry.status())) { // skip broken symlinks + if (exists(entry.status())) { + // skip broken symlinks if (entry.is_directory() && !entry.path().filename().string().contains(".default") && entry.path().filename().string() != "Crash Reports" && entry.path().filename().string() != "Pending Pings") @@ -176,7 +178,8 @@ bool clearFirefoxTracks(const std::string &configDir) { fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", profile); - if (fs::exists(entry.status())) { // Ignore broken symlinks + if (exists(entry.status())) { + // Ignore broken symlinks if (entry.is_regular_file()) { if (entry.path().filename() == "cookies.sqlite") { fs::remove(entry.path(), ec); @@ -203,9 +206,9 @@ bool clearFirefoxTracks(const std::string &configDir) { } } } - printColor(nonDefaultProfiles ? std::format("Deleted cookies and history for {} non-default profiles.", - nonDefaultProfiles) : "Non-default profiles not found.", - nonDefaultProfiles ? 'g' : 'r', true); + printColor(nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", nonDefaultProfiles) + : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); return true; } @@ -226,7 +229,7 @@ bool clearChromiumTracks(const std::string &configDir) { 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); - if (fs::exists(entry.status())) { + if (exists(entry.status())) { if (entry.is_directory() && (entry.path().filename() == "Default" || entry.path().filename() == "default")) { defaultProfileDir = entry.path(); @@ -252,8 +255,7 @@ bool clearChromiumTracks(const std::string &configDir) { 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); - if (fs::exists(entry.status())) { - + if (exists(entry.status())) { if (entry.is_directory() && entry.path().filename() != "Default" && entry.path().filename() != "default") { profileDirs.emplace_back(entry.path()); } @@ -271,7 +273,8 @@ bool clearChromiumTracks(const std::string &configDir) { fs::directory_options::follow_directory_symlink, ec)) { handleFileError(ec, "reading", profile); - if (fs::exists(entry.status())) { // ignore broken symlinks + if (exists(entry.status())) { + // ignore broken symlinks if (entry.is_regular_file()) { // Clearing cookies if (entry.path().filename() == "Cookies") { @@ -300,8 +303,10 @@ bool clearChromiumTracks(const std::string &configDir) { } } } - printColor(nonDefaultProfiles ? std::format("Deleted cookies and history for {} non-default profiles.", - nonDefaultProfiles) : "Non-default profiles not found.", + printColor(nonDefaultProfiles + ? std::format("Deleted cookies and history for {} non-default profiles.", + nonDefaultProfiles) + : "Non-default profiles not found.", nonDefaultProfiles ? 'g' : 'r', true); return true; @@ -329,7 +334,7 @@ bool clearOperaTracks(const std::string &profilePath) { if (ec) { handleFileError(ec, "deleting", profilePath + "/cookies"); ec.clear(); - ret = false; // We don't to return yet, we want to try to clear history too + ret = false; // We don't to return yet, we want to try to clear history too } } @@ -391,7 +396,7 @@ bool clearOperaTracks() { /// \return true if successful, false otherwise. bool clearSafariTracks() { #if __APPLE__ - std::string cookiesPath = getHomeDir() + "/Library/Cookies"; + const std::string cookiesPath = getHomeDir() + "/Library/Cookies"; if (!fs::exists(cookiesPath)) { printColor("Safari cookies directory not found.", 'r', true, std::cerr); return false; @@ -407,7 +412,7 @@ bool clearSafariTracks() { } } - std::string historyPath = getHomeDir() + "/Library/Safari"; + const std::string historyPath = getHomeDir() + "/Library/Safari"; if (!fs::exists(historyPath)) { printColor("Safari history directory not found.", 'r', true, std::cerr); return false; @@ -449,7 +454,7 @@ bool clearFirefoxTracks() { /// \param browsers the browsers to clear tracks for. /// \return true if successful, false otherwise. /// \note Only works for standard installations of the browsers. -bool clearTracks(unsigned int browsers) { +bool clearTracks(const unsigned int &browsers) { bool ret{true}; if (browsers & static_cast(Browser::Firefox)) { @@ -497,37 +502,35 @@ bool clearTracks(unsigned int browsers) { /// \brief Clears all tracks for all supported browsers installed on the system. /// \note Only works for standard installations of the browsers. -void clearPrivacyTracks() { +export void clearPrivacyTracks() { std::cout << "Scanning your system for browsers..." << std::endl; - unsigned int browsers = detectBrowsers(); - if (browsers == 0) { + const unsigned int browsers = detectBrowsers(); + if (browsers == 0) [[unlikely]] { printColor("No supported browsers found.", 'r', true, std::cerr); return; - } else [[likely]] { - printColor("Supported browsers found:", 'b', true); - if (browsers & static_cast(Browser::Firefox)) - printColor("Firefox", 'c', true); + } + printColor("Supported browsers found:", 'b', true); + if (browsers & static_cast(Browser::Firefox)) + printColor("Firefox", 'c', true); - if (browsers & static_cast(Browser::Chrome)) - printColor("Chrome", 'c', true); + if (browsers & static_cast(Browser::Chrome)) + printColor("Chrome", 'c', true); - if (browsers & static_cast(Browser::Chromium)) - printColor("Chromium", 'c', true); + if (browsers & static_cast(Browser::Chromium)) + printColor("Chromium", 'c', true); - if (browsers & static_cast(Browser::Opera)) - printColor("Opera", 'c', true); + if (browsers & static_cast(Browser::Opera)) + printColor("Opera", 'c', true); - if (browsers & static_cast(Browser::Safari)) - printColor("Safari", 'c', true); - } + if (browsers & static_cast(Browser::Safari)) + printColor("Safari", 'c', true); printColor("\nAll the cookies and browsing history of the above browsers will be deleted.", 'r', true); printColor("Continue? (y/n): ", 'c'); if (validateYesNo()) { - auto cleared{clearTracks(browsers)}; + const auto cleared{clearTracks(browsers)}; printColor(cleared ? "\nAll tracks cleared successfully." : "\nFailed to clear all tracks.", cleared ? 'g' : 'r', true, cleared ? std::cout : std::cerr); - } else printColor("Aborted.", 'r', true); } diff --git a/src/privacyTracks/privacyTracks.hpp b/src/privacyTracks/privacyTracks.hpp deleted file mode 100644 index 7878131..0000000 --- a/src/privacyTracks/privacyTracks.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -void clearPrivacyTracks(); diff --git a/src/secureAllocator.hpp b/src/secureAllocator.cppm similarity index 97% rename from src/secureAllocator.hpp rename to src/secureAllocator.cppm index 811928d..b1c5bce 100644 --- a/src/secureAllocator.hpp +++ b/src/secureAllocator.cppm @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see https://www.gnu.org/licenses. -#pragma once +module; #include #include @@ -22,7 +22,9 @@ #include #include -namespace privacy { +export module secureAllocator; + +export namespace privacy { template /// \brief Custom allocator for STL containers, which locks and zeroizes memory. diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 09b0c2d..01d98f5 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -13,60 +13,80 @@ // // 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 "utils.hpp" #include #include #include #include #include #include +#include +#include +#include +#include +#include + +module utils; + +import secureAllocator; + /// \brief Performs Base64 decoding of a string into binary data. /// \param encodedData Base64 encoded string. /// \return a vector of the decoded binary data. std::vector base64Decode(const std::string &encodedData) { - BIO *bio, *b64; - int len; - + // Allocate a buffer for the decoded data (the size of the decoded data is always less than the size of the encoded data) std::vector decodedData(encodedData.size()); - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - bio = BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())); + // Custom deleter for BIO objects + auto bioDeleter = [](BIO *bio) -> void { BIO_free_all(bio); }; + + // Create a BIO object to decode the data + const std::unique_ptr b64(BIO_new(BIO_f_base64()), bioDeleter); + // Don't use newlines to flush buffer + BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); + + // Create a memory BIO to store the encoded data + std::unique_ptr bio( + BIO_new_mem_buf(encodedData.data(), static_cast(encodedData.size())), bioDeleter); + + // Check if memory allocation failed if (b64 == nullptr || bio == nullptr) - throw std::bad_alloc(); // Memory allocation failed + throw std::bad_alloc(); - bio = BIO_push(b64, bio); + // Push the memory BIO to the base64 BIO + bio.reset(BIO_push(b64.get(), bio.get())); - len = BIO_read(bio, decodedData.data(), static_cast(decodedData.size())); + // Decode the data + const int len = BIO_read(bio.get(), decodedData.data(), static_cast(decodedData.size())); + // Check if the decoding failed if (len < 0) throw std::runtime_error("BIO_read() failed."); - BIO_free_all(bio); - - decodedData.resize(len); // Resize to the actual length of the decoded data + // 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>; +concept StringLike = std::same_as >; /// \brief Trims space (whitespace) off the beginning and end of a string. /// \param str the string to trim. -inline void trimSpace(StringLike auto &str) { - // Trim the leading space (my IDE finds the w-word offensive) +void stripString(StringLike auto &str) noexcept { + // Trim the leading space std::input_iterator auto it = std::ranges::find_if_not(str.begin(), str.end(), - [](char c) { return std::isspace(c); }); + [](const char c) { return std::isspace(c); }); str.erase(str.begin(), it); // Trim the trailing space - it = std::ranges::find_if_not(str.rbegin(), str.rend(), [](char c) { return std::isspace(c); }).base(); + it = std::ranges::find_if_not(str.rbegin(), str.rend(), [](const char c) { return std::isspace(c); }).base(); str.erase(it, str.end()); } @@ -81,11 +101,12 @@ std::string getResponseStr(const std::string &prompt) { std::cout << prompt << std::endl; char *tmp = readline("> "); auto str = std::string{tmp}; + // Trim leading and trailing spaces - trimSpace(str); + stripString(str); // tmp must be freed - free(tmp); + std::free(tmp); return str; } @@ -96,7 +117,7 @@ std::string getResponseStr(const std::string &prompt) { /// \return the user's input (an integer) on if it's convertible to integer, else 0. int getResponseInt(const std::string &prompt) { // A lambda to convert a string to an integer - constexpr auto toInt = [](std::string_view s) noexcept -> int { + 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; }; @@ -122,7 +143,7 @@ privacy::string getSensitiveInfo(const std::string &prompt) { std::free(tmp); // Trim leading and trailing spaces - trimSpace(secret); + stripString(secret); // Restore terminal settings tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); @@ -134,7 +155,7 @@ privacy::string getSensitiveInfo(const std::string &prompt) { /// \param prompt The confirmation prompt. /// \return True if the user confirms the action, else false. bool validateYesNo(const std::string &prompt) { - std::string resp = getResponseStr(prompt); + const std::string resp = getResponseStr(prompt); if (resp.empty()) return false; return std::tolower(resp.at(0)) == 'y'; } @@ -169,14 +190,14 @@ std::uintmax_t getAvailableSpace(const fs::path &path) noexcept { std::error_code ec; // For ignoring errors to avoid throwing // Find an existing component of the path - while ((!fs::exists(filePath, ec)) && filePath.has_parent_path()) + while ((!exists(filePath, ec)) && filePath.has_parent_path()) filePath = filePath.parent_path(); if (ec) ec.clear(); - const auto space = fs::space(fs::canonical(filePath, ec), ec); + auto [capacity, free, available] = space(canonical(filePath, ec), ec); // Return 0 in case of an error - return std::cmp_less(space.available, 0) || std::cmp_equal(space.available, UINTMAX_MAX) ? 0 : space.available; + return std::cmp_less(available, 0) || std::cmp_equal(available, UINTMAX_MAX) ? 0 : available; } diff --git a/src/utils/utils.cppm b/src/utils/utils.cppm new file mode 100644 index 0000000..f7bcff6 --- /dev/null +++ b/src/utils/utils.cppm @@ -0,0 +1,134 @@ +// 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 + +export module utils; + +import secureAllocator; + +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 +}; + +template +// Describes a type that can be formatted to the output stream +concept PrintableToStream = requires(std::ostream &os, const T &t) { + os << t; +}; + +template +// Describes a vector of unsigned characters (For use with vectors using different allocators) +concept uCharVector = std::copy_constructible && requires(T t, unsigned char c) { + { t.data() } -> std::same_as; + { t.size() } -> std::integral; + { t.capacity() } -> std::integral; + std::is_same_v; + t.push_back(c); + t.emplace_back(c); + t.shrink_to_fit(); +}; + +export { + + /// \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"; + + // Print a newline if requested + if (printNewLine) os << std::endl; + } + + /// \brief Performs Base64 encoding of binary data into a string. + /// \param input a vector of the binary data to be encoded. + /// \return Base64-encoded string. + std::string base64Encode(const uCharVector auto &input) { + // Custom deleter for BIO objects + auto bioDeleter = [](BIO *bio) { BIO_free_all(bio); }; + + // Create a BIO object to encode the data + std::unique_ptr b64(BIO_new(BIO_f_base64()), bioDeleter); + + // Don't use newlines to flush buffer + BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL); + + // Create a memory BIO to store the encoded data + std::unique_ptr bio(BIO_new(BIO_s_mem()), bioDeleter); + + // Check if memory allocation failed + if (b64 == nullptr || bio == nullptr) + throw std::bad_alloc(); + + // Push the memory BIO to the base64 BIO + bio.reset(BIO_push(b64.get(), bio.release())); + + if (BIO_write(bio.get(), input.data(), static_cast(input.size())) < 0) + throw std::runtime_error("BIO_write() failed."); + + BIO_flush(bio.get()); + + BUF_MEM *bufferPtr; + BIO_get_mem_ptr(b64.get(), &bufferPtr); + + std::string encodedData(bufferPtr->data, bufferPtr->length); + + return encodedData; + } + + std::vector base64Decode(const std::string &encodedData); + + int getResponseInt(const std::string &prompt = ""); + + std::string getResponseStr(const std::string &prompt = ""); + + bool isWritable(const std::string &filename); + + bool isReadable(const std::string &filename); + + std::uintmax_t getAvailableSpace(const fs::path &path) noexcept; + + bool copyFilePermissions(const std::string &srcFile, const std::string &destFile) noexcept; + + privacy::string getSensitiveInfo(const std::string &prompt = ""); + + bool validateYesNo(const std::string &prompt = ""); + + std::string getHomeDir() noexcept; + + std::optional getEnv(const char *var); +} diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp deleted file mode 100644 index 43cd517..0000000 --- a/src/utils/utils.hpp +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include "../secureAllocator.hpp" -#include -#include -#include -#include -#include -#include -#include - -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 -}; - -template -// Describes a type that can be formatted to the output stream -concept PrintableToStream = requires(std::ostream &os, const T &t) { - os << t; -}; - -/// \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.count(color) ? COLOR.at(color) : "") << text << "\033[0m"; - - // Print a newline if requested - if (printNewLine) os << std::endl; -} - -template -// Describes a vector of unsigned characters (For use with vectors using different allocators) -concept uCharVector = std::copy_constructible && requires(T t, unsigned char c) { - { t.data() } -> std::same_as; - { t.size() } -> std::integral; - { t.capacity() } -> std::integral; - std::is_same_v; - t.push_back(c); - t.emplace_back(c); - t.shrink_to_fit(); -}; - -/// \brief Performs Base64 encoding of binary data into a string. -/// \param input a vector of the binary data to be encoded. -/// \return Base64-encoded string. -std::string base64Encode(const uCharVector auto &input) { - BIO *bio, *b64; - BUF_MEM *bufferPtr; - - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - - bio = BIO_new(BIO_s_mem()); - - if (b64 == nullptr || bio == nullptr) - throw std::bad_alloc(); // Memory allocation failed - - b64 = BIO_push(b64, bio); - - if (BIO_write(b64, input.data(), static_cast(input.size())) < 0) - throw std::runtime_error("BIO_write() failed."); - - BIO_flush(b64); - BIO_get_mem_ptr(b64, &bufferPtr); - - std::string encodedData(bufferPtr->data, bufferPtr->length); - BIO_free_all(b64); - - return encodedData; -} - -std::vector base64Decode(const std::string &encodedData); - -int getResponseInt(const std::string &prompt = ""); - -std::string getResponseStr(const std::string &prompt = ""); - -bool isWritable(const std::string &filename); - -bool isReadable(const std::string &filename); - -std::uintmax_t getAvailableSpace(const fs::path &path) noexcept; - -bool copyFilePermissions(const std::string &srcFile, const std::string &destFile) noexcept; - -privacy::string getSensitiveInfo(const std::string &prompt = ""); - -bool validateYesNo(const std::string &prompt = ""); - -std::string getHomeDir() noexcept; - -std::optional getEnv(const char *var); diff --git a/vcpkg.json b/vcpkg.json index 4f0d741..7215932 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "privacyshield", - "version-string": "1.0.0", + "version-string": "2.0.0", "builtin-baseline": "7b5ca09708ae42dba9517d4e0a0c975d087f1061", "dependencies": [ {