prs-v0.5.2/000077500000000000000000000000001471372304600125175ustar00rootroot00000000000000prs-v0.5.2/.github/000077500000000000000000000000001471372304600140575ustar00rootroot00000000000000prs-v0.5.2/.github/FUNDING.yml000066400000000000000000000001611471372304600156720ustar00rootroot00000000000000# Funding links github: - timvisee custom: - "https://timvisee.com/donate" patreon: timvisee ko_fi: timvisee prs-v0.5.2/.gitignore000066400000000000000000000000101471372304600144760ustar00rootroot00000000000000/target prs-v0.5.2/.gitlab-ci.yml000066400000000000000000000344141471372304600151610ustar00rootroot00000000000000image: "rust:slim" stages: - check - build - test - package - pre-release - release # Variable defaults variables: RUST_VERSION: stable TARGET: x86_64-unknown-linux-gnu APT_GTK_LIBS: libgtk-3-dev FEATURES_CLI_UNIX: backend-gpgme,alias,clipboard,notify,select-skim,tomb,totp FEATURES_GTK3_UNIX: backend-gpgme FEATURES_CLI_WINDOWS: backend-gnupg-bin,clipboard,notify,select-fzf-bin,totp # Install compiler and OpenSSL dependencies before_script: - apt-get update - apt-get install -y --no-install-recommends libgpgme-dev build-essential pkg-config xorg-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev python3 libx11-xcb-dev libdbus-1-dev libgl1-mesa-dev $APT_GTK_LIBS $APT_PACKAGES - apt-get install -y $APT_PACKAGES_RECOMMENDS - | rustup install $RUST_VERSION rustup default $RUST_VERSION - | rustc --version cargo --version # Windows before script .before_script-windows: &before_script-windows before_script: # Install scoop - iex "& {$(irm get.scoop.sh)} -RunAsAdmin" # Install Rust - scoop install rustup - rustup install $RUST_VERSION - rustup default $RUST_VERSION - rustc --version - cargo --version # Install missing Rust target - rustup target add x86_64-pc-windows-msvc # Install GPGME - scoop install gpg # Check on stable, beta and nightly .check-base: &check-base stage: check script: - cargo check --verbose - cd cli; cargo check --no-default-features --features $FEATURES_CLI_UNIX --locked --verbose; cd .. - cd gtk3; cargo check --no-default-features --features $FEATURES_GTK3_UNIX --locked --verbose; cd .. - cd lib; cargo check --no-default-features --features backend-gpgme --locked --verbose; cd .. - cd lib; cargo check --no-default-features --features backend-gnupg-bin --locked --verbose; cd .. - cd cli; cargo check --no-default-features --features backend-gpgme --locked --verbose; cd .. - cd cli; cargo check --no-default-features --features backend-gnupg-bin --locked --verbose; cd .. - cd gtk3; cargo check --no-default-features --features backend-gpgme --locked --verbose; cd .. - cd gtk3; cargo check --no-default-features --features backend-gnupg-bin --locked --verbose; cd .. # check-stable: # <<: *check-base check-beta: <<: *check-base variables: RUST_VERSION: beta only: - master check-nightly: <<: *check-base variables: RUST_VERSION: nightly only: - master check-msrv: <<: *check-base variables: RUST_VERSION: "1.81.0" only: - master # Build using Rust stable on Linux build-x86_64-linux-gnu: stage: build needs: [] script: - cd cli; cargo build --target=$TARGET --release --no-default-features --features $FEATURES_CLI_UNIX --locked --verbose; cd .. - cd gtk3; cargo build --target=$TARGET --release --no-default-features --features $FEATURES_GTK3_UNIX --locked --verbose; cd .. - mv target/$TARGET/release/prs ./prs-$TARGET - mv target/$TARGET/release/prs-gtk3-copy ./prs-gtk3-copy-$TARGET - strip -g ./prs-$TARGET - strip -g ./prs-gtk3-copy-$TARGET artifacts: name: prs-x86_64-linux-gnu paths: - prs-$TARGET - prs-gtk3-copy-$TARGET expire_in: 1 month # Build a static version build-x86_64-linux-musl: image: alpine stage: build needs: [] variables: TARGET: x86_64-unknown-linux-musl FEATURES_CLI_UNIX: backend-gnupg-bin,alias,clipboard,notify,select-skim,tomb,totp before_script: [] script: - apk add rustup alpine-sdk libxcb-dev gtk+3.0-dev - rustup-init -y --target=$TARGET - source $HOME/.cargo/env - cd cli; cargo build --target=$TARGET --release --no-default-features --features $FEATURES_CLI_UNIX --locked --verbose; cd .. # Prepare the release artifact, strip it - find . -name prs -exec ls -lah {} \; - mv target/$TARGET/release/prs ./prs-$TARGET - strip -g ./prs-$TARGET artifacts: name: prs-x86_64-linux-musl paths: - prs-$TARGET expire_in: 1 month # # Build using Rust stable on Windows # build-x86_64-windows: # stage: build # tags: # - windows # only: # - master # - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ # needs: [] # variables: # TARGET: x86_64-pc-windows-msvc # <<: *before_script-windows # script: # - cd cli; cargo build --target=$TARGET --release --no-default-features --features $FEATURES_CLI_WINDOWS --locked --verbose; cd .. # - mv target\$env:TARGET\release\prs.exe .\prs-$env:TARGET.exe # artifacts: # name: prs-x86_64-windows # paths: # - prs-$TARGET.exe # expire_in: 1 month # Run the unit tests through Cargo on Linux test-cargo-x86_64-linux-gnu: stage: test needs: [] dependencies: [] script: - cargo test --locked --verbose # # Run the unit tests through Cargo on Windows # test-cargo-x86_64-windows: # stage: test # tags: # - windows # only: # - master # needs: [] # dependencies: [] # cache: {} # <<: *before_script-windows # script: # - cd lib; cargo test --locked --verbose; cd .. # - cd cli; cargo test --locked --verbose; cd .. # Run basic integration test with prs test-integration: image: alpine stage: test needs: - build-x86_64-linux-musl dependencies: - build-x86_64-linux-musl variables: TARGET: x86_64-unknown-linux-musl before_script: [] script: - apk add git gnupg gpgme - mv ./prs-$TARGET ./prs - git config --global user.email example@example.org - git config --global user.name "Example User" # TODO: add/edit/remove secrets - ./prs help - ./prs init - ./prs sync init - ./prs list - ./prs sync # Package a Docker image package-docker: image: docker:latest stage: package needs: - build-x86_64-linux-musl dependencies: - build-x86_64-linux-musl services: - docker:dind only: - master - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ variables: TARGET: x86_64-unknown-linux-musl IMAGE_NAME: prs:$CI_COMMIT_SHA DOCKER_HOST: tcp://docker:2375 # DOCKER_DRIVER: overlay2 before_script: [] script: - ls -al # Place binary in Docker directory - mv ./prs-$TARGET ./pkg/docker/prs # Build the Docker image, run it once to test - cd ./pkg/docker - docker build -t $IMAGE_NAME ./ - docker run --rm $IMAGE_NAME -V - cd ../.. # Export image as artifact - docker image save -o ./prs-docker-$TARGET.tar $IMAGE_NAME artifacts: name: prs-docker-x86_64-linux-musl paths: - prs-docker-$TARGET.tar expire_in: 1 month # Release binaries on GitLab as generic package release-gitlab-generic-package: image: curlimages/curl stage: pre-release dependencies: - build-x86_64-linux-gnu - build-x86_64-linux-musl # - build-x86_64-windows only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ variables: LINUX_GNU_BIN: "prs-x86_64-unknown-linux-gnu" LINUX_GNU_BIN_GTK_COPY: "prs-gtk3-copy-x86_64-unknown-linux-gnu" LINUX_MUSL_BIN: "prs-x86_64-unknown-linux-musl" # WINDOWS_BIN: "prs-x86_64-pc-windows-msvc.exe" before_script: [] script: # Get version based on tag, determine registry URL - VERSION=$(echo $CI_COMMIT_REF_NAME | cut -c 2-) - PACKAGE_REGISTRY_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/prs/${VERSION}" # Publish packages - | curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN} - | curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_GNU_BIN_GTK_COPY} ${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN_GTK_COPY} - | curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_MUSL_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN} # - | # curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${WINDOWS_BIN} ${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN} # Publish GitLab release release-gitlab-release: image: registry.gitlab.com/gitlab-org/release-cli stage: release only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ variables: LINUX_GNU_BIN: "prs-x86_64-unknown-linux-gnu" LINUX_GNU_BIN_GTK_COPY: "prs-gtk3-copy-x86_64-unknown-linux-gnu" LINUX_MUSL_BIN: "prs-x86_64-unknown-linux-musl" # WINDOWS_BIN: "prs-x86_64-pc-windows-msvc.exe" before_script: [] script: # Get version based on tag, determine registry URL - VERSION=$(echo $CI_COMMIT_REF_NAME | cut -c 2-) - PACKAGE_REGISTRY_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/prs/${VERSION}" # Publish release - | release-cli create --name "prs $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \ --assets-link "{\"name\":\"${LINUX_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}\"}" \ --assets-link "{\"name\":\"${LINUX_GNU_BIN_GTK_COPY}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN_GTK_COPY}\"}" \ --assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" # --assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}" # Publish GitHub release release-github: stage: release only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ dependencies: - build-x86_64-linux-gnu - build-x86_64-linux-musl # - build-x86_64-windows before_script: [] script: # Install dependencies - apt-get update - apt-get install -y curl wget gzip netbase # Download github-release binary - wget https://github.com/tfausak/github-release/releases/download/1.2.5/github-release-linux.gz -O github-release.gz - gunzip github-release.gz - chmod a+x ./github-release # Create the release, upload binaries - ./github-release release --token "$GITHUB_TOKEN" --owner timvisee --repo prs --tag "$CI_COMMIT_REF_NAME" --title "prs $CI_COMMIT_REF_NAME" - ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo prs --tag "$CI_COMMIT_REF_NAME" --file ./prs-x86_64-unknown-linux-gnu --name prs-$CI_COMMIT_REF_NAME-linux-x64 - ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo prs --tag "$CI_COMMIT_REF_NAME" --file ./prs-gtk3-copy-x86_64-unknown-linux-gnu --name prs-gtk3-copy-$CI_COMMIT_REF_NAME-linux-x64 - ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo prs --tag "$CI_COMMIT_REF_NAME" --file ./prs-x86_64-unknown-linux-musl --name prs-$CI_COMMIT_REF_NAME-linux-x64-static # - ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo prs --tag "$CI_COMMIT_REF_NAME" --file ./prs-x86_64-pc-windows-msvc.exe --name prs-$CI_COMMIT_REF_NAME-windows.exe # Cargo crate release release-crate: stage: release dependencies: [] only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ before_script: [] script: - echo "Creating release crate to publish on crates.io..." - echo $CARGO_TOKEN | cargo login - echo "Publishing crates to crates.io..." - cd lib; cargo publish --allow-dirty --no-verify --locked --verbose ; cd .. # Give package index some time to sync - sleep 60 - cd cli; cargo publish --allow-dirty --no-verify --locked --verbose; cd .. - cd gtk3; cargo publish --allow-dirty --no-verify --locked --verbose; cd .. # Publish Docker image on repository registry release-docker: image: docker:latest stage: release only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ dependencies: - package-docker services: - docker:dind variables: TARGET: x86_64-unknown-linux-musl DOCKER_HOST: tcp://docker:2375 IMAGE_NAME: prs:$CI_COMMIT_SHA # DOCKER_DRIVER: overlay2 before_script: [] script: # Import Docker image - docker image load -i ./prs-docker-$TARGET.tar # Retag version - VERSION=$(echo $CI_COMMIT_REF_NAME | cut -c 2-) - echo "Determined Docker image version number 'v$VERSION', retagging image..." - docker tag $IMAGE_NAME registry.gitlab.com/timvisee/prs:$VERSION - docker tag $IMAGE_NAME registry.gitlab.com/timvisee/prs:latest # Authenticate and push the Docker images - 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS' - docker push registry.gitlab.com/timvisee/prs:$VERSION - docker push registry.gitlab.com/timvisee/prs:latest # Publish Docker image on Docker hub release-docker-hub: image: docker:latest stage: release only: - /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ dependencies: - package-docker services: - docker:dind variables: TARGET: x86_64-unknown-linux-musl DOCKER_HOST: tcp://docker:2375 IMAGE_NAME: prs:$CI_COMMIT_SHA # DOCKER_DRIVER: overlay2 before_script: [] script: # Import Docker image - docker image load -i ./prs-docker-$TARGET.tar # Retag version - VERSION=$(echo $CI_COMMIT_REF_NAME | cut -c 2-) - echo "Determined Docker image version number 'v$VERSION', retagging image..." - docker tag $IMAGE_NAME timvisee/prs:$VERSION - docker tag $IMAGE_NAME timvisee/prs:latest # Authenticate and push the Docker images - echo "$DOCKER_HUB_PASS" | docker login -u "$DOCKER_HUB_USER" --password-stdin - docker push timvisee/prs:$VERSION - docker push timvisee/prs:latest # AUR packages release release-aur: image: archlinux stage: release only: - /^v(\d+\.)*\d+$/ timeout: 1h 30m before_script: [] script: - cd ./pkg/aur - ./publish prs-v0.5.2/CHANGELOG.md000066400000000000000000000306721471372304600143400ustar00rootroot00000000000000# Changelog ## 0.5.2 (2024-11-09) - Autocomplete secret names in zsh - Update dependencies ## 0.5.1 (2024-03-17) - Add `--pager` alias for `--viewer` - Add `PRS_PAGER` variable to set custom pager as viewer - Add scroll support to built-in secure viewer - Enforce use of TTY when using built-in secure viewer - Fix errors when `.gpg-id` contains comments - Fix prs not using existing remote on git repository - Fix current git branch not using newly configured remote - Fix auto completion of secrets for bash - Fix panic on `internal completions` command - Update dependencies ## 0.5.0 (2023-01-19) - Add `prs` homebrew package for macOS - Add `sync status` command to show sync status, changed files and sync command hints - Add `sync commit` command to commit all uncommitted changes in password store - Add `sync reset` command to reset all uncommitted changes in password store - Add secure viewer to show secret contents in TTY that clears when closed, instead of outputting to stdout - Add `--viewer` flag to show contents in secure viewer with `show` and other commands - Using `--timeout` now shows contents in secure viewer as this is much more reliable - Add new clipboard handling system making it more secure, reliable and robust - Make clipboard reverting much more reliable - Fix clipboard reverting breaking when user copies again before reverting - Using `totp copy` now recopies the token when it changes within the timeout - Using `generate -ec` now copies the generated password both before and after editing - Clipboard notifications now show if contents are reverted, replaced or cleared - Don't fork process on X11 when setting clipboard due to security concerns - Propagate `--quiet` and `--verbose` flags to clipboard handling and notifications - Fix errors when using relative path for password store with `--store` or `PASSWORD_STORE_DIR` - Fix `slam` errors, don't invoke `gpgconf`, `keychain` or `pkill` binaries if they don't exist - Abort `grep` and `recrypt` commands after 5 failures unless forced - Fix panic using `show` when secret has non UTF-8 contents - Fix runtime errors for some regular expressions due to missing Perl features - Show verbose output and detailed errors when using `--verbose` with `gnupg-bin` backend - Using `git` command now invokes git directly instead of calling through `sh` making it more robust - Make sync remote hint style after `sync init` consistent with other hints - Don't show `--verbose` and `--force` hints in error output when already used - Don't allow `--no-interact` together with `--viewer` or `--timeout` - Rename `prs-cli` to `prs` in help output to be consistent with the binary name - Remove all `unsafe` usages from codebase - Show compiler warning when no interaction selection mode feature is used - Update dependencies ## 0.4.1 (2023-01-11) - Add `grep` command to search secret contents - Add `search` alias for `list` - Add `pwgen` alias for `generate` - Show progress bar in long tasks such as `housekeeping recrypt` and `grep` - Show descriptive compiler error when required features aren't selected ## 0.4.0 (2023-01-07) - Reorder commands in help output to show the most useful commands first - Add TOTP token support for handling two factor authentication codes - Add support for Steam TOTP tokens - Add `totp show` command to show a TOTP token from the store - Add `totp copy` command to copy a TOTP token to the clipboard - Add `totp live` command to watch a TOTP token live until cancelled - Add `totp qr` command to show a TOTP QR code for adding to another authenticator - Add `slam` command to aggressively close the password store, Tomb, opened GPG keys and persistent SSH connections in case of an emergency - Move `--store` property to root of `prs`, making it globally usable - For the `generate` command, add `--stdout` alias for `--show` - Automatically open Tomb when using `sync` command - Make hints shown with `prs` Tomb aware, preventing weird suggestions when a Tomb is closed - Show warning when using `git` command if sync is not initialised - Fix Tomb's not closing due to persistent SSH connections, these connections are now dropped automatically - Make interactive selection through skim full screen - Fix password generator panicing on very short/long lengths - Improve various error messages making them more descriptive - Use GnuPG binary backend by default now, rather than GPGME - Improve GNU and musl CI builds - Fix errors and warnings for Windows builds - Remove macOS builds from releases, users can compile from source - Don't publish release candidate releases on Arch AUR - Remove unsafe code for handling UTF-16 output - Remove unsafe code for signalling SSH processes - Update command-line interface handling system - Disable unused features in dependencies to shrink dependency count - Update MSRV to 1.64 - Update dependencies ## 0.3.5 (2022-08-18) - Add secret autocompletion for bash - Update MSRV to 1.60 - Fix AUR release - Update dependencies ## 0.3.4 (2022-06-20) - Fix Windows release issue - Update dependencies ## 0.3.3 (2022-06-19) - Set `GPG_TTY` environment variable in GPGME backend with `--gpg-tty` on supported platforms - Update Arch AUR packages to latest standards, make tomb an optional dependency - Resolve dependency vulnerabilities (CVE-2020-36205, CVE-2021-45707) - Bump MSRV to 1.58.1 - Update dependencies ## 0.3.2 (2021-08-30) - Fix build error when `tomb` feature is not set ## 0.3.1 (2021-08-30) - Fix `--gpg-tty` not prompting in tty if `GPG_TTY` was not set - Update dependencies ## 0.3.0 (2021-08-25) - Add `--gpg-tty` flag to instruct GPG to ask passphrases in the TTY - Partially re-enable SSH connection reuse on whitelisted hosts to speed up syncing (https://gitlab.com/timvisee/prs/-/issues/31) - Fix tomb initialisation when not forcing, ask user to force - Fix permission error when initializing Tomb through temporary store - Add crypto/GPG config for more internal configurability - Update dependencies ## 0.2.15 (2021-08-18) - Add `--merge` to `generate` command, to prevent creating a new secret - Fix error on secret generation to new file - Fix `tomb init` error when user has a large password store - Fix AUR package release - Update dependencies ## 0.2.14 (2021-07-31) - Lib: operations on `Plaintext` now borrow instead of move (https://github.com/timvisee/prs/pull/9) - Fix AUR package release - Bump MSRV to 1.53 - Update dependencies ## 0.2.13 (2021-07-09) - Fix incorrect Tomb size when resizing because it was closed first - Fix AUR package release ## 0.2.12 (2021-07-08) - Add [Tomb](https://www.dyne.org/software/tomb/) support on Linux ([info](https://github.com/timvisee/prs#what-is-tomb)) (https://gitlab.com/timvisee/prs/-/issues/36) - Add `--copy` flag to show command - Show error if user tries to generate recipient with `--no-interact` - Rename secret argument from `DEST` to `NAME` for `add` and `generate` commands - Do not scan `lost+found` directory for secrets, add to `.gitignore` - Add compile time feature flag to handle interactive selection with `skim` or `fzf` binary - Update dependencies ## 0.2.11 (2021-04-30) - Fix panic when generating command completion script to file (https://gitlab.com/timvisee/prs/-/issues/35) - Update dependencies ## 0.2.10 (2021-04-29) - Add `--no-sync` to prevent syncing and committing when making changes, this keeps the store repository dirty - Add `--allow-dirty` to make changes to the store while the repository is still dirty - Add `print` alias for `show` - Show full secret name when user query is just a partial match - Do not show interactive secret selection if no secret matched the user query - Add `--no-interact` flag to `dmenu` and `rofi` scripts - Set that `--verbose` flag does not take a value - Update dependencies ## 0.2.9 (2021-04-23) - Add `insert` alias for `add` - Fix panic when generating ZSH shell completions (https://github.com/timvisee/prs/issues/7#issuecomment-825482490) - Improved command-line argument handling, updated `clap` to `v3.0` - Update dependencies ## 0.2.8 (2021-04-22) - Add `internal completions` command to generate shell completion scripts - Output nothing from `list` command if we have no secrets - Trim end of value when selecting property from secret using `show --property` - Rename hidden `_internal` command to `internal` - Update dependencies ## 0.2.7 (2021-03-30) - Do not allow users to remove last recipient from store, which would irrecoverably void it (https://gitlab.com/timvisee/prs/-/issues/32) - Update dependencies ## 0.2.6 (2021-03-29) - Fix errors when `git` or `gpg` binary path contains spaces, notably on Windows - Update dependencies ## 0.2.5 (2021-03-22) - Disable git SSH connection reuse, until additional logic to handle failures is implemented (https://github.com/timvisee/prs/issues/5#issuecomment-803940880) - Update dependencies ## 0.2.4 (2021-03-15) - Fix error caused by unexpected output from `gpg` binary - Update dependencies ## 0.2.3 (2021-03-04) - Show tree style output for `list` command (this changes the default behaviour) - Add `-l` flag to `list` command to output as plain file list - Update dependencies ## 0.2.2 (2021-02-28) - Add `PASSWORD_STORE_DIR` environment variable to customize password store path - Fix GnuPG binary backend errors on systems providing non-English `LANG`/`LANGUAGE` value to it - Fix GnuPG binary backend reporting GPGME backend errors - Improve GnuPG binary output parsing, fall back to UTF-16 decoding - Update dependencies ## 0.2.1 (2021-02-23) - Add Wayland support - Use GnuPG binary backend by default, instead of GPGME backend - Update dependencies ## 0.2.0 (2021-02-15) - Add Windows support, and Windows release binary - Add GPGME cryptography backend for GPG support (default, compiler feature: `backend-gpgme`) - Add GnuPG binary cryptography backend for GPG support (works on Windows, compiler feature: `backend-gnupg-bin`) - Add cryptography framework, allow use of different cryptography with different backends - Fix cancelling interactive secret selection not actually cancelling the action - Use `fzf` binary instead of `skim` for interactive selection on on non-Unix platforms - Do not quit with non-zero exit code when no subcommand is given - Disable colored output on Windows for compatibility - Always enable `alias` feature by default, instead of doing this just on Unix/Windows - Update dependencies ## 0.1.7 (2021-02-01) - Always end secret output to stdout with newline - Update dependencies ## 0.1.6 (2021-01-18) - Show or copy a specific secret property with `--property` - Add optional `--timeout` flag to `show` command, output is cleared afterwards - Ask to remove pointed to secrets when removing alias secret - Don't crash on re-encrypt failure, continue and show error summary instead - Extend list of password generator characters with `[]<>(),.;|` - Require `--copy` with `--timeout` with `generate` command - Only run alias management tasks on platforms that support it - Add `dmenu` and `rofi` scripts to type selected password - Update dependencies ## 0.1.5 (2021-01-11) - Generate password instead of passphrase by default with `generate` command - Add `--length` option to `generate` command - Do not require to store generated password to secret with `generate` when `--show` or `--copy` is provided - Generate passphrase with `generate --passphrase` - Improve secret listing performance - Update dependencies ## 0.1.4 (2021-01-10) - Add alias support (https://gitlab.com/timvisee/prs/-/issues/9) _You can now easily create aliases for secrets. Aliases are symlinks under the hood, compatible with most other `pass` clients. Aliases are automatically updated when moving/removing their pointed to secret._ - Add `alias` command to create new aliases - Add `--aliases` and `--no-aliases` flags to `list` command - Add `--password` alias for `--first` in `show` command - Update dependencies ## 0.1.3 (2020-12-14) - Add dmenu and rofi quick copy scripts - Use secure directory to edit secret if possible (such as `/dev/shm`) - Improve clipboard handling on Windows, do not block console when waiting for clear timeout. - Do not try to parse git flags/options passed to `prs git [GIT]` which caused errors - Improve security description in README - Improve various user prompts - Fix crash when setting clipboard when it was previously empty - Fix error on macOS when clearing clipboard after timeout (https://gitlab.com/timvisee/prs/-/issues/8) - Update dependencies ## 0.1.2 (2020-11-09) - Fix release automation ## 0.1.1 (2020-11-08) - Update dependencies - Fix release automation ## 0.1.0 (2020-11-08) - Initial release prs-v0.5.2/Cargo.lock000066400000000000000000002760141471372304600144360ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "ansi-escapes" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10b98e9c74265950a28ae39c02e290858714c23071ab4f71c61d2908e2edfe44" [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-broadcast" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if 1.0.0", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.59.0", ] [[package]] name = "async-lock" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "rustix", "tracing", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "async-signal" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if 1.0.0", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.59.0", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "atk" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" dependencies = [ "atk-sys", "glib", "libc", ] [[package]] name = "atk-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base32" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "beef" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "build-rs" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b00b8763668c99f8d9101b8a0dd82106f58265464531a79b2cef0d9a30c17dd2" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytesize" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "cairo-rs" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ "bitflags 2.6.0", "cairo-sys-rs", "glib", "libc", "once_cell", "thiserror 1.0.68", ] [[package]] name = "cairo-sys-rs" version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ "glib-sys", "libc", "system-deps", ] [[package]] name = "cc" version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "shlex", ] [[package]] name = "cfg-expr" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chbs" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a7298287f1443f422d3f46e8ce9f855e75f0e43c06605adb4c52a262faeabd" dependencies = [ "derive_builder 0.10.2", "getrandom", "rand", "thiserror 1.0.68", ] [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.52.6", ] [[package]] name = "clap" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim 0.11.1", ] [[package]] name = "clap_complete" version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595" dependencies = [ "clap", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clipboard-win" version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" dependencies = [ "lazy-bytes-cast", "winapi", ] [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ "lazy_static", "windows-sys 0.48.0", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.52.0", ] [[package]] name = "constant_time_eq" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "conv" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" dependencies = [ "custom_derive", ] [[package]] name = "copypasta" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133fc8675ee3a4ec9aa513584deda9aa0faeda3586b87f7f0f2ba082c66fb172" dependencies = [ "clipboard-win", "objc", "objc-foundation", "objc_id", "smithay-clipboard", "x11-clipboard", ] [[package]] name = "copypasta-ext" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9455f470ea0c7d50c3fe3d22389c3a482f38a9f5fbab1c8ee368121356c56718" dependencies = [ "copypasta", "which 4.4.2", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] [[package]] name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.6.0", "crossterm_winapi", "mio", "parking_lot", "rustix", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "cstr-argument" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bd9c8e659a473bce955ae5c35b116af38af11a7acb0b480e01f3ed348aeb40" dependencies = [ "cfg-if 1.0.0", "memchr", ] [[package]] name = "custom_derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" [[package]] name = "darling" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" dependencies = [ "darling_core 0.12.4", "darling_macro 0.12.4", ] [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ "darling_core 0.14.4", "darling_macro 0.14.4", ] [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core 0.20.10", "darling_macro 0.20.10", ] [[package]] name = "darling_core" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", "syn 1.0.109", ] [[package]] name = "darling_core" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", "syn 1.0.109", ] [[package]] name = "darling_core" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", "syn 2.0.87", ] [[package]] name = "darling_macro" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" dependencies = [ "darling_core 0.12.4", "quote", "syn 1.0.109", ] [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core 0.14.4", "quote", "syn 1.0.109", ] [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", "syn 2.0.87", ] [[package]] name = "defer-drop" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" dependencies = [ "crossbeam-channel", "once_cell", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "derive_builder" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" dependencies = [ "derive_builder_macro 0.10.2", ] [[package]] name = "derive_builder" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" dependencies = [ "derive_builder_macro 0.11.2", ] [[package]] name = "derive_builder" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro 0.20.2", ] [[package]] name = "derive_builder_core" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" dependencies = [ "darling 0.12.4", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "derive_builder_core" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "derive_builder_core" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "derive_builder_macro" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" dependencies = [ "derive_builder_core 0.10.2", "syn 1.0.109", ] [[package]] name = "derive_builder_macro" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" dependencies = [ "derive_builder_core 0.11.2", "syn 1.0.109", ] [[package]] name = "derive_builder_macro" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", "syn 2.0.87", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if 1.0.0", "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "dlib" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ "libloading", ] [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "edit" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f364860e764787163c8c8f58231003839be31276e821e2ad2092ddf496b1aa09" dependencies = [ "tempfile", "which 4.4.2", ] [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "error-chain" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "version_check", ] [[package]] name = "event-listener" version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "field-offset" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset 0.9.1", "rustc_version", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "fuzzy-matcher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" dependencies = [ "thread_local", ] [[package]] name = "gdk" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" dependencies = [ "cairo-rs", "gdk-pixbuf", "gdk-sys", "gio", "glib", "libc", "pango", ] [[package]] name = "gdk-pixbuf" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ "gdk-pixbuf-sys", "gio", "glib", "libc", "once_cell", ] [[package]] name = "gdk-pixbuf-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "gdk-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gio-sys", "glib-sys", "gobject-sys", "libc", "pango-sys", "pkg-config", "system-deps", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "gethostname" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" dependencies = [ "libc", "winapi", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "gio" version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-util", "gio-sys", "glib", "libc", "once_cell", "pin-project-lite", "smallvec", "thiserror 1.0.68", ] [[package]] name = "gio-sys" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", "winapi", ] [[package]] name = "git-state" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f0f3132e2c1356595caf0f85961a7cf21cac3734d40e54fe83fcdb3dd2fcbc" [[package]] name = "glib" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", "futures-task", "futures-util", "gio-sys", "glib-macros", "glib-sys", "gobject-sys", "libc", "memchr", "once_cell", "smallvec", "thiserror 1.0.68", ] [[package]] name = "glib-macros" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "glib-sys" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", "system-deps", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", "system-deps", ] [[package]] name = "gpg-error" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545aae14d0e95734d639c8076304e6e86de765c19c76bead3648583d9caed919" dependencies = [ "libgpg-error-sys", ] [[package]] name = "gpgme" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57539732fbe58eacdb984734b72b470ed0bca3ab7a49813271878567025ac44f" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", "conv", "cstr-argument", "gpg-error", "gpgme-sys", "libc", "memoffset 0.7.1", "once_cell", "smallvec", "static_assertions", ] [[package]] name = "gpgme-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "509223d659c06e4a26229437d6ac917723f02d31917c86c6ecd50e8369741cf7" dependencies = [ "build-rs", "libc", "libgpg-error-sys", "system-deps", "winreg 0.10.1", ] [[package]] name = "gtk" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" dependencies = [ "atk", "cairo-rs", "field-offset", "futures-channel", "gdk", "gdk-pixbuf", "gio", "glib", "gtk-sys", "gtk3-macros", "libc", "pango", "pkg-config", ] [[package]] name = "gtk-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", "gio-sys", "glib-sys", "gobject-sys", "libc", "pango-sys", "system-deps", ] [[package]] name = "gtk3-macros" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "hashbrown" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "home" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core 0.52.0", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "indicatif" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", "number_prefix", "portable-atomic", "unicode-width", ] [[package]] name = "instant" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy-bytes-cast" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libgpg-error-sys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500a4cbc0816ed820a5bcf73a19e74dd6df4bedeabc0f64471c61186938b6c82" dependencies = [ "build-rs", "system-deps", "winreg 0.52.0", ] [[package]] name = "libloading" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if 1.0.0", "windows-targets 0.52.6", ] [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", ] [[package]] name = "linkify" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" dependencies = [ "memchr", ] [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mac-notification-sys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91" dependencies = [ "cc", "dirs-next", "objc-foundation", "objc_id", "time", ] [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "mio" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", "log", "wasi", "windows-sys 0.52.0", ] [[package]] name = "nix" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" dependencies = [ "bitflags 1.3.2", "cc", "cfg-if 0.1.10", "libc", "void", ] [[package]] name = "nix" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", "libc", "memoffset 0.6.5", ] [[package]] name = "nix" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", "bitflags 1.3.2", "cfg-if 1.0.0", "libc", "memoffset 0.6.5", "pin-utils", ] [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", "libc", "memoffset 0.9.1", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", "cfg_aliases", "libc", ] [[package]] name = "notify-rust" version = "4.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5134a72dc570b178bff81b01e81ab14a6fcc015391ed4b3b14853090658cd3a3" dependencies = [ "log", "mac-notification-sys", "serde", "tauri-winrt-notification", "zbus", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "number_prefix" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "objc-foundation" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ "block", "objc", "objc_id", ] [[package]] name = "objc_id" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ "objc", ] [[package]] name = "ofiles" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6d996a29712d694df329d19f0e18632466a42c9de4aab73dca087a1a4c1bd7d" dependencies = [ "error-chain", "glob", "log", "nix 0.17.0", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "pango" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" dependencies = [ "gio", "glib", "libc", "once_cell", "pango-sys", ] [[package]] name = "pango-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if 1.0.0", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", "rustix", "tracing", "windows-sys 0.59.0", ] [[package]] name = "portable-atomic" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit 0.19.15", ] [[package]] name = "proc-macro-crate" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ "toml_datetime", "toml_edit 0.20.2", ] [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "prs-cli" version = "0.5.2" dependencies = [ "ansi-escapes", "anyhow", "base64", "bytesize", "chbs", "clap", "clap_complete", "colored", "copypasta-ext", "crossterm", "derive_builder 0.20.2", "dirs-next", "edit", "fs_extra", "indicatif", "lazy_static", "linkify", "notify-rust", "prs-lib", "qr2term", "rand", "regex", "shellexpand", "shlex", "skim", "substring", "text_trees", "thiserror 2.0.1", "totp-rs", "walkdir", "which 7.0.0", "x11-clipboard", ] [[package]] name = "prs-gtk3" version = "0.5.2" dependencies = [ "anyhow", "gdk", "gio", "glib", "gtk", "notify-rust", "prs-lib", "thiserror 2.0.1", ] [[package]] name = "prs-lib" version = "0.5.2" dependencies = [ "anyhow", "fs_extra", "git-state", "gpgme", "lazy_static", "nix 0.29.0", "ofiles", "quickcheck", "quickcheck_macros", "regex", "secstr", "shellexpand", "shlex", "thiserror 2.0.1", "version-compare", "walkdir", "which 7.0.0", "zeroize", ] [[package]] name = "qr2term" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6867c60b38e9747a079a19614dbb5981a53f21b9a56c265f3bfdf6011a50a957" dependencies = [ "crossterm", "qrcode", ] [[package]] name = "qrcode" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" [[package]] name = "quick-xml" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "rand", ] [[package]] name = "quickcheck_macros" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", "thiserror 1.0.68", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secstr" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04f657244f605c4cf38f6de5993e8bd050c8a303f86aeabff142d5c7c113e12" dependencies = [ "libc", ] [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "serde_repr" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest", ] [[package]] name = "shellexpand" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" dependencies = [ "dirs", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "skim" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d28de0a6cb2cdd83a076f1de9d965b973ae08b244df1aa70b432946dda0f32" dependencies = [ "beef", "bitflags 1.3.2", "chrono", "crossbeam", "defer-drop", "derive_builder 0.11.2", "fuzzy-matcher", "lazy_static", "log", "nix 0.25.1", "rayon", "regex", "time", "timer", "tuikit", "unicode-width", "vte", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" dependencies = [ "bitflags 1.3.2", "dlib", "lazy_static", "log", "memmap2", "nix 0.24.3", "pkg-config", "wayland-client", "wayland-cursor", "wayland-protocols", ] [[package]] name = "smithay-clipboard" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" dependencies = [ "smithay-client-toolkit", "wayland-client", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "substring" version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" dependencies = [ "autocfg", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "system-deps" version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", "toml", "version-compare", ] [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri-winrt-notification" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871" dependencies = [ "quick-xml", "windows", "windows-version", ] [[package]] name = "tempfile" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if 1.0.0", "fastrand", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "text_trees" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419ea7ab2f07ffc59c5e9424bcb73677933940f8bdadbe21649c1584dc06d0cf" [[package]] name = "thiserror" version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ "thiserror-impl 1.0.68", ] [[package]] name = "thiserror" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07c1e40dd48a282ae8edc36c732cbc219144b87fb6a4c7316d611c6b1f06ec0c" dependencies = [ "thiserror-impl 2.0.1", ] [[package]] name = "thiserror-impl" version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "thiserror-impl" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874aa7e446f1da8d9c3a5c95b1c5eb41d800045252121dc7f8e0ba370cee55f5" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if 1.0.0", "once_cell", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "num-conv", "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "timer" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" dependencies = [ "chrono", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "toml" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit 0.20.2", ] [[package]] name = "toml_datetime" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "toml_edit" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "totp-rs" version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" dependencies = [ "base32", "constant_time_eq", "hmac", "sha1", "sha2", "url", "urlencoding", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "tuikit" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" dependencies = [ "bitflags 1.3.2", "lazy_static", "log", "nix 0.24.3", "term", "unicode-width", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset 0.9.1", "tempfile", "winapi", ] [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "url" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version-compare" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vte" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" dependencies = [ "arrayvec", "utf8parse", "vte_generate_state_changes", ] [[package]] name = "vte_generate_state_changes" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if 1.0.0", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wayland-client" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" dependencies = [ "bitflags 1.3.2", "downcast-rs", "libc", "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", "wayland-sys", ] [[package]] name = "wayland-commons" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", ] [[package]] name = "wayland-cursor" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ "nix 0.24.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" dependencies = [ "bitflags 1.3.2", "wayland-client", "wayland-commons", "wayland-scanner", ] [[package]] name = "wayland-scanner" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ "proc-macro2", "quote", "xml-rs", ] [[package]] name = "wayland-sys" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ "dlib", "lazy_static", "pkg-config", ] [[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", "home", "once_cell", "rustix", ] [[package]] name = "which" version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", "home", "rustix", "winsafe", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-wsapoll" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ "windows-core 0.56.0", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ "windows-implement", "windows-interface", "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-implement" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "windows-interface" version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-version" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] [[package]] name = "winreg" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if 1.0.0", "windows-sys 0.48.0", ] [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x11-clipboard" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "980b9aa9226c3b7de8e2adb11bf20124327c054e0e5812d2aac0b5b5a87e7464" dependencies = [ "x11rb", ] [[package]] name = "x11rb" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" dependencies = [ "gethostname", "nix 0.24.3", "winapi", "winapi-wsapoll", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" dependencies = [ "nix 0.24.3", ] [[package]] name = "xcursor" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xdg-home" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "xml-rs" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" [[package]] name = "yoke" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", "synstructure", ] [[package]] name = "zbus" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "derivative", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix 0.27.1", "ordered-stream", "rand", "serde", "serde_repr", "sha1", "static_assertions", "tracing", "uds_windows", "windows-sys 0.52.0", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", "regex", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "zerofrom" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn 2.0.87", ] [[package]] name = "zvariant" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] prs-v0.5.2/Cargo.toml000066400000000000000000000001021471372304600144400ustar00rootroot00000000000000[workspace] members = [ "./lib", "./cli", "./gtk3", ] prs-v0.5.2/LICENSE000066400000000000000000000773301471372304600135360ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS prs-v0.5.2/README.md000066400000000000000000000647531471372304600140150ustar00rootroot00000000000000[![Build status on GitLab CI][gitlab-ci-master-badge]][gitlab-ci-link] [![Newest release on crates.io][crate-version-badge]][crate-link] [![Project license][crate-license-badge]](LICENSE) [crate-license-badge]: https://img.shields.io/crates/l/prs-lib.svg [crate-link]: https://crates.io/crates/prs-cli [crate-version-badge]: https://img.shields.io/crates/v/prs-lib.svg [gitlab-ci-link]: https://gitlab.com/timvisee/prs/pipelines [gitlab-ci-master-badge]: https://gitlab.com/timvisee/prs/badges/master/pipeline.svg # prs > A secure, fast & convenient password manager CLI using GPG and git to sync. `prs` is a secure, fast and convenient password manager for the terminal. It features [GPG][gpg] to securely store your secrets and integrates [`git`][git] for automatic synchronization between multiple machines. It also features a built-in password generator, recipient management, history tracking, rollbacks, housekeeping utilities, Tomb support and more. [![prs usage demo][usage-demo-svg]][usage-demo-asciinema] _No demo visible here? View it on [asciinema][usage-demo-asciinema]._ `prs` is heavily inspired by [`pass`][pass] and uses the same file structure with some additions. `prs` therefore works alongside with `pass` and all other compatible clients, extensions and migration scripts. - [Features](#features) - [Usage](#usage) - [Requirements](#requirements) - [Install](#install) - [Build](#build) - [Security](#security) - [FAQ](#faq) - [Help](#help) - [License](#license) ## Features - Fully featured fast & friendly command line tool - Temporary copy secrets to clipboard - Uses the battle-tested [GPG][gpg] to secure your secrets - Automatic synchronization with [`git`][git] including history tracking - Supports multiple machines with easy recipient management - Easily edit secrets using your default editor - Supports smart aliases, property selection - Compatible with [`pass`][pass][*](#is-prs-compatible-with-pass) - Supports Linux, macOS, Windows, FreeBSD and others, supports X11 and Wayland - Supports multiple cryptography backends (more backends & crypto in the future) - Seamless [Tomb][tomb] support to prevent metadata leakage[*](#what-is-tomb) - Support for TOTP tokens for two-factor authentication - Scriptable with `-y`, `-f`, `-I` flags - Accurate & useful error reporting `prs` includes some awesome tweaks and optimizations: - Greatly improved synchronisation speed through `git` with connection reuse[*](./docs/connection-reuse.md) - Super fast interactive secret/recipient selection through [`skim`][skim] - Prevents messing with your clipboard, no unexpected overwrites or clipboard loss - When using Tomb, it is automatically opened, closed and resized for you - Commands have short and conventional aliases for faster and more convenient usage - Uses security best practices (secrets: zeroed, `mlock`, `madvice`, no format, [etc](#security)) ## Usage ```bash # Show useful commands (based on current password store state) prs # Easily add, modify and remove secrets with your default editor: prs add site/gitlab.com prs edit site/gitlab.com prs duplicate my/secret extra/secret prs alias my/secret extra/alias prs move my/secret extra/secret prs remove site/gitlab.com # Or generate a new secure password prs generate site/gitlab.com # Temporary show or copy secrets to clipboard: prs show prs show site/gitlab.com prs copy prs copy site/gitlab.com # Manually synchronize password store with remote repository or do some housekeeping prs sync prs housekeeping prs housekeeping run prs housekeeping recrypt # Manage recipients when using multiple machines prs recipients add prs recipients list prs recipients remove prs recipients generate prs recipients export # Commands support shorter/conventional commands and aliases prs a secret # add prs c # copy prs s # show prs rm # remove prs yeet # remove # List all commands and help prs help ``` ## Requirements - Linux, macOS, FreeBSD, Windows (other BSDs might work) - A terminal :sunglasses: - _And:_ #### Recommended - Run: _`git`, `gnupg`, `gpgme`_ - Ubuntu, Debian and derivatives: `apt install git gpg libgpgme11 tomb` - CentOS/Red Hat/openSUSE/Fedora: `yum install git gnupg gpgme` - Arch: `pacman -S git gnupg gpgme tomb` - Alpine: `apk add git gnupg gpgme` - macOS: `brew install gnupg gpgme` - Windows: `scoop install git gpg fzf` - Build: _`git`, `gnupg`, `gpgme` dev packages and dev utilities_ - Ubuntu, Debian and derivatives: `apt install git gpg build-essential pkg-config python3 xorg-dev libx11-xcb-dev libdbus-1-dev libgpgme-dev tomb` - CentOS/Red Hat/openSUSE/Fedora: `yum install git gnupg gpgme-devel pkgconfig python3 xorg-x11-devel libxcb-devel` - Arch: `pacman -S git gnupg gpgme pkgconf python3 xorg-server libxcb tomb` - Alpine: `apk add git gnupg gpgme-dev pkgconfig` - macOS: `brew install gnupg gpgme` - Windows: `scoop install git gpg fzf` #### Specific Specific features or crates require specific dependencies as shown below. The listed dependencies might be incorrect or incomplete. If you believe there to be an error, please feel free to contribute.
[Required] Minimal requirements - Run & build: _`gpg` and `git`_ - Ubuntu, Debian and derivatives: `apt install git gpg` - CentOS/Red Hat/openSUSE/Fedora: `yum install git gnupg` - Arch: `pacman -S git gnupg` - Alpine: `apk add git gnupg` - macOS: `brew install gpg` - Windows: `scoop install git gpg fzf`
[Recommended] Feature: GPGME backend _`--feature=backend-gpgme`_ - Run: _`gpgme` & build tools_ - Ubuntu, Debian and derivatives: `apt install libgpgme11` - CentOS/Red Hat/openSUSE/Fedora: `yum install gpgme` - Arch: `pacman -S gpgme` - Alpine: `apk add gpgme` - macOS: `brew install gpgme` - Windows: _not supported_ - Build: _`gpgme` dev package - Ubuntu, Debian and derivatives: `apt install build-essential pkg-config libgpgme-dev` - CentOS/Red Hat/openSUSE/Fedora: `yum install pkgconfig gpgme-devel` - Arch: `pacman -S pkgconf gpgme` - Alpine: `apk add pkgconfig gpgme-dev` - macOS: `brew install gpgme` - Windows: _not supported_
[Recommended] Feature: Clipboard _`--feature=clipboard`_ - Run: - Ubuntu, Debian and derivatives: `apt install xorg libx11-xcb-dev wl-clipboard` - CentOS/Red Hat/openSUSE/Fedora: `yum install pkgconfig xorg libxcb wl-clipboard` - Arch: `pacman -S pkgconf xorg-server python3 libxcb wl-clipboard` - Alpine: _?_ - macOS: _none_ - Windows: _none_ - Build: - Ubuntu, Debian and derivatives: `apt install build-essential pkg-config python3 xorg-dev libx11-xcb-dev` - CentOS/Red Hat/openSUSE/Fedora: `yum install pkgconfig python3 xorg-x11-devel libxcb-devel` - Arch: `pacman -S pkgconf xorg-server python3 libxcb` - Alpine: _?_ - macOS: _none_ - Windows: _none_ Note: `xorg`, `libx11-xcb` are only required at runtime when using X11. `wl-clipboard` are only required at runtime when using Wayland.
[Recommended] Feature: Notifications _`--feature=notify`_ - Run: - Ubuntu, Debian and derivatives: _[something][linux-notifications] supporting notifications with libnotify_ - CentOS/Red Hat/openSUSE/Fedora: _[something][linux-notifications] supporting notifications with libnotify_ - Arch: _[something][linux-notifications] supporting notifications with libnotify_ - Alpine: _[something][linux-notifications] supporting notifications with libnotify_ - macOS: _none_ - Windows: _none_ - Build: _`gpgme` dev package_ - Ubuntu, Debian and derivatives: `apt install libdbus-1-dev` - CentOS/Red Hat/openSUSE/Fedora: `yum install dbus-libs` - Arch: `pacman -S dbus` - Alpine: `apk add dbus` - macOS: _none_ - Windows: _none_
Feature: Tomb _`--feature=tomb`_ - Run: `tomb` - Ubuntu, Debian and derivatives: `apt install tomb` - CentOS/Red Hat/openSUSE/Fedora: [installation][tomb-install] - Arch: `pacman -S tomb` - Alpine: [installation][tomb-install] - macOS: _not supported_ - Windows: _not supported_
Client: GTK3 client _crate: `prs-gtk3` @ [`./gtk3`](./gtk3)_ - Run: _`gtk3`_ - Ubuntu, Debian and derivatives: `apt install libgtk-3-0 libgl1-mesa0` - CentOS/Red Hat/openSUSE/Fedora: `yum install gtk3` - Arch: `pacman -S gtk3` - Alpine: `apk add gtk+3.0 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main` - macOS: `brew install gtk+3` - Windows: _not supported_ - Build: _`gtk3` dev packages_ - Ubuntu, Debian and derivatives: `apt install libgtk-3-dev libgl1-mesa-dev` - CentOS/Red Hat/openSUSE/Fedora: `yum install gtk3-devel` - Arch: `pacman -S gtk3` - Alpine: `apk add gtk+3.0-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main` - macOS: `brew install gtk+3` - Windows: _not supported_
## Install Because `prs` is still in early stages, only limited installation options are available right now. Feel free to contribute. Make sure you meet and install the 'Run' [requirements](#requirements). See the operating system/distribution specific instructions below: - [Linux](#linux-all-distributions) - [macOS](#macos) - [Windows](#windows) - [Other](#other) ### Linux (all distributions) Limited installation options are currently available. See the list below. Alternatively you may install it manually using the [prebuilt binaries](#linux-prebuilt-binaries). Only 64-bit (`x86_64`) packages and binaries are provided. For other architectures and configurations you may [compile from source](#build). More packages options will be coming soon. #### Linux: Arch AUR packages [» `prs`](https://aur.archlinux.org/packages/prs/) (compiles from source, latest release) [» `prs-git`](https://aur.archlinux.org/packages/prs-git/) (compiles from source, latest `master` commit) ```bash yay -S prs # or aurto add prs sudo pacman -S prs # or using any other AUR helper prs --help ``` #### Linux: Prebuilt binaries Check out the [latest release][github-latest-release] assets for Linux binaries. Use the `prs-v*-linux-x64-static` binary, to minimize the chance for issues. If it isn't available yet, you may use an artifact from a [previous version][github-releases] instead, until it is available. Make sure you meet and install the [requirements](#requirements) before you continue. You must make the binary executable, and may want to move it into `/usr/local/bin` to make it easily executable: ```bash # Rename binary to prs mv ./prs-* ./prs # Mark binary as executable chmod a+x ./prs # Move binary into path, to make it easily usable sudo mv ./prs /usr/local/bin/ prs ``` ### macOS `prs` can be installed using [homebrew]. Alternatively you may [compile from source](#build). Make sure you've [`homebrew`][homebrew-install] installed, then run: ```bash brew install prs prs ``` _Note: this package isn't automatically updated on release, feel free to contribute [here](https://github.com/Homebrew/homebrew-core/blob/master/Formula/prs.rb)._ ### Windows Using the [`scoop` package](#windows-scoop-package) is recommended. Alternatively you may install it manually using the [prebuilt binaries](#windows-prebuilt-binaries). If you're using the [Windows Subsystem for Linux][wsl], it's highly recommended to install the [prebuilt Linux binary](#prebuilt-binaries-for-linux) instead. Only 64-bit (`x86_64`) binaries are provided. For other architectures and configurations you may [compile from source](#build). #### Windows: scoop package Make sure you've [`scoop`][scoop-install] installed, then run: ```bash scoop install prs prs ``` #### Windows: Prebuilt binaries Check out the [latest release][github-latest-release] assets for Windows binaries. Use the `prs-v*-windows` binary. If it isn't available yet, you may use an artifact from a [previous version][github-releases] instead, until it is available. You can use `prs` from the command line in the same directory: ```cmd .\prs.exe ``` To make it globally invokable as `prs`, you must make the binary available in your systems `PATH`. #### Other Find the latest binaries on the latest release page: - [GitHub][github-release-latest] - [GitLab][gitlab-releases] - [GitLab package registry][gitlab-packages] for `prs` _Note: for Linux the GNU (not musl) binary is recommended if it works, because it has better clipboard/notification support._ ```bash # download binary from any source above # make executable chmod a+x ./prs # optional: make globally executable mv ./prs /usr/local/bin/prs ./prs --help ``` ## Build To build and install `prs` yourself, you need the following: - Rust 1.65 or newer (MSRV) - The 'Build' [requirements](#requirements). _Not all features are supported on macOS or Windows. The default configuration should work. When changing compile time features, make sure to check for compatibility. See [compiler features](#compile-features--use-flags)._ ### Compile and install To compile and install `prs` with the default features follow these steps: - Compile and install it directly from cargo: ```bash # Compile and install from cargo cargo install prs-cli -f # Start using prs prs --help ``` - Or clone the repository and install it with `cargo`: ```bash # Clone the project git clone https://github.com/timvisee/prs.git cd prs # Compile and install cargo install --path cli -f # Start using prs prs --help # or run it directly from cargo cargo run --release --package prs-cli -- --help # or invoke release binary directly ./target/release/prs --help ``` ### Compile features / use flags Different use flags are available for `prs` to toggle whether to include various features and cryptography backends. The following features are available, some of which are enabled by default: | Feature | In | Enabled | Description | | :-----------------: | :-------------------: | :-----: | :--------------------------------------------------------- | | `alias` | `prc-cli` | Default | Support for secret aliases (partially supported on Windows)| | `clipboard` | `prs-cli` | Default | Clipboard support: copy secret to clipboard | | `notify` | `prs-cli`, `prs-gtk3` | Default | Notification support: notify on clipboard clear | | `tomb` | _all_ | Default | Tomb support for password store (only supported on Linux) | | `totp` |`prs-cli` | Default | TOTP token support for 2FA | | `backend-gpgme` | _all_ | | GPG crypto backend using GPGME (not supported on Windows) | | `backend-gnupg-bin` | _all_ | Default | GPG crypto backend using GnuPG binary | | `select-skim` | `prc-cli` | Default | Interactive selection with skim (ignored on Windows) | | `select-skim-bin` | `prs-cli` | | Interactive selection through external `skim` binary | | `select-fzf-bin` | `prs-cli` | Default | Interactive selection through external `fzf` binary | To enable features during building or installation, specify them with `--features ` when using `cargo`. You may want to disable default features first using `--no-default-features`. Here are some examples: ```bash # Default set of features with backend-gnupg-bin, install or build, one of cargo install --path cli --features backend-gnupg-bin cargo build --path cli --release --features backend-gnupg-bin # No default features, except required, one of cargo install --path cli --no-default-features --features backend-gpgme cargo install --path cli --no-default-features --features backend-gnupg-bin # With alias, clipboard and notification support, one of cargo install --path cli --no-default-features --features backend-gpgme,alias,clipboard,notify cargo install --path cli --no-default-features --features backend-gnupg-bin,alias,clipboard,notify ``` ## Security Security is backed by [`gpg`][gpg] which is used all over the world and has been battle-tested for more than 20 years. In summary, `prs` is secure to keep your deepest secrets when assuming the following: - You keep the password store directory (`~/.password-store`) safe - When using sync with `git`: you keep your remote repository safe - You use secure GPG keys - Your machines are secure The content of secrets is encrypted and secured. Secrets are stored as encrypted GPG files. Some metadata is visible without decryption however. The name of a secret (file name), modification time (file modification time) and encrypted size (file size) are visible when you have access to the password store directory. To protect against this metadata leakage you may use a [Tomb][tomb-faq]. Security best practices are used in `prs` to prevent accidentally leaking any secret data. Sensitive data such as plaintext, ciphertext and others are referred to as 'secret' here. Secrets are/use: - Zeroed on drop - Locked to physical memory, cannot leak to swap/disk ([`mlock`][security-mlock]) - Locked into memory, cannot be dumped/not included in core ([`madvice`][security-madvice]) - Not written to disk to edit (if possible) - String formatting is blocked - Constant time comparison to prevent time based attacks - Minimally cloned [security-mlock]: https://man7.org/linux/man-pages/man2/mlock.2.html [security-madvice]: https://man7.org/linux/man-pages/man2/madvise.2.html The protection against leaking secrets has its boundaries, notably: - `prs show` prints secret data to stdout - `prs edit` may store secrets in a secure temporary file on disk if secure locations such as (`/dev/shm`) are not available, it then opens it in your default editor, and removes it afterwards - `prs copy` copies secret data to your clipboard, and clears it after 20 seconds [![Security](./res/xkcd_538.png)][xkcd538] _Reference: [XKCD 538][xkcd538]_ Note: `prs` does not provide any warranty in any way, shape or form for damage due to leaked secrets or other issues. ## FAQ #### Is `prs` secure? How secure is `prs`? Please read the [Security](#security) section. #### How do I use sync with git? If you already have a remote password store repository that is [compatible](#is-prs-compatible-with-pass) with `prs`, clone it using: ```bash # Clone existing remote password store, automatically enables sync prs clone MY_GIT_URL # List secrets prs list ``` If you do not have a remote password store repository yet, create one (an empty private repository on GitHub or GitLab for example), and run the following: ```bash # Initialize new password store (if you haven't done so yet) prs init # Initialize sync functionality (if you haven't done so yet) prs sync init # Set your remote repository URL and sync to push your password store prs sync remote MY_GIT_URL prs sync ``` When sync is enabled on your password store, all commands that modify your secrets will automatically keep your remote store in sync. To manually trigger a sync because you edited a secret on a different machine, run: ```bash prs sync ``` #### How do I use `prs` on multiple machines and sync between them? _Note: adding and using your existing password store on a new/additional machine requires you to have access to a machine that already uses the store during setup._ First, you must have a password store on one machine. Create one (with `prs init`) if you don't have any yet. You must set up sync with a remote git repository for this passwords store, see the [How do I use sync with git](#how-do-i-enable-sync-with-git) section. To use your existing password store on a new machine, install `prs` and clone your remote password store: ```bash # On new machine: clone existing password store from git remote prs clone MY_GIT_URL ``` Then add a recipient to the password store for your new machine. I highly recommend to use a new recipient (GPG key pair) for each machine (so you won't have to share secret GPG keys). Add an existing secret GPG key as recipient, or generate a new GPG key pair, using: ```bash # On new machine: add existing recipient or generate new one prs recipients add --secret # or prs recipients generate ``` Your new machine can't read any password store secrets yet, because they are not encrypted for its recipient yet. Go back to an existing machine you already use the store on, and re-encrypt all secrets to also encrypt them for the new recipient: ```bash # On existing machine: re-encrypt all secrets prs housekeeping recrypt --all ``` This may take a while. Once done, sync on your new machine to pull in the updated secrets: ```bash # On new machine: pull in all re-crypted secrets prs sync # You're done! prs list ``` #### How do I use `prs` on mobile? `prs` itself does not support mobile, but there are compatible clients you can use to use your password store on mobile. See [Compatible Clients][pass-compatible-clients] on `pass`s website. #### Can I recover my secrets if I lost my key? No, if you lose all keys, there is no way to recover your secrets. You might lose your key (recipient, GPG secret key) if your machine crashes or if you reinstall its operating system. If you are using the same password store on multiple machines with git sync, you can still read the secrets on your other machines. To re-add the machine you lost your key on, remove the password store from it and see [this](#how-do-i-use-prs-on-multiple-machines-and-sync-between-them) section. #### What is Tomb? [Tomb][tomb] is a file encryption system. It can be used with `prs` to protect against metadata leakage of your password store. When using Tomb with `prs`, your password store is stored inside an encrypted file. `prs` automatically opens and closes your password store Tomb for you as needed. This makes it significantly harder for malicious programs to list your password store contents. This feature is inspired by [`pass-tomb`](https://github.com/roddhjav/pass-tomb), which is a `pass` extension for Tomb support. In `prs` this functionality is built-in. _Note: Tomb is only supported on Linux._ #### How to use Tomb? `prs` has built-in support for [Tomb][tomb] on Linux systems. Please make sure `prs` is compiled with the `tomb` [feature](#compile-features--use-flags), and that Tomb is installed. To initialize a Tomb for your current password store, simply invoke: ```bash # Initialize tomb, this may take some time prs tomb init # Read tomb status prs tomb status ``` To initialize a new password store in a Tomb, first initialize the password store then initialize the Tomb: ```bash # Initialize new password store prs init # ... # Initialize tomb, this may take some time prs tomb init ``` If you already have a Tomb created with `pass-tomb`, no action is required. `prs` has seamless support for it, and it should automatically manage it for you. Invoke `prs tomb status` to confirm it is detected. #### How to use Tomb on multiple machines? A Tomb is local on your machine and is not synced. To use a Tomb on multiple machines you must initialize it on each of them. Simply run `prs tomb init` on machines you don't use a Tomb on yet, and after cloning your password store on a new machine. #### Is `prs` compatible with `pass`? Yes. `prs` uses the same file structure as [`pass`][pass]. Other `pass` clients should be able to view and edit your secrets. `prs` does add additional files and settings, some `prs` features may not work with other `pass` clients. While the backing file structure is compatible, the command-line interface is not and differs from `pass`. This is to remove ambiguity and to improve overall usability. See a list of compatible `pass` clients [here][pass-compatible-clients]. ## Help ``` $ prs help prs 0.5.2 Tim Visee <3a4fb3964f@sinenomine.email> Secure, fast & convenient password manager CLI with GPG & git sync Usage: prs [OPTIONS] [COMMAND] Commands: show Display a secret copy Copy secret to clipboard generate Generate a secure secret add Add a secret edit Edit a secret duplicate Duplicate a secret alias Alias/symlink a secret move Move a secret remove Remove a secret list List all secrets grep Grep all secrets init Initialize new password store clone Clone existing password store sync Sync password store slam Aggressively lock password store & keys preventing access (emergency) totp Manage TOTP tokens recipients Manage store recipients git Invoke git command in password store tomb Manage password store Tomb housekeeping Housekeeping utilities help Print this message or the help of the given subcommand(s) Options: -f, --force Force the action, ignore warnings -I, --no-interact Not interactive, do not prompt -y, --yes Assume yes for prompts -q, --quiet Produce output suitable for logging and automation -v, --verbose... Enable verbose information and logging -s, --store Password store to use [env: PASSWORD_STORE_DIR=] --gpg-tty Instruct GPG to ask passphrase in TTY rather than pinentry -h, --help Print help -V, --version Print version ``` ## License This project is released under the GNU GPL-3.0 license. Check out the [LICENSE](LICENSE) file for more information. The library portion of this project is licensed under the GNU LGPL-3.0 license. Check out the [lib/LICENSE](lib/LICENSE) file for more information. [git]: https://git-scm.com/ [github-latest-release]: https://github.com/timvisee/prs/releases/latest [github-release-latest]: https://github.com/timvisee/prs/releases/latest [github-releases]: https://github.com/timvisee/prs/releases [gitlab-packages]: https://gitlab.com/timvisee/prs/-/packages [gitlab-releases]: https://gitlab.com/timvisee/prs/-/releases [gpg]: https://gnupg.org/ [homebrew]: https://brew.sh/ [homebrew-install]: https://brew.sh/#install [linux-notifications]: https://wiki.archlinux.org/index.php/Desktop_notifications [pass-compatible-clients]: https://www.passwordstore.org#other [pass]: https://www.passwordstore.org/ [scoop-install]: https://scoop.sh/#installs-in-seconds [skim]: https://github.com/lotabout/skim [tomb-faq]: #what-is-tomb [tomb-install]: https://github.com/dyne/Tomb/blob/master/INSTALL.md [tomb]: https://www.dyne.org/software/tomb/ [usage-demo-asciinema]: https://asciinema.org/a/368611 [usage-demo-svg]: ./res/demo.svg [wsl]: https://docs.microsoft.com/en-us/windows/wsl/install-win10 [xkcd538]: https://xkcd.com/538/ prs-v0.5.2/ci/000077500000000000000000000000001471372304600131125ustar00rootroot00000000000000prs-v0.5.2/ci/gpgme/000077500000000000000000000000001471372304600142115ustar00rootroot00000000000000prs-v0.5.2/ci/gpgme/build000077500000000000000000000025321471372304600152400ustar00rootroot00000000000000#!/bin/bash set -xe if [ -z "$TARGET" ]; then echo "TARGET is not set"; exit 1 fi # Build libgpg-error export LIBGPG_ERROR_VER=1.39 cd /usr/src curl -sSL "https://www.gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-${LIBGPG_ERROR_VER}.tar.bz2" | tar -xj cd libgpg-error-$LIBGPG_ERROR_VER ./configure --host "$TARGET" --prefix="$PREFIX" --with-pic --enable-fast-install --disable-dependency-tracking --enable-static --disable-shared --disable-nls --disable-doc --disable-languages --disable-tests make -j$(nproc) install # Build libassuan export LIBASSUAN_VER=2.5.3 cd /usr/src curl -sSL "https://www.gnupg.org/ftp/gcrypt/libassuan/libassuan-${LIBASSUAN_VER}.tar.bz2" | tar -xj cd "libassuan-$LIBASSUAN_VER" ./configure --host "$TARGET" --prefix="$PREFIX" --with-pic --enable-fast-install --disable-dependency-tracking --enable-static --disable-shared --disable-doc --with-gpg-error-prefix="$PREFIX" make -j$(nproc) install # Build gpgme export GPGME_VER=1.14.0 cd /usr/src curl -sSL "https://www.gnupg.org/ftp/gcrypt/gpgme/gpgme-${GPGME_VER}.tar.bz2" | tar -xj cd "gpgme-$GPGME_VER" ./configure --host "$TARGET" --prefix="$PREFIX" --with-pic --enable-fast-install --disable-dependency-tracking --enable-static --disable-shared --disable-languages --disable-gpg-test --with-gpg-error-prefix="$PREFIX" --with-libassuan-prefix="$PREFIX" make -j$(nproc) install prs-v0.5.2/cli/000077500000000000000000000000001471372304600132665ustar00rootroot00000000000000prs-v0.5.2/cli/Cargo.toml000066400000000000000000000077251471372304600152310ustar00rootroot00000000000000[package] name = "prs-cli" version = "0.5.2" authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"] license = "GPL-3.0" readme = "../README.md" homepage = "https://timvisee.com/projects/prs" repository = "https://gitlab.com/timvisee/prs" description = "Secure, fast & convenient password manager CLI with GPG & git sync" keywords = ["pass", "passwordstore"] categories = [ "authentication", "command-line-utilities", "cryptography", ] edition = "2018" rust-version = "1.81.0" default-run = "prs" [[bin]] name = "prs" path = "./src/main.rs" [features] default = ["backend-gnupg-bin", "alias", "clipboard", "notify", "select-skim", "select-fzf-bin", "tomb", "totp"] ### Regular features # Option (default): alias management (symlink) support alias = [] # Option (default): clipboard support (copy password to clipboard) clipboard = ["copypasta-ext", "x11-clipboard", "base64"] # Option (default): notification support (clipboard notifications) notify = ["notify-rust"] # Option (default): tomb support for password store on Linux tomb = ["prs-lib/tomb", "bytesize", "fs_extra"] # Option (default): TOTP token support totp = ["totp-rs", "linkify", "qr2term"] ### Pluggable cryptography backends # Option: GnuPG cryptography backend using GPGME backend-gpgme = ["prs-lib/backend-gpgme"] # Option (default): GnuPG cryptography backend using gpg binary backend-gnupg-bin = ["prs-lib/backend-gnupg-bin"] ### Pluggable interactive selection systems # Option (default): interactive selection with skim (ignored on Windows) select-skim = ["skim"] # Option: interactive selection with skim binary select-skim-bin = [] # Option: interactive selection with fzf binary select-fzf-bin = [] [dependencies] ansi-escapes = "0.2" anyhow = "1.0" chbs = "0.1" clap = { version = "4.1", default-features = false, features = ["std", "help", "suggestions", "color", "usage", "cargo", "env"] } clap_complete = "4.1" colored = "2.0" crossterm = { version = "0.28", default-features = false, features = ["events", "windows"] } derive_builder = "0.20" edit = "0.1" indicatif = "0.17" lazy_static = "1.4" prs-lib = { version = "=0.5.2", path = "../lib", default-features = false } rand = { version = "0.8", default-features = false, features = ["std"] } regex = { version = "1.7", default-features = false, features = ["std", "unicode-perl"] } shellexpand = "3.0" shlex = "1.3" substring = "1.4.5" text_trees = "0.1" thiserror = "2.0" walkdir = "2.3" which = "7.0" # Notification support notify-rust = { version = "4.7", optional = true } # Tomb support bytesize = { version = "1.1", optional = true } fs_extra = { version = "1.2", optional = true } # TOTP support totp-rs = { version = "5.5", optional = true, default-features = false, features = ["otpauth", "steam"] } linkify = { version = "0.10", optional = true } qr2term = { version = "0.3", optional = true } # Clipboard support base64 = { version = "0.22", optional = true } # Clipboard support for non-X11/Wayland [target.'cfg(not(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten")))))'.dependencies] copypasta-ext = { version = "0.4.1", optional = true, default-features = false } # Clipboard support for X11/Wayland [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten")), not(target_env = "musl")))'.dependencies] copypasta-ext = { version = "0.4.1", optional = true, default-features = false, features = ["wayland-bin"] } x11-clipboard = { version = "0.7", optional = true } # Clipboard support for X11/Wayland musl [target.'cfg(all(unix, not(any(target_os="macos", target_os="android", target_os="emscripten")), target_env = "musl"))'.dependencies] copypasta-ext = { version = "0.4.1", optional = true, default-features = false, features = ["x11-bin", "wayland-bin"] } # Interactive selection with skim on unix platforms [target.'cfg(unix)'.dependencies] skim = { version = "0.10", optional = true, default-features = false } # Directory logic on Windows [target.'cfg(windows)'.dependencies] dirs-next = "2.0" prs-v0.5.2/cli/build.rs000066400000000000000000000007111471372304600147320ustar00rootroot00000000000000fn main() { // Warn at compiletime if no interactive selection tool is configured #[cfg(all( not(all(feature = "select-skim", unix)), not(feature = "select-skim-bin"), not(feature = "select-fzf-bin"), ))] { println!("cargo:warning=no interactive select mode features configured, falling back to basic mode"); println!("cargo:warning=use any of: select-skim, select-skim-bin, select-fzf-bin"); } } prs-v0.5.2/cli/src/000077500000000000000000000000001471372304600140555ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/000077500000000000000000000000001471372304600153325ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/add.rs000066400000000000000000000076641471372304600164450ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Plaintext, Secret, Store}; use thiserror::Error; use crate::cmd::matcher::{add::AddMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, edit, error, stdin, sync}; /// Add secret action. pub struct Add<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Add<'a> { /// Construct a new add action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the add action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_add = AddMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); let name = matcher_add.name(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_add.allow_dirty()); if !matcher_add.no_sync() { sync.prepare()?; } // Normalize destination path let path = store .normalize_secret_path(name, None, true) .map_err(Err::NormalizePath)?; let secret = Secret::from(&store, path.clone()); let mut plaintext = Plaintext::empty(); if matcher_add.stdin() { plaintext = stdin::read_plaintext(!matcher_main.quiet())?; } else if !matcher_add.empty() { if let Some(changed) = edit::edit(&plaintext).map_err(Err::Edit)? { plaintext = changed; } } // Check if destination already exists if not forcing if !matcher_main.force() && path.is_file() { eprintln!("A secret at '{}' already exists", path.display(),); if !cli::prompt_yes("Overwrite?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Addition cancelled"); } error::quit(); } } // Confirm if empty secret should be stored if !matcher_main.force() && !matcher_add.empty() && plaintext.is_empty() && !cli::prompt_yes("Secret is empty. Add?", Some(true), &matcher_main) { error::quit(); } // Encrypt and write changed plaintext // TODO: select proper recipients (use from current file?) let recipients = store.recipients()?; crate::crypto::context(&matcher_main)? .encrypt_file(&recipients, plaintext, &path) .map_err(Err::Write)?; // Finalize sync if !matcher_add.no_sync() { sync.finalize(format!("Add secret to {}", secret.name))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret added"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to normalize destination path")] NormalizePath(#[source] anyhow::Error), #[error("failed to edit secret in editor")] Edit(#[source] anyhow::Error), #[error("failed to write changed secret")] Write(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/alias.rs000066400000000000000000000133011471372304600167670ustar00rootroot00000000000000use std::fs; use std::path::{Path, PathBuf}; use anyhow::Result; use clap::ArgMatches; use prs_lib::{Secret, Store}; use thiserror::Error; use crate::cmd::matcher::{alias::AliasMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, error, select, sync}; /// Alias secret action. pub struct Alias<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Alias<'a> { /// Construct a new alias action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the alias action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_alias = AliasMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_alias.allow_dirty()); if !matcher_alias.no_sync() { sync.prepare()?; } let secret = select::store_select_secret(&store, matcher_alias.query()).ok_or(Err::NoneSelected)?; let dest = matcher_alias.destination(); // TODO: show secret name if not equal to query, unless quiet? // Normalize dest path let path = store .normalize_secret_path(dest, secret.path.file_name().and_then(|p| p.to_str()), true) .map_err(Err::NormalizePath)?; let link_secret = Secret::from(&store, path.clone()); // Check if destination already exists if not forcing if !matcher_main.force() && path.is_file() { eprintln!("A secret at '{}' already exists", path.display(),); if !cli::prompt_yes("Overwrite?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Alias cancelled"); } error::quit(); } // Remove existing file so we can overwrite fs::remove_file(&path).map_err(Err::RemoveExisting)?; } // Create alias create_alias(&store, &secret, &path, &path)?; // Finalize sync if !matcher_alias.no_sync() { sync.finalize(format!( "Alias from {} to {}", secret.name, link_secret.name ))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret aliased"); } Ok(()) } } /// Create an alias. /// /// Create an alias (symlink) file at `place_at` for a symlink at `dst` which points to `src`. /// /// `dst` and `place_at` are usually the same. /// This may be different to use the correct relative symlink path for a secret at `place_at` that /// will be moved to `dst` in the future. pub fn create_alias(store: &Store, src: &Secret, dst: &Path, place_at: &Path) -> Result<(), Err> { create_symlink(secret_link_path(store, src, dst)?, place_at) } /// Create a symlink. /// /// Create an symlink file at `dst` which points to `src`. fn create_symlink(src: P, dst: Q) -> Result<(), Err> where P: AsRef, Q: AsRef, { #[cfg(unix)] { std::os::unix::fs::symlink(src, dst).map_err(Err::Symlink) } #[cfg(windows)] { std::os::windows::fs::symlink_file(src, dst).map_err(Err::Symlink) } } /// Determine symlink path to use. /// /// This function determines what path to provide when creating a symlink at `dst`, which links to /// `src`. fn secret_link_path(store: &Store, src: &Secret, dst: &Path) -> Result { let target = src .relative_path(&store.root) .map_err(|_| Err::UnknownRoot)?; let depth = path_depth(store, dst)?; // Build and return path let mut path = PathBuf::from("."); for _ in 0..depth { path = path.join(".."); } Ok(path.join(target)) } /// Find path depth in the given store. /// /// Finds the depth (in matter of directories) of a secret path in the given store. /// /// Returns an error if the depth could not be determined, possibly because the given file is not /// in the given root. /// /// Returns `0` if the given secret is in the store root. fn path_depth(store: &Store, mut path: &Path) -> Result { let mut depth = 0; while let Some(parent) = path.parent() { path = parent; if store.root == path { return Ok(depth); } depth += 1; } Err(Err::UnknownRoot) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to normalize destination path")] NormalizePath(#[source] anyhow::Error), #[error("failed to symlink secret file")] Symlink(#[source] std::io::Error), #[error("failed to remove existing file to overwrite")] RemoveExisting(#[source] std::io::Error), #[error("failed to determine secret path relative to store root")] UnknownRoot, } prs-v0.5.2/cli/src/action/clone.rs000066400000000000000000000066621471372304600170120ustar00rootroot00000000000000use std::fs; use std::path::Path; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto, Store}; use thiserror::Error; use crate::cmd::matcher::{clone::CloneMatcher, MainMatcher, Matcher}; use crate::util::{self, style}; /// Clone store action. pub struct Clone<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Clone<'a> { /// Construct a new clone action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the clone action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_clone = CloneMatcher::with(self.cmd_matches).unwrap(); let path = matcher_main.store(); let path = shellexpand::full(&path).map_err(Err::ExpandPath)?; util::fs::ensure_dir_free(Path::new(path.as_ref()))?; // Create store dir, open it and clone fs::create_dir_all(path.as_ref()).map_err(Err::Init)?; let store = Store::open(path.as_ref()).map_err(Err::Store)?; let sync = store.sync(); sync.clone(matcher_clone.git_url(), matcher_main.quiet()) .map_err(Err::Clone)?; // Import repo recipients missing in keychain crypto::store::import_missing_keys_from_store(&store).map_err(Err::ImportRecipients)?; // Run housekeeping crate::action::housekeeping::run::housekeeping(&store, true, false) .map_err(Err::Housekeeping)?; // Check whether the store has any key we own the secret for, default to false let store_has_our_secret = store .recipients() .and_then(|recipients| crypto::recipients::contains_own_secret_key(&recipients)) .unwrap_or(false); // Hint user to add our recipient key if !matcher_main.quiet() { if !store_has_our_secret { let bin = util::bin_name(); let config = crate::crypto::config(&matcher_main); let system_has_secret = crypto::util::has_private_key(&config).unwrap_or(true); if system_has_secret { println!("Now add your own key as recipient or generate a new one:"); } else { println!("Now generate and add a new recipient key for yourself:"); } if system_has_secret { println!( " {}", style::highlight(format!("{bin} recipients add --secret")) ); } println!( " {}", style::highlight(format!("{bin} recipients generate")) ); println!(); } else { eprintln!("Store cloned"); } } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to expand store path")] ExpandPath(#[source] shellexpand::LookupError), #[error("failed to initialize store")] Init(#[source] std::io::Error), #[error("failed to access initialized password store")] Store(#[source] anyhow::Error), #[error("failed to clone remote store")] Clone(#[source] anyhow::Error), #[error("failed to import store recipients")] ImportRecipients(#[source] anyhow::Error), #[error("failed to run housekeeping tasks")] Housekeeping(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/copy.rs000066400000000000000000000053701471372304600166570ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; use crate::cmd::matcher::{copy::CopyMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{clipboard, secret, select}; /// Copy secret to clipboard action. pub struct Copy<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Copy<'a> { /// Construct a new copy action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the copy action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_copy = CopyMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_copy.query()).ok_or(Err::NoneSelected)?; secret::print_name(matcher_copy.query(), &secret, &store, matcher_main.quiet()); let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to property or first line if let Some(property) = matcher_copy.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } else if !matcher_copy.all() { plaintext = plaintext.first_line()?; } clipboard::copy_plaintext( plaintext, false, !matcher_main.force(), matcher_main.quiet(), matcher_main.verbose(), matcher_copy.timeout()?, )?; // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/duplicate.rs000066400000000000000000000067021471372304600176570ustar00rootroot00000000000000use std::fs; use anyhow::Result; use clap::ArgMatches; use prs_lib::{Secret, Store}; use thiserror::Error; use crate::cmd::matcher::{duplicate::DuplicateMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, error, select, sync}; /// Duplicate secret action. pub struct Duplicate<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Duplicate<'a> { /// Construct a new duplicate action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the duplicate action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_duplicate = DuplicateMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_duplicate.allow_dirty()); if !matcher_duplicate.no_sync() { sync.prepare()?; } let secret = select::store_select_secret(&store, matcher_duplicate.query()) .ok_or(Err::NoneSelected)?; let dest = matcher_duplicate.destination(); // TODO: show secret name if not equal to query, unless quiet? // Normalize dest path let path = store .normalize_secret_path(dest, secret.path.file_name().and_then(|p| p.to_str()), true) .map_err(Err::NormalizePath)?; let new_secret = Secret::from(&store, path.clone()); // Check if destination already exists if not forcing if !matcher_main.force() && path.is_file() { eprintln!("A secret at '{}' already exists", path.display(),); if !cli::prompt_yes("Overwrite?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Duplication cancelled"); } error::quit(); } } // Copy secret fs::copy(&secret.path, path).map_err(Err::Copy)?; // Finalize sync if !matcher_duplicate.no_sync() { sync.finalize(format!( "Duplicate from {} to {}", secret.name, new_secret.name ))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret duplicated"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to normalize destination path")] NormalizePath(#[source] anyhow::Error), #[error("failed to copy secret file")] Copy(#[source] std::io::Error), } prs-v0.5.2/cli/src/action/edit.rs000066400000000000000000000075241471372304600166350ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; use crate::cmd::matcher::{edit::EditMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, edit, error, secret, select, stdin, sync}; /// Edit secret plaintext action. pub struct Edit<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Edit<'a> { /// Construct a new edit action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the edit action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_edit = EditMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_edit.allow_dirty()); if !matcher_edit.no_sync() { sync.prepare()?; } let secret = select::store_select_secret(&store, matcher_edit.query()).ok_or(Err::NoneSelected)?; secret::print_name(matcher_edit.query(), &secret, &store, matcher_main.quiet()); let mut context = crate::crypto::context(&matcher_main)?; let mut plaintext = context.decrypt_file(&secret.path).map_err(Err::Read)?; if matcher_edit.stdin() { plaintext = stdin::read_plaintext(!matcher_main.quiet())?; } else { plaintext = match edit::edit(&plaintext).map_err(Err::Edit)? { Some(changed) => changed, None => { if !matcher_main.quiet() { eprintln!("Secret is unchanged"); } error::quit(); } }; } // Confirm if empty secret should be stored if !matcher_main.force() && plaintext.is_empty() && !cli::prompt_yes("Edited secret is empty. Save?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Secret is unchanged"); } error::quit(); } // Encrypt and write changed plaintext // TODO: select proper recipients (use from current file?) let recipients = store.recipients()?; context .encrypt_file(&recipients, plaintext, &secret.path) .map_err(Err::Write)?; // Finalize sync if !matcher_edit.no_sync() { sync.finalize(format!("Edit secret {}", secret.name))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret updated"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to edit secret in editor")] Edit(#[source] anyhow::Error), #[error("failed to write changed secret")] Write(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/generate.rs000066400000000000000000000177701471372304600175060ustar00rootroot00000000000000use std::path::PathBuf; use anyhow::Result; use chbs::{config::BasicConfig, prelude::*}; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Plaintext, Secret, Store}; use thiserror::Error; use crate::cmd::matcher::{generate::GenerateMatcher, MainMatcher, Matcher}; #[cfg(feature = "clipboard")] use crate::util::clipboard; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, edit, error, pass, secret, select, stdin, sync}; /// Generate secret action. pub struct Generate<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Generate<'a> { /// Construct a new generate action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the generate action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_generate = GenerateMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Select existing secret if merging, select based on path normally let dest: Option<(PathBuf, Secret)> = if matcher_generate.merge() { // Prepare store sync sync::ensure_ready(&sync, matcher_generate.allow_dirty()); if !matcher_generate.no_sync() { sync.prepare()?; } // Select secret let secret = select::store_select_secret(&store, matcher_generate.name().map(|s| s.to_owned())) .ok_or(Err::NoneSelected)?; Some((secret.path.clone(), secret)) } else { match matcher_generate.name() { Some(dest) => { let path = store .normalize_secret_path(dest, None, true) .map_err(Err::NormalizePath)?; let secret = Secret::from(&store, path.clone()); // Prepare store sync sync::ensure_ready(&sync, matcher_generate.allow_dirty()); if !matcher_generate.no_sync() { sync.prepare()?; } Some((path, secret)) } None => None, } }; // Generate secure password/passphrase plaintext let mut context = crate::crypto::context(&matcher_main)?; let mut plaintext = generate_password(&matcher_generate); // If destination already exists, merge if let Some(dest) = &dest { // Ask whether to merge let exists = dest.0.is_file(); if !matcher_main.force() && !matcher_generate.merge() && exists { eprintln!("A secret at '{}' already exists", dest.0.display(),); if !cli::prompt_yes("Merge?", Some(true), &matcher_main) { if !matcher_main.quiet() { eprintln!("No secret generated"); } error::quit(); } } // Append existing secret except first line to new secret if exists { let existing = context .decrypt_file(&dest.0) .and_then(|p| p.except_first_line()) .map_err(Err::Read)?; if !existing.is_empty() { plaintext.append(existing, true); } } } // Append from stdin if matcher_generate.stdin() { let extra = stdin::read_plaintext(!matcher_main.quiet())?; plaintext.append(extra, true); } // Edit in editor if matcher_generate.edit() { // Quietly copy generated password to clipboard before editing #[cfg(feature = "clipboard")] if matcher_generate.copy() { clipboard::copy_plaintext( plaintext.clone(), true, !matcher_main.force(), !matcher_main.verbose(), matcher_main.verbose(), matcher_generate.timeout()?, )?; } if let Some(changed) = edit::edit(&plaintext).map_err(Err::Edit)? { plaintext = changed; } } // Confirm if empty secret should be stored if !matcher_main.force() && plaintext.is_empty() && !cli::prompt_yes( "Generated secret is empty. Save?", Some(true), &matcher_main, ) { error::quit(); } // Encrypt and write changed plaintext if we need to store if let Some(dest) = &dest { // TODO: select proper recipients (use from current file?) let recipients = store.recipients()?; context .encrypt_file(&recipients, plaintext.clone(), &dest.0) .map_err(Err::Write)?; } // Copy to clipboard after editing #[cfg(feature = "clipboard")] if matcher_generate.copy() { clipboard::copy_plaintext( plaintext.clone(), true, !matcher_main.force(), matcher_main.quiet(), matcher_main.verbose(), matcher_generate.timeout()?, )?; } // Show in stdout if matcher_generate.show() { secret::print(plaintext).map_err(Err::Print)?; } // Finalize store sync if we saved the secret if let Some(dest) = &dest { if !matcher_generate.no_sync() { sync.finalize(format!("Generate secret to {}", dest.1.name))?; } } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; // Determine whehter we outputted anything to stdout/stderr #[cfg_attr(not(feature = "clipboard"), expect(unused_mut))] let mut output_any = matcher_generate.show(); #[cfg(feature = "clipboard")] { output_any = output_any || matcher_generate.copy(); } if matcher_main.verbose() || (!output_any && !matcher_main.quiet()) { eprintln!("Secret generated"); } Ok(()) } } /// Generate a random password. /// /// This generates a secure random password/passphrase based on user configuration. fn generate_password(matcher_generate: &GenerateMatcher) -> Plaintext { if matcher_generate.passphrase() { let config = BasicConfig { words: matcher_generate.length() as usize, ..Default::default() }; config.to_scheme().generate().into() } else { pass::generate_password(matcher_generate.length()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to normalize destination path")] NormalizePath(#[source] anyhow::Error), #[error("failed to edit secret in editor")] Edit(#[source] anyhow::Error), #[error("failed to read existing secret")] Read(#[source] anyhow::Error), #[error("failed to write changed secret")] Write(#[source] anyhow::Error), #[error("failed to print secret to stdout")] Print(#[source] std::io::Error), #[error("no secret selected")] NoneSelected, } prs-v0.5.2/cli/src/action/git.rs000066400000000000000000000053421471372304600164670ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::sync::{Readyness, Sync as StoreSync}; use prs_lib::Store; use thiserror::Error; use crate::cmd::matcher::{git::GitMatcher, MainMatcher, Matcher}; use crate::util; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; /// Binary name. #[cfg(not(windows))] pub const BIN_NAME: &str = "git"; #[cfg(windows)] pub const BIN_NAME: &str = "git.exe"; /// Git action. pub struct Git<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Git<'a> { /// Construct a new git action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the git action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_git = GitMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let sync = StoreSync::new(&store); #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Warn if sync is not configured if sync .readyness() .map(|r| r == Readyness::NoSync) .unwrap_or(false) { util::error::print_warning("sync not configured, store is not a git repository"); } #[cfg_attr( not(all(feature = "tomb", target_os = "linux")), expect(clippy::let_and_return) )] let result = git(&store, matcher_git.command(), matcher_main.verbose()); // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; result } } /// Invoke a git command. // TODO: call through Command directly, possibly through lib interface pub fn git(store: &Store, cmd: String, verbose: bool) -> Result<()> { util::invoke_cmd( &format!("{} -C {} {}", BIN_NAME, store.root.display(), cmd), Some(&store.root), verbose, ) .map_err(|err| Err::Invoke(err).into()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to invoke git command")] Invoke(#[source] std::io::Error), } prs-v0.5.2/cli/src/action/grep.rs000066400000000000000000000131761471372304600166450ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{ crypto::{prelude::*, Context}, store::SecretIterConfig, Plaintext, Secret, Store, }; use regex::Regex; use thiserror::Error; use crate::cmd::matcher::{grep::GrepMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{ error::{self, ErrorHints, ErrorHintsBuilder}, progress::{self, ProgressBarExt}, }; /// Maximum number of failures without forcing. const MAX_FAIL: usize = 4; /// Grep secrets action. pub struct Grep<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Grep<'a> { /// Construct a new grep action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the grep action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_grep = GrepMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Grep aliases based on filters, sort the list let config = SecretIterConfig { find_files: true, find_symlink_files: matcher_grep.with_aliases(), }; let mut secrets: Vec = store .secret_iter_config(config) .filter_name(matcher_grep.query()) .collect(); secrets.sort_unstable_by(|a, b| a.name.cmp(&b.name)); // Return none selected error if we have an empty list if secrets.is_empty() { return Err(Err::NoSecret.into()); } grep( &secrets, &matcher_grep.pattern(), &matcher_main, &matcher_grep, )?; // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } /// Grep the given secrets. fn grep( secrets: &[Secret], pattern: &str, matcher_main: &MainMatcher, matcher_grep: &GrepMatcher, ) -> Result<()> { let mut context = crate::crypto::context(matcher_main)?; let (mut found, mut failed) = (0, 0); // Parse regex if enabled let regex = if matcher_grep.regex() { Some(Regex::new(pattern).map_err(Err::Regex)?) } else { None }; // Progress bar let pb = progress::progress_bar(secrets.len() as u64, matcher_main.quiet()); for secret in secrets.iter() { pb.set_message_trunc(&secret.name); // Parse normally or with regex let result = match ®ex { Some(re) => grep_single_regex(&mut context, secret, re), None => grep_single(&mut context, secret, pattern), }; // Grep single secret match result { Ok(true) => { pb.println_always(&secret.name); found += 1; } Ok(false) => {} Err(err) => { error::print_error(err.context(format!("failed to grep: {}", secret.name))); failed += 1; } } pb.inc(1); // Stop after many failures if failed > MAX_FAIL && !matcher_main.force() { error::quit_error_msg( format!("stopped after {failed} failures"), ErrorHintsBuilder::from_matcher(matcher_main) .force(true) .build() .unwrap(), ); } } pb.finish_and_clear(); if !matcher_main.quiet() { if found > 0 { eprintln!(); eprintln!("Found {} of {} matches", found, secrets.len()); } else { eprintln!("No matches in {} secrets", secrets.len()); } } if failed > 0 { error::quit_error_msg( format!("Failed to grep {} of {} secrets", failed, secrets.len()), ErrorHints::default(), ); } Ok(()) } /// Grep a single secret. fn grep_single(context: &mut Context, secret: &Secret, pattern: &str) -> Result { let plaintext: Plaintext = context .decrypt_file(&secret.path) .map_err(Err::Read)? .unsecure_to_str() .map_err(Err::Utf8)? .to_uppercase() .into(); Ok(plaintext .unsecure_to_str() .unwrap() .contains(&pattern.to_uppercase())) } /// Grep a single secret using a regular expression. fn grep_single_regex(context: &mut Context, secret: &Secret, pattern: &Regex) -> Result { let plaintext = context.decrypt_file(&secret.path).map_err(Err::Read)?; Ok(pattern.is_match(plaintext.unsecure_to_str().map_err(Err::Utf8)?)) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("no secret to grep")] NoSecret, #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to parse pattern as regular expression")] Regex(#[source] regex::Error), #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to parse secret contents as UTF-8")] Utf8(#[source] std::str::Utf8Error), } prs-v0.5.2/cli/src/action/housekeeping/000077500000000000000000000000001471372304600200205ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/housekeeping/mod.rs000066400000000000000000000021441471372304600211460ustar00rootroot00000000000000pub mod recrypt; pub mod run; pub mod sync_keys; use anyhow::Result; use clap::ArgMatches; use crate::cmd::matcher::{HousekeepingMatcher, Matcher}; /// A file housekeeping action. pub struct Housekeeping<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Housekeeping<'a> { /// Construct a new housekeeping action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the housekeeping action. pub fn invoke(&self) -> Result<()> { // Create the command matcher let matcher_housekeeping = HousekeepingMatcher::with(self.cmd_matches).unwrap(); if matcher_housekeeping.recrypt().is_some() { return recrypt::Recrypt::new(self.cmd_matches).invoke(); } if matcher_housekeeping.run().is_some() { return run::Run::new(self.cmd_matches).invoke(); } if matcher_housekeeping.sync_keys().is_some() { return sync_keys::SyncKeys::new(self.cmd_matches).invoke(); } // Unreachable, clap will print help for missing sub command instead unreachable!() } } prs-v0.5.2/cli/src/action/housekeeping/recrypt.rs000066400000000000000000000131321471372304600220560ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use thiserror::Error; use prs_lib::{ crypto::{self, prelude::*, Context}, Recipients, Secret, Store, }; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ housekeeping::{recrypt::RecryptMatcher, HousekeepingMatcher}, MainMatcher, Matcher, }, util::{ self, error::{self, ErrorHintsBuilder}, progress::{self, ProgressBarExt}, style, sync, }, }; /// Maximum number of failures without forcing. const MAX_FAIL: usize = 4; /// A housekeeping recrypt action. pub struct Recrypt<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Recrypt<'a> { /// Construct a new recrypt action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the recrypt action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_housekeeping = HousekeepingMatcher::with(self.cmd_matches).unwrap(); let matcher_recrypt = RecryptMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_recrypt.allow_dirty()); if !matcher_recrypt.no_sync() { sync.prepare()?; } // Import new keys crypto::store::import_missing_keys_from_store(&store).map_err(Err::ImportRecipients)?; let secrets = store.secrets(matcher_recrypt.query()); recrypt(&store, &secrets, &matcher_main)?; // Finalize sync if !matcher_recrypt.no_sync() { sync.finalize("Re-encrypt secrets")?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; Ok(()) } } /// Re-encrypt all secrets in the given store. pub fn recrypt_all(store: &Store, matcher_main: &MainMatcher) -> Result<()> { recrypt(store, &store.secrets(None), matcher_main) } /// Re-encrypt all given secrets. pub fn recrypt(store: &Store, secrets: &[Secret], matcher_main: &MainMatcher) -> Result<()> { let mut context = crate::crypto::context(matcher_main)?; let recipients = store.recipients().map_err(Err::Store)?; let mut failed = Vec::new(); // Progress bar let pb = progress::progress_bar(secrets.len() as u64, matcher_main.quiet()); for secret in secrets.iter() { pb.set_message_trunc(&secret.name); // Recrypt secret, show status, remember errors if let Err(err) = recrypt_single(&mut context, secret, &recipients) { error::print_error(err.context(format!("recrypting failed: {}", secret.name))); failed.push(secret); } pb.inc(1); // Stop after many failures if failed.len() > MAX_FAIL && !matcher_main.force() { error::quit_error_msg( format!("stopped after {} failures", failed.len()), ErrorHintsBuilder::from_matcher(matcher_main) .force(true) .build() .unwrap(), ); } } pb.finish_and_clear(); // Show success message if any is recrypted let recrypted = secrets.len() - failed.len(); if !matcher_main.quiet() && recrypted > 0 { eprintln!("Re-encrypted {} of {} secrets", recrypted, secrets.len()); } // Show recrypt failures if !failed.is_empty() { let all = failed.len() >= secrets.len(); eprintln!(); error::print_error_msg(format!( "Failed to re-encrypt {} of {} secrets", failed.len(), secrets.len() )); if !matcher_main.quiet() { eprintln!( "Use '{}' to try again", style::highlight(format!( "{} housekeeping recrypt{}", util::bin_name(), &if all { " --all".into() } else if failed.len() == 1 { format!(" {}", &failed[0].name) } else { "".into() } )) ); } error::exit(1); } Ok(()) } /// Recrypt a single secret. fn recrypt_single(context: &mut Context, secret: &Secret, recipients: &Recipients) -> Result<()> { let path = &secret.path; let plaintext = context.decrypt_file(path).map_err(Err::Read)?; context .encrypt_file(recipients, plaintext, path) .map_err(Err::Write)?; Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to write changed secret")] Write(#[source] anyhow::Error), #[error("failed to import store recipients")] ImportRecipients(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/housekeeping/run.rs000066400000000000000000000120211471372304600211660ustar00rootroot00000000000000use std::fs::{self, OpenOptions}; use std::io::{Read, Write}; use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; /// Platform specific line ending character. #[cfg(not(windows))] const LINE_ENDING: &str = "\n"; #[cfg(windows)] const LINE_ENDING: &str = "\r\n"; use crate::{ cmd::matcher::{ housekeeping::{run::RunMatcher, HousekeepingMatcher}, MainMatcher, Matcher, }, util::sync, }; /// A housekeeping run action. pub struct Run<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Run<'a> { /// Construct a new run action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the run action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_housekeeping = HousekeepingMatcher::with(self.cmd_matches).unwrap(); let matcher_run = RunMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; housekeeping(&store, matcher_run.allow_dirty(), matcher_run.no_sync())?; // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Housekeeping done"); } Ok(()) } } /// Run housekeeping tasks. pub(crate) fn housekeeping(store: &Store, allow_dirty: bool, no_sync: bool) -> Result<()> { let sync = store.sync(); // Prepare sync sync::ensure_ready(&sync, allow_dirty); if !no_sync { sync.prepare()?; } set_store_permissions(store).map_err(Err::Perms)?; if sync.is_init() { set_git_ignore(store).map_err(Err::GitAttributes)?; set_git_attributes(store).map_err(Err::GitAttributes)?; } // Finalize sync if !no_sync { sync.finalize("Housekeeping")?; } Ok(()) } /// Set the password store directory permissions to a secure default. #[cfg(unix)] fn set_store_permissions(store: &Store) -> Result<(), std::io::Error> { use std::os::unix::fs::PermissionsExt; // Query existing permissions, update file mode to 600 let root = &store.root; let mut perms = fs::metadata(root)?.permissions(); perms.set_mode(0o700); fs::set_permissions(root, perms) } /// Set the password store directory permissions to a secure default. #[cfg(not(unix))] fn set_store_permissions(_store: &Store) -> Result<(), std::io::Error> { // Not supported on non-Unix Ok(()) } /// Set up the git ignore file. fn set_git_ignore(store: &Store) -> Result<(), std::io::Error> { const ENTRIES: [&str; 6] = [".host", ".last", ".tty", ".uid", ".timer", "lost+found"]; let file = store.root.join(".gitignore"); // Create file if it doesn't exist if !file.is_file() { fs::write(&file, ENTRIES.join(LINE_ENDING))?; return Ok(()); } // Open and read file let mut file = OpenOptions::new().append(true).read(true).open(file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; // Add each entry if it doesn't exist for entry in &ENTRIES { if !contents.lines().any(|l| &l.trim() == entry) { file.write_all(LINE_ENDING.as_bytes())?; file.write_all(entry.as_bytes())?; } } Ok(()) } /// Set up the git attributes file. fn set_git_attributes(store: &Store) -> Result<(), std::io::Error> { const GPG_ENTRY: &str = "*.gpg diff=gpg"; let file = store.root.join(".gitattributes"); // Create file if it doesn't exist if !file.is_file() { fs::write(&file, GPG_ENTRY)?; return Ok(()); } // Open and read file let mut file = OpenOptions::new().append(true).read(true).open(file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; // Append GPG entry if it doesn't exist if !contents.lines().any(|l| l.trim() == GPG_ENTRY) { file.write_all(LINE_ENDING.as_bytes())?; file.write_all(GPG_ENTRY.as_bytes())?; } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to set password store permissions")] Perms(#[source] std::io::Error), #[error("failed to set default .gitattributes")] GitAttributes(#[source] std::io::Error), } prs-v0.5.2/cli/src/action/housekeeping/sync_keys.rs000066400000000000000000000073301471372304600224000ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{ crypto::{self, store::ImportResult}, Store, }; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ housekeeping::{sync_keys::SyncKeysMatcher, HousekeepingMatcher}, MainMatcher, Matcher, }, util::sync, }; /// A housekeeping sync-keys action. pub struct SyncKeys<'a> { cmd_matches: &'a ArgMatches, } impl<'a> SyncKeys<'a> { /// Construct a new sync-keys action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the sync-keys action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_housekeeping = HousekeepingMatcher::with(self.cmd_matches).unwrap(); let matcher_sync_keys = SyncKeysMatcher::with(self.cmd_matches).unwrap(); if matcher_main.verbose() { eprintln!("Syncing public key files in store with selected recipients..."); } let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_sync_keys.allow_dirty()); if !matcher_sync_keys.no_sync() { sync.prepare()?; } // Import missing keys into keychain if !matcher_sync_keys.no_import() { import_missing_keys(&store, matcher_main.quiet(), matcher_main.verbose()) .map_err(Err::ImportKeys)?; } // Sync public key files in store let recipients = store.recipients().map_err(Err::Load)?; crypto::store::store_sync_public_key_files(&store, recipients.keys())?; // Finalize sync if !matcher_sync_keys.no_sync() { sync.finalize("Sync keys")?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Keys synced"); } Ok(()) } } /// Import missing keys from store to keychain. fn import_missing_keys(store: &Store, quiet: bool, verbose: bool) -> Result<()> { if verbose { eprintln!("Importing missing public keys from recipients..."); } // Import keys, report results for result in crypto::store::import_missing_keys_from_store(store)? { match result { ImportResult::Imported(fingerprint) => { if !quiet { eprintln!("Imported key to keychain: {fingerprint}"); } } ImportResult::Unavailable(fingerprint) => { eprintln!("Cannot import missing public key, not available in store: {fingerprint}",) } } } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to load store recipients")] Load(#[source] anyhow::Error), #[error("failed to import public keys to keychain")] ImportKeys(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/init.rs000066400000000000000000000050341471372304600166450ustar00rootroot00000000000000use std::fs; use std::path::Path; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto, Store}; use thiserror::Error; use crate::cmd::matcher::{init::InitMatcher, MainMatcher, Matcher}; use crate::util::{self, style}; /// Init store action. pub struct Init<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Init<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_init = InitMatcher::with(self.cmd_matches).unwrap(); let path = shellexpand::full(&matcher_main.store()) .map_err(Err::ExpandPath)? .to_string(); // Ensure store dir is free, then initialize util::fs::ensure_dir_free(Path::new(&path))?; fs::create_dir_all(&path).map_err(Err::Init)?; // Open new store let store = Store::open(&path).map_err(Err::Store)?; // Run housekeeping crate::action::housekeeping::run::housekeeping(&store, true, false) .map_err(Err::Housekeeping)?; // Hint user to add our recipient key if !matcher_main.quiet() { let bin = util::bin_name(); let config = crate::crypto::config(&matcher_main); let system_has_secret = crypto::util::has_private_key(&config).unwrap_or(true); if system_has_secret { eprintln!("Now add your own key as recipient or generate a new one:"); } else { eprintln!("Now generate and add a new recipient key for yourself:"); } if system_has_secret { eprintln!( " {}", style::highlight(format!("{bin} recipients add --secret")) ); } eprintln!( " {}", style::highlight(format!("{bin} recipients generate")) ); eprintln!(); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to expand store path")] ExpandPath(#[source] shellexpand::LookupError), #[error("failed to initialize store")] Init(#[source] std::io::Error), #[error("failed to access initialized password store")] Store(#[source] anyhow::Error), #[error("failed to run housekeeping tasks")] Housekeeping(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/internal/000077500000000000000000000000001471372304600171465ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/internal/clip.rs000066400000000000000000000025671471372304600204550ustar00rootroot00000000000000use std::io; use anyhow::Result; use clap::ArgMatches; use prs_lib::Plaintext; use thiserror::Error; use crate::cmd::matcher::{internal::clip::ClipMatcher, MainMatcher, Matcher}; use crate::util::{base64, clipboard}; /// A internal clipboard action. pub struct Clip<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Clip<'a> { /// Construct a new clipboard action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the clipboard action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_clip = ClipMatcher::with(self.cmd_matches).unwrap(); // Grab clipboard data from stdin let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; let data = base64::decode(buffer.trim()).map_err(Err::Data)?.into(); drop(Plaintext::from(buffer)); // Set clipboard contents clipboard::subprocess_copy(&data, matcher_main.quiet(), matcher_main.verbose()) .map_err(Err::Clip)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain clipboard content from stdin, malformed data")] Data(#[source] ::base64::DecodeError), #[error("failed to set clipboard contents")] Clip(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/internal/clip_revert.rs000066400000000000000000000040051471372304600220310ustar00rootroot00000000000000use std::io; use std::time::Duration; use anyhow::Result; use clap::ArgMatches; use prs_lib::Plaintext; use thiserror::Error; use crate::cmd::matcher::{internal::clip_revert::ClipRevertMatcher, MainMatcher, Matcher}; use crate::util::{base64, clipboard}; /// A internal clipboard revert action. pub struct ClipRevert<'a> { cmd_matches: &'a ArgMatches, } impl<'a> ClipRevert<'a> { /// Construct a new clipboard revert action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the clipboard revert action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_clip_revert = ClipRevertMatcher::with(self.cmd_matches).unwrap(); // Grab clipboard data from stdin let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; let (a, b) = buffer.split_once(',').ok_or(Err::Data(None))?; let (data, data_old) = ( base64::decode(a.trim()) .map_err(|err| Err::Data(Some(err)))? .into(), base64::decode(b.trim()) .map_err(|err| Err::Data(Some(err)))? .into(), ); drop(Plaintext::from(buffer)); let timeout = Duration::from_secs(matcher_clip_revert.timeout().unwrap()); // Set clipboard contents clipboard::subprocess_copy_revert( &data, &data_old, timeout, matcher_main.quiet(), matcher_main.verbose(), ) .map_err(Err::CopyRevert)?; if matcher_main.verbose() { eprintln!("Clipboard reverted"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain clipboard content from stdin, malformed data")] Data(#[source] Option<::base64::DecodeError>), #[error("failed to copy and revert clipboard contents")] CopyRevert(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/internal/completions.rs000066400000000000000000000107051471372304600220530ustar00rootroot00000000000000use std::fs::{self, File}; use std::io::{self, Write}; use anyhow::Result; use clap::{ArgMatches, Command}; use clap_complete::shells; use thiserror::Error; use crate::cmd::matcher::{ internal::completions::{CompletionsMatcher, Shell}, main::MainMatcher, Matcher, }; /// A file completions action. pub struct Completions<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Completions<'a> { /// Construct a new completions action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the completions action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_completions = CompletionsMatcher::with(self.cmd_matches).unwrap(); // Obtian shells to generate completions for, build application definition let shells = matcher_completions.shells(); let dir = matcher_completions.output(); let quiet = matcher_main.quiet(); let mut app = crate::cmd::handler::Handler::build(); // If the directory does not exist yet, attempt to create it if !dir.is_dir() { fs::create_dir_all(&dir).map_err(Error::CreateOutputDir)?; } // Generate completions for shell in shells { if !quiet { eprint!( "Generating completions for {}...", format!("{shell}").to_lowercase() ); } if matcher_completions.stdout() { generate( shell, &mut app, matcher_completions.name(), &mut std::io::stdout(), ); } else { // Determine path of final file, create file, write completion script to it let path = dir.join(shell.file_name(&matcher_completions.name())); let mut file = File::create(path).map_err(Error::Write)?; generate(shell, &mut app, matcher_completions.name(), &mut file); file.sync_all().map_err(Error::Write)?; } if !quiet { eprintln!(" done."); } } Ok(()) } } /// Generate completion script. fn generate(shell: Shell, app: &mut Command, bin_name: S, buf: &mut dyn Write) where S: Into, { match shell { Shell::Bash => { let mut inner_buf = Vec::new(); clap_complete::generate(shells::Bash, app, bin_name, &mut inner_buf); // Patch bash completion to complete secret names let inner_buf = String::from_utf8(inner_buf) .expect("clap_complete::generate should always return valid utf-8") .replace("", "$(prs list --list --quiet)") .replace("[QUERY]", "$(prs list --list --quiet)"); buf.write_fmt(format_args!("{inner_buf}")) .expect("failed to write to generated file"); // Same panic that clap_complete would trigger } Shell::Zsh => { let mut inner_buf = Vec::new(); clap_complete::generate(shells::Zsh, app, bin_name, &mut inner_buf); // Patch bash completion to complete secret names let inner_buf = String::from_utf8(inner_buf) .expect("clap_complete::generate should always return valid utf-8") .replace( ":QUERY -- Secret query:", ":QUERY -- Secret query:($(prs list --list --quiet))", ); buf.write_fmt(format_args!("{inner_buf}")) .expect("failed to write to generated file"); // Same panic that clap_complete would trigger } // TODO: patch other completion scripts to complete secret names Shell::Elvish => clap_complete::generate(shells::Elvish, app, bin_name, buf), Shell::Fish => clap_complete::generate(shells::Fish, app, bin_name, buf), Shell::PowerShell => clap_complete::generate(shells::PowerShell, app, bin_name, buf), } } #[derive(Debug, Error)] pub enum Error { /// An error occurred while creating the output directory. #[error("failed to create output directory, it doesn't exist")] CreateOutputDir(#[source] io::Error), /// An error occurred while writing completion scripts to a file. #[error("failed to write completion script to file")] Write(#[source] io::Error), } prs-v0.5.2/cli/src/action/internal/mod.rs000066400000000000000000000027541471372304600203030ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod clip; #[cfg(feature = "clipboard")] pub mod clip_revert; pub mod completions; #[cfg(all(feature = "clipboard", feature = "totp"))] pub mod totp_recopy; use anyhow::Result; use clap::ArgMatches; use crate::cmd::matcher::{InternalMatcher, Matcher}; /// An internal action. pub struct Internal<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Internal<'a> { /// Construct a new internal action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the internal action. pub fn invoke(&self) -> Result<()> { // Create the command matcher let matcher_internal = InternalMatcher::with(self.cmd_matches).unwrap(); #[cfg(feature = "clipboard")] if matcher_internal.clip().is_some() { return clip::Clip::new(self.cmd_matches).invoke(); } #[cfg(feature = "clipboard")] if matcher_internal.clip_revert().is_some() { return clip_revert::ClipRevert::new(self.cmd_matches).invoke(); } if matcher_internal.completions().is_some() { return completions::Completions::new(self.cmd_matches).invoke(); } #[cfg(all(feature = "clipboard", feature = "totp"))] if matcher_internal.totp_recopy().is_some() { return totp_recopy::TotpRecopy::new(self.cmd_matches).invoke(); } // Unreachable, clap will print help for missing sub command instead unreachable!() } } prs-v0.5.2/cli/src/action/internal/totp_recopy.rs000066400000000000000000000054261471372304600220720ustar00rootroot00000000000000use std::io; use std::time::{Duration, Instant}; use anyhow::{anyhow, Result}; use clap::ArgMatches; use prs_lib::Plaintext; use thiserror::Error; use crate::cmd::matcher::{internal::totp_recopy::TotpRecopyMatcher, MainMatcher, Matcher}; use crate::util::{base64, clipboard, totp::Totp}; /// A internal TOTP recopy action. pub struct TotpRecopy<'a> { cmd_matches: &'a ArgMatches, } impl<'a> TotpRecopy<'a> { /// Construct a new clipboard revert action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the clipboard revert action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_totp_recopy = TotpRecopyMatcher::with(self.cmd_matches).unwrap(); // Grab clipboard data from stdin let mut buffer = String::new(); io::stdin().read_line(&mut buffer)?; let totp = base64::decode(buffer.trim()).map_err(|err| Err::Data(anyhow!(err)))?; let totp = std::str::from_utf8(&totp).map_err(|err| Err::Data(anyhow!(err)))?; let totp = Totp::from_url(totp).map_err(Err::Data)?; drop(Plaintext::from(buffer)); // Determine until time based on timeout let timeout = matcher_totp_recopy.timeout().unwrap(); let until = Instant::now() + Duration::from_secs(timeout); // Keep recopying chaning token until the copy timeout is reached while until > Instant::now() { // Calculate remaining timeout time, get current TOTP TTL let remaining_timeout = until.duration_since(std::time::Instant::now()); let token = totp.generate_current().map_err(Err::Totp)?; let ttl = totp.ttl().map_err(Err::Totp)?; // Keep clipboard timeout within timeout remaining and current toeken TTL if recopying clipboard::copy_plaintext( token.clone(), false, !matcher_main.force(), matcher_main.quiet() || !matcher_main.verbose(), matcher_main.verbose(), remaining_timeout.as_secs() + 1, )?; // Wait for timeout, stop if clipboard was changed let ttl_duration = Duration::from_secs(ttl); if clipboard::timeout_or_clip_change(&token, ttl_duration) { if matcher_main.verbose() { eprintln!("Clipboard changed, TOTP copy stopped"); } break; } } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain TOTP from stdin, malformed data")] Data(#[source] anyhow::Error), #[error("failed to generate TOTP token")] Totp(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/list.rs000066400000000000000000000112241471372304600166530ustar00rootroot00000000000000use std::io; use anyhow::Result; use clap::ArgMatches; use prs_lib::{store::SecretIterConfig, Secret, Store}; use text_trees::{FormatCharacters, StringTreeNode, TreeFormatting}; use thiserror::Error; use crate::cmd::matcher::{list::ListMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; /// List secrets action. pub struct List<'a> { cmd_matches: &'a ArgMatches, } impl<'a> List<'a> { /// Construct a new list action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the list action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_list = ListMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // List aliases based on filters, sort the list let config = SecretIterConfig { find_files: !matcher_list.only_aliases(), find_symlink_files: !matcher_list.only_non_aliases(), }; let mut secrets: Vec = store .secret_iter_config(config) .filter_name(matcher_list.query()) .collect(); secrets.sort_unstable_by(|a, b| a.name.cmp(&b.name)); // Return nothing if we have an empty list if secrets.is_empty() { return Ok(()); } // Show a list or tree if matcher_list.list() { secrets.iter().for_each(|s| println!("{}", s.name)); } else { display_tree(&secrets); } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } /// Display a secrets tree. fn display_tree(secrets: &[Secret]) { // Build tree nodes from secrets list let names: Vec<_> = secrets.iter().map(|s| s.name.as_str()).collect(); let nodes = tree_nodes("", &names); // Build root tree, print to stdout StringTreeNode::with_child_nodes(".".into(), nodes.into_iter()) .write_with_format( &mut io::stdout(), &TreeFormatting::dir_tree(FormatCharacters::box_chars()), ) .expect("failed to print tree list"); } /// Build tree nodes from given secret names. /// /// The prefix defines the prefix to ignore from secret names. Should be `""` when parsing a new /// tree. /// /// # Warnings /// /// The given list must be sorted. fn tree_nodes(prefix: &str, mut secrets: &[&str]) -> Vec { let mut nodes = vec![]; // Walk through secret names, build list of tree nodes while !secrets.is_empty() { // Find name of a child node, return if we don't have child let child_name = secrets[0] .trim_start_matches(prefix) .trim_start_matches('/') .split('/') .next() .unwrap(); if child_name.trim().is_empty() { return vec![]; } // Build new prefix including child node let child_prefix = if prefix.is_empty() { child_name.to_string() } else { format!("{prefix}/{child_name}") }; // Find position after last child having selected name let next_child_name = secrets[1..] .iter() .position(|s| { s.trim_start_matches(prefix) .trim_start_matches('/') .split('/') .next() .unwrap() != child_name }) .unwrap_or(secrets.len() - 1) + 1; // Take children with same name from list, build child node let (children, todo) = secrets.split_at(next_child_name); secrets = todo; nodes.push(StringTreeNode::with_child_nodes( child_name.into(), tree_nodes(&child_prefix, children).into_iter(), )); } nodes } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/mod.rs000066400000000000000000000007111471372304600164560ustar00rootroot00000000000000pub mod add; #[cfg(feature = "alias")] pub mod alias; pub mod clone; #[cfg(feature = "clipboard")] pub mod copy; pub mod duplicate; pub mod edit; pub mod generate; pub mod git; pub mod grep; pub mod housekeeping; pub mod init; pub mod internal; pub mod list; pub mod r#move; pub mod recipients; pub mod remove; pub mod show; pub mod slam; pub mod sync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod tomb; #[cfg(feature = "totp")] pub mod totp; prs-v0.5.2/cli/src/action/move.rs000066400000000000000000000150331471372304600166500ustar00rootroot00000000000000use std::fs; #[cfg(feature = "alias")] use std::path::Path; use anyhow::Result; use clap::ArgMatches; use prs_lib::{Secret, Store}; use thiserror::Error; use crate::cmd::matcher::{r#move::MoveMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, error, select, sync}; /// Move secret action. pub struct Move<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Move<'a> { /// Construct a new move action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the move action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_move = MoveMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_move.allow_dirty()); if !matcher_move.no_sync() { sync.prepare()?; } let secret = select::store_select_secret(&store, matcher_move.query()).ok_or(Err::NoneSelected)?; // TODO: show secret name if not equal to query, unless quiet? let dest = matcher_move.destination(); // Normalize destination path let path = store .normalize_secret_path(dest, secret.path.file_name().and_then(|p| p.to_str()), true) .map_err(Err::NormalizePath)?; let new_secret = Secret::from(&store, path.clone()); // Check if destination already exists if not forcing if !matcher_main.force() && path.is_file() { eprintln!("A secret at '{}' already exists", path.display(),); if !cli::prompt_yes("Overwrite?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Move cancelled"); } error::quit(); } } #[cfg(feature = "alias")] { // Update this (relative) alias to point to the same target after moving update_secret_alias_target(&store, &secret, &new_secret)?; // Update other aliases pointing to this, to point to new location update_alias_for_secret_to(&store, &secret, &new_secret); } // Move secret fs::rename(&secret.path, path) .map(|_| ()) .map_err(Err::Move)?; super::remove::remove_empty_secret_dir(&secret); // Finalize sync if !matcher_move.no_sync() { sync.finalize(format!("Move from {} to {}", secret.name, new_secret.name))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret moved"); } Ok(()) } } /// Update secret if alias for moved target. /// /// This updates the secret if it is an alias, to update its relative alias path to point to the /// same target when it is moved. /// /// `secret` is the secret to update the realtive path for, `future_secret` is the future path /// `secret` will be moved to. This does not move `secret`, it just updates its relative target /// path for when it is moved afterwards. /// /// If `secret` is not an alias, nothing happens. /// /// Returns `true` if a symlink has been updated. #[cfg(feature = "alias")] fn update_secret_alias_target( store: &Store, secret: &Secret, future_secret: &Secret, ) -> Result { // Do not update anything if secret is not a symlink if !secret.path.symlink_metadata()?.file_type().is_symlink() { return Ok(false); } // Find the alias target secret let target = fs::read_link(&secret.path).map_err(Err::UpdateAlias)?; let target = secret .path .parent() .unwrap() .join(target) .canonicalize() .map_err(Err::UpdateAlias)?; let target = Secret::from(store, target); // Update alias to point to same target when moved update_alias(store, &target, &secret.path, &future_secret.path)?; Ok(true) } /// Update aliases for moved secret. /// /// Update the aliases for a secret that is moved. /// /// The `secret` is the old secret location, the `new_secret` is the location it is moved to. /// Aliases targetting `secret` will be updated to point to `new_secret`. #[cfg(feature = "alias")] fn update_alias_for_secret_to(store: &Store, secret: &Secret, new_secret: &Secret) { for secret in super::remove::find_symlinks_to(store, secret) { if let Err(err) = update_alias(store, new_secret, &secret.path, &secret.path) { error::print_error( err.context("failed to update path of alias that points to moved secret, ignoring"), ); } } } /// Update the path of an alias. /// /// Updates the symlink file at `symlink` to point to the new target `src`. /// /// # Panics /// /// Panics if the given `symlink` path is not an existing symlink. #[cfg(feature = "alias")] fn update_alias(store: &Store, src: &Secret, symlink: &Path, future_symlink: &Path) -> Result<()> { assert!( symlink.symlink_metadata()?.file_type().is_symlink(), "failed to update symlink, not a symlink" ); // Remove existing file fs::remove_file(symlink) .map(|_| ()) .map_err(Err::UpdateAlias)?; // Create new symlink super::alias::create_alias(store, src, future_symlink, symlink)?; Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to normalize destination path")] NormalizePath(#[source] anyhow::Error), #[error("failed to move secret file")] Move(#[source] std::io::Error), #[cfg(feature = "alias")] #[error("failed to update alias")] UpdateAlias(#[source] std::io::Error), } prs-v0.5.2/cli/src/action/recipients/000077500000000000000000000000001471372304600174775ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/recipients/add.rs000066400000000000000000000102551471372304600206000ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Recipients, Store}; use thiserror::Error; use crate::cmd::matcher::{ recipients::{add::AddMatcher, RecipientsMatcher}, MainMatcher, Matcher, }; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{self, error, select, style, sync}; /// A recipients add action. pub struct Add<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Add<'a> { /// Construct a new add action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the add action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); let matcher_add = AddMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_add.allow_dirty()); if !matcher_add.no_sync() { sync.prepare()?; } let mut context = crate::crypto::context(&matcher_main)?; let mut recipients = store.recipients().map_err(Err::Load)?; // Find unused keys, select one and add to recipients let mut tmp = Recipients::from( if !matcher_add.secret() { context.keys_public() } else { context.keys_private() } .map_err(Err::Load)?, ); tmp.remove_all(recipients.keys()); let key = select::select_key(tmp.keys(), None).ok_or(Err::NoneSelected)?; recipients.add(key.clone()); recipients.save(&store)?; if prs_lib::store::can_decrypt(&store) { // Recrypt secrets // TODO: do not quit on error, finish sync, ask to revert instead? if !matcher_add.no_recrypt() { crate::action::housekeeping::recrypt::recrypt_all(&store, &matcher_main) .map_err(Err::Recrypt)?; } } else if !matcher_main.quiet() { cannot_decrypt_show_recrypt_hints(); } // Finalize sync sync.finalize(format!("Add recipient {}", key.fingerprint(true)))?; // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Added recipient: {key}"); } Ok(()) } } /// Cannot decrypt on this machine, show recrypt hints. pub(crate) fn cannot_decrypt_show_recrypt_hints() { // TODO: only show this if adding secret key error::print_warning("cannot read secrets on this machine"); error::print_warning("re-encrypt secrets on another machine with this store to fix"); let bin = util::bin_name(); println!(); println!("Run this on another machine to re-encrypt secrets:"); println!( " {}", style::highlight(format!("{bin} housekeeping recrypt --all")) ); println!(); println!("When done, pull in the re-encrypted secrets here with:"); println!(" {}", style::highlight(format!("{bin} sync"))); println!(); } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no key selected")] NoneSelected, #[error("failed to load usable keys from keychain")] Load(#[source] anyhow::Error), #[error("failed to re-encrypt secrets in store")] Recrypt(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/recipients/export.rs000066400000000000000000000064241471372304600213740ustar00rootroot00000000000000use std::fs; use std::io::Write; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Plaintext, Store}; use thiserror::Error; use crate::cmd::matcher::{ recipients::{export::ExportMatcher, RecipientsMatcher}, MainMatcher, Matcher, }; #[cfg(feature = "clipboard")] use crate::util::clipboard; use crate::util::select; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; /// A recipients export action. pub struct Export<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Export<'a> { /// Construct a new export action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the export action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); let matcher_export = ExportMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let recipients = store.recipients().map_err(Err::Load)?; // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let key = select::select_key(recipients.keys(), None) .ok_or(Err::NoneSelected)? .clone(); // Export public key let data = Plaintext::from(crate::crypto::context(&matcher_main)?.export_key(key)?); let mut stdout = true; // Output to file if let Some(path) = matcher_export.output_file() { stdout = false; fs::write(path, data.unsecure_ref()).map_err(Err::Output)?; if !matcher_main.quiet() { eprintln!("Key exported to: {path}"); } } // Copy to clipboard #[cfg(feature = "clipboard")] if matcher_export.copy() { stdout = false; clipboard::copy(&data, matcher_main.quiet(), matcher_main.verbose()) .map_err(Err::Clipboard)?; } if stdout { std::io::stdout() .write_all(data.unsecure_ref()) .map_err(Err::Output)?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to load recipients from keychain")] Load(#[source] anyhow::Error), #[error("no key selected")] NoneSelected, #[error("failed to write key to file")] Output(#[source] std::io::Error), #[cfg(feature = "clipboard")] #[error("failed to copy key to clipboard")] Clipboard(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/recipients/generate.rs000066400000000000000000000140301471372304600216350ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Recipients, Store}; use thiserror::Error; use crate::cmd::matcher::{ recipients::{generate::GenerateMatcher, RecipientsMatcher}, MainMatcher, Matcher, }; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{ self, cli, error::{self, ErrorHintsBuilder}, style, sync, }; /// Binary name. #[cfg(not(windows))] const BIN_NAME: &str = "gpg"; #[cfg(windows)] const BIN_NAME: &str = "gpg.exe"; /// A recipients generate action. pub struct Generate<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Generate<'a> { /// Construct a new generate action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the generate action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); let matcher_generate = GenerateMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_generate.allow_dirty()); if !matcher_generate.no_sync() { sync.prepare()?; } // Generating recipient in no-interact mode is not supported if matcher_main.no_interact() && !matcher_main.force() { error::quit_error_msg( "generating recipient with --no-interact is not supported", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ) } // Show warning to user if !matcher_main.force() { eprintln!("This will start a key pair generation wizard through 'gpg'"); if !cli::prompt_yes("Continue?", Some(true), &matcher_main) { if matcher_main.verbose() { eprintln!("Generation cancelled"); } error::quit(); } } // Generate new key through GPG let new = gpg_generate(&matcher_main)?; let new_keys = new.keys(); if !matcher_generate.no_add() { if new.keys().is_empty() { error::quit_error_msg( "not adding recipient to store because no new keys are found", ErrorHintsBuilder::from_matcher(&matcher_main) .add_info(format!( "Use '{}' to add a recipient", style::highlight("prs recipients add") )) .build() .unwrap(), ); } // Add new keys to store let mut recipients = store.recipients().map_err(Err::Load)?; for key in new_keys { recipients.add(key.clone()); } recipients.save(&store)?; if prs_lib::store::can_decrypt(&store) { // Recrypt secrets // TODO: do not quit on error, finish sync, ask to revert instead? if !matcher_generate.no_recrypt() { crate::action::housekeeping::recrypt::recrypt_all(&store, &matcher_main) .map_err(Err::Recrypt)?; }; } else if !matcher_main.quiet() { super::add::cannot_decrypt_show_recrypt_hints(); } // Finalize sync if !matcher_generate.no_sync() { sync.finalize(format!( "Generate and add recipient {}", new_keys .iter() .map(|k| k.fingerprint(true)) .collect::>() .join(", "), ))?; } if !matcher_main.quiet() { for key in new_keys { eprintln!("Added recipient: {key}"); } } } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; Ok(()) } } /// Invoke GPG generate command. /// /// Return new keys as recipients. pub fn gpg_generate(matcher_main: &MainMatcher) -> Result { // List recipients before let mut context = crate::crypto::context(matcher_main)?; let before = Recipients::from(context.keys_private()?); // Generate key through GPG if !matcher_main.quiet() { eprintln!("===== GPG START ====="); } util::invoke_cmd( &format!("{BIN_NAME} --full-generate-key"), None, matcher_main.verbose(), ) .map_err(Err::Invoke)?; if !matcher_main.quiet() { eprintln!("===== GPG END ====="); } // List recipients after, keep new keys let mut diff = Recipients::from(context.keys_private()?); diff.remove_all(before.keys()); Ok(diff) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to load recipients from keychain")] Load(#[source] anyhow::Error), #[error("failed to invoke gpg command")] Invoke(#[source] std::io::Error), #[error("failed to re-encrypt secrets in store")] Recrypt(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/recipients/list.rs000066400000000000000000000041171471372304600210230ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; use crate::cmd::matcher::{recipients::RecipientsMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; /// A recipients list action. pub struct List<'a> { cmd_matches: &'a ArgMatches, } impl<'a> List<'a> { /// Construct a new list action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the list action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let recipients = store.recipients().map_err(Err::List)?; // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; recipients .keys() .iter() .map(|key| { if !matcher_main.quiet() { key.to_string() } else { key.fingerprint(false) } }) .for_each(|key| println!("{key}")); // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to list store recipients")] List(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/recipients/mod.rs000066400000000000000000000025651471372304600206340ustar00rootroot00000000000000pub mod add; pub mod export; pub mod generate; pub mod list; pub mod remove; use anyhow::Result; use clap::ArgMatches; use crate::cmd::matcher::{Matcher, RecipientsMatcher}; /// A file recipients action. pub struct Recipients<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Recipients<'a> { /// Construct a new recipients action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the recipients action. pub fn invoke(&self) -> Result<()> { // Create the command matcher let matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); if matcher_recipients.cmd_add().is_some() { return add::Add::new(self.cmd_matches).invoke(); } if matcher_recipients.cmd_export().is_some() { return export::Export::new(self.cmd_matches).invoke(); } if matcher_recipients.cmd_generate().is_some() { return generate::Generate::new(self.cmd_matches).invoke(); } if matcher_recipients.cmd_list().is_some() { return list::List::new(self.cmd_matches).invoke(); } if matcher_recipients.cmd_remove().is_some() { return remove::Remove::new(self.cmd_matches).invoke(); } // Unreachable, clap will print help for missing sub command instead unreachable!() } } prs-v0.5.2/cli/src/action/recipients/remove.rs000066400000000000000000000076501471372304600213520ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; use crate::cmd::matcher::{ recipients::{remove::RemoveMatcher, RecipientsMatcher}, MainMatcher, Matcher, }; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, error, select, sync}; /// A recipients remove action. pub struct Remove<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Remove<'a> { /// Construct a new remove action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the remove action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_recipients = RecipientsMatcher::with(self.cmd_matches).unwrap(); let matcher_remove = RemoveMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_remove.allow_dirty()); if !matcher_remove.no_sync() { sync.prepare()?; } let mut recipients = store.recipients().map_err(Err::Load)?; // Select key to remove let key = select::select_key(recipients.keys(), None) .ok_or(Err::NoneSelected)? .clone(); // Do not allow removing last recipient unless forcing if recipients.keys().len() == 1 && !matcher_main.force() { error::print_error_msg( "cannot remove last recipient from store, you will permanently loose access to it", ); error::ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .verbose(false) .build() .unwrap() .print(false); error::quit(); } // Confirm removal if !matcher_main.force() { eprintln!("{key}"); if !cli::prompt_yes( &format!("Remove '{}'?", key.fingerprint(true),), Some(true), &matcher_main, ) { if matcher_main.verbose() { eprintln!("Removal cancelled"); } error::quit(); } } recipients.remove(&key); recipients.save(&store)?; // Recrypt secrets if matcher_remove.recrypt() { crate::action::housekeeping::recrypt::recrypt_all(&store, &matcher_main) .map_err(Err::Recrypt)?; } // Finalize sync if !matcher_remove.no_sync() { sync.finalize(format!("Remove recipient {}", key.fingerprint(true)))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Removed recipient: {key}"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no key selected")] NoneSelected, #[error("failed to load existing keys from store")] Load(#[source] anyhow::Error), #[error("failed to re-encrypt secrets in store")] Recrypt(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/remove.rs000066400000000000000000000165771471372304600172150ustar00rootroot00000000000000use std::fs; use std::io; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; use clap::ArgMatches; #[cfg(feature = "alias")] use prs_lib::store::SecretIterConfig; use prs_lib::{Secret, Store}; use thiserror::Error; use walkdir::WalkDir; use crate::cmd::matcher::{remove::RemoveMatcher, MainMatcher, Matcher}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{cli, error, select, sync}; /// Remove secret action. pub struct Remove<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Remove<'a> { /// Construct a new remove action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the remove action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_remove = RemoveMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, matcher_remove.allow_dirty()); if !matcher_remove.no_sync() { sync.prepare()?; } let secret = select::store_select_secret(&store, matcher_remove.query()).ok_or(Err::NoneSelected)?; if !remove_confirm(&store, &secret, &matcher_main, &mut Vec::new())? { if matcher_main.verbose() { eprintln!("Removal cancelled"); } error::quit(); }; // Finalize sync if !matcher_remove.no_sync() { sync.finalize(format!("Remove secret {}", secret.name))?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Secret removed"); } Ok(()) } } /// Confirm to remove the given secret, then remove. /// /// This also asks to remove an alias target, and aliases targeting this secret, effectively asking /// to remove all linked aliases. fn remove_confirm( store: &Store, secret: &Secret, matcher_main: &MainMatcher, ignore: &mut Vec, ) -> Result { // Prevent infinite loops, skip removal if already on ignore list if ignore.contains(&secret.path) { return Ok(false); } // Check wheher secret is an alias, build prompt #[cfg(feature = "alias")] let is_alias = fs::symlink_metadata(&secret.path)?.file_type().is_symlink(); #[cfg(not(feature = "alias"))] let is_alias = false; let prompt = &format!( "Remove {}'{}'?", if is_alias { "alias " } else { "" }, secret.path.display(), ); // Confirm removal ignore.push(secret.path.clone()); if !matcher_main.force() && !cli::prompt_yes(prompt, Some(true), matcher_main) { return Ok(false); } // Ask to remove alias target if is_alias { match secret.alias_target(store) { Ok(secret) => { // TODO: is this error okay? if let Err(err) = remove_confirm(store, &secret, matcher_main, ignore) { error::print_error(err.context("failed to remove alias target, ignoring")); } } Err(err) => error::print_error(err.context("failed to query alias target, ignoring")), } } // Ask to remove aliases targeting this secret #[cfg(feature = "alias")] for secret in find_symlinks_to(store, secret) { if let Err(err) = remove_confirm(store, &secret, matcher_main, ignore) { error::print_error(err.context("failed to remove alias, ignoring")); } } // Remove secret, remove directories that become empty fs::remove_file(&secret.path) .map(|_| ()) .map_err(Err::Remove)?; remove_empty_secret_dir(secret); Ok(true) } /// Find symlink secrets to given secret. /// /// Collect all secrets that are a symlink which target the given `secret`. #[cfg(feature = "alias")] pub fn find_symlinks_to(store: &Store, secret: &Secret) -> Vec { // Configure secret iterator to only find symlinks let config = SecretIterConfig { find_files: false, find_symlink_files: true, }; // Collect secrets that symlink to given secret store .secret_iter_config(config) .filter(|sym| { // Find symlink target path let sym_path = match std::fs::read_link(&sym.path) { Ok(path) => path, Err(_) => return false, }; // Ignore secret if absolute symlink target doesn't match secret sym.path .parent() .unwrap() .join(sym_path) .canonicalize() .map(|full_path| secret.path == full_path) .unwrap_or(false) }) .collect() } /// Remove secret directory if empty. /// /// This removes the directory the given `secret` was in if the directory is empty. /// Parent directories will be removed if they're empty as well. /// /// If the given `secret` still exists, the directory is never removed because it is not empty. /// /// This never errors, but reports an error to the user when it does. pub fn remove_empty_secret_dir(secret: &Secret) { // Remove secret directory if empty if let Err(err) = remove_empty_dir(secret.path.parent().unwrap(), true) { error::print_error( anyhow!(err).context("failed to remove now empty secret directory, ignoring"), ); } } /// Remove directory if it's empty. /// /// Remove the directory `path` if it's empty. /// If the directory contains other empty directories, it's still considered empty. /// /// If `remove_empty_parents` is true, the parents that are empty will be removed too. fn remove_empty_dir(path: &Path, remove_empty_parents: bool) -> Result<(), io::Error> { // Stop if path is not an existing directory if !path.is_dir() { return Ok(()); } // Make sure directory is empty, assume no on error, stop if not empty let is_empty = WalkDir::new(path) .follow_links(true) .into_iter() .any(|entry| match entry { Ok(entry) => entry.file_type().is_file(), Err(_) => true, }); if is_empty { return Ok(()); } // Remove the directory fs::remove_dir_all(path)?; // Remove empty parents if remove_empty_parents { if let Some(parent) = path.parent() { return remove_empty_dir(parent, true); } } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to remove secret file")] Remove(#[source] std::io::Error), } prs-v0.5.2/cli/src/action/show.rs000066400000000000000000000072561471372304600166720ustar00rootroot00000000000000use std::time::Duration; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; use crate::cmd::matcher::{show::ShowMatcher, MainMatcher, Matcher}; #[cfg(feature = "clipboard")] use crate::util::clipboard; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{secret, select}; use crate::viewer; /// Show secret action. pub struct Show<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Show<'a> { /// Construct a new show action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the show action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_show = ShowMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_show.query()).ok_or(Err::NoneSelected)?; let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to first line or property if matcher_show.first_line() { plaintext = plaintext.first_line()?; } else if let Some(property) = matcher_show.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } // Copy to clipboard #[cfg(feature = "clipboard")] if matcher_show.copy() { clipboard::copy_plaintext( plaintext.clone(), true, !matcher_main.force(), matcher_main.quiet(), matcher_main.verbose(), matcher_show .timeout() .unwrap_or(Ok(crate::CLIPBOARD_TIMEOUT))?, )?; } // Show directly or in viewer if matcher_show.viewer() { viewer::viewer( &store, &secret, plaintext, matcher_show.timeout().transpose()?.map(Duration::from_secs), &matcher_main, matcher_show.query(), ) .map_err(Err::Viewer)?; } else { secret::print_name(matcher_show.query(), &secret, &store, matcher_main.quiet()); secret::print(plaintext).map_err(Err::Print)? } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), #[error("failed to print secret to stdout")] Print(#[source] std::io::Error), #[error("failed to start secret viewer")] Viewer(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/slam.rs000066400000000000000000000263231471372304600166420ustar00rootroot00000000000000use std::process::Command; use anyhow::{anyhow, Result}; use clap::ArgMatches; #[cfg(unix)] use prs_lib::util::git; use prs_lib::Store; use thiserror::Error; use crate::cmd::matcher::{slam::SlamMatcher, MainMatcher, Matcher}; use crate::util::error; #[cfg(all(feature = "tomb", target_os = "linux"))] use prs_lib::tomb::{self, TombSettings}; /// Slam password store action. pub struct Slam<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Slam<'a> { /// Construct a new slam action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the slam action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_slam = SlamMatcher::with(self.cmd_matches).unwrap(); // Attempt to open store for some locking operations let store = match Store::open(matcher_main.store()) { Ok(store) => Some(store), Err(err) => { error::print_error(Err::Store(err).into()); None } }; // Attempt to flush GPG agents flush_gpg_agents(&matcher_main); // Attempt to lock Tomb #[cfg(all(feature = "tomb", target_os = "linux"))] if let Some(store) = &store { if let Err(err) = tomb_lock(store, &matcher_main) { error::print_error(Err::Close(err).into()); } } // Attempt to invalidate cached sudo credentials match has_bin("sudo") { Ok(true) => invalidate_sudo(&matcher_main), Ok(false) => {} Err(err) => error::print_error(err.context("failed to invalidate sudo credentials")), } match has_bin("doas") { Ok(true) => invalidate_doas(&matcher_main), Ok(false) => {} Err(err) => error::print_error(err.context("failed to invalidate doas credentials")), } // Drop open prs persistent SSH sessions #[cfg(unix)] if let Some(store) = &store { drop_persistent_ssh(store, &matcher_main); } if !matcher_main.quiet() { eprintln!("Password store locked"); } Ok(()) } } /// Attempt to flush and clear all GPG agents that potentially unlock secrets. fn flush_gpg_agents(matcher_main: &MainMatcher) { let mut flushed = false; // Kill GPG agent through gpgconf match has_bin("gpgconf") { Ok(true) => flushed = gpgconf_kill(matcher_main) || flushed, Ok(false) => {} Err(err) => error::print_error(err.context("failed to kill GPG agent through gpgconf")), } // Clear GPG agent through keychain match has_bin("keychain") { Ok(true) => flushed = keychain_clear(matcher_main) || flushed, Ok(false) => {} Err(err) => error::print_error(err.context("failed to clear GPG agent through keychain")), } // Reload GPG agents through pkill #[cfg(unix)] match has_bin("pkill") { Ok(true) => flushed = pkill_reload_gpgagent(matcher_main) || flushed, Ok(false) => {} Err(err) => error::print_error(err.context("failed to reload GPG agents through pkill")), } // Show warning if not flushed if !flushed { error::print_warning("no GPG agent is flushed, cleared or killed"); } } /// Kill GPG agent using gpgconf. fn gpgconf_kill(matcher_main: &MainMatcher) -> bool { // Signal gpg-agent kill through gpgconf // Invoke: gpgconf --kill gpg-agent if !matcher_main.quiet() { eprint!("Signal gpgconf gpg-agent kill: "); } match Command::new("gpgconf") .args(["--kill", "gpg-agent"]) .status() { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error(anyhow!(err).context("failed to kill gpgconf gpg-agent")); false } Ok(status) if !status.success() => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error_msg(format!( "failed to kill gpgconf gpg-agent (exit status: {status})" )); false } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } true } } } /// Clear GPG agent through keychain. fn keychain_clear(matcher_main: &MainMatcher) -> bool { // Signal to clear keychain GPG agent // Invoke: keychain --quiet --clear --agents gpg if !matcher_main.quiet() { eprint!("Clear keychain GPG agent: "); } match Command::new("keychain") .args(["--quiet", "--clear", "--agents", "gpg"]) .status() { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error(anyhow!(err).context("failed to kill keychain GPG agent")); false } Ok(status) if !status.success() => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error_msg(format!( "failed to kill keychain GPG agent (exit status: {status})", )); false } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } true } } } /// Reload configuration of gpg-agent processes. #[cfg(unix)] fn pkill_reload_gpgagent(matcher_main: &MainMatcher) -> bool { // Kill any remaining gpg-agent processes // Invoke: pkill -HUP gpg-agent if !matcher_main.quiet() { eprint!("Reload gpg-agent processes: "); } match Command::new("pkill").args(["-HUP", "gpg-agent"]).status() { Ok(status) if status.code() == Some(0) => { if !matcher_main.quiet() { eprintln!("ok"); } true } Ok(status) if status.code() == Some(1) => { if !matcher_main.quiet() { eprintln!("ok"); } false } Ok(_) => { if !matcher_main.quiet() { eprintln!("FAIL"); } false } Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error(anyhow!(err).context("failed to reload gpg-agent processes")); false } } } /// Attempt to lock Tomb. #[cfg(all(feature = "tomb", target_os = "linux"))] fn tomb_lock(store: &Store, matcher_main: &MainMatcher) -> Result<()> { let tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Must be a tomb, must be open, assume it is if !tomb.is_tomb() || !tomb.is_open().unwrap_or(true) { return Ok(()); } // Close the tomb if !matcher_main.quiet() { eprint!("Close Tomb: "); } match tomb.close() { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } return Err(Err::Close(err).into()); } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } } } // Close any running close timers if let Err(err) = tomb.stop_timer() { error::print_error(err.context("failed to stop auto closing systemd timer, ignoring")); } // If the Tomb is still open, slam all open Tombs if tomb.is_open().unwrap_or(false) { tomb_slam(matcher_main)?; } Ok(()) } /// Attempt to slam Tombs. #[cfg(all(feature = "tomb", target_os = "linux"))] fn tomb_slam(matcher_main: &MainMatcher) -> Result<()> { let tomb_settings = TombSettings { quiet: matcher_main.quiet(), verbose: matcher_main.verbose(), force: matcher_main.force(), }; // Slam open tombs if !matcher_main.quiet() { eprint!("Slam Tombs: "); } match tomb::slam(tomb_settings) { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } Err(Err::Slam(err).into()) } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } Ok(()) } } } /// Attempt to invalidate cached sudo credentials that are still active. fn invalidate_sudo(matcher_main: &MainMatcher) { if !matcher_main.quiet() { eprint!("Invalidate cached sudo credentials: "); } match Command::new("sudo").args(["-K"]).status() { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error( anyhow!(err).context("failed to invalidate cached sudo credentials"), ); } Ok(status) if !status.success() => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error_msg(format!( "failed to invalidate cached sudo credentials (exit status: {status})", )); } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } } } } /// Attempt to invalidate cached doas credentials that are still active. fn invalidate_doas(matcher_main: &MainMatcher) { if !matcher_main.quiet() { eprint!("Invalidate cached doas credentials: "); } match Command::new("doas").args(["-L"]).status() { Err(err) => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error( anyhow!(err).context("failed to invalidate cached doas credentials"), ); } Ok(status) if !status.success() => { if !matcher_main.quiet() { eprintln!("FAIL"); } error::print_error_msg(format!( "failed to invalidate cached doas credentials (exit status: {status})", )); } Ok(_) => { if !matcher_main.quiet() { eprintln!("ok"); } } } } /// Drop any open prs persistent SSH sessions. #[cfg(unix)] fn drop_persistent_ssh(store: &Store, matcher_main: &MainMatcher) { if !matcher_main.quiet() { eprint!("Drop persistent SSH sessions: "); } // Kill any still open git::kill_ssh_by_session(store); if !matcher_main.quiet() { eprintln!("ok"); } } /// Check if the given binary is found and is invokable. fn has_bin(bin: &str) -> Result { match which::which(bin) { Ok(_) => Ok(true), Err(which::Error::CannotFindBinaryPath) => Ok(false), Err(err) => Err(Err::ProbeBinary(err, bin.into()).into()), } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to find binary: {1}")] ProbeBinary(#[source] which::Error, String), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to close password store tomb")] Close(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to slam open tombs")] Slam(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/000077500000000000000000000000001471372304600163065ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/sync/commit.rs000066400000000000000000000077211471372304600201530ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{sync::Readyness, Store}; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ sync::{commit::CommitMatcher, SyncMatcher}, MainMatcher, Matcher, }, util::{ cli, error::{self, ErrorHints, ErrorHintsBuilder}, sync, }, }; /// A sync commit action. pub struct Commit<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Commit<'a> { /// Construct a new commit action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the commit action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); let matcher_commit = CommitMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, true); if !matcher_commit.no_sync() { sync.prepare()?; } // Ensure store is dirty, or forcing match sync.readyness()? { Readyness::Dirty => {} _ if matcher_main.force() => {} Readyness::Ready => { error::quit_error_msg( "nothing to commit, password store is not dirty", ErrorHintsBuilder::from_matcher(&matcher_main) .sync(true) .force(true) .build() .unwrap(), ); } other => { error::quit_error_msg( format!("unexpected sync state: {other:?}"), ErrorHints::from_matcher(&matcher_main), ); } } // List changed files if !matcher_main.quiet() { if let Err(err) = super::status::print_changed_files(&sync, &matcher_main) { error::print_error(err.context("failed to print list of changed files, ignoring")); } eprintln!(); } // Confirm eprintln!("Password store got into a dirty state unexpectedly."); eprintln!("Committing the above changes may break your password store and may cause unexpected results."); if !cli::prompt_yes("Commit above changes?", Some(false), &matcher_main) { if matcher_main.verbose() { eprintln!("Commit cancelled"); } error::quit(); } // Commit changes let msg = matcher_commit.message().unwrap_or("Manual sync commit"); sync.commit_all(msg, matcher_main.force()) .map_err(Err::Commit)?; // Finalize sync if !matcher_commit.no_sync() { sync.finalize(msg)?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Changes committed"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to commit changes")] Commit(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/init.rs000066400000000000000000000054301471372304600176210ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{sync::SyncMatcher, MainMatcher, Matcher}, util::{ error::{self, ErrorHintsBuilder}, style::highlight, }, }; /// A sync init action. pub struct Init<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Init<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; if sync.is_init() { error::quit_error_msg( "sync is already initialized", ErrorHintsBuilder::from_matcher(&matcher_main) .sync(true) .build() .unwrap(), ); } // Initialize sync sync.init().map_err(Err::Init)?; // Run housekeeping crate::action::housekeeping::run::housekeeping(&store, true, false) .map_err(Err::Housekeeping)?; // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Sync initialized"); if !sync.has_remote()? { let bin = crate::util::bin_name(); eprintln!(); eprintln!("Sync remote not configured, to configure a remote use:"); eprintln!(" {}", highlight(format!("{bin} sync remote "))); } } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to initialize git sync")] Init(#[source] anyhow::Error), #[error("failed to run housekeeping tasks")] Housekeeping(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/mod.rs000066400000000000000000000101011471372304600174240ustar00rootroot00000000000000pub mod commit; pub mod init; pub mod remote; pub mod reset; pub mod status; use anyhow::Result; use clap::ArgMatches; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use prs_lib::{ crypto, sync::{Readyness, Sync as StoreSync}, Store, }; use crate::{ cmd::matcher::{sync::SyncMatcher, MainMatcher, Matcher}, util::{ error::{self, ErrorHintsBuilder}, sync, }, }; /// Sync secrets action. pub struct Sync<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Sync<'a> { /// Construct a new sync action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the sync action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); // Subcommands if matcher_sync.cmd_commit().is_some() { return commit::Commit::new(self.cmd_matches).invoke(); } if matcher_sync.cmd_init().is_some() { return init::Init::new(self.cmd_matches).invoke(); } if matcher_sync.cmd_status().is_some() { return status::Status::new(self.cmd_matches).invoke(); } if matcher_sync.cmd_remote().is_some() { return remote::Remote::new(self.cmd_matches).invoke(); } if matcher_sync.cmd_reset().is_some() { return reset::Reset::new(self.cmd_matches).invoke(); } let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let sync = StoreSync::new(&store); #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Don't sync if not initialized or no remote, show help on how to set up match sync.readyness()? { Readyness::NoSync => { error::quit_error_msg( "sync not configured", ErrorHintsBuilder::from_matcher(&matcher_main) .sync_init(true) .build() .unwrap(), ); } _ if !sync.has_remote()? => { if !matcher_main.quiet() { error::print_warning( "no sync remote configured, set using: prs sync remote ", ); } } _ => {} } sync::ensure_ready(&sync, matcher_sync.allow_dirty()); // Prepare, commit, finalize sync.prepare()?; sync.finalize("Sync dirty changes")?; // TODO: do housekeeping? // TODO: assume changed for now, fetch this state from syncer let changed = true; // Were done if nothing was changed if !changed { if !matcher_main.quiet() { eprintln!("Everything up-to-date"); } return Ok(()); } // Import new keys crypto::store::import_missing_keys_from_store(&store).map_err(Err::ImportRecipients)?; // TODO: assert not-dirty state? if !matcher_main.quiet() { eprintln!("Sync complete"); } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to import store recipients")] ImportRecipients(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/remote.rs000066400000000000000000000101001471372304600201370ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; /// The default name for a git remote. const DEFAULT_GIT_REMOTE_NAME: &str = "origin"; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ sync::{remote::RemoteMatcher, SyncMatcher}, MainMatcher, Matcher, }, util::{ self, error::{self, ErrorHintsBuilder}, style, }, }; /// A sync remote action. pub struct Remote<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Remote<'a> { /// Construct a new remote action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the remote action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); let matcher_remote = RemoteMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; if !sync.is_init() { error::quit_error_msg( "sync is not configured", ErrorHintsBuilder::from_matcher(&matcher_main) .sync_init(true) .build() .unwrap(), ); } // Get or set remote let remotes = sync.tracked_remote_or_remotes()?; match matcher_remote.git_url() { Some(url) => { match remotes.len() { 0 => sync.add_remote_url(DEFAULT_GIT_REMOTE_NAME, url)?, 1 => sync.set_remote_url(&remotes[0], url)?, _ => error::quit_error_msg( "multiple remotes configured, cannot set automatically", ErrorHintsBuilder::from_matcher(&matcher_main) .git(true) .build() .unwrap(), ), } if !matcher_main.quiet() { eprintln!("To sync with the remote now use:"); eprintln!( " {}", style::highlight(format!("{} sync", util::bin_name())) ); eprintln!(); } if matcher_main.verbose() { eprintln!("Sync remote set"); } } None => match remotes.len() { 0 => error::quit_error_msg( "no remote configured", ErrorHintsBuilder::from_matcher(&matcher_main) .sync_remote(true) .build() .unwrap(), ), 1 => println!("{}", sync.remote_url(&remotes[0])?), _ => error::quit_error_msg( "multiple remotes configured, cannot decide automatically", ErrorHintsBuilder::from_matcher(&matcher_main) .git(true) .build() .unwrap(), ), }, } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/reset.rs000066400000000000000000000075121471372304600200030ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{sync::Readyness, Store}; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ sync::{reset::ResetMatcher, SyncMatcher}, MainMatcher, Matcher, }, util::{ cli, error::{self, ErrorHints, ErrorHintsBuilder}, sync, }, }; /// A sync reset action. pub struct Reset<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Reset<'a> { /// Construct a new reset action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the reset action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); let matcher_reset = ResetMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Prepare sync sync::ensure_ready(&sync, true); if !matcher_reset.no_sync() { sync.prepare()?; } // Ensure store is dirty, or forcing match sync.readyness()? { Readyness::Dirty => {} _ if matcher_main.force() => {} Readyness::Ready => { error::quit_error_msg( "nothing to reset, password store is not dirty", ErrorHintsBuilder::from_matcher(&matcher_main) .sync(true) .force(true) .build() .unwrap(), ); } other => { error::quit_error_msg( format!("unexpected sync state: {other:?}"), ErrorHints::from_matcher(&matcher_main), ); } } // List changed files if !matcher_main.quiet() { if let Err(err) = super::status::print_changed_files(&sync, &matcher_main) { error::print_error(err.context("failed to print list of changed files, ignoring")); } eprintln!(); } // Confirm eprintln!("Password store got into a dirty state unexpectedly."); eprintln!("Resetting the above changes may wipe sensitive information irrecoverably."); if !cli::prompt_yes("Reset above changes?", Some(false), &matcher_main) { if matcher_main.verbose() { eprintln!("Reset cancelled"); } error::quit(); } // Reset changes sync.reset_hard_all().map_err(Err::Reset)?; // Finalize sync if !matcher_reset.no_sync() { sync.finalize("Manual sync reset")?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; if !matcher_main.quiet() { eprintln!("Changes reset"); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to reset changes")] Reset(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/sync/status.rs000066400000000000000000000127251471372304600202060ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{ sync::{Readyness, Sync}, Store, }; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ sync::{status::StatusMatcher, SyncMatcher}, MainMatcher, Matcher, }, util::style::highlight, }; /// A sync status action. pub struct Status<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Status<'a> { /// Construct a new status action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the status action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_sync = SyncMatcher::with(self.cmd_matches).unwrap(); let _matcher_status = StatusMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let sync = store.sync(); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; // Show state let readyness = sync.readyness()?; let state_msg = match readyness { Readyness::NoSync => "not enabled".into(), Readyness::Ready => "ok".into(), Readyness::Dirty => "dirty".into(), Readyness::RepoState(state) => format!("other: {state:?}"), }; let is_dirty = readyness == Readyness::Dirty; let has_remote = readyness != Readyness::NoSync && sync.has_remote()?; if !matcher_main.quiet() { println!("Sync state: {state_msg}"); println!( "Uncommitted changes: {}", if is_dirty { "yes" } else { "no" } ); println!( "Remote configured: {}", if has_remote { "yes" } else { "no" } ); } // List changed files if dirty or in unexpected state let mut show_changes = is_dirty || matches!(readyness, Readyness::RepoState(_)); if show_changes { if !matcher_main.quiet() { eprintln!(); } show_changes = print_changed_files(&sync, &matcher_main)?; } // Show hints if !matcher_main.quiet() { eprintln!(); let bin = crate::util::bin_name(); if readyness == Readyness::NoSync { eprintln!( "Use '{}' to initialize sync for your password store", highlight(format!("{bin} sync init")) ); } else { if readyness == Readyness::Dirty { eprintln!( "Use '{}' to commit dirty changes in your password store", highlight(format!("{bin} sync commit")) ); eprintln!( "Use '{}' to reset dirty changes in your password store", highlight(format!("{bin} sync reset")) ); } if show_changes { eprintln!( "Use '{}' to view changed files in detail", highlight(format!("{bin} git status")) ); } if readyness != Readyness::Ready { eprintln!( "Use '{}' to inspect or resolve sync repository issues using git", highlight(format!("{bin} git")) ); } if !has_remote { eprintln!( "Use '{}' to configure a remote", highlight(format!("{bin} sync remote ")) ); } eprintln!( "Use '{}' to sync your password store", highlight(format!("{bin} sync")) ); } } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, true).map_err(Err::Tomb)?; Ok(()) } } /// Print a list of changed files. /// /// Returns false if no file was listed. pub(super) fn print_changed_files(sync: &Sync, matcher_main: &MainMatcher) -> Result { // List changed files, return early if empty let changed_files = sync .changed_files_raw(!matcher_main.verbose()) .map_err(Err::ChangedFiles)?; if changed_files.is_empty() { return Ok(false); } if !matcher_main.quiet() { eprintln!("Changed files:"); } changed_files.lines().for_each(|line| { if !matcher_main.quiet() { println!("- {line}") } else { println!("{line}") } }); Ok(true) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("failed to list changed files")] ChangedFiles(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/tomb/000077500000000000000000000000001471372304600162735ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/tomb/close.rs000066400000000000000000000052761471372304600177600ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; use crate::{ cmd::matcher::{ tomb::{close::CloseMatcher, TombMatcher}, MainMatcher, Matcher, }, util::error::{self, ErrorHintsBuilder}, }; /// A tomb close action. pub struct Close<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Close<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); let matcher_close = CloseMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Must be a tomb if !tomb.is_tomb() && !matcher_main.force() { if matcher_close.do_try() { return Ok(()); } // TODO: error hint to initialize tomb error::quit_error_msg( "password store is not a tomb", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } // Must be open if !tomb.is_open().map_err(Err::Close)? && !matcher_main.force() { if matcher_close.do_try() { return Ok(()); } error::quit_error_msg( "password store tomb is not open", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } if matcher_main.verbose() { eprintln!("Closing Tomb..."); } // Close the tomb tomb.close().map_err(Err::Close)?; // Close any running close timers if let Err(err) = tomb.stop_timer() { error::print_error(err.context("failed to stop auto closing systemd timer, ignoring")); } if !matcher_main.quiet() { eprintln!("Password store Tomb closed"); } // TODO: show warning if there are still files in tomb directory Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to close password store tomb")] Close(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/tomb/init.rs000066400000000000000000000116771471372304600176200ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Recipients, Store}; use thiserror::Error; use crate::{ cmd::matcher::{ tomb::{init::InitMatcher, TombMatcher}, MainMatcher, Matcher, }, util::{ self, cli, error::{self, ErrorHintsBuilder}, select, style, sync, }, }; /// A tomb init action. pub struct Init<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Init<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); let matcher_init = InitMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let sync = store.sync(); let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let timer = matcher_init.timer(); // Must not be a tomb already if tomb.is_tomb() && !matcher_main.force() { error::quit_error_msg( "password store already is a tomb", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } // Ask user to confirm eprintln!("This will create a new Tomb and will move your current password store into it."); if !cli::prompt_yes( "Are you sure you want to continue?", Some(true), &matcher_main, ) { if matcher_main.verbose() { eprintln!("Tomb initialisation cancelled"); } error::quit(); } // Prompt user to add force flag if !tomb.settings.force && util::tomb::ask_to_force(&matcher_main) { tomb.settings.force = true; } // Select GPG key to encrypt Tomb key let mut context = crate::crypto::context(&matcher_main)?; let tmp = Recipients::from(context.keys_private().map_err(Err::Load)?); let key = select::select_key(tmp.keys(), Some("Select key for Tomb")).ok_or(Err::NoGpgKey)?; // Prepare sync sync::ensure_ready(&sync, matcher_init.allow_dirty()); if !matcher_init.no_sync() { sync.prepare()?; } // TODO: ask user to add selected key to recipients if not yet part of it? // Select Tomb size to use let mbs = tomb .fetch_size_stats() .map(|sizes| sizes.desired_tomb_size()) .unwrap_or(10); if !matcher_main.quiet() { eprintln!("Initializing Tomb, this may take a while..."); eprintln!(); } // Initialize tomb tomb.init(key, mbs).map_err(Err::Init)?; // Finalize sync if !matcher_init.no_sync() { sync.finalize("Initialize Tomb")?; } // Run housekeeping crate::action::housekeeping::run::housekeeping( &store, matcher_init.allow_dirty(), matcher_init.no_sync(), ) .map_err(Err::Housekeeping)?; // Start timer if let Some(timer) = timer { if let Err(err) = tomb.stop_timer() { error::print_error(err.context( "failed to stop existing timer to automatically close password store tomb, ignoring", )); } tomb.start_timer(timer, true).map_err(Err::Timer)?; } if !matcher_main.quiet() { eprintln!(); if let Some(timer) = timer { eprintln!( "Password store Tomb initialized and opened, will close in {}", util::time::format_duration(timer) ); } else { eprintln!("Password store Tomb initialized and opened"); } eprintln!(); eprintln!("To close the Tomb, use:"); eprintln!( " {}", style::highlight(format!("{} tomb close", util::bin_name())) ); } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to initialize tomb")] Init(#[source] anyhow::Error), #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to run housekeeping tasks")] Housekeeping(#[source] anyhow::Error), #[error("failed to load usable keys from keychain")] Load(#[source] anyhow::Error), #[error("no GPG key selected to create tomb")] NoGpgKey, #[error("failed to start timer to automatically close password store tomb")] Timer(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/tomb/mod.rs000066400000000000000000000024521471372304600174230ustar00rootroot00000000000000pub mod close; pub mod init; pub mod open; pub mod resize; pub mod status; use anyhow::Result; use clap::ArgMatches; use crate::cmd::matcher::{tomb::TombMatcher, Matcher}; /// Tomb management action. pub struct Tomb<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Tomb<'a> { /// Construct a new sync action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the sync action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); if matcher_tomb.cmd_init().is_some() { return init::Init::new(self.cmd_matches).invoke(); } if matcher_tomb.cmd_open().is_some() { return open::Open::new(self.cmd_matches).invoke(); } if matcher_tomb.cmd_close().is_some() { return close::Close::new(self.cmd_matches).invoke(); } if matcher_tomb.cmd_status().is_some() { return status::Status::new(self.cmd_matches).invoke(); } if matcher_tomb.cmd_resize().is_some() { return resize::Resize::new(self.cmd_matches).invoke(); } // Unreachable, clap will print help for missing sub command instead unreachable!() } } prs-v0.5.2/cli/src/action/tomb/open.rs000066400000000000000000000102471471372304600176060ustar00rootroot00000000000000use anyhow::{anyhow, Result}; use clap::ArgMatches; use prs_lib::tomb::Tomb; use prs_lib::Store; use thiserror::Error; use crate::{ cmd::matcher::{ tomb::{open::OpenMatcher, TombMatcher}, MainMatcher, Matcher, }, util::{ self, error::{self, ErrorHintsBuilder}, style, }, }; /// A tomb open action. pub struct Open<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Open<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); let matcher_open = OpenMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let timer = matcher_open.timer(); // TODO: show warning if there already are files in tomb directory? // Must be a tomb if !tomb.is_tomb() && !matcher_main.force() { // TODO: error hint to initialize tomb error::quit_error_msg( "password store is not a tomb", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } // Must not be already open if tomb.is_open().map_err(Err::Open)? && !matcher_main.force() { error::quit_error_msg( "password store tomb is already open", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } // Open the tomb open(&mut tomb, &matcher_main)?; // Start timer if let Some(timer) = timer { if let Err(err) = tomb.stop_timer() { error::print_error(err.context( "failed to stop existing timer to automatically close password store tomb, ignoring", )); } tomb.start_timer(timer, true).map_err(Err::Timer)?; } if !matcher_main.quiet() { if let Some(timer) = timer { eprintln!( "Password store Tomb opened, will close in {}", util::time::format_duration(timer) ); } else { eprintln!("Password store Tomb opened"); } eprintln!(); eprintln!("To close the Tomb, use:"); eprintln!( " {}", style::highlight(format!("{} tomb close", util::bin_name())) ); } Ok(()) } } /// Open the tomb. pub(crate) fn open(tomb: &mut Tomb, matcher_main: &MainMatcher) -> Result<(), Err> { // Prompt user to add force flag if !tomb.settings.force && util::tomb::ask_to_force(matcher_main) { tomb.settings.force = true; } if matcher_main.verbose() { eprintln!("Opening Tomb..."); } // Open the tomb let errs = tomb.open().map_err(Err::Open)?; // Report soft-fail errors to the user let show_error_hints = !errs.is_empty(); for err in errs { error::print_error( anyhow!(err).context("failed to run housekeeping task after opening tomb, ignoring"), ); } if show_error_hints { error::ErrorHintsBuilder::from_matcher(matcher_main) .force(true) .verbose(true) .build() .unwrap() .print(true); } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to open password store tomb")] Open(#[source] anyhow::Error), #[error("failed to start timer to automatically close password store tomb")] Timer(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/tomb/resize.rs000066400000000000000000000075141471372304600201510ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; use crate::cmd::matcher::{ tomb::{resize::ResizeMatcher, TombMatcher}, MainMatcher, Matcher, }; use crate::util::{ error, error::{ErrorHints, ErrorHintsBuilder}, style, }; /// A tomb resize action. pub struct Resize<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Resize<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); let matcher_resize = ResizeMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Must be a tomb if !tomb.is_tomb() && !matcher_main.force() { // TODO: error hint to initialize tomb error::quit_error_msg( "password store is not a tomb", ErrorHintsBuilder::from_matcher(&matcher_main) .force(true) .build() .unwrap(), ); } // Fetch Tomb size status let sizes = tomb.fetch_size_stats().map_err(Err::Size)?; // Get size, automatically select if not given let size = match matcher_resize.size() { Some(size) => size, None => { // Get desired size let size = sizes.desired_tomb_size(); // Quit if Tomb is already this big if let Some(tomb_size) = sizes.tomb_file_size_mbs() { if tomb_size >= size { eprintln!("Tomb is large enough, not resizing ({tomb_size}MB)"); eprintln!( "Use '{}' flag to specify a size", style::highlight("--size MEGABYTE") ); error::quit(); } } size } }; // Must be closed let tomb_open = tomb.is_open().unwrap_or(false); if tomb_open && !matcher_main.force() { if matcher_main.verbose() { eprintln!("Closing Tomb..."); } // Close the tomb tomb.close().map_err(Err::Close)?; } // New tomb size must be larger if let Some(tomb_file_size) = sizes.tomb_file_size_mbs() { if size <= tomb_file_size { error::quit_error_msg( format!("new tomb size must be larger than current size ({tomb_file_size}MB)",), ErrorHints::default(), ); } } // Resize tomb if !matcher_main.quiet() { eprintln!("Resizing Tomb to {size}MB..."); } tomb.resize(size).map_err(Err::Resize)?; // Open tomb if it was open before if tomb_open { super::open::open(&mut tomb, &matcher_main).map_err(Err::Open)?; } Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to resize tomb")] Resize(#[source] anyhow::Error), #[error("failed to open tomb after resizing")] Open(#[source] super::open::Err), #[error("failed to close tomb before resizing")] Close(#[source] anyhow::Error), #[error("failed to fetch password store size status")] Size(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/tomb/status.rs000066400000000000000000000054551471372304600201750ustar00rootroot00000000000000use anyhow::Result; use bytesize::ByteSize; use clap::ArgMatches; use prs_lib::Store; use thiserror::Error; use crate::cmd::matcher::{ tomb::{status::StatusMatcher, TombMatcher}, MainMatcher, Matcher, }; /// A tomb status action. pub struct Status<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Status<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_tomb = TombMatcher::with(self.cmd_matches).unwrap(); let matcher_status = StatusMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; let tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); let is_tomb = tomb.is_tomb(); if !is_tomb { eprintln!("Tomb: no"); return Ok(()); } // Open tomb on requet let mut is_open = tomb.is_open().map_err(Err::Status)?; if matcher_status.open() && !is_open { if !matcher_main.quiet() { eprintln!("Opening password store Tomb..."); } tomb.open().map_err(Err::Open)?; is_open = true; } let has_timer = tomb.has_timer().map_err(Err::Status)?; let tomb_path = tomb.find_tomb_path().unwrap(); let tomb_key_path = tomb.find_tomb_key_path().unwrap(); let sizes = tomb.fetch_size_stats().map_err(Err::Size)?; println!("Tomb: yes"); println!("Open: {}", if is_open { "yes" } else { "no" }); println!("Close timer: {}", if has_timer { "active" } else { "no" }); println!("Tomb path: {}", tomb_path.display()); println!("Tomb key path: {}", tomb_key_path.display()); println!( "Store size: {}", sizes .store .map(|s| ByteSize(s).to_string()) .unwrap_or_else(|| "?".into()) ); println!( "Tomb file size: {}", sizes .tomb_file .map(|s| ByteSize(s).to_string()) .unwrap_or_else(|| "?".into()) ); Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[error("failed to query password store tomb status")] Status(#[source] anyhow::Error), #[error("failed to open password store tomb")] Open(#[source] anyhow::Error), #[error("failed to fetch password store size status")] Size(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/totp/000077500000000000000000000000001471372304600163205ustar00rootroot00000000000000prs-v0.5.2/cli/src/action/totp/copy.rs000066400000000000000000000102421471372304600176370ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::util::{clipboard, error}; use crate::{ cmd::matcher::{ totp::{copy::CopyMatcher, TotpMatcher}, MainMatcher, Matcher, }, util::{secret, select, totp}, }; /// A TOTP copy action. pub struct Copy<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Copy<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_totp = TotpMatcher::with(self.cmd_matches).unwrap(); let matcher_copy = CopyMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_copy.query()).ok_or(Err::NoneSelected)?; secret::print_name(matcher_copy.query(), &secret, &store, matcher_main.quiet()); let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to property if let Some(property) = matcher_copy.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } // Get TOTP instance, determine timeout let totp = totp::find_token(&plaintext) .ok_or(Err::NoTotp)? .map_err(Err::Totp)?; let timeout = matcher_copy .timeout() .unwrap_or(Ok(crate::CLIPBOARD_TIMEOUT))?; let mut copied = false; // Use background token recopy implementation if token changes within timeout let ttl = totp.ttl().map_err(Err::Totp)?; if timeout > ttl && !matcher_copy.no_recopy() { match totp::spawn_process_totp_recopy(&totp, timeout) { Ok(_) => { if !matcher_main.quiet() { eprintln!("Token copied to clipboard. Clearing after {timeout} seconds...",); } copied = true; } Err(err) => error::print_error(Err::Recopy(err).into()), } } // Fall back to simply copy if !copied { clipboard::copy_plaintext( totp.generate_current().map_err(Err::Totp)?, false, true, matcher_main.quiet(), matcher_main.verbose(), timeout, )?; if !matcher_main.quiet() { eprintln!("Token copied to clipboard. Clearing after {timeout} seconds...",); } } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), #[error("no TOTP secret found")] NoTotp, #[error("failed to generate TOTP token")] Totp(#[source] anyhow::Error), #[error("failed to use TOTP recopy system, falling back to simple copy")] Recopy(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/totp/live.rs000066400000000000000000000076221471372304600176340ustar00rootroot00000000000000use std::thread; use std::time::Duration; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ totp::{live::LiveMatcher, TotpMatcher}, MainMatcher, Matcher, }, util::{ secret, select, totp::{self, Totp}, }, }; /// A TOTP live action. pub struct Live<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Live<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_totp = TotpMatcher::with(self.cmd_matches).unwrap(); let matcher_live = LiveMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_live.query()).ok_or(Err::NoneSelected)?; secret::print_name(matcher_live.query(), &secret, &store, matcher_main.quiet()); let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to property if let Some(property) = matcher_live.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } // Get current TOTP token let totp = totp::find_token(&plaintext) .ok_or(Err::NoTotp)? .map_err(Err::Totp)?; // Finalize tomb before watching tokens #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; // Watch or follow tokens if !matcher_live.follow() { watch(totp, matcher_main.quiet())?; } else { follow(totp, matcher_main.quiet())?; } Ok(()) } } /// Watch the token. /// /// Show countdown if not quiet, clear when a new token is shown. fn watch(totp: Totp, quiet: bool) -> Result<()> { loop { let token = totp.generate_current().map_err(Err::Totp)?; let ttl = totp.ttl().map_err(Err::Totp)?; totp::print_token(&token, quiet, Some(ttl)); thread::sleep(Duration::from_secs(if !quiet { 1 } else { ttl })); eprint!("{}", ansi_escapes::EraseLines(2)); } } /// Follow the token. /// /// Keep printing new tokens on a new line as they arrive. fn follow(totp: Totp, quiet: bool) -> Result<()> { loop { let token = totp.generate_current().map_err(Err::Totp)?; let ttl = totp.ttl().map_err(Err::Totp)?; totp::print_token(&token, quiet, Some(ttl)); thread::sleep(Duration::from_secs(ttl)); } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), #[error("no TOTP secret found")] NoTotp, #[error("failed to generate TOTP token")] Totp(#[source] anyhow::Error), } prs-v0.5.2/cli/src/action/totp/mod.rs000066400000000000000000000023001471372304600174400ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod copy; pub mod live; pub mod qr; pub mod show; use anyhow::Result; use clap::ArgMatches; use crate::cmd::matcher::{totp::TotpMatcher, Matcher}; /// TOTP action. pub struct Totp<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Totp<'a> { /// Construct a new sync action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the sync action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_totp = TotpMatcher::with(self.cmd_matches).unwrap(); #[cfg(feature = "clipboard")] if matcher_totp.cmd_copy().is_some() { return copy::Copy::new(self.cmd_matches).invoke(); } if matcher_totp.cmd_live().is_some() { return live::Live::new(self.cmd_matches).invoke(); } if matcher_totp.cmd_qr().is_some() { return qr::Qr::new(self.cmd_matches).invoke(); } if matcher_totp.cmd_show().is_some() { return show::Show::new(self.cmd_matches).invoke(); } // Unreachable, clap will print help for missing sub command instead unreachable!() } } prs-v0.5.2/cli/src/action/totp/qr.rs000066400000000000000000000062751471372304600173220ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::{ cmd::matcher::{ totp::{qr::QrMatcher, TotpMatcher}, MainMatcher, Matcher, }, util::{secret, select, totp}, }; /// A TOTP QR code action. pub struct Qr<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Qr<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_totp = TotpMatcher::with(self.cmd_matches).unwrap(); let matcher_qr = QrMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_qr.query()).ok_or(Err::NoneSelected)?; secret::print_name(matcher_qr.query(), &secret, &store, matcher_main.quiet()); let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to property if let Some(property) = matcher_qr.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } // Get current TOTP token let totp = totp::find_token(&plaintext) .ok_or(Err::NoTotp)? .map_err(Err::Parse)?; let url = totp.generate_url(); // Print TOTP URL and QR code if !matcher_main.quiet() { print!("TOTP: "); } println!("{}", url.unsecure_to_str().unwrap_or("?")); if !matcher_main.quiet() { qr2term::print_qr(url.unsecure_ref()).map_err(Err::Qr)?; } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), #[error("no TOTP secret found")] NoTotp, #[error("failed to parse TOTP secret")] Parse(#[source] anyhow::Error), #[error("failed to generate and print QR code")] Qr(#[source] qr2term::QrError), } prs-v0.5.2/cli/src/action/totp/show.rs000066400000000000000000000102031471372304600176420ustar00rootroot00000000000000use std::time::Duration; use anyhow::Result; use clap::ArgMatches; use prs_lib::{crypto::prelude::*, Store}; use thiserror::Error; #[cfg(feature = "clipboard")] use crate::util::clipboard; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::util::tomb; use crate::viewer; use crate::{ cmd::matcher::{ totp::{show::ShowMatcher, TotpMatcher}, MainMatcher, Matcher, }, util::{secret, select, totp}, }; /// A TOTP show action. pub struct Show<'a> { cmd_matches: &'a ArgMatches, } impl<'a> Show<'a> { /// Construct a new init action. pub fn new(cmd_matches: &'a ArgMatches) -> Self { Self { cmd_matches } } /// Invoke the init action. pub fn invoke(&self) -> Result<()> { // Create the command matchers let matcher_main = MainMatcher::with(self.cmd_matches).unwrap(); let _matcher_totp = TotpMatcher::with(self.cmd_matches).unwrap(); let matcher_show = ShowMatcher::with(self.cmd_matches).unwrap(); let store = Store::open(matcher_main.store()).map_err(Err::Store)?; #[cfg(all(feature = "tomb", target_os = "linux"))] let mut tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::prepare_tomb(&mut tomb, &matcher_main).map_err(Err::Tomb)?; let secret = select::store_select_secret(&store, matcher_show.query()).ok_or(Err::NoneSelected)?; let mut plaintext = crate::crypto::context(&matcher_main)? .decrypt_file(&secret.path) .map_err(Err::Read)?; // Trim plaintext to property if let Some(property) = matcher_show.property() { plaintext = plaintext.property(property).map_err(Err::Property)?; } // Get current TOTP token let totp = totp::find_token(&plaintext) .ok_or(Err::NoTotp)? .map_err(Err::Parse)?; let token = totp.generate_current().map_err(Err::Totp)?; let ttl = totp.ttl().map_err(Err::Totp)?; // Copy to clipboard #[cfg(feature = "clipboard")] if matcher_show.copy() { clipboard::copy_plaintext( token.clone(), true, !matcher_main.force(), matcher_main.quiet(), matcher_main.verbose(), matcher_show .timeout() .unwrap_or(Ok(crate::CLIPBOARD_TIMEOUT))?, )?; } // Show directly or in viewer if matcher_show.viewer() { viewer::viewer( &store, &secret, totp::format_token(&token, matcher_main.quiet(), Some(ttl)), matcher_show.timeout().transpose()?.map(Duration::from_secs), &matcher_main, matcher_show.query(), ) .map_err(Err::Viewer)?; } else { secret::print_name(matcher_show.query(), &secret, &store, matcher_main.quiet()); totp::print_token(&token, matcher_main.quiet(), Some(ttl)); } // Finalize tomb #[cfg(all(feature = "tomb", target_os = "linux"))] tomb::finalize_tomb(&mut tomb, &matcher_main, false).map_err(Err::Tomb)?; Ok(()) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to access password store")] Store(#[source] anyhow::Error), #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to prepare password store tomb for usage")] Tomb(#[source] anyhow::Error), #[error("no secret selected")] NoneSelected, #[error("failed to read secret")] Read(#[source] anyhow::Error), #[error("failed to select property from secret")] Property(#[source] anyhow::Error), #[error("no TOTP secret found")] NoTotp, #[error("failed to parse TOTP secret")] Parse(#[source] anyhow::Error), #[error("failed to generate TOTP token")] Totp(#[source] anyhow::Error), #[error("failed to start secret viewer")] Viewer(#[source] anyhow::Error), } prs-v0.5.2/cli/src/cmd/000077500000000000000000000000001471372304600146205ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/arg/000077500000000000000000000000001471372304600153715ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/arg/allow_dirty.rs000066400000000000000000000011051471372304600202650ustar00rootroot00000000000000use clap::Arg; use super::{CmdArg, CmdArgFlag}; /// The allow-dirty argument. pub struct ArgAllowDirty {} impl CmdArg for ArgAllowDirty { fn name() -> &'static str { "allow-dirty" } fn build() -> Arg { Arg::new("allow-dirty") .long("allow-dirty") .short('d') .alias("dirty") .alias("sync-allow-dirty") .alias("sync-dirty") .num_args(0) .global(true) .help("Allow commit and sync on dirty store repository") } } impl CmdArgFlag for ArgAllowDirty {} prs-v0.5.2/cli/src/cmd/arg/mod.rs000066400000000000000000000034111471372304600165150ustar00rootroot00000000000000pub mod allow_dirty; pub mod no_sync; pub mod property; pub mod query; pub mod store; pub mod timeout; pub mod viewer; use clap::{parser::ValuesRef, Arg, ArgMatches}; // Re-export to arg module pub use self::allow_dirty::ArgAllowDirty; pub use self::no_sync::ArgNoSync; pub use self::property::ArgProperty; pub use self::query::ArgQuery; pub use self::store::ArgStore; pub use self::timeout::ArgTimeout; pub use self::viewer::ArgViewer; /// A generic trait, for a reusable command argument struct. /// The `CmdArgFlag` and `CmdArgOption` traits further specify what kind of /// argument this is. pub trait CmdArg { /// Get the argument name that is used as main identifier. fn name() -> &'static str; /// Build the argument. fn build() -> Arg; } /// This `CmdArg` specification defines that this argument may be tested as /// flag. This will allow to test whether the flag is present in the given /// matches. pub trait CmdArgFlag: CmdArg { /// Check whether the argument is present in the given matches. fn is_present(matches: &ArgMatches) -> bool { matches.get_flag(Self::name()) } } /// This `CmdArg` specification defines that this argument may be tested as /// option. This will allow to fetch the value of the argument. pub trait CmdArgOption<'a>: CmdArg { /// The type of the argument value. type Value; /// Get the argument value. fn value(matches: &'a ArgMatches) -> Self::Value; /// Get the raw argument value, as a string reference. fn value_raw(matches: &'a ArgMatches) -> Option<&String> { matches.get_one(Self::name()) } /// Get the raw argument values, as a string reference. fn values_raw(matches: &'a ArgMatches) -> Option> { matches.get_many(Self::name()) } } prs-v0.5.2/cli/src/cmd/arg/no_sync.rs000066400000000000000000000012101471372304600174010ustar00rootroot00000000000000use clap::Arg; use super::{CmdArg, CmdArgFlag}; /// The no-sync argument. pub struct ArgNoSync {} impl CmdArg for ArgNoSync { fn name() -> &'static str { "no-sync" } fn build() -> Arg { Arg::new("no-sync") .long("no-sync") .short('D') .alias("keep-dirty") .alias("sync-no-sync") .alias("sync-keep-dirty") .num_args(0) .global(true) // This prevents: sync before action, committing changes, sync after action .help("Do not commit and sync changes, keep store dirty") } } impl CmdArgFlag for ArgNoSync {} prs-v0.5.2/cli/src/cmd/arg/property.rs000066400000000000000000000012241471372304600176220ustar00rootroot00000000000000use clap::{Arg, ArgMatches}; use super::{CmdArg, CmdArgOption}; /// The property argument. pub struct ArgProperty {} impl CmdArg for ArgProperty { fn name() -> &'static str { "property" } fn build() -> Arg { Arg::new("property") .long("property") .short('p') .alias("prop") .value_name("NAME") .num_args(1) .global(true) .help("Select a specific property") } } impl<'a> CmdArgOption<'a> for ArgProperty { type Value = Option<&'a String>; fn value(matches: &'a ArgMatches) -> Self::Value { Self::value_raw(matches) } } prs-v0.5.2/cli/src/cmd/arg/query.rs000066400000000000000000000010321471372304600171000ustar00rootroot00000000000000use clap::{Arg, ArgMatches}; use super::{CmdArg, CmdArgOption}; /// The query argument. pub struct ArgQuery {} impl CmdArg for ArgQuery { fn name() -> &'static str { "QUERY" } fn build() -> Arg { Arg::new("QUERY").help("Secret query") } } impl<'a> CmdArgOption<'a> for ArgQuery { type Value = Option; fn value(matches: &'a ArgMatches) -> Self::Value { let parts: Vec = Self::values_raw(matches)?.map(|s| s.to_string()).collect(); Some(parts.join(" ")) } } prs-v0.5.2/cli/src/cmd/arg/store.rs000066400000000000000000000013751471372304600171010ustar00rootroot00000000000000use clap::{Arg, ArgMatches}; use super::{CmdArg, CmdArgOption}; /// The store argument. pub struct ArgStore {} impl CmdArg for ArgStore { fn name() -> &'static str { "store" } fn build() -> Arg { Arg::new("store") .long("store") .short('s') .value_name("PATH") .env("PASSWORD_STORE_DIR") .num_args(1) .global(true) .help("Password store to use") } } impl<'a> CmdArgOption<'a> for ArgStore { type Value = String; fn value(matches: &'a ArgMatches) -> Self::Value { Self::value_raw(matches) .filter(|p| !p.trim().is_empty()) .unwrap_or(&prs_lib::STORE_DEFAULT_ROOT.into()) .to_string() } } prs-v0.5.2/cli/src/cmd/arg/timeout.rs000066400000000000000000000022131471372304600174230ustar00rootroot00000000000000use anyhow::Result; use clap::{Arg, ArgMatches}; use thiserror::Error; use super::{CmdArg, CmdArgOption}; /// The timeout argument. pub struct ArgTimeout {} impl ArgTimeout { #[cfg(feature = "clipboard")] pub fn value_or_default(matches: &ArgMatches) -> Result { Self::value(matches).unwrap_or(Ok(crate::CLIPBOARD_TIMEOUT)) } } impl CmdArg for ArgTimeout { fn name() -> &'static str { "timeout" } fn build() -> Arg { Arg::new("timeout") .long("timeout") .short('t') .alias("time") .alias("seconds") .alias("second") .value_name("SECONDS") .num_args(1) .global(true) .help("Timeout after which to clear clipboard") } } impl<'a> CmdArgOption<'a> for ArgTimeout { type Value = Option>; fn value(matches: &'a ArgMatches) -> Self::Value { Self::value_raw(matches).map(|t| t.parse().map_err(|err| Err::Parse(err).into())) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to parse timeout as seconds")] Parse(#[source] std::num::ParseIntError), } prs-v0.5.2/cli/src/cmd/arg/viewer.rs000066400000000000000000000007121471372304600172400ustar00rootroot00000000000000use clap::Arg; use super::{CmdArg, CmdArgFlag}; /// The viewer argument. pub struct ArgViewer {} impl CmdArg for ArgViewer { fn name() -> &'static str { "viewer" } fn build() -> Arg { Arg::new("viewer") .long("viewer") .short('V') .alias("pager") .num_args(0) .global(true) .help("Show output in secure viewer") } } impl CmdArgFlag for ArgViewer {} prs-v0.5.2/cli/src/cmd/handler.rs000066400000000000000000000174011471372304600166060ustar00rootroot00000000000000use clap::{Arg, ArgAction, ArgMatches, Command}; use super::arg::{ArgStore, CmdArg}; use super::matcher::{self, Matcher}; use super::subcmd; /// Custom template for help const HELP_TEMPLATE: &str = "\ {before-help}{name} {version} {author-with-newline}{about-with-newline} {usage-heading} {usage} {all-args}{after-help} "; /// CLI argument handler. pub struct Handler { /// The CLI matches. matches: ArgMatches, } impl<'a> Handler { /// Build the application CLI definition. pub fn build() -> Command { // Build the CLI application definition let app = Command::new(crate::NAME) .version(crate_version!()) .author(crate_authors!()) .about(crate_description!()) .help_template(HELP_TEMPLATE) .help_expected(true) .arg( Arg::new("force") .long("force") .short('f') .num_args(0) .global(true) .help("Force the action, ignore warnings"), ) .arg( Arg::new("no-interact") .long("no-interact") .short('I') .alias("no-interactive") .alias("non-interactive") .num_args(0) .global(true) .help("Not interactive, do not prompt"), ) .arg( Arg::new("yes") .long("yes") .short('y') .alias("assume-yes") .num_args(0) .global(true) .help("Assume yes for prompts"), ) .arg( Arg::new("quiet") .long("quiet") .short('q') .num_args(0) .global(true) .help("Produce output suitable for logging and automation"), ) .arg( Arg::new("verbose") .long("verbose") .short('v') .action(ArgAction::Count) .num_args(0) .global(true) .help("Enable verbose information and logging"), ) .arg(ArgStore::build()) .arg( Arg::new("gpg-tty") .long("gpg-tty") .num_args(0) .global(true) .help("Instruct GPG to ask passphrase in TTY rather than pinentry"), ) .subcommand(subcmd::CmdShow::build()); #[cfg(feature = "clipboard")] let app = app.subcommand(subcmd::CmdCopy::build()); let app = app .subcommand(subcmd::CmdGenerate::build()) .subcommand(subcmd::CmdAdd::build()) .subcommand(subcmd::CmdEdit::build()) .subcommand(subcmd::CmdDuplicate::build()); #[cfg(feature = "alias")] let app = app.subcommand(subcmd::CmdAlias::build()); let app = app .subcommand(subcmd::CmdMove::build()) .subcommand(subcmd::CmdRemove::build()) .subcommand(subcmd::CmdList::build()) .subcommand(subcmd::CmdGrep::build()) .subcommand(subcmd::CmdInit::build()) .subcommand(subcmd::CmdClone::build()) .subcommand(subcmd::CmdSync::build()) .subcommand(subcmd::CmdSlam::build()); #[cfg(feature = "totp")] let app = app.subcommand(subcmd::CmdTotp::build()); let app = app .subcommand(subcmd::CmdRecipients::build()) .subcommand(subcmd::CmdGit::build()); #[cfg(all(feature = "tomb", target_os = "linux"))] let app = app.subcommand(subcmd::CmdTomb::build()); app.subcommand(subcmd::CmdHousekeeping::build()) .subcommand(subcmd::CmdInternal::build()) } /// Parse CLI arguments. pub fn parse() -> Handler { Handler { matches: Handler::build().get_matches(), } } /// Get the raw matches. pub fn matches(&'a self) -> &'a ArgMatches { &self.matches } /// Get the add sub command, if matched. pub fn add(&'a self) -> Option { matcher::AddMatcher::with(&self.matches) } /// Get the alias sub command, if matched. #[cfg(feature = "alias")] pub fn alias(&'a self) -> Option { matcher::AliasMatcher::with(&self.matches) } /// Get the clone sub command, if matched. pub fn clone(&'a self) -> Option { matcher::CloneMatcher::with(&self.matches) } /// Get the copy sub command, if matched. #[cfg(feature = "clipboard")] pub fn copy(&'a self) -> Option { matcher::CopyMatcher::with(&self.matches) } /// Get the duplicate sub command, if matched. pub fn duplicate(&'a self) -> Option { matcher::DuplicateMatcher::with(&self.matches) } /// Get the edit sub command, if matched. pub fn edit(&'a self) -> Option { matcher::EditMatcher::with(&self.matches) } /// Get the generate sub command, if matched. pub fn generate(&'a self) -> Option { matcher::GenerateMatcher::with(&self.matches) } /// Get the git sub command, if matched. pub fn git(&'a self) -> Option { matcher::GitMatcher::with(&self.matches) } /// Get the grep sub command, if matched. pub fn grep(&'a self) -> Option { matcher::GrepMatcher::with(&self.matches) } /// Get the housekeeping sub command, if matched. pub fn housekeeping(&'a self) -> Option { matcher::HousekeepingMatcher::with(&self.matches) } /// Get the init sub command, if matched. pub fn init(&'a self) -> Option { matcher::InitMatcher::with(&self.matches) } /// Get the internal sub command, if matched. pub fn internal(&'a self) -> Option { matcher::InternalMatcher::with(&self.matches) } /// Get the list sub command, if matched. pub fn list(&'a self) -> Option { matcher::ListMatcher::with(&self.matches) } /// Get the slam sub command, if matched. pub fn slam(&'a self) -> Option { matcher::SlamMatcher::with(&self.matches) } /// Get the move sub command, if matched. pub fn r#move(&'a self) -> Option { matcher::MoveMatcher::with(&self.matches) } /// Get the recipients sub command, if matched. pub fn recipients(&'a self) -> Option { matcher::RecipientsMatcher::with(&self.matches) } /// Get the remove sub command, if matched. pub fn remove(&'a self) -> Option { matcher::RemoveMatcher::with(&self.matches) } /// Get the show sub command, if matched. pub fn show(&'a self) -> Option { matcher::ShowMatcher::with(&self.matches) } /// Get the sync sub command, if matched. pub fn sync(&'a self) -> Option { matcher::SyncMatcher::with(&self.matches) } /// Get the tomb sub command, if matched. #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn tomb(&'a self) -> Option { matcher::TombMatcher::with(&self.matches) } /// Get the TOTP sub command, if matched. #[cfg(feature = "totp")] pub fn totp(&'a self) -> Option { matcher::TotpMatcher::with(&self.matches) } } prs-v0.5.2/cli/src/cmd/matcher/000077500000000000000000000000001471372304600162435ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/add.rs000066400000000000000000000020651471372304600173440ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The add command matcher. pub struct AddMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> AddMatcher<'a> { /// Secret destination. pub fn name(&self) -> &String { self.matches.get_one("NAME").unwrap() } /// Check whether to create an empty secret. pub fn empty(&self) -> bool { self.matches.get_flag("empty") } /// Check whether to read from stdin. pub fn stdin(&self) -> bool { self.matches.get_flag("stdin") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for AddMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("add") .map(|matches| AddMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/alias.rs000066400000000000000000000017301471372304600177030ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The alias command matcher. pub struct AliasMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> AliasMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Secret destination. pub fn destination(&self) -> &String { self.matches.get_one("DEST").unwrap() } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for AliasMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("alias") .map(|matches| AliasMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/clone.rs000066400000000000000000000010121471372304600177030ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The clone command matcher. pub struct CloneMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CloneMatcher<'a> { /// The git URL to clone from. pub fn git_url(&self) -> &String { self.matches.get_one("GIT_URL").unwrap() } } impl<'a> Matcher<'a> for CloneMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("clone") .map(|matches| CloneMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/copy.rs000066400000000000000000000017301471372304600175640ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, CmdArgOption}; /// The copy command matcher. pub struct CopyMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CopyMatcher<'a> { /// Check whether to copy all of the secret. pub fn all(&self) -> bool { self.matches.get_flag("all") } /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Clipboard timeout in seconds. pub fn timeout(&self) -> Result { ArgTimeout::value_or_default(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } } impl<'a> Matcher<'a> for CopyMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("copy") .map(|matches| CopyMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/delete.rs000066400000000000000000000010601471372304600200500ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgQuery, CmdArgOption}; /// The delete command matcher. pub struct DeleteMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> DeleteMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } } impl<'a> Matcher<'a> for DeleteMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("delete") .map(|matches| DeleteMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/duplicate.rs000066400000000000000000000017601471372304600205670ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The duplicate command matcher. pub struct DuplicateMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> DuplicateMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Secret destination. pub fn destination(&self) -> &String { self.matches.get_one("DEST").unwrap() } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for DuplicateMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("duplicate") .map(|matches| DuplicateMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/edit.rs000066400000000000000000000017201471372304600175360ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The edit command matcher. pub struct EditMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> EditMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Check whether to read from stdin. pub fn stdin(&self) -> bool { self.matches.get_flag("stdin") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for EditMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("edit") .map(|matches| EditMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/generate.rs000066400000000000000000000046241471372304600204110ustar00rootroot00000000000000#[cfg(feature = "clipboard")] use anyhow::Result; use clap::ArgMatches; use super::Matcher; #[cfg(feature = "clipboard")] use crate::cmd::arg::ArgTimeout; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// Default password length in characters. const PASSWORD_LENGTH: u16 = 24; /// Default passphrase length in words. const PASSPHRASE_LENGTH: u16 = 5; /// The generate command matcher. pub struct GenerateMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> GenerateMatcher<'a> { /// Secret name. pub fn name(&self) -> Option<&String> { self.matches.get_one("NAME") } /// Check whether to generate a passphrase. pub fn passphrase(&self) -> bool { self.matches.get_flag("passphrase") } /// What length to use. pub fn length(&self) -> u16 { self.matches .get_one("length") .map(|l: &String| l.parse().expect("invalid length")) .unwrap_or_else(|| { if self.passphrase() { PASSPHRASE_LENGTH } else { PASSWORD_LENGTH } }) } /// Check whether to merge the secret. pub fn merge(&self) -> bool { self.matches.get_flag("merge") } /// Check whether to edit the secret. pub fn edit(&self) -> bool { self.matches.get_flag("edit") } /// Check whether to read from stdin. pub fn stdin(&self) -> bool { self.matches.get_flag("stdin") } /// Check whether to read from copy. #[cfg(feature = "clipboard")] pub fn copy(&self) -> bool { self.matches.get_flag("copy") } /// Clipboard timeout in seconds. #[cfg(feature = "clipboard")] pub fn timeout(&self) -> Result { ArgTimeout::value_or_default(self.matches) } /// Check whether to read from show. pub fn show(&self) -> bool { self.matches.get_flag("show") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for GenerateMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("generate") .map(|matches| GenerateMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/git.rs000066400000000000000000000012311471372304600173710ustar00rootroot00000000000000use clap::{parser::ValuesRef, ArgMatches}; use super::Matcher; /// The git command matcher. pub struct GitMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> GitMatcher<'a> { /// Get the git command to invoke. pub fn command(&self) -> String { self.matches .get_many("COMMAND") .map(|c: ValuesRef| c.map(|s| s.as_str()).collect::>().join(" ")) .unwrap_or_default() } } impl<'a> Matcher<'a> for GitMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("git") .map(|matches| GitMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/grep.rs000066400000000000000000000016221471372304600175470ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The grep command matcher. pub struct GrepMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> GrepMatcher<'a> { /// The grep pattern. pub fn pattern(&self) -> String { self.matches.get_one("PATTERN").cloned().unwrap() } /// The secret query. pub fn query(&self) -> Option { self.matches.get_one("query").cloned() } /// Whether to parse the pattern as regular expression. pub fn regex(&self) -> bool { self.matches.get_flag("regex") } /// Whether to include searching aliases. pub fn with_aliases(&self) -> bool { self.matches.get_flag("aliases") } } impl<'a> Matcher<'a> for GrepMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("grep") .map(|matches| GrepMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/housekeeping/000077500000000000000000000000001471372304600207315ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/housekeeping/mod.rs000066400000000000000000000020731471372304600220600ustar00rootroot00000000000000pub mod recrypt; pub mod run; pub mod sync_keys; use clap::ArgMatches; use super::Matcher; /// The housekeeping matcher. pub struct HousekeepingMatcher<'a> { root: &'a ArgMatches, _matches: &'a ArgMatches, } impl<'a: 'b, 'b> HousekeepingMatcher<'a> { /// Get the housekepeing recrypt sub command, if matched. pub fn recrypt(&'a self) -> Option { recrypt::RecryptMatcher::with(self.root) } /// Get the housekepeing run sub command, if matched. pub fn run(&'a self) -> Option { run::RunMatcher::with(self.root) } /// Get the housekepeing sync-keys sub command, if matched. pub fn sync_keys(&'a self) -> Option { sync_keys::SyncKeysMatcher::with(self.root) } } impl<'a> Matcher<'a> for HousekeepingMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("housekeeping") .map(|matches| HousekeepingMatcher { root, _matches: matches, }) } } prs-v0.5.2/cli/src/cmd/matcher/housekeeping/recrypt.rs000066400000000000000000000016461471372304600227760ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The housekeeping recrypt command matcher. pub struct RecryptMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> RecryptMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for RecryptMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("housekeeping")? .subcommand_matches("recrypt") .map(|matches| RecryptMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/housekeeping/run.rs000066400000000000000000000014031471372304600221010ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The housekeeping run command matcher. pub struct RunMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> RunMatcher<'a> { /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for RunMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("housekeeping")? .subcommand_matches("run") .map(|matches| RunMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/housekeeping/sync_keys.rs000066400000000000000000000016551471372304600233150ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The housekeeping sync-keys command matcher. pub struct SyncKeysMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> SyncKeysMatcher<'a> { /// Check whether to not import missing keys. pub fn no_import(&self) -> bool { self.matches.get_flag("no-import") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for SyncKeysMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("housekeeping")? .subcommand_matches("sync-keys") .map(|matches| SyncKeysMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/init.rs000066400000000000000000000006151471372304600175560ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The init command matcher. pub struct InitMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> InitMatcher<'a> {} impl<'a> Matcher<'a> for InitMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("init") .map(|matches| InitMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/internal/000077500000000000000000000000001471372304600200575ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/internal/clip.rs000066400000000000000000000007101471372304600213520ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The internal clipboard command matcher. pub struct ClipMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> ClipMatcher<'a> {} impl<'a> Matcher<'a> for ClipMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("internal")? .subcommand_matches("clip") .map(|matches| ClipMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/internal/clip_revert.rs000066400000000000000000000012431471372304600227430ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::ArgTimeout; /// The internal clipboard revert command matcher. pub struct ClipRevertMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ClipRevertMatcher<'a> { /// Clipboard timeout in seconds. pub fn timeout(&self) -> Result { ArgTimeout::value_or_default(self.matches) } } impl<'a> Matcher<'a> for ClipRevertMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("internal")? .subcommand_matches("clip-revert") .map(|matches| ClipRevertMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/internal/completions.rs000066400000000000000000000073451471372304600227720ustar00rootroot00000000000000use std::fmt; use std::path::PathBuf; use clap::ArgMatches; use super::Matcher; use crate::util; /// The completions completions command matcher. pub struct CompletionsMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CompletionsMatcher<'a> { /// Get the shells to generate completions for. pub(crate) fn shells(&'a self) -> Vec { // Get the raw list of shells let raw = self .matches .get_many("SHELL") .expect("no shells were given"); // Parse the list of shell names, deduplicate let mut shells: Vec<_> = raw .into_iter() .map(|name: &String| name.trim().to_lowercase()) .flat_map(|name| { if name == "all" { Shell::variants().iter().map(|s| s.name().into()).collect() } else { vec![name] } }) .collect(); shells.sort_unstable(); shells.dedup(); // Parse the shell names shells .into_iter() .map(|name| Shell::from_str(&name).expect("failed to parse shell name")) .collect() } /// The target directory to output the shell completion files to. pub fn output(&'a self) -> PathBuf { self.matches .get_one::("output") .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("./")) } /// Whether to print completion scripts to stdout. pub fn stdout(&'a self) -> bool { self.matches.get_flag("stdout") } /// Name of binary to generate completions for. pub fn name(&'a self) -> String { self.matches .get_one("name") .cloned() .unwrap_or_else(util::bin_name) } } impl<'a> Matcher<'a> for CompletionsMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("internal")? .subcommand_matches("completions") .map(|matches| CompletionsMatcher { matches }) } } /// Available shells. #[derive(Copy, Clone)] #[allow(clippy::enum_variant_names)] pub(crate) enum Shell { Bash, Elvish, Fish, PowerShell, Zsh, } impl Shell { /// List all supported shell variants. pub fn variants() -> &'static [Shell] { &[ Shell::Bash, Shell::Elvish, Shell::Fish, Shell::PowerShell, Shell::Zsh, ] } /// Select shell variant from name. pub fn from_str(shell: &str) -> Option { match shell.trim().to_ascii_lowercase().as_str() { "bash" => Some(Shell::Bash), "elvish" => Some(Shell::Elvish), "fish" => Some(Shell::Fish), "powershell" | "ps" => Some(Shell::PowerShell), "zsh" => Some(Shell::Zsh), _ => None, } } /// Get shell name. pub fn name(self) -> &'static str { match self { Shell::Bash => "bash", Shell::Elvish => "elvish", Shell::Fish => "fish", Shell::PowerShell => "powershell", Shell::Zsh => "zsh", } } /// Suggested file name for completions file of current shell. pub fn file_name(self, bin_name: &str) -> String { match self { Shell::Bash => format!("{bin_name}.bash"), Shell::Elvish => format!("{bin_name}.elv"), Shell::Fish => format!("{bin_name}.fish"), Shell::PowerShell => format!("_{bin_name}.ps1"), Shell::Zsh => format!("_{bin_name}"), } } } impl fmt::Display for Shell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name()) } } prs-v0.5.2/cli/src/cmd/matcher/internal/mod.rs000066400000000000000000000030561471372304600212100ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod clip; #[cfg(feature = "clipboard")] pub mod clip_revert; pub mod completions; #[cfg(all(feature = "clipboard", feature = "totp"))] pub mod totp_recopy; use clap::ArgMatches; use super::Matcher; /// The internal matcher. pub struct InternalMatcher<'a> { root: &'a ArgMatches, _matches: &'a ArgMatches, } impl<'a: 'b, 'b> InternalMatcher<'a> { /// Get the internal clipboard sub command, if matched. #[cfg(feature = "clipboard")] pub fn clip(&'a self) -> Option { clip::ClipMatcher::with(self.root) } /// Get the internal clipboard revert sub command, if matched. #[cfg(feature = "clipboard")] pub fn clip_revert(&'a self) -> Option { clip_revert::ClipRevertMatcher::with(self.root) } /// Get the internal completions generator sub command, if matched. pub fn completions(&'a self) -> Option { completions::CompletionsMatcher::with(self.root) } /// Get the internal clipboard revert sub command, if matched. #[cfg(all(feature = "clipboard", feature = "totp"))] pub fn totp_recopy(&'a self) -> Option { totp_recopy::TotpRecopyMatcher::with(self.root) } } impl<'a> Matcher<'a> for InternalMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("internal") .map(|matches| InternalMatcher { root, _matches: matches, }) } } prs-v0.5.2/cli/src/cmd/matcher/internal/totp_recopy.rs000066400000000000000000000012361471372304600227760ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::ArgTimeout; /// The internal TOTP recopy command matcher. pub struct TotpRecopyMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> TotpRecopyMatcher<'a> { /// Clipboard timeout in seconds. pub fn timeout(&self) -> Result { ArgTimeout::value_or_default(self.matches) } } impl<'a> Matcher<'a> for TotpRecopyMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("internal")? .subcommand_matches("totp-recopy") .map(|matches| TotpRecopyMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/list.rs000066400000000000000000000016371471372304600175730ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgQuery, CmdArgOption}; /// The list command matcher. pub struct ListMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ListMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Whether to show as plain list. pub fn list(&self) -> bool { self.matches.get_flag("list") } /// Whether to only show aliases. pub fn only_aliases(&self) -> bool { self.matches.get_flag("aliases") } /// Whether to only show aliases. pub fn only_non_aliases(&self) -> bool { self.matches.get_flag("non-aliases") } } impl<'a> Matcher<'a> for ListMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("list") .map(|matches| ListMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/main.rs000066400000000000000000000023261471372304600175400ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgStore, CmdArgOption}; /// The main command matcher. pub struct MainMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> MainMatcher<'a> { /// Check whether to force. pub fn force(&self) -> bool { self.matches.get_flag("force") } /// Check whether to use no-interact mode. pub fn no_interact(&self) -> bool { self.matches.get_flag("no-interact") } /// Check whether to assume yes. pub fn assume_yes(&self) -> bool { self.matches.get_flag("yes") } /// Check whether quiet mode is used. pub fn quiet(&self) -> bool { !self.verbose() && self.matches.get_flag("quiet") } /// Check whether verbose mode is used. pub fn verbose(&self) -> bool { self.matches.get_count("verbose") > 0 } /// The store. pub fn store(&self) -> String { ArgStore::value(self.matches) } /// Check whether to use GPG in TTY mode. pub fn gpg_tty(&self) -> bool { self.matches.get_flag("gpg-tty") } } impl<'a> Matcher<'a> for MainMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { Some(MainMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/mod.rs000066400000000000000000000031011471372304600173630ustar00rootroot00000000000000pub mod add; #[cfg(feature = "alias")] pub mod alias; pub mod clone; #[cfg(feature = "clipboard")] pub mod copy; pub mod duplicate; pub mod edit; pub mod generate; pub mod git; pub mod grep; pub mod housekeeping; pub mod init; pub mod internal; pub mod list; pub mod main; pub mod r#move; pub mod recipients; pub mod remove; pub mod show; pub mod slam; pub mod sync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod tomb; #[cfg(feature = "totp")] pub mod totp; // Re-export to matcher module pub use self::add::AddMatcher; #[cfg(feature = "alias")] pub use self::alias::AliasMatcher; pub use self::clone::CloneMatcher; #[cfg(feature = "clipboard")] pub use self::copy::CopyMatcher; pub use self::duplicate::DuplicateMatcher; pub use self::edit::EditMatcher; pub use self::generate::GenerateMatcher; pub use self::git::GitMatcher; pub use self::grep::GrepMatcher; pub use self::housekeeping::HousekeepingMatcher; pub use self::init::InitMatcher; pub use self::internal::InternalMatcher; pub use self::list::ListMatcher; pub use self::main::MainMatcher; pub use self::r#move::MoveMatcher; pub use self::recipients::RecipientsMatcher; pub use self::remove::RemoveMatcher; pub use self::show::ShowMatcher; pub use self::slam::SlamMatcher; pub use self::sync::SyncMatcher; #[cfg(all(feature = "tomb", target_os = "linux"))] pub use self::tomb::TombMatcher; #[cfg(feature = "totp")] pub use self::totp::TotpMatcher; use clap::ArgMatches; pub trait Matcher<'a>: Sized { // Construct a new matcher instance from these argument matches. fn with(matches: &'a ArgMatches) -> Option; } prs-v0.5.2/cli/src/cmd/matcher/move.rs000066400000000000000000000017221471372304600175610ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The move command matcher. pub struct MoveMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> MoveMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Secret destination. pub fn destination(&self) -> &String { self.matches.get_one("DEST").unwrap() } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for MoveMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("move") .map(|matches| MoveMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/000077500000000000000000000000001471372304600204105ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/recipients/add.rs000066400000000000000000000020131471372304600215020ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The recipients add command matcher. pub struct AddMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> AddMatcher<'a> { /// Check whether to skip re-encrypting secrets. pub fn no_recrypt(&self) -> bool { self.matches.get_flag("no-recrypt") } /// Check whether to add a secret key. pub fn secret(&self) -> bool { self.matches.get_flag("secret") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for AddMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("recipients")? .subcommand_matches("add") .map(|matches| AddMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/export.rs000066400000000000000000000013401471372304600222750ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The recipients export command matcher. pub struct ExportMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ExportMatcher<'a> { /// File to output to. pub fn output_file(&self) -> Option<&String> { self.matches.get_one("output-file") } /// Check whether to copy the key. #[cfg(feature = "clipboard")] pub fn copy(&self) -> bool { self.matches.get_flag("copy") } } impl<'a> Matcher<'a> for ExportMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("recipients")? .subcommand_matches("export") .map(|matches| ExportMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/generate.rs000066400000000000000000000020611471372304600225470ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The recipients generate command matcher. pub struct GenerateMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> GenerateMatcher<'a> { /// Check whether to skip adding key to store. pub fn no_add(&self) -> bool { self.matches.get_flag("no-add") } /// Check whether to skip re-encrypting secrets. pub fn no_recrypt(&self) -> bool { self.matches.get_flag("no-recrypt") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for GenerateMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("recipients")? .subcommand_matches("generate") .map(|matches| GenerateMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/list.rs000066400000000000000000000007071471372304600217350ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The recipients list command matcher. pub struct ListMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> ListMatcher<'a> {} impl<'a> Matcher<'a> for ListMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("recipients")? .subcommand_matches("list") .map(|matches| ListMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/mod.rs000066400000000000000000000026361471372304600215440ustar00rootroot00000000000000pub mod add; pub mod export; pub mod generate; pub mod list; pub mod remove; use clap::ArgMatches; use super::Matcher; /// The recipients matcher. pub struct RecipientsMatcher<'a> { root: &'a ArgMatches, _matches: &'a ArgMatches, } impl<'a: 'b, 'b> RecipientsMatcher<'a> { /// Get the recipient add sub command, if matched. pub fn cmd_add(&'a self) -> Option { add::AddMatcher::with(self.root) } /// Get the recipient export sub command, if matched. pub fn cmd_export(&'a self) -> Option { export::ExportMatcher::with(self.root) } /// Get the recipient generate sub command, if matched. pub fn cmd_generate(&'a self) -> Option { generate::GenerateMatcher::with(self.root) } /// Get the recipient list sub command, if matched. pub fn cmd_list(&'a self) -> Option { list::ListMatcher::with(self.root) } /// Get the recipient remove sub command, if matched. pub fn cmd_remove(&'a self) -> Option { remove::RemoveMatcher::with(self.root) } } impl<'a> Matcher<'a> for RecipientsMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("recipients") .map(|matches| RecipientsMatcher { root, _matches: matches, }) } } prs-v0.5.2/cli/src/cmd/matcher/recipients/remove.rs000066400000000000000000000016221471372304600222540ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; /// The recipients remove command matcher. pub struct RemoveMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> RemoveMatcher<'a> { /// Check whether to re-encrypt secrets. pub fn recrypt(&self) -> bool { self.matches.get_flag("recrypt") } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for RemoveMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("recipients")? .subcommand_matches("remove") .map(|matches| RemoveMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/remove.rs000066400000000000000000000015421471372304600201100ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArgFlag, CmdArgOption}; /// The remove command matcher. pub struct RemoveMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> RemoveMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for RemoveMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("remove") .map(|matches| RemoveMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/show.rs000066400000000000000000000024661471372304600176010ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, ArgViewer, CmdArgFlag, CmdArgOption}; /// The show command matcher. pub struct ShowMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ShowMatcher<'a> { /// Check whether to just show the first line of the secret. pub fn first_line(&self) -> bool { self.matches.get_flag("first") } /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Show timeout in seconds. pub fn timeout(&self) -> Option> { ArgTimeout::value(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } /// Check whether to read from copy. #[cfg(feature = "clipboard")] pub fn copy(&self) -> bool { self.matches.get_flag("copy") } /// Check whether to show in a viewer. pub fn viewer(&self) -> bool { ArgViewer::is_present(self.matches) || self.timeout().is_some() } } impl<'a> Matcher<'a> for ShowMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("show") .map(|matches| ShowMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/slam.rs000066400000000000000000000006151471372304600175470ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The slam command matcher. pub struct SlamMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> SlamMatcher<'a> {} impl<'a> Matcher<'a> for SlamMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("slam") .map(|matches| SlamMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/000077500000000000000000000000001471372304600172175ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/sync/commit.rs000066400000000000000000000013651471372304600210620ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgNoSync, CmdArgFlag}; /// The sync commit command matcher. pub struct CommitMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CommitMatcher<'a> { /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } /// Custom commit message. pub fn message(&self) -> Option<&str> { self.matches.get_one("message").map(|s: &String| s.as_str()) } } impl<'a> Matcher<'a> for CommitMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("sync")? .subcommand_matches("commit") .map(|matches| CommitMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/init.rs000066400000000000000000000006731471372304600205360ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The sync init command matcher. pub struct InitMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> InitMatcher<'a> {} impl<'a> Matcher<'a> for InitMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("sync")? .subcommand_matches("init") .map(|matches| InitMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/mod.rs000066400000000000000000000027731471372304600203550ustar00rootroot00000000000000pub mod commit; pub mod init; pub mod remote; pub mod reset; pub mod status; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, CmdArgFlag}; /// The sync command matcher. pub struct SyncMatcher<'a> { root: &'a ArgMatches, matches: &'a ArgMatches, } impl<'a: 'b, 'b> SyncMatcher<'a> { /// Get the sync commit sub command, if matched. pub fn cmd_commit(&'a self) -> Option { commit::CommitMatcher::with(self.root) } /// Get the sync init sub command, if matched. pub fn cmd_init(&'a self) -> Option { init::InitMatcher::with(self.root) } /// Get the sync remote sub command, if matched. pub fn cmd_remote(&'a self) -> Option { remote::RemoteMatcher::with(self.root) } /// Get the sync reset sub command, if matched. pub fn cmd_reset(&'a self) -> Option { reset::ResetMatcher::with(self.root) } /// Get the sync status sub command, if matched. pub fn cmd_status(&'a self) -> Option { status::StatusMatcher::with(self.root) } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } } impl<'a> Matcher<'a> for SyncMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("sync") .map(|matches| SyncMatcher { root, matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/remote.rs000066400000000000000000000010721471372304600210600ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The sync remote command matcher. pub struct RemoteMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> RemoteMatcher<'a> { /// Get the git URL to set. pub fn git_url(&self) -> Option<&String> { self.matches.get_one("GIT_URL") } } impl<'a> Matcher<'a> for RemoteMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("sync")? .subcommand_matches("remote") .map(|matches| RemoteMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/reset.rs000066400000000000000000000011301471372304600207020ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgNoSync, CmdArgFlag}; /// The sync reset command matcher. pub struct ResetMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ResetMatcher<'a> { /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for ResetMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("sync")? .subcommand_matches("reset") .map(|matches| ResetMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/sync/status.rs000066400000000000000000000007071471372304600211140ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The sync status command matcher. pub struct StatusMatcher<'a> { _matches: &'a ArgMatches, } impl<'a: 'b, 'b> StatusMatcher<'a> {} impl<'a> Matcher<'a> for StatusMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("sync")? .subcommand_matches("status") .map(|matches| StatusMatcher { _matches: matches }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/000077500000000000000000000000001471372304600172045ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/tomb/close.rs000066400000000000000000000010461471372304600206600ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The tomb close command matcher. pub struct CloseMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CloseMatcher<'a> { /// Whether to try to close. pub fn do_try(&self) -> bool { self.matches.get_flag("try") } } impl<'a> Matcher<'a> for CloseMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("tomb")? .subcommand_matches("close") .map(|matches| CloseMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/init.rs000066400000000000000000000022171471372304600205170ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArgFlag}; use crate::util::error::{quit_error, ErrorHints}; /// The tomb init command matcher. pub struct InitMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> InitMatcher<'a> { /// The time to automatically close. pub fn timer(&self) -> Option { let time: &String = self.matches.get_one("timer")?; match crate::util::time::parse_duration(time) { Ok(0) => None, Ok(time) => Some(time as u32), Err(err) => quit_error(err.into(), ErrorHints::default()), } } /// Whether to allow a dirty repository for syncing. pub fn allow_dirty(&self) -> bool { ArgAllowDirty::is_present(self.matches) } /// Whether to not sync. pub fn no_sync(&self) -> bool { ArgNoSync::is_present(self.matches) } } impl<'a> Matcher<'a> for InitMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("tomb")? .subcommand_matches("init") .map(|matches| InitMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/mod.rs000066400000000000000000000025021471372304600203300ustar00rootroot00000000000000pub mod close; pub mod init; pub mod open; pub mod resize; pub mod status; use clap::ArgMatches; use super::Matcher; /// The tomb command matcher. pub struct TombMatcher<'a> { root: &'a ArgMatches, _matches: &'a ArgMatches, } impl<'a: 'b, 'b> TombMatcher<'a> { /// Get the tomb init sub command, if matched. pub fn cmd_init(&'a self) -> Option { init::InitMatcher::with(self.root) } /// Get the tomb open sub command, if matched. pub fn cmd_open(&'a self) -> Option { open::OpenMatcher::with(self.root) } /// Get the tomb close sub command, if matched. pub fn cmd_close(&'a self) -> Option { close::CloseMatcher::with(self.root) } /// Get the tomb status sub command, if matched. pub fn cmd_status(&'a self) -> Option { status::StatusMatcher::with(self.root) } /// Get the tomb resize sub command, if matched. pub fn cmd_resize(&'a self) -> Option { resize::ResizeMatcher::with(self.root) } } impl<'a> Matcher<'a> for TombMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("tomb").map(|matches| TombMatcher { root, _matches: matches, }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/open.rs000066400000000000000000000015061471372304600205150ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::util::error::{quit_error, ErrorHints}; /// The tomb open command matcher. pub struct OpenMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> OpenMatcher<'a> { /// The time to automatically close. pub fn timer(&self) -> Option { let time: &String = self.matches.get_one("timer")?; match crate::util::time::parse_duration(time) { Ok(0) => None, Ok(time) => Some(time as u32), Err(err) => quit_error(err.into(), ErrorHints::default()), } } } impl<'a> Matcher<'a> for OpenMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("tomb")? .subcommand_matches("open") .map(|matches| OpenMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/resize.rs000066400000000000000000000020701471372304600210520ustar00rootroot00000000000000use anyhow::anyhow; use clap::ArgMatches; use super::Matcher; use crate::util::error::{quit_error, quit_error_msg, ErrorHints}; /// The tomb resize command matcher. pub struct ResizeMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ResizeMatcher<'a> { /// The size in megabytes. pub fn size(&self) -> Option { let size: &String = self.matches.get_one("size")?; let size = match size.parse::() { Ok(size) => size, Err(err) => quit_error( anyhow!(err).context("invalid tomb size"), ErrorHints::default(), ), }; // Size must be at least 10 if size < 10 { quit_error_msg("tomb size must be at least 10MB", ErrorHints::default()); } Some(size) } } impl<'a> Matcher<'a> for ResizeMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("tomb")? .subcommand_matches("resize") .map(|matches| ResizeMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/tomb/status.rs000066400000000000000000000010621471372304600210740ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; /// The tomb status command matcher. pub struct StatusMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> StatusMatcher<'a> { /// Check whether to open the tomb. pub fn open(&self) -> bool { self.matches.get_flag("open") } } impl<'a> Matcher<'a> for StatusMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("tomb")? .subcommand_matches("status") .map(|matches| StatusMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/totp/000077500000000000000000000000001471372304600172315ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/matcher/totp/copy.rs000066400000000000000000000020341471372304600205500ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, CmdArgOption}; /// The TOTP copy command matcher. pub struct CopyMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> CopyMatcher<'a> { /// Don't recopy if the token changes within the timeout. pub fn no_recopy(&self) -> bool { self.matches.get_flag("no-recopy") } /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Clipboard timeout in seconds. pub fn timeout(&self) -> Option> { ArgTimeout::value(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } } impl<'a> Matcher<'a> for CopyMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("totp")? .subcommand_matches("copy") .map(|matches| CopyMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/totp/live.rs000066400000000000000000000015211471372304600205350ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, CmdArgOption}; /// The TOTP live command matcher. pub struct LiveMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> LiveMatcher<'a> { /// Check whether to follow. pub fn follow(&self) -> bool { self.matches.get_flag("follow") } /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } } impl<'a> Matcher<'a> for LiveMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("totp")? .subcommand_matches("live") .map(|matches| LiveMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/totp/mod.rs000066400000000000000000000022461471372304600203620ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod copy; pub mod live; pub mod qr; pub mod show; use clap::ArgMatches; use super::Matcher; /// The TOTP command matcher. pub struct TotpMatcher<'a> { root: &'a ArgMatches, _matches: &'a ArgMatches, } impl<'a: 'b, 'b> TotpMatcher<'a> { /// Get the TOTP copy sub command, if matched. #[cfg(feature = "clipboard")] pub fn cmd_copy(&'a self) -> Option { copy::CopyMatcher::with(self.root) } /// Get the TOTP live sub command, if matched. pub fn cmd_live(&'a self) -> Option { live::LiveMatcher::with(self.root) } /// Get the TOTP QR code sub command, if matched. pub fn cmd_qr(&'a self) -> Option { qr::QrMatcher::with(self.root) } /// Get the TOTP show sub command, if matched. pub fn cmd_show(&'a self) -> Option { show::ShowMatcher::with(self.root) } } impl<'a> Matcher<'a> for TotpMatcher<'a> { fn with(root: &'a ArgMatches) -> Option { root.subcommand_matches("totp").map(|matches| TotpMatcher { root, _matches: matches, }) } } prs-v0.5.2/cli/src/cmd/matcher/totp/qr.rs000066400000000000000000000013271471372304600202240ustar00rootroot00000000000000use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, CmdArgOption}; /// The TOTP QR code command matcher. pub struct QrMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> QrMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } } impl<'a> Matcher<'a> for QrMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("totp")? .subcommand_matches("qr") .map(|matches| QrMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/matcher/totp/show.rs000066400000000000000000000023161471372304600205610ustar00rootroot00000000000000use anyhow::Result; use clap::ArgMatches; use super::Matcher; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, ArgViewer, CmdArgFlag, CmdArgOption}; /// The TOTP show command matcher. pub struct ShowMatcher<'a> { matches: &'a ArgMatches, } impl<'a: 'b, 'b> ShowMatcher<'a> { /// The secret query. pub fn query(&self) -> Option { ArgQuery::value(self.matches) } /// Show timeout in seconds. pub fn timeout(&self) -> Option> { ArgTimeout::value(self.matches) } /// The selected property. pub fn property(&self) -> Option<&String> { ArgProperty::value(self.matches) } /// Check whether to read from copy. #[cfg(feature = "clipboard")] pub fn copy(&self) -> bool { self.matches.get_flag("copy") } /// Check whether to show in a viewer. pub fn viewer(&self) -> bool { ArgViewer::is_present(self.matches) || self.timeout().is_some() } } impl<'a> Matcher<'a> for ShowMatcher<'a> { fn with(matches: &'a ArgMatches) -> Option { matches .subcommand_matches("totp")? .subcommand_matches("show") .map(|matches| ShowMatcher { matches }) } } prs-v0.5.2/cli/src/cmd/mod.rs000066400000000000000000000001651471372304600157470ustar00rootroot00000000000000pub mod arg; pub mod handler; pub mod matcher; pub mod subcmd; // Re-export modules pub use self::handler::Handler; prs-v0.5.2/cli/src/cmd/subcmd/000077500000000000000000000000001471372304600160755ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/add.rs000066400000000000000000000021401471372304600171700ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The add command definition. pub struct CmdAdd; impl CmdAdd { pub fn build() -> Command { Command::new("add") .alias("a") .alias("new") .alias("n") .alias("create") .alias("insert") .alias("ins") .about("Add a secret") .arg(Arg::new("NAME").help("Secret name and path").required(true)) .arg( Arg::new("empty") .long("empty") .short('e') .num_args(0) .help("Add empty secret, do not edit"), ) .arg( Arg::new("stdin") .long("stdin") .short('S') .alias("from-stdin") .num_args(0) .help("Read secret from stdin, do not open editor") .conflicts_with("empty"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/alias.rs000066400000000000000000000013321471372304600175330ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The alias command definition. pub struct CmdAlias; impl CmdAlias { pub fn build() -> Command { Command::new("alias") .alias("ln") .alias("link") .alias("symlink") .about("Alias/symlink a secret") .long_about("Alias/symlink a secret without duplicating its content") .arg(ArgQuery::build().required(true)) .arg( Arg::new("DEST") .help("Secret destination path") .required(true), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/clone.rs000066400000000000000000000006001471372304600175370ustar00rootroot00000000000000use clap::{Arg, Command}; /// The clone command definition. pub struct CmdClone; impl CmdClone { pub fn build() -> Command { Command::new("clone") .about("Clone existing password store") .arg( Arg::new("GIT_URL") .help("Remote git URL to clone from") .required(true), ) } } prs-v0.5.2/cli/src/cmd/subcmd/copy.rs000066400000000000000000000014141471372304600174150ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, CmdArg}; /// The copy command definition. pub struct CmdCopy; impl CmdCopy { pub fn build() -> Command { Command::new("copy") .alias("cp") .alias("c") .alias("yank") .alias("clip") .alias("clipboard") .about("Copy secret to clipboard") .arg( Arg::new("all") .long("all") .short('a') .num_args(0) .help("Copy whole secret, not just first line"), ) .arg(ArgQuery::build()) .arg(ArgTimeout::build()) .arg(ArgProperty::build().conflicts_with("all")) } } prs-v0.5.2/cli/src/cmd/subcmd/duplicate.rs000066400000000000000000000012501471372304600204130ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The duplicate command definition. pub struct CmdDuplicate; impl CmdDuplicate { pub fn build() -> Command { Command::new("duplicate") .alias("dup") .about("Duplicate a secret") .long_about("Duplicate the contents of a secret to a new file") .arg(ArgQuery::build().required(true)) .arg( Arg::new("DEST") .help("Secret destination path") .required(true), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/edit.rs000066400000000000000000000012571471372304600173750ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The edit command definition. pub struct CmdEdit; impl CmdEdit { pub fn build() -> Command { Command::new("edit") .alias("e") .about("Edit a secret") .arg(ArgQuery::build()) .arg( Arg::new("stdin") .long("stdin") .short('S') .alias("from-stdin") .num_args(0) .help("Read secret from stdin, do not open editor"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/generate.rs000066400000000000000000000056631471372304600202470ustar00rootroot00000000000000use clap::{Arg, Command}; #[cfg(feature = "clipboard")] use crate::cmd::arg::ArgTimeout; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The generate command definition. pub struct CmdGenerate; impl CmdGenerate { pub fn build() -> Command { #[cfg_attr(not(feature = "clipboard"), expect(clippy::let_and_return))] let cmd = Command::new("generate") .alias("gen") .alias("g") .alias("random") .alias("pwgen") .about("Generate a secure secret") .arg( Arg::new("NAME") .help("Secret name and path") .required_unless_present_any(["show", "copy"]), ) .arg( Arg::new("passphrase") .long("passphrase") .short('P') .num_args(0) .help("Generate passhprase instead of random string"), ) .arg( Arg::new("length") .value_name("NUM") .long("length") .short('l') .alias("len") .num_args(1) .help("Generated password length in characters") .long_help( "Generated password length in characters. Passphrase length in words.", ), ) .arg( Arg::new("merge") .long("merge") .short('m') .num_args(0) .help("Merge into existing secret, don't create new secret"), ) .arg( Arg::new("edit") .long("edit") .short('e') .num_args(0) .help("Edit secret after generation"), ) .arg( Arg::new("stdin") .long("stdin") .short('S') .alias("from-stdin") .num_args(0) .help("Append to generated secret from stdin") .conflicts_with("edit"), ) .arg( Arg::new("show") .long("show") .alias("cat") .alias("display") .alias("stdout") .num_args(0) .help("Display secret after generation"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()); #[cfg(feature = "clipboard")] let cmd = cmd .arg( Arg::new("copy") .long("copy") .short('c') .alias("cp") .num_args(0) .help("Copy secret to clipboard"), ) .arg(ArgTimeout::build().requires("copy")); cmd } } prs-v0.5.2/cli/src/cmd/subcmd/git.rs000066400000000000000000000006321471372304600172270ustar00rootroot00000000000000use clap::{Arg, Command}; /// The git command definition. pub struct CmdGit; impl CmdGit { pub fn build() -> Command { Command::new("git") .about("Invoke git command in password store") .arg( Arg::new("COMMAND") .help("Git command to invoke") .num_args(..), ) .trailing_var_arg(true) } } prs-v0.5.2/cli/src/cmd/subcmd/grep.rs000066400000000000000000000022561471372304600174050ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgQuery, CmdArg}; /// The grep command definition. pub struct CmdGrep; impl CmdGrep { pub fn build() -> Command { Command::new("grep") .alias("find") .about("Grep all secrets") .arg(Arg::new("PATTERN").required(true).help("Grep pattern")) .arg( ArgQuery::build() .id("query") .long("query") .short('Q') .help("Limit grep to secrets by query"), ) .arg( Arg::new("regex") .long("regex") .alias("regexp") .short('r') .num_args(0) .help("Interpret pattern as regular expression"), ) .arg( Arg::new("aliases") .long("aliases") .short('a') .alias("symlinks") .alias("with-aliases") .alias("with-symlinks") .num_args(0) .help("Include grepping aliases"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/housekeeping/000077500000000000000000000000001471372304600205635ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/housekeeping/mod.rs000066400000000000000000000011611471372304600217070ustar00rootroot00000000000000pub mod recrypt; pub mod run; pub mod sync_keys; use clap::Command; /// The housekeeping command definition. pub struct CmdHousekeeping; impl CmdHousekeeping { pub fn build() -> Command { Command::new("housekeeping") .about("Housekeeping utilities") .alias("housekeep") .alias("hk") .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("ACTION") .subcommand(run::CmdRun::build()) .subcommand(recrypt::CmdRecrypt::build()) .subcommand(sync_keys::CmdSyncKeys::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/housekeeping/recrypt.rs000066400000000000000000000013401471372304600226170ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The housekeeping recrypt command definition. pub struct CmdRecrypt; impl CmdRecrypt { pub fn build() -> Command { Command::new("recrypt") .alias("reencrypt") .about("Re-encrypt secrets") .arg( Arg::new("all") .long("all") .short('a') .num_args(0) .help("Re-encrypt all secrets") .conflicts_with("QUERY"), ) .arg(ArgQuery::build().required_unless_present("all")) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/housekeeping/run.rs000066400000000000000000000005341471372304600217370ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The housekeeping run command definition. pub struct CmdRun; impl CmdRun { pub fn build() -> Command { Command::new("run") .about("Run housekeeping tasks") .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/housekeeping/sync_keys.rs000066400000000000000000000012651471372304600231440ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The housekeeping sync-keys command definition. pub struct CmdSyncKeys; impl CmdSyncKeys { pub fn build() -> Command { Command::new("sync-keys") .alias("sync-recipients") .about("Sync public keys in store, import missing keys") .arg( Arg::new("no-import") .long("no-import") .alias("skip-import") .num_args(0) .help("Skip importing missing keys to keychain"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/init.rs000066400000000000000000000005731471372304600174130ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The init command definition. pub struct CmdInit; impl CmdInit { pub fn build() -> Command { Command::new("init") .alias("initialize") .about("Initialize new password store") .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/internal/000077500000000000000000000000001471372304600177115ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/internal/clip.rs000066400000000000000000000003271471372304600212100ustar00rootroot00000000000000use clap::Command; /// The internal clipboard command definition. pub struct CmdClip; impl CmdClip { pub fn build() -> Command { Command::new("clip").about("Set clipboard contents from stdin") } } prs-v0.5.2/cli/src/cmd/subcmd/internal/clip_revert.rs000066400000000000000000000005521471372304600225770ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgTimeout, CmdArg}; /// The internal clipboard revert command definition. pub struct CmdClipRevert; impl CmdClipRevert { pub fn build() -> Command { Command::new("clip-revert") .about("Revert clipboard after timeout") .arg(ArgTimeout::build().global(false).required(true)) } } prs-v0.5.2/cli/src/cmd/subcmd/internal/completions.rs000066400000000000000000000041271471372304600226170ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::matcher::internal::completions::Shell; /// The generate completions command definition. pub struct CmdCompletions; impl CmdCompletions { pub fn build() -> Command { let mut shell_variants = vec!["all"]; shell_variants.extend(Shell::variants().iter().map(|v| v.name())); Command::new("completions") .about("Shell completions") .alias("completion") .alias("complete") .arg( Arg::new("SHELL") .help("Shell type to generate completions for") .required(true) .num_args(1..) // TODO: replace this by a runtime list // Issue: https://github.com/clap-rs/clap/issues/4504#issuecomment-1326379595 // .value_parser(shell_variants) .value_parser(["all", "bash", "zsh", "fish", "elvish", "powershell"]) .ignore_case(true), ) .arg( Arg::new("output") .long("output") .short('o') .alias("output-dir") .alias("out") .alias("dir") .num_args(1) .value_name("DIR") .help("Shell completion files output directory"), ) .arg( Arg::new("stdout") .long("stdout") .alias("print") .num_args(0) .help("Output completion files to stdout instead") .conflicts_with("output"), ) .arg( Arg::new("name") .long("name") .short('n') .alias("bin") .alias("binary") .alias("bin-name") .alias("binary-name") .value_name("NAME") .num_args(1) .help("Name of binary to generate completions for"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/internal/mod.rs000066400000000000000000000020671471372304600210430ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod clip; #[cfg(feature = "clipboard")] pub mod clip_revert; pub mod completions; #[cfg(all(feature = "clipboard", feature = "totp"))] pub mod totp_recopy; use clap::Command; /// The internal command definition. pub struct CmdInternal; impl CmdInternal { pub fn build() -> Command { #[cfg_attr(not(feature = "clipboard"), expect(unused_mut))] let mut cmd = Command::new("internal") .about("Commands used by prs internally") .hide(true) .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("ACTION") .subcommand(completions::CmdCompletions::build()); #[cfg(feature = "clipboard")] { cmd = cmd .subcommand(clip::CmdClip::build()) .subcommand(clip_revert::CmdClipRevert::build()); } #[cfg(all(feature = "clipboard", feature = "totp"))] { cmd = cmd.subcommand(totp_recopy::CmdTotpRecopy::build()); } cmd } } prs-v0.5.2/cli/src/cmd/subcmd/internal/totp_recopy.rs000066400000000000000000000005511471372304600226270ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgTimeout, CmdArg}; /// The internal TOTP recopy command definition. pub struct CmdTotpRecopy; impl CmdTotpRecopy { pub fn build() -> Command { Command::new("totp-recopy") .about("Copy TOTP tokens, recopy on change") .arg(ArgTimeout::build().global(false).required(true)) } } prs-v0.5.2/cli/src/cmd/subcmd/list.rs000066400000000000000000000026621471372304600174240ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgQuery, CmdArg}; /// The list command definition. pub struct CmdList; impl CmdList { pub fn build() -> Command { Command::new("list") .alias("ls") .alias("l") .alias("search") .about("List all secrets") .arg(ArgQuery::build()) .arg( Arg::new("list") .long("list") .short('l') .alias("no-tree") .num_args(0) .help("Show as list, not as tree"), ) .arg( Arg::new("aliases") .long("aliases") .short('a') .alias("symlinks") .alias("only-aliases") .alias("only-symlinks") .num_args(0) .help("Show only alises"), ) .arg( Arg::new("non-aliases") .long("non-aliases") .short('A') .alias("non-symlinks") .alias("no-aliases") .alias("no-symlinks") .alias("only-non-aliases") .alias("only-non-symlinks") .num_args(0) .help("Show only non-alises") .conflicts_with("aliases"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/mod.rs000066400000000000000000000024061471372304600172240ustar00rootroot00000000000000pub mod add; #[cfg(feature = "alias")] pub mod alias; pub mod clone; #[cfg(feature = "clipboard")] pub mod copy; pub mod duplicate; pub mod edit; pub mod generate; pub mod git; pub mod grep; pub mod housekeeping; pub mod init; pub mod internal; pub mod list; pub mod r#move; pub mod recipients; pub mod remove; pub mod show; pub mod slam; pub mod sync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod tomb; #[cfg(feature = "totp")] pub mod totp; // Re-export to cmd module pub use self::add::CmdAdd; #[cfg(feature = "alias")] pub use self::alias::CmdAlias; pub use self::clone::CmdClone; #[cfg(feature = "clipboard")] pub use self::copy::CmdCopy; pub use self::duplicate::CmdDuplicate; pub use self::edit::CmdEdit; pub use self::generate::CmdGenerate; pub use self::git::CmdGit; pub use self::grep::CmdGrep; pub use self::housekeeping::CmdHousekeeping; pub use self::init::CmdInit; pub use self::internal::CmdInternal; pub use self::list::CmdList; pub use self::r#move::CmdMove; pub use self::recipients::CmdRecipients; pub use self::remove::CmdRemove; pub use self::show::CmdShow; pub use self::slam::CmdSlam; pub use self::sync::CmdSync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub use self::tomb::CmdTomb; #[cfg(feature = "totp")] pub use self::totp::CmdTotp; prs-v0.5.2/cli/src/cmd/subcmd/move.rs000066400000000000000000000012231471372304600174070ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The move command definition. pub struct CmdMove; impl CmdMove { pub fn build() -> Command { Command::new("move") .alias("mov") .alias("mv") .alias("rename") .alias("ren") .about("Move a secret") .arg(ArgQuery::build().required(true)) .arg( Arg::new("DEST") .help("Secret destination path") .required(true), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/000077500000000000000000000000001471372304600202425ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/recipients/add.rs000066400000000000000000000016701471372304600213440ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The recipient add command definition. pub struct CmdAdd; impl CmdAdd { pub fn build() -> Command { Command::new("add") .alias("a") .about("Add store recipient") .arg( Arg::new("secret") .long("secret") .alias("private") .num_args(0) .help("Add public key we have private key for"), ) .arg( Arg::new("no-recrypt") .long("no-recrypt") .alias("no-reencrypt") .alias("skip-recrypt") .alias("skip-reencrypt") .num_args(0) .help("Skip re-encrypting all secrets"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/export.rs000066400000000000000000000020271471372304600221320ustar00rootroot00000000000000use clap::{Arg, Command}; /// The recipient export command definition. pub struct CmdExport; impl CmdExport { pub fn build() -> Command { #[cfg_attr(not(feature = "clipboard"), expect(clippy::let_and_return))] let cmd = Command::new("export") .alias("exp") .alias("ex") .about("Export recipient key") .arg( Arg::new("output-file") .long("output-file") .short('o') .alias("output") .alias("file") .value_name("PATH") .num_args(1) .help("Write recipient key to file instead of stdout"), ); #[cfg(feature = "clipboard")] let cmd = cmd.arg( Arg::new("copy") .long("copy") .short('c') .alias("yank") .num_args(0) .help("Copy recipient key to clipboard instead of stdout"), ); cmd } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/generate.rs000066400000000000000000000020431471372304600224010ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The recipient generate command definition. pub struct CmdGenerate; impl CmdGenerate { pub fn build() -> Command { Command::new("generate") .alias("gen") .alias("g") .about("Generate new key pair, add it to the store") .arg( Arg::new("no-add") .long("no-add") .alias("skip-add") .num_args(0) .help("Skip adding key pair to store"), ) .arg( Arg::new("no-recrypt") .long("no-recrypt") .alias("no-reencrypt") .alias("skip-recrypt") .alias("skip-reencrypt") .num_args(0) .help("Skip re-encrypting all secrets") .conflicts_with("no-add"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/list.rs000066400000000000000000000004051471372304600215620ustar00rootroot00000000000000use clap::Command; /// The recipient list command definition. pub struct CmdList; impl CmdList { pub fn build() -> Command { Command::new("list") .alias("ls") .alias("l") .about("List store recipients") } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/mod.rs000066400000000000000000000014661471372304600213760ustar00rootroot00000000000000pub mod add; pub mod export; pub mod generate; pub mod list; pub mod remove; use clap::Command; /// The recipients command definition. pub struct CmdRecipients; impl CmdRecipients { pub fn build() -> Command { Command::new("recipients") .about("Manage store recipients") .alias("recipient") .alias("recip") .alias("rec") .alias("keys") .alias("kes") .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("CMD") .subcommand(add::CmdAdd::build()) .subcommand(generate::CmdGenerate::build()) .subcommand(list::CmdList::build()) .subcommand(remove::CmdRemove::build()) .subcommand(export::CmdExport::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/recipients/remove.rs000066400000000000000000000012461471372304600221100ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, CmdArg}; /// The recipient remove command definition. pub struct CmdRemove; impl CmdRemove { pub fn build() -> Command { Command::new("remove") .alias("rm") .alias("delete") .alias("del") .about("Remove store recipient") .arg( Arg::new("recrypt") .long("recrypt") .alias("reencrypt") .num_args(0) .help("Re-encrypting all secrets"), ) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/remove.rs000066400000000000000000000007551471372304600177470ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgAllowDirty, ArgNoSync, ArgQuery, CmdArg}; /// The remove command definition. pub struct CmdRemove; impl CmdRemove { pub fn build() -> Command { Command::new("remove") .alias("rm") .alias("delete") .alias("del") .alias("yeet") .about("Remove a secret") .arg(ArgQuery::build()) .arg(ArgAllowDirty::build()) .arg(ArgNoSync::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/show.rs000066400000000000000000000025671471372304600174350ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, ArgViewer, CmdArg}; /// The show command definition. pub struct CmdShow; impl CmdShow { pub fn build() -> Command { #[cfg_attr(not(feature = "clipboard"), expect(clippy::let_and_return))] let cmd = Command::new("show") .alias("s") .alias("cat") .alias("display") .alias("print") .about("Display a secret") .arg( Arg::new("first") .long("first") .alias("password") .alias("pass") .num_args(0) .help("Show only the first line of the secret"), ) .arg(ArgQuery::build()) .arg( ArgTimeout::build() .conflicts_with_all(["no-interact", "viewer"]) .help("Timeout after which to clear output, implies --viewer"), ) .arg(ArgProperty::build().conflicts_with("first")) .arg(ArgViewer::build()); #[cfg(feature = "clipboard")] let cmd = cmd.arg( Arg::new("copy") .long("copy") .short('c') .alias("cp") .num_args(0) .help("Copy secret to clipboard"), ); cmd } } prs-v0.5.2/cli/src/cmd/subcmd/slam.rs000066400000000000000000000006111471372304600173750ustar00rootroot00000000000000use clap::Command; /// The slam command definition. pub struct CmdSlam; impl CmdSlam { pub fn build() -> Command { Command::new("slam") .alias("lock") .alias("lockdown") .alias("shut") .alias("emergency") .alias("sos") .about("Aggressively lock password store & keys preventing access (emergency)") } } prs-v0.5.2/cli/src/cmd/subcmd/sync/000077500000000000000000000000001471372304600170515ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/sync/commit.rs000066400000000000000000000012571471372304600207140ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgNoSync, CmdArg}; /// The sync commit command definition. pub struct CmdCommit; impl CmdCommit { pub fn build() -> Command { Command::new("commit") .about("Commit all non-committed changes") .arg( Arg::new("message") .long("message") .short('m') .alias("msg") .value_name("MESSAGE") .num_args(1) .global(true) .help("Custom commit message"), ) .arg(ArgNoSync::build().help("Do not sync changes after commit")) } } prs-v0.5.2/cli/src/cmd/subcmd/sync/init.rs000066400000000000000000000003521471372304600203620ustar00rootroot00000000000000use clap::Command; /// The sync init command definition. pub struct CmdInit; impl CmdInit { pub fn build() -> Command { Command::new("init") .alias("initialize") .about("Initialize sync") } } prs-v0.5.2/cli/src/cmd/subcmd/sync/mod.rs000066400000000000000000000011541471372304600201770ustar00rootroot00000000000000pub mod commit; pub mod init; pub mod remote; pub mod reset; pub mod status; use clap::Command; use crate::cmd::arg::{ArgAllowDirty, CmdArg}; /// The sync command definition. pub struct CmdSync; impl CmdSync { pub fn build() -> Command { Command::new("sync") .about("Sync password store") .subcommand(init::CmdInit::build()) .subcommand(remote::CmdRemote::build()) .subcommand(status::CmdStatus::build()) .subcommand(commit::CmdCommit::build()) .subcommand(reset::CmdReset::build()) .arg(ArgAllowDirty::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/sync/remote.rs000066400000000000000000000004571471372304600207200ustar00rootroot00000000000000use clap::{Arg, Command}; /// The sync remote command definition. pub struct CmdRemote; impl CmdRemote { pub fn build() -> Command { Command::new("remote") .about("Get or set git remote URL for sync") .arg(Arg::new("GIT_URL").help("Remote git URL to set")) } } prs-v0.5.2/cli/src/cmd/subcmd/sync/reset.rs000066400000000000000000000005251471372304600205430ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgNoSync, CmdArg}; /// The sync reset command definition. pub struct CmdReset; impl CmdReset { pub fn build() -> Command { Command::new("reset") .about("Reset all non-committed changes") .arg(ArgNoSync::build().help("Do not sync changes after reset")) } } prs-v0.5.2/cli/src/cmd/subcmd/sync/status.rs000066400000000000000000000003051471372304600207400ustar00rootroot00000000000000use clap::Command; /// The sync status command definition. pub struct CmdStatus; impl CmdStatus { pub fn build() -> Command { Command::new("status").about("Show sync status") } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/000077500000000000000000000000001471372304600170365ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/tomb/close.rs000066400000000000000000000007501471372304600205130ustar00rootroot00000000000000use clap::{Arg, Command}; /// The tomb close command definition. pub struct CmdClose; impl CmdClose { pub fn build() -> Command { Command::new("close") .alias("c") .alias("stop") .alias("lock") .about("Close tomb") .arg( Arg::new("try") .long("try") .num_args(0) .help("Try to close, don't fail if already closed"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/init.rs000066400000000000000000000014571471372304600203560ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::util::time; lazy_static! { /// Default value for timer. static ref TIMER_DEFAULT: String = time::format_duration(prs_lib::tomb::TOMB_AUTO_CLOSE_SEC); } /// The tomb init command definition. pub struct CmdInit; impl CmdInit { pub fn build() -> Command { Command::new("init") .alias("initialize") .about("Initialize tomb in-place for current password store") .arg( Arg::new("timer") .long("timer") .short('t') .alias("time") .value_name("TIME") .default_value(TIMER_DEFAULT.as_str()) .num_args(1) .help("Time after which to close the Tomb"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/mod.rs000066400000000000000000000012201471372304600201560ustar00rootroot00000000000000pub mod close; pub mod init; pub mod open; pub mod resize; pub mod status; use clap::Command; /// The tomb command definition. pub struct CmdTomb; impl CmdTomb { pub fn build() -> Command { Command::new("tomb") .about("Manage password store Tomb") .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("CMD") .subcommand(open::CmdOpen::build()) .subcommand(close::CmdClose::build()) .subcommand(init::CmdInit::build()) .subcommand(status::CmdStatus::build()) .subcommand(resize::CmdResize::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/open.rs000066400000000000000000000010611471372304600203430ustar00rootroot00000000000000use clap::{Arg, Command}; /// The tomb open command definition. pub struct CmdOpen; impl CmdOpen { pub fn build() -> Command { Command::new("open") .alias("o") .alias("unlock") .about("Open tomb") .arg( Arg::new("timer") .long("timer") .short('t') .alias("time") .value_name("TIME") .num_args(1) .help("Time after which to close the Tomb"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/resize.rs000066400000000000000000000010511471372304600207020ustar00rootroot00000000000000use clap::{Arg, Command}; /// The tomb resize command definition. pub struct CmdResize; impl CmdResize { pub fn build() -> Command { Command::new("resize") .alias("r") .alias("size") .alias("grow") .about("Resize tomb") .arg( Arg::new("size") .long("size") .short('S') .value_name("MEGABYTE") .num_args(1) .help("Resize tomb to megabytes"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/tomb/status.rs000066400000000000000000000006041471372304600207270ustar00rootroot00000000000000use clap::{Arg, Command}; /// The tomb status command definition. pub struct CmdStatus; impl CmdStatus { pub fn build() -> Command { Command::new("status").about("Query tomb status").arg( Arg::new("open") .long("open") .alias("o") .num_args(0) .help("Open tomb is it is closed"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/totp/000077500000000000000000000000001471372304600170635ustar00rootroot00000000000000prs-v0.5.2/cli/src/cmd/subcmd/totp/copy.rs000066400000000000000000000015101471372304600204000ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgStore, ArgTimeout, CmdArg}; /// The TOTP copy command definition. pub struct CmdCopy; impl CmdCopy { pub fn build() -> Command { Command::new("copy") .alias("cp") .alias("c") .alias("yank") .alias("clip") .alias("clipboard") .about("Copy TOTP token to clipboard") .arg(ArgQuery::build()) .arg(ArgTimeout::build()) .arg(ArgStore::build()) .arg(ArgProperty::build()) .arg( Arg::new("no-recopy") .long("no-recopy") .short('C') .num_args(0) .help("Don't recopy token when it changes within the timeout"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/totp/live.rs000066400000000000000000000013211471372304600203650ustar00rootroot00000000000000use clap::{Arg, Command}; use crate::cmd::arg::{ArgProperty, ArgQuery, CmdArg}; /// The TOTP live command definition. pub struct CmdLive; impl CmdLive { pub fn build() -> Command { Command::new("live") .alias("watch") .alias("follow") .alias("l") .alias("w") .alias("f") .about("Watch TOTP token") .arg(ArgQuery::build()) .arg(ArgProperty::build()) .arg( Arg::new("follow") .long("follow") .short('F') .num_args(0) .help("Output new tokens on newline without clearing previous"), ) } } prs-v0.5.2/cli/src/cmd/subcmd/totp/mod.rs000066400000000000000000000013041471372304600202060ustar00rootroot00000000000000#[cfg(feature = "clipboard")] pub mod copy; pub mod live; pub mod qr; pub mod show; use clap::Command; /// The TOTP command definition. pub struct CmdTotp; impl CmdTotp { pub fn build() -> Command { let cmd = Command::new("totp") .alias("otp") .alias("hotp") .about("Manage TOTP tokens") .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("CMD") .subcommand(show::CmdShow::build()); #[cfg(feature = "clipboard")] let cmd = cmd.subcommand(copy::CmdCopy::build()); cmd.subcommand(live::CmdLive::build()) .subcommand(qr::CmdQr::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/totp/qr.rs000066400000000000000000000006711471372304600200570ustar00rootroot00000000000000use clap::Command; use crate::cmd::arg::{ArgProperty, ArgQuery, CmdArg}; /// The TOTP QR code command definition. pub struct CmdQr; impl CmdQr { pub fn build() -> Command { Command::new("qr") .alias("q") .alias("qrcode") .alias("qr-code") .alias("share") .about("Show TOTP QR code") .arg(ArgQuery::build()) .arg(ArgProperty::build()) } } prs-v0.5.2/cli/src/cmd/subcmd/totp/show.rs000066400000000000000000000020441471372304600204110ustar00rootroot00000000000000#[cfg(feature = "clipboard")] use clap::Arg; use clap::Command; use crate::cmd::arg::{ArgProperty, ArgQuery, ArgTimeout, ArgViewer, CmdArg}; /// The TOTP show command definition. pub struct CmdShow; impl CmdShow { pub fn build() -> Command { let cmd = Command::new("show") .alias("s") .alias("cat") .alias("display") .alias("print") .about("Show TOTP token") .arg(ArgQuery::build()) .arg( ArgTimeout::build() .conflicts_with_all(["no-interact", "viewer"]) .help("Timeout after which to clear output, implies --viewer"), ) .arg(ArgProperty::build()) .arg(ArgViewer::build()); #[cfg(feature = "clipboard")] let cmd = cmd.arg( Arg::new("copy") .long("copy") .short('c') .alias("cp") .num_args(0) .help("Copy secret to clipboard"), ); cmd } } prs-v0.5.2/cli/src/crypto.rs000066400000000000000000000011631471372304600157440ustar00rootroot00000000000000use crate::cmd::matcher::MainMatcher; use prs_lib::crypto::{self, Config, Context, Proto}; /// Default cryptography protocol. const PROTO: Proto = Proto::Gpg; /// Construct crypto config, respect CLI arguments. pub(crate) fn config(matcher_main: &MainMatcher) -> Config { let mut config = Config::from(PROTO); config.gpg_tty = matcher_main.gpg_tty(); config.verbose = matcher_main.verbose(); config } /// Construct crypto context, respect CLI arguments. pub(crate) fn context(matcher_main: &MainMatcher) -> Result { let config = config(matcher_main); crypto::context(&config) } prs-v0.5.2/cli/src/main.rs000066400000000000000000000201501471372304600153450ustar00rootroot00000000000000#[macro_use] extern crate clap; #[macro_use] extern crate derive_builder; #[allow(unused_imports)] #[macro_use] extern crate lazy_static; mod action; mod cmd; mod crypto; mod util; mod viewer; use anyhow::Result; use prs_lib::Store; use crate::{ cmd::matcher::{MainMatcher, Matcher}, cmd::Handler, util::{ error::{quit, quit_error, ErrorHints}, style, }, }; /// Binary name. pub const NAME: &str = "prs"; /// Clipboard timeout in seconds. #[cfg(feature = "clipboard")] const CLIPBOARD_TIMEOUT: u64 = 20; fn main() { // Do not use colored output on Windows #[cfg(windows)] colored::control::set_override(false); // Parse CLI arguments let cmd_handler = Handler::parse(); // Invoke the proper action if let Err(err) = invoke_action(&cmd_handler) { let matcher_main = MainMatcher::with(cmd_handler.matches()).unwrap(); quit_error(err, ErrorHints::from_matcher(&matcher_main)); }; } /// Invoke the proper action based on the CLI input. /// /// If no proper action is selected, the program will quit with an error /// message. fn invoke_action(handler: &Handler) -> Result<()> { if handler.add().is_some() { return action::add::Add::new(handler.matches()).invoke(); } #[cfg(feature = "alias")] if handler.alias().is_some() { return action::alias::Alias::new(handler.matches()).invoke(); } if handler.clone().is_some() { return action::clone::Clone::new(handler.matches()).invoke(); } #[cfg(feature = "clipboard")] if handler.copy().is_some() { return action::copy::Copy::new(handler.matches()).invoke(); } if handler.duplicate().is_some() { return action::duplicate::Duplicate::new(handler.matches()).invoke(); } if handler.edit().is_some() { return action::edit::Edit::new(handler.matches()).invoke(); } if handler.generate().is_some() { return action::generate::Generate::new(handler.matches()).invoke(); } if handler.git().is_some() { return action::git::Git::new(handler.matches()).invoke(); } if handler.grep().is_some() { return action::grep::Grep::new(handler.matches()).invoke(); } if handler.housekeeping().is_some() { return action::housekeeping::Housekeeping::new(handler.matches()).invoke(); } if handler.r#move().is_some() { return action::r#move::Move::new(handler.matches()).invoke(); } if handler.init().is_some() { return action::init::Init::new(handler.matches()).invoke(); } if handler.internal().is_some() { return action::internal::Internal::new(handler.matches()).invoke(); } if handler.list().is_some() { return action::list::List::new(handler.matches()).invoke(); } if handler.slam().is_some() { return action::slam::Slam::new(handler.matches()).invoke(); } if handler.recipients().is_some() { return action::recipients::Recipients::new(handler.matches()).invoke(); } if handler.remove().is_some() { return action::remove::Remove::new(handler.matches()).invoke(); } if handler.show().is_some() { return action::show::Show::new(handler.matches()).invoke(); } if handler.sync().is_some() { return action::sync::Sync::new(handler.matches()).invoke(); } #[cfg(all(feature = "tomb", target_os = "linux"))] if handler.tomb().is_some() { return action::tomb::Tomb::new(handler.matches()).invoke(); } #[cfg(feature = "totp")] if handler.totp().is_some() { return action::totp::Totp::new(handler.matches()).invoke(); } // Get the main matcher let matcher_main = MainMatcher::with(handler.matches()).unwrap(); if !matcher_main.quiet() { print_main_info(&matcher_main); } Ok(()) } /// Print the main info, shown when no subcommands were supplied. pub fn print_main_info(matcher_main: &MainMatcher) -> ! { // Get the name of the used executable let bin = util::bin_name(); // Attempt to load default store let store = Store::open(matcher_main.store()).ok(); let has_sync = store.as_ref().map(|s| s.sync().is_init()).unwrap_or(false); // Print the main info eprintln!("{NAME} {}", crate_version!()); eprintln!("Usage: {bin} [FLAGS] ..."); eprintln!(crate_description!()); eprintln!(); if let Some(store) = store { #[cfg(not(all(feature = "tomb", target_os = "linux")))] let has_closed_tomb = false; #[cfg(all(feature = "tomb", target_os = "linux"))] let has_closed_tomb = { let tomb = store.tomb( !matcher_main.verbose(), matcher_main.verbose(), matcher_main.force(), ); tomb.is_tomb() && !tomb.is_open().unwrap_or(true) }; // Hint tomb open command if has_closed_tomb { eprintln!("Open password store Tomb:"); eprintln!(" {}", style::highlight(format!("{bin} tomb open"))); eprintln!(); } // Hint user to add ourselves as recipient if it doesn't have recipient we own let we_own_any_recipient = store .recipients() .and_then(|recip| prs_lib::crypto::recipients::contains_own_secret_key(&recip)) .unwrap_or(false); if !has_closed_tomb && !we_own_any_recipient { let config = crate::crypto::config(matcher_main); let system_has_secret = prs_lib::crypto::util::has_private_key(&config).unwrap_or(true); if system_has_secret { eprintln!("Add your own key as recipient or generate a new one:"); } else { eprintln!("Generate and add a new recipient key for yourself:"); } if system_has_secret { eprintln!( " {}", style::highlight(format!("{bin} recipients add --secret")) ); } eprintln!( " {}", style::highlight(format!("{bin} recipients generate")) ); eprintln!(); } // Hint show/copy commands if user has secret let has_secret = store.secret_iter().next().is_some(); if has_closed_tomb || has_secret { #[cfg(not(feature = "clipboard"))] eprintln!("Show a secret:"); #[cfg(feature = "clipboard")] eprintln!("Show or copy a secret:"); eprintln!(" {}", style::highlight(format!("{bin} show [NAME]"))); #[cfg(feature = "clipboard")] eprintln!(" {}", style::highlight(format!("{bin} copy [NAME]"))); eprintln!(); } // Hint add/edit/remove commands if store has recipient we own if has_closed_tomb || we_own_any_recipient { eprintln!("Generate, add, edit or remove secrets:"); eprintln!(" {}", style::highlight(format!("{bin} generate "))); eprintln!(" {}", style::highlight(format!("{bin} add "))); eprintln!(" {}", style::highlight(format!("{bin} edit [NAME]"))); eprintln!(" {}", style::highlight(format!("{bin} remove [NAME]"))); eprintln!(); } // Hint about sync if !has_closed_tomb { if has_sync { eprintln!("Sync your password store:"); eprintln!(" {}", style::highlight(format!("{bin} sync"))); eprintln!(); } else { eprintln!("Enable sync for your password store:"); eprintln!(" {}", style::highlight(format!("{bin} sync init"))); eprintln!(); } } } else { eprintln!("Initialize a new password store or clone an existing one:"); eprintln!(" {}", style::highlight(format!("{bin} init"))); eprintln!(" {}", style::highlight(format!("{bin} clone "))); eprintln!(); } eprintln!("Show all subcommands, features and other help:"); eprintln!( " {}", style::highlight(format!("{bin} help [SUBCOMMAND]")) ); quit() } prs-v0.5.2/cli/src/util/000077500000000000000000000000001471372304600150325ustar00rootroot00000000000000prs-v0.5.2/cli/src/util/base64.rs000066400000000000000000000006331471372304600164660ustar00rootroot00000000000000use base64::{DecodeError, Engine}; /// Encode a string as base64 with standard encoding. pub(crate) fn encode>(input: T) -> String { base64::engine::general_purpose::STANDARD.encode(input) } /// Decode a string from base64 with standard decoding. pub(crate) fn decode>(input: T) -> Result, DecodeError> { base64::engine::general_purpose::STANDARD.decode(input) } prs-v0.5.2/cli/src/util/cli.rs000066400000000000000000000064571471372304600161630ustar00rootroot00000000000000use std::io::{stderr, stdin, Write}; use crate::cmd::matcher::MainMatcher; use crate::util::error::{quit_error, quit_error_msg, ErrorHints}; /// Prompt the user to enter some value. /// The prompt that is shown should be passed to `msg`, /// excluding the `:` suffix. pub fn prompt(msg: &str, main_matcher: &MainMatcher) -> String { // Quit with an error if we may not interact if main_matcher.no_interact() { quit_error_msg( format!("could not prompt for '{msg}' in no-interact mode, maybe specify it",), ErrorHints::default(), ); } // Show the prompt eprint!("{msg}: "); let _ = stderr().flush(); // Get the input let mut input = String::new(); if let Err(err) = stdin() .read_line(&mut input) .map_err(|err| -> anyhow::Error { err.into() }) { quit_error( err.context("failed to read input from prompt"), ErrorHints::default(), ); } // Trim and return input.trim().to_owned() } /// Prompt the user for a question, allowing a yes or now answer. /// True is returned if yes was answered, false if no. /// /// A default may be given, which is chosen if no-interact mode is /// enabled, or if enter was pressed by the user without entering anything. pub fn prompt_yes(msg: &str, def: Option, main_matcher: &MainMatcher) -> bool { // Define the available options string let options = format!( "[{}/{}]", match def { Some(def) if def => "Y", _ => "y", }, match def { Some(def) if !def => "N", _ => "n", } ); // Assume yes if main_matcher.assume_yes() { eprintln!("{msg} {options}: yes"); return true; } // Autoselect if in no-interact mode if main_matcher.no_interact() { if let Some(def) = def { eprintln!("{} {}: {}", msg, options, if def { "yes" } else { "no" }); return def; } else { quit_error_msg( format!("could not prompt question '{msg}' in no-interact mode, maybe specify it",), ErrorHints::default(), ); } } // Get the user input let answer = prompt(&format!("{msg} {options}"), main_matcher); // Assume the default if the answer is empty if answer.is_empty() { if let Some(def) = def { return def; } } // Derive a boolean and return match derive_bool(&answer) { Some(answer) => answer, None => prompt_yes(msg, def, main_matcher), } } /// Try to derive true or false (yes or no) from the given input. /// None is returned if no boolean could be derived accurately. fn derive_bool(input: &str) -> Option { // Process the input let input = input.trim().to_lowercase(); // Handle short or incomplete answers match input.as_str() { "y" | "ye" | "t" | "1" => return Some(true), "n" | "f" | "0" => return Some(false), _ => {} } // Handle complete answers with any suffix if input.starts_with("yes") || input.starts_with("true") { return Some(true); } if input.starts_with("no") || input.starts_with("false") { return Some(false); } // The answer could not be determined, return none None } prs-v0.5.2/cli/src/util/clipboard.rs000066400000000000000000000523451471372304600173500ustar00rootroot00000000000000use std::io::{Error as IoError, ErrorKind as IoErrorKind, Write}; use std::process::{Child, Stdio}; use std::sync::Mutex; use std::thread; use std::time::{Duration, Instant}; use anyhow::{anyhow, Result}; use copypasta_ext::display::DisplayServer; use copypasta_ext::prelude::*; #[cfg(all(feature = "notify", target_os = "linux", not(target_env = "musl")))] use notify_rust::Hint; #[cfg(all(feature = "notify", not(target_env = "musl")))] use notify_rust::Notification; use prs_lib::Plaintext; use thiserror::Error; use crate::util::{ base64, cmd::{self, CommandExt}, error::{self, ErrorHintsBuilder}, }; /// Delay for checking changed clipboard in `timeout_or_clip_change`. const TIMEOUT_CLIP_CHECK_DELAY: Duration = Duration::from_secs(3); /// Delay between each spin in `timeout_or_clip_change`. const TIMEOUT_CLIP_SPIN_DELAY: Duration = Duration::from_secs(5); #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), ))] /// Additional time in which a `TempClip` will be reused after its timeout has been reached. const TEMP_CLIP_REUSE_PERIOD: Duration = Duration::from_secs(1); lazy_static! { /// Shared clipboard context. static ref CLIP: SharedContext = Default::default(); /// Shared clipboard manager in this process. static ref CLIP_MANAGER: ClipManager = Default::default(); } /// Copy the given data to the clipboard. pub(crate) fn copy(data: &Plaintext, quiet: bool, verbose: bool) -> Result<()> { CLIP_MANAGER.copy(data, quiet, verbose) } /// Copy the given data to the clipboard, revert clipboard state after timeout. pub(crate) fn copy_timeout_revert( data: Plaintext, timeout: Duration, quiet: bool, verbose: bool, ) -> Result<()> { CLIP_MANAGER.copy_timeout_revert(data, timeout, quiet, verbose) } /// Copy the given plain text to the user clipboard. pub(crate) fn copy_plaintext( mut plaintext: Plaintext, first_line: bool, error_empty: bool, quiet: bool, verbose: bool, timeout: u64, ) -> Result<()> { if first_line { plaintext = plaintext.first_line()?; } // Do not copy empty secret if error_empty && plaintext.is_empty() { error::quit_error_msg( "secret is empty, did not copy to clipboard", ErrorHintsBuilder::default().force(true).build().unwrap(), ) } // Copy with timeout copy_timeout_revert(plaintext, Duration::from_secs(timeout), quiet, verbose) .map_err(Err::CopySecret)?; Ok(()) } /// Clipboard context shared across this process. /// /// This is lazy and initializes the context on use. #[derive(Default)] struct SharedContext { ctx: Mutex>>, } impl SharedContext { /// Ensure clipboard context is ready or error. /// /// If the context wasn't made ready yet, it will be initialized now. fn ensure_context(&self) -> Result<()> { let mut guard = self.ctx.lock().unwrap(); match *guard { Some(_) => Ok(()), None => { *guard = Some(copypasta_ext::try_context().ok_or(Err::NoProvider)?); Ok(()) } } } /// Get clipboard contents. pub fn get(&self) -> Result { self.ensure_context()?; let mut guard = self.ctx.lock().unwrap(); let ctx = guard.as_mut().ok_or(Err::NoProvider)?; Ok(ctx .get_contents() .map(|d| d.into_bytes().into()) .unwrap_or_else(|_| Plaintext::empty())) } /// Set clipboard contents. pub fn set(&self, data: &Plaintext) -> Result<()> { self.ensure_context()?; let mut guard = self.ctx.lock().unwrap(); let ctx = guard.as_mut().ok_or(Err::NoProvider)?; ctx.set_contents(data.unsecure_to_str().unwrap().into()) .map_err(|e| Err::Set(anyhow!(e)))?; Ok(()) } /// Get clipboard context display server. #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), not(target_env = "musl") ))] pub fn display_server(&self) -> Result<Option<DisplayServer>> { self.ensure_context()?; Ok(self .ctx .lock() .unwrap() .as_mut() .ok_or(Err::NoProvider)? .display_server()) } /// Check whether this clipboard context only holds the clipboard the lifetime of this binary. pub fn has_bin_lifetime(&self) -> Result<bool> { self.ensure_context()?; Ok(self .ctx .lock() .unwrap() .as_mut() .ok_or(Err::NoProvider)? .has_bin_lifetime()) } } /// A global clipboard manager for prs. /// /// Globally instantiated at `CLIP_MANAGER`. /// /// This allows to temporarily set clipboard contents after which the original state is reverted. /// On X11 and Wayland systems this ensures the clipboard is kept alive even if the prs binary /// exits. #[derive(Default)] struct ClipManager { clip: Mutex<Option<TempClip>>, } impl ClipManager { /// Copy the given data to the clipboard without reverting. pub fn copy(&self, data: &Plaintext, quiet: bool, verbose: bool) -> Result<()> { // Stop any current clipboard without reverting if let Some(clip) = self.clip.lock().unwrap().take() { clip.stop_no_revert(); } set(data, true, false, quiet, verbose).map_err(Err::Set)?; if !quiet { eprintln!("Secret copied to clipboard"); } Ok(()) } /// Copy the given data to the clipboard, revert clipboard state after timeout. pub fn copy_timeout_revert( &self, data: Plaintext, timeout: Duration, quiet: bool, verbose: bool, ) -> Result<()> { let mut clip_guard = self.clip.lock().unwrap(); if !quiet { eprintln!( "Secret copied to clipboard. Clearing after {} seconds...", timeout.as_secs(), ); } // If the temporary clipboard is still active, we should replace it if let Some(clip) = &mut *clip_guard { if clip.is_active() { clip.replace(data, timeout, quiet, verbose) .map_err(Err::ClipMan)?; return Ok(()); } } // Create new clip session *clip_guard = Some(TempClip::new(data, timeout, quiet, verbose).map_err(Err::ClipMan)?); Ok(()) } } /// Temporary clipboard content handler. /// /// Temporarily set clipboard contents, revert to original clipboard state after specified timeout. /// The clipboard remains intact if it is changed in the meanwhile. /// /// This internally spawns a subprocess which is disowned to handle the timeout in a non-blocking /// manner. Dropping this struct doesn't forget the subprocess if still active keeping the timeout /// and clipboard reversal intact. /// /// Warning: two TempClip instances should never be used at the same time, as this will introduce /// unexpected results. struct TempClip { data: Plaintext, old_data: Plaintext, process: Option<Child>, timeout_until: Instant, } impl TempClip { /// Construct a new clipboard session. /// /// Copy the given data for the given timeout, then revert the clipboard. /// This internally spawns a subprocess to handle the timeout and reversal. pub fn new(data: Plaintext, timeout: Duration, quiet: bool, verbose: bool) -> Result<Self> { let mut session = Self { data, old_data: get()?, process: None, timeout_until: Instant::now() + timeout, }; session.spawn(timeout, quiet, verbose)?; Ok(session) } /// Replace the contents of this clipboard session and reset the timeout. /// /// This keeps the original clipboard state intact but respawns the timeout and reversal subprocess. pub fn replace( &mut self, data: Plaintext, timeout: Duration, quiet: bool, verbose: bool, ) -> Result<()> { self.data = data; self.timeout_until = Instant::now() + timeout; self.spawn(timeout, quiet, verbose)?; Ok(()) } /// Spawn or respawn detached process to copy, timeout and revert. fn spawn(&mut self, timeout: Duration, quiet: bool, verbose: bool) -> Result<()> { // Kill current child to spawn new process self.kill_child(); self.process = Some(spawn_process_copy_revert( &self.data, &self.old_data, timeout.as_secs(), quiet, verbose, )?); Ok(()) } /// Check whether a reverting subprocess is currently active. fn is_active(&mut self) -> bool { // Assume active if child is still running let active = self.process.as_mut().map(is_child_running).unwrap_or(false); // On X11/Wayland systems the child remains active until the reverted clipboard is // replaced, this means that the lifetime may be a lot longer than the clipboard timeout. // On these systems, if active, we must test against the timeout time as well. If the // timeout is reached with a small grace period, this isn't active anymore. #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), ))] if active && (self.timeout_until + TEMP_CLIP_REUSE_PERIOD) <= Instant::now() { return false; } active } /// Kill the subprocess if still alive. fn kill_child(&mut self) { let mut process = self.process.take(); // Child must still be running if !process.as_mut().map(is_child_running).unwrap_or(false) { return; } // Detach and kill child if let Some(mut child) = process { match child.kill() { Ok(_) => {} Err(err) if err.kind() == IoErrorKind::InvalidInput => {} Err(err) => { error::print_warning(format!( "failed to kill clipboard subprocess, may cause weird behavior: {err}", )); } } } } /// Stop this temporary clipboard without reverting. pub fn stop_no_revert(mut self) { self.kill_child(); } /// Stop and revert the clipboard to its old contents if unchanged. /// /// If the clipboard data is still set, it reverts to the original clipboard state. If that /// state is unknown, the clipboard is cleared to prevent it holding sensitive data. #[allow(unused)] pub fn stop_revert(mut self) -> Result<()> { self.kill_child(); // Get current contents, revert to old if failed let current = match get() { Ok(data) => data, Err(_) => { return set(&self.old_data, true, false, true, false); } }; // If current data is still copied, revert to original state or clear for security reasons if current == self.data { if !self.old_data.is_empty() { return set(&self.old_data, true, false, true, false); } else { return set(&Plaintext::empty(), false, false, true, false); } } // If current data is empty, revert to old content if that isn't empty if current.is_empty() && !self.old_data.is_empty() { return set(&self.old_data, true, false, true, false); } Ok(()) } } /// Get current clipboard contents. fn get() -> Result<Plaintext> { CLIP.get() } /// Set clipboard data. /// /// On X11/Wayland this only sets the clipboard for the lifetime of the current process. /// Use `forever` to keep clipboard contents forever. /// If `forever_blocks` is true, setting forever will block until the clipboard is changed. If /// false, this spawns a subprocess to keep the clipboard non-blocking. /// /// On other display servers this will always set the clipboard forever. fn set( data: &Plaintext, forever: bool, #[allow(unused_variables)] forever_blocks: bool, quiet: bool, verbose: bool, ) -> Result<()> { // Set clipboard forever using subprocess/blocking on X11/Wayland if this context is limited to binary // lifetime if forever && CLIP.has_bin_lifetime().unwrap_or(false) { #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), not(target_env = "musl") ))] match CLIP.display_server() { Ok(Some(DisplayServer::X11)) if forever_blocks => { x11_set_blocking(data).map_err(Err::Set)?; return Ok(()); } Ok(Some(DisplayServer::Wayland)) if forever_blocks => { set_blocking(data, quiet, verbose).map_err(Err::Set)?; return Ok(()); } _ => {} } return spawn_process_copy(data, quiet, verbose).map(|_| ()); } // Set clipboard normally CLIP.set(data) } /// On any clipboard system, set the clipboard and block until it is changed. /// /// Warning: this implementation is very inefficient, and may block for up to a minute longer when /// the clipboard has changed. #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), not(target_env = "musl") ))] fn set_blocking(data: &Plaintext, quiet: bool, verbose: bool) -> Result<()> { const BASE: Duration = Duration::from_secs(2); const MAX_DELAY: Duration = Duration::from_secs(60); // Set clipboard set(data, false, false, quiet, verbose)?; // Spin until changed let mut delay = BASE; loop { thread::sleep(delay); // Stop if changed if !CLIP.get().map(|d| &d == data).unwrap_or(false) { return Ok(()); } // Increas edelay delay = (delay + BASE).min(MAX_DELAY); } } /// On X11 set the clipboard and block until its changed. #[cfg(all( unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), not(target_env = "musl") ))] fn x11_set_blocking(data: &Plaintext) -> Result<()> { use x11_clipboard::Clipboard as X11Clipboard; // Obtain new X11 clipboard context, set clipboard contents let clip = X11Clipboard::new().expect("failed to obtain X11 clipboard context"); clip.store( clip.setter.atoms.clipboard, clip.setter.atoms.utf8_string, data.unsecure_ref(), ) .map_err(|err| Err::Set(anyhow!(err)))?; // Wait for clipboard to change, then kill fork if let Err(err) = clip.load_wait( clip.setter.atoms.clipboard, clip.getter.atoms.utf8_string, clip.getter.atoms.property, ) { error::print_warning(format!( "failed wait for X11 clipboard change, may cause weird behavior: {err}", )); } Ok(()) } /// Copy the given data to the clipboard in a subprocess. fn spawn_process_copy(data: &Plaintext, quiet: bool, verbose: bool) -> Result<Child> { // Spawn & disown background process to set clipboard let mut process = cmd::current_cmd() .ok_or(Err::NoSubProcess)? .arg_if("--quiet", quiet) .arg_if("--verbose", verbose) .args(["internal", "clip"]) .stdin(Stdio::piped()) .spawn() .map_err(Err::SpawnProcess)?; // Send data to copy to process writeln!( process.stdin.as_mut().unwrap(), "{}", base64::encode(data.unsecure_ref()), ) .map_err(Err::ConfigProcess)?; Ok(process) } /// Copy the given data to the clipboard in a subprocess. /// Revert to the old data after the given timeout. fn spawn_process_copy_revert( data: &Plaintext, data_old: &Plaintext, timeout_sec: u64, quiet: bool, verbose: bool, ) -> Result<Child> { // Spawn & disown background process to set clipboard let mut process = cmd::current_cmd() .ok_or(Err::NoSubProcess)? .arg_if("--quiet", quiet) .arg_if("--verbose", verbose) .args(["internal", "clip-revert"]) .arg("--timeout") .arg(format!("{timeout_sec}")) .stdin(Stdio::piped()) .spawn() .map_err(Err::SpawnProcess)?; // Send data to copy to process writeln!( process.stdin.as_mut().unwrap(), "{},{}", base64::encode(data.unsecure_ref()), base64::encode(data_old.unsecure_ref()), ) .map_err(Err::ConfigProcess)?; Ok(process) } /// Subprocess logic for `spawn_process_copy`. /// /// This should be called in the subprocess that is spawned with `spawn_process_copy`. /// /// Copies the given data to the clipboard. pub(crate) fn subprocess_copy(data: &Plaintext, quiet: bool, verbose: bool) -> Result<()> { set(data, false, true, quiet, verbose).map_err(Err::Set)?; Ok(()) } /// Subprocess logic for `spawn_process_copy_revert`. /// /// This should be called in the subprocess that is spawned with `spawn_process_copy_revert`. /// /// Copies the given data to the clipboard, and reverts to the old data after the timeout if the /// clipboard contents have not been changed. pub(crate) fn subprocess_copy_revert( data: &Plaintext, data_old: &Plaintext, timeout: Duration, quiet: bool, verbose: bool, ) -> Result<()> { set(data, false, false, quiet, verbose).map_err(Err::Set)?; // Wait for timeout or until clipboard is changed let changed = timeout_or_clip_change(data, timeout); // Revert clipboard to previous if contents didn't change if changed { if !quiet { notify_cleared(true, false).map_err(Err::Notify)?; } } else if &get().map_err(Err::Get)? == data { if !quiet { notify_cleared(false, !data_old.is_empty()).map_err(Err::Notify)?; } set(data_old, true, true, quiet, verbose).map_err(Err::Revert)?; } Ok(()) } /// Wait for the given timeout or until the clipboard content is different than `data`. /// /// Has a warmup of at least 3 seconds before it tests the clipboard for changes. /// This is required to give the subprocess enough time to start and set the initial clipboard. /// /// Returns `true` if returned early due to a clipboard change. pub(crate) fn timeout_or_clip_change(data: &Plaintext, timeout: Duration) -> bool { let until = Instant::now() + timeout; let check_clip_from = Instant::now() + TIMEOUT_CLIP_CHECK_DELAY; loop { let now = Instant::now(); // Return early if content has changed if now >= check_clip_from { let got = CLIP.get(); if matches!(got, Ok(ref d) if d != data) { return true; } } // Test if timeout is reached if now >= until { return false; } // Wait a little before checking again thread::sleep((until - Instant::now()).min(TIMEOUT_CLIP_SPIN_DELAY)); } } /// Show notification to user about cleared clipboard. pub(crate) fn notify_cleared(changed: bool, restored: bool) -> Result<()> { // Do not show notification with not notify or on musl due to segfault #[cfg(all(feature = "notify", not(target_env = "musl")))] { let title = if changed { "changed" } else if restored { "restored" } else { "cleared" }; let mut n = Notification::new(); n.appname(&crate::util::bin_name()) .summary(&format!( "Clipboard {} - {}", title, crate::util::bin_name() )) .body("Secret wiped from clipboard") .auto_icon() .icon("lock") .timeout(3000); #[cfg(target_os = "linux")] n.urgency(notify_rust::Urgency::Low) .hint(Hint::Category("presence.offline".into())); n.show()?; return Ok(()); } // Fallback if we cannot notify #[cfg_attr( all(feature = "notify", not(target_env = "musl")), expect(unreachable_code) )] { eprintln!("Secret wiped from clipboard"); Ok(()) } } /// Check if the given child is still running. /// /// Assumes no on failure. fn is_child_running(child: &mut Child) -> bool { matches!(child.try_wait(), Ok(None)) } #[derive(Debug, Error)] pub enum Err { #[error("no supported clipboard provider available")] NoProvider, #[error("failed to prepare prs clipboard manager")] ClipMan(#[source] anyhow::Error), #[error("failed to use clipboard, no way to spawn subprocess for clipboard manager, must run as standalone binary")] NoSubProcess, #[error("failed to spawn subprocess for clipboard manager")] SpawnProcess(#[source] IoError), #[error("failed to configure subprocess for clipboard manager")] ConfigProcess(#[source] IoError), #[error("failed to get clipboard contents")] Get(#[source] anyhow::Error), #[error("failed to set clipboard contents")] Set(#[source] anyhow::Error), #[error("failed to revert clipboard contents")] Revert(#[source] anyhow::Error), #[error("failed to copy secret to clipboard")] CopySecret(#[source] anyhow::Error), #[error("failed to notify user for cleared clipboard")] Notify(#[source] anyhow::Error), } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/cmd.rs����������������������������������������������������������������������0000664�0000000�0000000�00000001432�14713723046�0016143�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#![cfg(feature = "clipboard")] use std::ffi::OsStr; use std::process::Command; /// Get the current command to spawn a subprocess. pub(crate) fn current_cmd() -> Option<Command> { let current_exe = match std::env::current_exe() { Ok(exe) => exe, Err(_) => match std::env::args().next() { Some(bin) => bin.into(), None => return None, }, }; Some(Command::new(current_exe)) } /// Command extensions. pub(crate) trait CommandExt { fn arg_if<S: AsRef<OsStr>>(&mut self, arg: S, condition: bool) -> &mut Command; } impl CommandExt for Command { fn arg_if<S: AsRef<OsStr>>(&mut self, arg: S, condition: bool) -> &mut Command { if condition { self.arg(arg) } else { self } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/edit.rs���������������������������������������������������������������������0000664�0000000�0000000�00000006210�14713723046�0016324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::env; use std::fs; use std::path::{Path, PathBuf}; use anyhow::Result; use thiserror::Error; use prs_lib::Plaintext; /// Shared memory tmpfs mount path on Unix (Linux?) systems. #[cfg(unix)] const UNIX_DIR_SHM: &str = "/dev/shm"; /// Edit given plaintext in default editor. /// /// Only returns `Plaintext` if changed. pub fn edit(plaintext: &Plaintext) -> Result<Option<Plaintext>> { // Create secure temporary file for secret let mut builder = edit::Builder::new(); builder.prefix(".prs-secret-"); builder.suffix(".txt"); let file = builder.tempfile_in(dir()).map_err(Err::Create)?; // Show Windows users where to save the file because notepad doesn't remember properly #[cfg(windows)] eprintln!( "Opening editor, save edited file at: {}", file.path().display() ); // Attempt to edit plaintext, explicitly close/remove file, handle errors last let new_plaintext = write_edit_read(plaintext, file.path()); file.close().map_err(Err::Close)?; let new_plaintext = new_plaintext?; // Return none if unchanged if plaintext == &new_plaintext { return Ok(None); } Ok(Some(new_plaintext)) } /// Edit the given plaintext in the given file. /// /// This writes the plaintext to the file, opens it in the default editor, and reads it after /// closing. fn write_edit_read(plaintext: &Plaintext, file: &Path) -> Result<Plaintext> { fs::write(file, plaintext.unsecure_ref()).map_err(Err::Write)?; edit::edit_file(file).map_err(Err::Edit)?; Ok(fs::read(file).map_err(Err::Read)?.into()) } /// Get directory to store files to edit in. /// /// This attempts to use a secure directory if available, such as `/dev/shm` which doesn't store /// anything on disk. Otherwise it defaults to the systems temporary directory. fn dir() -> PathBuf { // Default to home directory on Windows due to notepad issues // Notepad, the default editor on Windows, is too retarded to save at the path we opened the // file at. Instead it always shows the 'Save As' dialog, no matter what, which defaults to the // users home folder. We'll just store the file there then... #[cfg(windows)] { if let Some(home) = dirs_next::home_dir() { return home; } } // Default to temporary dir #[cfg_attr(not(unix), expect(unused_mut))] let mut dir = env::temp_dir(); // Prefer shared memory tmpfs if available so data won't leak to persistent disk #[cfg(unix)] { let dev_shm = PathBuf::from(UNIX_DIR_SHM); if dev_shm.is_dir() { dir = dev_shm; } } dir } #[derive(Debug, Error)] pub enum Err { #[error("failed to create secure temporary file to edit data in")] Create(#[source] std::io::Error), #[error("failed to write data to temporary file to edit")] Write(#[source] std::io::Error), #[error("failed to open default editor to edit file")] Edit(#[source] std::io::Error), #[error("failed to read from edited file")] Read(#[source] std::io::Error), #[error("failed to close/remove temporary file, this may be a security issue")] Close(#[source] std::io::Error), } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/error.rs��������������������������������������������������������������������0000664�0000000�0000000�00000014642�14713723046�0016540�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::borrow::Borrow; use std::fmt::{Debug, Display}; use std::io::{self, Write}; pub use std::process::exit; use anyhow::anyhow; use crate::cmd::matcher::MainMatcher; use crate::util::style::{highlight, highlight_error, highlight_info, highlight_warning}; // /// Print a success message. // pub fn print_success(msg: &str) { // eprintln!("{}", msg.green()); // } /// Print the given error in a proper format for the user, /// with it's causes. pub fn print_error(err: anyhow::Error) { // Report each printable error, count them let count = err .chain() .map(|err| format!("{err}")) .filter(|err| !err.is_empty()) .enumerate() .map(|(i, err)| { if i == 0 { eprintln!("{} {}", highlight_error("error:"), err); } else { eprintln!("{} {}", highlight_error("caused by:"), err); } }) .count(); // Fall back to a basic message if count == 0 { eprintln!("{} an undefined error occurred", highlight_error("error:"),); } } /// Print the given error message in a proper format for the user, /// with it's causes. pub fn print_error_msg<S>(err: S) where S: AsRef<str> + Display + Debug + Sync + Send + 'static, { print_error(anyhow!(err)); } /// Print a warning. pub fn print_warning<S>(err: S) where S: AsRef<str> + Display + Debug + Sync + Send + 'static, { eprintln!("{} {}", highlight_warning("warning:"), err); } /// Quit the application regularly. pub fn quit() -> ! { exit(0); } /// Quit the application with an error code, /// and print the given error. pub fn quit_error(err: anyhow::Error, hints: impl Borrow<ErrorHints>) -> ! { // Print the error print_error(err); // Print error hints hints.borrow().print(false); // Quit exit(1); } /// Quit the application with an error code, /// and print the given error message. pub fn quit_error_msg<S>(err: S, hints: impl Borrow<ErrorHints>) -> ! where S: AsRef<str> + Display + Debug + Sync + Send + 'static, { quit_error(anyhow!(err), hints); } /// The error hint configuration. #[derive(Clone, Builder)] #[builder(default)] pub struct ErrorHints { /// A list of info messages to print along with the error. info: Vec<String>, /// Show about the sync action. sync: bool, /// Show about the sync init action. sync_init: bool, /// Show about the sync remote action. sync_remote: bool, /// Show about the git action. git: bool, /// Show allow dirty flag. allow_dirty: bool, /// Show about the force flag. force: bool, /// Show about the verbose flag. verbose: bool, /// Show about the help flag. help: bool, } impl ErrorHints { /// Check whether any hint should be printed. pub fn any(&self) -> bool { !self.info.is_empty() || self.sync || self.sync_init || self.sync_remote || self.git || self.allow_dirty || self.force || self.verbose || self.help } /// Print the error hints. pub fn print(&self, end_newline: bool) { // Print info messages for msg in &self.info { eprintln!("{} {}", highlight_info("info:"), msg); } // Stop if nothing should be printed if !self.any() { return; } eprintln!(); // Print hints let bin = crate::util::bin_name(); if self.sync { eprintln!( "To sync your password store use '{}'", highlight(format!("{bin} sync")) ); } if self.sync_init { eprintln!( "To initialize sync for your password store use '{}'", highlight(format!("{bin} sync init")) ); } if self.sync_remote { eprintln!( "Use '{}' to get or set a remote sync URL", highlight(format!("{bin} sync remote [GIT_URL]")) ); } if self.git || self.allow_dirty { eprintln!( "Use '{}' to show sync status, uncommitted changes and help", highlight(format!("{bin} sync status")) ); } if self.git { eprintln!( "Use '{}' to inspect or resolve this issue using git", highlight(format!("{bin} git")) ); } if self.allow_dirty { eprintln!( "To make changes while the store repository is dirty add '{}'", highlight("--allow-dirty") ); } if self.force { eprintln!("Use '{}' to force", highlight("--force")); } if self.verbose { eprintln!("For a detailed log add '{}'", highlight("--verbose")); } if self.help { eprintln!("For more information add '{}'", highlight("--help")); } // End with additional newline if end_newline { eprintln!(); } // Flush let _ = io::stderr().flush(); } /// Construct an error hints object with defaults based on the main matcher. pub fn from_matcher(matcher_main: &MainMatcher) -> Self { ErrorHintsBuilder::from_matcher(matcher_main) .build() .unwrap() } } impl Default for ErrorHints { fn default() -> Self { ErrorHints { info: Vec::new(), sync: false, sync_init: false, sync_remote: false, git: false, allow_dirty: false, force: false, verbose: true, help: true, } } } impl ErrorHintsBuilder { /// Construct an error hints object with defaults based on the main matcher. pub fn from_matcher(matcher_main: &MainMatcher) -> Self { let mut builder = Self::default(); if matcher_main.force() { builder.force = Some(false); } if matcher_main.verbose() { builder.verbose = Some(false); } builder } /// Add a single info entry. pub fn add_info(mut self, info: String) -> Self { // Initialize the info list if self.info.is_none() { self.info = Some(Vec::new()); } // Add the item to the info list if let Some(ref mut list) = self.info { list.push(info); } self } } ����������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/fs.rs�����������������������������������������������������������������������0000664�0000000�0000000�00000002407�14713723046�0016013�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#[cfg(all(feature = "tomb", target_os = "linux"))] use std::fs; use std::path::Path; use anyhow::Result; use thiserror::Error; use crate::util::error::{self, ErrorHints}; /// Ensure the given path is a free directory. /// /// Checks whether the given path is not a directory, or whehter the directory is empty. /// Quits on error. pub fn ensure_dir_free(path: &Path) -> Result<(), std::io::Error> { // Fine if not a directory if !path.is_dir() { return Ok(()); } // Fine if no paths in dir if path.read_dir()?.count() == 0 { return Ok(()); } error::quit_error_msg( format!( "cannot initialize store, directory already exists: {}", path.display(), ), ErrorHints::default(), ) } /// Check whether the system has SWAP enabled. #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn has_swap() -> Result<bool, Err> { Ok(fs::read_to_string("/proc/swaps") .map_err(Err::HasSwap)? .lines() .nth(1) .filter(|l| !l.trim().is_empty()) .is_some()) } #[derive(Debug, Error)] pub enum Err { #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to check whether system has active SWAP")] HasSwap(#[source] std::io::Error), } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/mod.rs����������������������������������������������������������������������0000664�0000000�0000000�00000004664�14713723046�0016171�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#[cfg(feature = "clipboard")] pub mod base64; pub mod cli; #[cfg(feature = "clipboard")] pub mod clipboard; pub mod cmd; pub mod edit; pub mod error; pub mod fs; pub mod pass; pub mod progress; pub mod secret; pub mod select; pub mod select_basic; #[cfg(feature = "select-fzf-bin")] pub mod select_fzf_bin; #[cfg(all(feature = "select-skim", unix))] pub mod select_skim; #[cfg(feature = "select-skim-bin")] pub mod select_skim_bin; pub mod stdin; pub mod style; pub mod sync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod time; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod tomb; #[cfg(feature = "totp")] pub mod totp; use std::env; use std::path::{Path, PathBuf}; use std::process::Command; use crate::util::error::{quit_error_msg, ErrorHints}; /// Invoke a command. /// /// Quit on error. // TODO: do not wrap commands in sh/cmd, we should not have to do this and only causes problems // TODO: provide list of arguments instead of a command string for better reliability/compatability pub fn invoke_cmd(cmd: &str, dir: Option<&Path>, verbose: bool) -> Result<(), std::io::Error> { if verbose { eprintln!("$ {cmd}\n"); } // Invoke command let args = shlex::split(cmd).expect("no command specified"); let mut process = Command::new(&args[0]); process.args(&args[1..]); if let Some(dir) = dir { process.current_dir(dir); } let status = process.status()?; // Report status errors if !status.success() { eprintln!(); quit_error_msg( format!( "{} exited with status code {}", cmd.trim_start().split(' ').next().unwrap_or("command"), status.code().unwrap_or(-1) ), ErrorHints::default(), ); } Ok(()) } /// Get the name of the executable that was invoked. /// /// When a symbolic or hard link is used, the name of the link is returned. /// /// This attempts to obtain the binary name in the following order: /// - name in first item of program arguments via `std::env::args` /// - current executable name via `std::env::current_exe` /// - crate name pub fn bin_name() -> String { env::args_os() .next() .filter(|path| !path.is_empty()) .map(PathBuf::from) .or_else(|| env::current_exe().ok()) .and_then(|p| p.file_name().map(|n| n.to_owned())) .and_then(|n| n.into_string().ok()) .unwrap_or_else(|| crate::NAME.into()) } ����������������������������������������������������������������������������prs-v0.5.2/cli/src/util/pass.rs���������������������������������������������������������������������0000664�0000000�0000000�00000004062�14713723046�0016350�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use rand::Rng; use prs_lib::Plaintext; use crate::util::error; /// Character sets to use for password generation. /// /// When generating a password, characters are sampled from all these lists. Password generation is /// retried if it doesn't contain at least one character from all the lists. const PASSWORD_CHAR_SETS: [&str; 4] = [ "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789", "!@#$%&*+-=/[]<>(),.;|", ]; /// Generate secure random password. /// /// This generates a cryptografically secure random password string. /// Password entropy (defining its strength) is dependent on the given `len`. Don't use short /// lengths. /// /// The characters used in the password being generated is defined in `PASSWORD_CHAR_SETS`. A /// password always includes at least one character from each set. /// /// The returned password is embedded in `Plaintext` for security reasons. /// /// # Panics /// /// Panics if `len` is shorter than the number of sets in `PASSWORD_CHAR_SETS`. pub fn generate_password(len: u16) -> Plaintext { // Show warning if length if too short to cover all sets let too_short = (len as usize) < PASSWORD_CHAR_SETS.len(); if too_short { error::print_warning(format!( "password length too short to use all character sets (should be at least {})", PASSWORD_CHAR_SETS.len() )); } // Obtain secure random source, build char dictionary let mut rng = rand::thread_rng(); let chars = PASSWORD_CHAR_SETS.join(""); // Build password until we have an accepted one let mut pass = String::with_capacity(len as usize); loop { for _ in 0..len { let c = rng.gen_range(0..chars.len()); let c = chars.chars().nth(c).unwrap(); pass.push(c); } // Ensure password covers all sets if too_short || PASSWORD_CHAR_SETS .iter() .all(|set| set.chars().any(|c| pass.contains(c))) { return pass.into(); } pass.truncate(0); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/progress.rs�����������������������������������������������������������������0000664�0000000�0000000�00000003353�14713723046�0017250�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use indicatif::{ProgressBar, ProgressStyle}; const MSG_LEN: usize = 20; pub(crate) fn progress_bar(len: u64, quiet: bool) -> ProgressBar { if quiet { ProgressBar::hidden() } else { let pb = ProgressBar::new(len); pb.set_style( // TODO: 20 from len ProgressStyle::with_template("{msg:20} {wide_bar} {pos}/{len} ({eta})").unwrap(), ); pb } } /// Truncate the progress prefix to the given length limit. fn trunc_msg(msg: &str, limit: usize) -> String { // Return if already within limits if msg.len() <= limit { return msg.to_string(); } // Truncate each part to 1 until limit is satisfied let mut parts = msg .split('/') .map(|s| s.to_string()) .collect::<Vec<String>>(); let parts_len = parts.len(); while parts_len - 1 + parts.iter().map(|n| n.len()).sum::<usize>() > limit { match parts.iter_mut().take(parts_len - 1).find(|p| p.len() > 1) { Some(part) => part.truncate(1), None => break, } } // Rebuild path string and truncate a final time let mut msg = parts.join("/"); msg.truncate(limit); msg } pub(crate) trait ProgressBarExt { /// Set the progress bar message and truncate it. fn set_message_trunc(&self, msg: &str); /// Always print a newline, even if hidden. fn println_always<I: AsRef<str>>(&self, msg: I); } impl ProgressBarExt for ProgressBar { fn set_message_trunc(&self, msg: &str) { self.set_message(trunc_msg(msg, MSG_LEN)); } fn println_always<I: AsRef<str>>(&self, msg: I) { if self.is_hidden() { println!("{}", msg.as_ref()) } else { self.println(msg) } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/secret.rs�������������������������������������������������������������������0000664�0000000�0000000�00000004573�14713723046�0016676�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::io::Write; use prs_lib::{Plaintext, Secret, Store}; /// Secret alias recursion limit. const SECRET_ALIAS_DEPTH: u32 = 30; /// Print the given plaintext to stdout. pub fn print(plaintext: Plaintext) -> Result<(), std::io::Error> { let mut stdout = std::io::stdout(); stdout.write_all(plaintext.unsecure_ref())?; // Always finish with newline if let Some(&last) = plaintext.unsecure_ref().last() { if last != b'\n' { stdout.write_all(b"\n")?; } } let _ = stdout.flush(); Ok(()) } /// Show full secret name if query was partial. /// /// This notifies the user on what exact secret is selected when only part of the secret name is /// entered. This is useful for when a partial (short) query selects the wrong secret. pub fn display_name( query: Option<String>, secret: &Secret, store: &Store, quiet: bool, ) -> Option<String> { // If quiet or query matches exact name, do not print it if quiet || query.map(|q| secret.name.eq(&q)).unwrap_or(false) { return None; } // Show secret with alias target if available if let Some(alias) = resolve_alias(secret, store) { Some(format!("{} -> {}", secret.name, alias.name)) } else { Some(secret.name.to_string()) } } /// Show full secret name if query was partial. /// /// This notifies the user on what exact secret is selected when only part of the secret name is /// entered. This is useful for when a partial (short) query selects the wrong secret. pub fn print_name(query: Option<String>, secret: &Secret, store: &Store, quiet: bool) { if let Some(name) = display_name(query, secret, store, quiet) { eprintln!("Secret: {name}"); } } /// Resolve secret that is aliased. /// /// This find the target alias if the given secret is an alias. This uses recursive searching. /// If the secret is not an alias, `None` is returned. fn resolve_alias(secret: &Secret, store: &Store) -> Option<Secret> { fn f(secret: &Secret, store: &Store, depth: u32) -> Option<Secret> { assert!( depth < SECRET_ALIAS_DEPTH, "failed to resolve secret alias target, recursion limit reached" ); match secret.alias_target(store) { Ok(s) => f(&s, store, depth + 1), Err(_) if depth > 0 => Some(secret.clone()), Err(_) => None, } } f(secret, store, 0) } �������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/select.rs�������������������������������������������������������������������0000664�0000000�0000000�00000003702�14713723046�0016661�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use prs_lib::{store::FindSecret, Key, Secret, Store}; /// Find and select a secret in the given store. /// /// If no exact secret is found, the user will be able to choose. /// /// `None` is returned if no secret was found or selected. pub fn store_select_secret(store: &Store, query: Option<String>) -> Option<Secret> { // TODO: do not use interactive selection with --no-interact mode #[allow(unreachable_code)] match store.find(query) { FindSecret::Exact(secret) => Some(secret), FindSecret::Many(secrets) => { // Do not show selection dialog if no secret is selected if secrets.is_empty() { return None; } // When updating features, also update warning in build.rs #[cfg(all(feature = "select-skim", unix))] { return super::select_skim::select_secret(&secrets).cloned(); } #[cfg(feature = "select-skim-bin")] { return super::select_skim_bin::select_secret(&secrets).cloned(); } #[cfg(feature = "select-fzf-bin")] { return super::select_fzf_bin::select_secret(&secrets).cloned(); } super::select_basic::select_secret(&secrets).cloned() } } } /// Select key. #[allow(unreachable_code)] pub fn select_key<'a>(keys: &'a [Key], prompt: Option<&'a str>) -> Option<&'a Key> { // TODO: do not use interactive selection with --no-interact mode // When updating features, also update warning in build.rs #[cfg(all(feature = "select-skim", unix))] { return super::select_skim::select_key(keys, prompt); } #[cfg(feature = "select-skim-bin")] { return super::select_skim_bin::select_key(keys, prompt); } #[cfg(feature = "select-fzf-bin")] { return super::select_fzf_bin::select_key(keys, prompt); } super::select_basic::select_key(keys, prompt) } ��������������������������������������������������������������prs-v0.5.2/cli/src/util/select_basic.rs�������������������������������������������������������������0000664�0000000�0000000�00000003743�14713723046�0020027�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::collections::HashMap; use crate::util::error; use prs_lib::{Key, Secret}; /// Select secret. pub fn select_secret(secrets: &[Secret]) -> Option<&Secret> { // Return if theres just one to choose if secrets.len() == 1 { return secrets.first(); } let map: HashMap<_, _> = secrets .iter() .map(|secret| (secret.name.clone(), secret)) .collect(); let items: Vec<_> = map.keys().collect(); select_item("Select key", &items) .as_ref() .map(|item| map[item]) } /// Select key. pub fn select_key<'a>(keys: &'a [Key], prompt: Option<&'a str>) -> Option<&'a Key> { let map: HashMap<_, _> = keys.iter().map(|key| (key.to_string(), key)).collect(); let items: Vec<_> = map.keys().collect(); select_item(prompt.unwrap_or("Select key"), &items) .as_ref() .map(|item| map[item]) } /// Interactively select one of the given items. fn select_item<'a, S: AsRef<str>>(prompt: &'a str, items: &'a [S]) -> Option<String> { // Build sorted list of string references as items let mut items = items.iter().map(|i| i.as_ref()).collect::<Vec<_>>(); items.sort_unstable(); loop { // Print options and prompt items .iter() .enumerate() .for_each(|(i, item)| eprintln!("{}: {}", i + 1, item)); eprint!("{prompt} (number/empty): "); let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("failed to read user input from stdin"); // If empty, we selected none if input.trim().is_empty() { return None; } // Try to parse number, select item, or show error and retry match input.trim().parse::<usize>().ok() { Some(n) if n > 0 && n <= items.len() => return Some(items[n - 1].into()), _ => { error::print_error_msg("invalid selection input"); eprintln!(); } } } } �����������������������������prs-v0.5.2/cli/src/util/select_fzf_bin.rs�����������������������������������������������������������0000664�0000000�0000000�00000004315�14713723046�0020357�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::collections::HashMap; use std::io::Write; use std::process::{Command, Stdio}; use prs_lib::{Key, Secret}; /// Binary name. #[cfg(not(windows))] const BIN_NAME: &str = "fzf"; #[cfg(windows)] const BIN_NAME: &str = "fzf.exe"; /// Select secret. pub fn select_secret(secrets: &[Secret]) -> Option<&Secret> { // Return if theres just one to choose if secrets.len() == 1 { return secrets.first(); } let map: HashMap<_, _> = secrets .iter() .map(|secret| (secret.name.clone(), secret)) .collect(); let items: Vec<_> = map.keys().collect(); select_item("Select key", &items) .as_ref() .map(|item| map[item]) } /// Select key. pub fn select_key<'a>(keys: &'a [Key], prompt: Option<&'a str>) -> Option<&'a Key> { let map: HashMap<_, _> = keys.iter().map(|key| (key.to_string(), key)).collect(); let items: Vec<_> = map.keys().collect(); select_item(prompt.unwrap_or("Select key"), &items) .as_ref() .map(|item| map[item]) } /// Interactively select one of the given items. fn select_item<'a, S: AsRef<str>>(prompt: &'a str, items: &'a [S]) -> Option<String> { // Build sorted list of string references as items let mut items = items.iter().map(|i| i.as_ref()).collect::<Vec<_>>(); items.sort_unstable(); // Spawn fzf let mut child = Command::new(BIN_NAME) .arg("--prompt") .arg(format!("{prompt}: ")) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .expect("failed to spawn fzf"); // Communicate list of items to fzf let data = items.join("\n"); child .stdin .as_mut() .unwrap() .write_all(data.as_bytes()) .expect("failed to communicate list of items to fzf"); let output = child.wait_with_output().expect("failed to select with fzf"); // No item selected on non-zero exit code if !output.status.success() { return None; } // Get selected item, assert validity let stdout = std::str::from_utf8(&output.stdout).unwrap(); let stdout = stdout.strip_suffix('\n').unwrap_or(stdout); assert!(items.contains(&stdout)); Some(stdout.into()) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/select_skim.rs��������������������������������������������������������������0000664�0000000�0000000�00000007114�14713723046�0017705�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::borrow::Cow; use std::path::PathBuf; use std::sync::Arc; use prs_lib::{Key, Secret}; use skim::{ prelude::{SkimItemReceiver, SkimItemSender, SkimOptionsBuilder}, AnsiString, DisplayContext, Skim, SkimItem, }; /// Show an interactive selection view for the given list of `items`. /// The selected item is returned. If no item is selected, `None` is returned instead. fn skim_select(items: SkimItemReceiver, prompt: &str) -> Option<String> { let prompt = format!("{prompt}: "); let options = SkimOptionsBuilder::default() .prompt(Some(&prompt)) .multi(false) // Disabled becayse of: https://github.com/lotabout/skim/issues/494 // .height(Some("50%")) .build() .unwrap(); // Run skim, get output, abort on close let output = Skim::run_with(&options, Some(items))?; if output.is_abort { return None; } // Get the first selected, and return output .selected_items .first() .map(|i| i.output().to_string()) } /// Wrapped store secret item for skim. pub struct SkimSecret(Secret); impl From<Secret> for SkimSecret { fn from(secret: Secret) -> Self { Self(secret) } } impl SkimItem for SkimSecret { fn display(&self, _: DisplayContext) -> AnsiString { self.0.name.clone().into() } fn text(&self) -> Cow<str> { (&self.0.name).into() } fn output(&self) -> Cow<str> { self.0.path.to_string_lossy() } } /// Select secret. pub fn select_secret(secrets: &[Secret]) -> Option<&Secret> { // Return if theres just one to choose if secrets.len() == 1 { return secrets.first(); } // Let user select secret let items = skim_secret_items(secrets); let selected = skim_select(items, "Select secret")?; // Pick selected item from secrets list let path: PathBuf = selected.into(); Some(secrets.iter().find(|e| e.path == path).unwrap()) } /// Select key. pub fn select_key<'a>(keys: &'a [Key], prompt: Option<&'a str>) -> Option<&'a Key> { // Let user select secret let items = skim_key_items(keys); let selected = skim_select(items, prompt.unwrap_or("Select key"))?; // Pick selected item from keys list Some( keys.iter() .find(|e| e.fingerprint(false) == selected) .unwrap(), ) } /// Generate skim `SkimSecret` items from given secrets. fn skim_secret_items(secrets: &[Secret]) -> SkimItemReceiver { skim_items( secrets .iter() .cloned() .map(|e| e.into()) .collect::<Vec<SkimSecret>>(), ) } /// Generate skim `SkimSecret` items from given secrets. fn skim_key_items(keys: &[Key]) -> SkimItemReceiver { skim_items( keys.iter() .cloned() .map(|e| e.into()) .collect::<Vec<SkimKey>>(), ) } /// Create `SkimItemReceiver` from given array. fn skim_items<I: SkimItem>(items: Vec<I>) -> SkimItemReceiver { let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = skim::prelude::bounded(items.len()); items.into_iter().for_each(|g| { let _ = tx_item.send(Arc::new(g)); }); rx_item } /// Wrapped store key item for skim. pub struct SkimKey(Key); impl From<Key> for SkimKey { fn from(key: Key) -> Self { Self(key) } } impl SkimItem for SkimKey { fn display(&self, _: DisplayContext) -> AnsiString { format!("{}", self.0).into() } fn text(&self) -> Cow<str> { format!("{}", self.0).into() } fn output(&self) -> Cow<str> { self.0.fingerprint(false).into() } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/select_skim_bin.rs����������������������������������������������������������0000664�0000000�0000000�00000004574�14713723046�0020544�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::collections::HashMap; use std::io::Write; use std::process::{Command, Stdio}; use prs_lib::{Key, Secret}; /// Binary name. #[cfg(not(windows))] const BIN_NAME: &str = "sk"; #[cfg(windows)] const BIN_NAME: &str = "sk.exe"; /// Select secret. pub fn select_secret(secrets: &[Secret]) -> Option<&Secret> { // Return if theres just one to choose if secrets.len() == 1 { return secrets.first(); } let map: HashMap<_, _> = secrets .iter() .map(|secret| (secret.name.clone(), secret)) .collect(); let items: Vec<_> = map.keys().collect(); select_item("Select key", &items) .as_ref() .map(|item| map[item]) } /// Select key. pub fn select_key<'a>(keys: &'a [Key], prompt: Option<&'a str>) -> Option<&'a Key> { let map: HashMap<_, _> = keys.iter().map(|key| (key.to_string(), key)).collect(); let items: Vec<_> = map.keys().collect(); select_item(prompt.unwrap_or("Select key"), &items) .as_ref() .map(|item| map[item]) } /// Interactively select one of the given items. fn select_item<'a, S: AsRef<str>>(prompt: &'a str, items: &'a [S]) -> Option<String> { // Build sorted list of string references as items let mut items = items.iter().map(|i| i.as_ref()).collect::<Vec<_>>(); items.sort_unstable(); // Spawn skim let mut child = Command::new(BIN_NAME) .arg("--prompt") .arg(format!("{prompt}: ")) .arg("--no-multi") // Disabled becayse of: https://github.com/lotabout/skim/issues/494 // .arg("--height") // .arg("50%") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .expect("failed to spawn skim"); // Communicate list of items to skim let data = items.join("\n"); child .stdin .as_mut() .unwrap() .write_all(data.as_bytes()) .expect("failed to communicate list of items to skim"); let output = child .wait_with_output() .expect("failed to select with skim"); // No item selected on non-zero exit code if !output.status.success() { return None; } // Get selected item, assert validity let stdout = std::str::from_utf8(&output.stdout).unwrap(); let stdout = stdout.strip_suffix('\n').unwrap_or(stdout); assert!(items.contains(&stdout)); Some(stdout.into()) } ������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/stdin.rs��������������������������������������������������������������������0000664�0000000�0000000�00000001515�14713723046�0016523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::io::{self, Read}; use anyhow::Result; use prs_lib::Plaintext; use thiserror::Error; /// Read file from stdin. fn read_file(prompt: bool) -> Result<Vec<u8>> { if prompt { #[cfg(not(windows))] eprintln!("Enter input. Use [CTRL+D] to stop:"); #[cfg(windows)] eprintln!("Enter input. Use [CTRL+Z] to stop:"); } let mut data = vec![]; io::stdin() .lock() .read_to_end(&mut data) .map_err(Err::Stdin)?; Ok(data) } /// Read plaintext from stdin. pub fn read_plaintext(prompt: bool) -> Result<Plaintext> { Ok(read_file(prompt).map_err(Err::Plaintext)?.into()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to read from stdin")] Stdin(#[source] io::Error), #[error("failed to read plaintext")] Plaintext(#[source] anyhow::Error), } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/style.rs��������������������������������������������������������������������0000664�0000000�0000000�00000001133�14713723046�0016536�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use colored::{ColoredString, Colorize}; /// Highlight the given text with a color. pub fn highlight<S: AsRef<str>>(msg: S) -> ColoredString { msg.as_ref().yellow() } /// Highlight the given text with an error color. pub fn highlight_error(msg: impl AsRef<str>) -> ColoredString { msg.as_ref().red().bold() } /// Highlight the given text with an warning color. pub fn highlight_warning(msg: impl AsRef<str>) -> ColoredString { highlight(msg).bold() } /// Highlight the given text with an info color pub fn highlight_info(msg: impl AsRef<str>) -> ColoredString { msg.as_ref().cyan() } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/sync.rs���������������������������������������������������������������������0000664�0000000�0000000�00000002151�14713723046�0016353�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use prs_lib::sync::{Readyness, Sync}; use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder}; /// Ensure the store is ready, otherwise quit. pub fn ensure_ready(sync: &Sync, allow_dirty: bool) { let readyness = match sync.readyness() { Ok(readyness) => readyness, Err(err) => { quit_error( err.context("failed to query store sync readyness state"), ErrorHintsBuilder::default().git(true).build().unwrap(), ); } }; let mut error = ErrorHintsBuilder::default(); error.git(true); if let Readyness::Dirty = readyness { error.allow_dirty(true); } quit_error_msg( match readyness { Readyness::Ready | Readyness::NoSync => return, Readyness::Dirty if allow_dirty => return, Readyness::Dirty => "store git repository is dirty and has uncommitted changes".into(), Readyness::RepoState(state) => { format!("store git repository is in unfinished state: {state:?}") } }, error.build().unwrap(), ); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/time.rs���������������������������������������������������������������������0000664�0000000�0000000�00000006520�14713723046�0016341�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use regex::Regex; use thiserror::Error; /// Parse the given duration string from human readable format into seconds. /// This method parses a string of time components to represent the given duration. /// /// The following time units are used: /// /// - `w`: weeks /// - `d`: days /// - `h`: hours /// - `m`: minutes /// - `s`: seconds /// /// The following time strings can be parsed: /// /// - `8w6d` /// - `23h14m` /// - `9m55s` /// - `1s1s1s1s1s` pub fn parse_duration(duration: &str) -> Result<usize, ParseDurationError> { // Build a regex to grab time parts let re = Regex::new(r"(?i)([0-9]+)(([a-z]|\s*$))") .expect("failed to compile duration parsing regex"); // We must find any match if re.find(duration).is_none() { return Err(ParseDurationError::Empty); } // Parse each time part, sum it's seconds let mut seconds = 0; for capture in re.captures_iter(duration) { // Parse time value and modifier let number = capture[1] .parse::<usize>() .map_err(ParseDurationError::InvalidValue)?; let modifier = capture[2].trim().to_lowercase(); // Multiply and sum seconds by modifier seconds += match modifier.as_str() { "" | "s" => number, "m" => number * 60, "h" => number * 60 * 60, "d" => number * 60 * 60 * 24, "w" => number * 60 * 60 * 24 * 7, m => return Err(ParseDurationError::UnknownIdentifier(m.into())), }; } Ok(seconds) } /// Format the given duration in a human readable format. /// This method builds a string of time components to represent /// the given duration. /// /// The following time units are used: /// - `w`: weeks /// - `d`: days /// - `h`: hours /// - `m`: minutes /// - `s`: seconds /// /// Only the two most significant units are returned. /// If the duration is zero seconds or less `now` is returned. /// /// The following time strings may be produced: /// - `8w6d` /// - `23h14m` /// - `9m55s` /// - `1s` /// - `now` pub fn format_duration(mut secs: u32) -> String { // Get the total number of seconds, return immediately if zero or less if secs == 0 { return "now".into(); } // Build a list of time units, define a list for time components let mut components = Vec::new(); let units = [ (60 * 60 * 24 * 7, "w"), (60 * 60 * 24, "d"), (60 * 60, "h"), (60, "m"), (1, "s"), ]; // Fill the list of time components based on the units which fit for unit in &units { if secs >= unit.0 { components.push(format!("{}{}", secs / unit.0, unit.1)); secs %= unit.0; } } // Show only the two most significant components and join them in a string components.truncate(2); components.join("") } /// Represents a duration parsing error. #[derive(Debug, Error)] pub enum ParseDurationError { /// The given duration string did not contain any duration part. #[error("given string did not contain any duration part")] Empty, /// A numeric value was invalid. #[error("duration part has invalid numeric value")] InvalidValue(std::num::ParseIntError), /// The given duration string contained an invalid duration modifier. #[error("duration part has unknown time identifier '{0}'")] UnknownIdentifier(String), } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/tomb.rs���������������������������������������������������������������������0000664�0000000�0000000�00000004174�14713723046�0016347�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use anyhow::Result; use prs_lib::tomb::Tomb; use crate::cmd::matcher::MainMatcher; use crate::util::{self, error, style}; /// Prepare Tomb. pub fn prepare_tomb(tomb: &mut Tomb, matcher_main: &MainMatcher) -> Result<()> { // When opening a Tomb the user must force when SWAP is available, ask whether to force if !tomb.settings.force && tomb.is_tomb() { // Tomb must not be open yet, ignore errors if let Ok(false) = tomb.is_open() { if ask_to_force(matcher_main) { tomb.settings.force = true; } } } // Prepare as normal tomb.prepare() } /// Finalize Tomb. pub fn finalize_tomb(tomb: &mut Tomb, matcher_main: &MainMatcher, changed: bool) -> Result<()> { // Ask to enlarge Tomb if it gets too small when contents changed if changed && !matcher_main.quiet() && tomb.is_tomb() && tomb.is_open().unwrap_or(false) { if let Ok(sizes) = tomb.fetch_size_stats() { if sizes.should_resize() { let bin = crate::util::bin_name(); eprintln!(); error::print_warning( "your Tomb may not have enough space left for new password store changes.", ); error::print_warning(format!( "use '{}' to make your Tomb larger", style::highlight(format!("{bin} tomb resize")) )); } } } // Finalize as normal tomb.finalize() } /// Ask user to force Tomb command. /// /// This will only prompt if: /// - the system has SWAP /// - we're in interactive mode /// /// This will not check whether the Tomb is already open, in which case forcing would not be /// required. pub fn ask_to_force(matcher_main: &MainMatcher) -> bool { // Skip if already forced if matcher_main.force() { return true; } // Skip if no swap is active, assume yes if !util::fs::has_swap().unwrap_or(true) { return false; } // Prompt eprintln!("To open a Tomb with active swap you must force, this may be insecure."); util::cli::prompt_yes("Force open?", None, matcher_main) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/util/totp.rs���������������������������������������������������������������������0000664�0000000�0000000�00000014161�14713723046�0016371�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#[cfg(feature = "clipboard")] use std::io::{Error as IoError, Write}; #[cfg(feature = "clipboard")] use std::process::{Child, Stdio}; use std::time::SystemTimeError; use anyhow::Result; use linkify::{LinkFinder, LinkKind}; use prs_lib::Plaintext; use thiserror::Error; use totp_rs::{Algorithm, Secret, TOTP}; /// OTPAUTH URL scheme. const OTPAUTH_SCHEME: &str = "otpauth://"; /// Possible property names to search in for TOTP tokens. const PROPERTY_NAMES: [&str; 2] = ["totp", "2fa"]; /// Try to find a TOTP token in the given plaintext. /// /// Returns `None` if no TOTP is found. pub fn find_token(plaintext: &Plaintext) -> Option<Result<Totp>> { // Find first TOTP URL globally if let totp @ Some(_) = find_otpauth_url(plaintext) { return totp; } // Find first TOTP in common properties if let totp @ Some(_) = PROPERTY_NAMES .iter() .flat_map(|p| plaintext.property(p)) .find_map(|p| find_token(&p)) { return totp; } // Try to parse full secret as encoded TOTP secret parse_encoded(plaintext).map(Ok) } /// Scan the plaintext for `otpauth` URLs. fn find_otpauth_url(plaintext: &Plaintext) -> Option<Result<Totp>> { // Configure linkfinder let mut finder = LinkFinder::new(); finder.url_must_have_scheme(true); finder.kinds(&[LinkKind::Url]); finder .links(plaintext.unsecure_to_str().unwrap()) .filter(|l| l.as_str().starts_with(OTPAUTH_SCHEME)) .map(|l| Totp::from_url(l.as_str())) .next() } /// Try to parse a base32 encoded TOTP token from the given plaintext. /// /// Uses RFC6238 defaults, see: /// - https://docs.rs/totp-rs/3.1.0/totp_rs/struct.Rfc6238.html#method.with_defaults /// - https://tools.ietf.org/html/rfc6238 fn parse_encoded(plaintext: &Plaintext) -> Option<Totp> { // Trim plaintext, must be base32 encoded let plaintext = plaintext.unsecure_to_str().unwrap().trim(); if !is_base32(plaintext) { return None; } // Encoded secret must have at least 16 bytes if plaintext.len() < 16 { return None; } // Decode to bytes let secret = Secret::Encoded(plaintext.to_string()); let bytes = secret.to_bytes().unwrap(); // Parse RFC6238 TOTP (with looser requirements) Some(TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, bytes, None, "".into()).into()) } /// Format a token as a string. /// /// If `quiet` is `true` the token is printed with no formatting or TTL. /// If a TTL is specified, it is printed after. pub fn format_token(token: &Plaintext, quiet: bool, ttl: Option<u64>) -> Plaintext { // If quiet, print regularly if quiet { return token.clone(); } // Format with spaces let mut formatted = if token.unsecure_ref().len() > 5 { Plaintext::from( token .unsecure_ref() .chunks(3) .map(|c| std::str::from_utf8(c).unwrap()) .collect::<Vec<_>>() .join(" "), ) } else { token.clone() }; if let Some(ttl) = ttl { formatted.append(format!(" (valid for {ttl}s)").into(), false); } formatted } /// Print a nicely formatted token. /// /// If `quiet` is `true` the token is printed with no formatting or TTL. /// If a TTL is specified, it is printed after. pub fn print_token(token: &Plaintext, quiet: bool, ttl: Option<u64>) { println!( "{}", format_token(token, quiet, ttl).unsecure_to_str().unwrap() ); } /// A secure TOTP type. /// /// This TOTP type outputs tokens as secure `Plaintext` and zeroes on drop. pub struct Totp { totp: TOTP, } impl Totp { /// Construct a TOTP from the given TOTP URL. pub fn from_url(url: &str) -> Result<Self> { TOTP::from_url_unchecked(url) .map(|t| t.into()) .map_err(|e| Err::Url(e).into()) } /// Generate a token from the current system time. pub fn generate_current(&self) -> Result<Plaintext> { self.totp .generate_current() .map(|t| t.into()) .map_err(|e| Err::Time(e).into()) } /// Generate an URL for this TOTP secret. pub fn generate_url(&self) -> Plaintext { self.totp.get_url().into() } /// Give the ttl (in seconds) of the current token. pub fn ttl(&self) -> Result<u64> { self.totp.ttl().map_err(|e| Err::Time(e).into()) } } impl From<TOTP> for Totp { fn from(totp: TOTP) -> Self { Self { totp } } } /// Check if string is base32 compliant /// /// RFC: https://www.rfc-editor.org/rfc/rfc4648#page-9 pub fn is_base32(material: &str) -> bool { material .chars() .all(|c| c.is_ascii_uppercase() || ('2'..='7').contains(&c)) } /// Copy the given data to the clipboard in a subprocess. /// Revert to the old data after the given timeout. #[cfg(feature = "clipboard")] pub(crate) fn spawn_process_totp_recopy(totp: &Totp, timeout_sec: u64) -> Result<Child> { use super::{base64, cmd}; // Spawn & disown background process to set clipboard let mut process = cmd::current_cmd() .ok_or(Err::NoSubProcess)? .args(["internal", "totp-recopy"]) .arg("--timeout") .arg(format!("{timeout_sec}")) .stdin(Stdio::piped()) .spawn() .map_err(Err::SpawnProcess)?; // Send data to copy to process writeln!( process.stdin.as_mut().unwrap(), "{}", base64::encode(totp.generate_url().unsecure_to_str().unwrap()), ) .map_err(Err::ConfigProcess)?; Ok(process) } #[derive(Debug, Error)] pub enum Err { #[error("invalid TOTP secret URL")] Url(#[source] totp_rs::TotpUrlError), #[cfg(feature = "clipboard")] #[error("failed to use clipboard, no way to spawn subprocess for clipboard manager, must run as standalone binary")] NoSubProcess, #[cfg(feature = "clipboard")] #[error("failed to spawn subprocess for clipboard manager")] SpawnProcess(#[source] IoError), #[cfg(feature = "clipboard")] #[error("failed to configure subprocess for clipboard manager")] ConfigProcess(#[source] IoError), #[error("TOTP system time error")] Time(#[source] SystemTimeError), } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/cli/src/viewer.rs������������������������������������������������������������������������0000664�0000000�0000000�00000030651�14713723046�0015731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::io::{stdin, stdout, Write}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use anyhow::Result; use crossterm::{ cursor, event::{self, Event}, execute, queue, style, style::Stylize, terminal, tty::IsTty, }; use prs_lib::{util::env, Plaintext, Secret, Store}; use substring::Substring; use thiserror::Error; use crate::cmd::matcher::MainMatcher; use crate::util::{ error::{self, ErrorHintsBuilder}, secret, }; /// Environment variable to set custom viewer/pager. const ENV_VAR_PAGER: &str = "PRS_PAGER"; /// Scroll speed when using the mouse wheel. const SCROLL_SPEED: i16 = 3; /// Show plaintext in secure viewer with optional timeout. /// /// This shows plaintext in an alternative terminal buffer, in order to keep its contents as /// secure as possible. All data is cleared when the viewer is closed. /// /// The user may quit using a quit key, or the viewer may time out. pub(crate) fn viewer( store: &Store, secret: &Secret, plaintext: Plaintext, timeout: Option<Duration>, matcher_main: &MainMatcher, query: Option<String>, ) -> Result<()> { // Use custom viewer when prs pager is configured if env::has_non_empty_env(ENV_VAR_PAGER) { return pager(plaintext, timeout, matcher_main); } // Don't show anything if empty if plaintext.is_empty() & !matcher_main.force() { if matcher_main.verbose() { eprintln!("Secret is empty"); } return Ok(()); } // Require to be in a TTY if !stdin().is_tty() { error::quit_error_msg( "secure viewer can only be used in TTY", ErrorHintsBuilder::default().verbose(false).build().unwrap(), ); } // Get secret name and title let name = secret::display_name(query, secret, store, false) .unwrap_or_else(|| secret.name.to_string()); let title = format!("{}: {}", crate::NAME, name); // Enter alternative screen now, enable raw mode, queue paint actions execute!(stdout(), terminal::EnterAlternateScreen).map_err(Err::RawTerminal)?; terminal::enable_raw_mode().map_err(Err::RawTerminal)?; queue!( stdout(), style::ResetColor, cursor::Hide, terminal::SetTitle(&title), ) .map_err(Err::RawTerminal)?; // Timeout, scroll position and text size let timeout_at = timeout.map(|t| Instant::now() + t); let mut scroll_pos: (u16, u16) = (0, 0); let text_size = { let text = plaintext.unsecure_to_str().map_err(Err::Utf8)?; ( text.lines().map(|l| l.len()).max().unwrap_or(0), text.lines().count(), ) }; // Viewer drawing loop 'window: loop { // Grab terminal size let tty_size = terminal::size().map_err(Err::Size)?; // Paint window border paint_border(tty_size, &title, timeout_at, matcher_main).map_err(Err::Render)?; loop { // Painte plaintext paint_content(&plaintext, tty_size, scroll_pos, matcher_main).map_err(Err::Render)?; // Get actions from input, stop on quit or timeout let action = match timeout_at { Some(timeout_at) => wait_action_timeout(timeout_at - Instant::now()), None => wait_action(), }; match action { // Quit or timeout reached Some(Action::Quit) | None => break 'window, Some(Action::Redraw) => { continue 'window; } Some(Action::ScrollY(amount)) => { let scroll_max = (text_size.1 as i16 - tty_size.1 as i16 + if !matcher_main.quiet() { 2 } else { 0 }) .max(0); scroll_pos.1 = (scroll_pos.1 as i16 + amount).clamp(0, scroll_max) as u16; } Some(Action::ScrollX(amount)) => { let scroll_max = (text_size.0 as i16 - tty_size.0 as i16).max(0); scroll_pos.0 = (scroll_pos.0 as i16 + amount).clamp(0, scroll_max) as u16; } } } } // Clean up alternative screen, switch back to main execute!( stdout(), terminal::Clear(terminal::ClearType::Purge), style::ResetColor, cursor::Show, terminal::LeaveAlternateScreen, ) .map_err(Err::RawTerminal)?; terminal::disable_raw_mode().map_err(Err::RawTerminal)?; Ok(()) } /// Paint window borders. /// /// Doesn't paint anything in quiet mode. fn paint_border( tty_size: (u16, u16), title: &str, timeout_at: Option<Instant>, matcher_main: &MainMatcher, ) -> Result<()> { // Don't paint window if quiet if matcher_main.quiet() { return Ok(()); } // Header queue!(stdout(), cursor::MoveTo(0, 0)).map_err(Err::RawTerminal)?; print!("{}", banner_text(title, tty_size.0).reverse()); // Footer queue!(stdout(), cursor::MoveTo(0, tty_size.1 - 1)).map_err(Err::RawTerminal)?; print!( "{}", banner_text( if let Some(timeout_at) = timeout_at { format!( "Press Q to close. Closing in {} seconds...", (timeout_at - Instant::now()).as_secs() ) } else { "Press Q to close".to_string() }, tty_size.0 ) .reverse() ); stdout().flush().map_err(Err::RawTerminal)?; Ok(()) } /// Paint window contents. fn paint_content( plaintext: &Plaintext, tty_size: (u16, u16), scroll_pos: (u16, u16), matcher_main: &MainMatcher, ) -> Result<()> { // Determine viewport size let (vw, vh, vy) = if !matcher_main.quiet() { (tty_size.0, tty_size.1 - 2, 1) } else { (tty_size.0, tty_size.1, 0) }; // Get line count and lines iterator let (line_count, mut line_iter) = { let content = plaintext.unsecure_to_str().map_err(Err::Utf8)?; ( content.lines().count(), content.lines().skip(scroll_pos.1 as usize), ) }; // Paint each line for (y, line) in (vy..=vh).map(|y| (y, line_iter.next())) { // Set cursor, clear line queue!( stdout(), cursor::MoveTo(0, y), terminal::Clear(terminal::ClearType::CurrentLine), ) .map_err(Err::RawTerminal)?; // Render tilde if there is no line if line.is_none() { print!("{}", "~".dark_grey()); continue; } // Top scroll marker if there's hidden content let first = y == vy; if first && scroll_pos.1 > 0 { let marker = "^".repeat(vw as usize); print!("{}", marker.dark_grey()); continue; } // Bottom scroll marker if there's hidden content let last = y == vh; if last && line_count.saturating_sub(scroll_pos.1 as usize) > vh as usize { let marker = "v".repeat(vw as usize); print!("{}", marker.dark_grey()); break; } let line = line.unwrap(); let len = line.chars().count(); let mark_before = scroll_pos.0 > 0; let mark_after = len.saturating_sub(scroll_pos.0 as usize) > vw as usize; let mut start = scroll_pos.0 as usize; let mut end = scroll_pos.0 as usize + vw as usize; if mark_before { start += 1; } if mark_after { end -= 1; } if mark_before { print!("{}", "<".dark_grey(),); } print!("{}", line.substring(start, end)); if mark_after { print!("{}", ">".dark_grey(),); } } stdout().flush().map_err(Err::RawTerminal)?; Ok(()) } /// Possible actions. enum Action { /// Quit viewer. Quit, /// Redraw viewer. Redraw, /// Scroll horizontal action. ScrollX(i16), /// Scroll vertical action. ScrollY(i16), } /// Wait for an action based on a terminal event indefinately. fn wait_action() -> Option<Action> { match event::read() { // Quit with Q, Esc or <c-C> Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Char('q') | event::KeyCode::Esc, .. })) => Some(Action::Quit), Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Char('c'), modifiers, .. })) if modifiers.contains(event::KeyModifiers::CONTROL) => Some(Action::Quit), // Scrolling Ok(Event::Mouse(event::MouseEvent { kind: event::MouseEventKind::ScrollUp, .. })) => Some(Action::ScrollY(-SCROLL_SPEED)), Ok(Event::Mouse(event::MouseEvent { kind: event::MouseEventKind::ScrollDown, .. })) => Some(Action::ScrollY(SCROLL_SPEED)), Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Up | event::KeyCode::Char('k'), .. })) => Some(Action::ScrollY(-1)), Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Down | event::KeyCode::Char('j'), .. })) => Some(Action::ScrollY(1)), Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Left | event::KeyCode::Char('h'), .. })) => Some(Action::ScrollX(-1)), Ok(event::Event::Key(event::KeyEvent { code: event::KeyCode::Right | event::KeyCode::Char('l'), .. })) => Some(Action::ScrollX(1)), // Resize Ok(Event::Resize(_, _) | Event::FocusGained) => Some(Action::Redraw), // Ignore other input Ok(_) | Err(_) => None, } } /// Block until a user presses an action key, with timeout. fn wait_action_timeout(timeout: Duration) -> Option<Action> { let until = Instant::now() + timeout; loop { // Return if timeout is reached let now = Instant::now(); if until <= now { return None; } // Poll, stop if timeout is reached or on error if matches!(event::poll(until - now), Ok(false) | Err(_)) { return None; } if let Some(input) = wait_action() { return Some(input); } } } /// Create a banner spanning the whole width. fn banner_text<S: AsRef<str>>(text: S, width: u16) -> String { let text = text.as_ref().trim(); // Truncate if text is too long if text.len() >= width as usize { return text.substring(0, width as usize).into(); } let empty = width as usize - text.len(); let start = empty / 2; let end = empty - start; let start = " ".repeat(start); let end = " ".repeat(end); format!("{start}{text}{end}") } /// Use custom viewer/pager from `PRS_PAGER`. fn pager( plaintext: Plaintext, timeout: Option<Duration>, matcher_main: &MainMatcher, ) -> Result<()> { // Parse pager arguments, build command let args = shlex::split(&std::env::var(ENV_VAR_PAGER).map_err(Err::PagerEnvUtf8)?).unwrap(); let mut pager = Command::new(&args[0]) .args(&args[1..]) .stdin(Stdio::piped()) .spawn() .map_err(Err::PagerSpawn)?; pager .stdin .as_mut() .unwrap() .write_all(plaintext.unsecure_ref()) .map_err(Err::PagerPipe)?; // Wait for pager to quit let status = pager.wait().map_err(Err::PagerSpawn)?; if !status.success() { return Err(Err::PagerStatus(status).into()); } // Warn if timeout is configured if timeout.is_some() && !matcher_main.quiet() { error::print_warning("timeout is not supported with custom pager (env: PRS_PAGER)"); } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to parse secret contents as UTF-8, required when using viewer")] Utf8(#[source] std::str::Utf8Error), #[error("failed to manage raw terminal")] RawTerminal(#[source] std::io::Error), #[error("failed to determine terminal size")] Size(#[source] std::io::Error), #[error("failed to render secret viewer")] Render(#[source] anyhow::Error), #[error("failed to parse PRS_PAGER env as UTF-8")] PagerEnvUtf8(#[source] std::env::VarError), #[error("failed to invoke pager from PRS_PAGER")] PagerSpawn(#[source] std::io::Error), #[error("failed to pipe secret contents to pager")] PagerPipe(#[source] std::io::Error), #[error("pager exited with non-zero status code: {0}")] PagerStatus(std::process::ExitStatus), } ���������������������������������������������������������������������������������������prs-v0.5.2/docs/������������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0013447�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/docs/connection-reuse.md�����������������������������������������������������������������0000664�0000000�0000000�00000003610�14713723046�0017251�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Connection reuse `prs` automatically tries to reuse SSH connections to greatly speed up operations in a password store that has sync enabled. Support for this is limited depending on the used operating system and remote host. A list of [requirements](#requirements) has been set up for this. Based on this, `prs` guesses whether connection reuse is supported and automatically enables it when that is the case. This feature uses the `ControlPersist` feature within OpenSSH. An open connection is stored in a file at `/tmp/.prs-session--*`. OpenSSH manages the connection and the file. If you're experiencing problems with this feature, please [disable](#how-to-disable) it and open an issue. This implementation is still limited and may be troublesome, it requires additional work. A better way to configure and determine whether connection reuse is supported should be implemented. Please see the following issues: - https://gitlab.com/timvisee/prs/-/issues/31 - https://github.com/timvisee/prs/issues/5 ## Requirements You must meet these requirements for connection reuse to be used: - Password store must have sync enabled (with `git`) - Password store must use SSH remote - Only supported on Unix platforms - Environment variable `GIT_SSH_COMMAND` must not be set - All password store git remotes that use SSH must have [whitelisted](#host-whitelist) domain ## Host whitelist The following hosts are whitelisted: ``` github.com gitlab.com ``` _To add a host to this whitelist, please contribute to this project or open an issue. See `SSH_PERSIST_HOST_WHITELIST` in [`lib/src/util/git.rs`](../lib/src/util/git.rs)._ ## How to disable There's no need to manually disable this if you don't meet the [requirements](#requirements). To disable this feature, you may set the `GIT_SSH_COMMAND` variable: ```bash # Use default ssh connection in git, disables prs connection reuse export GIT_SSH_COMMAND=ssh ``` ������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/gtk3/������������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0013367�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/gtk3/Cargo.toml��������������������������������������������������������������������������0000664�0000000�0000000�00000002537�14713723046�0015326�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[package] name = "prs-gtk3" version = "0.5.2" authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"] license = "GPL-3.0" readme = "../README.md" homepage = "https://timvisee.com/projects/prs" repository = "https://gitlab.com/timvisee/prs" description = "Secure, fast & convenient password manager CLI with GPG & git sync" keywords = ["pass", "passwordstore"] categories = [ "authentication", "command-line-utilities", "cryptography", ] edition = "2018" rust-version = "1.81.0" [features] default = ["backend-gnupg-bin", "notify", "tomb"] ### Regular features # Option (default): notification support (clipboard notifications) notify = ["notify-rust"] # Option (default): tomb support for password store on Linux tomb = ["prs-lib/tomb"] ### Pluggable cryptography backends # Option: GnuPG cryptography backend using GPGME backend-gpgme = ["prs-lib/backend-gpgme"] # Option (default): GnuPG cryptography backend using gpg binary backend-gnupg-bin = ["prs-lib/backend-gnupg-bin"] [dependencies] anyhow = "1.0" gdk = "0.18" gio = { version = "0.18", features = ["v2_72"] } glib = "0.18" gtk = { version = "0.18", features = ["v3_24"] } prs-lib = { version = "=0.5.2", path = "../lib", default-features = false } thiserror = "2.0" # Notification support notify-rust = { version = "4.6", optional = true } [[bin]] name = "prs-gtk3-copy" path = "./src/main.rs" �����������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/gtk3/src/��������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0014156�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/gtk3/src/main.rs�������������������������������������������������������������������������0000664�0000000�0000000�00000025246�14713723046�0015461�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::sync::{Arc, Mutex}; use gio::prelude::*; #[cfg(all(feature = "notify", not(target_env = "musl")))] use glib::clone; use glib::{ControlFlow, Propagation}; use gtk::prelude::*; #[cfg(all(feature = "notify", target_os = "linux", not(target_env = "musl")))] use notify_rust::Hint; #[cfg(all(feature = "notify", not(target_env = "musl")))] use notify_rust::Notification; use prs_lib::{ crypto::{self, prelude::*, Config, Proto}, store::FindSecret, Secret, Store, }; /// Application ID. const APP_ID: &str = "com.timvisee.prs.gtk3-copy"; /// Application name. #[cfg(all(feature = "notify", not(target_env = "musl")))] const APP_NAME: &str = "prs"; /// Application window title. const APP_TITLE: &str = "prs quick copy"; /// Default cryptography protocol. const PROTO: Proto = Proto::Gpg; /// Clipboard timeout in seconds. const CLIPBOARD_TIMEOUT: u32 = 20; fn main() { let application = gtk::Application::new(Some(APP_ID), Default::default()); application.connect_activate(|app| { build_ui(app); }); // When activated, shuts down the application let quit = gio::SimpleAction::new("quit", None); #[cfg(all(feature = "notify", not(target_env = "musl")))] quit.connect_activate(clone!(@weak application => move |_action, _parameter| { application.quit(); })); application.set_accels_for_action("app.quit", &["Escape"]); application.add_action(&quit); // Run the application application.run(); } /// Wraps a store secret. struct Data { secret: Secret, } impl Data { fn name(&self) -> &str { &self.secret.name } } impl From<Secret> for Data { fn from(secret: Secret) -> Self { Data { secret } } } /// Create GTK list model for given secrets. fn create_list_model(secrets: Vec<Secret>) -> gtk::ListStore { let data: Vec<Data> = secrets.into_iter().map(|s| s.into()).collect(); let col_types: [glib::Type; 1] = [glib::Type::STRING]; let store = gtk::ListStore::new(&col_types); for d in data.iter() { let values: [(u32, &dyn ToValue); 1] = [(0, &d.name())]; store.set(&store.append(), &values); } store } fn build_ui(application: &gtk::Application) { // Load store let store = match Store::open(prs_lib::STORE_DEFAULT_ROOT) { Ok(store) => store, Err(err) => { error_dialog( &format!("Failed to load password store.\n\nError: {err}"), None, ); application.quit(); return; } }; #[cfg(all(feature = "tomb", target_os = "linux"))] let tomb = store.tomb(false, false, true); // Prepare tomb #[cfg(all(feature = "tomb", target_os = "linux"))] if let Err(err) = tomb.prepare() { eprintln!("{err}"); error_dialog("Failed to prepare password store tomb", None); application.quit(); return; } // Find secrets let secrets = store.secrets(None); // Quit if user has no secrets if secrets.is_empty() { error_dialog("Your password store does not have any secrets.", None); application.quit(); return; } // Create the main window let window = gtk::ApplicationWindow::new(application); window.set_title(APP_TITLE); window.set_border_width(5); window.set_position(gtk::WindowPosition::Center); window.set_keep_above(true); window.set_urgency_hint(true); window.set_type_hint(gdk::WindowTypeHint::Dialog); window.stick(); // Create an EntryCompletion widget let completion = gtk::EntryCompletion::new(); completion.set_text_column(0); completion.set_minimum_key_length(1); completion.set_popup_completion(true); completion.set_inline_completion(true); completion.set_inline_selection(true); completion.set_match_func(|completion, query, iter| { model_item_text(&completion.model().unwrap(), iter) .map(|text| text.contains(query)) .unwrap_or(false) }); let ls = create_list_model(secrets); completion.set_model(Some(&ls)); let input_field = gtk::SearchEntry::new(); input_field.set_completion(Some(&completion)); input_field.set_width_chars(40); input_field.set_placeholder_text(Some("Search for a secret...")); input_field.set_input_hints(gtk::InputHints::NO_SPELLCHECK); // Action handlers to copy selected secret let input_field_signal = input_field.clone(); completion.connect_match_selected(move |_self, _model, _iter| { input_field_signal.emit_activate(); Propagation::Stop }); let window_ref = window.clone(); let input_ref = input_field.clone(); input_field.connect_activate(move |entry| { selected_entry( store.clone(), entry.text().into(), window_ref.clone(), input_ref.clone(), ); }); window.add(&input_field); // show everything window.show_all(); window.grab_focus(); // TODO: finalize store tomb somewhere } /// Called when we've selected a secret in the input field. /// /// Shows an error if it doesn't resolve to exactly one. fn selected_entry( store: Store, query: String, window: gtk::ApplicationWindow, input: gtk::SearchEntry, ) { // Show error for empty query if query.trim().is_empty() { error_dialog("Please enter the name of a secret to copy.", Some(&window)); return; } let secret = match store.find(Some(query)) { FindSecret::Exact(secret) => secret, FindSecret::Many(secrets) if secrets.len() == 1 => secrets[0].clone(), FindSecret::Many(secrets) if secrets.is_empty() => { error_dialog( "Found no secrets for this query. Please name a specific secret.", Some(&window), ); return; } FindSecret::Many(secrets) => { error_dialog( &format!( "Found {} secrets for this query. Please refine your query.", secrets.len() ), Some(&window), ); return; } }; selected(secret, window, input); } /// Called when we've selected a secret. /// /// Copies to clipboard with revert timeout. fn selected(secret: Secret, window: gtk::ApplicationWindow, input: gtk::SearchEntry) { // Decrypt first line of plaintext let config = Config::from(PROTO); let plaintext = match crypto::context(&config) .map_err(|err| err.into()) .and_then(|mut context| context.decrypt_file(&secret.path)) .and_then(|plaintext| plaintext.first_line()) { Ok(plaintext) => plaintext, Err(err) => { error_dialog( &format!("Failed to decrypt first line of secret.\n\nError: {err}"), Some(&window), ); window.close(); return; } }; let text = plaintext.unsecure_to_str().unwrap(); // Copy with revert timeout copy(text.to_string(), CLIPBOARD_TIMEOUT); // Move to back, disable input window.set_keep_above(false); window.set_sensitive(false); window.set_deletable(false); window.unstick(); input.set_text(""); input.set_placeholder_text(Some(&format!( "Copied, clearing in {CLIPBOARD_TIMEOUT} seconds...", ))); // Hack to unfocus and move window to back window.set_accept_focus(false); window.set_focus(None::<&gtk::Widget>); if let Some(window) = window.window() { window.hide(); window.show_unraised(); window.lower(); } // Close window after clipboard revert // TODO: wait for clipboard revert instead, do not use own timeout glib::timeout_add_seconds_local(CLIPBOARD_TIMEOUT + 1, move || { window.close(); ControlFlow::Continue }); } /// Copy given text to clipboard with revert timeout. fn copy(text: String, timeout: u32) { // Get clipboard context let clipboard = gtk::Clipboard::get(&gdk::SELECTION_CLIPBOARD); // Obtain previous clipboard contents let previous = Arc::new(Mutex::new(None)); let previous_clone = previous.clone(); clipboard.request_text(move |_clipboard, text| { if let Ok(mut previous) = previous_clone.lock() { *previous = text.map(|t| t.to_string()); } }); clipboard.set_text(&text); // Wait for timeout, then revert clipboard glib::timeout_add_seconds_local(timeout, move || { let previous = previous.clone(); let text = text.clone(); // Obtain current clipboard contents, change to previous if secret is still in it clipboard.request_text(move |clipboard, current| { if current != Some(&text) { return; } // Set to previous if secret is still in if let Ok(previous) = previous.lock() { if let Some(ref previous) = *previous { clipboard.set_text(previous); notify_cleared(); return; } } // Fallback clipboard.set_text(""); notify_cleared(); }); ControlFlow::Continue }); } /// Show notification to user about cleared clipboard. fn notify_cleared() { // Do not show notification with not notify or on musl due to segfault #[cfg(all(feature = "notify", not(target_env = "musl")))] { let mut n = Notification::new(); n.appname(APP_NAME) .summary(&format!("Clipboard cleared - {APP_NAME}")) .body("Secret cleared from clipboard") .auto_icon() .icon("lock") .timeout(3000); #[cfg(target_os = "linux")] n.urgency(notify_rust::Urgency::Low) .hint(Hint::Category("presence.offline".into())); let _ = n.show(); return; } // Fallback if we cannot notify #[cfg_attr( all(feature = "notify", not(target_env = "musl")), expect(unreachable_code) )] { eprintln!("Secret cleared from clipboard"); } } /// Show an error dialog. fn error_dialog(msg: &str, window: Option<&gtk::ApplicationWindow>) { let dialog = gtk::MessageDialog::new( window, gtk::DialogFlags::MODAL, gtk::MessageType::Error, gtk::ButtonsType::Close, msg, ); dialog.connect_response(|dialog, _response| dialog.close()); dialog.run(); } /// Get the text for a tree model item by iterator. fn model_item_text(model: &gtk::TreeModel, iter: &gtk::TreeIter) -> Option<String> { let item = model.value(iter, 0); // Get item text let text: Result<Option<String>, _> = item.get(); match text { Ok(Some(text)) => Some(text), _ => None, } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/�������������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0013265�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/Cargo.toml���������������������������������������������������������������������������0000664�0000000�0000000�00000003432�14713723046�0015217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[package] name = "prs-lib" version = "0.5.2" authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"] license = "LGPL-3.0" readme = "../README.md" homepage = "https://timvisee.com/projects/prs" repository = "https://gitlab.com/timvisee/prs" description = "Secure, fast & convenient password manager CLI with GPG & git sync" keywords = ["pass", "passwordstore"] categories = [ "authentication", "command-line-utilities", "cryptography", ] edition = "2018" rust-version = "1.81.0" [features] default = ["backend-gnupg-bin"] ### Regular features # Option: tomb support for password store on Linux tomb = ["fs_extra"] ### Pluggable cryptography backends # Option: GnuPG cryptography backend using GPGME backend-gpgme = ["gpgme"] # Option (default): GnuPG cryptography backend using gpg binary backend-gnupg-bin = ["regex", "shlex", "version-compare"] ### Private/internal/automatic features # GnuPG (gpg) crypto support _crypto-gpg = [] [dependencies] anyhow = "1.0" git-state = "0.1" lazy_static = "1.4" secstr = "0.5" shellexpand = "3.0" thiserror = "2.0" walkdir = "2.3" which = "7.0" zeroize = "1.5" # Tomb support fs_extra = { version = "1.2", optional = true } # Crypto backend: GPGME gpgme = { version = "0.11", optional = true } # Crypto backend: gnupg binary regex = { version = "1.7", optional = true, default-features = false, features = ["std", "unicode-perl"] } shlex = { version = "1.3", optional = true } version-compare = { version = "0.2", optional = true } [target.'cfg(unix)'.dependencies] nix = { version = "0.29", default-features = false, features = ["user", "signal"] } ofiles = "0.2" [dev-dependencies] quickcheck = { version = "1.0", default-features = false } quickcheck_macros = "1.0" [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/LICENSE������������������������������������������������������������������������������0000664�0000000�0000000�00000016744�14713723046�0014306�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ����������������������������prs-v0.5.2/lib/build.rs�����������������������������������������������������������������������������0000664�0000000�0000000�00000000634�14713723046�0014735�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������fn main() { // Crypto features warning #[cfg(not(any(feature = "backend-gnupg-bin", feature = "backend-gpgme")))] { compile_error!("no crypto backend selected, must set any of these features: backend-gnupg-bin, backend-gpgme"); } // GPG cryptography #[cfg(any(feature = "backend-gpgme", feature = "backend-gnupg-bin"))] println!("cargo:rustc-cfg=feature=\"_crypto-gpg\""); } ����������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/���������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0014054�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0015374�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0016763�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gnupg_bin/��������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0020733�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gnupg_bin/context.rs����������������������������������������������0000664�0000000�0000000�00000010034�14713723046�0022763�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Provides GnuPG binary context adapter. use anyhow::Result; use thiserror::Error; use version_compare::Version; use super::raw_cmd::gpg_stdout_ok; use super::{raw, Config}; use crate::crypto::{proto, Config as CryptoConfig, IsContext, Key, Proto}; use crate::{Ciphertext, Plaintext, Recipients}; /// Binary name. #[cfg(not(windows))] const BIN_NAME: &str = "gpg"; #[cfg(windows)] const BIN_NAME: &str = "gpg.exe"; /// Minimum required version. const VERSION_MIN: &str = "2.0.0"; /// Create GnuPG binary context. pub fn context(config: &CryptoConfig) -> Result<Context, Err> { let mut gpg_config = find_gpg_bin().map_err(Err::Context)?; gpg_config.gpg_tty = config.gpg_tty; gpg_config.verbose = config.verbose; Ok(Context::from(gpg_config)) } /// GnuPG binary context. pub struct Context { /// GPG config. config: Config, } impl Context { /// Construct context from GPG config. fn from(config: Config) -> Self { Self { config } } } impl IsContext for Context { fn encrypt(&mut self, recipients: &Recipients, plaintext: Plaintext) -> Result<Ciphertext> { let fingerprints: Vec<String> = recipients .keys() .iter() .map(|key| key.fingerprint(false)) .collect(); let fingerprints: Vec<&str> = fingerprints.iter().map(|fp| fp.as_str()).collect(); raw::encrypt(&self.config, &fingerprints, plaintext) } fn decrypt(&mut self, ciphertext: Ciphertext) -> Result<Plaintext> { raw::decrypt(&self.config, ciphertext) } fn can_decrypt(&mut self, ciphertext: Ciphertext) -> Result<bool> { raw::can_decrypt(&self.config, ciphertext) } fn keys_public(&mut self) -> Result<Vec<Key>> { Ok(raw::public_keys(&self.config)? .into_iter() .map(|key| { Key::Gpg(proto::gpg::Key { fingerprint: key.0, user_ids: key.1, }) }) .collect()) } fn keys_private(&mut self) -> Result<Vec<Key>> { Ok(raw::private_keys(&self.config)? .into_iter() .map(|key| { Key::Gpg(proto::gpg::Key { fingerprint: key.0, user_ids: key.1, }) }) .collect()) } fn import_key(&mut self, key: &[u8]) -> Result<()> { raw::import_key(&self.config, key) } fn export_key(&mut self, key: Key) -> Result<Vec<u8>> { raw::export_key(&self.config, &key.fingerprint(false)) } fn supports_proto(&self, proto: Proto) -> bool { proto == Proto::Gpg } } /// Find the `gpg` binary, make GPG config. // TODO: also try default path at /usr/bin/gpg fn find_gpg_bin() -> Result<Config> { let path = which::which(BIN_NAME).map_err(Err::Unavailable)?; let config = Config::from(path); test_gpg_compat(&config)?; Ok(config) } /// Test gpg binary compatibility. fn test_gpg_compat(config: &Config) -> Result<()> { // Strip stdout to just the version number let stdout = gpg_stdout_ok(config, ["--version"])?; let stdout = stdout .trim_start() .lines() .next() .and_then(|stdout| stdout.trim().strip_prefix("gpg (GnuPG) ")) .map(|stdout| stdout.trim()) .ok_or(Err::UnexpectedOutput)?; // Assert minimum version number let ver_min = Version::from(VERSION_MIN).unwrap(); let ver_gpg = Version::from(stdout).unwrap(); if ver_gpg < ver_min { return Err(Err::UnsupportedVersion(ver_gpg.to_string()).into()); } Ok(()) } /// GnuPG binary context error. #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain GnuPG binary cryptography context")] Context(#[source] anyhow::Error), #[error("failed to find GnuPG gpg binary")] Unavailable(#[source] which::Error), #[error("failed to communicate with GnuPG gpg binary, got unexpected output")] UnexpectedOutput, #[error("failed to use GnuPG gpg binary, unsupported version: {}", _0)] UnsupportedVersion(String), } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gnupg_bin/mod.rs��������������������������������������������������0000664�0000000�0000000�00000001113�14713723046�0022054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto backend using GnuPG for GPG. pub mod context; pub mod raw; mod raw_cmd; use std::path::PathBuf; /// GPG config. pub struct Config { /// GPG binary. bin: PathBuf, /// Use TTY for GPG password input, rather than GUI pinentry. pub gpg_tty: bool, /// Whether to show verbose output. pub verbose: bool, } impl Config { /// Construct with given binary. /// /// - `config`: path to `gpg` binary pub fn from(bin: PathBuf) -> Self { Self { bin, gpg_tty: false, verbose: false, } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gnupg_bin/raw.rs��������������������������������������������������0000664�0000000�0000000�00000017141�14713723046�0022076�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Raw interface to GPG binary. //! //! This provides the most basic and bare functions to interface with a GnuPG backend binary. use std::collections::VecDeque; use anyhow::Result; use regex::Regex; use thiserror::Error; use super::raw_cmd::{gpg_stdin_output, gpg_stdin_stdout_ok_bin, gpg_stdout_ok, gpg_stdout_ok_bin}; use super::Config; use crate::crypto::util; use crate::{Ciphertext, Plaintext}; /// Partial output from gpg if the user does not own the secret key. const GPG_OUTPUT_ERR_NO_SECKEY: &str = "decryption failed: No secret key"; /// Encrypt plaintext for the given recipients. /// /// - `config`: GPG config /// - `recipients`: list of recipient fingerprints to encrypt for /// - `plaintext`: plaintext to encrypt /// /// # Panics /// /// Panics if list of recipients is empty. pub fn encrypt(config: &Config, recipients: &[&str], plaintext: Plaintext) -> Result<Ciphertext> { assert!( !recipients.is_empty(), "attempting to encrypt secret for empty list of recipients" ); // Build argument list let mut args = vec!["--quiet", "--openpgp", "--trust-model", "always"]; for fp in recipients { args.push("--recipient"); args.push(fp); } args.push("--encrypt"); Ok(Ciphertext::from( gpg_stdin_stdout_ok_bin(config, args.as_slice(), plaintext.unsecure_ref()) .map_err(Err::Decrypt)?, )) } /// Decrypt ciphertext. /// /// - `config`: GPG config /// - `ciphertext`: ciphertext to decrypt pub fn decrypt(config: &Config, ciphertext: Ciphertext) -> Result<Plaintext> { // TODO: ensure ciphertext ends with PGP footer Ok(Plaintext::from( gpg_stdin_stdout_ok_bin(config, ["--quiet", "--decrypt"], ciphertext.unsecure_ref()) .map_err(Err::Decrypt)?, )) } /// Check whether we can decrypt ciphertext. /// /// This checks whether whether we own the secret key to decrypt the given ciphertext. /// /// - `config`: GPG config /// - `ciphertext`: ciphertext to check // To check this, actual decryption is attempted, see this if this can be improved: // https://stackoverflow.com/q/64633736/1000145 pub fn can_decrypt(config: &Config, ciphertext: Ciphertext) -> Result<bool> { // TODO: ensure ciphertext ends with PGP footer let output = gpg_stdin_output(config, ["--quiet", "--decrypt"], ciphertext.unsecure_ref()) .map_err(Err::Decrypt)?; match output.status.code() { Some(0) | None => Ok(true), Some(2) => Ok(!std::str::from_utf8(&output.stdout)?.contains(GPG_OUTPUT_ERR_NO_SECKEY)), Some(_) => Ok(true), } } /// Get all public keys from keychain. /// /// - `config`: GPG config pub fn public_keys(config: &Config) -> Result<Vec<KeyId>> { let list = gpg_stdout_ok(config, ["--list-keys", "--keyid-format", "LONG"]).map_err(Err::Keys)?; parse_key_list(list).ok_or_else(|| Err::UnexpectedOutput.into()) } /// Get all private/secret keys from keychain. /// /// - `config`: GPG config pub fn private_keys(config: &Config) -> Result<Vec<KeyId>> { let list = gpg_stdout_ok(config, ["--list-secret-keys", "--keyid-format", "LONG"]) .map_err(Err::Keys)?; parse_key_list(list).ok_or_else(|| Err::UnexpectedOutput.into()) } /// Import given key from bytes into keychain. /// /// - `config`: GPG config /// /// # Panics /// /// Panics if the provides key does not look like a public key. pub fn import_key(config: &Config, key: &[u8]) -> Result<()> { // Assert we're importing a public key let key_str = std::str::from_utf8(key).expect("exported key is invalid UTF-8"); assert!( !key_str.contains("PRIVATE KEY"), "imported key contains PRIVATE KEY, blocked to prevent accidentally leaked secret key" ); assert!( key_str.contains("PUBLIC KEY"), "imported key must contain PUBLIC KEY, blocked to prevent accidentally leaked secret key" ); // Import key with gpg command gpg_stdin_stdout_ok_bin(config, ["--quiet", "--import"], key) .map(|_| ()) .map_err(|err| Err::Import(err).into()) } /// Export the given key as bytes. /// /// # Panics /// /// Panics if the received key does not look like a public key. This should never happen unless the /// gpg binary backend is broken. pub fn export_key(config: &Config, fingerprint: &str) -> Result<Vec<u8>> { // Export key with gpg command let data = gpg_stdout_ok_bin(config, ["--quiet", "--armor", "--export", fingerprint]) .map_err(Err::Export)?; // Assert we're exporting a public key let data_str = std::str::from_utf8(&data).expect("exported key is invalid UTF-8"); assert!( !data_str.contains("PRIVATE KEY"), "exported key contains PRIVATE KEY, blocked to prevent accidentally leaking secret key" ); assert!( data_str.contains("PUBLIC KEY"), "exported key must contain PUBLIC KEY, blocked to prevent accidentally leaking secret key" ); Ok(data) } /// A key identifier with a fingerprint and user IDs. #[derive(Clone)] pub struct KeyId(pub String, pub Vec<String>); /// Parse key list output from gnupg. // TODO: throw proper errors on parse failure fn parse_key_list(list: String) -> Option<Vec<KeyId>> { // Return empty list if there's no key loaded if list.trim().is_empty() { return Some(vec![]); } let mut lines: VecDeque<_> = list.lines().collect(); // Second line must be a line lines.pop_front()?; if lines .pop_front()? .bytes() .filter(|&b| b != b'-') .take(1) .count() > 0 { return None; } let re_fingerprint = Regex::new(r"^[0-9A-F]{16,}$").unwrap(); let re_user_id = Regex::new(r"^uid\s*\[[a-z ]+\]\s*(.*)$").unwrap(); // Walk through the list, collect list of keys let mut keys = Vec::new(); while !lines.is_empty() { match lines.pop_front()? { // Start reading a new key l if l.starts_with("pub ") || l.starts_with("sec ") => { // Get the fingerprint let fingerprint = util::format_fingerprint(lines.pop_front()?.trim()); if !re_fingerprint.is_match(&fingerprint) { return None; } // Find and parse user IDs let mut user_ids = Vec::new(); while !lines.is_empty() { match lines.pop_front()? { // Read user ID l if l.starts_with("uid ") => { let captures = re_user_id.captures(l)?; user_ids.push(captures[1].to_string()); } // Finalize on empty line l if l.trim().is_empty() => break, _ => {} } } // Add read key to list keys.push(KeyId(fingerprint, user_ids)); } // Ignore empty lines l if l.trim().is_empty() => {} // Got something unexpected _ => return None, } } Some(keys) } /// GnuPG binary error. #[derive(Debug, Error)] pub enum Err { #[error("failed to communicate with gpg binary, got unexpected output")] UnexpectedOutput, #[error("failed to encrypt plaintext")] Encrypt(#[source] anyhow::Error), #[error("failed to decrypt ciphertext")] Decrypt(#[source] anyhow::Error), #[error("failed to obtain keys from gpg keychain")] Keys(#[source] anyhow::Error), #[error("failed to import key into gpg keychain")] Import(#[source] anyhow::Error), #[error("failed to export key from gpg keychain")] Export(#[source] anyhow::Error), } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gnupg_bin/raw_cmd.rs����������������������������������������������0000664�0000000�0000000�00000015317�14713723046�0022724�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Command helpers for raw interface. use std::char::decode_utf16; use std::ffi::OsStr; use std::io::{self, Write}; use std::process::{Command, Output, Stdio}; use anyhow::Result; use thiserror::Error; use super::Config; use crate::util; /// Invoke a gpg command, returns output. pub(super) fn gpg_output<I, S>(config: &Config, args: I) -> Result<Output> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { cmd_gpg(config, args) .output() .map_err(|err| Err::System(err).into()) } /// Invoke a gpg command, returns output. pub(super) fn gpg_stdin_output<I, S>(config: &Config, args: I, stdin: &[u8]) -> Result<Output> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let mut cmd = cmd_gpg(config, args); // Pass stdin to child process let mut child = cmd.spawn().unwrap(); if let Err(err) = child.stdin.as_mut().unwrap().write_all(stdin) { return Err(Err::System(err).into()); } child .wait_with_output() .map_err(|err| Err::System(err).into()) } /// Invoke a gpg command with the given arguments, return stdout on success. pub(super) fn gpg_stdout_ok_bin<I, S>(config: &Config, args: I) -> Result<Vec<u8>> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let output = gpg_output(config, args)?; cmd_assert_status(config, &output)?; Ok(output.stdout) } /// Invoke a gpg command with the given arguments, return stdout on success. pub(super) fn gpg_stdout_ok<I, S>(config: &Config, args: I) -> Result<String> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { Ok(parse_output(&gpg_stdout_ok_bin(config, args)?) .map_err(|err| Err::GpgCli(err.into()))? .trim() .into()) } /// Invoke a gpg command with the given arguments, return stdout on success. pub(super) fn gpg_stdin_stdout_ok_bin<I, S>( config: &Config, args: I, stdin: &[u8], ) -> Result<Vec<u8>> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let output = gpg_stdin_output(config, args, stdin)?; cmd_assert_status(config, &output)?; Ok(output.stdout) } /// Build a gpg command to run. fn cmd_gpg<I, S>(config: &Config, args: I) -> Command where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { // TODO: select proper locale here, must be available on system // TODO: see: https://linuxconfig.org/how-to-list-all-available-locales-on-rhel7-linux let mut cmd = Command::new(&config.bin); cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env("LANG", "en_US.UTF-8") .env("LANGUAGE", "en_US.UTF-8"); if config.gpg_tty { cmd.arg("--pinentry-mode").arg("loopback"); if !util::env::has_gpg_tty() { if let Some(tty) = util::tty::get_tty() { cmd.env("GPG_TTY", tty); } } } cmd.args(args); if config.verbose { log_cmd(&cmd); } cmd } /// Assert the exit status of a command based on its output. /// /// On error, this prints stdout/stderr output in verbose mode. /// /// Returns error is status is not succesful. fn cmd_assert_status(config: &Config, output: &Output) -> Result<()> { if !output.status.success() { // Output stdout/stderr in verbose mode if config.verbose { if !output.stdout.is_empty() { let mut stdout = io::stdout(); eprintln!("= gnupg stdout: ================"); stdout .write_all(&output.stdout) .expect("failed to print gnupg stdout"); let _ = stdout.flush(); eprintln!("================================"); } if !output.stderr.is_empty() { let mut stderr = io::stderr(); eprintln!("= gnupg stderr: ================"); stderr .write_all(&output.stderr) .expect("failed to print gnupg stderr"); let _ = stderr.flush(); eprintln!("================================"); } } return Err(Err::Status(output.status).into()); } Ok(()) } /// Log the command to stderr. // TODO: output working directory as well // TODO: show stdin given to command fn log_cmd(cmd: &Command) { let mut sh_cmd: Vec<String> = vec![]; // Add environment variables sh_cmd.extend(cmd.get_envs().map(|(k, v)| { format!( "{}={}", shlex::try_quote(k.to_str().expect("gpg env key is not valid UTF-8")) .expect("failed to quite gpg env key"), shlex::try_quote( v.map(|v| v.to_str().expect("gpg env value is not valid UTF-8")) .unwrap_or("") ) .expect("failed to quite gpg env value"), ) })); // Add binary name sh_cmd.push( shlex::try_quote( cmd.get_program() .to_str() .expect("gpg command binary is not valid UTF-8"), ) .expect("failed to quote gpg command binary") .into(), ); // Add program arguments sh_cmd.extend(cmd.get_args().map(|a| { shlex::try_quote(a.to_str().expect("gpg argument is not valid UTF-8")) .expect("failed to quote gpg command argument") .into() })); // Join invoked command into single string, and print let sh_cmd = sh_cmd .into_iter() .filter(|a| !a.is_empty()) .collect::<Vec<_>>() .join(" "); eprintln!("$ {sh_cmd}"); } /// Try to parse command output bytes as text. /// /// Command output formatting might not always be consistent. This function tries to parse both as /// UTF-8 and UTF-16. fn parse_output(bytes: &[u8]) -> Result<String, std::str::Utf8Error> { // Try to parse as UTF-8, remember error on failure let err = match std::str::from_utf8(bytes) { Ok(s) => return Ok(s.into()), Err(err) => err, }; // Try to parse as UTF-16 if let Some(s) = u8_as_utf16(bytes) { return Ok(s); } Err(err) } /// Try to parse u8 slice as UTF-16 string. fn u8_as_utf16(bytes: &[u8]) -> Option<String> { // Bytes must be multiple of 2 if bytes.len() % 2 != 0 { return None; } // Decode UTF-16 chars one by one and build a string let iter = (0..bytes.len() / 2).map(|i| u16::from_be_bytes([bytes[2 * i], bytes[2 * i + 1]])); decode_utf16(iter).collect::<Result<_, _>>().ok() } #[derive(Debug, Error)] pub enum Err { #[error("failed to complete gpg operation")] GpgCli(#[source] anyhow::Error), #[error("failed to invoke gpg command")] System(#[source] std::io::Error), #[error("gpg command exited with non-zero status code: {0}")] Status(std::process::ExitStatus), } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gpgme/������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0020062�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gpgme/context.rs��������������������������������������������������0000664�0000000�0000000�00000005752�14713723046�0022125�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Provides GPGME binary context adapter. use std::env; use anyhow::Result; use gpgme::{Context as GpgmeContext, PinentryMode, Protocol}; use thiserror::Error; use super::raw; use crate::crypto::{proto, Config, IsContext, Key, Proto}; use crate::{util, Ciphertext, Plaintext, Recipients}; /// Protocol to use. const PROTO: Protocol = Protocol::OpenPgp; /// Create GPGME crypto context. pub fn context(config: &Config) -> Result<Context, Err> { // Set environment when using GPG TTY if config.gpg_tty && !util::env::has_gpg_tty() { if let Some(tty) = util::tty::get_tty() { env::set_var("GPG_TTY", tty); } } let mut context = gpgme::Context::from_protocol(PROTO).map_err(Err::Context)?; // Set pinentry mode when using GPG TTY if config.gpg_tty { context .set_pinentry_mode(PinentryMode::Loopback) .map_err(Err::Context)?; } Ok(Context::from(context)) } /// GPGME crypto context. pub struct Context { /// GPGME crytp context. context: GpgmeContext, } impl Context { pub fn from(context: GpgmeContext) -> Self { Self { context } } } impl IsContext for Context { fn encrypt(&mut self, recipients: &Recipients, plaintext: Plaintext) -> Result<Ciphertext> { let fingerprints: Vec<String> = recipients .keys() .iter() .map(|key| key.fingerprint(false)) .collect(); let fingerprints: Vec<&str> = fingerprints.iter().map(|fp| fp.as_str()).collect(); raw::encrypt(&mut self.context, &fingerprints, plaintext) } fn decrypt(&mut self, ciphertext: Ciphertext) -> Result<Plaintext> { raw::decrypt(&mut self.context, ciphertext) } fn can_decrypt(&mut self, ciphertext: Ciphertext) -> Result<bool> { raw::can_decrypt(&mut self.context, ciphertext) } fn keys_public(&mut self) -> Result<Vec<Key>> { Ok(raw::public_keys(&mut self.context)? .into_iter() .map(|key| { Key::Gpg(proto::gpg::Key { fingerprint: key.0, user_ids: key.1, }) }) .collect()) } fn keys_private(&mut self) -> Result<Vec<Key>> { Ok(raw::private_keys(&mut self.context)? .into_iter() .map(|key| { Key::Gpg(proto::gpg::Key { fingerprint: key.0, user_ids: key.1, }) }) .collect()) } fn import_key(&mut self, key: &[u8]) -> Result<()> { raw::import_key(&mut self.context, key) } fn export_key(&mut self, key: Key) -> Result<Vec<u8>> { raw::export_key(&mut self.context, &key.fingerprint(false)) } fn supports_proto(&self, proto: Proto) -> bool { proto == Proto::Gpg } } /// GPGME context error. #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain GPGME cryptography context")] Context(#[source] gpgme::Error), } ����������������������prs-v0.5.2/lib/src/crypto/backend/gpgme/mod.rs������������������������������������������������������0000664�0000000�0000000�00000000107�14713723046�0021205�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto backend using GPGME for GPG. pub mod context; pub mod raw; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/backend/gpgme/raw.rs������������������������������������������������������0000664�0000000�0000000�00000015765�14713723046�0021237�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Raw interface to GPGME. //! //! This provides the most basic and bare functions to interface with the GPGME backend. use anyhow::Result; use gpgme::{Context, EncryptFlags, Key}; use thiserror::Error; use zeroize::Zeroize; use crate::{Ciphertext, Plaintext}; /// GPGME encryption flags. const ENCRYPT_FLAGS: EncryptFlags = EncryptFlags::ALWAYS_TRUST; /// Encrypt plaintext for the given recipients. /// /// - `context`: GPGME context /// - `recipients`: list of recipient fingerprints to encrypt for /// - `plaintext`: plaintext to encrypt /// /// # Panics /// /// Panics if list of recipients is empty. pub fn encrypt( context: &mut Context, recipients: &[&str], plaintext: Plaintext, ) -> Result<Ciphertext> { assert!( !recipients.is_empty(), "attempting to encrypt secret for empty list of recipients" ); let mut ciphertext = vec![]; let keys = fingerprints_to_keys(context, recipients)?; context .encrypt_with_flags( keys.iter(), plaintext.unsecure_ref(), &mut ciphertext, ENCRYPT_FLAGS, ) .map_err(Err::Encrypt)?; Ok(Ciphertext::from(ciphertext)) } /// Decrypt ciphertext. /// /// - `context`: GPGME context /// - `ciphertext`: ciphertext to decrypt pub fn decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<Plaintext> { let mut plaintext = vec![]; context .decrypt(ciphertext.unsecure_ref(), &mut plaintext) .map_err(Err::Decrypt)?; Ok(Plaintext::from(plaintext)) } /// Check whether we can decrypt ciphertext. /// /// This checks whether whether we own the secret key to decrypt the given ciphertext. /// Assumes `true` if GPGME returns an error different than `NO_SECKEY`. /// /// - `context`: GPGME context /// - `ciphertext`: ciphertext to check // To check this, actual decryption is attempted, see this if this can be improved: // https://stackoverflow.com/q/64633736/1000145 pub fn can_decrypt(context: &mut Context, ciphertext: Ciphertext) -> Result<bool> { // Try to decrypt, explicit zeroing of unsecure buffer required let mut plaintext = vec![]; let result = context.decrypt(ciphertext.unsecure_ref(), &mut plaintext); plaintext.zeroize(); match result { Ok(_) => Ok(true), Err(err) if gpgme::error::Error::NO_SECKEY.code() == err.code() => Ok(false), Err(_) => Ok(true), } } /// Get all public keys from keychain. /// /// - `context`: GPGME context pub fn public_keys(context: &mut Context) -> Result<Vec<KeyId>> { Ok(context .keys()? .filter_map(|k| k.ok()) .filter(|k| k.can_encrypt()) .map(|k| k.into()) .collect()) } /// Get all private/secret keys from keychain. /// /// - `context`: GPGME context pub fn private_keys(context: &mut Context) -> Result<Vec<KeyId>> { Ok(context .secret_keys()? .filter_map(|k| k.ok()) .filter(|k| k.can_encrypt()) .map(|k| k.into()) .collect()) } /// Import given key from bytes into keychain. /// /// - `context`: GPGME context /// /// # Panics /// /// Panics if the provides key does not look like a public key. pub fn import_key(context: &mut Context, key: &[u8]) -> Result<()> { // Assert we're importing a public key let key_str = std::str::from_utf8(key).expect("exported key is invalid UTF-8"); assert!( !key_str.contains("PRIVATE KEY"), "imported key contains PRIVATE KEY, blocked to prevent accidentally leaked secret key" ); assert!( key_str.contains("PUBLIC KEY"), "imported key must contain PUBLIC KEY, blocked to prevent accidentally leaked secret key" ); // Import the key context .import(key) .map(|_| ()) .map_err(|err| Err::Import(err.into()).into()) } /// Export the given key as bytes. /// /// # Panics /// /// Panics if the received key does not look like a public key. This should never happen unless the /// gpg binary backend is broken. pub fn export_key(context: &mut Context, fingerprint: &str) -> Result<Vec<u8>> { // Find the GPGME key to export let key = context .get_key(fingerprint) .map_err(|err| Err::Export(Err::UnknownFingerprint(err).into()))?; // Export key to memoy with armor enabled let mut data: Vec<u8> = vec![]; let armor = context.armor(); context.set_armor(true); context.export_keys(&[key], gpgme::ExportMode::empty(), &mut data)?; context.set_armor(armor); // Assert we're exporting a public key let data_str = std::str::from_utf8(&data).expect("exported key is invalid UTF-8"); assert!( !data_str.contains("PRIVATE KEY"), "exported key contains PRIVATE KEY, blocked to prevent accidentally leaking secret key" ); assert!( data_str.contains("PUBLIC KEY"), "exported key must contain PUBLIC KEY, blocked to prevent accidentally leaking secret key" ); Ok(data) } /// A key identifier with a fingerprint and user IDs. #[derive(Clone)] pub struct KeyId(pub String, pub Vec<String>); impl From<Key> for KeyId { fn from(key: Key) -> Self { Self( key.fingerprint() .expect("GPGME key does not have fingerprint") .to_string(), key.user_ids() .map(|user| { let mut parts = vec![]; if let Ok(name) = user.name() { if !name.trim().is_empty() { parts.push(name.into()); } } if let Ok(comment) = user.comment() { if !comment.trim().is_empty() { parts.push(format!("({comment})")); } } if let Ok(email) = user.email() { if !email.trim().is_empty() { parts.push(format!("<{email}>")); } } parts.join(" ") }) .collect(), ) } } /// Transform fingerprints into GPGME keys. /// /// Errors if a fingerprint does not match a public key. fn fingerprints_to_keys(context: &mut Context, fingerprints: &[&str]) -> Result<Vec<Key>> { let mut keys = vec![]; for fp in fingerprints { keys.push( context .get_key(fp.to_owned()) .map_err(Err::UnknownFingerprint)?, ); } Ok(keys) } /// GnuPG binary error. #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain GPGME cryptography context")] Context(#[source] gpgme::Error), #[error("failed to encrypt plaintext")] Encrypt(#[source] gpgme::Error), #[error("failed to decrypt ciphertext")] Decrypt(#[source] gpgme::Error), #[error("failed to import key")] Import(#[source] anyhow::Error), #[error("failed to export key")] Export(#[source] anyhow::Error), #[error("fingerprint does not match public key in keychain")] UnknownFingerprint(#[source] gpgme::Error), } �����������prs-v0.5.2/lib/src/crypto/backend/mod.rs������������������������������������������������������������0000664�0000000�0000000�00000000277�14713723046�0020116�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto backends. //! //! This module groups all crytpo backend implementations. #[cfg(feature = "backend-gnupg-bin")] pub mod gnupg_bin; #[cfg(feature = "backend-gpgme")] pub mod gpgme; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/mod.rs��������������������������������������������������������������������0000664�0000000�0000000�00000022047�14713723046�0016526�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto interface. //! //! This module provides an interface to all cryptography features that are used in prs. //! //! It supports multiple cryptography protocols (e.g. GPG) and multiple backends (e.g. GPGME, //! GnuPG). The list of supported protocols and backends may be extended in the future. pub mod backend; pub mod proto; pub mod recipients; pub mod store; pub mod util; use std::collections::HashMap; use std::fmt; use std::fs; use std::path::Path; use anyhow::Result; use thiserror::Error; use crate::{Ciphertext, Plaintext, Recipients}; /// Crypto protocol. /// /// This list contains all protocols supported by the prs project. This does not mean that all /// protocols are supported at runtime in a given build. #[non_exhaustive] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum Proto { /// GPG crypto. Gpg, } impl Proto { /// Get the protocol display name. pub fn name(&self) -> &str { match self { Self::Gpg => "GPG", } } } /// Crypto configuration. /// /// Allows configuring extra properties for contexts globally. pub struct Config { /// Protocol used. pub proto: Proto, /// Use TTY for password input with GPG. pub gpg_tty: bool, /// Whether to show verbose output. pub verbose: bool, } impl Config { /// Construct config with given protocol. pub fn from(proto: Proto) -> Self { Self { proto, gpg_tty: false, verbose: false, } } } /// Represents a key. /// /// The key type may be any of the supported crypto proto types. #[derive(Clone, PartialEq)] #[non_exhaustive] pub enum Key { /// An GPG key. #[cfg(feature = "_crypto-gpg")] Gpg(proto::gpg::Key), } impl Key { /// Get key protocol type. pub fn proto(&self) -> Proto { match self { #[cfg(feature = "_crypto-gpg")] Key::Gpg(_) => Proto::Gpg, } } /// Key fingerprint. pub fn fingerprint(&self, short: bool) -> String { match self { #[cfg(feature = "_crypto-gpg")] Key::Gpg(key) => key.fingerprint(short), } } /// Display string for user. pub fn display(&self) -> String { match self { #[cfg(feature = "_crypto-gpg")] Key::Gpg(key) => key.display_user(), } } } impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "[{}] {} - {}", self.proto().name(), self.fingerprint(true), self.display(), ) } } /// Get crypto context for given proto type at runtime. /// /// This selects a compatible crypto context at runtime. /// /// # Errors /// /// Errors if no compatible crypto context is available for the selected protocol because no /// backend is providing it. Also errors if creating the context fails. pub fn context(config: &Config) -> Result<Context, Err> { // Select proper crypto backend match config.proto { #[allow(unreachable_code)] Proto::Gpg => { #[cfg(feature = "backend-gpgme")] return Ok(Context::from(Box::new( backend::gpgme::context::context(config).map_err(|err| Err::Context(err.into()))?, ))); #[cfg(feature = "backend-gnupg-bin")] return Ok(Context::from(Box::new( backend::gnupg_bin::context::context(config) .map_err(|err| Err::Context(err.into()))?, ))); } } #[allow(unreachable_code)] Err(Err::Unsupported(config.proto)) } /// Generic context. pub struct Context { /// Inner context. context: Box<dyn IsContext>, } impl Context { pub fn from(context: Box<dyn IsContext>) -> Self { Self { context } } } impl IsContext for Context { fn encrypt(&mut self, recipients: &Recipients, plaintext: Plaintext) -> Result<Ciphertext> { self.context.encrypt(recipients, plaintext) } fn decrypt(&mut self, ciphertext: Ciphertext) -> Result<Plaintext> { self.context.decrypt(ciphertext) } fn can_decrypt(&mut self, ciphertext: Ciphertext) -> Result<bool> { self.context.can_decrypt(ciphertext) } fn keys_public(&mut self) -> Result<Vec<Key>> { self.context.keys_public() } fn keys_private(&mut self) -> Result<Vec<Key>> { self.context.keys_private() } fn import_key(&mut self, key: &[u8]) -> Result<()> { self.context.import_key(key) } fn export_key(&mut self, key: Key) -> Result<Vec<u8>> { self.context.export_key(key) } fn supports_proto(&self, proto: Proto) -> bool { self.context.supports_proto(proto) } } /// Defines generic crypto context. /// /// Implemented on backend specific cryptography contexcts, makes using it possible through a /// single simple interface. pub trait IsContext { /// Encrypt plaintext for recipients. fn encrypt(&mut self, recipients: &Recipients, plaintext: Plaintext) -> Result<Ciphertext>; /// Encrypt plaintext and write it to the file. fn encrypt_file( &mut self, recipients: &Recipients, plaintext: Plaintext, path: &Path, ) -> Result<()> { fs::write(path, self.encrypt(recipients, plaintext)?.unsecure_ref()) .map_err(|err| Err::WriteFile(err).into()) } /// Decrypt ciphertext. fn decrypt(&mut self, ciphertext: Ciphertext) -> Result<Plaintext>; /// Decrypt ciphertext from file. fn decrypt_file(&mut self, path: &Path) -> Result<Plaintext> { self.decrypt(fs::read(path).map_err(Err::ReadFile)?.into()) } /// Check whether we can decrypt ciphertext. fn can_decrypt(&mut self, ciphertext: Ciphertext) -> Result<bool>; /// Check whether we can decrypt ciphertext from file. fn can_decrypt_file(&mut self, path: &Path) -> Result<bool> { self.can_decrypt(fs::read(path).map_err(Err::ReadFile)?.into()) } /// Obtain all public keys from keychain. fn keys_public(&mut self) -> Result<Vec<Key>>; /// Obtain all public keys from keychain. fn keys_private(&mut self) -> Result<Vec<Key>>; /// Obtain a public key from keychain for fingerprint. fn get_public_key(&mut self, fingerprint: &str) -> Result<Key> { self.keys_public()? .into_iter() .find(|key| util::fingerprints_equal(key.fingerprint(false), fingerprint)) .ok_or_else(|| Err::UnknownFingerprint.into()) } /// Find public keys from keychain for fingerprints. /// /// Skips fingerprints no key is found for. fn find_public_keys(&mut self, fingerprints: &[&str]) -> Result<Vec<Key>> { let keys = self.keys_public()?; Ok(fingerprints .iter() .filter_map(|fingerprint| { keys.iter() .find(|key| util::fingerprints_equal(key.fingerprint(false), fingerprint)) .cloned() }) .collect()) } /// Import the given key from bytes into keychain. fn import_key(&mut self, key: &[u8]) -> Result<()>; /// Import the given key from a file into keychain. fn import_key_file(&mut self, path: &Path) -> Result<()> { self.import_key(&fs::read(path).map_err(Err::ReadFile)?) } /// Export the given key from the keychain as bytes. fn export_key(&mut self, key: Key) -> Result<Vec<u8>>; /// Export the given key from the keychain to a file. fn export_key_file(&mut self, key: Key, path: &Path) -> Result<()> { fs::write(path, self.export_key(key)?).map_err(|err| Err::WriteFile(err).into()) } /// Check whether this context supports the given protocol. fn supports_proto(&self, proto: Proto) -> bool; } /// A pool of proto contexts. /// /// Makes using multiple contexts easy, by caching contexts by protocol type and initializing them /// on demand. pub struct ContextPool { /// All loaded contexts. contexts: HashMap<Proto, Context>, } impl ContextPool { /// Create new empty pool. pub fn empty() -> Self { Self { contexts: HashMap::new(), } } /// Get mutable context for given proto. /// /// This will initialize the context if no context is loaded for the given proto yet. This /// may error.. pub fn get_mut<'a>(&'a mut self, config: &'a Config) -> Result<&'a mut Context> { Ok(self .contexts .entry(config.proto) .or_insert(context(config)?)) } } /// Crypto error. #[derive(Debug, Error)] pub enum Err { #[error("failed to obtain GPG cryptography context")] Context(#[source] anyhow::Error), #[error("failed to built context, protocol not supportd: {:?}", _0)] Unsupported(Proto), #[error("failed to write to file")] WriteFile(#[source] std::io::Error), #[error("failed to read from file")] ReadFile(#[source] std::io::Error), #[error("fingerprint does not match public key in keychain")] UnknownFingerprint, } /// Prelude for common crypto traits. pub mod prelude { pub use super::{store::StoreRecipients, IsContext}; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/proto/��������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0016537�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/proto/gpg.rs��������������������������������������������������������������0000664�0000000�0000000�00000001607�14713723046�0017666�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto GPG protocol. /// Represents a GPG key. #[derive(Clone)] pub struct Key { /// Full fingerprint. pub fingerprint: String, /// Displayable user ID strings. pub user_ids: Vec<String>, } impl Key { /// Key fingerprint. pub fn fingerprint(&self, short: bool) -> String { if short { &self.fingerprint[self.fingerprint.len() - 16..] } else { &self.fingerprint } .trim() .to_uppercase() } /// Key displayable user data. pub fn display_user(&self) -> String { self.user_ids.join("; ") } /// Transform into generic key. pub fn into_key(self) -> crate::crypto::Key { crate::crypto::Key::Gpg(self) } } impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { self.fingerprint.trim().to_uppercase() == other.fingerprint.trim().to_uppercase() } } �������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/proto/mod.rs��������������������������������������������������������������0000664�0000000�0000000�00000000104�14713723046�0017657�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Crypto protocols. #[cfg(feature = "_crypto-gpg")] pub mod gpg; ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/recipients.rs�������������������������������������������������������������0000664�0000000�0000000�00000004502�14713723046�0020110�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Provides interface for crypto recipients. use anyhow::Result; use super::Key; use crate::crypto::{self, prelude::*, util}; /// A list of recipients. /// /// This list is used to define identities (as keys) to encrypt secrets for. /// All keys should always use the same protocol. /// /// In the future this may support recipients using multiple protocols. #[derive(Clone, PartialEq)] pub struct Recipients { keys: Vec<Key>, } impl Recipients { /// Construct recipients set from list of keys. /// /// # Panics /// /// Panics if keys use multiple protocols. pub fn from(keys: Vec<Key>) -> Self { assert!(keys_same_proto(&keys), "recipient keys must use same proto"); Self { keys } } /// Get recipient keys. pub fn keys(&self) -> &[Key] { &self.keys } /// Add recipient. /// /// # Panics /// /// Panics if new key uses different protocol. pub fn add(&mut self, key: Key) { self.keys.push(key); assert!( keys_same_proto(&self.keys), "added recipient key uses different proto" ); } /// Remove the given key if existent. pub fn remove(&mut self, key: &Key) { self.keys.retain(|k| k != key); } /// Remove the given keys. /// /// Keys that are not found are ignored. pub fn remove_all(&mut self, keys: &[Key]) { self.keys.retain(|k| !keys.contains(k)); } /// Check whether this recipient list has the given fingerprint. pub fn has_fingerprint(&self, fingerprint: &str) -> bool { self.keys .iter() .any(|k| util::fingerprints_equal(k.fingerprint(false), fingerprint)) } } /// Check whether the given recipients contain any key that we have a secret key in our keychain /// for. pub fn contains_own_secret_key(recipients: &Recipients) -> Result<bool> { let secrets = Recipients::from(crypto::context(&crate::CONFIG)?.keys_private()?); Ok(recipients .keys() .iter() .any(|k| secrets.has_fingerprint(&k.fingerprint(false)))) } /// Check if given keys all use same proto. /// /// Succeeds if no key is given. fn keys_same_proto(keys: &[Key]) -> bool { if keys.len() < 2 { true } else { let proto = keys[0].proto(); keys[1..].iter().all(|k| k.proto() == proto) } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/crypto/store.rs������������������������������������������������������������������0000664�0000000�0000000�00000017714�14713723046�0017110�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Helpers to use recipients with password store. use std::fs; use std::path::{Path, PathBuf}; use anyhow::Result; use thiserror::Error; use super::{prelude::*, recipients::Recipients, util, Config, ContextPool, Key, Proto}; use crate::Store; /// Password store GPG IDs file. const STORE_GPG_IDS_FILE: &str = ".gpg-id"; /// Password store public key directory. const STORE_PUB_KEY_DIR: &str = ".public-keys/"; /// Get the GPG IDs file for a store. pub fn store_gpg_ids_file(store: &Store) -> PathBuf { store.root.join(STORE_GPG_IDS_FILE) } /// Get the public keys directory for a store. pub fn store_public_keys_dir(store: &Store) -> PathBuf { store.root.join(STORE_PUB_KEY_DIR) } /// Read GPG fingerprints from store. pub fn store_read_gpg_fingerprints(store: &Store) -> Result<Vec<String>> { let path = store_gpg_ids_file(store); if path.is_file() { read_fingerprints(path) } else { Ok(vec![]) } } /// Write GPG fingerprints to a store. /// /// Overwrites any existing file. pub fn store_write_gpg_fingerprints<S: AsRef<str>>( store: &Store, fingerprints: &[S], ) -> Result<()> { write_fingerprints(store_gpg_ids_file(store), fingerprints) } /// Read fingerprints from the given file. fn read_fingerprints<P: AsRef<Path>>(path: P) -> Result<Vec<String>> { Ok(fs::read_to_string(path) .map_err(Err::ReadFile)? .lines() // Strip comments .map(|fp| match fp.split_once('#') { Some((fp, _)) => fp, None => fp, }) .map(|fp| fp.trim()) .filter(|fp| !fp.is_empty()) .map(Into::into) .collect()) } /// Write fingerprints to the given file. fn write_fingerprints<P: AsRef<Path>, S: AsRef<str>>(path: P, fingerprints: &[S]) -> Result<()> { fs::write( path, fingerprints .iter() .map(|k| k.as_ref()) .collect::<Vec<_>>() .join("\n"), ) .map_err(|err| Err::WriteFile(err).into()) } /// Load the keys for the given store. /// /// This will try to load the keys for all configured protocols, and errors if it fails. pub fn store_load_keys(store: &Store) -> Result<Vec<Key>> { let mut keys = Vec::new(); // TODO: what to do if ids file does not exist? // TODO: what to do if recipients is empty? // TODO: what to do if key listed in file is not found, attempt to install? // Load GPG keys // TODO: do not crash here if GPG ids file is not found! let fingerprints = store_read_gpg_fingerprints(store)?; if !fingerprints.is_empty() { let mut context = super::context(&crate::CONFIG)?; let fingerprints: Vec<_> = fingerprints.iter().map(|fp| fp.as_str()).collect(); keys.extend(context.find_public_keys(&fingerprints)?); } // NEWPROTO: if a new proto is added, keys for a store should be loaded here Ok(keys) } /// Load the recipients for the given store. /// /// This will try to load the recipient keys for all configured protocols, and errors if it fails. pub fn store_load_recipients(store: &Store) -> Result<Recipients> { Ok(Recipients::from(store_load_keys(store)?)) } /// Save the keys for the given store. /// /// This overwrites any existing recipient keys. pub fn store_save_keys(store: &Store, keys: &[Key]) -> Result<()> { // Save GPG keys let gpg_fingerprints: Vec<_> = keys .iter() .filter(|key| key.proto() == Proto::Gpg) .map(|key| key.fingerprint(false)) .collect(); store_write_gpg_fingerprints(store, &gpg_fingerprints)?; // Sync public keys for all proto's store_sync_public_key_files(store, keys)?; // TODO: import missing keys to system? Ok(()) } /// Save the keys for the given store. /// /// This overwrites any existing recipient keys. pub fn store_save_recipients(store: &Store, recipients: &Recipients) -> Result<()> { store_save_keys(store, recipients.keys()) } /// Sync public key files in store with selected recipients. /// /// - Removes obsolete keys that are not a selected recipient /// - Adds missing keys that are a recipient /// /// This syncs public key files for all protocols. This is because the public key files themselves /// don't specify what protocol they use. All public key files and keys must therefore be taken /// into consideration all at once. pub fn store_sync_public_key_files(store: &Store, keys: &[Key]) -> Result<()> { // Get public keys directory, ensure it exists let dir = store_public_keys_dir(store); fs::create_dir_all(&dir).map_err(Err::SyncKeyFiles)?; // List key files in keys directory let files: Vec<(PathBuf, String)> = dir .read_dir() .map_err(Err::SyncKeyFiles)? .filter_map(|e| e.ok()) .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false)) .filter_map(|e| { e.file_name() .to_str() .map(|fp| (e.path(), util::format_fingerprint(fp))) }) .collect(); // Remove unused keys for (path, _) in files .iter() .filter(|(_, fp)| !util::keys_contain_fingerprint(keys, fp)) { fs::remove_file(path).map_err(Err::SyncKeyFiles)?; } // Add missing keys let mut contexts = ContextPool::empty(); for (key, fp) in keys .iter() .map(|k| (k, k.fingerprint(false))) .filter(|(_, fp)| !files.iter().any(|(_, other)| fp == other)) { // Lazy load compatible context let proto = key.proto(); let config = Config::from(proto); let context = contexts.get_mut(&config)?; // Export public key to disk let path = dir.join(&fp); context.export_key_file(key.clone(), &path)?; } // NEWPROTO: if a new proto is added, public keys should be synced here Ok(()) } /// Import keys from store that are missing in the keychain. pub fn import_missing_keys_from_store(store: &Store) -> Result<Vec<ImportResult>> { // Get public keys directory, ensure it exists let dir = store_public_keys_dir(store); if !dir.is_dir() { return Ok(vec![]); } // Cache protocol contexts let mut contexts = ContextPool::empty(); let mut results = Vec::new(); // Check for missing GPG keys based on fingerprint, import them let gpg_fingerprints = store_read_gpg_fingerprints(store)?; for fingerprint in gpg_fingerprints { let context = contexts.get_mut(&crate::CONFIG)?; if context.get_public_key(&fingerprint).is_err() { let path = &store_public_keys_dir(store).join(&fingerprint); if path.is_file() { context.import_key_file(path)?; results.push(ImportResult::Imported(fingerprint)); } else { results.push(ImportResult::Unavailable(fingerprint)); } } } // NEWPROTO: if a new proto is added, import missing keys here Ok(results) } /// Missing key import results. pub enum ImportResult { /// Key with given fingerprint was imported into keychain. Imported(String), /// Key with given fingerprint was not found and was not imported in keychain. Unavailable(String), } /// Recipients extension for store functionality. pub trait StoreRecipients { /// Load recipients from given store. fn load(store: &Store) -> Result<Recipients>; /// Save recipients to given store. fn save(&self, store: &Store) -> Result<()>; } impl StoreRecipients for Recipients { /// Load recipients from given store. fn load(store: &Store) -> Result<Recipients> { store_load_recipients(store) } /// Save recipients to given store. fn save(&self, store: &Store) -> Result<()> { store_save_recipients(store, self) } } /// Store crypto error. #[derive(Debug, Error)] pub enum Err { #[error("failed to write to file")] WriteFile(#[source] std::io::Error), #[error("failed to read from file")] ReadFile(#[source] std::io::Error), #[error("failed to sync public key files")] SyncKeyFiles(#[source] std::io::Error), } ����������������������������������������������������prs-v0.5.2/lib/src/crypto/util.rs�������������������������������������������������������������������0000664�0000000�0000000�00000001712�14713723046�0016720�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Common crypto utilities. use anyhow::Result; use super::{prelude::*, Config, Key}; /// Format fingerprint in consistent format. /// /// Trims and uppercases. pub fn format_fingerprint<S: AsRef<str>>(fingerprint: S) -> String { fingerprint.as_ref().trim().to_uppercase() } /// Check whether two fingerprints match. pub fn fingerprints_equal<S: AsRef<str>, T: AsRef<str>>(a: S, b: T) -> bool { !a.as_ref().trim().is_empty() && a.as_ref().trim().to_uppercase() == b.as_ref().trim().to_uppercase() } /// Check whether a list of keys contains the given fingerprint. pub fn keys_contain_fingerprint<S: AsRef<str>>(keys: &[Key], fingerprint: S) -> bool { keys.iter() .any(|key| fingerprints_equal(key.fingerprint(false), fingerprint.as_ref())) } /// Check whether the user has any private/secret key in their keychain. pub fn has_private_key(config: &Config) -> Result<bool> { Ok(!super::context(config)?.keys_private()?.is_empty()) } ������������������������������������������������������prs-v0.5.2/lib/src/git.rs���������������������������������������������������������������������������0000664�0000000�0000000�00000024461�14713723046�0015214�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::ffi::OsStr; use std::path::Path; use std::process::{Command, ExitStatus, Output}; use std::time::SystemTime; use anyhow::Result; use thiserror::Error; use crate::util; // Re-exports pub use git_state::{git_state, RepositoryState}; /// Binary name. #[cfg(not(windows))] pub const BIN_NAME: &str = "git"; #[cfg(windows)] pub const BIN_NAME: &str = "git.exe"; /// The git FETCH_HEAD file. const GIT_FETCH_HEAD_FILE: &str = ".git/FETCH_HEAD"; /// Git exit status when a config item is not found. const GIT_EXIT_STATUS_NOT_FOUND: i32 = 1; /// Invoke git init. pub fn git_init(repo: &Path) -> Result<()> { git(repo, ["init", "-q"], false) } /// Invoke git clone. /// /// Shows progress unless `quiet` is set. pub fn git_clone(repo: &Path, url: &str, path: &str, quiet: bool) -> Result<()> { let mut args = vec!["clone", "-q"]; if !quiet { args.push("--progress"); } args.extend_from_slice(&[url, path]); git(repo, &args, true) } /// Git stage all files and changes. pub fn git_add_all(repo: &Path) -> Result<()> { git(repo, ["add", "."], false) } /// Invoke git commit. pub fn git_commit(repo: &Path, msg: &str, commit_empty: bool) -> Result<()> { // Quit if no changes and we don't allow empty commit if !commit_empty && !git_has_changes(repo)? { return Ok(()); } let mut args = vec!["commit", "-q", "--no-edit", "-m", msg]; if commit_empty { args.push("--allow-empty"); } git(repo, &args, false) } /// Git hard reset all changes. pub fn git_reset_hard(repo: &Path) -> Result<()> { git(repo, ["reset", "--hard", "-q"], false) } /// Git status. pub fn git_status(repo: &Path, short: bool) -> Result<String> { let mut args = vec!["status"]; if short { args.push("--short"); } git_stdout_ok(repo, &args, false) } /// Invoke git push. pub fn git_push(repo: &Path, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> { // TODO: do not set -q flag if in verbose mode? let mut args = vec!["push", "-q"]; if let Some(upstream) = set_upstream { args.extend_from_slice(&["--set-upstream", upstream]); } if let Some(branch) = set_branch { args.push(branch); } git(repo, &args, true) } /// Invoke git pull. pub fn git_pull(repo: &Path) -> Result<()> { // TODO: do not set -q flag if in verbose mode? git(repo, ["pull", "-q"], true) } /// Invoke git fetch. pub fn git_fetch(repo: &Path, reference: Option<&str>) -> Result<()> { // TODO: do not set -q flag if in verbose mode? let mut args = vec!["fetch", "-q"]; if let Some(reference) = reference { args.push(reference); } git(repo, &args, true) } /// Check if repository has (staged/unstaged) changes. pub fn git_has_changes(repo: &Path) -> Result<bool> { Ok(!git_stdout_ok(repo, ["status", "-s"], false)?.is_empty()) } /// Check if repository has remote configured. pub fn git_has_remote(repo: &Path) -> Result<bool> { Ok(!git_stdout_ok(repo, ["remote"], false)?.is_empty()) } /// Git get remote list. pub fn git_remote(repo: &Path) -> Result<Vec<String>> { Ok(git_stdout_ok(repo, ["remote"], false)? .lines() .map(|r| r.into()) .collect()) } /// Get get remote URL. pub fn git_remote_get_url(repo: &Path, remote: &str) -> Result<String> { git_stdout_ok(repo, ["remote", "get-url", remote], false) } /// Get add remote URL. pub fn git_remote_add(repo: &Path, remote: &str, url: &str) -> Result<()> { git(repo, ["remote", "add", remote, url], false) } /// Get remove remote URL. pub fn git_remote_remove(repo: &Path, remote: &str) -> Result<()> { git(repo, ["remote", "remove", remote], false) } /// Get the current git branch name. pub fn git_current_branch(repo: &Path) -> Result<String> { let branch = git_stdout_ok(repo, ["rev-parse", "--abbrev-ref", "HEAD"], false)?; assert!(!branch.is_empty(), "git returned empty branch name"); assert!(!branch.contains('\n'), "git returned multiple branches"); Ok(branch) } /// Get the current remote name for a git branch. pub fn git_config_branch_remote(repo: &Path, branch: &str) -> Result<Option<String>> { // Grap configured remote for branch let mut remote = git_stdout_ok_or( repo, ["config", "--get", &format!("branch.{branch}.remote")], false, GIT_EXIT_STATUS_NOT_FOUND, )?; // Or grab default configured remote if remote.is_empty() { remote = git_stdout_ok_or( repo, ["config", "--get", "remote.pushDefault"], false, GIT_EXIT_STATUS_NOT_FOUND, )?; } // Or return none if no remote is configured if remote.is_empty() { return Ok(None); } assert!(!branch.contains('\n'), "git returned multiple remotes"); Ok(Some(remote)) } /// Set the current remote name for a git branch. pub fn git_config_branch_set_remote(repo: &Path, branch: &str, remote: &str) -> Result<()> { git_stdout_ok( repo, ["config", &format!("branch.{branch}.remote"), remote], false, ) .map(|_| ()) } /// List remote git branches. pub fn git_branch_remote(repo: &Path) -> Result<Vec<String>> { Ok(git_stdout_ok(repo, ["branch", "-r", "--no-color"], false)? .lines() .map(|r| { match r.strip_prefix("* ") { Some(r) => r, None => r, } .to_string() }) .collect()) } /// Get upstream branch for given branch if there is any. /// /// If there is none, `None` is returned. pub fn git_branch_upstream<S: AsRef<str>>(repo: &Path, reference: S) -> Result<Option<String>> { // Invoke command let output = git_output( repo, [ "rev-parse", "--abbrev-ref", &format!("{}@{{upstream}}", reference.as_ref()), ], false, )?; // Scan stderr for 'no upstream' messages let stderr = std::str::from_utf8(&output.stderr) .map_err(|err| Err::GitCli(err.into()))? .trim(); if stderr.contains("fatal: no upstream configured for branch") { return Ok(None); } // Assert status cmd_assert_status(output.status)?; // Find upstream branch let upstream = std::str::from_utf8(&output.stdout) .map_err(|err| Err::GitCli(err.into()))? .trim(); if upstream.is_empty() { return Ok(None); } assert!( upstream.contains('/'), "git returned invalid upstream branch name" ); Ok(Some(upstream.into())) } /// Set upstream branch for the given branch. pub fn git_branch_set_upstream(repo: &Path, reference: Option<&str>, upstream: &str) -> Result<()> { let mut args = vec!["branch", "--set-upstream-to", upstream]; if let Some(reference) = reference { args.push(reference); } git(repo, &args, false) } /// Get the hash of a reference. pub fn git_ref_hash<S: AsRef<str>>(repo: &Path, reference: S) -> Result<String> { let hash = git_stdout_ok(repo, ["rev-parse", reference.as_ref()], false)?; assert_eq!(hash.len(), 40, "git returned invalid hash"); Ok(hash) } /// Get system time the repository was last pulled. /// See: https://stackoverflow.com/a/9229377/1000145 (stat -c %Y .git/FETCH_HEAD) pub fn git_last_pull_time(repo: &Path) -> Result<SystemTime> { Ok(repo .join(GIT_FETCH_HEAD_FILE) .metadata() .and_then(|m| m.modified()) .map_err(Err::Other)?) } /// Invoke a git command with the given arguments. /// /// The command will take over the user console for in/output. fn git<I, S>(repo: &Path, args: I, connects_remote: bool) -> Result<()> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { cmd_assert_status( cmd_git(args, repo, connects_remote) .status() .map_err(Err::System)?, ) } /// Invoke a git command, returns output. fn git_output<I, S>(repo: &Path, args: I, connects_remote: bool) -> Result<Output> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { cmd_git(args, repo, connects_remote) .output() .map_err(|err| Err::System(err).into()) } /// Invoke a git command with the given arguments, return stdout on success or when the exit code /// is allowed. fn git_stdout_ok_or<I, S>( repo: &Path, args: I, connects_remote: bool, allow_exit_code: i32, ) -> Result<String> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let output = git_output(repo, args, connects_remote)?; // Assert exit status, but allow specified exit code if output.status.code() != Some(allow_exit_code) { cmd_assert_status(output.status)?; } Ok(std::str::from_utf8(&output.stdout) .map_err(|err| Err::GitCli(err.into()))? .trim() .into()) } /// Invoke a git command with the given arguments, return stdout on success. fn git_stdout_ok<I, S>(repo: &Path, args: I, connects_remote: bool) -> Result<String> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let output = git_output(repo, args, connects_remote)?; cmd_assert_status(output.status)?; Ok(std::str::from_utf8(&output.stdout) .map_err(|err| Err::GitCli(err.into()))? .trim() .into()) } /// Build a git command to run. fn cmd_git<I, S>(args: I, dir: &Path, connects_remote: bool) -> Command where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let mut cmd = Command::new(BIN_NAME); cmd.arg("-C"); cmd.arg(dir); cmd.current_dir(dir); // Configure session reuse if connecting to a remote and supported if connects_remote && util::git::guess_ssh_persist_support(dir) { util::git::configure_ssh_persist(&mut cmd); } cmd.args(args); // Debug invoked git commands // eprintln!("Invoked: {:?}", &cmd); cmd } /// Assert the exit status of a command. /// /// Returns error is status is not succesful. fn cmd_assert_status(status: ExitStatus) -> Result<()> { if !status.success() { return Err(Err::Status(status).into()); } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to complete git operation")] Other(#[source] std::io::Error), #[error("failed to complete git operation")] GitCli(#[source] anyhow::Error), #[error("failed to invoke system command")] System(#[source] std::io::Error), #[error("git operation exited with non-zero status code: {0}")] Status(std::process::ExitStatus), } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/lib.rs���������������������������������������������������������������������������0000664�0000000�0000000�00000001747�14713723046�0015201�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pub mod crypto; pub(crate) mod git; pub mod store; pub mod sync; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod systemd_bin; #[cfg(all(feature = "tomb", target_os = "linux"))] pub mod tomb; #[cfg(all(feature = "tomb", target_os = "linux"))] pub(crate) mod tomb_bin; pub mod types; pub mod util; #[cfg(test)] extern crate quickcheck; #[cfg(test)] #[macro_use(quickcheck)] extern crate quickcheck_macros; #[macro_use] extern crate lazy_static; // Re-exports pub use crypto::{recipients::Recipients, Key}; pub use store::{Secret, Store}; pub use types::{Ciphertext, Plaintext}; use crate::crypto::{Config, Proto}; /// Default password store directory. #[cfg(not(windows))] pub const STORE_DEFAULT_ROOT: &str = "~/.password-store"; #[cfg(windows)] pub const STORE_DEFAULT_ROOT: &str = "~\\.password-store"; /// Default proto config. // TODO: remove when multiple protocols are supported. const CONFIG: Config = Config { proto: Proto::Gpg, gpg_tty: false, verbose: false, }; �������������������������prs-v0.5.2/lib/src/store.rs�������������������������������������������������������������������������0000664�0000000�0000000�00000030573�14713723046�0015566�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Interface to a password store and its secrets. use std::ffi::OsString; use std::fs; use std::path::{self, Path, PathBuf}; use anyhow::{ensure, Result}; use thiserror::Error; use walkdir::{DirEntry, WalkDir}; #[cfg(all(feature = "tomb", target_os = "linux"))] use crate::tomb::Tomb; use crate::{ crypto::{self, prelude::*}, sync::Sync, Recipients, }; /// Password store secret file suffix. pub const SECRET_SUFFIX: &str = ".gpg"; /// Represents a password store. #[derive(Clone)] pub struct Store { /// Root directory of the password store. /// /// This path is always absolute. pub root: PathBuf, } impl Store { /// Open a store at the given path. pub fn open<P: AsRef<str>>(root: P) -> Result<Self> { let root: PathBuf = shellexpand::full(&root) .map_err(Err::ExpandPath)? .as_ref() .into(); let root = root.canonicalize().map_err(Err::CanonicalizePath)?; // Make sure store directory exists ensure!(root.is_dir(), Err::NoRootDir(root)); // TODO: check if .gpg-ids exists? this does not work if this is a tomb Ok(Self { root }) } /// Get the recipient keys for this store. pub fn recipients(&self) -> Result<Recipients> { Recipients::load(self) } /// Get a sync helper for this store. pub fn sync(&self) -> Sync { Sync::new(self) } /// Get a tomb helper for this store. #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn tomb(&self, quiet: bool, verbose: bool, force: bool) -> Tomb { Tomb::new(self, quiet, verbose, force) } /// Create secret iterator for this store. pub fn secret_iter(&self) -> SecretIter { self.secret_iter_config(SecretIterConfig::default()) } /// Create secret iterator for this store with custom configuration. pub fn secret_iter_config(&self, config: SecretIterConfig) -> SecretIter { SecretIter::new(self.root.clone(), config) } /// List store password secrets. pub fn secrets(&self, filter: Option<String>) -> Vec<Secret> { self.secret_iter().filter_name(filter).collect() } /// Try to find matching secret at path. pub fn find_at(&self, path: &str) -> Option<Secret> { // Build path let path = self.root.as_path().join(path); let path = path.to_str()?; // Try path with secret file suffix let with_suffix = PathBuf::from(format!("{path}{SECRET_SUFFIX}")); if with_suffix.is_file() { return Some(Secret::from(self, with_suffix)); } // Try path without secret file suffix let without_suffix = Path::new(path); if without_suffix.is_file() { return Some(Secret::from(self, without_suffix.to_path_buf())); } None } /// Try to find matching secrets for given query. /// /// If secret is found at exact query path, `FindSecret::Found` is returned. /// Otherwise any number of closely matching secrets is returned as `FindSecret::Many`. pub fn find(&self, query: Option<String>) -> FindSecret { // Try to find exact secret match if let Some(query) = &query { if let Some(secret) = self.find_at(query) { return FindSecret::Exact(secret); } } // Find all closely matching FindSecret::Many(self.secrets(query)) } /// Normalizes a path for a secret in this store. /// /// - Ensures path is within store. /// - If directory is given, name hint is appended. /// - Sets correct extension. /// - Creates parent directories if non existant (optional). pub fn normalize_secret_path<P: AsRef<Path>>( &self, target: P, name_hint: Option<&str>, create_dirs: bool, ) -> Result<PathBuf> { // Take target as base path let mut path = PathBuf::from(target.as_ref()); // Expand path if let Some(path_str) = path.to_str() { path = PathBuf::from( shellexpand::full(path_str) .map_err(Err::ExpandPath)? .as_ref(), ); } let target_is_dir = path.is_dir() || target .as_ref() .to_str() .and_then(|s| s.chars().last()) .map(path::is_separator) .unwrap_or(false); // Strip store prefix if let Ok(tmp) = path.strip_prefix(&self.root) { path = tmp.into(); } // Make relative if path.is_absolute() { path = PathBuf::from(format!(".{}{}", path::MAIN_SEPARATOR, path.display())); } // Prefix store root path = self.root.as_path().join(path); // Add current secret name if target is dir if target_is_dir { path.push(name_hint.ok_or_else(|| Err::TargetDirWithoutNamehint(path.clone()))?); } // Add secret extension if non existent let ext: OsString = SECRET_SUFFIX.trim_start_matches('.').into(); if path.extension() != Some(&ext) { let mut tmp = path.as_os_str().to_owned(); tmp.push(SECRET_SUFFIX); path = PathBuf::from(tmp); } // Create parent dir if it doesn't exist if create_dirs { let parent = path.parent().unwrap(); if !parent.is_dir() { fs::create_dir_all(parent).map_err(Err::CreateDir)?; } } Ok(path) } } /// Find secret result. pub enum FindSecret { /// Found exact secret match. Exact(Secret), /// Found any number of non-exact secret matches. Many(Vec<Secret>), } /// A password store secret. #[derive(Debug, Clone)] pub struct Secret { /// Display name of the secret, relative path to the password store root. pub name: String, /// Full path to the password store secret. pub path: PathBuf, } impl Secret { /// Construct secret at given full path from given store. pub fn from(store: &Store, path: PathBuf) -> Self { Self::in_root(&store.root, path) } /// Construct secret at given path in the given password store root. pub fn in_root(root: &Path, path: PathBuf) -> Self { let name: String = relative_path(root, &path) .ok() .and_then(|f| f.to_str()) .map(|f| f.trim_end_matches(SECRET_SUFFIX)) .unwrap_or_else(|| "?") .to_string(); Self { name, path } } /// Get relative path to this secret, root must be given. pub fn relative_path<'a>( &'a self, root: &'a Path, ) -> Result<&'a Path, std::path::StripPrefixError> { relative_path(root, &self.path) } /// Returns pointed to secret. /// /// If this secret is an alias, this will return the pointed to secret. /// If this secret is not an alias, an error will be returned. /// /// The pointed to secret may be an alias as well. pub fn alias_target(&self, store: &Store) -> Result<Secret> { // Read alias target path, make absolute, attempt to canonicalize let mut path = self.path.parent().unwrap().join(fs::read_link(&self.path)?); if let Ok(canonical_path) = path.canonicalize() { path = canonical_path; } Ok(Secret::from(store, path)) } } /// Get relative path in given root. pub fn relative_path<'a>( root: &'a Path, path: &'a Path, ) -> Result<&'a Path, std::path::StripPrefixError> { path.strip_prefix(root) } /// Secret iterator configuration. /// /// Used to configure what files are found by the secret iterator. #[derive(Clone, Debug)] pub struct SecretIterConfig { /// Find pure files. pub find_files: bool, /// Find files that are symlinks. /// /// Will still find files if they're symlinked to while `find_files` is `false`. pub find_symlink_files: bool, } impl Default for SecretIterConfig { fn default() -> Self { Self { find_files: true, find_symlink_files: true, } } } /// Iterator that walks through password store secrets. /// /// This walks all password store directories, and yields password secrets. /// Hidden files or directories are skipped. pub struct SecretIter { /// Root of the store to walk. root: PathBuf, /// Directory walker. walker: Box<dyn Iterator<Item = DirEntry>>, } impl SecretIter { /// Create new store secret iterator at given store root. pub fn new(root: PathBuf, config: SecretIterConfig) -> Self { let walker = WalkDir::new(&root) .follow_links(true) .into_iter() .filter_entry(|e| !is_hidden_subdir(e)) .filter_map(|e| e.ok()) .filter(is_secret_file) .filter(move |entry| filter_by_config(entry, &config)); Self { root, walker: Box::new(walker), } } /// Transform into a filtered secret iterator. pub fn filter_name(self, filter: Option<String>) -> FilterSecretIter<Self> { FilterSecretIter::new(self, filter) } } impl Iterator for SecretIter { type Item = Secret; fn next(&mut self) -> Option<Self::Item> { self.walker .next() .map(|e| Secret::in_root(&self.root, e.path().into())) } } /// Check if given WalkDir DirEntry is hidden sub-directory. fn is_hidden_subdir(entry: &DirEntry) -> bool { entry.depth() > 0 && entry .file_name() .to_str() .map(|s| s.starts_with('.') || s == "lost+found") .unwrap_or(false) } /// Check if given WalkDir DirEntry is a secret file. fn is_secret_file(entry: &DirEntry) -> bool { entry.file_type().is_file() && entry .file_name() .to_str() .map(|s| s.ends_with(SECRET_SUFFIX)) .unwrap_or(false) } /// Check if given WalkDir DirEntry passes the configuration. fn filter_by_config(entry: &DirEntry, config: &SecretIterConfig) -> bool { // Optimization, config permutation which includes all files if config.find_files && config.find_symlink_files { return true; } // Find symlinks if config.find_symlink_files && entry.path_is_symlink() { return true; } // Do not find symlinks if !config.find_symlink_files && entry.path_is_symlink() { return false; } // Find files if !config.find_files && !entry.path_is_symlink() { return false; } true } /// Check whether we can decrypt the first secret in the store. /// /// If decryption fails, and this returns false, it means we don't own any compatible secret key. /// /// Returns true if there is no secret. pub fn can_decrypt(store: &Store) -> bool { // Try all proto's here once we support more store .secret_iter() .next() .map(|secret| { crypto::context(&crate::CONFIG) .map(|mut context| context.can_decrypt_file(&secret.path).unwrap_or(true)) .unwrap_or(false) }) .unwrap_or(true) } /// Iterator that wraps a `SecretIter` with a filter. pub struct FilterSecretIter<I> where I: Iterator<Item = Secret>, { inner: I, filter: Option<String>, } impl<I> FilterSecretIter<I> where I: Iterator<Item = Secret>, { /// Construct a new filter secret iterator. pub fn new(inner: I, filter: Option<String>) -> Self { Self { inner, filter } } } impl<I> Iterator for FilterSecretIter<I> where I: Iterator<Item = Secret>, { type Item = Secret; fn next(&mut self) -> Option<Self::Item> { // Return all with no filter, or lowercase filter text let filter = match &self.filter { None => return self.inner.next(), Some(filter) => filter.to_lowercase(), }; self.inner .find(|secret| secret.name.to_lowercase().contains(&filter)) } } /// Password store error. #[derive(Debug, Error)] pub enum Err { #[error("failed to expand store root path")] ExpandPath(#[source] shellexpand::LookupError<std::env::VarError>), #[error("failed to canonicalize store root path")] CanonicalizePath(#[source] std::io::Error), #[error("failed to open password store, not a directory: {0}")] NoRootDir(PathBuf), #[error("failed to create directory")] CreateDir(#[source] std::io::Error), #[error("cannot use directory as target without name hint")] TargetDirWithoutNamehint(PathBuf), } �������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/sync.rs��������������������������������������������������������������������������0000664�0000000�0000000�00000024453�14713723046�0015406�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Password store synchronization functionality. use std::path::Path; use std::time::Duration; use anyhow::Result; use crate::{ git::{self, RepositoryState}, Store, }; /// Store git directory. pub const STORE_GIT_DIR: &str = ".git/"; /// Duration after which pull refs are considered outdated. /// /// If the last pull is within this duration, some operations such as a push may be optimized away /// if not needed. pub const GIT_PULL_OUTDATED: Duration = Duration::from_secs(30); /// Sync helper for given store. pub struct Sync<'a> { /// The store. store: &'a Store, } impl<'a> Sync<'a> { /// Construct new sync helper for given store. pub fn new(store: &'a Store) -> Sync<'a> { Self { store } } /// Get the repository path. fn path(&self) -> &Path { &self.store.root } /// Check readyness of store for syncing. /// /// This checks whether the repository state is clean, which means that there's no active /// merge/rebase/etc. /// The repository might be dirty, use `sync_is_dirty` to check that. pub fn readyness(&self) -> Result<Readyness> { let path = self.path(); if !self.is_init() { return Ok(Readyness::NoSync); } match git::git_state(path).unwrap() { RepositoryState::Clean => { if is_dirty(path)? { Ok(Readyness::Dirty) } else { Ok(Readyness::Ready) } } state => Ok(Readyness::RepoState(state)), } } /// Prepare the store for new changes. /// /// - If sync is not initialized, it does nothing. /// - If sync remote is set, it pulls changes. pub fn prepare(&self) -> Result<()> { // TODO: return error if dirty? // Skip if no sync if !self.is_init() { return Ok(()); } // We're done if we don't have a remote if !self.has_remote()? { return Ok(()); } // We must have upstream set, otherwise try to automatically set or don't pull let repo = self.path(); if git::git_branch_upstream(repo, "HEAD")?.is_none() { // Get remotes, we cannot decide upstream if we don't have exactly one let remotes = self.tracked_remote_or_remotes()?; if remotes.len() != 1 { return Ok(()); } // Fetch remote branches let remote = &remotes[0]; git::git_fetch(repo, Some(remote))?; // List remote branches, stop if there are none let remote_branches = git::git_branch_remote(repo)?; if remote_branches.is_empty() { return Ok(()); } // Determine upstream reference let branch = git::git_current_branch(repo)?; let upstream_ref = format!("{remote}/{branch}"); // Set upstream reference if available on remote, otherwise stop if !remote_branches.contains(&upstream_ref) { return Ok(()); } git::git_branch_set_upstream(repo, None, &upstream_ref)?; } self.pull()?; Ok(()) } /// Finalize the store with new changes. /// /// - If sync is not initialized, it does nothing. /// - If sync is initialized, it commits changes. /// - If sync remote is set, it pushes changes. pub fn finalize<M: AsRef<str>>(&self, msg: M) -> Result<()> { // Skip if no sync if !self.is_init() { return Ok(()); } // Commit changes if dirty if is_dirty(self.path())? { self.commit_all(msg, false)?; } // Do not push if no remote or not out of sync if !self.has_remote()? || !safe_need_to_push(self.path()) { return Ok(()); } // We must have upstream set, otherwise try to automatically set or don't push let mut set_branch = None; let mut set_upstream = None; let repo = self.path(); if git::git_branch_upstream(repo, "HEAD")?.is_none() { // Get remotes, we cannot decide upstream if we don't have exactly one let remotes = git::git_remote(repo)?; if remotes.len() == 1 { // Fetch and list remote branches let remote = &remotes[0]; git::git_fetch(repo, Some(remote))?; let remote_branches = git::git_branch_remote(repo)?; // Determine upstream reference let branch = git::git_current_branch(repo)?; let upstream_ref = format!("{remote}/{branch}"); // Set upstream reference if not yet used on remote if !remote_branches.contains(&upstream_ref) { set_branch = Some(branch); set_upstream = Some(remote.to_string()); } } } self.push(set_branch.as_deref(), set_upstream.as_deref())?; Ok(()) } /// Initialize sync. pub fn init(&self) -> Result<()> { git::git_init(self.path())?; self.commit_all("Initialize sync with git", true)?; Ok(()) } /// Clone sync from a remote URL. pub fn clone(&self, url: &str, quiet: bool) -> Result<()> { let path = self .path() .to_str() .expect("failed to determine clone path"); git::git_clone(self.path(), url, path, quiet)?; Ok(()) } /// Check whether sync has been initialized in this store. pub fn is_init(&self) -> bool { self.path().join(STORE_GIT_DIR).is_dir() } /// Get a list of sync remotes. pub fn remotes(&self) -> Result<Vec<String>> { git::git_remote(self.path()) } /// Get a list of tracked remote or all sync remotes. pub fn tracked_remote_or_remotes(&self) -> Result<Vec<String>> { // Get current branch and remote let branch = git::git_current_branch(self.path())?; let remote = git::git_config_branch_remote(self.path(), &branch); if let Ok(Some(remote)) = remote { return Ok(vec![remote]); } // Fall back to remote list git::git_remote(self.path()) } /// Get the URL of the given remote. pub fn remote_url(&self, remote: &str) -> Result<String> { git::git_remote_get_url(self.path(), remote) } /// Add the URL of the given remote. pub fn add_remote_url(&self, remote: &str, url: &str) -> Result<()> { git::git_remote_add(self.path(), remote, url)?; // Set this remote for the current branch if none is set, ignore errors let branch = git::git_current_branch(self.path()); if let Ok(ref branch) = branch { let _ = git::git_config_branch_set_remote(self.path(), branch, remote); } Ok(()) } /// Set the URL of the given remote. pub fn set_remote_url(&self, remote: &str, url: &str) -> Result<()> { // Do not set but remove and add to flush any fetched remote data git::git_remote_remove(self.path(), remote)?; self.add_remote_url(remote, url) } /// Check whether this store has a remote configured. pub fn has_remote(&self) -> Result<bool> { if !self.is_init() { return Ok(false); } git::git_has_remote(self.path()) } /// Pull changes from remote. fn pull(&self) -> Result<()> { git::git_pull(self.path()) } /// Push changes to remote. fn push(&self, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> { git::git_push(self.path(), set_branch, set_upstream) } /// Add all changes and commit them. pub fn commit_all<M: AsRef<str>>(&self, msg: M, commit_empty: bool) -> Result<()> { let path = self.path(); git::git_add_all(path)?; git::git_commit(path, msg.as_ref(), commit_empty) } /// Hard reset all changes. pub fn reset_hard_all(&self) -> Result<()> { let path = self.path(); git::git_add_all(path)?; git::git_reset_hard(path) } /// Get a list of changed files as raw output. /// This output is directly from git, is not processed, and is not stable. /// /// If the list is empty, an empty string is returned. pub fn changed_files_raw(&self, short: bool) -> Result<String> { let path = self.path(); let mut status = git::git_status(path, short)?; // If empty when trimmed, wipe completely if status.trim().is_empty() { status.truncate(0); } Ok(status) } } /// Defines readyness of store sync. /// /// Some states block sync usage, including: /// - Sync not initialized /// - Git repository is dirty #[derive(Debug, Eq, PartialEq)] pub enum Readyness { /// Sync is not initialized for this store. NoSync, /// Special repository state. RepoState(git::RepositoryState), /// Repository is dirty (has uncommitted changes). Dirty, /// Ready to sync. Ready, } impl Readyness { /// Check if ready. pub fn is_ready(&self) -> bool { matches!(self, Self::Ready) } } /// Check if repository is dirty. /// /// Repository is dirty if it has any uncommitted changed. fn is_dirty(repo: &Path) -> Result<bool> { git::git_has_changes(repo) } /// Check whether we need to push to the remote. /// /// This defaults to true on error. fn safe_need_to_push(repo: &Path) -> bool { match need_to_push(repo) { Ok(push) => push, Err(err) => { eprintln!("failed to test if local branch is different than remote, ignoring: {err}",); true } } } /// Check whether we need to push to the remote. /// /// If the upstream branch is unknown, this always returns true. fn need_to_push(repo: &Path) -> Result<bool> { // If last pull is outdated, always push let last_pulled = git::git_last_pull_time(repo)?; if last_pulled.elapsed()? > GIT_PULL_OUTDATED { return Ok(true); } // Get branch and upstream branch name let branch = git::git_current_branch(repo)?; let upstream = match git::git_branch_upstream(repo, &branch)? { Some(upstream) => upstream, None => return Ok(true), }; // Compare local and remote branch hashes Ok(git::git_ref_hash(repo, branch)? != git::git_ref_hash(repo, upstream)?) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/systemd_bin.rs�������������������������������������������������������������������0000664�0000000�0000000�00000012264�14713723046�0016747�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::ffi::OsStr; use std::process::{Command, ExitStatus, Stdio}; use anyhow::Result; use thiserror::Error; /// sudo binary. pub const SUDO_BIN: &str = "sudo"; /// systemd-run binary. pub const SYSTEMD_RUN_BIN: &str = "systemd-run"; /// systemctl binary. pub const SYSTEMCTL_BIN: &str = "systemctl"; /// Spawn systemd timer to run the given command. /// /// This may ask for root privileges through sudo. pub fn systemd_cmd_timer(time: u32, description: &str, unit: &str, cmd: &[&str]) -> Result<()> { // Remove unit first if it failed before let _ = systemd_remove_timer(unit); let _ = systemctl_reset_failed_timer(unit); // TODO: do not set -q flag if in verbose mode? let time = format!("{time}"); let mut systemd_cmd = vec![ "--quiet", "--system", "--on-active", &time, "--timer-property=AccuracySec=1s", "--description", description, "--unit", unit, "--", ]; systemd_cmd.extend(cmd); systemd_run(&systemd_cmd) } /// Reset a given failed unit. /// /// This errors if the given unit is unknown, or if it didn't fail. /// /// Because this involves timers, if this operation fails it will also internally try to do the /// same for the given unit with a `.timer` suffix. fn systemctl_reset_failed_timer(unit: &str) -> Result<()> { // Invoke command, collect result let result = cmd_systemctl(["--quiet", "--system", "reset-failed", unit]) .stderr(Stdio::null()) .status() .map_err(Err::Systemctl); // Do the same with .timer unit suffix, ensure one succeeds if unit.ends_with(".service") { let unit = format!("{}.timer", unit.trim_end_matches(".service")); systemctl_reset_failed_timer(&unit).or_else(|_| cmd_assert_status(result?)) } else { cmd_assert_status(result?) } } /// Check whether the given unit (transient timer) is running. /// /// This may ask for root privileges through sudo. /// /// Because this involves timers, if this operation fails it will also internally try to do the /// same for the given unit with a `.timer` suffix. pub fn systemd_has_timer(unit: &str) -> Result<bool> { // TODO: check whether we can optimize this, the status command may be expensive let cmd = cmd_systemctl(["--system", "--no-pager", "--quiet", "status", unit]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(Err::Systemctl)?; // Check special status codes match cmd.code() { Some(0) | Some(3) => Ok(true), Some(4) if unit.ends_with(".service") => { let unit = format!("{}.timer", unit.trim_end_matches(".service")); systemd_has_timer(&unit) } Some(4) => Ok(false), _ => cmd_assert_status(cmd).map(|_| false), } } /// Remove a systemd transient timer. /// /// Errors if the timer is not available. /// This may ask for root privileges through sudo. /// /// Because this involves timers, if this operation fails it will also internally try to do the /// same for the given unit with a `.timer` suffix. pub fn systemd_remove_timer(unit: &str) -> Result<()> { // Invoke command, collect result let result = cmd_systemctl(["--system", "--quiet", "stop", unit]) .stderr(Stdio::null()) .status() .map_err(Err::Systemctl); // Do the same with .timer unit suffix, ensure one succeeds if unit.ends_with(".service") { let unit = format!("{}.timer", unit.trim_end_matches(".service")); systemd_remove_timer(&unit).or_else(|_| cmd_assert_status(result?)) } else { cmd_assert_status(result?) } } /// Invoke a systemd-run command with the given arguments. fn systemd_run<I, S>(args: I) -> Result<()> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { cmd_assert_status(cmd_systemd_run(args).status().map_err(Err::SystemdRun)?) } // /// Invoke a systemctl command with the given arguments. // fn systemctl<I, S>(args: I) -> Result<()> // where // I: IntoIterator<Item = S>, // S: AsRef<OsStr>, // { // cmd_assert_status(cmd_systemctl(args).status().map_err(Err::Systemctl)?) // } /// Build a systemd-run command to run. fn cmd_systemd_run<I, S>(args: I) -> Command where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let mut cmd = Command::new(SUDO_BIN); cmd.arg("--"); cmd.arg(SYSTEMD_RUN_BIN); cmd.args(args); cmd } /// Build a systemctl command to run. fn cmd_systemctl<I, S>(args: I) -> Command where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { let mut cmd = Command::new(SUDO_BIN); cmd.arg("--"); cmd.arg(SYSTEMCTL_BIN); cmd.args(args); cmd } /// Assert the exit status of a command. /// /// Returns error is status is not succesful. fn cmd_assert_status(status: ExitStatus) -> Result<()> { if !status.success() { return Err(Err::Status(status).into()); } Ok(()) } #[derive(Debug, Error)] pub enum Err { #[error("failed to invoke systemd-run command")] SystemdRun(#[source] std::io::Error), #[error("failed to invoke systemctl command")] Systemctl(#[source] std::io::Error), #[error("systemd exited with non-zero status code: {0}")] Status(std::process::ExitStatus), } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/tomb.rs��������������������������������������������������������������������������0000664�0000000�0000000�00000036454�14713723046�0015377�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Password store Tomb functionality. use std::env; use std::os::linux::fs::MetadataExt; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; use thiserror::Error; use crate::crypto::Proto; pub use crate::tomb_bin::TombSettings; use crate::util; use crate::{systemd_bin, tomb_bin, Key, Store}; /// Default time after which to automatically close the password tomb. pub const TOMB_AUTO_CLOSE_SEC: u32 = 5 * 60; /// Common tomb file suffix. pub const TOMB_FILE_SUFFIX: &str = ".tomb"; /// Common tomb key file suffix. pub const TOMB_KEY_FILE_SUFFIX: &str = ".tomb.key"; /// Name of SSH client process. pub const SSH_PROCESS_NAME: &str = "ssh"; /// Tomb helper for given store. pub struct Tomb<'a> { /// The store. store: &'a Store, /// Tomb settings. pub settings: TombSettings, } impl<'a> Tomb<'a> { /// Construct new Tomb helper for given store. pub fn new(store: &'a Store, quiet: bool, verbose: bool, force: bool) -> Tomb<'a> { Self { store, settings: TombSettings { quiet, verbose, force, }, } } /// Find the tomb path. /// /// Errors if it cannot be found. pub fn find_tomb_path(&self) -> Result<PathBuf> { find_tomb_path(&self.store.root).ok_or_else(|| Err::CannotFindTomb.into()) } /// Find the tomb key path. /// /// Errors if it cannot be found. pub fn find_tomb_key_path(&self) -> Result<PathBuf> { find_tomb_key_path(&self.store.root).ok_or_else(|| Err::CannotFindTombKey.into()) } /// Open the tomb. /// /// This will keep the tomb open until it is manually closed. See `start_timer()`. /// /// On success this may return a list with soft-fail errors. pub fn open(&self) -> Result<Vec<Err>> { // Open tomb let tomb = self.find_tomb_path()?; let key = self.find_tomb_key_path()?; tomb_bin::tomb_open(&tomb, &key, &self.store.root, None, self.settings) .map_err(Err::Open)?; // Soft fail on following errors, collect them let mut errs = vec![]; // Change mountpoint directory permissions to current user if let Err(err) = util::fs::sudo_chown_current_user(&self.store.root, false).map_err(Err::Chown) { errs.push(err); } Ok(errs) } /// Resize the tomb. /// /// The Tomb must not be mounted and the size must be larger than the current. pub fn resize(&self, mbs: u32) -> Result<()> { let tomb = self.find_tomb_path()?; let key = self.find_tomb_key_path()?; tomb_bin::tomb_resize(&tomb, &key, mbs, self.settings).map_err(Err::Resize)?; Ok(()) } /// Close the tomb. pub fn close(&self) -> Result<()> { let tomb = self.find_tomb_path()?; // Kill SSH clients that still have a persistent session open for this store util::git::kill_ssh_by_session(self.store); tomb_bin::tomb_close(&tomb, self.settings).map_err(Err::Close)?; Ok(()) } /// Prepare a Tomb store for usage. /// /// - If this store is a Tomb, the tomb is opened. pub fn prepare(&self) -> Result<()> { // TODO: return error if dirty? // Skip if not a tomb if !self.is_tomb() { return Ok(()); } // Skip if already open if self.is_open()? { return Ok(()); } if !self.settings.quiet { eprintln!("Opening password store Tomb..."); } // Open tomb, set up auto close timer self.open().map_err(Err::Prepare)?; self.start_timer(TOMB_AUTO_CLOSE_SEC, false) .map_err(Err::Prepare)?; eprintln!(); if self.settings.verbose { eprintln!("Opened password store, automatically closing in 5 seconds"); } Ok(()) } /// Set up a timer to automatically close password store tomb. /// /// TODO: add support for non-systemd systems pub fn start_timer(&self, sec: u32, force: bool) -> Result<()> { // Figure out tomb path and name let tomb_path = self.find_tomb_path()?; let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap"); let unit = format!("prs-tomb-close@{name}.service"); // Skip if already running if !force && systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? { return Ok(()); } // Spawn timer to automatically close tomb // TODO: better method to find current exe path // TODO: do not hardcode exe, command and store path systemd_bin::systemd_cmd_timer( sec, "prs tomb close timer", &unit, &[ std::env::current_exe() .expect("failed to determine current exe") .to_str() .expect("current exe contains invalid UTF-8"), "tomb", "--store", self.store .root .to_str() .expect("password store path contains invalid UTF-8"), "close", "--try", "--verbose", ], ) .map_err(Err::AutoCloseTimer)?; Ok(()) } /// Check whether the timer is running. pub fn has_timer(&self) -> Result<bool> { // Figure out tomb path and name let tomb_path = self.find_tomb_path()?; let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap"); let unit = format!("prs-tomb-close@{name}.service"); systemd_bin::systemd_has_timer(&unit).map_err(|err| Err::AutoCloseTimer(err).into()) } /// Stop automatic close timer if any is running. pub fn stop_timer(&self) -> Result<()> { // Figure out tomb path and name let tomb_path = self.find_tomb_path()?; let name = tomb_bin::name(&tomb_path).unwrap_or(".unwrap"); let unit = format!("prs-tomb-close@{name}.service"); // We're done if none is running if !systemd_bin::systemd_has_timer(&unit).map_err(Err::AutoCloseTimer)? { return Ok(()); } systemd_bin::systemd_remove_timer(&unit).map_err(Err::AutoCloseTimer)?; Ok(()) } /// Finalize the Tomb. pub fn finalize(&self) -> Result<()> { // This is currently just a placeholder for special closing functionality in the future Ok(()) } /// Initialize tomb. /// /// `mbs` is the size in megabytes. /// /// The given GPG key is used to encrypt the Tomb key with. /// /// # Panics /// /// Panics if given key is not a GPG key. pub fn init(&self, key: &Key, mbs: u32) -> Result<()> { // Assert key is GPG assert_eq!(key.proto(), Proto::Gpg, "key for Tomb is not a GPG key"); // TODO: map errors // TODO: we need these paths even though tomb does not exist yet let tomb_file = tomb_paths(&self.store.root).first().unwrap().to_owned(); let key_file = tomb_key_paths(&self.store.root).first().unwrap().to_owned(); let store_tmp_dir = util::fs::append_file_name(&self.store.root, ".tomb-init").map_err(Err::Init)?; // Dig tomb, forge key, lock tomb with key, open tomb tomb_bin::tomb_dig(&tomb_file, mbs, self.settings).map_err(Err::Init)?; tomb_bin::tomb_forge(&key_file, key, self.settings).map_err(Err::Init)?; tomb_bin::tomb_lock(&tomb_file, &key_file, key, self.settings).map_err(Err::Init)?; tomb_bin::tomb_open( &tomb_file, &key_file, &store_tmp_dir, Some(key), self.settings, ) .map_err(Err::Init)?; // Change temporary mountpoint directory permissions to current user util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?; // Copy password store contents util::fs::copy_dir_contents(&self.store.root, &store_tmp_dir).map_err(Err::Init)?; // Close tomb tomb_bin::tomb_close(&tomb_file, self.settings).map_err(Err::Init)?; util::fs::sudo_chown_current_user(&store_tmp_dir, true).map_err(Err::Chown)?; // Remove both main and temporary store fs_extra::dir::remove(&self.store.root).map_err(|err| Err::Init(anyhow!(err)))?; fs_extra::dir::remove(&store_tmp_dir).map_err(|err| Err::Init(anyhow!(err)))?; // Open tomb as regular // TODO: do something with Ok(errors)? self.open()?; Ok(()) } /// Check whether the password store is a tomb. /// /// This guesses based on existence of some files. /// If this returns false you may assume this password store doesn't use a tomb. pub fn is_tomb(&self) -> bool { find_tomb_path(&self.store.root).is_some() } /// Check whether the password store is currently opened. /// /// This guesses based on mount information for the password store directory. pub fn is_open(&self) -> Result<bool> { // Password store directory must exist if !self.store.root.is_dir() { return Ok(false); } // If device ID of store dir and it's parent differ we can assume it is mounted if let Some(parent) = self.store.root.parent() { let meta_root = self.store.root.metadata().map_err(Err::OpenCheck)?; let meta_parent = parent.metadata().map_err(Err::OpenCheck)?; return Ok(meta_root.st_dev() != meta_parent.st_dev()); } // TODO: do extensive mount check here Ok(false) } /// Fetch Tomb size statistics. /// /// This attempts to gather password store and tomb size statistics, whether this store is a /// tomb or not. /// /// This is expensive. pub fn fetch_size_stats(&self) -> Result<TombSize> { // Get sizes depending on whether this store uses a tomb match self.find_tomb_path() { Ok(tomb_path) => { let store = if self.is_open().unwrap_or(false) { util::fs::dir_size(&self.store.root).ok() } else { None }; let tomb_file = tomb_path.metadata().map(|m| m.len()).ok(); Ok(TombSize { store, tomb_file }) } Err(_) => Ok(TombSize { store: util::fs::dir_size(&self.store.root).ok(), tomb_file: None, }), } } } /// Slam all open tombs. /// /// Warning: this may be dangerous and could have unwanted side effects. This also closes /// non-password Tombs and kills all programs using it. pub fn slam(settings: TombSettings) -> Result<()> { tomb_bin::tomb_slam(settings).map_err(Err::Slam)?; Ok(()) } /// Holds information for password store Tomb sizes. #[derive(Debug, Copy, Clone)] pub struct TombSize { /// Store directory. pub store: Option<u64>, /// Tomb file size. pub tomb_file: Option<u64>, } impl TombSize { /// Get Tomb file size in MBs. pub fn tomb_file_size_mbs(&self) -> Option<u32> { self.tomb_file.map(|s| (s / 1024 / 1024) as u32) } /// Get the desired Tomb size in megabytes based on the current state. /// /// Currently twice the password store size, defaults to minimum of 10. pub fn desired_tomb_size(&self) -> u32 { self.store .map(|bytes| ((bytes * 3) / 1024 / 1024).max(10) as u32) .unwrap_or(10) } /// Determine whether the password store should be resized. pub fn should_resize(&self) -> bool { // TODO: determine this based on 'tomb list' output self.store .zip(self.tomb_file) .map(|(store, tomb_file)| store * 2 > tomb_file) .unwrap_or(false) } } #[derive(Debug, Error)] pub enum Err { #[error("failed to find tomb file for password store")] CannotFindTomb, #[error("failed to find tomb key file to unlock password store tomb")] CannotFindTombKey, #[error("failed to prepare password store tomb for usage")] Prepare(#[source] anyhow::Error), #[error("failed to initialize new password store tomb")] Init(#[source] anyhow::Error), #[error("failed to open password store tomb through tomb CLI")] Open(#[source] anyhow::Error), #[error("failed to close password store tomb through tomb CLI")] Close(#[source] anyhow::Error), #[error("failed to resize password store tomb through tomb CLI")] Resize(#[source] anyhow::Error), #[error("failed to slam all open tombs through tomb CLI")] Slam(#[source] anyhow::Error), #[error("failed to change permissions to current user for tomb mountpoint")] Chown(#[source] anyhow::Error), #[error("failed to check if password store tomb is opened")] OpenCheck(#[source] std::io::Error), #[error("failed to set up systemd timer to auto close password store tomb")] AutoCloseTimer(#[source] anyhow::Error), } /// Build list of probable tomb paths for given store root. fn tomb_paths(root: &Path) -> Vec<PathBuf> { let mut paths = Vec::with_capacity(4); // Get parent directory and file name let parent = root.parent(); let file_name = root.file_name().and_then(|n| n.to_str()); // Same path as store root with .tomb suffix if let (Some(parent), Some(file_name)) = (parent, file_name) { paths.push(parent.join(format!("{file_name}{TOMB_FILE_SUFFIX}"))); } // Path from pass-tomb in store parent and in home if let Some(parent) = parent { paths.push(parent.join(format!(".password{TOMB_FILE_SUFFIX}"))); } paths.push(format!("~/.password{TOMB_FILE_SUFFIX}").into()); paths } /// Find tomb path for given store root. /// /// Uses `PASSWORD_STORE_TOMB_FILE` if set. /// This does not guarantee that the returned path is an actual tomb file. /// This is a best effort search. fn find_tomb_path(root: &Path) -> Option<PathBuf> { // Take path from environment variable if let Ok(path) = env::var("PASSWORD_STORE_TOMB_FILE") { return Some(path.into()); } // TODO: ensure file is large enough to be a tomb (tomb be at least 10 MB) tomb_paths(root).into_iter().find(|p| p.is_file()) } /// Build list of probable tomb key paths for given store root. fn tomb_key_paths(root: &Path) -> Vec<PathBuf> { let mut paths = Vec::with_capacity(4); // Get parent directory and file name let parent = root.parent(); let file_name = root.file_name().and_then(|n| n.to_str()); // Same path as store root with .tomb suffix if let (Some(parent), Some(file_name)) = (parent, file_name) { paths.push(parent.join(format!("{file_name}{TOMB_KEY_FILE_SUFFIX}"))); } // Path from pass-tomb in store parent and in home if let Some(parent) = parent { paths.push(parent.join(format!(".password{TOMB_KEY_FILE_SUFFIX}"))); } paths.push(format!("~/.password{TOMB_KEY_FILE_SUFFIX}").into()); paths } /// Find tomb key path for given store root. /// /// Uses `PASSWORD_STORE_TOMB_KEY` if set. /// This does not guarantee that the returned path is an actual tomb key file. /// This is a best effort search. fn find_tomb_key_path(root: &Path) -> Option<PathBuf> { // Take path from environment variable if let Ok(path) = env::var("PASSWORD_STORE_TOMB_KEY") { return Some(path.into()); } tomb_key_paths(root).into_iter().find(|p| p.is_file()) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/tomb_bin.rs����������������������������������������������������������������������0000664�0000000�0000000�00000012377�14713723046�0016225�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::env; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, ExitStatus}; use anyhow::Result; use thiserror::Error; use crate::crypto::Key; use crate::util; /// Binary name. pub const BIN_NAME: &str = "tomb"; /// Invoke tomb dig. /// /// `mbs` is the size of the tomb to create in megabytes. pub fn tomb_dig(tomb_file: &Path, mbs: u32, settings: TombSettings) -> Result<()> { tomb( [ "dig", tomb_file .to_str() .expect("tomb path has invalid UTF-8 characters"), "-s", &format!("{mbs}"), ], settings, ) } /// Invoke tomb forge. pub fn tomb_forge(key_file: &Path, key: &Key, settings: TombSettings) -> Result<()> { tomb( [ "forge", key_file .to_str() .expect("tomb key path has invalid UTF-8 characters"), "-gr", &key.fingerprint(false), ], settings, ) } /// Invoke tomb lock. pub fn tomb_lock( tomb_file: &Path, key_file: &Path, key: &Key, settings: TombSettings, ) -> Result<()> { tomb( [ "lock", tomb_file .to_str() .expect("tomb path has invalid UTF-8 characters"), "-k", key_file .to_str() .expect("tomb key path has invalid UTF-8 characters"), "-gr", &key.fingerprint(false), ], settings, ) } /// Invoke tomb open. pub fn tomb_open( tomb_file: &Path, key_file: &Path, store_dir: &Path, key: Option<&Key>, settings: TombSettings, ) -> Result<()> { // Build command arguments list let key_fp = key.map(|key| key.fingerprint(false)); let mut args = vec![ "open", tomb_file .to_str() .expect("tomb path contains invalid UTF-8"), "-k", key_file .to_str() .expect("tomb key path contains invalid UTF-8"), "-p", ]; match &key_fp { Some(fp) => args.extend(&["-gr", fp]), None => args.extend(&["-g"]), } args.push( store_dir .to_str() .expect("password store directory path contains invalid UTF-8"), ); // TODO: ensure tomb file, key and store dir exist tomb(&args, settings) } /// Invoke tomb close. pub fn tomb_close(tomb_file: &Path, settings: TombSettings) -> Result<()> { tomb( ["close", name(tomb_file).expect("failed to get tomb name")], settings, ) } /// Invoke tomb slam. pub fn tomb_slam(settings: TombSettings) -> Result<()> { tomb(["slam"], settings) } /// Invoke tomb resize. pub fn tomb_resize( tomb_file: &Path, key_file: &Path, size_mb: u32, settings: TombSettings, ) -> Result<()> { tomb( [ "resize", tomb_file .to_str() .expect("tomb path contains invalid UTF-8"), "-k", key_file .to_str() .expect("tomb key path contains invalid UTF-8"), "-g", "-s", &format!("{size_mb}"), ], settings, ) } /// Get tomb name based on path. pub fn name(path: &Path) -> Option<&str> { path.file_name()?.to_str()?.rsplitn(2, '.').last() } /// Invoke a tomb command with the given arguments. /// /// The command will take over the user console for in/output. fn tomb<I, S>(args: I, settings: TombSettings) -> Result<()> where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { cmd_assert_status(cmd_tomb(args, settings).status().map_err(Err::Tomb)?) } /// Build a tomb command to run. fn cmd_tomb<I, S>(args: I, settings: TombSettings) -> Command where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { // Build base command let mut cmd = if let Ok(bin) = env::var("PASSWORD_STORE_TOMB") { Command::new(bin) } else { Command::new(BIN_NAME) }; // Explicitly set GPG_TTY on Wayland to current stdin if not set // Fixed GPG not showing pinentry prompt properly on Wayland // Issue: https://github.com/timvisee/prs/issues/8#issuecomment-871090949 if util::env::is_wayland() && !util::env::has_gpg_tty() { if let Some(tty) = util::tty::get_tty() { cmd.env("GPG_TTY", tty); } } // Set global flags, add arguments if settings.quiet { cmd.arg("-q"); } if settings.verbose { cmd.arg("-D"); } if settings.force { cmd.arg("-f"); } cmd.args(args); cmd } /// Assert the exit status of a command. /// /// Returns error is status is not succesful. fn cmd_assert_status(status: ExitStatus) -> Result<()> { if !status.success() { return Err(Err::Status(status).into()); } Ok(()) } /// Tomb command settings. #[derive(Copy, Clone)] pub struct TombSettings { /// Run in quiet (-q) mode. pub quiet: bool, /// Run in verbose (-D) mode. pub verbose: bool, /// Run with force (-f). pub force: bool, } #[derive(Debug, Error)] pub enum Err { #[error("failed to invoke tomb command")] Tomb(#[source] std::io::Error), #[error("tomb operation exited with non-zero status code: {0}")] Status(std::process::ExitStatus), } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/types.rs�������������������������������������������������������������������������0000664�0000000�0000000�00000034011�14713723046�0015565�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Secret plaintext and ciphertext types. use anyhow::Result; use secstr::SecVec; use thiserror::Error; use zeroize::Zeroize; /// Delimiter for properties. const PROPERTY_DELIMITER: char = ':'; /// Newline character(s) on this platform. #[cfg(not(windows))] pub const NEWLINE: &str = "\n"; #[cfg(windows)] pub const NEWLINE: &str = "\r\n"; /// Ciphertext. /// /// Wraps ciphertext bytes. This type is limited on purpose, to prevent accidentally leaking the /// ciphertext. Security properties are enforced by `secstr::SecVec`. pub struct Ciphertext(SecVec<u8>); impl Ciphertext { /// New empty ciphertext. pub fn empty() -> Self { vec![].into() } /// Get unsecure reference to inner data. /// /// # Warning /// /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care! /// /// The reference itself is safe to use and share. Data may be cloned from this reference /// though, when that happens we lose track of it and are unable to securely handle it in /// memory. You should clone `Ciphertext` instead. pub(crate) fn unsecure_ref(&self) -> &[u8] { self.0.unsecure() } } impl From<Vec<u8>> for Ciphertext { fn from(mut other: Vec<u8>) -> Ciphertext { // Explicit zeroing of unsecure buffer required let into = Ciphertext(other.to_vec().into()); other.zeroize(); into } } /// Plaintext. /// /// Wraps plaintext bytes. This type is limited on purpose, to prevent accidentally leaking the /// plaintext. Security properties are enforced by `secstr::SecVec`. #[derive(Clone, Eq, PartialEq)] pub struct Plaintext(SecVec<u8>); impl Plaintext { /// New empty plaintext. pub fn empty() -> Self { vec![].into() } /// Get unsecure reference to inner data. /// /// # Warning /// /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care! /// /// The reference itself is safe to use and share. Data may be cloned from this reference /// though, when that happens we lose track of it and are unable to securely handle it in /// memory. You should clone `Plaintext` instead. pub fn unsecure_ref(&self) -> &[u8] { self.0.unsecure() } /// Get the plaintext as UTF8 string. /// /// # Warning /// /// Unsecure because we cannot guarantee that the referenced data isn't cloned. Use with care! /// /// The reference itself is safe to use and share. Data may be cloned from this reference /// though, when that happens we lose track of it and are unable to securely handle it in /// memory. You should clone `Plaintext` instead. pub fn unsecure_to_str(&self) -> Result<&str, std::str::Utf8Error> { std::str::from_utf8(self.unsecure_ref()) } /// Get the first line of this secret as plaintext. /// /// Returns empty plaintext if there are no lines. pub fn first_line(&self) -> Result<Plaintext> { Ok(self .unsecure_to_str() .map_err(Err::Utf8)? .lines() .next() .map(|l| l.as_bytes().into()) .unwrap_or_else(Vec::new) .into()) } /// Get all lines execpt the first one. /// /// Returns empty plaintext if there are no lines. pub fn except_first_line(&self) -> Result<Plaintext> { Ok(self .unsecure_to_str() .map_err(Err::Utf8)? .lines() .skip(1) .collect::<Vec<&str>>() .join(NEWLINE) .into_bytes() .into()) } /// Get line with the given property. /// /// Returns line with the given property. The property prefix is removed, and only the trimmed /// value is returned. Returns an error if the property does not exist. /// /// This will never return the first line being the password. pub fn property(&self, property: &str) -> Result<Plaintext> { let property = property.trim().to_uppercase(); self.unsecure_to_str() .map_err(Err::Utf8)? .lines() .skip(1) .find_map(|line| { let mut parts = line.splitn(2, PROPERTY_DELIMITER); if parts.next().unwrap().trim().to_uppercase() == property { Some(parts.next().map(|value| value.trim()).unwrap_or("").into()) } else { None } }) .ok_or_else(|| Err::Property(property.to_lowercase()).into()) } /// Append other plaintext. /// /// Optionally adds platform newline. pub fn append(&mut self, other: Plaintext, newline: bool) { let mut data = self.unsecure_ref().to_vec(); if newline { data.extend_from_slice(NEWLINE.as_bytes()); } data.extend_from_slice(other.unsecure_ref()); self.0 = data.into(); } /// Check whether this plaintext is empty. /// /// - Empty if 0 bytes /// - Empty if bytes parsed as UTF-8 has trimmed length of 0 characters (ignored on encoding failure) pub fn is_empty(&self) -> bool { self.unsecure_ref().is_empty() || std::str::from_utf8(self.unsecure_ref()) .map(|s| s.trim().is_empty()) .unwrap_or(false) } } impl From<String> for Plaintext { fn from(mut other: String) -> Plaintext { // Explicit zeroing of unsecure buffer required let into = Plaintext(other.as_bytes().into()); other.zeroize(); into } } impl From<Vec<u8>> for Plaintext { fn from(mut other: Vec<u8>) -> Plaintext { // Explicit zeroing of unsecure buffer required let into = Plaintext(other.to_vec().into()); other.zeroize(); into } } impl From<&str> for Plaintext { fn from(s: &str) -> Self { Self(s.as_bytes().into()) } } /// A plaintext or ciphertext handling error. #[derive(Debug, Error)] pub enum Err { #[error("failed parse plaintext as UTF-8")] Utf8(#[source] std::str::Utf8Error), #[error("property '{}' does not exist in plaintext", _0)] Property(String), } #[cfg(test)] mod tests { use super::*; #[test] fn plaintext_empty() { let empty = Plaintext::empty(); assert!(empty.is_empty(), "empty plaintext should be empty"); } #[test] fn plaintext_is_empty() { // Test empty let mut plaintext = Plaintext::from(""); assert!(plaintext.is_empty(), "empty plaintext should be empty"); assert!( plaintext.unsecure_ref().is_empty(), "empty plaintext should be empty" ); // Test not empty plaintext.append(Plaintext::from("abc"), false); assert!(!plaintext.is_empty(), "empty plaintext should not be empty"); assert!( !plaintext.unsecure_ref().is_empty(), "empty plaintext should not be empty" ); } #[test] fn plaintext_first_line() { // (input, output) let set = vec![ ("", ""), ("\n", ""), ("abc", "abc"), ("abc\n", "abc"), ("abc\ndef\r\nghi", "abc"), ("abc\r\ndef\nghi", "abc"), ]; for (input, output) in set { assert_eq!( Plaintext::from(input) .first_line() .unwrap() .unsecure_to_str() .unwrap(), output, "first line of plaintext is incorrect", ); } } #[test] fn plaintext_except_first_line() { // (input, output) let set = vec![ ("", ""), ("\n", ""), ("abc", ""), ("abc\n", ""), ("abc\ndef\r\nghi", "def\nghi"), ("abc\r\ndef\nghi", "def\nghi"), ]; for (input, output) in set { assert_eq!( Plaintext::from(input) .except_first_line() .unwrap() .unsecure_to_str() .unwrap(), output, "first line of plaintext is incorrect", ); } } #[test] fn plaintext_append() { // Append to empty without newline let mut plaintext = Plaintext::empty(); plaintext.append(Plaintext::from("abc"), false); assert_eq!(plaintext.unsecure_to_str().unwrap(), "abc"); plaintext.append(Plaintext::from("def"), false); assert_eq!(plaintext.unsecure_to_str().unwrap(), "abcdef"); // Append to empty with newline let mut plaintext = Plaintext::empty(); plaintext.append(Plaintext::from("abc"), true); assert_eq!( plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"), "\nabc" ); plaintext.append(Plaintext::from("def"), true); assert_eq!( plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"), "\nabc\ndef" ); // Append empty to empty let mut plaintext = Plaintext::empty(); plaintext.append(Plaintext::empty(), false); assert!(plaintext.is_empty()); plaintext.append(Plaintext::empty(), true); assert_eq!( plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"), "\n" ); // Keep existing newlines let mut plaintext = Plaintext::from("\n\n"); plaintext.append(Plaintext::from("\n\n"), false); assert_eq!(plaintext.unsecure_to_str().unwrap(), "\n\n\n\n"); plaintext.append(Plaintext::from("\n\n"), true); assert_eq!( plaintext.unsecure_to_str().unwrap().replace("\r\n", "\n"), "\n\n\n\n\n\n\n" ); } #[quickcheck] fn plaintext_append_string(a: String, b: String, c: String) { // Appending lots of random stuff and parsing as string should never fail let mut plaintext = Plaintext::from(a); plaintext.append(Plaintext::from(b), false); plaintext.append(Plaintext::from(c), true); plaintext.unsecure_to_str().unwrap(); } #[test] fn plaintext_property() { // Never select property from first line, but do from others assert!( Plaintext::from("Name: abc").property("name").is_err(), "should never select property from first line" ); assert_eq!( Plaintext::from("Name: abc\nName: def") .property("name") .unwrap() .unsecure_to_str() .unwrap(), "def", "should select property value from all but the first line" ); // (input, property to select, output) #[rustfmt::skip] let set = vec![ // Nothing/empty ("", "", None), // Properties ("\nName: abc", "Name", Some("abc")), ("\n Name : abc ", "Name", Some("abc")), ("\nName: abc\nName: def", "Name", Some("abc")), ("\nName: abc\nMail: abc@example.com", "Mail", Some("abc@example.com")), ("\nName: abc\nMail: abc@example.com", "Name", Some("abc")), // Empty property ("\nEmpty:", "Empty", Some("")), ("\nEmpty: ", "Empty", Some("")), // Missing ("\nName: abc\nMail: abc@example.com", "missing", None), // Capitalization ("\nName: abc", "name", Some("abc")), ("\nName: abc", "NAME", Some("abc")), ("\nName: abc", "nAME", Some("abc")), ("\nNAME: abc", "name", Some("abc")), ("\nnAmE: abc", "name", Some("abc")), ("\nNAME: abc\nname: def", "name", Some("abc")), ]; for (input, property, output) in set { let val = Plaintext::from(input).property(property).ok(); if let Some(output) = output { assert_eq!( val.unwrap().unsecure_to_str().unwrap(), output, "incorrect property value", ); } else { assert!(val.is_none(), "no property should be selected",); } } } #[quickcheck] fn plaintext_must_zero_on_drop(plaintext: String) -> bool { // Skip all-zero/empty because we cannot reliably test if plaintext.len() < 16 || plaintext.bytes().all(|b| b == 0) { return true; } // Create plaintext, remember memory range and data, then drop plaintext let plaintext = Plaintext::from(plaintext); let must_not_match = plaintext.0.unsecure().to_vec(); let range = plaintext.0.unsecure().as_ptr_range(); drop(plaintext); // Retake same slice of memory that we've dropped let slice: &[u8] = unsafe { std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize) }; // Memory must have been explicitly zeroed, it must never be the same as before slice != must_not_match } #[test] fn ciphertext_empty() { let empty = Ciphertext::empty(); assert!( empty.unsecure_ref().is_empty(), "empty ciphertext should be empty" ); } #[quickcheck] fn ciphertext_must_zero_on_drop(ciphertext: Vec<u8>) -> bool { // Skip all-zero/empty because we cannot reliably test if ciphertext.len() < 16 || ciphertext.iter().all(|b| *b == 0) { return true; } // Create ciphertext, remember memory range and data, then drop ciphertext let ciphertext = Ciphertext::from(ciphertext); let must_not_match = ciphertext.0.unsecure().to_vec(); let range = ciphertext.0.unsecure().as_ptr_range(); drop(ciphertext); // Retake same slice of memory that we've dropped let slice: &[u8] = unsafe { std::slice::from_raw_parts(range.start, range.end as usize - range.start as usize) }; // Memory must have been explicitly zeroed, it must never be the same as before slice != must_not_match } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/util/����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0015031�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/util/env.rs����������������������������������������������������������������������0000664�0000000�0000000�00000000717�14713723046�0016174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::env; /// Check whether we're in a Wayland environment #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn is_wayland() -> bool { has_non_empty_env("WAYLAND_DISPLAY") } /// Check whether `GPG_TTY` is set. pub fn has_gpg_tty() -> bool { has_non_empty_env("GPG_TTY") } /// Check if an environment variable is set and is not empty. pub fn has_non_empty_env(env: &str) -> bool { env::var_os(env).map(|v| !v.is_empty()).unwrap_or(false) } �������������������������������������������������prs-v0.5.2/lib/src/util/fs.rs�����������������������������������������������������������������������0000664�0000000�0000000�00000006314�14713723046�0016013�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::path::{Path, PathBuf}; #[cfg(all(feature = "tomb", target_os = "linux"))] use std::process::{Command, Stdio}; use anyhow::Result; #[cfg(all(feature = "tomb", target_os = "linux"))] use fs_extra::dir::CopyOptions; use thiserror::Error; /// sudo binary. #[cfg(all(feature = "tomb", target_os = "linux"))] pub const SUDO_BIN: &str = crate::systemd_bin::SUDO_BIN; /// chown binary. pub const CHOWN_BIN: &str = "chown"; /// Calcualte directory size in bytes. #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn dir_size(path: &Path) -> Result<u64, Err> { fs_extra::dir::get_size(path).map_err(Err::DirSize) } /// Copy contents of one directory to another. /// /// This will only copy directory contents recursively. This will not copy the directory itself. #[cfg(all(feature = "tomb", target_os = "linux"))] pub fn copy_dir_contents(from: &Path, to: &Path) -> Result<()> { let mut options = CopyOptions::new(); options.overwrite = true; options.copy_inside = true; options.content_only = true; Ok(fs_extra::dir::copy(from, to, &options) .map(|_| ()) .map_err(Err::CopyDirContents)?) } /// Append a suffix to the filename of a path. /// /// Errors if the path parent or file name could not be determined. pub fn append_file_name(path: &Path, suffix: &str) -> Result<PathBuf> { Ok(path.parent().ok_or(Err::NoParent)?.join(format!( "{}{}", path.file_name().ok_or(Err::UnknownName)?.to_string_lossy(), suffix, ))) } /// Chown a path to the current process' with `sudo`. #[cfg(all(feature = "tomb", target_os = "linux"))] pub(crate) fn sudo_chown(path: &Path, uid: u32, gid: u32, recursive: bool) -> Result<()> { // Build command let mut cmd = Command::new(SUDO_BIN); cmd.stdin(Stdio::inherit()); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); cmd.arg("--"); cmd.arg(CHOWN_BIN); if recursive { cmd.arg("--recursive"); } cmd.arg(format!("{uid}:{gid}")); cmd.arg(path); // Invoke and handle status let status = cmd.status().map_err(Err::SudoChown)?; if status.success() { Ok(()) } else { Err(Err::Status(status).into()) } } /// Chown a path to the current process' UID/GID with `sudo`. #[cfg(all(feature = "tomb", target_os = "linux"))] pub(crate) fn sudo_chown_current_user(path: &Path, recursive: bool) -> Result<()> { sudo_chown( path, nix::unistd::Uid::effective().as_raw(), nix::unistd::Gid::effective().as_raw(), recursive, ) } #[derive(Debug, Error)] pub enum Err { #[cfg(all(feature = "tomb", target_os = "linux"))] #[error("failed to measure directory size")] DirSize(#[source] fs_extra::error::Error), #[error("failed to copy directory contents")] #[cfg(all(feature = "tomb", target_os = "linux"))] CopyDirContents(#[source] fs_extra::error::Error), #[error("failed to append suffix to file path, unknown parent")] NoParent, #[error("failed to append suffix to file path, unknown name")] UnknownName, #[error("failed to invoke 'sudo chown' on path")] SudoChown(std::io::Error), #[error("system command exited with non-zero status code: {0}")] Status(std::process::ExitStatus), } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/util/git.rs����������������������������������������������������������������������0000664�0000000�0000000�00000015737�14713723046�0016177�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::collections::HashMap; use std::env; #[cfg(unix)] use std::fs; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Mutex; use crate::git; #[cfg(unix)] use crate::Store; #[cfg(unix)] use ofiles::opath; /// Environment variable git uses to modify the ssh command. const GIT_ENV_SSH: &str = "GIT_SSH_COMMAND"; /// Custom ssh command for git. /// /// With this custom SSH command we enable SSH connection persistence for session reuse to make /// remote git operations much quicker for repositories using an SSH URL. This greatly improves prs /// sync speeds. /// /// This sets up a session file in the users `/tmp` directory. A timeout of 10 seconds is set to /// quickly abort a connection attempt if the persistent connection fails. const SSH_PERSIST_CMD: &str = "ssh -o 'ControlMaster auto' -o 'ControlPath /tmp/.prs-session--%r@%h:%p' -o 'ControlPersist 1h' -o 'ConnectTimeout 10'"; /// Directory for SSH persistent session files. #[cfg(unix)] pub(crate) const SSH_PERSIST_SESSION_FILE_DIR: &str = "/tmp"; /// Prefix for SSH persistent session files. #[cfg(unix)] pub(crate) const SSH_PERSIST_SESSION_FILE_PREFIX: &str = ".prs-session--"; /// A whitelist of SSH hosts that support connection persisting. const SSH_PERSIST_HOST_WHITELIST: [&str; 2] = ["github.com", "gitlab.com"]; lazy_static! { /// Cache for SSH connection persistence support guess. static ref SSH_PERSIST_GUESS_CACHE: Mutex<HashMap<PathBuf, bool>> = Mutex::new(HashMap::new()); } /// Configure given git command to use SSH connection persisting. /// /// `guess_ssh_connection_persist_support` should be used to guess whether this is supported. pub(crate) fn configure_ssh_persist(cmd: &mut Command) { cmd.env(self::GIT_ENV_SSH, self::SSH_PERSIST_CMD); } /// Guess whether SSH connection persistence is supported. /// /// This does a best effort to determine whether SSH connection persistence is supported. This is /// used to enable connection reuse. This internally caches the guess in the current process by /// repository path. /// /// - Disabled on non-Unix /// - Disabled if user set `GIT_SSH_COMMAND` /// - Requires all repository SSH remote hosts to be whitelisted /// /// Related: https://gitlab.com/timvisee/prs/-/issues/31 /// Related: https://github.com/timvisee/prs/issues/5#issuecomment-803940880 // TODO: make configurable, add current user ID to path pub(crate) fn guess_ssh_persist_support(repo: &Path) -> bool { // We must be using Unix, unreliable on Windows (and others?) if !cfg!(unix) { return false; } // User must not have set GIT_SSH_COMMAND variable if env::var_os(GIT_ENV_SSH).is_some() { return false; } // Get cached result if let Ok(guard) = (*SSH_PERSIST_GUESS_CACHE).lock() { if let Some(supported) = guard.get(repo) { return *supported; } } // Gather git remotes, assume not supported if no remote or error let remotes = match git::git_remote(repo) { Ok(remotes) if remotes.is_empty() => return false, Ok(remotes) => remotes, Err(_) => return false, }; // Get remote host bits, ensure we have all let ssh_uris: Vec<_> = remotes .iter() .filter_map(|remote| git::git_remote_get_url(repo, remote).ok()) .filter(|uri| !remote_is_http(uri)) .collect(); // Ensure all SSH URI hosts are part of whitelist, assume incompatible on error let supported = ssh_uris.iter().all(|uri| match ssh_uri_host(uri) { Some(host) => SSH_PERSIST_HOST_WHITELIST.contains(&host.to_lowercase().as_str()), None => false, }); // Cache result if let Ok(mut guard) = (*SSH_PERSIST_GUESS_CACHE).lock() { guard.insert(repo.to_path_buf(), supported); } supported } /// Check if given git remote URI is using HTTP(S) rather than SSH. fn remote_is_http(mut url: &str) -> bool { url = url.trim(); url.starts_with("http://") || url.starts_with("https://") } /// Grab the host bit of an SSH URI. /// /// This will do a best effort to grap the host bit of an SSH URI. If an HTTP(S) URL is given, or /// if the host bit could not be determined, `None` is returned. Note that this may not be very /// reliable. #[allow(clippy::manual_split_once, clippy::needless_splitn)] fn ssh_uri_host(mut uri: &str) -> Option<&str> { // Must not be a HTTP(S) URL if remote_is_http(uri) { return None; } // Strip any ssh prefix if let Some(stripped) = uri.strip_prefix("ssh://") { uri = stripped; } // Strip the URI until we're left with the host // TODO: this is potentially unreliable, improve this logic let before_slash = uri.splitn(2, '/').next().unwrap(); let after_at = before_slash.splitn(2, '@').last().unwrap(); let before_collon = after_at.splitn(2, ':').next().unwrap(); let uri = before_collon.trim(); // Ensure the host is at least 3 characters long if uri.len() >= 3 { Some(uri) } else { None } } /// Kill SSH clients that have an opened persistent session on a password store. /// /// Closing these is required to close any open Tomb mount. #[cfg(unix)] pub fn kill_ssh_by_session(store: &Store) { // If persistent SSH isn't used, we don't have to close sessions if !guess_ssh_persist_support(&store.root) { return; } // TODO: guess SSH session directory and file details from environment variable // Find prs persistent SSH session files let dir = match fs::read_dir(SSH_PERSIST_SESSION_FILE_DIR) { Ok(dir) => dir, Err(_) => return, }; let session_files = dir .flatten() .filter(|e| e.file_type().map(|t| t.is_socket()).unwrap_or(false)) .filter(|e| { e.file_name() .to_str() .map(|n| n.starts_with(SSH_PERSIST_SESSION_FILE_PREFIX)) .unwrap_or(false) }) .map(|e| e.path()); // For each session file, kill attached SSH clients session_files.for_each(|p| { // List PIDs having this session file open let pids = match opath(p) { Ok(pids) => pids, Err(_) => return, }; pids.into_iter() .map(Into::into) .filter(|pid: &u32| pid > &0 && pid < &(i32::MAX as u32)) .filter(|pid| { // Only handle ssh clients fs::read_to_string(format!("/proc/{pid}/cmdline")) .map(|cmdline| { let cmd = cmdline.split([' ', ':']).next().unwrap(); cmd.starts_with("ssh") }) .unwrap_or(true) }) .for_each(|pid| { if let Err(err) = nix::sys::signal::kill( nix::unistd::Pid::from_raw(pid as i32), Some(nix::sys::signal::Signal::SIGTERM), ) { eprintln!("Failed to kill persistent SSH client (pid: {pid}): {err}",); } }); }); } ���������������������������������prs-v0.5.2/lib/src/util/mod.rs����������������������������������������������������������������������0000664�0000000�0000000�00000000063�14713723046�0016155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pub mod env; pub mod fs; pub mod git; pub mod tty; �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/lib/src/util/tty.rs����������������������������������������������������������������������0000664�0000000�0000000�00000002513�14713723046�0016220�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::path::{Path, PathBuf}; /// Maximum symlink resolving depth. const SYMLINK_DEPTH_MAX: u8 = 31; /// Local TTY path. const LOCAL_TTY_PATH: &str = "/dev/stdin"; /// Get TTY path for this process. /// /// Returns `None` if not in a TTY. Always returns `None` if not Linux, FreeBSD or OpenBSD. pub fn get_tty() -> Option<PathBuf> { // None on unsupported platforms if cfg!(not(any( target_os = "linux", target_os = "freebsd", target_os = "openbsd", ))) { return None; } let path = PathBuf::from(LOCAL_TTY_PATH); resolve_symlink(&path, 0) } /// Resolve symlink to the final accessible path. /// /// Returns `None` if the given link could not be read (and `depth` is 0). /// /// # Panics /// /// Panics if a depth of `SYMLINK_DEPTH_MAX` is reached to prevent infinite loops. fn resolve_symlink(path: &Path, depth: u8) -> Option<PathBuf> { // Panic if we're getting too deep if depth >= SYMLINK_DEPTH_MAX { // TODO: do not panic, return last unique path or return error panic!("failed to resolve symlink because it is too deep, possible loop?"); } // Read symlink path, recursively find target match path.read_link() { Ok(path) => resolve_symlink(&path, depth + 1), Err(_) if depth == 0 => None, Err(_) => Some(path.into()), } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/���������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0014206�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/dmenu/���������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0015316�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/dmenu/prs-copy�������������������������������������������������������������������0000775�0000000�0000000�00000000170�14713723046�0017016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash secret=$(prs list --list | dmenu -p "secret" -i) [[ ! -z "$secret" ]] && prs copy "$secret" --no-interact ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/dmenu/prs-type�������������������������������������������������������������������0000775�0000000�0000000�00000000235�14713723046�0017027�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash secret=$(prs list --list | dmenu -p "secret" -i) [[ ! -z "$secret" ]] && xdotool type "$(prs show "$secret" --password --no-interact --quiet)" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/rofi/����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14713723046�0015145�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/rofi/prs-copy��������������������������������������������������������������������0000775�0000000�0000000�00000000176�14713723046�0016653�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash secret=$(prs list --list | rofi -p "secret" -dmenu -i) [[ ! -z "$secret" ]] && prs copy "$secret" --no-interact ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������prs-v0.5.2/scripts/rofi/prs-type��������������������������������������������������������������������0000775�0000000�0000000�00000000243�14713723046�0016655�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash secret=$(prs list --list | rofi -p "secret" -dmenu -i) [[ ! -z "$secret" ]] && xdotool type "$(prs show "$secret" --password --quiet --no-interact)" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������