[
  {
    "path": ".codeclimate.yml",
    "content": "version: \"2\"\n\nchecks:\n  argument-count:\n    config:\n      threshold: 4\n  complex-logic:\n    config:\n      threshold: 4\n  file-lines:\n    config:\n      threshold: 250\n  method-complexity:\n    config:\n      threshold: 16\n  method-count:\n    config:\n      threshold: 20\n  method-lines:\n    config:\n      threshold: 100\n  nested-control-flow:\n    config:\n      threshold: 4\n  return-statements:\n    config:\n      threshold: 4\n\nplugins:\n gofmt:\n   enabled: true\n golint:\n   enabled: true\n govet:\n   enabled: true\n   \nratings:\n paths:\n - \"**.go\"\n \nexclude_patterns:\n- \"vendor/\"\n- \"utils/notify/icon.go\"\n- \"**/*_test.go\"\n"
  },
  {
    "path": ".codecov.yml",
    "content": "coverage:\n  range: 40..90\n  round: nearest\n  precision: 2\n  status:\n    project:\n      default: on\n    patch:\n      default: off\n    changes:\n      default: off\nignore:\n  - \"vendor/\"\n"
  },
  {
    "path": ".errcheck.excl",
    "content": "fmt.Fprintf\nfmt.Fprintln\nfmt.Fprint\n"
  },
  {
    "path": ".gemini/settings.json",
    "content": "{\n    \"contextFileName\": \"AGENTS.md\"\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "CHANGELOG.md merge=union\n\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: dominikschulz\npatreon: gopass \ncustom: \"https://paypal.me/doschulz\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve gopass\n---\n\n### Summary\n<!--\nPlease provide a clear and concise description of what the bug is.\n-->\n\n### Steps To Reproduce\n<!--\nSteps to reproduce the problem\n-->\n\n### Expected behavior\n<!--\nA clear and concise description of what you expected to happen.\n-->\n\n### Environment\n<!--\nPlease complete the following information (see note below)\n-->\n\n- OS: [e.g. Mac OS X High Sierra, Ubuntu 18.04, Windows 10, ...]\n- OS version: [uname -a]\n- gopass Version: [gopass version]\n- Installation method: [e.g. from source, brew, gopass repo]\n\n<!--\n**PLEASE NOTE**\n\nThere is a package named gopass in the official Debian repository.\nThis package is not related to this project in any way. If you\ninstalled gopass from the Debian archives report any bugs in\nthe Debian BTS.\n-->\n\n### Additional context\n<!--\nAdd any other context about the problem here.\n-->\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "Refer to [AGENTS.md](../AGENTS.md) for detailed instructions on how to set up and use agents with gopass.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    open-pull-requests-limit: 15\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 120\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 60\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking an issue as stale\nstaleLabel: wontfix\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n# Set to true to ignore issues in a milestone (defaults to false)\nexemptMilestones: true\n"
  },
  {
    "path": ".github/workflows/autorelease.yml",
    "content": "# This is a basic workflow to help you get started with Actions\n\nname: release\n\n# Controls when the action will run. \non:\n  # Triggers the workflow on push or pull request events but only for the master branch\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  contents: read\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n        with:\n          egress-policy: audit\n\n      -\n        name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n      -\n        name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version-file: 'go.mod'\n\n      - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n        with:\n          path: ~/go/pkg/mod\n          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-\n      - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0\n      - uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0\n      # ubuntu is missing wixl https://github.com/actions/virtual-environments/issues/3857\n      -\n        name: \"Install GNOME msitools (wixl)\"\n        run: sudo apt update -qq && sudo apt install -qq -y wixl\n      -\n        name: Import GPG signing key\n        id: import_gpg\n        uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          passphrase: ${{ secrets.PASSPHRASE }}\n      -\n        name: Debug\n        run:  |\n          echo \"GPG ---------------------\"\n          echo \"fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}\"\n          echo \"keyid:       ${{ steps.import_gpg.outputs.keyid }}\"\n          echo \"name:        ${{ steps.import_gpg.outputs.name }}\"\n          echo \"email:       ${{ steps.import_gpg.outputs.email }}\"\n          echo \"Go env ------------------\"\n          pwd\n          echo ${HOME}\n          echo ${GITHUB_WORKSPACE}\n          echo ${GOPATH}\n          echo ${GOROOT}\n          env\n      -\n        name: Generate release-notes\n        run: |\n          go run helpers/changelog/main.go >../RELEASE_NOTES\n      -\n        name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0\n        with:\n          version: latest\n          args: release --release-notes=../RELEASE_NOTES\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n          GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}\n          GOPATH: /home/runner/go\n      -\n        name: \"Add Windows installer (msi) to release\"\n        run: |  # until https://github.com/goreleaser/goreleaser/issues/1295, disabled until #2038 is fixed\n          tag=\"${GITHUB_REF#refs/tags/}\"\n          version=${tag#v}\n          make msi\n          msi=dist/gopass-x64-windows-${version}.msi\n          gh release upload \"${tag}\" \"${msi}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n      -\n        name: \"Upload deb files to apt hosting\"\n        run: |\n          for D in dist/*.deb; do\n            curl -H\"X-Filename: ${D}\" -H\"X-Apikey: ${APIKEY}\" -XPOST --data-binary @$D https://packages.gopass.pw/repos/gopass/upload\n            curl -H\"X-Filename: ${D}\" -H\"X-Apikey: ${APIKEY}\" -XPOST --data-binary @$D https://packages.gopass.pw/repos/gopass-unstable/upload\n          done\n        env:\n          APIKEY: ${{ secrets.APT_APIKEY }}\n\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build gopass\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  linux:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go: ['1.25']\n    name: Go ${{ matrix.go }}\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n      with:\n        egress-policy: block\n        allowed-endpoints: >\n          github.com:443\n          objects.githubusercontent.com:443\n          proxy.golang.org:443\n          raw.githubusercontent.com:443\n          release-assets.githubusercontent.com:443\n          storage.googleapis.com:443\n          sum.golang.org:443\n          golang.org:443\n          go.dev:443\n          azure.archive.ubuntu.com:443\n          archive.ubuntu.com:443\n          security.ubuntu.com:443\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        fetch-depth: 0\n\n    - name: Set up Go\n      uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      with:\n        go-version: ${{ matrix.go }}\n    - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n      with:\n        path: ~/go/pkg/mod\n        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-go-\n    - name: Ubuntu Dependencies\n      run: sudo apt-get install --yes git gnupg\n    - run: git config --global user.name nobody\n    - run: git config --global user.email foo.bar@example.org\n\n    -\n      name: Debug\n      run:  |\n        echo \"Go env ------------------\"\n        pwd\n        echo ${HOME}\n        echo ${GITHUB_WORKSPACE}\n        echo ${GOPATH}\n        echo ${GOROOT}\n        env  \n\n    - name: Build and Unit Test\n      run: make gha-linux\n    - name: Integration Test\n      run: make test-integration\n\n  container:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n        with:\n          egress-policy: audit\n\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051\n        env:\n          IMAGE_NAME: ${{ github.repository }}\n        with:\n          images: ${{ env.IMAGE_NAME }}\n\n      - name: Build container image\n        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8\n        with:\n          context: .\n          push: false\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n  windows:\n    runs-on: windows-latest\n    defaults:\n      run:\n          shell: msys2 {0}\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n      with:\n        egress-policy: audit\n\n    - uses: msys2/setup-msys2@fb197b72ce45fb24f17bf3f807a388985654d1f2 # v2.29.0\n      with:\n        release: false\n        path-type: inherit\n        install: >-\n          base-devel\n          git\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        fetch-depth: 0\n\n    - name: Set up Go\n      uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      with:\n        go-version-file: 'go.mod'\n\n    - run: git config --global user.name nobody\n    - run: git config --global user.email foo.bar@example.org\n      \n    - name: Build and Unit Test\n      run: make gha-windows\n\n  macos:\n    runs-on: macos-latest\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n      with:\n        egress-policy: audit\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        fetch-depth: 0\n\n    - name: Set up Go\n      uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      with:\n        go-version-file: 'go.mod'\n\n    - run: git config --global user.name nobody\n    - run: git config --global user.email foo.bar@example.org\n      \n    - name: Build and Unit Test\n      run: make gha-osx\n      env:\n        SLOW_TEST_FACTOR: 100 \n\n  dependabot:\n    needs: [linux]\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n      contents: write\n    if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}\n    steps:\n      - id: metadata\n        uses: dependabot/fetch-metadata@v2\n        with:\n          github-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      - run: |\n          gh pr review --approve \"$PR_URL\"\n          gh pr merge --squash --auto \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches:\n      - master\n  schedule:\n    - cron: '19 21 * * 0'\n\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  analyze:\n    permissions:\n      actions: read  # for github/codeql-action/init to get workflow details\n      contents: read  # for actions/checkout to fetch code\n      security-events: write  # for github/codeql-action/autobuild to send a status report\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n      with:\n        disable-sudo: true\n        egress-policy: block\n        allowed-endpoints: >\n          api.github.com:443\n          github.com:443\n          objects.githubusercontent.com:443\n          release-assets.githubusercontent.com:443\n          proxy.golang.org:443\n          raw.githubusercontent.com:443\n          storage.googleapis.com:443\n          sum.golang.org:443\n\n    - name: Checkout repository\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5\n"
  },
  {
    "path": ".github/workflows/container.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\n# GitHub recommends pinning actions to a commit SHA.\n# To get a newer version, you will need to update the SHA.\n# You can also reference a tag or branch, but the action may change without warning.\n\nname: Create and publish a Docker image\n\n# Controls when the action will run. \non:\n push:\n   tags:\n     - 'v*'\n\npermissions:\n  contents: read\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n        with:\n          egress-policy: audit\n\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n  pull-requests: read\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n        with:\n          disable-sudo: true\n          egress-policy: block\n          allowed-endpoints: >\n            api.github.com:443\n            github.com:443\n            golangci-lint.run:443\n            objects.githubusercontent.com:443\n            release-assets.githubusercontent.com:443\n            proxy.golang.org:443\n            raw.githubusercontent.com:443\n            storage.googleapis.com:443\n            sum.golang.org:443\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version-file: 'go.mod'\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0\n        with:\n          # Note: there are 2 different version of golangci-lint used inside the project.\n          # https://github.com/gopasspw/gopass/blob/master/.github/workflows/build.yml#L65\n          # https://github.com/gopasspw/gopass/blob/master/.github/workflows/golangci-lint.yml#L46\n          # https://github.com/gopasspw/gopass/blob/master/Makefile#L136\n          version: v2.6.1 # we have a list of linters in our .golangci.yml config file\n          only-new-issues: true\n"
  },
  {
    "path": ".github/workflows/grype.yml",
    "content": "name: Scan gopass\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  linux:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0\n      with:\n        egress-policy: audit\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        fetch-depth: 0\n    - name: Set up Go\n      uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      with:\n        go-version-file: 'go.mod'\n\n    - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n      with:\n        path: ~/go/pkg/mod\n        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-go-\n    - name: Scan current project\n      uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2\n      with:\n        path: \".\"\n        fail-build: true\n        severity-cutoff: critical\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "# This workflow uses actions that are not certified by GitHub. They are provided\n# by a third-party and are governed by separate terms of service, privacy\n# policy, and support documentation.\n\nname: Scorecard supply-chain security\non:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n    - cron: '39 8 * * 2'\n  push:\n    branches: [ \"master\" ]\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n      # Uncomment the permissions below if installing in a private repository.\n      # contents: read\n      # actions: read\n\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n          # - you want to enable the Branch-Protection check on a *public* repository, or\n          # - you are installing Scorecard on a *private* repository\n          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.\n          # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".gitignore",
    "content": "gopass\ngopass-*-amd64\ngopass-full\ndev.sh\n!pkg/gopass/\ncoverage.out\ncoverage-all.*\n.vscode/\n\n# Profiling\n*.out\n\n# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\n# gopass specific ignores\n*.sublime-*\n*.swp\n/.env\n\n# package files\n*.deb\n*.pkg.tar.xz\n*.rpm\n*.tar.bz2\n\nreleases/\ndist/\n\nmanifest-*.json\n\n# go-fuzz\n*-fuzz.zip\nworkdir/\n\n.vscode/\nNOTICE.new\n\ndebian/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\noutput:\n  sort-order:\n    - linter\n    - file\nlinters:\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - containedctx\n    - copyloopvar\n    - cyclop\n    - decorder\n    - dogsled\n    - errchkjson\n    - errname\n    - errorlint\n    - exhaustive\n    - forcetypeassert\n    - funlen\n    - ginkgolinter\n    - gocheckcompilerdirectives\n    - gochecksumtype\n    - godot\n    - goheader\n    - gomoddirectives\n    - gomodguard\n    - goprintffuncname\n    - gosmopolitan\n    - grouper\n    - importas\n    - intrange\n    - loggercheck\n    - makezero\n    - mirror\n    - misspell\n    - nakedret\n    - nestif\n    - nilnil\n    - nlreturn\n    - nonamedreturns\n    - nosprintfhostport\n    - prealloc\n    - predeclared\n    - promlinter\n    - protogetter\n    - reassign\n    - sloglint\n    - spancheck\n    - tagalign\n    - testableexamples\n    - testifylint\n    - thelper\n    - unconvert\n    - usestdlibvars\n    - usetesting\n    - whitespace\n    - zerologlint\n  settings:\n    cyclop:\n      max-complexity: 24\n    errcheck:\n      exclude-functions:\n        - fmt.Fprint\n        - fmt.Fprintf\n        - fmt.Fprintln\n    funlen:\n      lines: -1\n      statements: 100\n    gocyclo:\n      min-complexity: 22\n    staticcheck:\n      checks:\n        - all\n        - -SA1019\n        - -ST1000\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - cyclop\n        path: (.+)_test\\.go\n      - linters:\n          - govet\n        path: (.+)_fuzz\\.go\n    paths:\n      - helpers/\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofmt\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - helpers/\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n# goreleaser.yml\n# Release automation\n#\n# Build customization\nproject_name: gopass\nversion: 2\n\nbefore:\n  hooks:\n    - make clean\n    - make completion\n    - go mod download\n\nbuilds:\n  - id: gopass\n    binary: gopass\n    flags:\n      - -trimpath\n      - -tags=netgo\n    env:\n      - CGO_ENABLED=0\n    asmflags:\n      - all=-trimpath={{.Env.HOME}}\n    gcflags:\n      - all=-trimpath={{.Env.HOME}}\n    ldflags: |\n      -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -extldflags '-static'\n    goos:\n      - darwin\n      - freebsd\n      - linux\n      - openbsd\n      - windows\n    goarch:\n      - amd64\n      - arm\n      - arm64\n    goarm:\n      - 6\n      - 7\n    mod_timestamp: '{{ .CommitTimestamp }}'\narchives:\n  - id: gopass\n    name_template: \"{{.Binary}}-{{.Version}}-{{.Os}}-{{.Arch}}{{ if .Arm }}v{{.Arm }}{{ end }}\"\n    formats: ['tar.gz']\n    format_overrides:\n      - goos: windows\n        formats: ['zip']\n    files:\n      - CHANGELOG.md\n      - LICENSE\n      - README.md\n      - bash.completion\n      - fish.completion\n      - zsh.completion\n\nrelease:\n  github:\n    owner: gopasspw\n    name: gopass\n  draft: false\n  prerelease: auto\n\nnfpms:\n  - id: gopass_deb\n    vendor: Gopass Authors\n    homepage: \"https://www.gopass.pw\"\n    maintainer: \"Gopass Authors <gopass@gopass.pw>\"\n    description: |-\n      gopass password manager - full featured CLI replacement for pass, designed for teams.\n       .\n      gopass is a simple but powerful password manager for your terminal. It is a\n      Pass implementation in Go that can be used as a drop in replacement.\n       .\n      Every secret lives inside of a gpg (or: age) encrypted textfile. These secrets\n      can be organized into meaninful hierachies and are by default versioned using\n      git.\n       .\n      This package contains the main gopass binary from gopass.pw. In Debian and\n      Ubuntu there is an unfortunate name clash with another gopass package. That is\n      completely different and not related to this package.\n    license: MIT\n    formats:\n      - deb\n    dependencies:\n      - git\n      - gnupg\n    recommends:\n      - rng-tools\n      - bash-completion\n    contents:\n      - src: gopass.1\n        dst: /usr/share/man/man1/gopass.1\n      - src: LICENSE\n        dst: /usr/share/doc/gopass/LICENSE\n      - src: CHANGELOG.md\n        dst: /usr/share/doc/gopass/CHANGELOG.md\n      - src: bash.completion\n        dst: /usr/share/bash-completion/completions/gopass\n      - src: fish.completion\n        dst: /usr/share/fish/vendor_completions.d/gopass.fish\n      - src: zsh.completion\n        dst: /usr/share/zsh/functions/Completion/Linux/_gopass\n  - id: gopass_rpm\n    vendor: Gopass Authors\n    homepage: \"https://www.gopass.pw\"\n    maintainer: \"Gopass Authors <gopass@gopass.pw>\"\n    description: |-\n      gopass password manager - full featured CLI replacement for pass, designed for teams.\n\n      gopass is a simple but powerful password manager for your terminal. It is a\n      Pass implementation in Go that can be used as a drop in replacement.\n\n      Every secret lives inside of a gpg (or: age) encrypted textfile. These secrets\n      can be organized into meaninful hierachies and are by default versioned using\n      git.\n    license: MIT\n    formats:\n      - rpm\n    dependencies:\n      - git\n      - gnupg2\n    recommends:\n      - rng-tools\n      - bash-completion\n    contents:\n      - src: gopass.1\n        dst: /usr/share/man/man1/gopass.1\n      - src: LICENSE\n        dst: /usr/share/doc/gopass/LICENSE\n      - src: CHANGELOG.md\n        dst: /usr/share/doc/gopass/CHANGELOG.md\n      - src: bash.completion\n        dst: /usr/share/bash-completion/completions/gopass\n      - src: fish.completion\n        dst: /usr/share/fish/vendor_completions.d/gopass.fish\n      - src: zsh.completion\n        dst: /usr/share/zsh/functions/Completion/Linux/_gopass\n\nsource:\n  enabled: true\n  name_template: \"{{.ProjectName}}-{{.Version}}\"\n  \nchecksum:\n  name_template: \"{{.ProjectName}}_{{.Version}}_SHA256SUMS\"\n\nmilestones:\n  -\n    repo:\n      owner: gopasspw\n      name: gopass\n    close: true\n    fail_on_error: false\n    name_template: \"{{ .Major }}.{{ .Minor }}.{{ .Patch }}\"\n\nsigns:\n  -\n    id: gopass\n    artifacts: checksum\n    args: [\"--batch\", \"-u\", \"{{ .Env.GPG_FINGERPRINT }}\", \"--armor\", \"--output\", \"${signature}\", \"--detach-sign\", \"${artifact}\"]\n\n# creates SBOMs of all archives and the source tarball using syft\n# https://goreleaser.com/customization/sbom\nsboms:\n  - artifacts: archive\n  - id: source # Two different sbom configurations need two different IDs\n    artifacts: source\n"
  },
  {
    "path": ".license-lint.yml",
    "content": "unrestricted_licenses:\n  - Apache-2.0\n  - MIT\n  - BSD-3-Clause\n  - BSD-2-Clause\n  - 0BSD\n  - WTFPL\n  - CC0-1.0\nreciprocal_licenses:\n  - MPL-2.0\n  - MPL-2.0-no-copyleft-exception\nallowlisted_modules:\n# Simplified BSD (BSD-2-Clause): https://github.com/russross/blackfriday/blob/master/LICENSE.txt\n- github.com/russross/blackfriday\n- github.com/russross/blackfriday/v2\n# Apache license\n- github.com/dgraph-io/ristretto\n- github.com/spf13/afero\n# Modified BSD-2-Clause with extra no-Google clause: https://github.com/jezek/xgb/blob/master/LICENSE\n- github.com/jezek/xgb\n# MIT\n- github.com/jwalton/go-supportscolor"
  },
  {
    "path": ".revive.toml",
    "content": "# Ignores files with \"GENERATED\" header, similar to golint\nignoreGeneratedHeader = false\n\n# Sets the default severity to \"warning\"\nseverity = \"error\"\n\n# Sets the default failure confidence. This means that linting errors\n# with less than 0.8 confidence will be ignored.\nconfidence = 0.6\n\n# Sets the error code for failures with severity \"error\"\nerrorCode = 1\n\n# Sets the error code for failures with severity \"warning\"\nwarningCode = 1\n\n[rule.argument-limit]\n  arguments = [10]\n[rule.blank-imports]\n[rule.context-as-argument]\n[rule.context-keys-type]\n[rule.cyclomatic]\n  arguments = [21]\n[rule.dot-imports]\n[rule.error-naming]\n[rule.error-return]\n[rule.error-strings]\n[rule.errorf]\n[rule.exported]\n[rule.if-return]\n[rule.increment-decrement]\n[rule.indent-error-flow]\n[rule.package-comments]\n[rule.range]\n[rule.receiver-naming]\n[rule.time-naming]\n[rule.unexported-return]\n[rule.var-declaration]\n[rule.var-naming]\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Project Overview\n\ngopass is a command line application that allows users to managed their passwords and other secrets inside encrypted files. Those files are usually encrypted using gpg (but other backends like age do exist). The files are usually managed using git (but other VCS backends exist as well). The CLI is primarily intended for human users.\n\nSeveral integration exist, these are stand alone projects that use the exposed gopass API to interact with an existing password store.\n\ngopass supports multiple password stores. It requires at least one root store but any number of additional stores can be mounted, just like filesystems on Linux, inside the root store. Each store can use a different encryption method and VCS.\n\nThe primary use case of using different password stores is to encrypt and share the content with a different set of recipients.\n\nThe project is specifically targeting users on all major platform, i.e. Linux, Unix, MacOS and Windows.\n\n## Folder Structure\n\n- `/docs`: Contains human readable documentation for the project.\n- `/helpers`: Contains tools used to maintain the project. Users usually don't use those, these are mainly for developers and maintainers of the project. Do not touch this directory unless instructed to do so.\n- `/internal`: Contains most of the implementation of the project. It is visibility restricted so other projects can not depend on it and we can be very liberal with breaking changes.\n- `/pkg`: Contains the public API (inside `/pkg/gopass`) used by our integrations and other projects as well as necessary support packages to make using the API feasible.\n- `/tests`: Contains only integration tests, i.e. those mock a real GPG-based gopass installation. They are quite slow but provide kind of a regression testing. Remember to add or adjust those when adding major new features.\n- `/internal/action`: Contains the different CLI subcommands. Usually one file per top-level subcommand (e.g. the implementation for `gopass ls` is in `/internal/action/list.go`) with an accompanying `_test.go` file that contains the unit tests. All commands need to be registered in `/internal/action/commands.go`.\n- `/internal/audit`: Contains the audit code that checks password stores for weak passwords or related issues.\n- `/internal/backend`: Contains the different backend implementations for both encryption as well as version controlled storage. Storage implementations need to register themselves in `/internal/backend.StorageRegistry` while encryption backends need to register in `/internal/backend.CryptoRegistry`.\n- `/internal/backend/crypto/age`: Contains the `age` encryption backend. It is a pure-Go implementation. Refer to the [docs](docs/backends/age.md) as well.\n- `/internal/backend/crypto/gpg/cli`: Contains the `gpg` encryption backend. It mostly uses the `gpg` binary to support the different configurations (e.g. smart cards) which wouldn't be possible with existing pure-Go implementation. Refer to [docs](docs/backends/gpg.md) as well.\n- `/internal/backend/crypto/plain`: Contains the plaintext backend (no encryption). This should only be used for testing. Users should never use this.\n- `/internal/backend/storage/fossilfs`: Contains an experimental storage backend using the Fossil SCM. It might be removed in the future.\n- `/internal/backend/storage/fs`: Contains a storage backend without SCM integration, i.e. it simply writes to files on disk without versioning support. Should usually only be used for tests or if users have some kind of transparent versioning system underneath.\n- `/internal/backend/storage/gitfs`: Contains the primary storage backend that is using `git` to manage files.\n- `/internal/config`: Contains our custom config handling. It is based on the git configuration file format as implemented by our [gitconfig](http://github.com/gopasspw/gitconfig) package. When reading config settings prefer to using `config.Bool(ctx, key)`, `config.String(ctx, key)` or `config.Int(ctx, key)`. Use the low-level methods only when those are not sufficient. Avoid touching the `legacy` package underneath unless asked to.\n- `/internal/out`: Contains our output helpers. Prefer those over Go standard lib packages (like fmt) for consistency.\n- `/internal/store`: Contains the core of the password store implementation (utilizing the configured backends).\n- `/internal/store/root`: Contains the root store. This always exist once in a gopass process. It delegates most operations to one or more leaf stores.\n- `/internal/store/leaf`: Contains the leaf store. There must be at least one initialized leaf store per gopass instance. But there can be as many as necessary.\n- `/pkg/appdir`: Contains a facility for providing system-dependentt paths for application resources, like config or cache directories. It does honor the `GOPASS_HOMEDIR` variable. This is very useful for testing since a gopass instance running with this variable set to a temporary location will not interfere with the actual production instance a user might be using.\n- `/pkg/clipboard`: Contains methods to interact with clipboards on all major operating systems. It is using our [clipboard](http://github.com/gopasspw/clipboard) package. It also supports clearing the clipboard after a given interval.\n- `/pkg/ctxutil`: Provides the necessary plumbling to interact with config values stored in the context. Avoid adding new context keys if possible and prefer config values. But if adding context keys is necessary they should only be defined in this file.\n- `/pkg/debug`: Contains a debug package with different verbosity levels. Use it to output debug information to a debug log.\n- `/pkg/fsutil`: Contains various helpers for interacting with the filesystem, e.g. checking for presence of files or directories. Prefer those over implementing these checks from scratch.\n- `/pkg/gopass`: Contains the public gopass API to interact with existing password stores. The `api` sub package contains the actual API and the `secrets` sub package the different secret types we support.\n- `/pkg/pwgen`: Contains a pure-Go implementation of the `pwgen` utility.\n- `/pkg/set`: Contains a generic set type.\n- `/pkg/tempfile`: Contains utility functions for creating and dealing with temp files. It attempts to be more secure than the normal temp file functions from the stdlib. Prefer those over the stdlib.\n- `/pkg/termio`: Contains functions for interacting with the user of the terminal.\n\n## Libraries and Frameworks\n\n- Avoid introducing new external dependencies unless absolutely necessary.\n- If a new dependency is required, please state the reason.\n- The project is licensed under the terms of the MIT license and we can only add compatible licenses. See [.license-lint.yml](.license-lint.yml) for a list of compatible licenses.\n- We must avoid introducing CGo dependencies since this make cross-compiling infeasible.\n\n## Testing instructions\n\n- Always run `make test` and `make codequality` before submitting.\n- Run `make fmt` to properly format the code. Run this before `make codequality`.\n- Before mailing a PR run `make test-integration`\n\n"
  },
  {
    "path": "ARCHITECTURE.md",
    "content": "# Architecture\n\nThis document describes the high-level architecture of gopass. If you want to\nget familiar with the code base you are in the right place.\n\n## Overview\n\nOn the highest level gopass manages directories (called `stores` or `mounts`)\nthat contain (mostly) GPG encrypted text files. gopass transparently handles\nencryption and decryption when accessing these files. It applies some heuristics\nto parse the file content and support certain operations on that content.\n\n`gopass` is licensed under the terms of the MIT license and we require\ncompatible licenses from our dependencies as well (when we link against them).\n\nFor licensing reasons and security considerations we try to keep the number of\nexternal dependencies (libraries) well-arranged. Try to avoid adding new\ndependencies unless absolutely necessary.\n\n## Generalized control flow\n\n![](docs/components.png)\n\nThis flow chart shows a high level control flow common to most operations.\nIt leaves out a lot of details but should give a better understanding how\ninformation flows within the program and where changes might be necessary.\n\n## Code Map\n\nThis section talks briefly about the various directories and some data\nstructures.\n\nWe're trying to clearly separate between our public API and implementation\ndetails. To that extent we're in the process of moving packages to `internal/`\n(and sometimes back to `pkg/`, if necessary).\n\nA note on semantic versioning: `gopass` is both an CLI and an API (Go module).\nThe expectations around semantic versioning and Go modules make it difficult\nto express both concerns in the same versioning scheme, e.g. does a breaking\nchange in the API require a major version bump even if nothing about the tool\n(CLI) has changed? What about the other way round? Thus we have decided to\napply semantic versioning only to the CLI tool, not the Go module. This is not\nideal and might change with sufficient active contributors.\n\n### `docs/backends`\n\nThis folder contains documentation about each of our supported backends. See\n`internal/backend` below for more information about our backend design.\n\n### `docs/commands`\n\nThis folder contains the specification of each sub command the tool offers.\nWe have many sub commands with sometimes dozens of flags each. In the past we\ndid encounter some inconsistencies and decided to introduce specifications for\neach command. If the specification and the implementation disagree this should\nbe reported as a bug and fixed or the specification needs to be changed (but the\ngeneral assumptions should be that the specification is correct, not the code).\n\n### `docs/usecases`\n\nThis directory contains an (incomplete) list of our core use cases, i.e. the\ncritical user journeys we aim to support. `gopass` can be used in various ways\nand try to remain flexible and extensible, but if we encounter a conflict\nbetween a blessed use case and a corner case we prefer the former.\n\n### `helpers/`\n\nThis directory contains some release automation tooling that is supposed to be\ninvoked with `go run`. The changelog generator in `helpers/changelog` is used\nby our GitHub Action based release automation and shouldn't be invoked manually.\n\nThe tooling in `helpers/release` will prepare a new release and helps to file a\nrelease pull request will all the required updates in place.\n\n### `internal/` and `pkg/`\n\n`gopass` used to not have either of these and all our packages were rooted\ndirectly in the repository. However we began to notice that other projects\nwere starting to depend directly on our internal packages and we sometimes\nbroke them. This put us and the other project into an unpleasant\nsituation so we tried to clarify the expectations by using Go's `internal/`\nvisibility rule to keep other projects from depending on our implementation\ndetails.\n\nNote: If we have a good reasons to use one of our `internal/` packages either\ncopy it (our license should rarely be an issue) or nicely ask us and explain\nwhy something should move to `pkg/`.\n\nAs we are in the process of formalizing a proper API surface we sometimes need\nto move packages from `internal/` to `pkg/`. The other direction might also\noccur, but much less often.\n\n### `internal/action`\n\nThis directory contains one file, and sometimes sub folders, for each command\n`gopass` supports. These are mostly self-contained, but some (e.g. show / edit\n/ find) need to depend on each other.\n\nTODO: There is a lot to be said about this package, e.g. custom errors.\n\n### `internal/backend`\n\n`gopass` is built around the concept of multiple independent password stores\nthat can be mounted into one namespace, much like regular file systems. Each\nof these stores can have a different storage and crypto backend. We used to\nhave independent revision control backends as well, but since the RCS (e.g.\ngit) interacts so closely with the storage (you can't use regular git w/o a\nfilesystem-based storage) we have merged storage and RCS backends.\n\nThe backend package defines the interfaces for the backend implementation\nand provides a registry that returns the concrete backend from the list of\nregistered ones. Registration happens through blank imports of either the\n`internal/backend/crypto` and `internal/backend/storage` packages.\n\nEach backend needs to have a loader implementation in its `loader.go` (please\nstick to this name). We try to auto-detect the most applicable backend when\ninitializing the process, but some backends look alike (e.g. a `fs` and an \nuninitialized `gitfs`). So the loader comes with a priority which is respected\nduring lookup.\n\n### `internal/config`\n\nThe `config` package implements support for a simple YAML-based configuration\nformat for `gopass`. Most of the code in this package is for backwards\ncompatibility. Whenever we introduce or remove a config option we need to\nintroduce a new fallback version that is automatically attempted when loading\na config file. To resolve ambiguities when parsing different config versions\nwe use a \"catch-all\" field to catch any unused keys and check that this is\nempty after parsing - otherwise we need to try a different config version.\n\nNOTE: We did support nested configurations for sub-stores but removed this\nbecause the maintenance cost did not justify the benefits of this feature.\n\n### `internal/cui`\n\nThe name `cui` is an abbreviation for `console-user-interface` and contains\nseveral helper functions to interact with humans over a text based interface.\n\nMost of these ask the user to select some item from a selection or provide\nsome input.\n\nNOTE: We used to support rich terminal UIs with arrow navigation and such.\nHowever all existing libraries that were available without CGO were either\nabandoned or buggy on some platforms and we didn't have any capacity to fix\nthem. So we had to remove support for this feature.\n\n### `internal/queue`\n\nThe `queue` package implements a FIFO queue that executes\nin the background. This allows for certain operations, like a git push, to be\ntaken out of the critical path wrt. user interactions. The queue will be fully\nprocessed before the process exits.\n\n### `internal/store/root`\n\nThe `root store` package implements an internal password store API that (only)\nsupports mounting `leaf` stores. It will forward (almost) all operations to\nits `leaf` stores (moves across stores being a notable exception) and do the\nnecessary manipulations of the affected path components (e.g. removing/adding\nthe mount prefix from the secret name as needed).\n\nThis package makes `gopass` multi-store capable.\n\n### `internal/store/leaf`\n\nThe `leaf store` package implements a password store that is mostly compatible\nwith any other password store implementation (while aiming for interoperability,\nnot at 100% feature parity). The low-level operations like filesystem and / or\nversion control and crypto operations are passed to the configured `storage`\nor `crypto` backend.\n\n### `internal/tree`\n\nThe `tree` package implements a simple tree structure that prints an output\nsimilar to the output of the Unix tool `tree`. It does support different\n`gopass` specific properties (like mounts or templates) not easily implemented\nwith other tree packages.\n\n### `internal/updater`\n\nThe `updater` package implements a secure and anonymous self updater.\n\nNote: The self updater contacts GitHub. If this is a concern one should use\nother sources, e.g. distro packages.\n\nIt retrieves the latest stable release from GitHub, fetches its metadata\nand verifies the signature against the built-in release signing keyring.\n\nIt tries to avoid conflicting with any `gopass` binary managed by the OS\nand refuse to update these.\n\n### `pkg/`\n\nThe package `pkg/` contains our public API surface, i.e. packages we want or\nhave to expose externally. Some packages (e.g. `otp`) are only exposed because\nthey are being used by some of our integrations. Others (e.g. `pinentry` or\n`pwgen`) are designed for wider use. We are considering to split some of the\nmore widely used packages into their own repositories to work better with\nGo module and semantic versioning expectations.\n\n#### `pkg/appdir`\n\nThe `appdir` package contains a set of [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)\ncompatible implementations with some `gopass` specifics. For testing purposes\nwe want to honor the setting of `GOPASS_HOMEDIR` before everything else, so our\nimplementation has to take this into account before following the XDG spec.\n\n#### `pkg/clipboard`\n\nThe `clipboard` package is a wrapper around a clipboard package that adds\nsupport for clearing the clipboard.\n\n#### `pkg/ctxutil`\n\nThe `ctxutil` is the pragmatic (read: non-idiomatic) approach to pass very\nspecific configuration options through multiple layers of abstraction. This is\narguably not the best design, but it works well and avoids bloated interfaces.\n\n#### `pkg/gopass`\n\nThis package contains **the** gopass API interface. We provide a concrete\nimplementation that should work with any properly initialized gopass setup\nand a mock for tests.\n\nThis package is designed as the main entry point for any integration that wants\nto integrate with gopass.\n\n### `tests`\n\n`gopass` comes with a comprehensive set of integration tests, i.e. tests that\nare executed by running a newly compiled gopass binary without access to any\nkind of internal state. These tests can't be as exhaustive as the unit tests\nbut they exist to ensure basic functionality aren't broken by a change.\n\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 1.16.1 / 2025-12-13\n\n* chore(deps): bump actions/checkout from 5.0.0 to 6.0.0 (#3299)\n* chore(deps): bump actions/setup-go from 6.0.0 to 6.1.0 (#3300)\n* chore(deps): bump anchore/sbom-action from 0.20.9 to 0.20.10 (#3296)\n* chore(deps): bump anchore/scan-action from 7.1.0 to 7.2.1 (#3298)\n* chore(deps): bump docker/metadata-action from 5.8.0 to 5.10.0 (#3297)\n* chore(deps): bump github/codeql-action from 4.31.2 to 4.31.6 (#3295)\n* chore(deps): bump golangci/golangci-lint-action from 9.0.0 to 9.1.0 (#3302)\n* chore(deps): bump step-security/harden-runner from 2.13.1 to 2.13.2 (#3301)\n* fix(config): use the config propery generate.strict as default value for Strict rules (#3303)\n* fix: Fix version check against latest release (#3292)\n\n## 1.16.0 / 2025-11-12\n\n* [BUGFIX] reorg: List all secrets instead of just top-level folders (#3245)\n* [chore] Add capability and vulnerability checks (#3266)\n* [chore] Initial fixes and added a warning for CryptFS and JJFS (#3270)\n* [chore] Logging improvements (#3273)\n* [chore] Run linux builds with multiple Go versions (#3272)\n* [fix] Correctly handle IsGitCommit false in store.Move (#3246)\n* [fix] Drop Go 1.23 (#3274)\n* [fix] Fix clipboard issues (#3267)\n* [fix] Fix version check (#3268)\n* chore(deps): bump actions/cache from 4.2.4 to 4.3.0 (#3263)\n* chore(deps): bump actions/setup-go from 5.5.0 to 6.0.0 (#3262)\n* chore(deps): bump actions/upload-artifact from 4.6.2 to 5.0.0 (#3281)\n* chore(deps): bump anchore/sbom-action from 0.20.5 to 0.20.6 (#3258)\n* chore(deps): bump anchore/sbom-action from 0.20.6 to 0.20.9 (#3284)\n* chore(deps): bump anchore/scan-action from 6.5.1 to 7.0.0 (#3264)\n* chore(deps): bump anchore/scan-action from 7.0.0 to 7.1.0 (#3280)\n* chore(deps): bump docker/login-action from 3.5.0 to 3.6.0 (#3260)\n* chore(deps): bump github/codeql-action from 3.30.0 to 3.30.5 (#3261)\n* chore(deps): bump github/codeql-action from 3.30.5 to 4.31.2 (#3282)\n* chore(deps): bump msys2/setup-msys2 from 2.28.0 to 2.29.0 (#3257)\n* chore(deps): bump ossf/scorecard-action from 2.4.2 to 2.4.3 (#3259)\n* chore(deps): bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#3283)\n* chore(deps): bump sigstore/cosign-installer from 3.9.2 to 3.10.0 (#3255)\n* chore(deps): bump step-security/harden-runner from 2.13.0 to 2.13.1 (#3256)\n* chore: Update golangci-lint (#3287)\n* docs: Add GoDoc to pkg and improve markdown files (#3251)\n* feat(age): Add unlock command to age agent (#3244)\n* feat: Add cryptfs storage backend for filename encryption (#3249)\n* feat: Clone remote on init (#3247)\n* fix: Fix release helper and update capabilities for caplos (#3288)\n\n## 1.15.18 / 2025-09-19\n\n* [fix] Enable Windows builders (#3237)\n* [fix] Fix recipient check error (#3235)\n* [fix] Update gitconfig to v0.0.3 to pull in Windows fixes (#3236)\n* [fix] Use Go 1.24 instead of Go 1.25 (#3226)\n* docs: Add note about pass compatibility (#3229)\n* feat: Add reorg command (#3232)\n* feat: Allow to customize commit messages (#3231)\n* feat: Improve usability of 'gopass mounts add' command (#3238)\n* fix(config): Make core.exportkeys handling consistent (#3228)\n* fix(gpg): Opportunistic key comparison on import (#3230)\n\n## 1.15.17 / 2025-09-15\n\n* [BUGFIX] Fix --force flag in recipients add (#3173)\n* [chore] Add tests and comments for hasPwRuleForSecret (#3162)\n* [chore] Automatically approve and merge dependabot PRs (#3220)\n* [chore] Bump github.com/gopasspw/clipboard to v0.0.3 (#3219)\n* [chore] Disable updating gopasspw.github.io (#3184)\n* [chore] Expose gopass env in help (#3158)\n* [chore] Fix hardened runner (#3196)\n* [chore] Update Go versions (#3139)\n* [chore] Update dependencies (#3197)\n* [feat] Add Jujutsu storage backend (#3202)\n* [feat] Honor generator options in the create workflow (#3149)\n* [fix] Add workaround for pre-release test failures (#3198)\n* [fix] Disable Windows tests (#3204)\n* [fix] Fixes creation template lookup on Windows (#3157)\n* [fix] avoid length prompt when input is within rule boundary (#3159)\n* [fix] skip redundant confirmation when --edit is used (#3161)\n* [fix] use WritePassword for secure write (#3200)\n* [testing] use `/usr/bin/env cat` instead of `/bin/cat` (#3160)\n\n## 1.15.16 / 2025-04-21\n\n* [BUGFIX] Allow use of trailing slash for cp/mv command (#3080)\n* [BUGFIX] Check if any usable key matches on clone (#3027)\n* [BUGFIX] Fixed max length check for strings in create/wizard (#3056)\n* [BUGFIX] Fixed password not saving to clipboard with safecontent and autoclip true (#3053)\n* [BUGFIX] replace return of wrong error variable (#3015)\n* [ENHANCEMENT] Add support for autocompletion with flags in REPL mode (#3057)\n* [ENHANCEMENT] Make it possible to override `show.autoclip` (#3082)\n* [FEATURE] Add option -r/--regex to find (#3083)\n* [UX] Make single store sync more intuitive / verbose (#3076)\n* [bugfix] Don't check for autosync on manual triggered sync (#3026) (#3029)\n* [chore] Add keep-sorted linter (#3130)\n* [chore] Add tpl func tests and fix two small issues (#3058)\n* [chore] Do not run linters twice (#3119)\n* [chore] Migrate goreleaser config to v2 (#3122)\n* [chore] Migrate to golangci-lint v2 (#3104)\n* [chore] Move gitconfig to their own repo (#3131)\n* [chore] Move set from internal to pkg (#3129)\n* [chore] Update dependencies (#3120)\n* [feat] Add conditional includes for gitconfig (#3128)\n* [feat] Add unconditional includes for gitconfig (#3127)\n* [feat] Remove expensive and unmaintained zxcvbn-go strength checker (#3133)\n* [feat] Replace clipboard library to support wl-copy args (#3123)\n* [fix] Add LICENSE, Changelog, manpage and shell completions to deb and (#3121)\n* [fix] Fix a flaky test (#3137)\n* [fix] Fix debug.ModuleVersion (#3079)\n* [fix] Fix test failure due to ambient variables (#3135)\n* [fix] Fix test regressions (#3116)\n* [fix] Fix this annoying test\n* [fix] Include git commit hash in tarballs (#3124)\n* [fix] Relase fixes (#3136)\n* [fix] Update Makefile and fix lint violations (#3134)\n\n## 1.15.15 / 2024-11-24\n\n* [BUGFIX] Replace ~ with user homedir if `$GOPASS_HOMEDIR` is not set (#2961)\n* [CLEANUP] Replace experimental `maps` and `slices` with stdlib (#2993)\n* [CLEANUP] remove unreachable code (#2977)\n* [DEPRECATION] Remove references to deprecated rand.Seed (#2953)\n* [ENHANCEMENT] Allow for whitespace-trailing passwords (#2873) (#2954)\n* [FEATURE] Adding support for `age.Plugin` identities (#2960)\n* [FEATURE] Allow for non-interactive age setup (#2970)\n* [FEATURE] Ask for setup if not initialized (#2975)\n* [bugfix] Copy with trailing slash at destination. (#2966)\n* [chore] use the same version of golangci-lint (#2948)\n\n## 1.15.14 / 2024-08-03\n\n* [bugfix] Fix parsing of key-value pairs according to the gitconfig (#2911)\n* [chore] Update dependency to github.com/cenkalti/backoff/v4 (#2864)\n* [chore] Update dependency to github.com/godbus/dbus/v5 (#2860)\n* [chore] Update dependency to github.com/google/go-github/v61 (#2863)\n* [chore] Update dependency to github.com/xhit/go-str2duration/v2 (#2865)\n* [chore] Update hashicorp/golang-lru to v2 (#2859)\n\n## 1.15.13 / 2024-04-06\n\n* [bugfix] Default to true for core.exportkeys even in substores (#2848)\n* [bugfix] Do not report findings with severity none in audit summary (#2843)\n* [bugfix] Fix loading of git configs (#2849)\n* [chore] Update dependencies (#2850)\n* [chore] Use clean filepath in all of the fs.Set operation (#2846)\n* [chore] use the same version of golangci-lint (#2841)\n* [feat] Add an multi-line input type to the create wizard (#2847)\n* [feat] Add option to disable notification icon (#2845)\n* [feat] Add verbosity levels to the debug package (#2851)\n* [fix] Disble safecontent parsing if noparsing is requested (#2855)\n* [fix] Pass remote, if given, to local init as well (#2852)\n\n## 1.15.12 / 2024-03-17\n\n* [BUGFIX] Use 'en' as default language for the xkcd generator (#2793)\n* [DOCUMENTATION] Fix typo: initilize -> initialize (#2796)\n* [bugfix] Bring back audit summary (#2820)\n* [bugfix] Do not abort saving if the OTP counter is aborted (#2775)\n* [bugfix] Fix NPE when using recipients completion (#2823)\n* [bugfix] Warn if trying to use fscopy inside the store (#2832)\n* [chore] Upgrade to Go 1.22 (#2805)\n* [cleanup] Add better logging in case no owner key is found (#2748)\n* [feat] Add .gopass-audit-ignore support to ignore secrets from audits (#2822)\n* [feat] Allow supression of password generation in create templates (#2821)\n* [ux] Add hint that computing recipients takes some time (#2833)\n* [ux] Do not show create type chooser if only one exists (#2752)\n\n## 1.15.11 / 2023-12-01\n\n* [bugfix] Disable multi-line description for deb packages (#2729)\n* [bugfix] Fix writes to global config from tests (#2727)\n* [bugfix] Workaround for goreleaser/nfpm#742 (#2732)\n* [feature] Allow setting autosync.interval in different time units (#2731)\n\n## 1.15.10 / 2023-11-25\n\n* [BUGFIX] Allow to move shadowed entries into their own folder (#2718)\n* [BUGFIX] Try to always honor local config for mounts (#2724)\n* [chore] Add OSSF scorecard link and improve security posture (#2704)\n* [chore] Update goxkcdpwgen dependency to include my PR (#2722)\n* [chore] Update grype workflow and pin Docker base images (#2706)\n* [cleanup] Add package description (#2702)\n* [feature] Add new pwgen options to capitalize and include numbers in (#2703)\n\n## 1.15.9 / 2023-11-18\n\n* [BUGFIX] Disabling the OTP snip screenshot feature on OpenBSD (#2685)\n* [CLEANUP] Migration of options to more appropriate sections (#2681)\n* [bugfix] Improve git version parsing (#2690)\n* [bugfix] Remove leading and trailing slashes from mounts (#2698)\n* [enhancement] Add blake3 to the template functions (#2693)\n* [enhancement] Add input validation to block illegal mount points (#2672)\n\n## 1.15.8 / 2023-09-11\n\n* [BUGFIX] Use goreleaser build for crosscompile (#2635)\n* [bugfix] Allow fsck to check a single secret (#2659)\n* [bugfix] Do not remove unused keys on import by default (#2657)\n* [bugfix] Fix parsing of large secrets (#2654)\n* [chore] Update dependencies (#2660)\n* [docs] add/update choco, scoop, winget instructions (#2647)\n* [feat] Add --store option to gopass fsck (#2658)\n* [feat] Add XCKD pwgen config options (#2651)\n\n## 1.15.7 / 2023-08-04\n\n* [BUGFIX] Fix build issues on various non-Linux platforms (#2630, #2633)\n\n## 1.15.6 / 2023-07-30\n\n* [DOCUMENTATION] fix Arch Linux package url (#2598)\n* [BUGFIX] Only show desktop notifications if there are changes (#2627)\n* [ENHANCEMENT] Add a global nosync flag (#2626)\n* [BUGFIX] Correctly handle multiline secrets (#2625)\n* [ENHANCEMENT] Add screen parsing for OTP QR codes (#2597)\n\n## 1.15.5 / 2023-04-07\n\n* [CLEANUP] Use Go1.20 (#2567)\n* [ENHANCEMENT] Add internal pager (ov). (#2510)\n\n## 1.15.4 / 2023-02-12\n\n* [BUGFIX] Also accept lower case CTE headers. (#2539, #2518)\n* [BUGFIX] Commit changes to mount config changes. (#2542, #2530)\n* [BUGFIX] Do not restrict pwlen when maxlen is zero. (#2537, #2536)\n* [BUGFIX] Fix fossilfs sync (#2549, #2516)\n* [BUGFIX] Fix recipients check for age. (#2545, #2544)\n* [BUGFIX] Hide harmless git error messages. (#2547, #2543)\n* [BUGFIX] Improve error handling for gopass convert (#2548, #2520)\n* [ENHANCEMENT] Add edit.auto-create (#2538)\n\n## 1.15.3 / 2023-01-07\n\n* [BUGFIX] Check recipients before launching editor. (#2488, #1565)\n* [BUGFIX] Fix possible concurrency issues in fsck. (#2486, #2459)\n* [BUGFIX] Honor core.autosync (#2497, #2495)\n* [BUGFIX] Honor fuzzy search abort (#2491, #2490)\n* [ENHANCEMENT] Add nicer gopass audit HTML output (#2508)\n* [ENHANCEMENT] Check recipients before adding a new one. (#2487, #1918)\n* [ENHANCEMENT] Do not enforce lower case keys (#2489, #1777)\n* [ENHANCEMENT] Do not rewrite ~. (#2496, #2083)\n* [ENHANCEMENT] Rewrite gopass audit. Add HTML and CSV (#2506, #2504)\n* [ENHANCEMENT] gitconfig: Support MultiVars (#2476, #2457)\n\n## 1.15.2 / 2022-12-18\n\n* [BUGFIX] [gitconfig] Properly parse Key-Value pairs with (#2482, #2479)\n* [ENHANCEMENT] Add --force-regen flag to generate (#2475, #2474)\n* [ENHANCEMENT] Add recipients hash checking. (#2481, #2478)\n\n## 1.15.1 / 2022-12-11\n\n* [BUGFIX] Fix domain alias lookup (#2455, #2453)\n* [BUGFIX] Fix vim invocation. (#2456, #2424)\n* [CLEANUP] Unhide fscopy and fsmove (#2444, #1831)\n* [ENHANCEMENT] Add DirName template (#2452)\n* [ENHANCEMENT] Add generate.symbols and generate.length (#2443, #2151)\n* [ENHANCEMENT] Add template docs (#2445, #1562)\n* [ENHANCEMENT] Document supported secret formats. (#2439, #1585)\n* [ENHANCEMENT] Pre-populate ID with git values (#2442, #968)\n* [ENHANCEMENT] Support german language in the password (#2454, #2451)\n\n## 1.15.0 / 2022-12-03\n\n* [BREAKING] New config format based on git config. (#2395, #1567, #1764, #1819, #1878, #2387, #2418)\n* [BUGFIX] Fix symlink deduplication. (#2437, #2402)\n* [ENHANCEMENT] Maintain secret structure when parsing (#2433, #2431)\n* [ENHANCEMENT] Retain recipients file format (#2432, #2430)\n\n### New config format: gitconfg\n\nGopass is getting a new config format based on the one use by git itself.\nThe new implementation is much more flexible and extensible and will allow us\nto more easily support new config options going forward. It does also support\na hierachy of configs. That means we can now support system wide defaults\nas well as per mount config options.\n\nThe system wide configuration gives package maintainers and admins of multi\nuser deployments the option to pre-set certain options to their liking.\n\n### New default secret format\n\nThe default secret format has been rewritten to replace two of the existing\nones (KV and Plain). The new format puts a strong emphasis on retaining the\ninput as close as possible. And small change that might be visible in some\ncorner cases is that every secret now contains a terminating new line.\n\n### Recipient files can now contain comments\n\nThe parsing of the recipients files (`.gpg-id`) has become more flexible and\ncan now contain comments. These will be retained when updating these files\nthrough gopass as well.\n\n## 1.14.11 / 2022-11-25\n\n* [BUGFIX] Fix edit on MacOS Ventura (#2426, #2400)\n* [BUGFIX] Handle nvi (#2414)\n* [BUGFIX] Improve support for non-vim editors (#2427, #2424)\n* [BUGFIX] Only pass vim options to vim (#2421, #2412)\n* [ENHANCEMENT] Support combined short flags (#2420, #2419)\n\n## 1.14.10 / 2022-11-09\n\n* [BUGFIX] Correctly handle key removal on Windows (#2372, #2371)\n* [DOCUMENTATION] (#1878)\n* [ENHANCEMENT] Ignore comments in recipient files. (#2394, #2393)\n* [ENHANCEMENT] Improve key expiration handling (#2383, #2369)\n* [ENHANCEMENT] allow re-encrypting entire directory when (#2373)\n\n## 1.14.9 / 2022-09-28\n\n* [ENHANCEMENT] Make DBus notifications transient (#2364, #2358)\n\n## 1.14.8 / 2022-09-27\n\n* [BUGFIX] Ignore not-existing .ssh dir (#2347, #2333)\n* [BUGFIX] Use Wait() to avoid Zombies (#2354, #1666)\n* [ENHANCEMENT] Allow modifying default create templates (#2349, #2291)\n* [ENHANCEMENT] Improve passage support (#2352, #2059)\n* [ENHANCEMENT] Use OS keychain for age passphrase caching (new config option, off by default). (#2351, #2350)\n\n## 1.14.7 / 2022-09-20\n\n* [BUGFIX] Do not ignore symlinks when listing (#2344, #2173)\n* [BUGFIX] Do not shadow entries behind folders. (#2341, #2338)\n* [BUGFIX] Fix updater on Windows. (#2345, #2011)\n* [BUGFIX] Handle Ctrl+C in TOTP (#2342, #2320)\n* [ENHANCEMENT] Set vim options instead of sniffing (#2343, #2317)\n\n## 1.14.6 / 2022-09-10\n\n* [BUGFIX] Do not show setup message on version (#2327)\n* [BUGFIX] Remove exported public keys of removed (#2328, #2315)\n* [ENHANCEMENT] Document extension model. (#2329, #2290)\n\n## 1.14.5 / 2022-09-03\n\n* [BUGFIX] Fix fsck progress bar. Mostly. (#2303)\n* [DOCUMENTATION] fix in recommended vim setting (#2318)\n\n## 1.14.4 / 2022-08-02\n\n* [BREAKING] gopass otp will automatically update the counter key in HTOP secrets! (#2278)\n* [BUGFIX] Allow removing unknown recipients with --force (#2253)\n* [BUGFIX] Honor PASSWORD_STORE_DIR (#2272)\n* [BUGFIX] Honor OTP key period from URL (#2278)\n* [BUGFIX] Wizard: Enforce min and max length. (#2293)\n* [CLEANUP] Use Go 1.19 (#2296)\n* [ENHANCEMENT] Automatically sync once a week (#2191)\n* [ENHANCEMENT] Scan for vulnerabilities and add SBOM on (#2268)\n* [ENHANCEMENT] Use packages.gopass.pw for APT packages (#2261)\n\n## 1.14.3 / 2022-05-31\n\n* [BUGFIX] Do not print progress bar on otp --clip (#2243)\n* [BUGFIX] Removing shadowing warning when using -o/--password (#2245)\n* [CLEANUP] Deprecate OutputIsRedirected in favour of IsTerminal (#2248)\n* [DOCUMENTATION] Adding doc about YAML entries and unsafe-keys (#2244)\n* [ENHANCEMENT] Allow deleting multiple secrets (#2239)\n\n## 1.14.2 / 2022-05-22\n\n* [BUGFIX] Fix gpg identity detection (#2218, #2179)\n* [BUGFIX] Handle different line breaks in recipient (#2221, #2220)\n* [BUGFIX] Stop eating secrets on move (#2211, #2210)\n* [ENHANCEMENT] Add flag to keep env variable capitalization (#2226, #2225)\n* [ENHANCEMENT] Environment variable GOPASS_PW_DEFAULT_LENGTH can be used to overwrite default password length of 24 characters. (#2219)\n\n## 1.14.1 / 2022-05-02\n\n* [BUGFIX] Do not print missing public key for age. (#2166)\n* [BUGFIX] Improve convert output (#2171)\n* [BUGFIX] fix errors in zsh completions (#2005)\n* [CLEANUP] Migrating to a maintained version of openpgp (#2193)\n* [ENHANCEMENT] Avoid decryption on move or copy (#2183, #2181)\n* [UX] Upgrade xkcdpwgen to a new version that removes German (#2187)\n\n## 1.14.0 / 2022-03-16\n\n* Add --chars option to print subset of secrets (#2155, #2068)\n* [BUGFIX] Always re-encrypt when fsck is invoked with --decrypt. (#2119, #2015)\n* [BUGFIX] Body only entries are detected now by show -o (#2109)\n* [BUGFIX] Do not hide git error messages (#2118, #1959)\n* [BUGFIX] Fix completion when password name contains (#2150)\n* [BUGFIX] Fix template func arg order (#2117, #2116)\n* [BUGFIX] Fixes an issue where recipients remove may fail (#2147, #1964)\n* [BUGFIX] Fixes an issue where recipients remove may fail (#2147, #1964)\n* [BUGFIX] Handle from prefix correctly on mv (#2110, #2079)\n* [BUGFIX] Handle unencoded secret on cat (#2105)\n* [BUGFIX] Make man page consistent with other docs (#2133)\n* [BUGFIX] Reject invalid salt with MD5Crypt templates (#2128)\n* [BUGFIX] depend *.deb on gnupg instead of dummy (#2050)\n* [CLEANUP] Deprecate gopasspw/pinentry (#2095)\n* [CLEANUP] Use Go 1.18 (#2156)\n* [CLEANUP] Use debug.ReadBuildInfo (#2032)\n* [DOCUMENTATION] Fixed link to passwordstore.org (#2129)\n* [DOCUMENTATION] document 'gopass cat' (#2051)\n* [DOCUMENTATION] improve 'gopass cat' (#2070)\n* [DOCUMENTATION] improve 'gopass show -revision -<N>' (#2070)\n* [ENHANCEMENT] Add age subcommand (#2103, #2098)\n* [ENHANCEMENT] Add gopass audit --expiry (#2067)\n* [ENHANCEMENT] Add gopass process (#2066, #1913)\n* [ENHANCEMENT] Allow overriding GPG path (#2153)\n* [ENHANCEMENT] Automatically export creators key to the (#2159, #1919)\n* [ENHANCEMENT] Bump to Go 1.18 (#2058)\n* [ENHANCEMENT] Enforce TLSv1.3 (#2085)\n* [ENHANCEMENT] Generics (#2034, #2030)\n* [ENHANCEMENT] Hide password on MacOS clipboards (#2065)\n* [ENHANCEMENT] Passage compat improvements (#2060, #2060)\n* [ENHANCEMENT] gopass git invokes git directly (#2102)\n* [ENHANCEMENT] Template support for the create wizard (#2064)\n* [ENHANCENMENT] Check for MacOS Keychain storing the GPG (#2144)\n* [EXPERIMENTAL] Support the Fossil SCM (#2092, #2022)\n* [FEATURE] Add env variables for custom clipboard commands. (#2091, #2042)\n* [FEATURE] only accept keys with \"encryption\" key capability (#2047, #1917, #1917)\n* [TESTING] Improve two line test ambiguity. (#2091, #2042)\n* [TESTING] Use a helper to unset env vars in clipboard tests. (#2091, #2042)\n* [UX] OTP code now runs in loop until canceled or used with -o (#2041)\n\n## 1.13.1 / 2022-01-15\n\n* [BUGFIX] Handle from prefix correctly on mv (#2110, #2079)\n* [BUGFIX] Handle unencoded secret on cat\n\n## 1.13.0 / 2021-11-13\n\n* [BUGFIX] Do not print OTP progress bar if not in terminal (#2019)\n* [BUGFIX] Don't prompt to retype password unnecessarily (#1983)\n* [BUGFIX] Fix AutoClip handling on generate (#2024, #2023)\n* [BUGFIX] Replace Build Status badge in README (#2016)\n* [BUGFIX] The field 'parsing' is now honored with legacy config pre v1.12.7 (#1997)\n* [BUGFIX] Use default git branch on setup (#2026, #1945)\n* [ENHANCEMENT] Adding a MSI installer for Windows (#2001)\n* [ENHANCEMENT] Move password prompts to stderr (#2004)\n* [FEATURE] Add capitalized words to memorable passwords (#1985, #1984)\n* [UX] Use new progress bar for OTP expiry time (#2019)\n\n## 1.12.8 / 2021-08-28\n\n* [BUGFIX] Use same default for partial config files (#1968)\n* [CLEANUP] Remove GOPASS_NOCOLOR in favor of NO_COLOR (#1937, #1936)\n* [ENHACNEMENT] Add gopass merge (#1979, #1948)\n* [ENHANCEMENT] Add --symbols to gopass pwgen (#1966)\n* [ENHANCEMENT] Warn on untracked files (#1972)\n\n## 1.12.7 / 2021-07-02\n\n* DOCUMENTATION Fixed Single Line Formating for Clone Documentation (#1943)\n* [BUGFIX] Allow --strict to be chained with --symbols (#1952, #1941)\n* [BUGFIX] Normalize recipient IDs before comparison (#1953, #1900)\n* [BUGFIX] Use /tmp for GIT_SSH_COMMAND on Mac (#1951, #1896)\n* [ENHANCEMENT] Add warning when parsing content (#1950)\n\n## 1.12.6 / 2021-05-01\n\n* [BUGFIX] Do not recurse with a key (#1907, #1906)\n* [BUGFIX] Fix SSH control path (#1899, #1896)\n* [BUGFIX] Fix gopass env with subtrees (#1894, #1893)\n* [BUGFIX] Honor create -s flag (#1891)\n* [BUGFIX] Ignore commented values in gpg config (#1901, #1898)\n* [ENHANCEMENT] Add better usage instructions (#1912)\n\n## 1.12.5 / 2021-03-27\n\n* [BUGFIX] Allow subkeys (#1843, #1841, #1842)\n* [BUGFIX] Avoid logging credentials (#1886, #1883)\n* [BUGFIX] Fix SSH Command override on termux (#1881)\n* [CLEANUP] Moving pkg/pinentry to gopasspw/pinentry (#1876)\n* [ENHANCEMENT] Add -f flag to create (#1867, #1811)\n* [ENHANCEMENT] Add gopass ln (#1828)\n* [ENHANCEMENT] Add proper diff numbers on sync (#1882)\n* [ENHANCEMENT] Update password rules (#1861)\n\n## 1.12.4 / 2021-03-20\n\n* [BUGFIX] Bring back --yes (#1862, #1858)\n* [BUGFIX] Fix make install on BSD (#1859)\n\n## 1.12.3 / 2021-03-20\n\n* [BUGFIX] Fix generate -c (#1846, #1844)\n* [BUGFIX] Fix gopass update (#1838, #1837)\n* [BUGFIX] Fix progress bar on 32 bit archs (#1855, #1854)\n* [CLEANUP] Remove the custom formula in favour of the official one. (#1847)\n* [ENHANCEMENT] Install manpage when using `make install` (#1845)\n\n## 1.12.2 / 2021-03-13\n\n* [BUGFIX] Do not fail if reminder is unavailable (#1835, #1832)\n* [BUGFIX] Do not shadow directories (#1817, #1813)\n* [BUGFIX] Do not trigger ClamAV FP (#1810, #1807)\n* [BUGFIX] Fix -o (#1822)\n* [BUGFIX] Honor Ctrl+C while waiting for user input (#1805, #1800)\n* [ENHANCEMENT] Add gopass.1 man page (#1827, #1824)\n* [UX] Adding the grep command to --help (#1826, #1825)\n\n## 1.12.1 / 2021-02-17\n\n* [BUGFIX] Enable updater on Windows (#1790, #1789)\n* [BUGFIX] Fix progress bar nil pointer access (#1790, #1789)\n* [BUGFIX] Fix % char in passwords being treated as formatting (#1794, #1793, #1801)\n* [ENHANCEMENT] Add ARCHITECTURE.md (#1787)\n* [ENHANCEMENT] Added a env var to disable reminders (#1792)\n* [ENHANCEMENT] Remind to run gopass update/fsck/audit after 90d (#1792)\n\n## 1.12.0 / 2021-02-11\n\nWARNING: The self updater does not support updating from 1.11.0 to 1.12.0. Our\nrelease infrastructure does not support the key type used in 1.11.0.\n\nNOTE: This release drops the integrations that were moved to their own repos,\ni.e. `git-credential-gopass`, `gopass-hibp`, `gopass-jsonapi` and\n`gopass-summon-provider`.\n\nWe have implemented proper release signing and verification for the self\nupdater and brought it back.\n\n* [BUGFIX] Add signature verification for updater (#1717, #1676)\n* [BUGFIX] Allow using tilde (#1713, #872)\n* [BUGFIX] Always allow removing mounts (#1748, #1746)\n* [BUGFIX] Ask passphrase upon key generation (#1715, #1698)\n* [BUGFIX] Do not overwrite age keyring (#1734, #1678)\n* [BUGFIX] Remove empty parents on gopass rm -r (#1725, #1723)\n* [BUGFIX] The empty password must now be confirmed too (#1719)\n* [BUGFIX] Use the first GPG found in path on Windows (#1751, #1635)\n* [BUGFIX] Warn about --throw-keyids (#1759, #1756)\n* [BUGFIX] fixed mixed case keys for key-value, all keys are lower case now (#1778)\n* [CLEANUP] Remove migrated binaries (#1712, #1673, #1649, #1652, #1631, #1165, #1711, #1670, #1639)\n* [CLEANUP] Remove the ondisk backend (#1720)\n* [ENHANCEMENT] Add -A and -B to pwgen (#1716)\n* [ENHANCEMENT] Add Pinentry CLI fallback (#1697, #1655)\n* [ENHANCEMENT] Add REPL cmd lock (#1744)\n* [ENHANCEMENT] Add optional pinentry unescaping (#1621)\n* [ENHANCEMENT] Add tpl funcs for Bcrypt and Argon2 (#1706, #1689)\n* [ENHANCEMENT] Add windows support to the self updater (#1724, #1722)\n* [ENHANCEMENT] Confirm new age keyring passphrases (#1747)\n* [ENHANCEMENT] KV secrets are now key-values, supporting multiple same key with different values (#1741)\n* [ENHANCEMENT] UTF-8 emojis (#1715, #1698)\n* [ENHANCEMENT] Use gpgconf to the the gpg binary (#1758, #1757)\n* [ENHANCEMENT] Use main as the git default branch (#1749, #1742)\n* [ENHANCEMENT] Use persistent SSH connections (#1755)\n* [TESTING] Adding DI to Github Actions (#1728)\n\n## 1.11.0 / 2020-01-12\n\nThis is an important bugfix release that should resolve several outstanding\nissues and concerns. Since 1.10.0 was released was engaged in a lot of\ndiscussions and realized that compatibility is more important than we first\nthought. So we're rolling back some breaking changes and revise some parts of\nour roadmap. We will strive to remain compatible with other password store\nimplementations - but remember this is a goal, not a promise. This means we'll\ncontinue using compatible secrets formats as well as GPG and Git.\n\n* [BUGFIX] Allow secret names to have a colon in the name\n* [BUGFIX] Apply limit in list correctly\n* [BUGFIX] Correcting newlines handling\n* [BUGFIX] Correct missing padding to TOTP entry\n* [BUGFIX] Create cache folder if doesn't exist. Relevant\n* [BUGFIX] Disable gopass update\n* [BUGFIX] Disabling all kind of parsing of the input\n* [BUGFIX] Do not duplicate key password in K/V secrets\n* [BUGFIX] Do not search for new secrets\n* [BUGFIX] fixes gopass-jsonapi for MacTools GPGSuite users.\n* [BUGFIX] Fix legacy config parsing\n* [BUGFIX] fsck won't correct recipients without --decrypt\n* [BUGFIX] Insert is not resetting the pw now if a key:value pair is specified inline\n* [BUGFIX] Insert is now parsing its stdin input\n* [BUGFIX] Invalidate GPG key list after generation\n* [BUGFIX] List no longer uses the store size as its default depth\n* [BUGFIX] Nil dereference in cui\n* [BUGFIX] Pass arguments to a notification program\n* [BUGFIX] Password insert prompt now works on Windows but\n* [BUGFIX] Re-adding the global --yes flag\n* [BUGFIX] Remove GPG location caching\n* [BUGFIX] Restore path-removal from old config-format\n* [BUGFIX] Show now correctly handles -C and -u together\n* [BUGFIX] The deprecation warning is now output on stderr\n* [BUGFIX] Trim version prefix in jsonapi\n* [CLEANUP] Remove MIME\n* [CLEANUP] Remove the unfinished xc backend\n* [CLEANUP] Update to minio/v7\n* [DOCUMENTATION] Edited features.md\n* [DOCUMENTATION] Improve contributing guide.\n* [DOCUMENTATION] Slight updates to reflect the recent code\n* [ENHANCEMENT] Adding a trailing separator to the listed folders\n* [ENHANCEMENT] Adding the flag show -n to disable output parsing\n* [ENHANCEMENT] Adding the option parsing to disable all parsing\n* [ENHANCEMENT] fsck now detects leftover Mime secrets\n* [ENHANCEMENT] Full windows support\n* [ENHANCEMENT] Prompt for edit search result\n* [ENHANCEMENT] Re-introduce gopass -c\n* [ENHANCEMENT] Show GPG --gen-key error to the user\n* [ENHANCEMENT] This is required when using e.g. Gnome Keyring.\n* [ENHANCEMENT] Use 32 byte salt by default\n* [UX] Preserve content across retries\n\n## 1.10.1\n\n* [BUGFIX] Fix the Makefile\n* [BUGFIX] Remove misleading config error message\n* [BUGFIX] Re-use existing root store\n* [BUGFIX] Use standard Unix directories on MacOS\n\n## 1.10.0\n\nWARNING: This release contains a few breaking changes as well as necessary\npackaging changes.\n\nThis release is building the foundation for an eventual 2.0 release\nwhich will drop many legacy features and significantly shrink the\ncodebase to ensure long term maintainability. The goal is to remove\nthe support for multiple backends and any external dependencies,\nincluding `git` and `gpg` binaries. By default the tool should be easy to use,\nsecure and modern. We will still support our flagship use cases,\nlike working in teams. Also gopass might eventually move to an\nfully encrypted backend where we don't leak information through\nfilenames.\n\nAny gopass 1.x release should still be compatible with any\npassword store implementation (possibly with some caveats).\nBeyond that we plan to drop any compatibility goals.\n\nIf you are using different Password Store implementations to access your\nsecrets, e.g. on mobile devices, you might want to run `gopass config mime false`\nbefore performing any kind of write operation on the password store. Otherwise\nmutated secrets will be written using the new native gopass MIME format and\nmight not be readable from other implementations.\n\nThis release adds documentation for all supported subcommands in the `docs/commands`\nfolder and starts define our core use cases in the `docs/usecases` folder.\nPlease note that the command documentation also serves as a specification on\nhow these commands are supposed to operate.\n\nNote: We have accumulated too many changes so we've decided to skip the 1.9.3\nrelease and issue the first release of the 1.10. series.\n\nNote to package maintainers: This release adds additional binaries which should\nbe included in any binary re-distribution of gopass.\n\n* [BREAKING] New secrets format\n* [BUGFIX] Allow deleting shadowed secret\n* [BUGFIX] Correctly handle exportkeys and auto import for noop\n* [BUGFIX] Do not allow malformed secrets\n* [BUGFIX] Do not return error on no grep matches\n* [BUGFIX] Fix config panic with mounts\n* [BUGFIX] Fix fsck progress bar.\n* [BUGFIX] Fix git init\n* [BUGFIX] Fix optional key passed through find\n* [BUGFIX] Fix tree shadowing.\n* [BUGFIX] Handle relative path during init\n* [BUGFIX] Honor generate --print\n* [BUGFIX] Honor trust level during onboarding.\n* [BUGFIX] Print RCS error message\n* [BUGFIX] Print config parse error to STDERR\n* [BUGFIX] Properly initialize crypto during onboarding and\n* [BUGFIX] env command: do not crash if called without a command to execute\n* [CLEANUP] Merge Storage and RCS backends\n* [CLEANUP] Move internal packages to internal\n* [CLEANUP] Remove autoclip for gopass show\n* [CLEANUP] Remove config option confirm\n* [CLEANUP] Remove curses UI\n* [CLEANUP] Remove the --sync flag to gopass show\n* [CLEANUP] Rename --force to --unsafe for show\n* [CLEANUP] Rename xkcd generator options\n* [DEPRECATION] Mark gopass git as deprecated\n* [DEPRECATION] Remove AutoPrint\n* [DEPRECATION] Remove askformore, autosync\n* [DEPRECATION] Retire editrecipients option\n* [DOCUMENTATION] Document audit, generate, insert and show\n* [DOCUMENTATION] Document list flags\n* [DOCUMENTATION] Improve documentation of Zsh completion setup\n* [ENHANCEMENT] Add GOPASS_DISABLE_MIME to disable new\n* [ENHANCEMENT] Add arm and arm64 binaries\n* [ENHANCEMENT] Add gopass API (unstable)\n* [ENHANCEMENT] Add regexp support to gopass grep\n* [ENHANCEMENT] Add zxcvbn password strength checker\n* [ENHANCEMENT] Avoid direct show on gopass search\n* [ENHANCEMENT] Cache gpg binary location\n* [ENHANCEMENT] Ignore binary secrets for audit\n* [ENHANCEMENT] Introduce --generator flag\n* [ENHANCEMENT] Introduce unsafe-keys\n* [ENHANCEMENT] Make audit report passwords not changed\n* [ENHANCEMENT] Make show --qr flag complementary\n* [ENHANCEMENT] New Debug package\n* [ENHANCEMENT] New progress bar\n* [ENHANCEMENT] Print password before sync\n* [ENHANCEMENT] Provide more helpful config parse errors\n* [ENHANCEMENT] Rewrite tree implementation\n* [ENHANCEMENT] Show recipients from subfolder id files\n* [ENHANCEMENT] Speed up gpg store init\n* [ENHANCEMENT] Support changing path with gopass config\n* [ENHANCEMENT] Support relative revisions for show\n* [ENHANCEMENT] Warn if vim might be leaking secrets\n* [ENHANCEMENT] env command: more tests\n* [FEATURE] Add Password Rules and Domain Alias support\n* [FEATURE] Add experimental backend converter\n* [FEATURE] Add remote config for ondisk storage\n* [FEATURE] Add remote sync support for the ondisk backend\n* [FEATURE] Add summon provider\n* [FEATURE] Pinentry API: support OPTION API call\n* [FEATURE] REPL\n* [TESTING] Add a test to detect shadowing issue with mount\n\n## 1.9.2 / 2020-05-13\n\n* [BUGFIX] Bring back the custom fish completion.\n* [BUGFIX] Disable AutoClip when redirecting stdout\n* [ENHANCEMENT] Create new sub stores in XDG compliant locations.\n\n## 1.9.1 / 2020-05-09\n\n* [BUGFIX] Do not copy to clipboard with -f\n* [BUGFIX] Encrypt parent directory if leaf node exists.\n* [BUGFIX] Fix -c and -C for default show action.\n* [BUGFIX] Hide git-credential store warning.\n* [BUGFIX] Honor notifications setting.\n* [BUGFIX] Simplify autoclip behavior\n* [DEPRECATION] Remove PASSWORD_STORE_DIR support\n* [ENHANCEMENT] Add exportkeys option.\n* [ENHANCEMENT] Add memorable password generator\n* [ENHANCEMENT] Add preliminary age encryption support.\n\n## 1.9.0 / 2020-05-01\n\n* [ENHANCEMENT] Proper windows support [#1295]\n* [ENHANCEMENT] Add pwgen subcommand [#1308]\n* [ENHANCEMENT] Only decrypt when needed [#1289]\n* [ENHANCEMENT] Full unattended password generation [#1259]\n* [ENHANCEMENT] Add -C flag [#1272]\n* [ENHANCEMENT] Migrate to urface/cli/v2 [#1276]\n* [ENHANCEMENT] Support Termux [#913]\n* [BUGFIX] Do not fail if nothing to commit [#1168, #1103]\n* [BUGFIX] Restore PASSWORD_STORE_DIR support [#1213]\n* [BUGFIX] Do not remove empty second line [#1235]\n* [BUGFIX] Do not disable color if no PAGER is available [#1244]\n* [BUGFIX] Do not overwrite entry when reading from STDIN [#1245]\n* [BUGFIX] Commit when using concurrency gt 1 [#1246]\n* [BUGFIX] Do not error out when listing a leaf node [#1300]\n* [BUGFIX] Do not overwrite config if PASSWORD_STORE_DIR is set [#1286]\n* [BUGFIX] Fix go get support [#1288]\n* [DEPRECATION] Remove Dockerfile [#1309]\n* [DEPRECATION] Remove Bintray [#1304]\n* [DEPRECATION] Deprecate OTP, Binary, YAML git-credentials and xc support [#1301]\n* [DEPRECATION] Remove support for OpenPGP (library), GoGit, Vault, Consul and encrypted configs [#1290, #1283, #1282, #1279]\n\n## 1.8.6 / 2019-07-26\n\n* [ENHANCEMENT] Add --password to otp command [#1150]\n* [ENHANCEMENT] Support adding key values with colons [#1128]\n* [BUGFIX] Allow overwriting directories with --force [#1149]\n* [BUGFIX] Sort list of stores when adding recipients [#1144]\n* [BUGFIX] Sort recipients by Name not by ID [#1143]\n* [BUGFIX] Handle slashes in recipient names [#1139]\n\n## 1.8.5 / 2019-03-03\n\n* [ENHANCEMENT] Improve template handling [#1029]\n* [ENHANCEMENT] Remove empty directories [#1009]\n* [ENHANCEMENT] Improve performance of unclip [#923]\n* [ENHANCEMENT] Add AutoPrint option [#1065]\n* [ENHANCEMENT] Follow the rsync convention for cp/mv commands [#1055]\n* [BUGFIX] Fix bash completion for MSYS on Windows [#1053]\n* [BUGFIX] Git clone failing [#1036]\n\n## 1.8.4 / 2018-12-26\n\n* [ENHANCEMENT] Evaluate templates when inserting single secrets [#1023]\n* [ENHANCEMENT] Add fuzzy search dialog for gopass otp [#1021]\n* [ENHANCEMENT] Add edit option to search dialog [#1019]\n* [ENHANCEMENT] Introduce build tags for experimental features [#1000]\n* [BUGFIX] Fix recursive delete [#1024]\n* [BUGFIX] Abort tests on critical failures [#997]\n* [BUGFIX] Zsh autocompletion [#996]\n\n## 1.8.3 / 2018-11-19\n\n* [ENHANCEMENT] Add zsh autocompletion for insert and generate [#988]\n* [ENHANCEMENT] Set exit code for filtered ls without result [#983]\n* [ENHANCEMENT] Improve generate command [#948]\n* [ENHANCEMENT] Print summary for grep [#943]\n* [ENHANCEMENT] Documentation updates [#924, #890, #918, #919, #920, #944, #952, #958, #969, #985]\n* [ENHANCEMENT] jsonapi: Add windows support for configure [#904]\n* [ENHANCEMENT] jsonapi: Add getVersion [#893]\n* [ENHANCEMENT] Support symlinks for fs storage backend [#886]\n* [BUGFIX] Offer store selection with exactly one mount point as well [#987]\n* [BUGFIX] Edit entry selected by fuzzy search [#979]\n* [BUGFIX] Fix path handling on windows [#970]\n* [BUGFIX] Remove quotes [#967]\n* [BUGFIX] Properly handle git add for removed files [#946]\n* [BUGFIX] HAndle already mounted and not initialized errors [#945]\n* [BUGFIX] Fix HIBP command options [#936]\n* [BUGFIX] Offer secret selection on edit command [#929]\n* [BUGFIX] jsonapi: add initialize [#903]\n* [BUGFIX] Update external dependencies [#884, #932, #981]\n* [BUGFIX] Use valid crypto backend for key selection [#889]\n\n## 1.8.2 / 2018-06-28\n\n* [ENHANCEMENT] Improve fsck output [#859]\n* [ENHANCEMENT] Enable notifications on FreeBSD [#863]\n* [ENHANCEMENT] Redirect errors to stderr [#880]\n* [ENHANCEMENT] Do not writer version to config [#883]\n* [BUGFIX] Fix commit on move [#860]\n* [BUGFIX] Properly check store initialization [#865]\n\n## 1.8.1 / 2018-06-08\n\n* [BUGFIX] Trim fsck path [#856]\n* [BUGFIX] Handle URL parse errors in create [#855]\n\n## 1.8.0 / 2018-06-06\n\nThis release includes several possibly breaking changes.\nThe `gopass move` implementation was refactored to properly support moving\nentries and subtrees across mount points. This may change the behaviour slightly.\nAlso the build flags were changed to build PIE binaries. This should not affect\nthe runtime behaviour, but we could not test this on all platforms, yet.\n\n* [BREAKING] Make move work recursively and across stores [#821]\n* [FEATURE] Add git credential caching [#743]\n* [FEATURE] Add local recipient integrity checks [#800 #826]\n* [ENHANCEMENT] Handle key-value pairs on generate and insert [#790]\n* [ENHANCEMENT] Add gpg.listKeys caching [#804]\n* [ENHANCEMENT] Add append mode for gopass insert [#807]\n* [ENHANCEMENT] Support external password generators [#811]\n* [ENHANCEMENT] Add gopass generate completion heuristic [#817]\n* [ENHANCEMENT] Add revive linter checks [#822]\n* [ENHANCEMENT] Remove -static build flag, enable CGO and -buildmode=PIE [#823]\n* [ENHANCEMENT] Warn if RCS backend is noop during gopass sync [#825]\n* [ENHANCEMENT] Support for special password rules on generate [#832]\n* [ENHANCEMENT] Improve create wizard [#842]\n* [ENHANCEMENT] Honor templates on generate [#847]\n* [ENHANCEMENT] Support NO_COLOR [#851]\n* [BUGFIX] Reset clipboard timer on repeated copy [#813]\n* [BUGFIX] Add --force to git add invocation [#839]\n* [BUGFIX] Rename updater GitHub Organisation [#818]\n* [BUGFIX] Default to origin master for git pull [#819]\n* [BUGFIX] Properly propagate RCS backend on gopass clone [#820]\n* [BUGFIX] Fix sub store config propagation [#837 #841]\n* [BUGFIX] Use default for password store dir [#846]\n* [BUGFIX] Properly handle autosync on recipients save [#848]\n* [BUGFIX] Resolve key IDs to fingerprints before adding or removing [#850]\n\n## 1.7.2 / 2018-05-28\n\n* [BUGFIX] Fix tilde expansion [#802]\n\n## 1.7.1 / 2018-05-25\n\n* [BUGFIX] Add nogit compat handler [#792]\n* [BUGFIX] Fix reencrypt [#796]\n\n## 1.7.0 / 2018-05-22\n\n* [FEATURE] Pluggable crypto, storage and RCS backends. Including a pure-Go NaCl based crypto backend [#645] [#680] [#736] [#777]\n* [FEATURE] Password history [#660]\n* [FEATURE] Vault backend [#723] [#730]\n* [FEATURE] Consul backend [#697]\n* [FEATURE] HIBPv2 Dump and API support [#666] [#706]\n* [FEATURE] Select recipients per secret [#703]\n* [FEATURE] Add experimental OpenPGP crypto backend [#670]\n* [ENHANCEMENT] Support HIBPv2 API and Dumps [#666]\n* [ENHANCEMENT] Robust K/V parser with YAML fallback [#659]\n* [ENHANCEMENT] Restrict fsck to given path [#721]\n* [ENHANCEMENT] Refactor [#702] [#708] [#715] [#722] [#731]\n* [ENHANCEMENT] Proper Makefile dependencies [#707]\n* [ENHANCEMENT] Auto-copy with safecontent [#685]\n* [ENHANCEMENT] Add disable notifications option [#690]\n* [ENHANCEMENT] Migrate from govendor to dep [#688]\n* [ENHANCEMENT] Improve test coverage [#732] [#781] [#782]\n* [ENHANCEMENT] Improvate YAML handling [#739]\n* [ENHANCEMENT] Audit freshly generated passwords [#761]\n* [BUGFIX] Use sh instead of bash [#699]\n* [BUGFIX] Lookup correct remote for current branch [#692]\n* [BUGFIX] Fix GPG binary detection on Windows [#681] [#693]\n* [BUGFIX] Version [#727]\n* [BUGFIX] Git init [#729]\n* [BUGFIX] Secret.String() [#738]\n* [BUGFIX] Fix generate --symbols [#742] [#783]\n\n## 1.6.11 / 2018-02-20\n\n* [ENHANCEMENT] Documentation updates [#648] [#656]\n* [ENHANCEMENT] Add secret completions to edit command in zsh [#654]\n* [BUGFIX] Avoid escaping values added to secrets [#658]\n* [BUGFIX] Fix parsing of GPG UIDs [#650]\n\n## 1.6.10 / 2018-01-18\n\n* [ENHANCEMENT] Add Travis MacOS builds [#618]\n* [ENHANCEMENT] Make gopass build on DragonFlyBSD [#619]\n* [ENHANCEMENT] Increase test coverage [#621] [#622] [#624]\n* [BUGFIX] Properly handle sub-store configuration [#625]\n* [BUGFIX] Fix Makefile [#615] [#617]\n* [BUGFIX] Fix failing tests on MacOS [#614]\n\n## 1.6.9 / 2018-01-05\n\n* [BUGFIX] Fix update URL check [#610]\n\n## 1.6.8 / 2018-01-05\n\n* [ENHANCEMENT] Add OpenBSD Ksh completion [#586]\n* [ENHANCEMENT] Increase test coverage [#589] [#590] [#592] [#595] [#596] [#597] [#601] [#602] [#603] [#604]\n* [ENHANCEMENT] Update Documentation and Dockerfile [#591] [#605]\n* [BUGFIX] Use Termwiz CUI on OpenBSD [#588]\n* [BUGFIX] Fix create wizard [#594]\n* [BUGFIX] Use persistent bufio.Reader [#607]\n\n## 1.6.7 / 2017-12-31\n\n* [ENHANCEMENT] Add --sync flag to gopass show [#544]\n* [ENHANCEMENT] Update dependencies [#547]\n* [ENHANCEMENT] Use gocui for terminal UI [#562]\n* [ENHANCEMENT] Increase test coverage [#548] [#549] [#567] [#568] [#570] [#572] [#574] [#575] [#577] [#578] [#583] [#584]\n* [ENHANCEMENT] Add Dockerfile [#561]\n* [ENHANCEMENT] Add zsh and fish completion generator [#565]\n* [ENHANCEMENT] Add go-fuzz instrumentation [#576]\n* [BUGFIX] Catch URL parse errors [#546]\n\n## 1.6.6 / 2017-12-20\n\n* [FEATURE] Selective Sync [#538]\n* [ENHANCEMENT] Make termwiz honor copy flag [#534]\n* [ENHANCEMENT] Make shell completion respect binary name [#536]\n* [ENHANCEMENT] Refactor [#533] [#540] [#541] [#542]\n* [BUGFIX] Show git output [#529]\n\n## 1.6.5 / 2017-12-15\n\n* [ENHANCEMENT] Handle errors gracefully [#524]\n* [BUGFIX] Follow symlinks [#519]\n* [BUGFIX] Improve GPG binary detection [#520] [#522]\n\n## 1.6.4 / 2017-12-13\n\n* [ENHANCEMENT] Support desktop notifications on Mac and Windows [#513]\n* [BUGFIX] Fix slice out of bounds error [#517]\n* [BUGFIX] Allow .password-store to be a symlink [#516]\n* [BUGFIX] Respect --store flag to git sub command [#512]\n\n## 1.6.3 / 2017-12-12\n\n* [ENHANCEMENT] Avoid altering YAML secrets unless necessary [#508]\n* [ENHANCEMENT] Documentation updates [#493] [#509]\n* [ENHANCEMENT] Abort if no GPG binary was found [#506]\n* [ENHANCEMENT] Support GOPASS_GPG_OPTS and GOPASS_UMASK [#504]\n* [BUGFIX] Create .gpg-keys if it does not exist [#507]\n\n## 1.6.2 / 2017-12-02\n\n* [FEATURE] Add gopass fix command [#471]\n* [ENHANCEMENT] Add pledge support on OpenBSD [#469]\n* [ENHANCEMENT] Improve no clipboard warning [#484]\n* [BUGFIX] Allow OTP entry in password field [#467]\n* [BUGFIX] Default to vi if no other editor is available [#479]\n* [BUGFIX] Avoid auto-search running non-interactively [#483]\n\n## 1.6.1 / 2017-11-15\n\n* [FEATURE] Add generic OTP action [#440]\n* [ENHANCEMENT] Ignore any secret that does not end with .gpg [#461]\n* [ENHANCEMENT] Add option to display only the password [#455]\n* [ENHANCEMENT] Disable fuzzy search for gopass find [#454]\n* [BUGFIX] Fix .gpg-id selection for sub folders [#465]\n* [BUGFIX] Set gpg.program if possible [#464]\n* [BUGFIX] Allow access to secrets shadowed by a folder [#463]\n* [BUGFIX] Set GPG_TTY [#452]\n* [BUGFIX] Fix termbox UI on OpenBSD [#446]\n* [BUGFIX] Fix tests and paths on Windows [#421] [#431] [#442] [#450]\n\n## 1.6.0 / 2017-11-03\n\n* [FEATURE] Add Desktop notifications (Linux/DBus only) [#434] [#435]\n* [ENHANCEMENT] Show public key identities before importing [#427]\n* [ENHANCEMENT] Initialize local git config on gopass clone [#429]\n* [ENHANCEMENT] Do not print generated passwords by default [#430]\n* [ENHANCEMENT] Clear KDE Klipper History on clipboard clearing [#434]\n* [ENHANCEMENT] Refactor git backend [#437]\n* [BUGFIX] Fix recipients remove when using email as identifier [#436]\n\n## 1.5.1 / 2017-10-25\n\n* [ENHANCEMENT] Re-introduce usecolor config option [#414]\n* [ENHANCEMENT] Improve documentation [#407] [#409] [#416] [#417]\n* [ENHANCEMENT] Add language switch for xckd-style generation [#406]\n* [BUGFIX] Fix GPG binary detection [#419]\n* [BUGFIX] Fix tests on windows [#421]\n\n## 1.5.0 / 2017-10-17\n\n* [FEATURE] Add secret creation wizard [#386]\n* [FEATURE] Add onboarding wizard [#387]\n* [FEATURE] Wizard for recipients add/remove [#359]\n* [FEATURE] XKCD#936 inspired password generation [#368]\n* [FEATURE] Add update wizard [#395]\n* [ENHANCEMENT] Overhaul documentation [#383] [#384]\n* [ENHANCEMENT] Attempt to get TOTP key from YAML [#376]\n* [ENHANCEMENT] Allow find to take -c [#378]\n* [ENHANCEMENT] Improve terminal wizard [#385]\n* [ENHANCEMENT] Improve responsiveness by context usage [#388]\n* [ENHANCEMENT] Improve output readability [#392] [#393]\n* [ENHANCEMENT] Automatic GPG key generation [#391]\n* [BUGFIX] Relax YAML document marker handling [#398]\n\n## 1.4.1 / 2017-10-05\n\n* [BUGFIX] Support pre-1.3.0 configs [#382]\n* [BUGFIX] Turn YAML errors into warnings [#380]\n\n## 1.4.0 / 2017-10-04\n\n* [FEATURE] Add fuzzy search [#317]\n* [FEATURE] Allow restricting charset of generated passwords [#270]\n* [FEATURE] Check quality of newly inserted passwords with crunchy [#276]\n* [FEATURE] JSON API [#326]\n* [FEATURE] Per-Mount configuration options [#330]\n* [FEATURE] Terminal selection of results [#259]\n* [FEATURE] gopass sync [#303]\n* [ENHANCEMENT] Build with Go 1.9 [#294]\n* [ENHANCEMENT] Display single find result directly [#265]\n* [ENHANCEMENT] Global --yes flag [#327]\n* [ENHANCEMENT] Improve error handling and propagation [#280]\n* [ENHANCEMENT] Omit newline when not writing to a terminal [#325]\n* [ENHANCEMENT] Only commit once per recipient batch operation [#329]\n* [ENHANCEMENT] Provide partial support for .gpg-id files in sub folders [#291]\n* [ENHANCEMENT] Trim any trailing newlines or carriage returns in show output [#296]\n* [ENHANCEMENT] Use contexts [#310]\n* [ENHANCEMENT] Use contexts to cancel long running operations [#358]\n* [ENHANCEMENT] Use default editors [#286]\n* [ENHANCEMENT] Improve documentation [#365]\n* [ENHANCEMENT] Print selected entry [#372]\n* [BUGFIX] Confirm removal of directories [#309]\n* [BUGFIX] Only confirm recipients once during batch operations [#328]\n* [BUGFIX] Only overwrite password on insert [#323]\n* [BUGFIX] Avoid Show/Find recursion [#360]\n* [BUGFIX] Remove deprecated special case for .yaml files [#362]\n* [BUGFIX] Do not offer invalid keys [#364]\n* [BUGFIX] Assign path only if resolving symlink succeeds [#370]\n\n## 1.3.2 / 2017-08-22\n\n* [BUGFIX] Fix git version output [#274]\n\n## 1.3.1 / 2017-08-15\n\n* [BUGFIX] Enable AutoSync by default [#267]\n* [BUGFIX] git - do not abort if a store has no remote [#261]\n* [BUGFIX] Fix IFS in bash completion [#268]\n\n## 1.3.0 / 2017-08-11\n\n* [BREAKING] Enforce YAML document markers [#193]\n* [BREAKING] Simplify configuration [#213]\n* [BREAKING] Align gopass init flags with other commands [#252]\n* [FEATURE] Implement pager feature [#163]\n* [FEATURE] Add basic fish completion [#168]\n* [FEATURE] Add version check [#205]\n* [FEATURE] Add gopass audit command [#228]\n* [FEATURE] Add gopass audit hibp command [#239]\n* [ENHANCEMENT] Disable auto-push while re-encrypting [#171]\n* [ENHANCEMENT] Configure git user and email before initial git commit [#185]\n* [ENHANCEMENT] Add recursive git operations [#186]\n* [ENHANCEMENT] Document missing config options [#188]\n* [ENHANCEMENT] Only check and load missing GPG keys after git pull [#190]\n* [ENHANCEMENT] Only encrypt for valid recipients [#191]\n* [ENHANCEMENT] Check and import missing GPG keys on recipients show [#204]\n* [ENHANCEMENT] Save recipients on show [#207]\n* [ENHANCEMENT] Include GPG and Git version in gopass version output [#210]\n* [ENHANCEMENT] Support more flexible YAML documents [#217]\n* [ENHANCEMENT] Simplify mounts add by inferring local path [#219]\n* [ENHANCEMENT] Add contributor documentation [#222]\n* [ENHANCEMENT] Re-use selected encryption key for git signing [#247]\n* [ENHANCEMENT] Setup git push.default [#248]\n* [BUGFIX] Fix nil-pointer check on non existing sub tree [#183]\n* [BUGFIX] Fix load-keys [#203]\n* [BUGFIX] Only match mounts on folders [#240]\n* [BUGFIX] Disable checkRecipients as it conflicts with alwaysTrust [#242]\n\n## 1.2.0 / 2017-06-21\n\n* [FEATURE] YAML support [#125]\n* [FEATURE] Binary support [#136]\n* [ENHANCEMENT] Increase test coverage [#160]\n* [ENHANCEMENT] Use secure temporary storage on MacOS [#144]\n* [ENHANCEMENT] Use goreleaser [#151]\n* [BUGFIX] Fix git invocation [#140]\n* [BUGFIX] Fix missing recipients on init [#141]\n* [BUGFIX] Fix sorting of mount points [#148]\n\n## 1.1.2 / 2017-06-14\n\n* [BUGFIX] Fix gopass init --store [#129]\n* [BUGFIX] Fix gopass init [#127]\n\n## 1.1.1 / 2017-06-13\n\n* [ENHANCEMENT] Allow files and folders with the same name [#124]\n* [ENHANCEMENT] Improve error messages [#121]\n* [ENHANCEMENT] Add rm aliases to remove commands [#119]\n* [BUGFIX] Several bug fixes for multi-repository handling [#123]\n\n## 1.1.0 / 2017-05-31\n\n* [FEATURE] Support templates [#1]\n* [FEATURE] QR Code output [#64]\n* [ENHANCEMENT] If entry was not found start search [#109]\n* [ENHANCEMENT] Do not write color codes unless terminal [#111]\n* [ENHANCEMENT] Make find compare case insensitive [#108]\n* [ENHANCEMENT] Enforce UNIX style line endings [#105]\n* [ENHANCEMENT] Use XDG_CONFIG_HOME [#67]\n* [ENHANCEMENT] Support symlinks [#41]\n* [ENHANCEMENT] Add nocolor config flag [#33]\n* [ENHANCEMENT] Accept args for editor [#30]\n* [BUGFIX] Build fixes for Windows [#14]\n\n## 1.0.2 / 2017-03-24\n\n* [ENHANCEMENT] Improve mounts and init commands [#87]\n* [ENHANCEMENT] Document behavior of `-c` [#82]\n* [ENHANCEMENT] Pass custom arguments to dmenu completion [#72]\n* [ENHANCEMENT] Build with Go 1.8 [#65]\n* [BUGFIX] Improve recursive deletes [#55]\n* [BUGFIX] Bypass prompts on gopass insert --force [#66]\n* [BUGFIX] Able to store secrets, but with errors [#13]\n* [BUGFIX] Don't prompt if input from stdin [#58]\n* [BUGFIX] Git add fails to \"add\" removed files [#57]\n\n## 1.0.1 / 2017-02-13\n\n* [FEATURE] Add dmenu support [#47]\n* [ENHANCEMENT] Extend GOPASS_DEBUG coverage [#31]\n* [ENHANCEMENT] Accept args for editor [#30]\n* [ENHANCEMENT] Use gpg2 if available [#9]\n* [BUGFIX] Fix git error handling in saveRecipients [#32]\n* [BUGFIX] Check if ExpirationDate is set [#28]\n* [BUGFIX] Change user.signkey to user.signingkey [#26]\n* [BUGFIX] Only copy the first line to the clipboard [#21]\n* [BUGFIX] Add search alias to find [#8]\n\n## 1.0.0 / 2017-02-02\n\n* [ENHANCEMENT] Support mounted sub-stores\n* [ENHANCEMENT] git auto-push and auto-pull\n* [ENHANCEMENT] git-style config editing\n* [ENHANCEMENT] Simplified recipient management\n* [ENHANCEMENT] Interactive questions for missing parameters\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n`gopass` uses GitHub to manage reviews of pull requests.\n\n* If you are a new contributor see: [Steps to Contribute](#steps-to-contribute)\n\n* If you have a trivial fix or improvement, go ahead and create a pull request.\n\n* If you plan to do something more involved, first raise an issue to discuss\n  your idea. This will avoid unnecessary work.\n\n* Relevant coding style guidelines are  the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments)\n  and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style).\n\n## Steps to Contribute\n\nShould you wish to work on an issue, please claim it first by commenting on the GitHub issue you want to work on it.\nThis will prevent duplicated efforts from contributors.\n\nPlease check the [`help-wanted`](https://github.com/gopasspw/gopass/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) label to find issues that need help.\nIf you have questions about one of the issues please comment on them and one of the maintainers\nwill try to clarify it.\n\n## Pull Request Checklist\n\n* Use that [latest stable Go release](https://golang.org/dl/)\n\n* Branch from master and, if needed, rebase to the current master branch before submitting your pull request.\n  If it doesn't merge cleanly with master you will be asked to rebase your changes.\n\n* Commits should be as small as possible, while ensuring that each commit is correct independently.\n\n* Add tests relevant to the fixed bug or new feature.\n\n* Commit messages must contain [Developer Certificate of Origin](https://developercertificate.org/) / `Signed-off-by` line, for example:\n\n      One line description of commit\n\n      More detailed description of commit, if needed.\n\n      Signed-off-by: Your Name <your@email.com>\n\n* The first line of the commit message, the subject line, should be prefix with a tag indicating the type of the change. These tags will be extracted and used to populate the changelog.\n  Valid `[TAG]`s are `[BREAKING]`, `[BUGFIX]`, `[CLEANUP]`, `[DEPRECATION]`,\n  `[DOCUMENTATION]`, `[ENHANCEMENT]`, `[FEATURE]`, `[TESTING]`, and `[UX]`.\n\n## Building & Testing\n\n* Build via `go build` to create the binary file `./gopass`.\n* Run unit tests with: `make test`\n* Run meta tests with: `make codequality`\n* Run integration tests `make test-integration`\n\nIf any of the above don't work check out the [troubleshooting section](#troubleshooting-build).\n\n## Releasing\n\nSee [docs/releases.md](docs/releases.md).\n\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM docker.io/library/golang:1.25-alpine@sha256:b6ed3fd0452c0e9bcdef5597f29cc1418f61672e9d3a2f55bf02e7222c014abd AS build-env\n\nENV CGO_ENABLED=0\n\nRUN apk add --no-cache make git ncurses\n\n# Build gopass\nWORKDIR /home/runner/work/gopass/gopass\n\nCOPY go.mod .\nCOPY go.sum .\nRUN go mod download\n\nCOPY . .\n\nARG goflags_arg=\"\"\nENV GOFLAGS=$goflags_arg\n\nRUN make clean\nRUN make gopass\n\n# Build gopass-jsonapi\nWORKDIR /home/runner/work/gopass\n\nRUN git clone https://github.com/gopasspw/gopass-jsonapi.git\n\nWORKDIR /home/runner/work/gopass/gopass-jsonapi\nRUN go mod download\nRUN make clean\nRUN make gopass-jsonapi\n\n# Build gopass-hibp\nWORKDIR /home/runner/work/gopass\n\nRUN git clone https://github.com/gopasspw/gopass-hibp.git\n\nWORKDIR /home/runner/work/gopass/gopass-hibp\nRUN go mod download\nRUN make clean\nRUN make gopass-hibp\n\n# Build gopass-summon-provider\nWORKDIR /home/runner/work/gopass\n\nRUN git clone https://github.com/gopasspw/gopass-summon-provider.git\n\nWORKDIR /home/runner/work/gopass/gopass-summon-provider\nRUN go mod download\nRUN make clean\nRUN make gopass-summon-provider\n\n# Build git-credential-gopass\nWORKDIR /home/runner/work/gopass\n\nRUN git clone https://github.com/gopasspw/git-credential-gopass.git\n\nWORKDIR /home/runner/work/gopass/git-credential-gopass\nRUN go mod download\nRUN make clean\nRUN make git-credential-gopass\n\nFROM docker.io/library/alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1\nRUN apk add --no-cache ca-certificates git gnupg\nCOPY --from=build-env /home/runner/work/gopass/gopass/gopass /usr/local/bin/\nCOPY --from=build-env /home/runner/work/gopass/gopass-jsonapi/gopass-jsonapi /usr/local/bin/\nCOPY --from=build-env /home/runner/work/gopass/gopass-hibp/gopass-hibp /usr/local/bin/\nCOPY --from=build-env /home/runner/work/gopass/gopass-summon-provider/gopass-summon-provider /usr/local/bin/\nCOPY --from=build-env /home/runner/work/gopass/git-credential-gopass/git-credential-gopass /usr/local/bin/\n"
  },
  {
    "path": "GOVERNANCE.md",
    "content": "# gopass project governance\n\n## Overview\n\nThe gopass project uses a governance model commonly described as Benevolent\nDictator For Life (BDFL). This document outlines our understanding of what this\nmeans. It is derived from the [i3 window manager project\ngovernance](https://raw.githubusercontent.com/i3/i3/next/.github/GOVERNANCE.md). \n\n## Roles\n\n* user: anyone who interacts with the gopass project\n* core contributor: a handful of people who have contributed significantly to\n  the project by any means (issue triage, support, documentation, code, etc.).\n  Core contributors are recognizable via GitHub’s “Member” badge.\n* Benevolent Dictator For Life (BDFL): a single individual who makes decisions\n  when consensus cannot be reached. gopass’s current BDFL is [@dominikschulz](https://github.com/dominikschulz).\n\n## Decision making process\n\nIn general, we try to reach consensus in discussions. In case consensus cannot\nbe reached, the BDFL makes a decision.\n\n## Contribution process\n\nPlease see [CONTRIBUTING](CONTRIBUTING.md).\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2017 JustWatch GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "FIRST_GOPATH              := $(firstword $(subst :, ,$(GOPATH)))\nPKGS                      := $(shell go list ./... | grep -v /tests | grep -v /xcpb | grep -v /gpb)\nGOFILES_NOVENDOR          := $(shell find . -name vendor -prune -o -type f -name '*.go' -not -name '*.pb.go' -print)\nGOFILES_BUILD             := $(shell find . -type f -name '*.go' -not -name '*_test.go')\nGOPASS_VERSION            ?= $(shell cat VERSION)\nGOPASS_OUTPUT             ?= gopass\nGOPASS_REVISION           := $(shell cat COMMIT 2>/dev/null || git rev-parse --short=8 HEAD)\nBASH_COMPLETION_OUTPUT    := bash.completion\nFISH_COMPLETION_OUTPUT    := fish.completion\nZSH_COMPLETION_OUTPUT     := zsh.completion\nCLIPHELPERS               ?= \"\"\n# Support reproducible builds by embedding date according to SOURCE_DATE_EPOCH if present\nDATE                      := $(shell date -u -d \"@$(SOURCE_DATE_EPOCH)\" '+%FT%T%z' 2>/dev/null || date -u '+%FT%T%z')\nBUILDFLAGS_NOPIE          := -buildvcs=true -tags=netgo -trimpath -ldflags=\"-s -w -X main.version=$(GOPASS_VERSION) -X main.commit=$(GOPASS_REVISION) -X main.date=$(DATE) $(CLIPHELPERS)\" -gcflags=\"-trimpath=$(GOPATH)\" -asmflags=\"-trimpath=$(GOPATH)\"\nBUILDFLAGS                ?= $(BUILDFLAGS_NOPIE) -buildmode=pie\nTESTFLAGS                 ?=\nPWD                       := $(shell pwd)\nPREFIX                    ?= $(GOPATH)\nBINDIR                    ?= $(PREFIX)/bin\nGO                        ?= GO111MODULE=on CGO_ENABLED=0 go\nGOOS                      ?= $(shell $(GO) version | cut -d' ' -f4 | cut -d'/' -f1)\nGOARCH                    ?= $(shell $(GO) version | cut -d' ' -f4 | cut -d'/' -f2)\nTAGS                      ?= netgo\nexport GO111MODULE=on\n\nOK := $(shell tput setaf 6; echo ' [OK]'; tput sgr0;)\n\nall: sysinfo build\nbuild: $(GOPASS_OUTPUT)\ncompletion: $(BASH_COMPLETION_OUTPUT) $(FISH_COMPLETION_OUTPUT) $(ZSH_COMPLETION_OUTPUT)\ngha-linux: sysinfo licensecheck crosscompile build fulltest completion\ngha-osx: sysinfo build test completion\ngha-windows: sysinfo build test-win completion\n\nsysinfo:\n\t@echo \">> SYSTEM INFORMATION\"\n\t@echo -n \"     PLATFORM   : $(shell uname -a)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     PWD:       : $(shell pwd)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     GO         : $(shell $(GO) version)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     BUILDFLAGS : $(BUILDFLAGS)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     GIT        : $(shell git version)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     GPG        : $(shell which gpg) $(shell gpg --version | head -1)\"\n\t@printf '%s\\n' '$(OK)'\n\t@echo -n \"     GPGAgent   : $(shell which gpg-agent) $(shell gpg-agent --version | head -1)\"\n\t@printf '%s\\n' '$(OK)'\n\nclean:\n\t@echo -n \">> CLEAN\"\n\t@rm -rf vendor/\n\t@$(GO) clean -i ./...\n\t@rm -f ./coverage-all.html\n\t@rm -f ./coverage-all.out\n\t@rm -f ./coverage.out\n\t@find . -type f -name \"coverage.out\" -delete\n\t@rm -f gopass_*.deb\n\t@rm -f gopass-*.pkg.tar.xz\n\t@rm -f gopass-*.rpm\n\t@rm -f gopass-*.tar.bz2\n\t@rm -f gopass-*.tar.gz\n\t@rm -f gopass-*-*\n\t@rm -f tests/tests\n\t@rm -f *.test\n\t@rm -rf dist/*\n\t@printf '%s\\n' '$(OK)'\n\n$(GOPASS_OUTPUT): $(GOFILES_BUILD)\n\t@echo -n \">> BUILD, version = $(GOPASS_VERSION)/$(GOPASS_REVISION), output = $@\"\n\t@$(GO) build -o $@ $(BUILDFLAGS)\n\t@printf '%s\\n' '$(OK)'\n\ninstall: all install-completion install-man\n\t@echo -n \">> INSTALL, version = $(GOPASS_VERSION)\"\n\t@install -m 0755 -d $(DESTDIR)$(BINDIR)\n\t@install -m 0755 $(GOPASS_OUTPUT) $(DESTDIR)$(BINDIR)/gopass\n\t@printf '%s\\n' '$(OK)'\n\ninstall-completion:\n\t@install -d $(DESTDIR)$(PREFIX)/share/zsh/site-functions $(DESTDIR)$(PREFIX)/share/bash-completion/completions $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d\n\t@install -m 0644 $(ZSH_COMPLETION_OUTPUT) $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_gopass\n\t@install -m 0644 $(BASH_COMPLETION_OUTPUT) $(DESTDIR)$(PREFIX)/share/bash-completion/completions/gopass\n\t@install -m 0644 $(FISH_COMPLETION_OUTPUT) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/gopass.fish\n\t@printf '%s\\n' '$(OK)'\n\ninstall-man: gopass.1\n\t@install -d -m 0755 $(DESTDIR)$(PREFIX)/share/man/man1\n\t@install -m 0644 gopass.1 $(DESTDIR)$(PREFIX)/share/man/man1/gopass.1\n\nfulltest: $(GOPASS_OUTPUT)\n\t@echo \">> TEST, \\\"full-mode\\\": race detector off\"\n\t@echo \"mode: atomic\" > coverage-all.out\n\t@$(foreach pkg, $(PKGS),\\\n\t    echo -n \"     \";\\\n\t\t$(GO) test -run '(Test|Example)' $(BUILDFLAGS) $(TESTFLAGS) -coverprofile=coverage.out -covermode=atomic $(pkg) || exit 1;\\\n\t\ttail -n +2 coverage.out >> coverage-all.out;)\n\t@$(GO) tool cover -html=coverage-all.out -o coverage-all.html\n\t@which go-cover-treemap > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/nikolaydubina/go-cover-treemap@latest; \\\n\tfi\n\t@go-cover-treemap -coverprofile coverage-all.out > coverage-all.svg\n\ntest: $(GOPASS_OUTPUT)\n\t@echo \">> TEST, \\\"fast-mode\\\": race detector off\"\n\t@$(foreach pkg, $(PKGS),\\\n\t    echo -n \"     \";\\\n\t\t$(GO) test -test.short -run '(Test|Example)' $(BUILDFLAGS) $(TESTFLAGS) $(pkg) || exit 1;)\n\ntest-win: $(GOPASS_OUTPUT)\n\t@echo \">> TEST, \\\"fast-mode-win\\\": race detector off\"\n\t@$(foreach pkg, $(PKGS),\\\n\t\t$(GO) test -test.short -run '(Test|Example)' $(pkg) || exit 1;)\n\ntest-integration: $(GOPASS_OUTPUT)\n\tcd tests && GOPASS_BINARY=$(PWD)/$(GOPASS_OUTPUT) GOPASS_TEST_DIR=$(PWD)/tests $(GO) test -v $(TESTFLAGS)\n\ncrosscompile:\n\t@echo \">> CROSSCOMPILE\"\n\t@which goreleaser > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/goreleaser/goreleaser/v2@v2.11.2; \\\n\tfi\n\t@goreleaser build --snapshot\n\n%.completion: $(GOPASS_OUTPUT)\n\t@printf \">> $* completion, output = $@\"\n\t@./gopass completion $* > $@\n\t@printf \"%s\\n\" \"$(OK)\"\n\ncodequality: licensecheck\n\t@echo \">> CODE QUALITY\"\n\n\t@echo -n \"     GOLANGCI-LINT \"\n\t@which golangci-lint > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.1; \\\n\tfi\n\t@golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 || exit 1\n\t@printf '%s\\n' '$(OK)'\n\n\t@echo -n \"     KEEP-SORTED   \"\n\t@which keep-sorted > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/google/keep-sorted@latest; \\\n\tfi\n\t@keep-sorted --mode lint $(GOFILES_NOVENDOR) || exit 1\n\t@printf '%s\\n' '$(OK)'\n\n\t@echo -n \"     CAPSLOCK      \"\n\t@which capslock > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install github.com/google/capslock/cmd/capslock@latest; \\\n\tfi\n\t@capslock -packages ./... -output=compare .capabilities.json || exit 1\n\t@printf '%s\\n' '$(OK)'\n\n\t@echo -n \"     GOVULNCHECK   \"\n\t@which govulncheck > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install golang.org/x/vuln/cmd/govulncheck@latest; \\\n\tfi\n\t@govulncheck >/dev/null || exit 1\n\t@printf '%s\\n' '$(OK)'\n\nlicensecheck:\n\t@echo \">> LICENSE CHECK\"\n\n\t@echo -n \"     LICENSE-LINT  \"\n\t@which license-lint > /dev/null; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install istio.io/tools/cmd/license-lint@latest; \\\n\tfi\n\t@license-lint --config .license-lint.yml >/dev/null || exit 1\n\n\t@printf '%s\\n' '$(OK)'\n\ngen:\n\t@$(GO) generate ./...\n\nfmt:\n\t@keep-sorted --mode fix $(GOFILES_NOVENDOR)\n\t@gofumpt -w $(GOFILES_NOVENDOR)\n\t@$(GO) mod tidy\n\ndeps:\n\t@$(GO) build -v ./...\n\nupgrade: gen fmt\n\t@$(GO) get -u ./...\n\t@$(GO) mod tidy\n\nman:\n\t@$(GO) run helpers/man/main.go > gopass.1\n\nmsi:\n\t@$(GO) run helpers/msipkg/main.go\n\ndocker:\n\tdocker build -t gopass:latest .\n\n.PHONY: clean build completion install sysinfo crosscompile test codequality release goreleaser debsign man msi docker\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"docs/logo.png\" height=\"250\" alt=\"gopass Gopher by Vincent Leinweber, remixed from the Renée French original Gopher\" title=\"gopass Gopher by Vincent Leinweber, remixed from the Renée French original Gopher\" />\n</p>\n\n# Overview\n\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/gopasspw/gopass/badge)](https://securityscorecards.dev/viewer/?uri=github.com/gopasspw/gopass)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/gopasspw/gopass/build.yml?branch=master)](https://github.com/gopasspw/gopass/actions/workflows/build.yml?query=branch%3Amaster)\n[![Go Report Card](https://goreportcard.com/badge/github.com/gopasspw/gopass)](https://goreportcard.com/report/github.com/gopasspw/gopass)\n[![Packaging status](https://repology.org/badge/tiny-repos/gopass-gopasspw.svg)](https://repology.org/project/gopass-gopasspw/versions)\n[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/gopasspw/gopass/blob/master/LICENSE)\n[![Github All Releases](https://img.shields.io/github/downloads/gopasspw/gopass/total.svg)](https://github.com/gopasspw/gopass/releases)\n[![Gopass Slack](https://img.shields.io/badge/%23gopass-Slack-brightgreen)](https://join.slack.com/t/gopassworkspace/shared_invite/zt-17jl74b5x-U1OUW4ts4AQ7eAf2V4QaaQ)\n\n> The slightly more awesome standard UNIX password manager for teams.\n\nManage your credentials with ease. In a globally distributed team, on multiple devices or fully offline on an air-gapped machine.\n\n- **Works everywhere** - The same user experience on Linux, MacOS, *BSD or Windows\n- **Built for teams** - Built from our experience working in distributed development teams\n- **Full autonomy** - No network connectivity required, unless you want it\n\n# How Does It Work?\n\nGopass is a drop-in replacement for pass, the standard UNIX password manager.\nBy default your credentials are encrypted with GPG and versioned in git. This can be customized easily.\nOther backends for encryption (e.g. age) and storage (e.g. fossil) are also available.\nThe primary interface is the command line, making it an excellent choice for CLI fans, CI/CD systems or\nanything you can hook it up with. Gopass can also integrate with your browser so you can largely avoid\nthe command line - if you want.\n\n# Installation\n\n## Necessary prerequisites for running `gopass`\n\n`gopass` can operate without any dependencies but most users will use it with `gpg` and `git`.\nAn external editor is required to use `gopass edit`.\n\n## Installation through package managers\n\n### [Homebrew](https://brew.sh) (Linux/MacOS)\n\n[![homebrew version](https://img.shields.io/homebrew/v/gopass)](https://github.com/Homebrew/homebrew-core/blob/master/Formula/gopass.rb)\n\n```shell\nbrew install gopass\n```\n\n### [MacPorts](https://www.macports.org) (macOS)\n\n[![macports version](https://repology.org/badge/version-for-repo/macports/gopass-gopasspw.svg)](https://ports.macports.org/port/gopass/)\n\n```shell\nsudo port install gopass\n```\n\n### Debian (Ubuntu, Debian, Raspbian, ...)\n\n**Warning**: Do not install the `gopass` package from the official repositories. That is a completely different project that has no relation to us.\n\n```shell\ncurl https://packages.gopass.pw/repos/gopass/gopass-archive-keyring.gpg | sudo tee /usr/share/keyrings/gopass-archive-keyring.gpg >/dev/null\ncat << EOF | sudo tee /etc/apt/sources.list.d/gopass.sources\nTypes: deb\nURIs: https://packages.gopass.pw/repos/gopass\nSuites: stable\nArchitectures: all amd64 arm64 armhf\nComponents: main\nSigned-By: /usr/share/keyrings/gopass-archive-keyring.gpg\nEOF\nsudo apt update\nsudo apt install gopass gopass-archive-keyring\n```\n\n### Fedora / RedHat / CentOS\n\n[![Fedora version](https://img.shields.io/fedora/v/gopass)](https://packages.fedoraproject.org/pkgs/gopass/gopass/)\n\n```shell\ndnf install gopass\n```\n\nNote: You might need to run `dnf copr enable daftaupe/gopass` first.\n\n### Arch Linux\n\n[![Arch version](https://img.shields.io/archlinux/v/extra/x86_64/gopass)](https://archlinux.org/packages/extra/x86_64/gopass/)\n\n```shell\npacman -S gopass\n```\n\n### Windows\n\n[![Scoop version](https://img.shields.io/scoop/v/gopass)](https://github.com/ScoopInstaller/Main/blob/master/bucket/gopass.json)\n\n```shell\n# WinGet\nwinget install Git.Git\nwinget install GnuPG.Gpg4win\nwinget install gopass.gopass\n# Chocolatey\nchoco install gpg4win\nchoco install gopass\n# Alternatively\nscoop install gopass\n```\n\n### FreeBSD / OpenBSD\n\n```shell\ncd /usr/ports/security/gopass\nmake install\n```\n\n### Alpine Linux\n\n```shell\napk add gopass\n```\n\n## Other installation options\n\nPlease see [docs/setup.md](https://github.com/gopasspw/gopass/blob/master/docs/setup.md) for other options.\n\n### From Source\n\n```shell\ngo install github.com/gopasspw/gopass@latest\n```\n\nNote: `latest` is not a stable release. We recommend to only use released versions.\n\n### Manual download\n\nDownload the [latest release](https://github.com/gopasspw/gopass/releases/latest) and add the binary to your PATH.\n\n# Quick start guide\n\nInitialize a new `gopass` configuration:\n\n```shell\ngopass setup\n\n   __     _    _ _      _ _   ___   ___\n /'_ '\\ /'_'\\ ( '_'\\  /'_' )/',__)/',__)\n( (_) |( (_) )| (_) )( (_| |\\__, \\\\__, \\\n'\\__  |'\\___/'| ,__/''\\__,_)(____/(____/\n( )_) |       | |\n \\___/'       (_)\n\n🌟 Welcome to gopass!\n🌟 Initializing a new password store ...\n🌟 Configuring your password store ...\n🎮 Please select a private key for encrypting secrets:\n[0] gpg - 0xFEEDBEEF - John Doe <john.doe@example.org>\nPlease enter the number of a key (0-12, [q]uit) (q to abort) [0]: 0\n❓ Do you want to add a git remote? [y/N/q]: y\nConfiguring the git remote ...\nPlease enter the git remote for your shared store []: git@gitlab.example.org:john/passwords.git\n✅ Configured\n```\n\nBy default `gopass setup` will use `gpg` encryption and `git` storage. This will create a new password store in `$HOME/.local/share/gopass/stores/root` and a configuration in `$HOME/.config/gopass/config` using `gpg` encryption and `git` for versioned storage. Users can override these with e.g. `--crypto=age` to use `age` encryption instead or opt out of using a versioned store with `--storage=fs`.\n\nAn existing store can be cloned with e.g. `gopass clone git@gitlab.example.org:john/passwords.git`.\n\nCreate a new secret:\n\n```shell\ngopass create\n```\n\nList all existing secrets:\n\n```shell\ngopass ls\n```\n\nCopy an existing password to the clipboard:\n\n```shell\ngopass show -c foo\n```\n\nRemove an existing secret:\n\n```shell\ngopass rm foo\n```\n\nOther examples:\n\n```shell\n# Command structure\ngopass [<command>] [options] [args]\n# Shortcut for gopass show [<key>]\ngopass [<key>]\n\n# Enter the gopass REPL\ngopass\n\n# Find all entries matching the search string\ngopass find github\n\n# List your store\ngopass ls\n\n# List all mounts\ngopass mounts\n\n# List all recipients\ngopass recipients\n\n# Sync with all remotes\ngopass sync\n\n# Setup a new store\ngopass setup\n```\n\n## Screenshot\n\n![screenshot](docs/showcase.png)\n\n## Support\n\nPlease ask on [Slack](https://join.slack.com/t/gopassworkspace/shared_invite/zt-17jl74b5x-U1OUW4ts4AQ7eAf2V4QaaQ).\n\n## Contributing\n\nWe welcome any contributions. Please see [CONTRIBUTING.md](https://github.com/gopasspw/gopass/blob/master/CONTRIBUTING.md) for more information.\n\n## Credit & License\n\ngopass is licensed under the terms of the MIT license. You can find the complete text in [`LICENSE`](https://github.com/gopasspw/gopass/blob/master/LICENSE).\n\nPlease refer to our [Contributors](https://github.com/gopasspw/gopass/graphs/contributors) page for a complete list of our contributors.\n"
  },
  {
    "path": "VERSION",
    "content": "1.16.1\n"
  },
  {
    "path": "bash.completion",
    "content": "_gopass_bash_autocomplete() {\n     local cur opts base\n     COMPREPLY=()\n     cur=\"${COMP_WORDS[COMP_CWORD]}\"\n     # Use error handling to prevent crashes from invalid flags\n     opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion 2>/dev/null ) || opts=\"\"\n     local IFS=$'\\n'\n     COMPREPLY=( $(compgen -W \"${opts}\" -- ${cur}) )\n     return 0\n }\n\ncomplete -F _gopass_bash_autocomplete gopass\n"
  },
  {
    "path": "docs/backends/age.md",
    "content": "# age crypto backend\n\nThe `age` backend is an experimental crypto backend based on [age](https://age-encryption.org). It adds an\nencrypted keyring on top (using age in scrypt password mode). It also has\n(largely untested) support for specifying recipients as github users. This will\nuse their ssh public keys for age encryption.\nIt is well positioned to eventually replace `gpg` as the default crypto backend.\n\n## Getting started\n\nWARNING: This backend is experimental and the on-disk format likely to change.\n\nTo start using the `age` backend initialize a new (sub) store with the `--crypto=age` flag:\n\n```\n$ gopass age identity add [AGE-... age1...]\n<if you do not specify an age secret key, you'll be prompted for one>\n$ gopass init --crypto age\n```\n\nor use the wizard that will help you create a new age key:\n```\n$ gopass setup --crypto age\n```\n\nThis will automatically create a new age keypair and initialize the new store.\n\nExisting stores can be migrated using `gopass convert --crypto age`.\n\nN.B. for a fully scripted or **non-interactive setup**, you can use the `GOPASS_AGE_PASSWORD` env variable\nto set your identity file secret passphrase, and specify the age identity and recipients\nthat should be used for encrypting/decrypting passwords as follows:\n```\n$ gopass age identity add <AGE-...> <age1...>\n$  GOPASS_AGE_PASSWORD=mypassword gopass init --crypto age <age1...>\n```\nNotice the extra space in front of the command to skip most shell's history.\nYou'll need to set your name and username using `git` directly if you're using it as storage backend (the default one).\n\nYou can also specify the ssh directory by setting environment variable\n```\n$  GOPASS_SSH_DIR=/Downloads/new_ssh_dir gopass init --crypto age <age1...>\n```\n\n## Features\n\n* Encryption using `age` library, can be decrypted using the `age` CLI\n* Support for native age, ssh-ed25519 and ssh-rsa recipients\n* Support for encrypted ssh private keys\n* Support for using GitHub users' private keys, e.g. `github:user` as recipient\n* Automatic downloading and caching of SSH keys from GitHub\n* Encrypted keyring for age keypairs\n* Support for age plugins\n* Caching of passphrases via an agent\n\n## Agent\n\nThe age backend comes with an agent that can cache the passphrases for your age identities.\nThe agent is started automatically by gopass if it's not already running.\nYou can disable the agent by setting `age.agent-enabled` to `false` in your gopass config.\n\nThe agent performs the decryption and the passphrase never leaves the agent process.\nThe agent listens on a unix socket at `$XDG_RUNTIME_DIR/gopass/gopass-age-agent.sock`.\n\nYou can interact with the agent using the following commands:\n- `gopass age agent`: starts the agent in the foreground.\n- `gopass age lock`: locks the agent, clearing all cached passphrases.\n\n## Usage with a yubikey\n\nTo use with a Yubikey, `age` requires the usage of the [age-plugin-yubikey plugin](https://github.com/str4d/age-plugin-yubikey/).\n\nAssuming you have Rust installed:\n```bash\n$ cargo install age-plugin-yubikey\n$ age-plugin-yubikey -i\n<should be empty>\n$ age-plugin-yubikey\n✨ Let's get your YubiKey set up for age! ✨\n<follow instructions to setup a PIV slot>\n$ age-plugin-yubikey -i\n<should display your PIV slot information now>\n$ gopass age identities add\nEnter the age identity starting in AGE-:\n<paste the `AGE-PLUGIN-YUBIKEY-...` identity from the previous command>\nProvide the corresponding age recipient starting in age1:\n<paste the `age1yubikey1...` recipient from the previous command>\n```\n\nIf gopass tells you `waiting on yubikey plugin...` when decrypting secrets, it probably is waiting for you to touch\nyour Yubikey because you've set a Touch policy when setting up your PIV slot.\n\n## Roadmap\n\nThe future of this backend largely depends on what is happening in the `age` project itself.\n\nAssuming `age` is supporting this, we'd like to:\n\n* Finalize GitHub recipient support\n* Add Hardware token support\n* Make age the default gopass backend\n"
  },
  {
    "path": "docs/backends/cryptfs.md",
    "content": "# cryptfs storage backend\n\nThe `cryptfs` backend is an experimental storage backend **PREVIEW**. It hashes secret names and stores the mapping from names to actual file inside an `age` encrypted lookup table. The filesystem backing this storage backend is flexible, but by default uses `gitfs`.\n\n**WARNING**: Do not use unless you want to contribute to the development of this backend!\n"
  },
  {
    "path": "docs/backends/fossilfs.md",
    "content": "# `fossilfs` storage backend\n\nThis is an **EXPERIMENTAL** storage backend that uses the Fossil SCM. It isn't well tested and only exists to provide an example how a non-git backend could look like.\n"
  },
  {
    "path": "docs/backends/fs.md",
    "content": "# fs storage backend\n\nThe simplest storage backend, often used for testing.\nIt stores data directly in the filesystem without any RCS support.\n"
  },
  {
    "path": "docs/backends/gitfs.md",
    "content": "# `gitfs` storage backend\n\nThis is the default storage backend. It stores the encrypted data directly in the filesystem. It uses an external git binary to provide history and remote sync operations.\n\ngopass configures git to use persistent ssh connections. If you do not want\nthis set `GIT_SSH_COMMAND` to an empty string to override the built-in default.\n"
  },
  {
    "path": "docs/backends/gpg.md",
    "content": "# gpg crypto backend\n\nThe `gpgcli` backend is the default crypto backend based on the `gpg` CLI. It depends on the GPG installation to be working and having a properly initialized keyring.\n\n## Getting started\n\nWARNING: This backend suffers from myriads of different configuration options, a poor scripting interface and not pure-Go libarary bindings being available.\n\nTo start using the `gpgcli` backend initialize a new (sub) store with the `--crypto=gpgcli` flag:\n\n```\ngopass init --crypto gpgcli\ngopass recipients add 0xDEADBEEF\n```\n\n## Features\n\n* Compatible with other password store implementations\n* Support for all GPG features, like smart-cards or hardware tokens\n\n## Caveats\n\n* Using long key sizes (e.g. 4096 bit or longer) can make many operations a lot slower\n* Some GPG installations don't work well with concurrent operations\n\n## Roadmap\n\nThis backend is the single most annoying source of maintenance workload in this project.\nWe try to keep this backend working as good as possible but there are a lot of reasons\nwhy we'd prefer eventually move beyond GPG.\n\n### GPG Critism\n\nThis section is a growing list of references why GPG is bad and why you should avoid it.\nThat might sound like an unusual thing to say for the authors of a tool whose main use case\nrelies on GPG but whenever we tried to move beyond GPG we got a lot of backlash. So I guess\nfirst we need to try to make use understand why you shouldn't hold on to GPG and by then we'll\ntry to have a replacement ready for you.\n\n* [What's the matter with PGP](https://blog.cryptographyengineering.com/2014/08/13/whats-matter-with-pgp/)\n* [The PGP Problem](https://latacora.micro.blog/2019/07/16/the-pgp-problem.html)\n* [I'm giving up on PGP](https://blog.filippo.io/giving-up-on-long-term-pgp/)\n* [GPG and Me](https://moxie.org/2015/02/24/gpg-and-me.html)\n"
  },
  {
    "path": "docs/backends/jjfs.md",
    "content": "# `jjfs` storage backend\n\nThis is an **EXPERIMENTAL** storage backend that uses the JJ / Git. It isn't well tested and only exists to provide an example how a non-git backend could look like."
  },
  {
    "path": "docs/backends.md",
    "content": "# Backends\n\ngopass supports pluggable backends for Storage and Revision Control System (`storage`) and Encryption (`crypto`).\n\nAs of today, the names and responsibilities of these backends are still unstable and will probably change.\n\nBy providing suitable backends, gopass can use different kinds of encryption or storage.\nFor example, it is pretty straightforward to add mercurial or bazaar as an SCM backend.\n\nAll backends are in their own packages below `backend/`. They need to implement the\ninterfaces defined in the backend package and have their identification added to\nthe context handlers in the same package.\n\n## Storage and RCS Backends (storage)\n\n* [fs](backends/fs.md) - Filesystem storage without RCS support\n* [gitfs](backends/gitfs.md) - Filesystem storage with Git RCS\n* [fossilfs](backends/fossilfs.md) - Filesystem storage with Fossil RCS. **Highly experimental, likely broken**. Use only if you want to contributed to the backend.\n* [jjfs](backends/jjfs.md) - Filesystem storage with JJ RCS. **Highly experimental, likely broken**. Use only if you want to contributed to the backend.\n* [cryptfs](backends/cryptfs.md) - Fully encrypted filesystem storage. **Highly experimental, likely broken**. Use only if you want to contributed to the backend.\n\n## Crypto Backends (crypto)\n\n* [gpgcli](backends/gpg.md) - depends on a working gpg installation\n* plain -  A no-op backend used for testing. WARNING: DOES NOT ENCRYPT!\n* [age](backends/age.md) -  This backend is based on [age](https://github.com/FiloSottile/age). It adds an encrypted keyring on top (using age in scrypt password mode). It also has (largely untested) support for specifying recipients as github users. This will use their ssh public keys for age encryption. This backend might very well become the new default backend.\n"
  },
  {
    "path": "docs/commands/audit.md",
    "content": "# `audit` command\n\nThe `audit` command will decrypt all secrets and scan for weak passwords or other common flaws.\n\n## Synopsis\n\n```\n$ gopass audit\n```\n\n## Excludes\n\nYou can exclude certain secrets from the audit by adding a `.gopass-audit-exclude` file to the secret. The file should contain a list of RE2 patters to exclude, one per line. For example:\n\n```\n# Lines starting with # are ignored. Trailing comments are not supported.\n# Exclude all secrets in the pin folder.\n# Note: These are RE2, not Glob patterns!\npin/.*\n# Literal matches are also valid RE2 patterns\ntest_folder/ignore_this\n# Gopass internally uses forward slashes as path separators, even on Windows. So no need to escape backslashes.\n```\n\n## Password strength backends\n\n| Backend                                         | Description                                                            |\n|-------------------------------------------------|------------------------------------------------------------------------|\n| [`crunchy`](https://github.com/muesli/crunchy)  | Crunchy password strength checker                                      |\n| `name`                                          | Checks if password equals the name of the secret                       |\n"
  },
  {
    "path": "docs/commands/cat.md",
    "content": "# `cat` command\n\nThe `cat` command is used to pipe password in and out of STDIN and STDOUT\nrespectively. As it is intended to be used with binary data, it encodes the\ndata-stream to store it.\n\n## Synopsis\n\n```bash\n$ echo \"test\" | gopass cat test/new\n$ gopass cat test/new\n```\n\n## Modes of operation\n\n* Create a new entry with data-stream from STDIN\n* Change an existing entry to data-stream from STDIN\n* Retrive encoded data from password-store and echo it to STDOUT\n\nCat is intended to work with binary data, so it accepts any kind of stream from\nSTDIN. It reads the binary-stream from STDIN and encodes it Base64 and saves it\nin the password store encoded, with some metadata about the input-stream and the\nused encoding (currently only Base64 supported).\n\n### Example\n```\n$ echo \"234\" | gopass cat test/new\n$ gopass show -f test/new\nSecret: test/new\n\n\ncontent-disposition: attachment; filename=\"STDIN\"\ncontent-transfer-encoding: Base64\nMjM0Cg==\n$ gopass cat test/new\n234\n```\n\n### Differences to `insert`\n\nIn contrast to `insert` it handles any kind of data-stream from STDIN and\nencodes it.\nDrawback: you can not just simply read the password with `gopass show`.\n\n## Flags\n\nThis command has currently no supported flags except the gopass globals.\n"
  },
  {
    "path": "docs/commands/clone.md",
    "content": "# `clone` command\n\nThe `clone` command allows cloning and setting up a new password store\nfrom a remote location, e.g. a remote git repo.\n\n## Synopsis\n\n```\n$ gopass clone git@example.com/store.git\n$ gopass clone git@example.com/store.git sub/store\n```\n\n## Flags\n\n| Flag       | Aliases | Description                                                     |\n|------------|---------|-----------------------------------------------------------------|\n| `--path`   |         | The path to clone the repo to.                                  |\n| `--crypto` |         | Override the crypto backend to use if the auto-detection fails. |\n"
  },
  {
    "path": "docs/commands/config.md",
    "content": "# `config` command\n\nThe config command allows displaying and altering configuration options.\n\nNote: To manage mounts use `gopass mounts`.\n\n## Synopsis\n\n```bash\ngopass config\ngopass config generate.autoclip\ngopass config generate.autoclip false\n```\n\n## Flags\n\n| Flag      | Description                    |\n|-----------|--------------------------------|\n| `--store` | Only sync a specific sub store |\n"
  },
  {
    "path": "docs/commands/convert.md",
    "content": "# `convert` command\n\nThe `convert` command exists to migrate stores between different backend\nimplementations.\n\nNote: This command exists to enable a possible migration path. If we agree\non a single set of backend implementations the multiple backend support\nmight go away and this command as well.\n\nWarning: Converting between different RCS backends will loose part of the history. While we try to retain as much information as possible especially the commit timestamps will be set to the convert time.\n\n## Synopsis\n\n```\n$ gopass convert --store=foo --move=true --storage=gitfs --crypto=age\n$ gopass convert --store=bar --move=false --storage=fs --crypto=plain\n```\n\n## Flags\n\nFlag | Description\n---- | -----------\n`--store` | Substore to convert.\n`--move` | Remove backup after converting? (default: `false`)\n`--storage` | Target storage backend.\n`--crypto` | Target crypto backend.\n"
  },
  {
    "path": "docs/commands/create.md",
    "content": "# `create` command\n\nThe `create` command creates a new secret using a set of built-in or custom templates.\nIt implements a wizard that guides inexperienced users through the secret creating.\n\nThe main design goal of this command was to guide users through the creation of a secret\nand asking for the necessary information to create a reasonable secret location.\n\n## Synopsis\n\n```bash\ngopass create\ngopass create --store=foo\n```\n\n## Modes of operation\n\n* Create a new secret using a wizard\n\n## Templates\n\n`gopass create` will look for files ending in `.yml` in the folder `.gopass/create` inside\nthe selected store (by default the root store).\n\nTo add new templates to the wizard add templates to this folder.\n\nExample:\n\n```bash\n$ cat $(gopass config mounts.path)/.gopass/create/aws.yml\n---\npriority: 5\nname: \"AWS\"\nprefix: \"aws\"\nname_from:\n  - \"org\"\n  - \"user\"\nwelcome: \"🧪 Creating AWS credentials\"\nattributes:\n  - name: \"org\"\n    type: \"string\"\n    prompt: \"Organization\"\n    min: 1\n  - name: \"user\"\n    type: \"string\"\n    prompt: \"User\"\n    min: 1\n  - name: \"password\"\n    type: \"password\" # hide input\n    prompt: \"Password\"\n    charset: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*\"\n    min: 10\n    strict: true # ensure at least one char from each detected class (upper, lower, digit, symbol)\n  - name: \"comment\"\n    type: \"string\"\n    prompt: \"Comments\"\n```\n\n## Template Attributes\n\nTemplate attributes support the following fields:\n\n| Field           | Type   | Description                                                                                                                                                                                                                                                                  |\n|-----------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `name`          | string | The name of the attribute. This will be used as the key in the secret's YAML data.                                                                                                                                                                                          |\n| `type`          | string | The type of attribute. Supported values: `string`, `hostname`, `password`.                                                                                                                                                                                                  |\n| `prompt`        | string | The prompt text to display to the user.                                                                                                                                                                                                                                     |\n| `charset`       | string | For password type: Custom character set to use when generating the password. If not specified, standard character classes will be used.                                                                                                                                      |\n| `min`           | int    | Minimum length validation for the attribute value.                                                                                                                                                                                                                           |\n| `max`           | int    | Maximum length validation for the attribute value.                                                                                                                                                                                                                           |\n| `always_prompt` | bool   | For password type: Always prompt for the password instead of offering password generation. Default: `false`.                                                                                                                                                                 |\n| `strict`        | bool   | For password type with `charset`: Enforce that all detected character classes (uppercase, lowercase, digits, symbols) present in the charset are represented in the generated password. Similar to `--strict` in `gopass generate`. Default: `false`.                        |\n\n## Flags\n\n| Flag      | Aliases | Description                                                      |\n|-----------|---------|------------------------------------------------------------------|\n| `--store` | `-s`    | Select the store to use. Will be used to look up user templates. |\n| `--force` | `-f`    | For overwriting existing entries.                                |\n| `--print` | `-p`    | Print the password to STDOUT.                                    |\n"
  },
  {
    "path": "docs/commands/delete.md",
    "content": "# `delete` command\n\nThe `delete` command is used to remove a single secret or a whole subtree.\n\nNote: Recursive operations crossing mount points are intentionally not supported.\n\n## Synopsis\n\n```\n$ gopass delete entry\n$ gopass rm -r path/to/folder\n$ gopass rm -f entry\n$ gopass delete entry key\n```\n\n## Modes of operation\n\n* Delete a single secret\n* Delete a single key from an existing secret\n* Delete a directoy of secrets\n\n## Flags\n\n| Flag          | Aliases | Description                           |\n|---------------|---------|---------------------------------------|\n| `--recursive` | `-r`    | Recursively delete files and folders. |\n| `--force`     | `-f`    | Do not ask for confirmation.          |\n\n## Details\n\n* Removing a single key will need to decrypt the secret\n"
  },
  {
    "path": "docs/commands/edit.md",
    "content": "# `edit` command\n\nThe `edit` command loads a new or existing secret into your `$EDITOR` (default: `vim`)\nand saves the resulting content in the password store. It will attempt to create secure\ntemporary directory (depending on the OS) and will warn if insecure editor configuration\n(currently only `vim`) is detected.\n\nNative `gopass` MIME secrets are syntax checked and invalid encodings are rejected.\nAny other type of secret is accepted as is.\n\n`gopass` will honor templates when creating a new entry.\n\n## Synopsis\n\n```\n$ gopass edit entry\n$ gopass edit -e /bin/nano entry\n$ EDITOR=/bin/nano gopass edit entry\n```\n\n## Modes of operation\n\n* Create a new secret\n* Edit an existing secret\n\n## Flags\n\n| Flag       | Aliases | Description                                                                                                                           |\n|------------|---------|---------------------------------------------------------------------------------------------------------------------------------------|\n| `--editor` | `-e`    | Specify the path to an editor. Must accept the filename as it's first argument.                                                       |\n| `--create` | `-c`    | Create a new secret. You can create a new secret with `edit` with or without `-c`, but `-c` will skip searching for existing matches. |\n"
  },
  {
    "path": "docs/commands/env.md",
    "content": "# `env` command\n\nThe `env` command runs a binary as a subprocess with a pre-populated environment.\nThe environment of the subprocess is populated with a set of environment variables corresponding\nto the secret subtree specified on the command line.\n\n## Synopsis\n\n```\n$ gopass env entry env\n```\n\n"
  },
  {
    "path": "docs/commands/find.md",
    "content": "# `find` command\n\nThe `find` command will attempt to do a simple substring match on the names of all secrets.\nIf there is a single match it will directly invoke `show` and display the result.\nIf there are multiple matches a selection will be shown.\n\nNote: The find command will not fall back to a fuzzy search.\n\n## Synopsis\n\n```\n$ gopass find entry\n$ gopass find -f entry\n$ gopass find -c entry\n```\n\n## Flags\n\n| Flag       | Aliases | Description                                                   |\n|------------|---------|---------------------------------------------------------------|\n| `--clip`   | `-c`    | Copy the password into the clipboard.                         |\n| `--unsafe` | `-u`    | Display any unsafe content, even if `safecontent` is enabled. |\n\n"
  },
  {
    "path": "docs/commands/fsck.md",
    "content": "# `fsck` command\n\n`gopass` can check integrity of it's password stores with the `fsck` command.\nIt will ensure proper file and directory permissions as well as proper\nrecipient coverage (on supported crypto backends, only).\n\n## Synopsis\n\n```\n$ gopass fsck\n```\n\n## Modes of operation\n\n* Check the entire password store, incl. all mounts\n* Check only the specified mount\n\n## Flags\n\nFlag | Aliases | Description\n---- | ------- | -----------\n`--decrypt` | | Decrypt and reencrypt all secrets.\n"
  },
  {
    "path": "docs/commands/fscopy.md",
    "content": "# `fscopy` command\n\nThe `fscopy` command is used to copy a file from your filesystem into your\npassword store, while keeping it in clear in your local filesystem after\nhaving stored it in your encrypted store.\n\n## Synopsis\n\n```bash\n$ gopass fscopy ~/test/file data/test/file-entry\n$ gopass fscopy data/test/file-entry ~/file\n```\n\n## Modes of operation\n\nThis command either reads a file from the filesystem and writes the\nencoded and encrypted version in the store or it decrypts and decodes\na secret and writes the result to a file. Either source or destination\nmust be a file and the other one a secret.\nIf you want the source to be removed use 'gopass fsmove'.\n\n`fscopy` is intended to work with raw files.\n\n### Example\n```\n$ gopass fscopy ~/test/file data/test/file-entry\n$ gopass cat data/test/file-entry\n```\n\nSee also the docs for the [`cat` action](cat.md).\n\n## Flags\n\nThis command has currently no supported flags except the gopass globals.\n"
  },
  {
    "path": "docs/commands/fsmove.md",
    "content": "# `fsmove` command\n\nThe `fsmove` command is used to move a file from your filesystem into your\npassword store, erasing it from your local filesystem after having stored it in your encrypted store.\n\n## Synopsis\n\n```bash\n$ gopass fsmove ~/test/file data/test/file-entry\n$ gopass fsmove data/test/file-entry ~/file\n```\n\n## Modes of operation\n\nThis command either reads a file from the filesystem and writes the\nencoded and encrypted version in the store or it decrypts and decodes\na secret and writes the result to a file. Either source or destination\nmust be a file and the other one a secret. The source will be wiped\nfrom disk or from the store after it has been copied successfully\nand validated. If you don't want the source to be removed use\n'gopass fscopy'.\n\n`fsmove` is intended to work with raw files.\n\n### Example\n```\n$ gopass fsmove ~/test/file data/test/file-entry\n$ gopass cat data/test/file-entry\n```\n\nSee also the docs for the [`cat` action](cat.md).\n\n## Flags\n\nThis command has currently no supported flags except the gopass globals.\n"
  },
  {
    "path": "docs/commands/generate.md",
    "content": "# `generate` command\n\nThe `generate` command is used to generate a new password and store it into the password store.\n\nNote: If you only want generate a password without storing it in the store, use the `pwgen` command.\n\n## Synopsis\n\n```\n$ gopass generate entry [length]\n$ gopass generate entry key [length]\n```\n\n## Modes of operation\n\n* Generate a new entry with a new password, e.g. a new login. Setting the `Password` field, `gopass generate entry [chars]`\n* Re-generating a new password and setting it in the `Password` field of an existing entry\n* Generate a new password and setting it to a new key of an existing secret, e.g. `gopass generate entry key [chars]`\n* Re-generate a new password for an existing key in an existing entry\n\n## Flags\n\n| Flag          | Aliases | Description                                                                                                                                                        |\n|---------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `--clip`      | `-c`    | Copy the generated password into the clipboard. Default: Value of `autoclip`                                                                                       |\n| `--print`     | `-p`    | Print the generated password to the terminal. Default: false.                                                                                                      |\n| `--force`     | `-f`    | Force overwriting an existing entry.                                                                                                                               |\n| `--edit`      | `-e`    | Generate a password and open the entry for editing in `$EDITOR`.                                                                                                   |\n| `--generator` | `-g`    | Choose of of the available password generators, desribed below. Default: `cryptic`                                                                                 |\n| `--symbols`   | `-s`    | Include symbols in the generated password (default: `false`)                                                                                                       |\n| `--strict`    |         | Ensure each requested character class is actually included. Without this option all requested classes can be included, but not necessarily are. (default: `false`) |\n| `--sep`       |         | Word separator for multi-word generators.                                                                                                                          |\n| `--lang`      |         | Language for word-based generators.                                                                                                                                |\n\n## Password Generators\n\nUse `--generator` to select one of the available password generators:\n\n| Generator   | Description                                                                                                                                                                                                                                                                      |\n|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `cryptic`   | The default generator yields cryptic passwords that should work with most sites. Use `--symbols` and `--strict` if the site has specific requirements. Please note that we auto-detect the correct rules for some sites. The length argument specifies the number of characters. |\n| `xkcd`      | Use an [XKCD#936](https://xkcd.com/936/) style password. Use `--lang` and `--sep` to refine it's behaviour. The length argument specifies the number of words.                                                                                                                   |\n| `memorable` | Generate a memorable password. The length argument specifies the minimum lenght of characters. Please note that the password might be longer if not all necessary rules were satisfied by the minimum length solution.                                                           |\n| `external`  | Use the external generator from `$GOPASS_EXTERNAL_PWGEN`                                                                                                                                                                                                                         |\n\n## Relevant configuration options\n\n* `autoclip` only applies to `generate`. If set the generated password is automatically copied to the clipboard - unless `--clip` is explicitly set to `--clip=false`\n* `safecontent` will suppress printing of the password, unless `-p` is set. The password will not be copied, unless `-c` or the `autoclip` option are set.\n\n## Templates\n\nWhen creating a new entry gopass will look for the most specific template\nby going up in the secret path looking for a file called `.pass-template`.\n\nIf any such file is found it will be used to pre-populate the generated\nsecret.\n"
  },
  {
    "path": "docs/commands/gopass.md",
    "content": "# `gopass` command\n\nCalling `gopass` without any command argument is a common entry point and\nhas two different modes.\n\n## Synopsis\n\n```\n$ gopass\n$ gopass entry\n$ gopass -c entry\n```\n\n## Modes of operation\n\n* Invoked without any arguments `gopass` will start an interactive REPL shell. This includes zero-setup command completion and passphrase caching (for non-GPG backends).\n* Invoked with one argument it will perform a (fuzzy) search and display a list of matches or the secret directly (if exactly one match).\n* Invoked with two arguments it will do search and if there is a match display the named key.\n\n## Flags\n\nNote: DO NOT use in scripts! Use `gopass show` instead.\n\n| Flag       | Aliases | Description                                                                                                                |\n|------------|---------|----------------------------------------------------------------------------------------------------------------------------|\n| `--clip`   | `-c`    | Copy the password value into the clipboard and don't show the content.                                                     |\n| `--unsafe` | `-u`    | Display unsafe content (e.g. the password) even when the `safecontent` option is set. No-op when `safecontent` is `false`. |\n| `--yes`    |         | Assume yes on all yes/no questions or use the default on all others.                                                       |\n\n"
  },
  {
    "path": "docs/commands/grep.md",
    "content": "# `grep` command\n\nThe `grep` command works like the Unix `grep` tool. It decrypts all secrets\nand performs a substring or regexp match on the given pattern.\n\n## Synopsis\n\n```\n$ gopass grep foobar\n```\n\n## Modes of operations\n\n* Search for the given pattern in all secrets\n\n## Flags\n\nNone.\nFlag | Aliases | Description\n---- | ------- | -----------\n`--regexp` | | Parse the pattern as a RE2 regular expression.\n"
  },
  {
    "path": "docs/commands/history.md",
    "content": "# `history` command\n\nThe `gopass history` command will show all revisions of a given secret.\n\n## Synopsis\n\n```\n$ gopass history entry\n```\n\n## Modes of operation\n\n* Display all revisions of the given secret.\n\n## Flags\n\nNone.\n"
  },
  {
    "path": "docs/commands/init.md",
    "content": "# `init` command\n\nThe `init` command is used to initialize a new password store.\nIf no recipients are specified a useable existing private key is used.\n\nThe `init` command must be used to initilize new mounts. `gopass mounts add` only supports adding existing mounts.\n\nNote: We do not support adding recipients using `init`. Please use `gopass recipients add` for that!\n\n## Synopsis\n\n```\n$ gopass init\n$ gopass init --crypto [age|gpg] --storage=[fs|gitfs]\n```\n\n## Flags\n\n| Flag        | Aliases | Description                                                                                                 |\n|-------------|---------|-------------------------------------------------------------------------------------------------------------|\n| `--path`    | `-p`    | Initialize the (sub) store in this location.                                                                |\n| `--store`   | `-s`    | Mount the newly initialized sub-store at this mount point                                                   |\n| `--crypto`  |         | Select the crypto backend. Choose one of: `gpgcli`, `age`, `xc` (deprecated)  or `plain`. Default: `gpgcli` |\n| `--storage` |         | Select the storage and RCS backend. Choose one of: `gitfs`, `fs`. Default: `gitfs`                          |\n\nSee [backends.md](../backends.md) for more information on the available backends.\n"
  },
  {
    "path": "docs/commands/insert.md",
    "content": "# `insert` command\n\nThe `insert` command is used to manually set (insert, or change) a password in the store. It applies to either new or existing secrets.\n\n## Synopsis\n\n```\n$ gopass insert entry\n$ gopass insert entry key\n```\n\n## Modes of operation\n\n* Create a new entry with a user-supplied password, e.g. a new site with a user-generated password or one picked from `gopass pwgen`: `gopass insert entry`\n* Change an existing entry to a user-supplied password\n* Create and change any field of a new or existing secret: `gopass insert entry key`\n* Read data from STDIN and insert (or append) to a secret\n\nInsert is similar in effect to `gopass edit` with the advantage of not displaying any content of the secret when changing a key.\n\nNote: `insert` will not change anything but the `Password` field (using the `insert entry` invocation) or the specified key (using the `insert entry key` invocation).\n\n## Flags\n\n| Flag          | Aliases | Description                                                                                                            |\n|---------------|---------|------------------------------------------------------------------------------------------------------------------------|\n| `--echo`      | `-e`    | Display the secret while typing (default: `false`)                                                                     |\n| `--multiline` | `-m`    | Insert using `$EDITOR` (default: `false`). This identical to running `gopass edit entry`. All other flags are ignored. |\n| `--force`     | `-f`    | Overwrite any existing value and do not prompt. (default: `false`)                                                     |\n| `--append`    | `-a`    | Append to any existing data. Only applies if reading from STDIN. (default: `false`)                                    |\n"
  },
  {
    "path": "docs/commands/link.md",
    "content": "# `link` command\n\nThe `link` (or `ln`) command is used to create a symlink from one secret in a\nstore to a target in the same store.\n\nNote: Symlinks across different stores / mounts are currently not supported!\n\nNote: `audit` and `list` do not recognize symlinks, yet. They will treat\nsymlinks as regular (different) entries.\n\n## Synopsis\n\n```\n$ gopass ln foo/bar bar/baz\n$ gopass show foo/bar\n$ gopass show bar/baz\n```\n\n## Modes of operations\n\n* Create a symlink from an existing secret to a new name, the target must not exist, yet\n\nNote: Use `gopass rm` to remove a symlink.\n\n## Flags\n\nNone.\n\n"
  },
  {
    "path": "docs/commands/list.md",
    "content": "# `list` command\n\nThe `list` command is used to list all the entries in the password store or at a given prefix.\n\n## Synopsis\n\n```bash\ngopass ls\ngopass ls path/to/entries\n```\n\n- List all the entries in the password store including the one in mounted stores: `gopass list`\n- List all the entries in a given folder showing their relative path from the root: `gopass list path/to/entries`\n\nNote: `list` will not change anything, nor encrypt or decrypt anything.\n\n## Flags\n\n| Flag             | Aliases    | Description                                         |\n|------------------|------------|-----------------------------------------------------|\n| `--limit value`  | `-l value` | Max tree depth (default: -1)                        |\n| `--flat`         | `-f`       | Print a flat list of secrets (default: false)       |\n| `--folders`      | `-d`       | Print a flat list of folders (default: false)       |\n| `--strip-prefix` | `-s`       | Strip prefix from filtered entries (default: false) |\n\nThe `--flat` and `--folders` flags provide a plaintext list of the entries located at\nthe given prefix (default prefix being the root `/`). They are notably used to produce the\ncompletion results.\nThe `--flat` one will list all entries, one per line, using its full path.\nThe `--folders` one will display all the folders, one per line, recursively per level.\nFor instance an entry `folder/sub/entry` would cause it to list both:\n\n```bash\n$ gopass list --folders\nfolder\nfolder/sub\n```\n\nwhereas `gopass list --flat` would have just displayed one line: `folder/sub/entry`.\n\nThe `--strip-prefix` flag is meant to be used along with `--flat` or `--folders`.\nIt will list the relative path from the current prefix, removing the said prefix,\ninstead of listing the relative paths from the root.\nFor instance on entry `folder/sub/entry`, running `gopass ls -f -s folder` would display\n only `sub/entry` instead of `folder/sub/entry`.\n\nThe `--limit` flag starts counting its depth from the root store, which means that\na depth of 0 only lists the items in the root gopass store:\n\n```bash\n$ gopass list -l 0\ngopass\n├── bar/\n├── foo/\n└── test (/home/user/.local/share/gopass/stores/substore1)\n```\n\nA value of 1 would list all the items in the root, plus their sub-items but no more:\n\n```bash\n$ gopass list -l 1\ngopass\n├── bar/\n│   └── bar\n├── foo/\n│   ├── bar\n│   └── foo\n└── test (/home/user/.local/share/gopass/stores/substore1)\n    └── foo\n```\n\nA negative value lists all the items without any depth limit.\n\n```bash\n$ gopass list -l -1\ngopass\n├── bar/\n│   └── bar\n├── foo/\n│   ├── bar/\n│   │   ├── bar/\n│   │   │   └── bar\n│   │   └── baz\n│   └── foo\n└── test (/home/user/.local/share/gopass/stores/substore1)\n    └── foo\n```\n\nThe flags can be used together: `gopass -l 1 -d` will list only the folders up to a depth of 1:\n\n```bash\n$ gopass list -l 1 -d\nbar/\nfoo/\nfoo/bar/\ntest/\ntest/foo/\n```\n\n## Shadowing\n\nIt is possible to have a path that is both an entry and a folder. In that case the list command\nwill display the folder with a marker of `(shadowed)`, it can still be accessed using\n`gopass show path/to/it`, while the content of the folder can be listed using `gopass list path/to/it`.\n\nIt should also be noted that the `mount` command can completely \"shadow\" an entry in a password store,\nsimply by having the same name and this entry and its subentries will not be visible\nusing `ls` anymore until the substore is unmounted.\nThe entries shadowed by a mount will not show up in a search and cannot be accessed at all without unmounting.\n\nFor instance in our example above, maybe there is an entry test/zaz in the root store,\nbut since the substore is mounted as `test/`, it only displays the content of the substore.\nUnmounting it reveals its shadowed entries:\n\n```bash\n$ gopass list test\ntest/ \n└── foo\n$ gopass mounts rm test\n$ gopass list test\ntest/ \n└── zaz\n```\n"
  },
  {
    "path": "docs/commands/mounts.md",
    "content": "# `mounts` commands\n\nThe `mounts` commands allow managing mounted substores. This is one of the\ndistinctive core features of `gopass` and we aim making working with substores\nas seamless as possible.\n\nInstead of support for encrypting different parts of a store for different\nrecipients we instead encourage users to mount different stores - each\nencrypted to a uniform set of recipients - into a semless virtual tree structure.\n\nThis feature is modeled after standard POSIX mount semantics.\n\n## Synopsis\n\n```\n$ gopass mounts\n$ gopass mounts add mount/point /path/to/store\n$ gopass mounts remove mount/point\n```\n\n## Modes of operation\n\n* Add a new mount\n* List existing mounts\n* Remove an existing mount\n\n## Creating new mounts\n\nYou can also create new mounts using `init` even if your store is already initialized:\n\n```\ngopass init --store mynewsubstore pgpkeyidentitfier\n```\n\n(You can also specify a specific local path using `--path`, just make sure to keep your PGP key identifier, e.g. its email or fingerprint, as the last argument.)\n"
  },
  {
    "path": "docs/commands/move.md",
    "content": "# `move` command\n\nNote: The implementations for `copy` and `move` are exactly the same. The only difference is that `move` will remove the source after a successful copy.\n\nThe `move` command works like the Unix `mv` or `rsync` binaries. It allows moving either single entries or whole folders around. Moving across mounts is supported.\n\nIf the source is a directory, the source directory is re-created at the destination if no trailing slash is found. Otherwise the contained secrets are placed into the destination directory (similar to what `rsync` does).\n\nPlease note that `move` will always decrypt the source and re-encrypt at the destination.\n\nMoving a secret onto itself is a no-op.\n\n## Synopsis\n\n```\n# Overwrite new/leaf\n$ gopass move path/to/leaf new/leaf\n# Move the content of path/to/somedir to new/dir/somedir\n$ gopass move path/to/somedirdir new/dir\n# Does nothing\n$ gopass move entry entry\n```\n\n## Modes of operation\n\n* Move a single secret from source to destination\n* Move a folder of secrets, possibly with sub folders, from source to destination\n\n## Flags\n\n| Flag      | Aliases | Description                                    |\n|-----------|---------|------------------------------------------------|\n| `--force` | `-f`    | Overwrite existing destination without asking. |\n\n## Details\n\n* To simplify the implementation and support multiple backends a `copy` or `move` operation will always decrypt and re-encrypt all affected secrets. Even if moving encrypted files around might be possible.\n* You can move a secret to another secret, i.e. overwrite the destination. But `gopass` won't let you move a directory over a file. In that case you have to delete the destination first.\n\n"
  },
  {
    "path": "docs/commands/otp.md",
    "content": "# `otp` command\n\nThe `otp` command generates TOTP tokens from an OTP URL (`otpauth://`).\nThe command tries to parse the password and the totp fields as an OTP URI.\n\nNote: HTOP is supported, but requires a `counter` field to keep track of it.\n\nNote: If `show.safecontent` is enabled, OTP URIs are hidden from the `show` command,\nsee the [docs for show](show.md#parsing-and-secrets) to learn more about it.\n\n## Modes of operation\n\n* Generate the current TOTP token from a valid OTP URL\n* Snip the screen to add a TOTP QR code as an OTP field to an entry.\n\n## Flags\n\n| Flag         | Aliases | Description                                                              |\n|--------------|---------|--------------------------------------------------------------------------|\n| `--clip`     | `-c`    | Copy the time-based token into the clipboard.                            |\n| `--alsoclip` | `-C`    | Copy the time-based token into the clipboard and show it.                |\n| `--qr`       | `-q`    | Write QR code to file.                                                   |\n| `--chained`  | `-p`    | chain the token to the password                                          |\n| `--password` | `-o`    | Only display the token. For use in scripts.                              |\n| `--snip`     | `-s`    | Try and find a QR code in the screen content to add as OTP to the entry. |\n\n## Supported formats\n\nYour secret needs to either contain a `otpauth`, `hotp` or a `totp` field.\nWhen using the OTP code directly you can simply add it to a secret using\n`gopass insert your/entry totp`.\n\nThe `otp` command also tries to parse the body of your secret to try and find a line starting\nby `otpauth://` in case you're not using the key-value format for your secret.\n\nFinally, if your secret contains nothing but a password on the first line, the `otp` command\nwill try and use that password to generate an OTP code. This allows use-cases where you\nstore your password in a given entry and your OTP code in another dedicated entry.\n\nThe otpauth URIs are typically communicated through a QR code which can be read on Linux using\nthe `gopass otp -s your/entry` flag. It should also work if they are added using\n`gopass insert your/entry otpauth`, but won't work if you add them under the `totp`\nor `hotp` keys.\n\nSteam OTP is supported, but requires using the `otpauth` URI input to specify the\nencoder, e.g. `otpauth://totp/username%20steam:username?secret=qlt6vmy6svfx4bt4rpmisaiyol6hihca&period=30&digits=5&issuer=username%20steam&encoder=steam`.\n"
  },
  {
    "path": "docs/commands/process.md",
    "content": "# `process` command\n\nThe `process` command extends the `gopass` templating to support user-supplied\ntemplate files that will be processed. These templates can access the users\ncredentials with the template functions documented below. That way users can\nstore their full configuration files publicly accessible and have any of the\nrecipients automatically populate it to generate a complete configuration file\non the fly.\n\n`gopass process` writes the result to `STDOUT`. You'll likely want to redirect\nit to a file.\n\n## Synopsis\n\n```\n$ gopass process <TEMPLATE> > <OUTPUT>\n```\n\n## Flags\n\nNone.\n\n## Examples\n\nThe templates are processed using Go's [`text/template`](https://pkg.go.dev/text/template) package.\nA set of helpful template functions is added to the template. See below for a list.\n\n### Populate a MySQL configuration\n\n```\n$ cat /etc/mysql/my.cnf.tpl\n[client]\nhost=127.0.0.1\nport=3306\nuser={{ getval \"server/local/mysql\" \"username\" }}\npassword={{ getpw \"server/local/mysql\" }}\n$ gopass process /etc/mysql/my.cnf.tpl\n[client]\nhost=127.0.0.1\nport=3306\nuser=admin\npassword=hunter2\n```\n\n## Template functions\n\nFunction | Example | Description\n-------- | ------- | -----------\n`md5sum` | `{{ getpw \"foo/bar\" \\| md5sum }}` | Calculate the hex md5sum of the input.\n`sha1sum` | `{{ getpw \"foo/bar\" \\| sha1sum }}` | Calculate the hex sha1sum of the input.\n`md5crypt` | `{{ getpw \"foo/bar\" \\| md5crypt }}` | Calculate the md5crypt of the input.\n`ssha` | `{{ getpw \"foo/bar\" \\| ssha }}` | Calculate the salted SHA-1 of the input.\n`ssha256` | `{{ getpw \"foo/bar\" \\| ssha256 }}` | Calculate the salted SHA-256 of the input.\n`ssha512` | `{{ getpw \"foo/bar\" \\| ssha512 }}` | Calculate the salted SHA-512 of the input.\n`get` | `{{ get \"foo/bar\" }}` | Insert the full secret.\n`getpw` | `{{ getpw \"foo/bar\" }}` | Insert the value of the password field from the given secret.\n`getval` | `{{ getval \"foo/bar\" \"baz\" }}` | Insert the value of the named field from the given secret.\n`argon2i` | `{{ getpw \"foo/bar\" \\| argon2i }}` | Calculate the Argon2i hash of the input.\n`argon2id` | `{{ getpw \"foo/bar\" \\| argon2id }}` | Calculate the Argon2id hash of the input.\n`bcrypt` | `{{ getpw \"foo/bar\" \\| bcrypt }}` | Calculate the Bcrypt hash of the input.\n`blake3` | `{{ getpw \"foo/bar\" \\| blake3 }}` | Calculate the BLAKE-3 hash of the input.\n"
  },
  {
    "path": "docs/commands/pwgen.md",
    "content": "# `pwgen` command\n\nThe `pwgen` command implements a subset of the features of the Unix/Linux\n`pwgen` command line tool. It aims to eventually support most of the `pwgen`\nflags and mirror it's behaviour. It is mainly implemented as a curtosy for\nWindows users.\n\n## Modes of operation\n\n* Generate a few dozen random passwords with the chosen length\n\n## Usage\n\n```bash\ngopass pwgen [optional length]\n```\n\n## Synopsis\n\n```bash\ngopass pwgen\ngopass pwgen 24\n```\n\n## Flags\n\nFlag | Aliases | Description\n---- | ------- | -----------\n`--no-numerals` | `-0` | Do not include numerals in the generated passwords.\n`--one-per-line` | `-1` | Print one password per line.\n`--xkcd` | `-x` | Use multiple random english words combined to a password.\n`--sep` | `--xs` | Word separator for multi-word passwords.\n`--lang` | `--xl` | Language to generate password from. Currently only supports english (en, default).\n"
  },
  {
    "path": "docs/commands/recipients.md",
    "content": "# `recipients` commands\n\nThe set of `recipients` commands allow managing public keys that are able to\ndecrypt a given password store.\n\nThese commands are one of the more unique `gopass` features and we aim to\nmake working with this as seamless as possible.\n\n## Synopsis\n\n```\n$ gopass recipients\n$ gopass recipients add\n$ gopass recipients remove\n$ gopass recipients ack\n```\n\n## Modes of operation\n\n* List all existing recipients, per mount: `gopass recipients`\n* Add/Authorize a new public key to decrypt a store (mount): `gopass recipients add`\n* Remove/Deuathorize an existing public key from a store (mount): `gopass recipients remove`\n* Acknowledge changes in the `recipients.hash`\n\n## Flags\n\nFlag | Aliases | Description\n`--store` | | Store to operate on.\n`--force` | | Do not ask for confirmation.\n\n## Important Remarks\n\nWARNING: Removing a recipient can only ever work for new or changed secrets.\nWhen a recipient is removed they will still be able to access anything that\nthey used to have access to. As a logical consequence one **should** change\nall secrets when removing a recipient.\n\n## Recipients hashing\n\nThis is an experimental feature that will hash the content of each mounts\nrecipients file (only the top most file) to display a warning when this is\nchanged by anyone else (local changes update it without warning). This\ncan happen either when a teammate modifies that file or when an attacker\ntries to modify the recipients file in the central storage to get themselves\nadded to any newly modified secrets.\n"
  },
  {
    "path": "docs/commands/show.md",
    "content": "# `show` command\n\nThe `show` command is the most important and most frequently used command.\nIt allows displaying and copying the content of the secrets managed by gopass.\n\n## Synopsis\n\n```\n$ gopass show entry\n$ gopass show entry key\n$ gopass show entry --qr\n$ gopass show entry --password\n```\n\n## Modes of operation\n\n* Show the whole entry: `gopass show entry`\n* Show a specific key of the given entry: `gopass show entry key` (only works for key-value or YAML secrets)\n\n## Flags\n\nFlag | Aliases | Description\n---- | ------- | -----------\n`--clip` | `-c` | Copy the password value into the clipboard and don't show the content.\n`--alsoclip` | `-C` | Copy the password value into the clipboard and show the content.\n`--qr` | | Encode the password field as a QR code and print it. Note: When combining with `-c`/`-C` the unencoded password is copied. Not the QR code.\n`--unsafe` | `-u` | Display unsafe content (e.g. the password) even when the `safecontent` option is set. No-op when `safecontent` is `false`.\n`--password` | `-o` | Display only the password. For use in scripts. Takes precedence over other flags.\n`--revision` | `-r` | Display a specific revision of the entry. Use an exact version identifier from `gopass history` or the special `-<N>` syntax. Does not work with native (e.g. git) refs.\n`--noparsing` | `-n` | Do not parse the content, disable YAML and Key-Value functions.\n`--chars` | | Display selected characters from the password.\n\n## Details\n\nThis section describes the expected behaviour of the `show` command with respect to different combinations of flags and\nconfig options.\n\nNote: This section describes the expected behaviour, not necessarily the observed behaviour.\nIf you notice any discrepancies please file a bug and we will try to fix it.\n\nTODO: We need to specify the expectations around new lines.\n\n* When no flag is set the `show` command will display the full content of the secret and will parse it to support key-value lookup and YAML entries.\n  If the `safecontent` option is set to `true` any secret fields (current default is only `password`) are replaced with a random number of '*' characters (length: 5-10).\n  Using the `--unsafe` flag will reveal these fields even if `safecontent` is enabled. `--password` takes precedence of `safecontent=true` as well and displays only the password.\n* The `--noparsing` flag will disable all parsing of the output, this can help debugging YAML secrets for example, where `key: 0123` actually parses into octal for 83.\n* The `--clip` flag will copy the value of the `Password` field to the clipboard and doesn't display any part of the secret.\n* The `--alsoclip` option will copy the value of the `Password` field but also display the secret content depending on the `safecontent` setting, i.e. obstructing the `Password` field if `safecontent` is `true` or just displaying it if not.\n* The `--qr` flags operates complementary to other flags. It will *additionally* format the value of the `Password` entry as a QR code and display it. Other than that it will honor the other options, e.g. `gopass show --qr` will display the QR code *and* the whole secret content below. One special case is the `-o` flag, this flag doesn't make a lot of sense in combination, so if both `--qr` and `-o` are given only the QR code will be displayed.\n* Since gopass plans to supports different RCS backends we do not support arbitrary git refs as arguments to the `--revision` flag. Using those might work, but this is explicitly not supported and bug reports will be closed as `wont-fix`. There are two issues with using arbitrary git refs is that (a) this doesn't work with non-git RCS backends and (b) git versions a whole repository, not single files. So the revision `HEAD^`\n  might not have any changes for a given entry. Thus we only support specifc revisions obtained from `gopass history` or our custom syntax `-N` where N is an integer identifying a specific commit before `HEAD` (cf. `HEAD~N`).\n\n## Parsing and secrets\n\nSecrets are stored on disk as provided, but are parsed upon display to provide extra features such as the ability\nto show the value of a key using:  `gopass show entry key`.\n\nThe secrets are split into 3 categories:\n - the plain type, which is just a plain secret without key-value capabilities\n    ```\n    this is a plain secret\n    using multiple lines\n\n    and that's it\n    ```\n    gets parsed to the same value\n\n\n - the key-value type, which allows to query the value of a specific key. This does not preserve ordering.\n    ```\n    this is a KV secret\n    where: the first line is the password\n    and: the keys are separated from their value by :\n\n    and maybe we have a body text\n    below it\n    ```\n    will be parsed into (with `safecontent` enabled):\n   ```\n    and: the keys are separated from their value by :\n    where: the first line is the password\n\n\n    and maybe we have a body text\n    below it\n    ```\n\n\n - the YAML type which implements YAML support, which means that secrets are parsed as per YAML standard.\n    ```\n    s3cret\n    ---\n    invoice: 0123\n    date   : 2001-01-23\n    bill-to: &id001\n        given  : Bob\n        family : Doe\n    ship-to: *id001\n    ```\n   will be parsed into (with `safecontent` enabled):\n   ```\n    bill-to: map[family:Doe given:Bob]\n    date: 2001-01-23 00:00:00 +0000 UTC\n    invoice: 83\n    ship-to: map[family:Doe given:Bob]\n    ```\n   Note how the `0123` is interpreted as octal for 83. If you want to store a string made of digits such as a numerical\n   username, it should be enclosed in string delimiters: `username: \"0123\"` will always be parsed as the string `0123`\n   and not as octal.\n\nBy default, `safecontent` will remove the first line (the password), every line starting with `otpauth://` in the body, and every YAML values where the key is one of the following: `hotp`, `otpauth`, `password`, `totp`.\n\nBoth the key-value and the YAML format support so-called \"unsafe-keys\", which is a key-value that allows you to specify keys that should be hidden when using `gopass show` with `gopass config safecontent` set to true.\nE.g:\n```\nsupersecret\n---\nage: 27\nsecret: The rabbit outran the tortoise\nname: John Smith\nunsafe-keys: age,secret\n```\nwill display (with safecontent enabled):\n```\nage: *****\nname: John Smith\nsecret: *****\nunsafe-keys: age,secret\n\n```\nunless it is called with `gopass show -n` that would disable parsing of the body, but still hide the password, or `gopass show -f` that would show everything that was hidden, including the password.\n\nYou can read more about secrets formats in its [documentation](docs/secrets.md).\n\nNotice that if the option `parsing` is disabled in the config, then all secrets are handled as plain secrets.\n"
  },
  {
    "path": "docs/commands/sync.md",
    "content": "# `sync` command\n\nThe `sync` command is the preferred way to manually synchronize changes between\nyour local stores and any configured remotes.\n\nYou can also `cd` into a git-based store and manually perform git operations,\nor use the `gopass git` command to automatically run a command in the correct\ndirectory.\n\nNote: `gopass sync` only supports one remote per store.\n\n## Flags\n\n| Flag      | Description                    |\n|-----------|--------------------------------|\n| `--store` | Only sync a specific sub store |\n"
  },
  {
    "path": "docs/commands/templates.md",
    "content": "# `templates` commands\n\nThe template support is one of the more unique `gopass` features. It allows\npassword stores to define templates that will automatically apply to any new\nsecret create at or below the template path. For example this can be useful\nto generate a new email password and its salted hash at the same time. Or a\nPostgreSQL password with the custom salted hash. This is certainly a feature\nthat's not used very often, but if used correctly it can greatly reduce the\ntoil of some common operations.\n\nThis uses Go's [text/template](https://pkg.go.dev/text/template) package.\n\n## Synopsis\n\n```shell\ngopass templates\ngopass templates show template\ngopass templates edit template\ngopass templates remove template\n```\n\n## Flags\n\nNone.\n\n## Examples\n\n### Compute the salted hash for the password\n\n```text\nPassword: {{ .Content }}\nSSHA256: {{ .Content | ssha256 }}\n```\n\n### Compute the SQL statements to create a new PostgreSQL user\n\n```text\n{{ .Content }}\n---\nsql:  |\n  CREATE ROLE {{ .Name }} LOGIN PASSWORD '{{ .Content }}';\n  GRANT {{ .Name }} TO {{ .Name }};\n  ALTER USER {{ .Name }} SET search_path = '{{ .Name }}';\n```\n\n## Template functions\n\nFunction | Example | Description\n-------- | ------- | -----------\n`md5sum` | `{{ .Content \\| md5sum }}` | Calculate the hex md5sum of the input.\n`sha1sum` | `{{ .Content \\| sha1sum }}` | Calculate the hex sha1sum of the input.\n`md5crypt` | `{{ .Content \\| md5crypt }}` | Calculate the md5crypt of the input.\n`ssha` | `{{ .Content \\| ssha }}` | Calculate the salted SHA-1 of the input.\n`ssha256` | `{{ .Content \\| ssha256 }}` | Calculate the salted SHA-256 of the input.\n`ssha512` | `{{ .Content \\| ssha512 }}` | Calculate the salted SHA-512 of the input.\n`get` | `{{ get \"foo/bar\" }}` | Insert the full secret.\n`getpw` | `{{ getpw \"foo/bar\" }}` | Insert the value of the password field from the given secret.\n`getval` | `{{ getval \"foo/bar\" \"baz\" }}` | Insert the value of the named field from the given secret.\n`argon2i` | `{{ .Content \\| argon2i }}` | Calculate the Argon2i hash of the input.\n`argon2id` | `{{ .Content \\| argon2id }}` | Calculate the Argon2id hash of the input.\n`bcrypt` | `{{ .Content \\| bcrypt }}` | Calculate the Bcrypt hash of the input.\n`blake3` | `{{ .Content \\| blake3 }}` | Calculate the BLAKE-3 hash of the input.\n\n## Template variables\n\nNote: These examples assume being evaluated for the secret `foo/bar/baz` and\nthe generated password `VerySecure`.\n\nName | Example | Description\n---- | ------- | -----------\n`Dir` | `foo/bar` | The directory containing the secret.\n`DirName` | `bar` | The directory name containing the secret.\n`Path` | `foo/bar/baz` | The path or full name of the secret.\n`Name` | `baz` | The last element of the path or short name of the secret.\n`Content` | `VerySecure` | The generated password.\n"
  },
  {
    "path": "docs/commands/update.md",
    "content": "# `update` command\n\nThe `update` command will attempt to auto-update `gopass` by downloading the\nlatest release from GitHub. It performs several pre-flight checks in order to\ndetermine if the binary can be updated or not (e.g. if managed by a package\nmanager).\n\n## Synopsis\n\n```\n$ gopass update\n$ gopass update --pre\n```\n\n## Flags\n\nFlag | Description\n---- | -----------\n`--pre` | Update to pre-releases / release candidates (default: `false`).\n"
  },
  {
    "path": "docs/components.dot",
    "content": "digraph G {\n  gopass [shape=box,style=filled,color=\".2 .2 .6\",peripheries=2];\n  gopass -> action;\n  action [label=\"internal/action\"];\n  action -> root;\n  root [label=\"internal/store/root\"];\n  root -> leaf;\n  root -> tree;\n  tree [label=\"internal/tree\"];\n  leaf [label=\"internal/store/leaf\"];\n  leaf -> gitfs;\n  gitfs [label=\"internal/backend/storage/gitfs\"];\n  gitfs -> gitcli;\n  gitcli [label=\"git binary\",shape=Mdiamond];\n  leaf -> gpg;\n  gpg [label=\"internal/backend/crypto/gpg/cli\"];\n  leaf -> age [style=\"dotted\"];\n  age [label=\"internal/backend/crypto/age\"];\n  gpg -> gpgcli;\n  gpgcli [label=\"gpg/gpg2 binary\",shape=Mdiamond];\n  leaf -> secret;\n  secret [label=\"pkg/gopass/secrets\"];\n  secret -> root;\n  jsonapi [label=\"gopass-jsonapi\",shape=box];\n  jsonapi -> api;\n  api [label=\"pkg/gopass/api\"];\n  api -> root;\n  api -> config;\n  gopass -> config;\n  config [label=\"internal/config\"];\n  summon -> api;\n  summon [label=\"gopass-summon-provider\",shape=box];\n  hibp -> api;\n  hibp [label=\"gopass-hibp\",shape=box];\n  hibp -> pkghibp;\n  pkghibp [label=\"pkg/hibp\"];\n  gitcreds -> api;\n  gitcreds [label=\"git-credential-gopass\",shape=box];\n}\n"
  },
  {
    "path": "docs/config.md",
    "content": "# Configuration\n\n## Environment Variables\n\nSome configuration options are only available through setting environment variables.\n\n| **Option**                   | **Type** | **Description**                                                                                                                                                   |\n| ---------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `CHECKPOINT_DISABLE`         | `bool`   | Set to any non-empty value to disable calling the GitHub API when running `gopass version`.                                                                       |\n| `GOPASS_AGE_PASSWORD`        | `string` | Set to any value (including the empty string) to use as a password for the age identity file containing your secret age identities.                               |\n| `GOPASS_AUTOSYNC_INTERVAL`   | `int`    | Set this to the number of days between autosync runs.                                                                                                             |\n| `GOPASS_CHARACTER_SET`       | `bool`   | Set to any non-empty value to restrict the character set used in generated passwords.                                                                             |\n| `GOPASS_CLIPBOARD_CLEAR_CMD` | `string` | Use an external command to remove a password from the clipboard. See [GPaste](usecases/gpaste.md) for an example                                                  |\n| `GOPASS_CLIPBOARD_COPY_CMD`  | `string` | Use an external command to copy a password to the clipboard. See [GPaste](usecases/gpaste.md) for an example                                                      |\n| `GOPASS_CONFIG_NO_MIGRATE`   | `bool`   | Do not attempt to migrate old gopass configs and option names                                                                                                     |\n| `GOPASS_CONFIG_NOSYSTEM`     | `bool`   | Do not read `/etc/gopass/config` (if it exists)                                                                                                                   |\n| `GOPASS_CONFIG`              | `string` | Set this to the absolute path to the configuration file                                                                                                           |\n| `GOPASS_CPU_PROFILE`         | `string` | Path to write a CPU Profile to. Use `go tool pprof` to visualize.                                                                                                 |\n| `GOPASS_DEBUG_FILES`         | `string` | Comma separated filter for console debug output (files)                                                                                                           |\n| `GOPASS_DEBUG_FUNCS`         | `string` | Comma separated filter for console debug output (functions)                                                                                                       |\n| `GOPASS_DEBUG_LOG_SECRETS`   | `bool`   | Set to any non-empty value to enable logging of credentials                                                                                                       |\n| `GOPASS_DEBUG_LOG`           | `string` | Set to a filename to enable debug logging (only set GOPASS_DEBUG to log to stderr)                                                                                |\n| `GOPASS_DEBUG`               | `bool`   | Set to any non-empty value to enable verbose debug output, by default on stderr, unless GOPASS_DEBUG_LOG is set                                                   |\n| `GOPASS_DEBUG_VERBOSE`       | `int`    | Set to any integer value larger than zero to increase the verbosity of debug output                                                                               |\n| `GOPASS_EXTERNAL_PWGEN`      | `string` | Use an external password generator. See [Features](features.md#using-custom-password-generators) for details                                                      |\n| `GOPASS_FORCE_CHECK`         | `string` | (internal) Force the updater to check for updates. Used for testing.                                                                                              |\n| `GOPASS_FORCE_UPDATE`        | `bool`   | Set to any non-empty value to force an update (if available)                                                                                                      |\n| `GOPASS_GPG_BINARY`          | `string` | Set this to the absolute path to the GPG binary if you need to override the value returned by `gpgconf`, e.g. [QubesOS](https://www.qubes-os.org/doc/split-gpg/). |\n| `GOPASS_GPG_OPTS`            | `string` | Add any extra arguments, e.g. `--armor` you want to pass to GPG on every invocation                                                                               |\n| `GOPASS_HOMEDIR`             | `string` | Set this to the absolute path of the directory containing the `.config/` tree                                                                                     |\n| `GOPASS_HOOK`                | `int`    | (internal) Set when invoking hook scripts.                                                                                                                        |\n| `GOPASS_MEM_PROFILE`         | `string` | Path to write a Memory Profile to. Use `go tool pprof` to visualize.                                                                                              |\n| `GOPASS_NO_AUTOSYNC`         | `bool`   | Set this to `true` to disable autosync. Deprecated. Please use `core.autosync`                                                                                    |\n| `GOPASS_NO_NOTIFY`           | `bool`   | Set to any non-empty value to prevent notifications                                                                                                               |\n| `GOPASS_NO_REMINDER`         | `bool`   | Set to any non-empty value to prevent reminders                                                                                                                   |\n| `GOPASS_PW_DEFAULT_LENGTH`   | `int`    | Set to any integer value larger than zero to define a different default length in the `generate` command. By default the length is 24 characters.                 |\n| `GOPASS_SSH_DIR`             | `string` | Set to a filepath that contains ssh keys. Overrides default location.                                                                                             |\n| `GOPASS_UMASK`               | `octal`  | Set to any valid umask to mask bits of files created by gopass                                                                                                    |\n| `GOPASS_UNCLIP_CHECKSUM`     | `string` | (internal) Used between gopass and it's unclip helper.                                                                                                            |\n| `GOPASS_UNCLIP_NAME`         | `string` | (internal) Used between gopass and it's unclip helper.                                                                                                            |\n| `PWGEN_RULES_FILE`           | `string` | (internal) Used for testing the pwgen rules generator.                                                                                                            |\n\nVariables not exclusively used by gopass:\n\n| **Option**             | **Type** | **Description**                                                                                        |\n| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------ |\n| `PASSWORD_STORE_DIR`   | `string` | absolute path containing the password store (a directory). Only supported during initialization!       |\n| `PASSWORD_STORE_UMASK` | `string` | Set to any valid umask to mask bits of files created by gopass (GOPASS_UMASK has precedence over this) |\n| `EDITOR`               | `string` | command name to execute for editing password entries                                                   |\n| `PAGER`                | `string` | the pager program used for `gopass list`. See [Features](features.md#auto-pager) for details           |\n| `GIT_AUTHOR_NAME`      | `string` | name of the author, used by the rcs backend to create a commit                                         |\n| `GIT_AUTHOR_EMAIL`     | `string` | email of the author, used by the rcs backend to create a commit                                        |\n| `NO_COLOR`             | `bool`   | disable color output. See [no-color.org](https://no-color.org) for more information.                   |\n\n## Configuration Options\n\nDuring start up, gopass will look for a configuration file at `$HOME/.config/gopass/config` on unix-like systems or at `%APPDATA%\\gopass\\config` on Windows. If one is not present, it will create one. If the config file already exists, it will attempt to parse it and load the settings. If this fails, the program will abort. Thus, if gopass is giving you trouble with a broken or incompatible configuration file, simply rename it or delete it.\n\nAll configuration options are also available for reading and writing through the sub-command `gopass config`.\n\n- To display all values: `gopass config`\n- To display a single value: `gopass config generate.autoclip`\n- To update a single value: `gopass config generate.autoclip false`\n- As many other sub-commands this command accepts a `--store` flag to operate on a given sub-store, provided the sub-store is a remote one.\n\n### Configuration format\n\n`gopass` uses a configuration format inspired by and mostly compatible with the configuration format used by git. We support\ndifferent configuration sources that take precedence over each other, just like [git](https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-config.html).\n\n#### Configuration precedence\n\n- Hard-coded presets apply if nothing else is set\n- System-wide configuration file allows operators or package maintainers to supply system-wide defaults in `/etc/gopass/config`.\n- User-wide (aka. global) configuration allows to set per-user settings. This is the closest equivalent to the old gopass configs. Located in `$HOME/.config/gopass/config`\n- Per-store (aka. local) configuration allow to set per-store settings, e.g. read-only. Located in `<STORE_DIR>/config`.\n- Per-store unversioned (aka `config.worktree`) configuration allows to override versioned per-store settings, e.g. disabling read-only. Located in `<STORE_DIR>/config.worktree`\n- Environment variables (or command line flags) override all other values. Read from `GOPASS_CONFIG_KEY_n` and `GOPASS_CONFIG_VALUE_n` up to `GOPASS_CONFIG_COUNT`. Command line flags take precedence over environment variables.\n\n### Configuration options\n\nThis is a list of available options:\n\n| **Option**                      | **Type** | Description                                                                                                                                                                                                                        | _Default_                           |\n| ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |\n| `age.agent-enabled`             | `bool`   | Enable the persistent Agent for caching of age identities. This will remove the need to repeatedly enter the passphrase. EXPERIMENTAL. |\n| `age.agent-timeout` | `int` | Automatically lock the agent after this many seconds of inactivity. | `0` |\n| `age.ssh-key-path`              | `string` | Path to a custom SSH key file or directory containing SSH keys. If set, this will be used to load SSH identities for age. If not set, the default SSH directory is used.                                                           | `~/.ssh`                            |\n| `age.usekeychain`               | `bool`   | Use the OS keychain to cache age passphrases.                                                                                                                                                                                      | `false`                             |\n| `audit.concurrency`             | `int`    | Number of concurrent audit workers.                                                                                                                                                                                                | ``                                  |\n| `audit.hibp-dump-file`          | `string` | Specify a HIBPv2 Dump file (sorted) if you want `audit` to check password hashes against this file.                                                                                                                               | `None`                              |\n| `audit.hibp-use-api`            | `bool`   | Set to true if you want `gopass audit` to check your secrets against the public HIBPv2 API. Use with caution. This will leak a few bits of entropy.                                                                                | `false`                             |\n| `autosync.interval`             | `string` | AutoSync interval, for example `2d`, `4h`, `2m` (for days, hours, minutes). A plain number without suffix is taken as days.                                                                                                        | `3`                                 |\n| `core.autoimport`               | `bool`   | Import missing keys stored in the pass repository without asking.                                                                                                                                                                  | `false`                             |\n| `core.autopush`                 | `bool`   | Always do a `git push` after a commit to the store. Makes sure your local changes are always available on your git remote.                                                                                                         | `true`                              |\n| `core.autosync`                 | `bool`   | Automatically sync (fetch & push) the git remote on an interval.                                                                                                                                                                   | `true`                              |\n| `core.cliptimeout`              | `int`    | How many seconds the secret is stored when using `-c`. Setting this to `0` disables auto-clear.                                                                                                                                    | `45`                                |\n| `core.exportkeys`               | `bool`   | Export public keys of all recipients to the store.                                                                                                                                                                                 | `true`                              |\n| `core.nocolor`                  | `bool`   | Do not use color.                                                                                                                                                                                                                  | `false`                             |\n| `core.follow-references`        | `bool`   | Follow references in passwords. You can reference another file using `gopass://path` in store to use that file password.                                                                                                           | `false`                             |\n| `core.nopager`                  | `bool`   | Do not invoke a pager to display long lists.                                                                                                                                                                                       | `false`                             |\n| `core.noreminder`               | `bool`   | Set to true to disable periodic update reminder. Equivalent to setting `GOPASS_NO_REMINDER`.                                                                                                                                       | `false`                             |\n| `core.notifications`            | `bool`   | Enable desktop notifications.                                                                                                                                                                                                      | `true`                              |\n| `core.post-hook`                | `string` | This hook is executed after any command invocation.                                                                                                                                                                                | `None`                              |\n| `core.pre-hook`                 | `string` | This hook is executed before any command invocation.                                                                                                                                                                               | `None`                              |\n| `core.readonly`                 | `bool`   | Disable writing to a store. Note: This is just a convenience option to prevent accidental writes. Enforcement can only happen on a central server (if repos are set up around a central one).                                      | `false`                             |\n| `create.default-username`       | `string` | The settings allows users to specify the default username for logins created with `gopass create`.                                                                                                                                 | `None`                              |\n| `create.post-hook`              | `string` | This hook is executed right after the secret creation. If the hook exits with a non-zero exit value the generated secret is discarded.                                                                                             | `None`                              |\n| `create.pre-hook`               | `string` | This hook is executed right before the secret creation during `gopass create`.                                                                                                                                                     | `None`                              |\n| `cryptfs.substorage`            | `string` | Storage backend to use for CryptFS.          | `gitfs` |\n| `delete.post-hook`              | `string` | This hook is run right after removing a record with `gopass rm`.                                                                                                                                                                   | `None`                              |\n| `domain-alias.<from>.insteadOf` | `string` | Alias from domain to the string value of this entry. Currently not supported at the local config level.                                                                                                                            | ``                                  |\n| `edit.auto-create`              | `bool`   | Automatically create new secrets when editing.                                                                                                                                                                                     | `false`                             |\n| `edit.editor`                   | `string` | This setting controls which editor is used when opening a file with `gopass edit`. Currently not supported at the local config level. It takes precedence over the `$EDITOR` environment variable. This setting can contain flags. | `None`                              |\n| `edit.post-hook`                | `string` | This hook is run right after editing a record with `gopass edit`.                                                                                                                                                                  | `None`                              |\n| `edit.pre-hook`                 | `string` | This hook is run right before editing a record with `gopass edit`.                                                                                                                                                                 | `None`                              |\n| `generate.autoclip`             | `bool`   | Always copy the password created with `gopass generate`.                                                                                                                                                                           | `false`                             |\n| `generate.generator`            | `string` | Default password generator. `xkcd`, `memorable`, `external` or ``.                                                                                                                                                                 | ``                                  |\n| `generate.length`               | `int`    | Default length for generated password.                                                                                                                                                                                             | `24`                                |\n| `generate.strict`               | `bool`   | Use strict mode for generated password.                                                                                                                                                                                            | `false`                             |\n| `generate.symbols`              | `bool`   | Include symbols in generated password.                                                                                                                                                                                             | `false`                             |\n| `mounts.path`                   | `string` | Path to the root store.                                                                                                                                                                                                            | `$XDG_DATA_HOME/gopass/stores/root` |\n| `notify.disable-icon`           | `bool`   | Do not show notification icon (not available on every platform).                                                                                                                                                                   | `None`                              |\n| `otp.autoclip`                  | `bool`   | Automatically clip in `gopass otp` by default, while still displaying the codes and timers.                                                                                                                                        | `false`                             |\n| `otp.onlyclip`                  | `bool`   | Automatically clip in `gopass otp` by default, without displaying the OTP codes. This takes precedence over `otp.autoclip`. Requires using `gopass otp --clip=false` to force display the codes.                                   | `false`                             |\n| `recipients.check`              | `bool`   | Check recipients hash. The global config option takes precedence over local ones here for security reasons.                                                                                                                        | `false`                             |\n| `recipients.hash`               | `string` | SHA256 hash of the recipients file. Used to notify the user when the recipients files change. Not set, nor read at the local level for security reasons.                                                                           | ``                                  |\n| `recipients.remove-extra-keys`  | `bool`   | Remove extra recipients during key import. Not supported at the local level for security reasons.                                                                                                                                  | `false`                             |\n| `show.autoclip`                 | `bool`   | Autoclip in `gopass show` by default.                                                                                                                                                                                              | `false`                             |\n| `show.post-hook`                | `string` | This hook is run right after displaying a secret with `gopass show`.                                                                                                                                                               | `None`                              |\n| `show.safecontent`              | `bool`   | Only output _safe content_ (i.e. everything but the first line of a secret) to the terminal. Use _copy_ (`-c`) to retrieve the password in the clipboard, or _force_ (`-f`) to still print it.                                     | `false`                             |\n| `updater.check`                 | `bool`   | Check for updates when running `gopass version`. Only supported as a global, system or env config option, not at the local level.                                                                                                  | `true`                              |\n| `output.internal-pager`         | `bool`   | Use the internal pager `ov`.                                                                                                                                                                                                       | `false`                             |\n| `pwgen.xkcd-sep`                | `string` | `xkcd` password generator separator.                                                                                                                                                                                               | ` `                                 |\n| `pwgen.xkcd-lang`               | `string` | `xkcd` password generator language.                                                                                                                                                                                                | `en`                                |\n| `pwgen.xkcd-capitalize`         | `bool`   | Capitalize the first character of each word. Default is `false`, except when the separator is empty.                                                                                                                               | `false`                             |\n| `pwgen.xkcd-numbers`            | `bool`   | Add random numbers after each word.                                                                                                                                                                                                | `false`                             |\n| `pwgen.xkcd-len`                | `bool`   | The number of words to be generated.                                                                                                                                                                                               | `4`                                 |\n\nFurthermore, the following table list the legacy options (starting with v1.15.9) and their new names, their migration should be automatic\nunless you've set them at the system level or using Env variables, in which case you'll need to migrate them manually:\n\n| **Legacy option name** | **New option name** | **Version of migration** |\n| ---------------------- | ------------------- | ------------------------ |\n| `core.showsafecontent` | `show.safecontent`  | v1.15.9                  |\n| `core.autoclip`        | `generate.autoclip` | v1.15.9                  |\n| `core.showautoclip`    | `show.autoclip`     | v1.15.9                  |\n"
  },
  {
    "path": "docs/entropy.md",
    "content": "# Entropy\n\nGenerating cryptographic keys needs a lot of entropy. Especially `gnupg --gen-key`\ndepletes the kernel entropy pool (`/dev/random`) quite fast and may appear to be\nstuck when it's waiting for new entropy.\n\nIf you wonder how to speed this up consider installing `rng-tools`\nif this is available on your platform.\n\nAfter installing `rng-tools` please make sure `rngd` is actually running and\nreplenishing your entropy pool.\n\nYou can do so by keeping a watch on your available entropy and running an entropy\nconsuming process as follows:\n\n```bash\nwatch -n1 cat /proc/sys/kernel/random/entropy_avail\n# switch to another terminal / screen\ncat /dev/random | rngtest -c 1000\n```\n\nThe second command should complete within a few seconds and report no errors.\nIf it takes much longer you probably don't have an hardware RNG and will have\nto generate some entropy by triggering some network activity and input.\n\nYou should avoid `havaged`.\n\n### Debian / Ubuntu\n\n```bash\nsudo apt-get install rng-tools\n```\n\n### CentOS / Fedora / Red Hat\n\n```bash\nsudo yum install rng-tools\n```\n\n## Further Information\n\n* [RNG-Tools on the Arch Linux Wiki](https://wiki.archlinux.org/index.php/Rng-tools)\n* [gopass Issue #486](https://github.com/gopasspw/gopass/issues/486)\n"
  },
  {
    "path": "docs/faq.md",
    "content": "# FAQ\n\n## How does gopass relate to HashiCorp vault?\n\nWhile [Vault](https://www.vaultproject.io/) is for machines, gopass is for humans [#7](https://github.com/gopasspw/gopass/issues/7)\n\n## Is gopass compatible with pass?\n\nYes, gopass is a drop-in replacement for pass. It has a similar command structure and is compatible with the pass storage format. However, gopass offers additional features like multiple stores, mounts, and structured secrets, while pass has a more flexible plugin system.\n\n## `gopass show secret` displays `Error: Failed to decrypt`\n\nThis issue may happen if your GPG setup is broken. On MacOS try `brew link --overwrite gnupg`. You also may need to set `export GPG_TTY=$(tty)` in your `.bashrc` [#208](https://github.com/gopasspw/gopass/issues/208), [#209](https://github.com/gopasspw/gopass/issues/209)\n\n## `gopass recipients add` fails with `Warning: No matching valid key found`\n\nIf the key you're trying to add is already in your keyring you may need to trust it. If this is your key run `gpg --edit-key [KEYID]; trust (set to ultimate); quit`, if this is not your key run `gpg --edit-key [KEYID]; lsign; save; quit`\n\n## How can gopass handle binary data?\n\ngopass is designed not to change the content of the secrets in any way except that it will add a final newline at the end of the secret if it does not have one already and the output is going to a terminal. This means that the output may mess up your terminal if it's not only text. In this case you should either encode the secret to text (e.g. base64) before inserting or use the special `gopass binary` sub-command that does that for you.\n\n## Why does gopass delete my whole KDE klipper history?\n\nKDEs klipper provides a clipboard history for your convenience. Since we currently can't figure out which entry may contain a secret copied to the clipboard, we just clear the whole history once the clipboard timer expires.\n\n## Can I use gopass as an token helper for Vault?\n\nYes, there is [a repo](https://github.com/frntn/vault-token-helper-gopass) that provides the necessary scripts and instructions.\n\n## Does gopass support re-encryption? \n\nAdding or removing recipients with `gopass recipients add` or `gopass recipients remove` will automatically re-encrypt all affected secrets. Further, `gopass fsck` checks for missing recipients and reencrypts the secret if necessary.\n\n## gopass can automatically import missing recipient keys, but can it export them as well?\n\nWhen adding a recipient with `gopass recipients add`, their public key will automatically be exported to the store `.gpg-keys/<ID>`.\n\n## Can gopass be used with Terraform?\n\nYes, there is a gopass-based [Terraform provider](https://github.com/camptocamp/terraform-provider-pass) available.\n\n## How can I fix `\"gpg: decryption failed: No secret key\"` errors?\n\nSet the `auto-expand-secmem` option in your gpg-agent.conf, if your version of GnuPG supports it.\n\n## I'm getting `Path too long for Unix domain socket` errors, usually on MacOS.\n\nThis can be fixed by setting `export TMPDIR=/tmp` (or any other suiteable location with a path shorter than 80 characters).\n\n## Empty secret?\n\nOld version of `gpg` may fail to decode message encrypted with newer version without any message. The encrypted secret in such case is just empty and gopass will warn you about this. One case of such behaviour we have seen so far is when the encryption key generated with `gpg` version 2.3.x encrypt a password that is then decrypted on `gpg` version 2.2.x (default on Ubuntu 18.04). In this particular case old `gpg` does not understand `AEAD` encryption extension, and it fails without any error.  If it is your case then follw the instructions in listed in #2283.\n\n## Expired recipients\n\n`gopass` will refuse to add new recipients when any invalid (e.g. expired) recipients are present in a password store.\nIn such cases manual intervention is required. Expired keys can either be removed or extended. Unknown keys that\ncan not be automatically imported need to be obtained and manually imported first. These are restrictions from the underlying\ncrypto implementation (GPG) and we can not easily work around these.\n\n## API Stability\n\nThis repository primarily delivers gopass as a command-line interface (CLI) tool. While the underlying Go packages might be importable, we explicitly state that semantic versioning applies solely to changes in the CLI. We offer no API stability guarantees for the Go packages within this repository, and breaking changes may occur without a major version bump of `gopass` itself.\n\nIf you choose to utilize `gopass` packages as libraries, it is strongly recommended to vendor them to mitigate potential integration issues arising from non-backward-compatible updates.\n\nShould specific Go packages within this project prove valuable for independent use, we encourage you to request their extraction into separate repositories. In such dedicated repositories, we will adhere to strict semantic versioning principles, ensuring predictable API stability for those packages.\n\n## Further Reading\n\n* [GPGTools](https://gpgtools.org/) for MacOS\n* [GitHub Help on GPG](https://help.github.com/articles/signing-commits-with-gpg/)\n* [Git - the simple guide](http://rogerdudler.github.io/git-guide/)\n"
  },
  {
    "path": "docs/features.md",
    "content": "# Features\n\nThis document provides a broad overview over the features and use-cases\ngopass supports.\n\nSome examples are available in our [example password store](https://github.com/gopasspw/password-store-example).\n\n| **Feature**                 | **State**     | **Description**                                                   |\n| --------------------------- | ------------- | ----------------------------------------------------------------- |\n| Secure secret storage       | *stable*      | Securely storing encrypted secrets                                |\n| Multiple stores             | *stable*      | Mount multiple stores in your root store, like file systems       |\n| Recipient management        | *stable*      | Easily manage multiple users of each store                        |\n| password quality assistance | *beta*        | Checks existing or new passwords for common flaws **offline**     |\n| password leak checker       | *integration* | Perform **offline** checks against known leaked passwords using [gopass-hibp](https://github.com/gopasspw/gopass-hibp)  |\n| PAGER support               | *stable*      | Automatically invoke a pager on long output                       |\n| JSON API                    | *integration* | Allow gopass to be used as a native extension for browser plugins |\n| Automatic fuzzy search      | *stable*      | Automatically search for matching store entries if a literal entry was not found |\n| gopass sync                 | *stable*      | Easy to use syncing of remote repos and GPG keys                  |\n| Desktop Notifications       | *stable*      | Display desktop notifications and completing long running operations |\n| REPL                        | *beta*        | Integrated Read-Eval-Print-Loop shell with autocompletion by running `gopass`. |\n| OTP support                 | *stable*      | Generate TOTP/(HOTP) tokens based on the stored secret            |\n| Extensions                  |               | [Extend](docs/hacking.md#extending-gopass) gopass with custom commands using our [API](https://pkg.go.dev/github.com/gopasspw/gopass/pkg/gopass/api)                  |\n| Fully open source!          |               | No need to trust it, check our code and/or improve it!            |\n\n## Integrations\n\n- [gopassbridge](https://github.com/gopasspw/gopassbridge): Browser plugin for Firefox, Chrome and other Chromium based browsers\n- [gopass-ui](https://github.com/codecentric/gopass-ui): Graphical user interface for gopass\n- [kubectl gopass](https://github.com/gopasspw/kubectl-gopass): Kubernetes / kubectl plugin to support reading and writing secrets directly from/to gopass.\n- [gopass alfred](https://github.com/gopasspw/gopass-alfred): Alfred workflow to use gopass from the Alfred Mac launcher\n- [git-credential-gopass](https://github.com/gopasspw/git-credential-gopass): Integrate gopass as an git-credential helper\n- [gopass-hibp](https://github.com/gopasspw/gopass-hibp): haveibeenpwned.com leak checker\n- [gopass-jsonapi](https://github.com/gopasspw/gopass-jsonapi): native messaging for browser plugins, e.g. gopassbridge\n- [gopass-summon-prover](https://github.com/gopasspw/gopass-summon-provider): gopass as a summon provider\n- [`terraform-provider-gopass`](https://github.com/camptocamp/terraform-provider-pass): a Terraform provider to interact with gopass\n- [chezmoi](https://github.com/twpayne/chezmoi): dotfile manager with gopass support\n- [tessen](https://github.com/ayushnix/tessen): autotype and copy gopass data on wayland compositors on Linux\n- [raycast-gopass](https://github.com/raycast/extensions/tree/main/extensions/gopass): a gopass extension for Raycast Mac launcher\n- [gnome-pass-search-provider](https://github.com/jle64/gnome-pass-search-provider): pass search provider for GNOME Shell, which also supports gopass\n- [gopass-secret-service](https://github.com/nikicat/gopass-secret-service): D-Bus daemon implementing the [Secret Service API](https://specifications.freedesktop.org/secret-service/) with gopass as the backend\n\n## Mobile apps\n\n- [Pass - Password Store](https://apps.apple.com/us/app/pass-password-store/id1205820573) - iOS, [source code](https://github.com/mssun/passforios), [supports only 1 repository now](https://github.com/mssun/passforios/issues/88)\n- [Password Store](https://play.google.com/store/apps/details?id=dev.msfjarvis.aps) - Android, [source code](https://github.com/android-password-store/android-password-store)\n\n## Standard Features\n\nNote: Running `gopass` without any arguments opens up an interactive mode where\nall commands explained below are available without the need to prefix them with\n`gopass`. Also this mode offers tab completion without the need to configure\nthe shell.\n\n### Data Organization\n\nBefore you start using gopass, you should know a little bit about how it stores your data.\nIt's actually really simple! Each password (or secret) will live in its own file.\nAnd you can stick related passwords (or secrets) together in a directory.\nSo, for example, if you had 3 laptops and wanted to store the root passwords for all 3, then your file system might look something like the following:\n\n```text\n.password-store\n└── laptops\n    ├── dell.gpg\n    ├── hp.gpg\n    └── macbook.gpg\n```\n\nWith this file system, if you typed the `gopass ls` command, it would report the following:\n\n```text\ngopass\n└── laptops\n    ├── dell\n    ├── hp\n    └── macbook\n```\n\nIn this example, the key for the MacBook is `laptops/macbook`.\n\ngopass does not impose any specific layout for your data. Any key can contain any kind of data. Please note that sensitive data **should not** be put into the name of a secret.\n\nIf you plan to use the password store for website credentials or plan to use [browserpass](https://github.com/dannyvankooten/browserpass), you should follow the following pattern for storing passwords:\n\n```text\nexample1.com/username\nexample2.com/john@doe.com\n```\n\n### Initializing a Password Store\n\nAfter installing gopass, the first thing you should do is initialize a password store.\n(If you are migrating to gopass from pass and already have a password store, you can skip this step.)\n\nNote that this document uses the term *password store* to refer to a directory that is managed by gopass.\nThis is entirely different from any OS-level credential store, your GPG key ring, or your SSH keys.\n\nTo initialize a password store, just do:\n\n```shell\ngopass init\n```\n\nThis will prompt you for which GPG key you want to associate the store with.\nThen it will create a `.local/share/gopass/stores/root` directory in your home directory.\n\nIf you don't want gopass to use this default directory, you can instead initialize a password store with:\n\n```shell\ngopass init --path /custom/path/to/password/store\n```\n\nIf you don't want gopass to prompt you for the GPG key to use, you can specify it inline. For example, this might be useful if you have a huge number of GPG keys on the system or if you are initializing a password store from a script. You can do this in three different ways:\n\n```shell\ngopass init gopher@golang.org # By specifying the email address associated with the GPG key\ngopass init A3683834 # By specifying the 8 character ID found by typing \"gpg --list-keys\" and looking at the \"pub\" line\ngopass init 1E52C1335AC1F4F4FE02F62AB5B44266A3683834 # By specifying the GPG key fingerprint found by typing \"gpg --fingerprint\" and removing all of the spaces\n```\n\n### Cloning an Existing Password Store\n\nIf you already have an existing password store that exists in a Git repository, then use `gopass` to clone it:\n\n```shell\ngopass clone git@example.com/pass.git\n```\n\nThis runs `git clone` in the background. If you don't want gopass to use the default root mount of \"$HOME/.local/share/gopass/stores/root\", then you can specify an additional mount parameter:\n\n```shell\ngopass clone git@example.com/pass-work.git work # This will initialize the password store in the \"$HOME/.local/share/gopass/stores/work\" directory\n```\n\nPlease note that all cloned repositories must already have been initialized with gopass. (See the previous section for more details.)\n\nNote too that unless you are already a recipient of the cloned repository, you must add the destination's public GPG key as a recipient to the existing store.\n\nFinally notice that if you really want your password-store directory at a specific location, you should `git clone` it manually at that location, and either set Gopass' `PASSWORD_STORE_DIR` env var to that location, or set Gopass' config `path` option to that location. \n\nAn existing `$HOME/.password-store` directory should also be automatically picked-up by Gopass upon first run.\n\n### Adding Secrets\n\nLet's say you want to create an account.\n\n| Website    | User   |\n| ---------- | ------ |\n| golang.org | gopher |\n\n#### Type in a new secret\n\n```shell\n$ gopass insert golang.org/gopher\nEnter secret for golang.org/gopher:       # hidden on Linux / MacOS\nRetype secret for golang.org/gopher:      # hidden on Linux / MacOS\ngopass: Encrypting golang.org/gopher for these recipients:\n - 0xB5B44266A3683834 - Gopher <gopher@golang.org>\n\nDo you want to continue? [yn]: y\n```\n\n#### Generate a new secret\n\n```shell\n$ gopass generate golang.org/gopher\nHow long should the secret be? [20]:\ngopass: Encrypting golang.org/gopher for these recipients:\n - 0xB5B44266A3683834 - Gopher <gopher@golang.org>\n\nDo you want to continue? [yn]: y\nThe generated secret for golang.org/gopher is:\nEech4ahRoy2oowi0ohl\n```\n\n```shell\n$ gopass generate golang.org/gopher 16    # length as parameter\ngopass: Encrypting golang.org/gopher for these recipients:\n - 0xB5B44266A3683834 - Gopher <gopher@golang.org>\n\nDo you want to continue? [yn]: y\nThe generated password for golang.org/gopher is:\nEech4ahRoy2oowi0ohl\n```\n\nThe `generate` command will ask for any missing arguments, like the name of the secret or the length. By default the password is copied to clipboard. If you don't want the password to be copied, but displayed instead, use the `-p` flag to print it.\n\nBy default the password is copied to clipboard, but you can disable this using the `AutoClip` option, which, when set to`false`, will neither display, nor print the password. This is overridden by the `-p` or `-c` flags.\n\n### Edit a secret\n\n```shell\ngopass edit golang.org/gopher\n```\n\nThe `edit` command uses the `$EDITOR` environment variable to start your preferred editor where you can easily edit multi-line content. `vim` will be the default if `$EDITOR` is not set.\n\n### Adding OTP Secrets\n\n*Note: Depending on your security needs, it may not behoove you to store your OTP secrets alongside your passwords! Look into [Multiple Stores](https://github.com/gopasspw/gopass/blob/master/docs/features.md#multiple-stores) if you need things to be separate! Ideally using a hardware token requiring user interaction to store the key that is able to decrypt your OTP codes.*\n\nTypically sites will display a QR code containing a URL that starts with `oauth://`. This string contains information about generating your OTPs and can be directly added to your password file. For example:\n\n```shell\n$ gopass show golang.org/gopher\nsecret1234\notpauth://totp/golang.org:gopher?secret=ABC123\n```\n\nAlternatively, you can use YAML (notice the usage of the YAML separator `---` to indicate it is a YAML secret):\n\n```shell\n$ gopass show golang.org/gopher\nsecret1234\n---\ntotp: ABC123\n```\n\n*Note: any values for `totp:` need to be base32 (32, not 64 and uppercase letters only) encoded. Often sites will display the raw secret alongside the QR*\n\nSome sites will not directly show you the URL contained in the QR code. If this is the case, you can use something like [zbar](http://zbar.sourceforge.net/) to extract the URL.\n\nBoth TOTP and HOTP are supported. However, to generate HOTP tokens, the counter in the stored URL must be manually incremented (e.g. via `gopass edit myhotpsecret`).  \n\n### Listing existing secrets\n\nYou can list all entries of the store:\n\n```shell\n$ gopass ls\ngopass\n├── golang.org\n│   └── gopher\n└── emails\n    ├── user@example.com\n    └── user@justwatch.com\n```\n\nIf your terminal supports colors the output will use ANSI color codes to highlight directories and mounted sub stores. Mounted sub stores include the mount point and source directory. See below for more details on mounts and sub stores.\n\n### Show a secret\n\n```shell\n$ gopass show golang.org/gopher\n\nEech4ahRoy2oowi0ohl\n```\n\nThe default action of `gopass` is show, so the previous command is exactly the same as typing `gopass golang.org/gopher`. It also accepts the `-c` flag to copy the content of the secret directly to the clipboard.\n\nIn order to display only the password, the flag `-o` can be used. One can also copy the password to the clipboard while still showing the entry using the flag `-C`.\n\nWARNING: The short form `gopass <secret>` is deprecated. Use `gopass show <secret>`.\n\nSince it may be dangerous to always display the password, the `safecontent` setting may be set to `true` to allow one to display only the rest of the password entries by default but hiding the password. In order to display the whole entry, with the password in clear, the `-u`/`--unsafe` flag must then be used.\nThe password can still be shown using the `-o` flag.\n\nWARNING: The `safecontent` setting is not perfect and *might* be removed in the future.\n\n#### Copy a secret to the clipboard\n\n```shell\n$ gopass show -c golang.org/gopher\n\nCopied golang.org/gopher to clipboard. Will clear in 45 seconds.\n```\n\n### Removing a secret\n\n```shell\ngopass rm golang.org/gopher\n```\n\n`rm` will remove a secret from the store. Use `-r` to delete a whole folder. Please note that you **can not** remove a folder containing a mounted sub store. You have to unmount any mounted sub stores first.\n\n### Moving a secret\n\n```shell\ngopass mv emails/example.com emails/user@example.com\n```\n\n*Moving also works across different sub-stores.*\n\n### Copying a secret\n\n```shell\ngopass cp emails/example.com emails/user@example.com\n```\n\n*Copying also works across different sub-stores.*\n\n## Advanced Features\n\n### Auto-Pager\n\nLike other popular open-source projects, gopass automatically pipe the output to `$PAGER` if it's longer than one terminal page. You can disable this behavior by unsetting `$PAGER` or `gopass config nopager true`.\n\n### Sync\n\nGopass offers as simple and intuitive way to sync one or many stores with their\nremotes. This will perform and git pull, push and import or export any missing\nGPG keys.\n\n```shell\ngopass sync\n```\n\n### Desktop Notifications\n\nCertain long running operations, like `gopass sync` or `copy to clipboard` will\ntry to show desktop notifications [Linux only].\n\n### git auto-push and sync\n\ngopass always pushes changes to your default git remote server (origin).\n\nIf you want to pull changes from git, you need to run the sync command:\n\n```shell\ngopass sync \n```\n\nYou can selectively pull changes into named stores:\n\n```shell\ngopass sync --store foo \n```\n\nFor details see: [`sync` command](commands/sync.md)\n\n### Check Passwords for Common Flaws\n\ngopass can check your passwords for common flaws, like being too short or coming from a dictionary.\n\n```shell\n$ gopass audit\nDetected weak secret for 'golang.org/gopher': Password is too short\n```\n\n### Check Passwords against leaked passwords\n\n[gopass-hibp](https://github.com/gopasspw/gopass-hibp) can assist you in checking your passwords against those included in recent data breaches.\nSee its [dedicated repo](https://github.com/gopasspw/gopass-hibp) to install `gopass-hibp`.\n\nYou can either check against the HIBPv2 API (recommended) or download the dumps (v1 or v2) and\nperform the check fully offline.\n\n#### Using the API\n\nThis will check the SHA1 hashes of all your password against the online HIBP API. Your actual passwords aren't leaked, but weak passwords can be found using a dictionary attack if an adversary obtains its SHA1 hashes. Use this if:\n\n- you trust HIBP website and API\n- you trust your network\n- you don't have small (<14 characters), easy to crack passwords\n\n```shell\ngopass-hibp api\n```\n\n#### Using the Dumps\n\nFirst go to [haveibeenpwned.com/Passwords](https://haveibeenpwned.com/Passwords) and download the dumps. Then unpack the 7-zip archives somewhere. Note that full path to those files and provide it to `gopass-hibp dump --files` flag.\n\n```shell\ngopass-hibp dump --files /tmp/pwned-passwords-ordered-2.0.txt\n```\n\n### Support for Binary Content\n\ngopass provides secure and easy support for working with binary files through the `cat`, `fscopy`, `fsmove` and `sum` family of sub-commands. One can copy or move secret from or to the store. gopass will attempt to securely overwrite and remove any secret moved to the store.\n\n```shell\n# copy file \"/some/file.jpg\" to \"some/secret\" in the store\n$ gopass fscopy /some/file.jpg some/secret\n# move file \"/home/user/private.key\" to \"my/private.key\", removing the file on disk\n# after the file has been encoded, stored and verified to be intact (SHA256)\n$ gopass fsmove /home/user/private.key my/private.key\n# Calculate the checksum of some asset\n$ gopass sha256 my/private.key\n```\n\n### Multiple Stores\n\ngopass supports multi-stores that can be mounted over each other like file systems on Linux/UNIX systems. Mounting new stores can be done through gopass:\n\n```shell\n# Mount a new store\n$ gopass mounts add test /tmp/password-store-test\n# Show mounted stores\n$ gopass mounts\n# Unmount a store\n$ gopass mounts remove test\n```\n\nYou can initialize a new store using `gopass init --store mount-point --path /path/to/store`.\n\nWhere possible sub stores are supported transparently through the path to the secret. When specifying the name of a secret it's matched against any mounted sub stores and the given action is executed on this store.\n\nCommands that don't accept an secret name, e.g. `gopass recipients add` or `gopass init` usually accept a `--store` parameter. Please check the help output of each command for more information, e.g. `gopass help init` or `gopass recipients help add`.\n\n\nCommands that support the `--store` flag:\n\n| **Command**                | **Example**                                   | **Description** |\n| -------------------------- | --------------------------------------------- | --------------- |\n| `gopass git push`          | `gopass git push --store=foo origin master`   | Push all changes in the sub store *foo* to master |\n| `gopass git pull`          | `gopass git pull --store=foo origin master`   | Pull all changes in the sub store *foo* from master |\n| `gopass git init`          | `gopass git init --store=foo`                 | Initialize git in the sub store *foo* |\n| `gopass init`              | `gopass init --store=foo`                     | Initialize and mount the new sub store *foo* |\n| `gopass recipients add`    | `gopass recipients add --store=foo GPGxID`    | Add the new recipient *GPGxID* to the store *foo* |\n| `gopass recipients remove` | `gopass recipients remove --store=foo GPGxID` | Remove the existing recipients *GPGxID* from the store *foo* |\n\n### Directly edit structured secrets aka. YAML support\n\ngopass supports directly editing structured secrets (simple key-value maps):\n\n```shell\n$ gopass generate -n foo/bar 12\nThe generated password for foo/bar is:\n7fXGKeaZgzty\n$ gopass insert foo/bar baz\nEnter password for foo/bar/baz:\nRetype password for foo/bar/baz:\n$ gopass foo/bar baz\nzab\n$ gopass foo/bar\n7fXGKeaZgzty\nbaz: zab\n```\n\nOr even YAML:\n\n```yaml\nsecret1234\n---\nmulti: |\n    text\n    more text\noctal: 0123\ndate   : 2001-01-23\nbill-to: &id001\n    given  : Bob\n    family : Doe\nship-to: *id001\n```\n\nNote that YAML entries currently support only one YAML block and **must start with the separator** `---` after the password and body text, if any. We do not support comments directly after the separator.\n\nPlease note that gopass will try to leave your secret as is whenever possible,\nbut as soon as you mutate the YAML content through gopass, i.e. `gopass insert secret key`,\nit will employ a YAML marshaler that may alter the order and escaping of your\nentries.\n\nSee also [gopass show doc entry](/docs/commands/show.md#parsing-and-secrets) for more information about parsing and how to disable it.\n\n### Edit the Config\n\ngopass allows editing the config from the command-line. This is similar to how git handles config changes through the command-line. Any change will be written to the configured gopass config file.\n\n```shell\n$ gopass config\naskformore: false\nautoclip: true\nautoimport: false\ncliptimeout: 10\nnoconfirm: false\npath: /home/user/.password-store\n\n$ gopass config cliptimeout 60\n$ gopass config cliptimeout\n```\n\n### Managing Recipients\n\nYou can list, add and remove recipients from the command-line.\n\n```shell\n$ gopass recipients\ngopass\n└── 0xB5B44266A3683834 - Gopher <gopher@golang.org>\n\n$ gopass recipients add 1ABB2C1A\n\n$ gopass recipients\ngopass\n├── 0xB1C7DF661ABB2C1A - Someone <someone@example.com>\n└── 0xB5B44266A3683834 - Gopher <gopher@golang.org>\n\n$ gopass recipients remove 0xB5B44266A3683834\n\n$ gopass recipients\ngopass\n└── 0xB1C7DF661ABB2C1A - Someone <someone@example.com>\n```\n\nRunning `gopass recipients` will also try to load and save any missing GPG keys from and to the store.\n\nThe commands manipulating recipients, i.e. `gopass recipients add` and `gopass recipients remove` accept a `--store` flag that expects the *name of a mount point* to operate on this mounted sub store.\n\nTo check and reencrypt secrets if recipients are missing, run `gopass fsck`.\n\n### Debugging\n\nTo debug gopass, set the environment variable `GOPASS_DEBUG_LOG` to a output filename.\n\n### Restricting the characters in generated passwords\n\nTo restrict the characters used in generated passwords set `GOPASS_CHARACTER_SET` to any non-empty string. Please keep in mind that this can considerably weaken the strength of generated passwords.\n\n### Using custom password generators\n\nTo use an external password generator set `GOPASS_EXTERNAL_PWGEN` to any valid executable with all required arguments. Please note that the command will be run as-is. Not parameters to control length or complexity can be passed. Any errors will be silently ignored and gopass will fall back to the internal password generator instead.\n\n### In-place updates to existing passwords\n\nRunning `gopass [generate|insert] foo/bar` on an existing entry `foo/bar` will only update the first line of the secret, leaving any trailing data in place.\n\n*Note: if the trailing data is marked as YAML (has a line with `---` after the password line), invalid YAML will be removed!*\n\n### Disabling Colors\n\nDisabling colors is as simple as setting `NO_COLOR` to `true`. See [no-color.org](https://no-color.org) for more information.\n\n### Password Templates\n\nWith gopass you can create templates which are searched when executing `gopass edit` on a new secret. If the folder, or any parent folder, contains a file called `.pass-template` it's parsed as a [Go template](https://pkg.go.dev/text/template), executed with the name of the new secret and an auto-generated password and loaded into your `$EDITOR`.\n\nThis makes it easy to use templates for certain kind of secrets such as database passwords.\n\n#### Examples\n\n```text\n# Insert the password of an arbitrary secret\nPassword-value of existing entry: {{ getpw \"foo\" }}\n\n# Insert the full body of another secret\nContent of the new entry: {{ .Content }}\n\n# MD5 hash (hex)\nMd5sum of the new password: {{ .Content | md5sum }}\n\n# SHA1 hash (hex)\nSha1sum of the new password: {{ .Content | sha1sum }}\n\n# MD5Crypt (hex)\nMd5crypt of the new password: {{ .Content | md5crypt }}\n\n# Salted-SHA1\nSSHA of the new password: {{ .Content | ssha }}\n\n# Salted-SHA256\nSSHA256 of the new password: {{ .Content | ssha256 }}\n\n# Salted-SHA512\nSSHA512 of the new password: {{ .Content | ssha512 }}\n\n# Argon2i\nArgon2i of the new password: {{ .Content | argon2i }}\n\n# Argon2id\nArgon2id of the new password: {{ .Content | argon2id }}\n\n# Bcrypt\nBcrypt of the new password: {{ .Content | bcrypt }}\n```\n\n### Domain Aliases\n\n`gopass` supports domain aliases. Given a secret structure like the following example and\na vendor that operates the same authentication backend behind several different domains\nthis will allow looking up an existing secret using either of the aliases.\n\n```shell\n$ gopass ls\ngopass\n└── websites/\n    ├── rainforest.com/\n    │   └── jim\n    └── woodlands.com/\n        └── jimbo\n$ cat .config/gopass/config\n...\n[domain-alias \"rainforest.com\"]\n\tinsteadOf = rainforest.de\n[domain-alias \"woodlands.com\"]\n\tinsteadOf = woodlands.de\n$ gopass show websites/rainforest.de/jim\n<password>\n$ gopass show websites/woodlands.de/jimbo\n<password>\n```\n\nNote: Until the gitconfig package support multi-values only one alias per domain is possible.\n\n### Safecontent\n\nGopass can limit display of certain *unsafe* fields in secrets.\nBy default no fields are obstructed, but if the `safecontent`\nconfig option is set to `true` the `Password` field is obstructed.\nAlso the special `unsafe-keys` key is evaluated. It expectes\na comma separated list of keys that will be obstructed when\nprinting the secret.\n\n## Related Projects\n\n- [pass](https://www.passwordstore.org) - The inspiration for this project, by Jason A. Donenfeld. `gopass` is a drop-in replacement for `pass` and can be used interchangeably (mostly!).\n- [passage](https://github.com/FiloSottile/passage) - passage is a fork of [password-store](https://www.passwordstore.org) that uses\n[age](https://age-encryption.org) as a backend instead of GnuPG. `gopass` has some amount of support for `passage` but can not be used fully interchangeably as of today. This might change in the future.\n\n## External Documentation\n\n* [gopass cheat sheet](https://woile.github.io/gopass-cheat-sheet/) ([source on github](https://github.com/Woile/gopass-cheat-sheet))\n* [gopass presentation](https://woile.github.io/gopass-presentation/) ([source on github](https://github.com/Woile/gopass-presentation))\n"
  },
  {
    "path": "docs/hacking.md",
    "content": "# Hacking on gopass\n\nNote: See [CONTRIBUTING.md](../CONTRIBUTING.md) for an overview.\n\nThis document provides an overview on how to develop on gopass.\n\n## Development\n\nThis project uses [GitHub Flow](https://guides.github.com/introduction/flow/). In other words, create feature branches from master, open an PR against master, and rebase onto master if necessary.\n\nWe aim for compatibility with the [latest stable Go Release](https://golang.org/dl/) only.\n\nWhile this project is maintained by volunteers in their free time we aim to triage issues weekly and release a new version at least every quarter.\n\n## Setting up an isolated development environment\n\n### With GPG\n\n`gopass` should fully respect `GOPASS_HOMEDIR` overriding all gopass internal paths.\nHowever it will still use your normal GPG keyring and configuration. To override this\nyou will need to set `GNUPGHOME` as well and possibly generate a new keyring.\n\n```bash\n$ export GOPASS_DEBUG_LOG=/tmp/gp1.log\n$ export GOPASS_HOMEDIR=/tmp/gp1\n$ mkdir -p $GOPASS_HOMEDIR\n$ export GNUPGHOME=$GOPASS_HOMEDIR/.gnupg\n# Make sure that you're using the correct keyring.\n$ gpg -K\ngpg: directory '/tmp/gp1/.gnupg' created\ngpg: keybox '/tmp/gp1/.gnupg/pubring.kbx' created\ngpg: /tmp/gp1/.gnupg/trustdb.gpg: trustdb created\n$ gpg --gen-key\n$ go build && ./gopass setup --crypto gpg --storage gitfs\n```\n\n### With age\n\nUsing `age` is recommended for development since it's easier to set up. Setting\n`GOPASS_HOMEDIR` should be sufficient to ensure an isolated environment.\n\n```bash\n$ export GOPASS_DEBUG_LOG=/tmp/gp1.log\n$ export GOPASS_HOMEDIR=/tmp/gp1\n$ mkdir -p $GOPASS_HOMEDIR\n$ go build && ./gopass setup --crypto age --storage gitfs\n```\n\n## Extending gopass\n\nThe main extension model are small binaries that use the [gopass API](https://pkg.go.dev/github.com/gopasspw/gopass/pkg/gopass/api) package. This package provides a small and easy to use API that should work with any up to date gopass setup.\n\nThis API encapsulates the exact same implementation that the CLI uses in a more nicely packaged format that's easier to use.\n\nNote: The API is operating directly on the password store. It does not involve network operations or connecting to a gopass instance.\n\nThe API does not support setting up a new password store (yet). Users will need have an existing password store\nor use `gopass setup` to create a new one. The API will attempt to load an existing configuration or use it's built-in\ndefaults. Then it initializes an existing password store and provides a simple set of CRUD operations.\n\nOur API has some [examples](../pkg/gopass/api/api_test.go) on how to use the API. The [gopass-hibp](https://github.com/gopasspw/gopass-hibp/blob/master/main.go) binary should provide a more complete example that can be used as a blueprint.\n\n```go\nimport (\n \"context\"\n \"fmt\"\n\n \"github.com/gopasspw/gopass/pkg/gopass/api\"\n \"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n)\n\n ctx := context.Background()\n\n gp, err := api.New(ctx)\n if err != nil {\n  panic(err)\n }\n\n // Listing secrets by their names (path within the store).\n ls, err := gp.List(ctx)\n if err != nil {\n  panic(err)\n }\n\n for _, s := range ls {\n  fmt.Printf(\"Secret: %s\", s)\n }\n\n // Writing secrets to a specific location (path) in the store.\n sec := secrets.New()\n sec.SetPassword(\"foobar\")\n if err := gp.Set(ctx, \"my/new/secret\", sec); err != nil {\n  panic(err)\n }\n\n // Reading secrets by their name and revision from within the store.\n sec, err = gp.Get(ctx, \"my/new/secret\", \"latest\")\n if err != nil {\n  panic(err)\n }\n fmt.Printf(\"content of %s: %s\\n\", \"my/new/secret\", string(sec.Bytes()))\n\n // Removing a secret by their name.\n if err := gp.Remove(ctx, \"my/new/secret\"); err != nil {\n  panic(err)\n }\n\n // Cleaning up (waiting for background processing to complete).\n if err := gp.Close(ctx); err != nil {\n  panic(err)\n }\n```\n"
  },
  {
    "path": "docs/hooks.md",
    "content": "# Gopass Hooks\n\n`gopass` exposes some hook-able events during it's invocation lifecycle. This allows users to inject additional functionality or perform addition logging.\n\n## Hook API\n\nAll hooks are subject to the following constraints:\n\n* Hooks do not inherit `STDIN` or `STDOUT` from the parent process.\n* Hooks do inherit `STDERR` from the parent process and may use it to print anything they want.\n* Hooks always run from the `password-store` directory.\n* Hooks are run with the `GOPASS_HOOK=1` in their environment and with `GOPASS_CONFIG_DIR` set to the configuration directory with which the original `gopass` command was started.\n* An exit from a hook (or execution failure) cases the entire `gopass` command to fail.\n* Hooks have at most one minute to complete.\n\n## Reentrancy\n\n`gopass` hooks are non-reentrant by default.\n\nFor example take this setup:\n\n```text\n[rm]\n  post-hook: ~/.config/gopass/hooks/post-rm.sh\n```\n\n```shell\n# ~/.config/gopass/hooks/post-rm.sh\ngopass rm some-other-entry\n```\n\nand finally\n\n```shell\n$ gopass rm foo\n```\n\nIn this scenario users should expect `post-rm.sh` to be executed exactly once on `gopass rm foo`.\n\nBut in fact it would be run twice: Once on `gopass rm foo` and once on `gopass rm some-other-entry`, i.e. hooks would reenter themselves when they try to use `gopass` internally.\n\nSince most users would find this confusing, `gopass` does not do this by default. However, if you really need to allow reentrant hooks you currently have one workaround:\n\n* You can `unset` the `GOPASS_HOOK` environment variable in your hook before running `gopass` internally.\n"
  },
  {
    "path": "docs/releases.md",
    "content": "## Releases\n\nNote: Only members who have at least `write` [access](https://github.com/gopasspw/gopass/settings/access) to the gopass repo can create releases.\n\nGopass uses [goreleaser](https://goreleaser.com/) to create releases. The configuration is in the file [`.goreleaser.yml`](../.goreleaser.yml).\n\nGoreleaser automates most but not all steps of a new release.\n\nNote: We use semantic versioning for the command line interface and tool behaviour\nbut not for the API (i.e. `pkg/gopass`). Maintaining both properties in the\nsame repository / Go module is too cumbersome.\n\n### Development overview\n\nPreparing and creating a new release requires a number of steps.\nStarting right after the previous release these are roughly:\n\n* Create a new Milestone in the [GitHub issue tracker](https://github.com/gopasspw/gopass/milestones)\n* Create or assign issues for the next Milestone\n  * We use [Slack](https://gopassworkspace.slack.com/) to discuss prioritization and responsibilities\n* After enough changes have been accumulated on the master branch we might agree to cut a new release\n* Now we survey open issues for any \"blockers\" that should make it into the next release\n  * This usually either happens in Slack or on semi-regular video calls\n* After all blockers have been addressed we move the remaining issues to the next milestone and prepare the release\n\n### Cutting a release\n\nThis section is a reference for contributors with write access to the gopass\nrepository.\n\n### Preparation\n\ngopass release should work with the latest upstream version of goreleaser.\n\n```bash\ngo get -u github.com/goreleaser/goreleaser\ncd $GOPATH/src/github.com/goreleaser/goreleaser\ngo install\n```\n\n### Releasing a new release\n\nThis subsection applies to a new release in direct succession of the previous one, i.e. releasing what's in the master branch. If you need to cherry-pick\nand base a release off of a previous one see the next subsection.\n\nWe develop new features and fixes and feature branches which are frequently\nmerged into master in our own forks of the repository.\n\n**Important: Do not push feature branches to the main repo.**\n\nWe have some helpers and automation in place to help us release new versions.\nA new release can be prepared by anyone (doesn't need to be a maintainer).\nReleasing it involves sending a PR that needs to be reviwed by the maintainers.\nOnce approved the PR will be merged and a tag needs to be pushed to trigger\nthe release process automation (using GitHub actions).\n\n```bash\n$ go run helpers/release/main.go\n# Follow the instructions to release a new minor version.\n# If you want to skip a patch level or bump the minor version\n# specify a version argument.\n$ go run helpers/release/main.go v1.18.2\n# If that confuses the changelog parser, you can specify a previous version\n# as well.\n$ go run helpers/release/main.go v1.18.2 v1.17.2\n```\n\nAfter the helper ran it will show you instructions how to push your release\nbranch and create a PR for review. Once it's merged a maintainer only needs\nto tag it and push the tag to trigger the release process automation.\n\nAfterwards a maintainer should run the post-release automation that will\nperform some cleanup, create new GitHub milestones and send out PRs to\nrolling release distributions.\n\n### Releasing a cherry-pick release\n\nThis subsection applies to a new release that should be based on a previous\nrelease that is not a direct ancestor of the master branch, i.e. because\nbreaking changes were introduced or other releases have happend in between.\n\nThis can still use our release automation but it will require some adjustments:\n\n* Check out the previous release tag (we usually only publish a release branch if we need to): `git checkout v1.12.2`\n* Create a new release preparation branch (the release automation will create the actual release branch later): `git checkout -b prep/v.1.12.3`\n* Cherry-pick the changes from the previous release into the release preparation branch: `git cherry-pick -x HASH1 HASH2 ...`\n  * Resolve any conflicts, make sure all tests pass and `git cherry-pick --continue`\n* Trigger the release preparation, it should pick up any changelog entries from the cherry-picked commit messages: `PATCH_RELEASE=true go run helpers/release/main.go`\n  * `PATCH_RELEASE=true` instructs it to not change the current branch to master\n* Push the release branch printed at the end to the repository (or your fork) and open a PR.\n  * IMPORANT: This PR will not be merged into master! We will just use it to create a tag and trigger the release automation.\n\n### Reproducible Builds\n\n`gopass` supports [reproducible builds](https://reproducible-builds.org/). When\nbuilding from git [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/)\ncan be used to override the compile date, .e.g `SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)`.\nWhen building a release `goreleaser` will automatically use the exact timestamp\nof the last commit.\n\nInternal paths are stripped using `-trimpath` and appropriate `-ldflags` (e.g. \n`-s`, `-w`). See the Makefile header for the exact set of flags.\n\n"
  },
  {
    "path": "docs/secrets.md",
    "content": "# Secrets\n\n`gopass` supports different secret formats. This page documents the different formats.\nYou can read more about how secrets are shown and parsed in the documentation for the [`show` commands](commands/show.md#parsing-and-secrets).\n\n## Key-Value\n\nThe new [Key-Value implementation](../pkg/gopass/secrets/akv.go) fully maintains the secret format\nwhen parsing but still does offer (limited) support for Key-Value operations, i.e. retrieving keys,\nlisting keys and writing (the first instance) keys. Some multi-value operations are not directly\nsupported. Use `gopass edit` for these.\n\nNote: The parser will ensure that every parsed secret contains a terminating newline. Even if the\ninput didn't have one.\n\nFormat:\n\n```text\nLine | Description\n   0 | Password\n 1-n | Body\n```\n\nThe parser uses the `: ` separator to identify potential Key Value pairs.\nWhen updating existing pairs only the first value will be rewritten.\nNew pairs are always appended at the end.\n\n## YAML\n\nNote: Using YAML is discouraged as YAML can be troublesome for humans, e.g. parsing of unquoted numbers.\n\nThe [YAML Format](../pkg/gopass/secrets/yaml.go) is used if there is a YAML marker (`---`) after the body:\n\n```text\nYAML is a gopass secret that contains a parsed YAML data structure.\nThis is a legacy data type that is discouraged for new users as YAML\nis neither trivial nor intuitive for users manually editing secrets (e.g.\nunquoted phone numbers being parsed as octal and such).\n\nFormat\n------\nLine  | Description\n    0 | Password\n  1-n | Body\n  n+1 | Separator (\"---\")\n  n+2 | YAML content.\n```\n\n## Deprecated formats\n\n`gopass` used to support different secret formats. These were deemed suboptimal and retired.\nWe still support parsing of these formats but don't write them anymore.\n\n### MIME\n\n`gopass` briefly had a custom secrets format based on multipart MIME. This did prove to be even more troublesome for humans than YAML so it was quickly deprecated.\n\nThese secrets are identified by a well known header.\n\n```text\nGOPASS-SECRET-1.0\nPassword: ...\n[other headers]\n\n[Body]\n```\n\n### Plain\n\nThe old KV implementation had some limitations so we did sometimes fall back to the old Plain format. With the new KV implementation this is not necessary anymore so this was removed.\n"
  },
  {
    "path": "docs/security.md",
    "content": "# Security, Known Limitations, and Caveats\n\nThis project aims to provide a secure and dependable credential store that can be used by individuals or teams.\n\nWe acknowledge that designing and implementing bullet-proof cryptography is very hard and try to leverage existing and proven technology instead of rolling our own implementations.\n\n## Security Goals\n\n* **Confidentiality** - Ensure that only authorized parties can understand the data.\n  * gopass attempts to protect the content of the secrets that it manages using [GNU Privacy Guard](#gnu-privacy-guard).\n  * gopass does NOT protect the presence of the secrets OR the names of the secrets. Care must be taken not to disclose any confidential information through the\n\tname of the secrets.\n* **Integrity** - Ensure that only authorized parties are allowed to modify data.\n  * gopass makes no attempt at protecting the integrity of a store. However, we plan to do this in the future.\n* **Availability** - Secrets must always be readable by exactly the specified recipients.\n  * gopass provides fairly good availability due to its decentralized nature.\n    For example, if your local password store is corrupted or destroyed, you can easily clone it from the Git server again.\n    Conversely, if the Git server is offline or is destroyed, you (and your teammates) have a complete copy of all of the secrets on your local machine(s).\n* **Non-repudiation** - Ensure that the involved parties actually transmitted and received messages.\n  * gopass makes no attempt to ensure this.\n\n### Additional Usability Goals\n\n* Sensible Defaults - This project shall try to make the right things easy to do and make the wrong things hard to do.\n\n## Threat Model\n\nThe threat model of gopass assumes there are no attackers on your local machine.\nCurrently no attempts are taken to verify the integrity of the password store.\nWe plan on using signed git commits for this.\nAnyone with access to the git repository can see which secrets are stored inside the store, but not their content.\n\n## GNU Privacy Guard\n\ngopass uses [GNU Privacy Guard](https://www.gnupg.org) (or GPG for short) to encrypt its secrets.\nThis makes it easy to build software we feel comfortable trusting our credentials with.\nThe first production release of GPG was on [September 9th, 1999](https://en.wikipedia.org/wiki/GNU_Privacy_Guard#History) and by now it is mature enough for most security experts to place a high degree of confidence in the software.\n\nWith that said, GPG isn't known for being the most user-friendly software.\nWe try to work around some of the usability limitations of GPG but we always do so keeping security in mind.\nThis means that, in some cases, the project carefully makes some security trade-offs in order to achieve better usability.\n\nSince gopass uses GPG to encrypt data, GPG needs to be properly set up beforehand.\n(GPG installation is covered in the [gopass installation documentation](https://github.com/gopasspw/gopass/blob/master/docs/setup.md).)\nHowever, basic knowledge of how [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) and the [web of trust model](https://en.wikipedia.org/wiki/Web_of_trust) work is assumed and necessary.\n\n## Generating Passwords\n\nPassword generation uses the same approach as the popular tool `pwgen`.\nIt uses `crypto/rand` to select random characters from the selected character classes.\n\n## git history and local files\n\nPlease keep in mind that by default, gopass stores its encrypted secrets in git.\n*This is a deviation from the behavior of `pass`, which does not force you to use `git`.* This has important implications.\n\nFirst, it means that every user of gopass (and any attacker with access to your git repo) has a local copy with the full history.\nIf we revoke access to a store from a user and re-encrypt the whole store, this user won't be able to access any *changed* or *added* secrets -- but they'll always be able to access\nsecrets by checking out old revisions from the repository.\n\n**If you revoke access from a user you SHOULD change all secrets they had access to!**\n\n## Private Keys Required\n\nPlease note that we try to make it hard to lock yourself out from your secrets.\nTo ensure that a user is always able to decrypt his own secrets, gopass requires that the user has at least one private key available (that matches the current public keys on file for the password store).\n"
  },
  {
    "path": "docs/setup.md",
    "content": "# Setup\n\n## Table of Contents\n\n1. [Pre-Installation Steps](#pre-installation-steps)\n2. [Installation Steps](#installation-steps)\n3. [Optional Post-Installation Steps](#optional-post-installation-steps)\n4. [Using gopass](#using-gopass)\n\n## Pre-Installation Steps\n\n### Download and Install Dependencies\n\ngopass needs some external programs to work:\n\n* `gpg` - [GnuPG](https://www.gnupg.org/), preferably in Version 2 or later\n* `git` - [Git SCM](https://git-scm.com/), any Version should be OK\n\nIt is recommended to have either `rng-tools` or `haveged` installed to speed up\nkey generation if these are available for your platform.\n\n#### Ubuntu & Debian\n\n```bash\napt-get update\napt-get install git gnupg rng-tools\n```\n\n#### RHEL & CentOS\n\n```bash\nyum install gnupg2 git rng-tools\n```\n\n#### Arch Linux\n\n```bash\npacman -S gnupg2 git rng-tools\n```\n\n#### MacOS\n\nIf you haven't already, install [homebrew](http://brew.sh). And then:\n\n```bash\nbrew install gnupg2 git\n```\n\n#### Windows\n\n* Download and install [GPG4Win](https://www.gpg4win.org/).\n* Download and install [the Windows git installer](https://git-scm.com/download/win).\n\nAlternatively, these can be installed via [chocolatey](https://chocolatey.org), \n\n```ps1\nchoco install git\nchoco install gpg4win\n```\n\n[Scoop](https://scoop.sh), \n\n```ps1\nscoop install git\n# add Extras bucket, if you haven't already\n# scoop bucket add extras\nscoop install gpg4win\n```\n\nor [winget](https://aka.ms/winget-docs).\n\n```ps1\nwinget install Git.Git\nwinget install GnuPG.Gpg4win\n```\n\n#### OpenBSD\n\nFor OpenBSD -current:\n\n```shell\npkg_add gopass\n```\n\nFor OpenBSD 6.2 and earlier, install via `go install`.\n\nPlease note that the OpenBSD builds uses `pledge(2)` to disable some syscalls,\nso some features (e.g. version checks, auto-update) are unavailable.\n\n#### FreeBSD\n\nFor FreeBSD 11 and newer:\n\n```shell\npkg install gopass\n```\n\n### Set up a GPG key pair\n\ngopass depends on the `gpg` program for encryption and decryption. You **must** have a\nsuitable key pair. To list your current keys, you can do:\n\n```bash\ngpg --list-secret-keys\n```\n\nIf there is no output, then you don't have any keys. To create a new key:\n\n```bash\ngpg --full-generate-key\n```\n\nYou will be presented with several options:\n\n* Key type: Choose either \"RSA and RSA\" or \"DSA and ElGamal\".\n* Key size: Choose at least 2048.\n* Validity: 5 to 10 years is a good default.\n* Enter your real name and primary email address.\n* A comment is not necessary.\n* Pass phrase: Make sure to pick a very long pass phrase, not just a simple password. Remember this should be stronger than any of the secrets you store in the password store. You can configure the GPG Agent later to save you repetitive typing.\n\nNow, you have created a public and private key pair. If you don't know what that means, or if you are not familiar with GPG, we highly recommend you do a little reading on the subject. Check out the following manuals:\n\n* [\"git + gpg, know thy commits\" at coderwall](https://coderwall.com/p/d3uo3w/git-gpg-know-thy-commits)\n* [\"Generating a new GPG key\" by GitHub](https://help.github.com/articles/generating-a-new-gpg-key/)\n\n## Installation Steps\n\nDepending on your operating system, you can either use a package manager, download a pre-built binary, or install from source. If you have a working Go development environment, we recommend building from source.\n\n### MacOS\n\nIf you haven't already, install [homebrew](http://brew.sh). And then:\n\n```bash\nbrew install gopass\n```\n\nAlternatively, you can install gopass from the appropriate Darwin release from the repository [releases page](https://github.com/gopasspw/gopass/releases).\n\nIf you're using a password on your GPG key, you also have to install `pinentry-mac` from brew and configure it in your `~/gpg/gpg-agent.conf`:\n\n```bash\nbrew install pinentry-mac\nPINENTRY=$(which pinentry-mac)\necho \"pinentry-program ${PINENTRY}\" >>~/.gnupg/gpg-agent.conf\ndefaults write org.gpgtools.common UseKeychain NO\n```\n\nThe last step is important if you want to stop `pinentry-mac` from caching your passphrase in the MacOS Keychain by default (indefinitely).\n\n### Ubuntu, Debian, Deepin, Devuan, Kali Linux, Pardus, Parrot, Raspbian\n\n**WARNING**: The official Debian repositories (and derived distributions) contain\na package named `gopass` that is not related to this project in any way.\nIt's a similar tool with a completely independent implementation and feature set.\nWe are aware of this issue but can not do anything about it.\n\nWhen installing on Ubuntu or Debian you can either download the `deb` package,\n[install manually or build from source](#installing-from-source) or use our APT repository.\n\n```bash\n$ curl https://packages.gopass.pw/repos/gopass/gopass-archive-keyring.gpg | sudo tee /usr/share/keyrings/gopass-archive-keyring.gpg >/dev/null\n$ cat << EOF | sudo tee /etc/apt/sources.list.d/gopass.sources\nTypes: deb\nURIs: https://packages.gopass.pw/repos/gopass\nSuites: stable\nArchitectures: all amd64 arm64 armhf\nComponents: main\nSigned-By: /usr/share/keyrings/gopass-archive-keyring.gpg\nEOF\n$ sudo apt update\n$ sudo apt install gopass gopass-archive-keyring\n```\n\nNote: We also have an unstable track that sometimes contains pre-release versions. Use `https://packages.gopass.pw/repos/gopass-unstable` if you want to help with early testing.\n\n#### Manual download\n\nFirst, find the latest .deb release from the repository [releases page](https://github.com/gopasspw/gopass/releases). Then, download and install it:\n\n```bash\nwget [the URL of the latest .deb release]\nsudo dpkg -i gopass-1.2.0-linux-amd64.deb\n```\n\n### Gentoo\n\nThere is an overlay that includes gopass. Run these commands to install gopass through `emerge`.\n\n```bash\nlayman -a go-overlay\nemerge -av gopass\n```\n\n### Fedora\n\n```bash\ndnf install gopass\n```\n\n### Red Hat / CentOS\n\nThere is an unofficial RPM build maintained by a contributor.\n\n```bash\n# if you're using dnf (needs dnf-plugins-core)\ndnf copr enable daftaupe/gopass\ndnf install gopass\n# of if you're using an older distribution (needs yum-plugin-copr)\nyum copr enable daftaupe/gopass\nyum install gopass\n```\n\n### Arch Linux\n\n```bash\npacman -S gopass\n```\n\n### Windows\n\nYou can install `gopass` with [Chocolatey](https://chocolatey.org/packages/gopass),\n\n```ps1\nchoco install gopass\n```\n\n[Scoop](https://scoop.sh/#/apps?q=gopass),\n\n```ps1\nscoop install gopass\n```\n\nor winget.\n\n```ps1\nwinget install gopass.gopass\n```\n\nAlternatively, download and install a suitable Windows build from the repository [releases page](https://github.com/gopasspw/gopass/releases).\n\n### Installing from Source\n\nIf you have [Go](https://golang.org/) already installed, you can use `go install` to automatically download the latest version:\n\n```bash\nGO111MODULE=on go install -u github.com/gopasspw/gopass@latest\n```\n\nWARNING: `latest` is not a stable release. It is recommended to use a specific version.\n\nIf `$GOPATH/bin` is in your `$PATH`, you can now run `gopass` from anywhere on your system.\n\n### Upgrade\n\nTo use the self-updater run:\n\n```bash\ngopass update\n```\n\nor to upgrade with Go installed, run:\n\n```bash\ngo install github.com/gopasspw/gopass@latest\n```\n\n## Optional Post-Installation Steps\n\n### Securing Your Editor\n\nVarious editors may store temporary files outside of the secure working directory when editing secrets.\nWe advise you to check and disable this behavior for your editor of choice.\n\nHere are a few useful example settings:\n\n```vim\n\" neovim on Linux\nautocmd BufNewFile,BufRead /dev/shm/gopass* setlocal noswapfile nobackup noundofile shada=\"\"\n\" neovim on MacOS\nautocmd BufNewFile,BufRead /private/**/gopass** setlocal noswapfile nobackup noundofile shada=\"\"\n\" vim on Linux\nautocmd BufNewFile,BufRead /dev/shm/gopass* setlocal noswapfile nobackup noundofile viminfo=\"\"\n\" vim on MacOS\nautocmd BufNewFile,BufRead /private/**/gopass** setlocal noswapfile nobackup noundofile viminfo=\"\"\n```\n\nNote: gopass will attempt to detect the correct hardning flags to be used for the editor. It will pass them on\ninvocation.\n\n### Migrating from pass to gopass\n\ngopass is compatible with pass and can be used as a drop-in replacement. If you are migrating from pass to gopass, you can simply use your existing password store and everything should just work.\n\nYou can even alias `pass` to `gopass` to start using it right away. For example, assuming `$HOME/bin/` exists and is present in your `$PATH`:\n\n```bash\nln -s $(which gopass) $HOME/bin/pass\n```\n\nWhile gopass maintains compatibility with pass, it also introduces new features such as mounts for managing multiple stores and structured secrets using YAML. On the other hand, pass has a more extensive plugin system. You can choose the tool that best fits your workflow, or even use both in parallel.\n\n### Migrating to gopass from Other Password Stores\n\nBefore migrating to gopass, you may have been using other password managers (such as [KeePass](https://keepass.info/), for example). If you were, you might want to import all of your existing passwords over. Because gopass is fully backwards compatible with pass, you can use any of the existing migration tools found under the \"Migrating to pass\" section of the [official pass website](https://www.passwordstore.org/), for example [pass-import](https://github.com/roddhjav/pass-import).\n\n### Enable Bash Auto completion\n\nIf you use Bash, you can use the following command to enable auto completion for all users for sub-commands like `gopass show`, `gopass ls` and others.\n\n```bash\ngopass completion bash | sudo tee $(pkg-config --variable=completionsdir bash-completion)/gopass\n```\n\nTo enable bash completions for the current user only:\n```bash\ngrep -q \"source <(gopass completion bash)\" ~/.bashrc || echo \"source <(gopass completion bash)\" >> ~/.bashrc\n```\n\n\n**MacOS**: The version of bash shipped with MacOS may [require a workaround](https://stackoverflow.com/questions/32596123/why-source-command-doesnt-work-with-process-substitution-in-bash-3-2) to enable auto completion. If the instructions above do not work try the following:\n\n```bash\nsource /dev/stdin <<<\"$(gopass completion bash)\"\n```\n\n### Enable Z Shell Auto completion\n\nIf you use zsh, `make install` or `make install-completion` should install the completion in the correct location.\n\nIf zsh autocompletion is still not functional, or if you want to install it manually, you can run the following commands:\n\n```bash\ngopass completion zsh > ~/_gopass \nsudo mv ~/_gopass /usr/share/zsh/site-functions/_gopass\nrm -i ${ZDOTDIR:-${HOME:?No ZDOTDIR or HOME}}/.zcompdump && compinit\n\n```\n\nThen exit and re-run zsh if the last command failed.\n\nNotice that it is important to directly redirect Gopass' output to a file,\nusing pipes or echo mess up the output.\na completion file that is supposed to be handled by zsh and to be installed in the zsh\ncompletions directory, as defined by either the standard `/usr/share/zsh/site-functions/` path,\nor by a user-specified `fpath` folder. It is not meant to used with `source`.\n\nIf zsh completion is still not working, you might want to add the following to your `.zshrc` file:\n\n```bash\nautoload -U compinit && compinit\n```\n\nif you don't have it already.\n\n### Enable fish completion\n\nIf you use the [fish](https://fishshell.com/) shell, you can enable experimental shell completion by the following command:\n\n```fish\nmkdir -p ~/.config/fish/completions and; gopass completion fish > ~/.config/fish/completions/gopass.fish\n```\n\nand start a new shell afterwards.\n\nSince writing fish completion scripts is not yet supported by the CLI library we use, this completion script is missing a few features. Feel free to contribute if you want to improve it.\n\n### dmenu / rofi support\n\nIn earlier versions gopass supported [dmenu](http://tools.suckless.org/dmenu/). We removed this and encourage you to call dmenu yourself now.\n\nThis also makes it easier to call gopass with any drop-in replacement of dmenu, like [rofi](https://github.com/DaveDavenport/rofi), for example, since you would just need to replace the `dmenu` call below by `rofi -dmenu`.\n\n```bash\n# Simply copy the selected password to the clipboard\ngopass ls --flat | dmenu | xargs --no-run-if-empty gopass show -c\n# First pipe the selected name to gopass, decrypt it and type the password with xdotool.\ngopass ls --flat | dmenu | xargs --no-run-if-empty gopass show -o | xdotool type --clearmodifiers --file -\n# First pipe the selected name to gopass, and type the value from the key \"username\" with xdotool.\ngopass ls --flat | dmenu | xargs --no-run-if-empty -- bash -c 'gopass show -f $0 username' | head -n 1 | xdotool type --clearmodifiers --file -\n# Otherwise type the name of the entry using xdotool, in case you are not including a username key in your entries\ngopass ls --flat | dmenu | sed 's!.*/!!' | tr -d '\\n' | xdotool type --clearmodifiers --file -\n```\n\nYou can then bind these command lines to your preferred shortcuts in your window manager settings, typically under `System Settings > Keyboard > Shortcuts > Custom Shortcuts`. In some cases you may need to wrap it with `bash -c 'your command'` in order for it to work (tested and working in Ubuntu 18.04).\n\n### Filling in passwords from browser\n\nGopass allows filling in passwords in browsers leveraging a browser plugin like [gopass bridge](https://github.com/gopasspw/gopassbridge).\nThe browser plugin communicates with gopass-jsonapi via JSON messages.\nTo allow the plugin to start gopass-jsonapi, a [native messaging manifest](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) must be installed for each browser.\nChrome, Chromium and Firefox are supported, currently.\n\n**Upgrade to gopass v1.10 / v1.11**:\n`gopass-jsonapi` is now its own binary file, which you need to install separately.\n\nThe binary for v1.10 and v1.11 can be downloaded and unpacked from\n[archive files on Github Releases](https://github.com/gopasspw/gopass/releases/tag/v1.11.0).\n\nYou need to run `gopass-jsonapi configure` after the upgrade to configure your browser for the new command.\n\n**Upgrade to gopass v1.12**\nThe new binary can be downloaded from the latest\n[Github Release on gopass-jsonapi](https://github.com/gopasspw/gopass-jsonapi/releases).\n\nFor more detailed instructions, please read: [gopass-jsonapi/README](https://github.com/gopasspw/gopass-jsonapi/blob/main/README.md).\n\n### Storing and Syncing your Password Store with git\n\nThis is the recommended way to use `gopass`.\n\nNOTE: We recommend using a private Git repository. A public one will keep\nyour credentials secure, but it will leak metadata.\n\nTo use `gopass` with `git` either create a new git repository or clone an existing\npassword store.\n\n#### New password store with git\n\nCreate a new repository, either locally or on a server, then specify this\nrepository during the `gopass setup`.\n\n```bash\n$ gopass setup --crypto gpg --storage gitfs # used by default\n[...]\n# provide an existing, empty git remote, e.g. git@gitlab.example.org:john/passwords.git\n```\n\n#### Existing password store with git\n\nIf you have created a password store with `git`, `gopass` can easily clone it.\n\n```bash\ngopass clone git@gitlab.example.org:john/passwords.git\n```\n\n### Storing and Syncing your Password Store with Google Drive / Dropbox / Syncthing / etc\n\nThe recommended way to use Gopass is to sync your store with a git repository, preferably a private one, since the name and path of your secrets might reveal information that you'd prefer to keep private.\nHowever, shall you prefer to, you might also use the `noop` storage backend that is meant to store data on a cloud provider instead of a git server.\n\nPlease be warned that using cloud-based storage may negatively impact the confidentiality of your store. However, if you wish to use one of these services, you can do so.\n\nFor example, to use gopass with [Google Drive](https://drive.google.com):\n\n```bash\ngopass setup --storage fs\nmv .password-store/ \"Google Drive/Password-Store\"\ngopass config mounts.path \"~/Google Drive/Password-Store\"\n```\n\n### Download a GUI\n\nBecause gopass is fully backwards compatible with pass, you can use some existing graphical user interfaces / frontends:\n\n* Android - [PwdStore](https://github.com/zeapo/Android-Password-Store)\n* iOS - [Pass for iOS](https://github.com/davidjb/pass-ios#readme)\n* Windows / MacOS / Linux -  [QtPass](https://qtpass.org/)\n\nThere is also [Gopass UI](https://github.com/codecentric/gopass-ui) which was exclusively implemented for gopass and is available for MacOS, Linux and Windows.\n\nOthers can be found at the \"Compatible Clients\" section of the [official pass website](https://www.passwordstore.org/).\n\n## Using gopass\n\nOnce you have installed gopass, check out the [features documentation](https://github.com/gopasspw/gopass/blob/master/docs/features.md) for some quick usage examples.\n\n### Using the onboarding wizard\n\nRunning `gopass` with no existing store will start the onboarding wizard which\nwill guide you through the setup of gopass.\n\n### Batch bootstrapping\n\nIn order to simplify the setup of gopass for your team members it can be run in a fully scripted bootstrap mode.\n\n```bash\n# First initialize a new shared store and push it to an empty remote\ngopass --yes setup --remote github.com/example/pass.git --alias example --create --name \"John Doe\" --email \"john.doe@example.com\"\n\n# For every other team member initialize a new store and clone the existing remote\ngopass --yes setup --remote github.com/example/pass.git --alias example --name \"Jane Doe\" --email \"jane.doe@example.com\"\n```\n\nThe first command will create a new mount named `example` and push it to an empty (`--create`) remote.\nIt will fail if the remote at `github.com/example/pass.git` is not empty.\n\nThe second command will clone the existing (no `--create` flag) remote `github.com/example/pass.git`\nand mount it as the mount point `example`.\n"
  },
  {
    "path": "docs/usecases/gpaste.md",
    "content": "# Use case: GPaste Clipboard management system\n\n## Summary\n\nOn Linux one might want to use the [GPaste](https://github.com/Keruspe/GPaste) clipboard manager. Using the `GOPASS_CLIPBOARD_COPY_CMD` and `GOPASS_CLIPBOARD_CLEAR_CMD` environment variables one can instruct gopass to use the GPaste client directly. This hides passwords when viewed in the manager and removes them by name after the timeout.\n\n## Usage\n\n### Helper scripts\n\nBoth environment variables expect a path to an executable or the name of an executable in the `PATH` environment variable. The executables receive the name of the password as the first argument and the password (copy) or its checksum (clear) in `STDIN`. To use the GPaste client one has to use helper scripts like this:\n\n`~/.local/scripts/gopass_clipboard_copy_cmd.sh`\n```sh\n#!/bin/sh\n\n# gpaste-client will use the password in /dev/stdin\ngpaste-client add-password \"$1\"\n```\n\n`~/.local/scripts/gopass_clipboard_clear_cmd.sh`\n```sh\n#!/bin/sh\n\ngpaste-client delete-password \"$1\"\n```\n\nMake sure both are executable: `chmod +x ~/.local/scripts/gopass_clipboard_{copy,clear}_cmd.sh`\n\n### Setting the environment variables\n\n#### Shell\n\nYou can set the environment variables in the `.profile` file of your shell, for example:\n\n`~/.bash_profile`\n```sh\n# [...]\nexport GOPASS_CLIPBOARD_COPY_CMD=\"$HOME/.local/scripts/gopass_clipboard_copy_cmd.sh\"\nexport GOPASS_CLIPBOARD_CLEAR_CMD=\"$HOME/.local/scripts/gopass_clipboard_clear_cmd.sh\"\n# [...]\n```\n\n#### Graphical environment\n\nIf you are using X11 you can set the above in `~/.xprofile`.\n\nOn Wayland one may use systemd user environment variables:\n\n`~/.config/environment.d/gopass.conf`\n```sh\nGOPASS_CLIPBOARD_COPY_CMD=\"$HOME/.local/scripts/gopass_clipboard_copy_cmd.sh\"\nGOPASS_CLIPBOARD_CLEAR_CMD=\"$HOME/.local/scripts/gopass_clipboard_clear_cmd.sh\"\n```\n\nA reboot might be required."
  },
  {
    "path": "docs/usecases/multi-store.md",
    "content": "# Use case: Multiple Stores\n\n`gopass` aims to support up to 100 mounted substores without noticeable\nimpact on most operations.\n\nUsing multiple stores is the preferred approach to solving different tasks\nlike encrypting different sets of secrets for different recipients (as\nopposed to e.g. recipient lists in sub directories).\n\nWe understand that being able to use multiple stores is a key features of\n`gopass` and we commit to maintaining and improving this feature in the\nlong term.\n\n"
  },
  {
    "path": "docs/usecases/readonly-store.md",
    "content": "# Use case: Readonly Store\n\n## Summary\n\nAllow a password store or a set of sub-stores to be configured in readonly mode in team sharing scenario.\n\n## Background\n\nIn a team sharing scenario that we share password store among team members, usually we want each member to be able to pull and push so that anyone can share secret data with others. However, typically in a large size team, we may want the sharing to be more restricted where only priviledged users are allowed to push secret data to the remote store while others can only pull from the remote store.\n\nThe current gopass behavior is that it will auto sync (pull and push) betweeen local store and remote store any time when there is a change at local. This means anyone can push their personal data to the centrally controlled remote store which will be polluted with arbitrary data unexpectedly.\n\nTo workaround this, we can configure \"Collaborators & teams\" on GitHub side to grant read only permission to those who do not necessarily need push, but the gopass on their machines will still keep auto pushing and prompt with errors which is annoying.\n\n## Proposal\n\nUltimately it turns out that this scenario requires a feature such as a store in readonly mode, where people can configure their local store or a set of sub-stores in readonly mode, to disable the writes and the autosync-on-writes to the store, but they can still pull to sync the latest changes from the remote store. This is not a one-stop solution for the RBAC model of the team sharing store, because we still need GitHub to setup the store access at server-side, but it will provide better usage experience from gopass client side.\n\nConfiguration examples:\n\n```bash\n# To print the config\n$ gopass config core.readonly\n# To setup the config\n$ gopass config core.readonly true\ncore.readonly: true\n# To apply the config to a sub-store\n$ gopass config --store team-sharable core.readonly true\ncore.readonly: true\n```\n\n## References\n\n* https://github.com/gopasspw/gopass/issues/1878\n"
  },
  {
    "path": "docs/usecases/secure-otp/sign-in.puml",
    "content": "@startuml\ntitle Sign-In with local otp\n\nactor User\ndatabase \"Git-Passwordstore\"\ndatabase \"Local-Passwordstore\"\ndatabase \"gpg-key\"\n\nactivate User\n\nactivate \"Git-Passwordstore\"\nUser -> \"Git-Passwordstore\": show (login,password)\n\"Git-Passwordstore\" -> \"gpg-key\": decrypt\n\"gpg-key\" -> User: enter passphrase\ndeactivate \"Git-Passwordstore\"\n\nactivate Website\nUser -> Website: sign-in (login,password)\nWebsite -> User: request: otp-code\n\nUser -> \"Local-Passwordstore\": otp\nactivate \"Local-Passwordstore\"\n\"Local-Passwordstore\" -> \"gpg-key\": decrypt(otp-token)\n\"Local-Passwordstore\" -> \"Local-Passwordstore\": generate otp-code (otp-token,local-time)\ndeactivate \"Local-Passwordstore\"\n\nUser -> Website: enter (otp-code)\nWebsite -> Website: validate (otp-code,website-time,otp-token)\nUser <-- Website: success\n\n@enduml\n"
  },
  {
    "path": "docs/usecases/secure-otp/sign-up.puml",
    "content": "@startuml\ntitle Sign-Up with local otp\n\nactor User\ndatabase \"Git-Passwordstore\"\ndatabase \"Local-Passwordstore\"\ndatabase \"gpg-key\"\n\nactivate User\nactivate Website\nUser -> Website: sign-up(login,password)\nWebsite -> Website: create otp-token\nUser <-- Website: show otp-token\ndeactivate Website\n\nUser -> \"Git-Passwordstore\": store (login,password) for Website\n\"Git-Passwordstore\" -> \"gpg-key\": encrypt\n\nUser -> \"Local-Passwordstore\": store (otp-token) for Website\n\"Local-Passwordstore\" -> \"gpg-key\": encrypt\n\n\n@enduml"
  },
  {
    "path": "docs/usecases/secure-otp.md",
    "content": "# Use case: Secure otp\n\n## Summary\nOTP is typically used to increase security of login process by using an additional factor. Depending on the threat-level, you can store OTP tokens separately from login and password.\n\n## Normal secure setup\nMost threats are mitigated by storing otp tokens in your \"Git-Passwordstore\" next to your login & password. An entry may look like:\n\n```\ngopass show git-passwordstore/website/yourLogin\n```\nWill result in\n\n```\nyourPassword\n---\nlogin: yourLogin\nurl: https://website.com\ntotp: YourOtpTokenBase32Encoded\n\n```\n\nYou can generate your otp code with\n\n```\ngopass otp git-passwordstore/website\n\n897402 lasts 17s \t|-------------=================|\n```\n\n\n## Advanced Secure Setup\nFor protection against exposed \"Git-Passwordstores\" you can use a \"Local-Passwordstore\" to store your otp-tokens. Entries may look like:\n\n```\ngopass show git-passwordstore/website/yourLogin\n```\n\nwill result in \n\n```\nyourPassword\n---\nlogin: yourLogin\nurl: https://website.com\n```\n\n```\ngopass show local-passwordstore/website/yourOtp\n```\n\nwill result in \n\n\n```\notpauth://totp/Website:yourLogin?secret=YourOtpTokenBase32Encoded&issuer=Website\n```\n\nYou can generate your otp code with\n\n```\ngopass otp local-passwordstore/website/yourOtp\n\n897402 lasts 17s \t|-------------=================|\n```\n\n## Rely on a hardware token\n\nNotice that ideally, the secret key that's able to decrypt your OTP secrets should be stored on a hardware token that is requiring some kind of user interaction to decrypt them.\n\nThis is done with Gopass by setting up a second store and not using the same public keys as for your main password store.\nRefer to [our mount doc for setting one up](docs/commands/mounts.md).\n\nThe public keys used for your OTP store should ideally be stored only on hardware tokens, or maybe generated on an airgaped machined and then backuped offline, before being transferred on a hardware token.\n(There are [multiple](https://research.kudelskisecurity.com/2017/04/28/configuring-yubikey-for-gpg-and-u2f/) [guides](https://support.yubico.com/hc/en-us/articles/360013790259-Using-Your-YubiKey-with-OpenPGP) online about how to do this.)\n\nIt is highly recommended to set your [\"touch policy\" to `always`](https://docs.yubico.com/yesdk/users-manual/application-piv/pin-touch-policies.html#touch-policies) when using a hardware token for OTP.\n\nThis avoids storing 2FA codes in the same place as your passwords, which would kind of nullify the security advantage of relying on a 2FA code in the first place.\n\n## Threat analysis\n\n### Assets & Dataflow\n![Sign-In with local otp](secure-otp/Sign-In.png)\n\n### Actors\n* Shop-Hacker-Kid: Buys pawned credentials.\n* Organised-Crime-Hacker: Uses phishing, may hack your git server.\n* Customs-Officer-Hacker: Copies your hard drive, may ask for your facebook password.\n* Intelligence-Hacker: Break in to your flat physically or hack your computer remote, may place a key logger.\n\n### Threats\n\n1. Shop-Hacker-Kid tests bought credentials for your account on \"Website\".\n   1. Mitigated by using otp (both password store locations are secure enough).\n2. Organised-Crime-Hacker phishes your \"Website\" login and password.\n   1. Mitigated by using otp (both password store locations are secure enough).\n3. Organised-Crime-Hacker hacks your git server and gets a clone of your Git-Passwordstore.\n   1. Mitigated by using otp in your Git-Passwordstore as long as your gpg-key and passphrase is unexposed.\n   2. Mitigated by using otp in your Local-Passwordstore.\n4. Customs-Officer-Hacker copies your hard drive.\n   1. Mitigated as long as your hard drive is encrypted\n   2. Mitigated if your passphrase remains unexposed.\n5. Intelligence-Hacker copies your hard drive, places a key logger and after some weeks reads all your keyboard inputs.\n   1. Only a not exposed hardware otp token will mitigate this threat.\n"
  },
  {
    "path": "fish.completion",
    "content": "#!/usr/bin/env fish\nset PROG 'gopass'\n\nfunction __fish_gopass_needs_command\n  set -l cmd (commandline -opc)\n  if [ (count $cmd) -eq 1 ] && [ $cmd[1] = $PROG ]\n    return 0\n  end\n  return 1\nend\n\nfunction __fish_gopass_uses_command\n  set cmd (commandline -opc)\n  if [ (count $cmd) -gt 1 ]\n    if [ $argv[1] = $cmd[2] ]\n      return 0\n    end\n  end\n  return 1\nend\n\nfunction __fish_gopass_print_gpg_keys\n  gpg2 --list-keys | grep uid | sed 's/.*&lt;\\(.*\\)>/\\1/'\nend\n\nfunction __fish_gopass_print_entries\n  gopass ls --flat | sed \"s/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/'/\\\\\\\\'/g\"\nend\n\nfunction __fish_gopass_print_dir\n  for i in (gopass ls --flat | sed \"s/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/'/\\\\\\\\'/g\")\n\t  echo (dirname $i)\n\tend | sort -u\nend\n\n# erase any existing completions for gopass\ncomplete -c $PROG -e\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -s c -l clip -r -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a age -d 'Command: age commands'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age' -a agent -d 'Subcommand: Manage the age agent'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age agent -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age' -a identities -d 'Subcommand: List age identities used for decryption and encryption'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age identities -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age' -a lock -d 'Subcommand: Lock the age agent'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command age lock -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a alias -d 'Command: Print domain aliases'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a audit -d 'Command: Decrypt all secrets and scan for weak or leaked passwords'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a cat -d 'Command: Decode and print content of a binary secret to stdout, or encode and insert from stdin'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a clone -d 'Command: Clone a password store from a git repository'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a completion -d 'Command: Bash and ZSH completion'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion' -a bash -d 'Subcommand: Source for auto completion in bash'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion bash -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion' -a zsh -d 'Subcommand: Source for auto completion in zsh'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion zsh -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion' -a fish -d 'Subcommand: Source for auto completion in fish'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion fish -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion' -a openbsdksh -d 'Subcommand: Source for auto completion in OpenBSD&#39;s ksh'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion openbsdksh -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion' -a help -d 'Subcommand: Shows a list of commands or help for one command'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command completion help -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a config -d 'Command: Display and edit the configuration file'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a convert -d 'Command: Convert a store to different backends'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a copy -d 'Command: Copy secrets from one location to another'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command copy' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a create -d 'Command: Easy creation of new secrets'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a delete -d 'Command: Remove one or many secrets from the store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command delete' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a edit -d 'Command: Edit new or existing secrets'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command edit' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a env -d 'Command: Run a subprocess with a pre-populated environment'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a find -d 'Command: Search for secrets'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a fsck -d 'Command: Check store integrity, clean up artifacts and possibly re-write secrets'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a fscopy -d 'Command: Copy files from or to the password store'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a fsmove -d 'Command: Move files from or to the password store'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a generate -d 'Command: Generate a new password'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command generate' -a \"(__fish_gopass_print_dir)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a git -d 'Command: Run a git command inside a password store: gopass git [--store=&lt;store&gt;] &lt;git-command&gt;'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a grep -d 'Command: Search for secrets files containing search-string when decrypted.'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a history -d 'Command: Show password history'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a init -d 'Command: Initialize new password store.'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a insert -d 'Command: Insert a new secret'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command insert' -a \"(__fish_gopass_print_dir)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a link -d 'Command: Create a symlink'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a list -d 'Command: List existing secrets'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command list' -a \"(__fish_gopass_print_dir)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a merge -d 'Command: Merge multiple secrets into one'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a mounts -d 'Command: Edit mounted stores'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts' -a add -d 'Subcommand: Mount a password store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l create -d \"Create a new store at this location\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts add -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts' -a remove -d 'Subcommand: Umount an mounted password store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts remove -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts' -a versions -d 'Subcommand: Display mount provider versions'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command mounts versions -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a move -d 'Command: Move secrets from one location to another'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command move' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a otp -d 'Command: Generate time- or hmac-based tokens'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command otp' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a process -d 'Command: Process a template file'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a pwgen -d 'Command: Generate passwords'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a rcs -d 'Command: Run a RCS command inside a password store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs' -a init -d 'Subcommand: Init RCS repo'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l store -d \"Store to operate on\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l name -d \"Git Author Name\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l email -d \"Git Author Email\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l storage -d \"Select storage backend [cryptfs fossilfs gitfs jjfs]\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs init -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs' -a status -d 'Subcommand: RCS status'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l store -d \"Store to operate on\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command rcs status -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a recipients -d 'Command: Edit recipient permissions'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients' -a ack -d 'Subcommand: Update recipients.hash'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l store -d \"Store to operate on\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients ack -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients' -a add -d 'Subcommand: Add any number of Recipients to any store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l store -d \"Store to operate on\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l force -d \"Force adding non-existing keys\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients add -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients' -a remove -d 'Subcommand: Remove any number of Recipients from any store'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l store -d \"Store to operate on\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l force -d \"Force adding non-existing keys\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command recipients remove -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a reorg -d 'Command: Reorganize a password store by editing a text file'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a setup -d 'Command: Initialize a new password store'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a show -d 'Command: Display the content of a secret'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command show' -a \"(__fish_gopass_print_entries)\"\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a sum -d 'Command: Compute the SHA256 checksum'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a sync -d 'Command: Sync all local stores with their remotes'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a templates -d 'Command: Edit templates'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates' -a show -d 'Subcommand: Show a secret template.'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates show -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates' -a edit -d 'Subcommand: Edit secret templates.'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates edit -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates' -a remove -d 'Subcommand: Remove secret templates.'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l yes -d \"Always answer yes to yes/no questions\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l clip -d \"Copy the password value into the clipboard\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l alsoclip -d \"Copy the password and show everything\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l qr -d \"Print the password as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l qrbody -d \"Print the body as a QR Code\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l unsafe -d \"Display unsafe content (e.g. the password) even if safecontent is enabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l safe -d \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l password -d \"Display only the password. Takes precedence over all other flags.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l revision -d \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -&lt;N&gt; to select the Nth oldest revision of this entry.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l noparsing -d \"Do not parse the output.\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l nosync -d \"Disable auto-sync\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l chars -d \"Print specific characters from the secret\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l help -d \"show help\"'\ncomplete -c $PROG -f -n '__fish_gopass_uses_command templates remove -l version -d \"print the version\"'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a unclip -d 'Command: Internal command to clear clipboard'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a update -d 'Command: Check for updates'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a version -d 'Command: Display version'\ncomplete -c $PROG -f -n '__fish_gopass_needs_command' -a help -d 'Command: Shows a list of commands or help for one command'\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gopasspw/gopass\n\ngo 1.25\n\nrequire (\n\tfilippo.io/age v1.2.1\n\tgithub.com/ProtonMail/go-crypto v1.3.0\n\tgithub.com/blang/semver/v4 v4.0.0\n\tgithub.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b\n\tgithub.com/cenkalti/backoff/v4 v4.3.0\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/ergochat/readline v0.1.3\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/godbus/dbus/v5 v5.1.0\n\tgithub.com/gokyle/twofactor v1.0.1\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-github/v61 v61.0.0\n\tgithub.com/gopasspw/clipboard v0.0.4\n\tgithub.com/gopasspw/gitconfig v0.0.4\n\tgithub.com/gopasspw/gopass-hibp v1.15.18\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7\n\tgithub.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51\n\tgithub.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018\n\tgithub.com/makiuchi-d/gozxing v0.1.1\n\tgithub.com/martinhoefling/goxkcdpwgen v0.1.2-0.20231122080842-e51aa57005ca\n\tgithub.com/mattn/go-colorable v0.1.14\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mattn/go-tty v0.0.7\n\tgithub.com/mitchellh/go-ps v1.0.0\n\tgithub.com/muesli/crunchy v0.4.0\n\tgithub.com/noborus/ov v0.45.1\n\tgithub.com/pquerna/otp v1.5.0\n\tgithub.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/twpayne/go-pinentry/v4 v4.0.0\n\tgithub.com/urfave/cli/v2 v2.27.7\n\tgithub.com/xhit/go-str2duration/v2 v2.1.0\n\tgithub.com/zalando/go-keyring v0.2.6\n\tgithub.com/zeebo/blake3 v0.2.4\n\tgolang.org/x/crypto v0.44.0\n\tgolang.org/x/exp v0.0.0-20260209203927-2842357ff358\n\tgolang.org/x/mod v0.33.0\n\tgolang.org/x/net v0.47.0\n\tgolang.org/x/oauth2 v0.33.0\n\tgolang.org/x/sys v0.38.0\n\tgolang.org/x/term v0.37.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tal.essio.dev/pkg/shellescape v1.6.0 // indirect\n\tcodeberg.org/tslocum/cbind v0.1.6 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/boombuler/barcode v1.1.0 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.3.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/creack/pty v1.1.24 // indirect\n\tgithub.com/danieljoos/wincred v1.2.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gdamore/encoding v1.0.1 // indirect\n\tgithub.com/gdamore/tcell/v2 v2.9.0 // indirect\n\tgithub.com/gen2brain/shm v0.1.1 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/jezek/xgb v1.1.1 // indirect\n\tgithub.com/jwalton/gchalk v1.3.0 // indirect\n\tgithub.com/jwalton/go-supportscolor v1.2.0 // indirect\n\tgithub.com/kjk/lzmadec v0.0.0-20210713164611-19ac3ee91a71 // indirect\n\tgithub.com/klauspost/compress v1.18.1 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/noborus/guesswidth v0.4.0 // indirect\n\tgithub.com/noborus/tcellansi v0.2.0 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/text v0.31.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\trsc.io/qr v0.2.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=\nal.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=\nc2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=\nc2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=\ncodeberg.org/tslocum/cbind v0.1.6 h1:RhnKC7tmrCf0ZJBTQ6b1voAFcGqIEjDsKzqlqFWwkV8=\ncodeberg.org/tslocum/cbind v0.1.6/go.mod h1:gfR4e1lfYqC4xlR0N//omQc1JbHx+e1Mk5F8UfotYYc=\nfilippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=\nfilippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=\ngithub.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=\ngithub.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w=\ngithub.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=\ngithub.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=\ngithub.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=\ngithub.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=\ngithub.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b h1:2K3B6Xm7/lnhOugeGB3nIk50bZ9zhuJvXCEfUuL68ik=\ngithub.com/caspr-io/yamlpath v0.0.0-20200722075116-502e8d113a9b/go.mod h1:4rP9T6iHCuPAIDKdNaZfTuuqSIoQQvFctNWIAUI1rlg=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=\ngithub.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=\ngithub.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/ergochat/readline v0.1.3 h1:/DytGTmwdUJcLAe3k3VJgowh5vNnsdifYT6uVaf4pSo=\ngithub.com/ergochat/readline v0.1.3/go.mod h1:o3ux9QLHLm77bq7hDB21UTm6HlV2++IPDMfIfKDuOgY=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=\ngithub.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=\ngithub.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=\ngithub.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=\ngithub.com/gen2brain/shm v0.1.1 h1:1cTVA5qcsUFixnDHl14TmRoxgfWEEZlTezpUj1vm5uQ=\ngithub.com/gen2brain/shm v0.1.1/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gokyle/twofactor v1.0.1 h1:uRhvx0S4Hb82RPIDALnf7QxbmPL49LyyaCkJDpWx+Ek=\ngithub.com/gokyle/twofactor v1.0.1/go.mod h1:4gxzH1eaE/F3Pct/sCDNOylP0ClofUO5j4XZN9tKtLE=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=\ngithub.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/gopasspw/clipboard v0.0.4 h1:v3HUlVHfBXPx9woIQnsBIbs9ZM3i77OCtVKRMLhmR+c=\ngithub.com/gopasspw/clipboard v0.0.4/go.mod h1:i0cShr7JEbOXZ/iKM5RyfBLbu1FPzouO8BTCJy0uHy8=\ngithub.com/gopasspw/gitconfig v0.0.4 h1:7JE0iTm92OdXCtkS33CnbqcAEqQXYWTUriYFf3sRTBk=\ngithub.com/gopasspw/gitconfig v0.0.4/go.mod h1:W5AHsZgCbBRsc8TnElO82GYflOz/l2dIndncymoCv+A=\ngithub.com/gopasspw/gopass-hibp v1.15.18 h1:OoMr1ugY0aso5Qjn3MRPaVJoYGuwdhaebUxKP611Cj0=\ngithub.com/gopasspw/gopass-hibp v1.15.18/go.mod h1:5JOaWMoQbOFTRUdiCOw3JTNPtjbUPWKpw7OEFjQwAdo=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=\ngithub.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=\ngithub.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150 h1:ta6N7DaOQEACq28cLa0iRqXIbchByN9Lfll08CT2GBc=\ngithub.com/jsimonetti/pwscheme v0.0.0-20220922140336-67a4d090f150/go.mod h1:SiNTKDgjKQORnazFVHXhpny7UtU0iJOqtxd7R7sCfDI=\ngithub.com/jwalton/gchalk v1.3.0 h1:uTfAaNexN8r0I9bioRTksuT8VGjrPs9YIXR1PQbtX/Q=\ngithub.com/jwalton/gchalk v1.3.0/go.mod h1:ytRlj60R9f7r53IAElbpq4lVuPOPNg2J4tJcCxtFqr8=\ngithub.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs=\ngithub.com/jwalton/go-supportscolor v1.2.0 h1:g6Ha4u7Vm3LIsQ5wmeBpS4gazu0UP1DRDE8y6bre4H8=\ngithub.com/jwalton/go-supportscolor v1.2.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=\ngithub.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=\ngithub.com/kjk/lzmadec v0.0.0-20210713164611-19ac3ee91a71 h1:TYp9Fj0apeZMWentXRaFM6B0ixdFefrlgY8n8XYEz1s=\ngithub.com/kjk/lzmadec v0.0.0-20210713164611-19ac3ee91a71/go.mod h1:2zRkQCuw/eK6cqkYAeNqyBU7JKa2Gcq40BZ9GSJbmfE=\ngithub.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=\ngithub.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=\ngithub.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=\ngithub.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=\ngithub.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=\ngithub.com/martinhoefling/goxkcdpwgen v0.1.2-0.20231122080842-e51aa57005ca h1:jV6vw7U2RoS1sI7f6f12/wsCwMjADZ/xUxi/lhUqjV8=\ngithub.com/martinhoefling/goxkcdpwgen v0.1.2-0.20231122080842-e51aa57005ca/go.mod h1:IKRlPM0t4ZmK9YZ33QZ2hB1DcSY8WnQedKRDyYeNRp4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=\ngithub.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/muesli/crunchy v0.4.0 h1:qdiml8gywULHBsztiSAf6rrE6EyuNasNKZ104mAaahM=\ngithub.com/muesli/crunchy v0.4.0/go.mod h1:9k4x6xdSbb7WwtAVy0iDjaiDjIk6Wa5AgUIqp+HqOpU=\ngithub.com/noborus/guesswidth v0.4.0 h1:+PPh+Z+GM4mKmVrhYR4lpjeyBuLMSVo2arM+VErdHIc=\ngithub.com/noborus/guesswidth v0.4.0/go.mod h1:ghA6uh9RcK+uSmaDDmBMj/tRZ3BSpspDP6DMF5Xk3bc=\ngithub.com/noborus/ov v0.45.1 h1:wlyehWHzsn/9QcS1Y2E52q+PbcufI98Vn+pJgcF5BQs=\ngithub.com/noborus/ov v0.45.1/go.mod h1:3XW/dJo/FtcMJz6amDMaYR0LmUj/W+uSSjm5NRtsWd4=\ngithub.com/noborus/tcellansi v0.2.0 h1:GoSLIya57T7IiojDyNLps+9SjjCaCuLmvnGqHr3ldYU=\ngithub.com/noborus/tcellansi v0.2.0/go.mod h1:hZhcjTUr0iNe3XmyErkIMW3kBQmKZTngSARCWF+DxT0=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=\ngithub.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e h1:HFUDYOpUVZ0oTXeZy2A59Lkf69SsOF03Lg1GsI3Xh9o=\ngithub.com/schollz/closestmatch v0.0.0-20190308193919-1fbe626be92e/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/twpayne/go-pinentry/v4 v4.0.0 h1:8WcNa+UDVRzz7y9OEEU/nRMX+UGFPCAvl5XsqWRxTY4=\ngithub.com/twpayne/go-pinentry/v4 v4.0.0/go.mod h1:aXvy+awVXqdH+GS0ddQ7AKHZ3tXM6fJ2NK+e16p47PI=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=\ngithub.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=\ngithub.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=\ngithub.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=\ngithub.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=\ngithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=\ngithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=\ngithub.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=\ngithub.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=\ngithub.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=\ngithub.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=\ngithub.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=\ngithub.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=\ngolang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=\ngolang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c=\ngolang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220921155015-db77216a4ee9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=\ngolang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=\ngolang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=\ngolang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=\ngolang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\nrsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=\nrsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=\n"
  },
  {
    "path": "gopass.1",
    "content": "\n.TH GOPASS \"1\" \"December 2025\" \"gopass (github.com/gopasspw/gopass) 1.16.1\" \"User Commands\"\n.SH NAME\ngopass - The standard Unix password manager\n.SH SYNOPSIS\n.B gopass\n[\\fI\\,global options\\/\\fR] \\fI\\,command\\/\\fR [\\fI\\,command options\\/\\fR] [\\fI,arguments\\/\\fR...]\n.SH GLOBAL OPTIONS\n\n.TP\n\\fB\\-\\-alsoclip\\fR,\n\\fB\\-C\\fR,\nCopy the password and show everything\n.TP\n\\fB\\-\\-chars\\fR,\nPrint specific characters from the secret\n.TP\n\\fB\\-\\-clip\\fR,\n\\fB\\-c\\fR,\nCopy the password value into the clipboard\n.TP\n\\fB\\-\\-noparsing\\fR,\n\\fB\\-n\\fR,\nDo not parse the output.\n.TP\n\\fB\\-\\-nosync\\fR,\nDisable auto-sync\n.TP\n\\fB\\-\\-password\\fR,\n\\fB\\-o\\fR,\nDisplay only the password. Takes precedence over all other flags.\n.TP\n\\fB\\-\\-qr\\fR,\nPrint the password as a QR Code\n.TP\n\\fB\\-\\-qrbody\\fR,\nPrint the body as a QR Code\n.TP\n\\fB\\-\\-revision\\fR,\n\\fB\\-r\\fR,\nShow a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry.\n.TP\n\\fB\\-\\-unsafe\\fR,\n\\fB\\-u\\fR,\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nDisplay unsafe content (e.g. the password) even if safecontent is enabled\n.TP\n\\fB\\-\\-yes\\fR,\n\\fB\\-y\\fR,\nAlways answer yes to yes/no questions\n.SH COMMANDS\n\n.SS age\nage commands\n\nBuilt-in commands for the age backend.\nThese allow limited interactions with the gopass specific age identities.\n Added identities are automatically added as recipient to your secrets when encrypting, but not toyour recipients, make sure to keep your recipients and identities in sync as you want to.\nAll age identities, including plugin ones should be supported. We also still support githubidentities despite them being deprecated by age, we do so by falling back to the ssh identitiesfor these and keeping a local cache of ssh keys for a given github identity.\n\n.B Flags\n.TP\n\\fB\\-\\-age-ssh-key-path\\fR,\nCustom path to SSH key or directory for age backend\n.SS alias\nPrint domain aliases\n\nPrint defined domain aliases.\n.SS audit\nDecrypt all secrets and scan for weak or leaked passwords\n\nThis command decrypts all secrets and checks for common flaws and (optionally) against a list of previously leaked passwords.\n\n.B Flags\n.TP\n\\fB\\-\\-format\\fR,\nOutput format. text, csv or html. Default: text\n.TP\n\\fB\\-\\-full\\fR,\nPrint full details of all findings. Default: false\n.TP\n\\fB\\-\\-output-file\\fR,\n\\fB\\-o\\fR,\nOutput filename. Used for csv and html\n.TP\n\\fB\\-\\-summary\\fR,\nPrint a summary of the audit results. Default: true (print summary)\n.TP\n\\fB\\-\\-template\\fR,\nHTML template. If not set use the built-in default.\n.SS cat\nDecode and print content of a binary secret to stdout, or encode and insert from stdin\n\nThis command is similar to the way cat works on the command line. It can either be used to retrieve the decoded content of a secret similar to 'cat file' or vice versa to encode the content from STDIN to a secret.\n.SS clone\nClone a password store from a git repository\n\nThis command clones an existing password store from a git remote to a local password store. Can be either used to initialize a new root store or to add a new mounted sub-store. Needs at least one argument (git URL) to clone from. Accepts a second argument (mount location) to clone and mount a sub-store, e.g. 'gopass clone git@example.com/store.git foo/bar'\n\n.B Flags\n.TP\n\\fB\\-\\-check-keys\\fR,\nCheck for valid decryption keys. Generate new keys if none are found.\n.TP\n\\fB\\-\\-crypto\\fR,\nSelect crypto backend [age gpgcli plain]\n.TP\n\\fB\\-\\-path\\fR,\nPath to clone the repo to\n.TP\n\\fB\\-\\-storage\\fR,\nSelect storage backend [cryptfs fossilfs gitfs jjfs]\n.SS config\nDisplay and edit the configuration file\n\nThis command allows for easy printing and editing of the configuration. Without argument, the entire config is printed. With a single argument, a single key can be printed. With two arguments a setting specified by key can be set to value.\n\n.B Flags\n.TP\n\\fB\\-\\-store\\fR,\nSet options to a specific store\n.SS convert\nConvert a store to different backends\n\nConvert a store to a different set of backends\n\n.B Flags\n.TP\n\\fB\\-\\-crypto\\fR,\nWhich crypto backend? [age gpgcli plain]\n.TP\n\\fB\\-\\-move\\fR,\nReplace store?\n.TP\n\\fB\\-\\-storage\\fR,\nWhich storage backend? [cryptfs fossilfs fs gitfs jjfs]\n.TP\n\\fB\\-\\-store\\fR,\nSpecify which store to convert\n.SS copy\nCopy secrets from one location to another\n\nThis command copies an existing secret in the store to another location. This also works across different sub-stores. If the source is a directory it will automatically copy recursively. In that case, the source directory is re-created at the destination if no trailing slash is found, otherwise the contents are flattened (similar to rsync).\n\n.B Flags\n.TP\n\\fB\\-\\-commit-message\\fR,\n\\fB\\-m\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nForce to copy the secret and overwrite existing one\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.SS create\nEasy creation of new secrets\n\nThis command starts a wizard to aid in creation of new secrets.\n\n.B Flags\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nForce path selection\n.TP\n\\fB\\-\\-store\\fR,\n\\fB\\-s\\fR,\nWhich store to use\n.SS delete\nRemove one or many secrets from the store\n\nThis command removes secrets. It can work recursively on folders. Recursing across stores is purposefully not supported.\n\n.B Flags\n.TP\n\\fB\\-\\-commit-message\\fR,\n\\fB\\-m\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nForce to delete the secret\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.TP\n\\fB\\-\\-recursive\\fR,\n\\fB\\-r\\fR,\nRecursive delete files and folders\n.SS edit\nEdit new or existing secrets\n\nUse this command to insert a new secret or edit an existing one using your $EDITOR. It will attempt to create a secure temporary directory for storing your secret while the editor is accessing it. Please make sure your editor doesn't leak sensitive data to other locations while editing.\nNote: If $EDITOR is not set we will try 'editor'. If that's not available either we fall back to 'vi'. Consider using 'update-alternatives --config editor to change the defaults.\n\n.B Flags\n.TP\n\\fB\\-\\-commit-message\\fR,\n\\fB\\-m\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-create\\fR,\n\\fB\\-c\\fR,\nCreate a new secret if none found\n.TP\n\\fB\\-\\-editor\\fR,\n\\fB\\-e\\fR,\nUse this editor binary\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.SS env\nRun a subprocess with a pre-populated environment\n\nThis command runs a sub process with the environment populated from the keys of a secret.\n\n.B Flags\n.TP\n\\fB\\-\\-keep-case\\fR,\n\\fB\\-\\-kc\\fR,\nDo not capitalize the environment variable and instead retain the original capitalization\n.SS find\nSearch for secrets\n\nThis command will first attempt a simple pattern match on the name of the secret.  If there is an exact match it will be shown directly; if there are multiple matches, a selection will be shown.\n\n.B Flags\n.TP\n\\fB\\-\\-regex\\fR,\n\\fB\\-r\\fR,\nInterpret pattern as regular expression\n.TP\n\\fB\\-\\-unsafe\\fR,\n\\fB\\-u\\fR,\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nIn the case of an exact match, display the password even if safecontent is enabled\n.SS fsck\nCheck store integrity, clean up artifacts and possibly re-write secrets\n\nCheck the integrity of the given sub-store or all stores if none are specified. Will automatically fix all issues found, i.e. it will change permissions, re-write secrets and remove outdated configs.\n\n.B Flags\n.TP\n\\fB\\-\\-decrypt\\fR,\nDecrypt and reencrypt during fsck.\n.TP\n\\fB\\-\\-store\\fR,\nLimit fsck to this mount point\n.SS fscopy\nCopy files from or to the password store\n\nThis command either reads a file from the filesystem and writes the encoded and encrypted version in the store or it decrypts and decodes a secret and writes the result to a file. Either source or destination must be a file and the other one a secret. If you want the source to be securely removed after copying, use 'gopass binary move'\n.SS fsmove\nMove files from or to the password store\n\nThis command either reads a file from the filesystem and writes the encoded and encrypted version in the store or it decrypts and decodes a secret and writes the result to a file. Either source or destination must be a file and the other one a secret. The source will be wiped from disk or from the store after it has been copied successfully and validated. If you don't want the source to be removed use 'gopass binary copy'\n.SS generate\nGenerate a new password\n\nDialog to generate a new password and write it into a new or existing secret. By default, the new password will replace the first line of an existing secret (or create a new one).\n\n.B Flags\n.TP\n\\fB\\-\\-clip\\fR,\n\\fB\\-c\\fR,\nCopy the generated password to the clipboard\n.TP\n\\fB\\-\\-commit-message\\fR,\n\\fB\\-m\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-edit\\fR,\n\\fB\\-e\\fR,\nOpen secret for editing after generating a password\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nForce to overwrite existing password\n.TP\n\\fB\\-\\-force-regen\\fR,\n\\fB\\-t\\fR,\nForce full re-generation, incl. evaluation of templates. Will overwrite the entire secret!\n.TP\n\\fB\\-\\-generator\\fR,\n\\fB\\-g\\fR,\nChoose a password generator, use one of: cryptic, memorable, xkcd or external. Default: cryptic\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.TP\n\\fB\\-\\-lang\\fR,\n\\fB\\-\\-xkcdlang\\fR,\n\\fB\\-\\-xl\\fR,\nLanguage to generate password from, currently only en (english, default) or de are supported\n.TP\n\\fB\\-\\-print\\fR,\n\\fB\\-p\\fR,\nPrint the generated password to the terminal\n.TP\n\\fB\\-\\-sep\\fR,\n\\fB\\-\\-xkcdsep\\fR,\n\\fB\\-\\-xs\\fR,\nWord separator for generated passwords. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised.\n.TP\n\\fB\\-\\-strict\\fR,\nRequire strict character class rules\n.TP\n\\fB\\-\\-symbols\\fR,\n\\fB\\-s\\fR,\nUse symbols in the password\n.SS git\nRun a git command inside a password store: gopass git [--store=<store>] <git-command>\n\nIf the password store is a git repository, execute a git command specified by git-command-args.\n\n.B Flags\n.TP\n\\fB\\-\\-store\\fR,\nStore to operate on\n.SS grep\nSearch for secrets files containing search-string when decrypted.\n\nThis command decrypts all secrets and performs a pattern matching on the content.\n\n.B Flags\n.TP\n\\fB\\-\\-regexp\\fR,\n\\fB\\-r\\fR,\nInterpret pattern as RE2 regular expression\n.SS history\nShow password history\n\nDisplay the change history for a secret\n\n.B Flags\n.TP\n\\fB\\-\\-password\\fR,\n\\fB\\-p\\fR,\nInclude passwords in output\n.SS init\nInitialize new password store.\n\nInitialize new password storage and use gpg-id for encryption.\n\n.B Flags\n.TP\n\\fB\\-\\-crypto\\fR,\nSelect crypto backend [age gpgcli plain]\n.TP\n\\fB\\-\\-path\\fR,\n\\fB\\-p\\fR,\nSet the sub-store path to operate on\n.TP\n\\fB\\-\\-storage\\fR,\nSelect storage backend [cryptfs fossilfs fs gitfs jjfs]\n.TP\n\\fB\\-\\-store\\fR,\n\\fB\\-s\\fR,\nSet the name of the sub-store\n.SS insert\nInsert a new secret\n\nInsert a new secret. Optionally, echo the secret back to the console during entry. Or, optionally, the entry may be multiline. Prompt before overwriting existing secret unless forced.\n\n.B Flags\n.TP\n\\fB\\-\\-append\\fR,\n\\fB\\-a\\fR,\nAppend data read from STDIN to existing data\n.TP\n\\fB\\-\\-commit-message\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-echo\\fR,\n\\fB\\-e\\fR,\nDisplay secret while typing\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nOverwrite any existing secret and do not prompt to confirm recipients\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.TP\n\\fB\\-\\-multiline\\fR,\n\\fB\\-m\\fR,\nInsert using $EDITOR\n.SS link\nCreate a symlink\n\nThis command creates a symlink from one entry in a mounted store to another entry. Important: Does not cross mounts!\n.SS list\nList existing secrets\n\nThis command will list all existing secrets. Provide a folder prefix to list only certain subfolders of the store.\n\n.B Flags\n.TP\n\\fB\\-\\-flat\\fR,\n\\fB\\-f\\fR,\nPrint a flat list\n.TP\n\\fB\\-\\-folders\\fR,\n\\fB\\-d\\fR,\nPrint a flat list of folders\n.TP\n\\fB\\-\\-limit\\fR,\n\\fB\\-l\\fR,\nDisplay no more than this many levels of the tree\n.TP\n\\fB\\-\\-strip-prefix\\fR,\n\\fB\\-s\\fR,\nStrip this prefix from filtered entries\n.SS merge\nMerge multiple secrets into one\n\nThis command implements a merge workflow to help deduplicate secrets. It requires exactly one destination (may already exist) and at least one source (must exist, can be multiple). gopass will then merge all entries into one, drop into an editor, save the result and remove all merged entries.\n\n.B Flags\n.TP\n\\fB\\-\\-delete\\fR,\n\\fB\\-d\\fR,\nRemove merged entries\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nSkip editor, merge entries unattended\n.SS mounts\nEdit mounted stores\n\nThis command displays all mounted password stores. It offers several subcommands to create or remove mounts.\n.SS move\nMove secrets from one location to another\n\nThis command moves a secret from one path to another. This also works across different sub-stores. If the source is a directory, the source directory is re-created at the destination if no trailing slash is found, otherwise the contents are flattened (similar to rsync).\n\n.B Flags\n.TP\n\\fB\\-\\-commit-message\\fR,\n\\fB\\-m\\fR,\nSet the commit message\n.TP\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nForce to move the secret and overwrite existing one\n.TP\n\\fB\\-\\-interactive-commit\\fR,\n\\fB\\-i\\fR,\nOpen an editor for the commit message\n.SS otp\nGenerate time- or hmac-based tokens\n\nTries to parse an OTP URL (otpauth://). URL can be TOTP or HOTP. The URL can be provided on its own line or on a key value line with a key named 'totp'.\n\n.B Flags\n.TP\n\\fB\\-\\-alsoclip\\fR,\n\\fB\\-C\\fR,\nCopy the time-based token and show it\n.TP\n\\fB\\-\\-chained\\fR,\n\\fB\\-p\\fR,\nchain the token to the password\n.TP\n\\fB\\-\\-clip\\fR,\n\\fB\\-c\\fR,\nCopy the time-based token into the clipboard\n.TP\n\\fB\\-\\-password\\fR,\n\\fB\\-o\\fR,\nOnly display the token\n.TP\n\\fB\\-\\-qr\\fR,\n\\fB\\-q\\fR,\nWrite QR code to FILE\n.TP\n\\fB\\-\\-snip\\fR,\n\\fB\\-s\\fR,\nScan screen content to insert a OTP QR code into provided entry\n.SS process\nProcess a template file\n\nThis command processes a template file. It will read the template file and replace all variables with their values.\n.SS pwgen\nGenerate passwords\n\nPrint any number of password to the console. The optional length parameter specifies the length of each password.\n\n.B Flags\n.TP\n\\fB\\-\\-ambiguous\\fR,\n\\fB\\-B\\fR,\nDo not include characters that could be easily confused with each other, like '1' and 'l' or '0' and 'O'\n.TP\n\\fB\\-\\-lang\\fR,\n\\fB\\-\\-xkcdlang\\fR,\n\\fB\\-\\-xl\\fR,\nLanguage to generate password from, currently only en (english, default) or de are supported\n.TP\n\\fB\\-\\-no-capitalize\\fR,\n\\fB\\-A\\fR,\nDo not include capital letter in the generated passwords.\n.TP\n\\fB\\-\\-no-numerals\\fR,\n\\fB\\-0\\fR,\nDo not include numerals in the generated passwords.\n.TP\n\\fB\\-\\-one-per-line\\fR,\n\\fB\\-1\\fR,\nPrint one password per line\n.TP\n\\fB\\-\\-sep\\fR,\n\\fB\\-\\-xkcdsep\\fR,\n\\fB\\-\\-xs\\fR,\nWord separator for generated xkcd style password. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised. This flag implies -xkcd\n.TP\n\\fB\\-\\-symbols\\fR,\n\\fB\\-y\\fR,\nInclude at least one symbol in the password.\n.TP\n\\fB\\-\\-xkcd\\fR,\n\\fB\\-x\\fR,\nUse multiple random english words combined to a password. By default, space is used as separator and all words are lowercase\n.TP\n\\fB\\-\\-xkcdcapitalize\\fR,\n\\fB\\-\\-xc\\fR,\nCapitalize first letter of each word in generated xkcd style password. This flag implies -xkcd\n.TP\n\\fB\\-\\-xkcdnumbers\\fR,\n\\fB\\-\\-xn\\fR,\nAdd a random number to the end of the generated xkcd style password. This flag implies -xkcd\n.SS rcs\nRun a RCS command inside a password store\n\nIf the password store is a git repository, execute a git command specified by git-command-args.\n.SS recipients\nEdit recipient permissions\n\nThis command displays all existing recipients for all mounted stores. The subcommands allow adding or removing recipients.\n\n.B Flags\n.TP\n\\fB\\-\\-pretty\\fR,\nPretty print recipients\n.SS reorg\nReorganize a password store by editing a text file\n\nThis command lists all the secrets in a text file, line by line, and then opens it in an editor. Once the user saves and leaves the editor, gopass will read the temp file, calculate the necessary moves and show a diff and a confirmation prompt. Once the user acknowledges that it will reorganize the secrets and create a meaningful commit message.\n.SS setup\nInitialize a new password store\n\nThis command is automatically invoked if gopass is started without any existing password store. This command exists so users can be provided with simple one-command setup instructions.\n\n.B Flags\n.TP\n\\fB\\-\\-alias\\fR,\nLocal mount point for the given remote\n.TP\n\\fB\\-\\-create\\fR,\nCreate a new team (default: false, i.e. join an existing team)\n.TP\n\\fB\\-\\-crypto\\fR,\nSelect crypto backend [age gpgcli plain]\n.TP\n\\fB\\-\\-email\\fR,\nEMail for unattended GPG key generation\n.TP\n\\fB\\-\\-name\\fR,\nFirstname and Lastname for unattended GPG key generation\n.TP\n\\fB\\-\\-remote\\fR,\nURL to a git remote, will attempt to join this team\n.TP\n\\fB\\-\\-storage\\fR,\nSelect storage backend [cryptfs fossilfs fs gitfs jjfs]\n.SS show\nDisplay the content of a secret\n\nShow an existing secret and optionally put its first line on the clipboard. If put on the clipboard, it will be cleared after 45 seconds.\n\n.B Flags\n.TP\n\\fB\\-\\-alsoclip\\fR,\n\\fB\\-C\\fR,\nCopy the password and show everything\n.TP\n\\fB\\-\\-chars\\fR,\nPrint specific characters from the secret\n.TP\n\\fB\\-\\-clip\\fR,\n\\fB\\-c\\fR,\nCopy the password value into the clipboard\n.TP\n\\fB\\-\\-noparsing\\fR,\n\\fB\\-n\\fR,\nDo not parse the output.\n.TP\n\\fB\\-\\-nosync\\fR,\nDisable auto-sync\n.TP\n\\fB\\-\\-password\\fR,\n\\fB\\-o\\fR,\nDisplay only the password. Takes precedence over all other flags.\n.TP\n\\fB\\-\\-qr\\fR,\nPrint the password as a QR Code\n.TP\n\\fB\\-\\-qrbody\\fR,\nPrint the body as a QR Code\n.TP\n\\fB\\-\\-revision\\fR,\n\\fB\\-r\\fR,\nShow a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry.\n.TP\n\\fB\\-\\-unsafe\\fR,\n\\fB\\-u\\fR,\n\\fB\\-\\-force\\fR,\n\\fB\\-f\\fR,\nDisplay unsafe content (e.g. the password) even if safecontent is enabled\n.TP\n\\fB\\-\\-yes\\fR,\n\\fB\\-y\\fR,\nAlways answer yes to yes/no questions\n.SS sum\nCompute the SHA256 checksum\n\nThis command decodes an Base64 encoded secret and computes the SHA256 checksum over the decoded data. This is useful to verify the integrity of an inserted secret.\n.SS sync\nSync all local stores with their remotes\n\nSync all local stores with their git remotes, if any, and check any possibly affected gpg keys.\n\n.B Flags\n.TP\n\\fB\\-\\-store\\fR,\n\\fB\\-s\\fR,\nSelect the store to sync\n.SS templates\nEdit templates\n\nList existing templates in the password store and allow for editing and creating them.\n.SS unclip\nInternal command to clear clipboard\n\nClear the clipboard if the content matches the checksum.\n\n.B Flags\n.TP\n\\fB\\-\\-force\\fR,\nClear clipboard even if checksum mismatches\n.TP\n\\fB\\-\\-timeout\\fR,\nTime to wait\n.SS update\nCheck for updates\n\nThis command checks for gopass updates at GitHub and automatically downloads and installs any missing update.\n.SS version\nDisplay version\n\nThis command displays version and build time information.\n\n.SH \"REPORTING BUGS\"\nReport bugs to <https://github.com/gopasspw/gopass/issues/new>\n.SH \"COPYRIGHT\"\nCopyright \\(co 2021 Gopass Authors\nThis program is free software; you may redistribute it under the terms of\nthe MIT license. This program has absolutely no warranty.\n"
  },
  {
    "path": "helpers/changelog/main.go",
    "content": "// Copyright 2021 The gopass Authors. All rights reserved.\n// Use of this source code is governed by the MIT license,\n// that can be found in the LICENSE file.\n\n// Changelog implements the changelog extractor that is called by the autorelease GitHub action\n// and used to extract the changelog from the CHANGELOG.md file. It's content is then used to\n// populate the release description on GitHub.\n//\n// This tool will extract every line between the first and the second subheading (##).\n// This way the changelog can have a common header under the top most heading (#) and we\n// still only get the content of the latest release in the GitHub release notes.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar filename = \"CHANGELOG.md\"\n\nfunc main() {\n\tfh, err := os.Open(filename)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer fh.Close()\n\n\ts := bufio.NewScanner(fh)\n\tvar in bool\n\tfor s.Scan() {\n\t\tline := s.Text()\n\t\tif strings.HasPrefix(line, \"## \") {\n\t\t\tif in {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tin = true\n\t\t}\n\n\t\tif !in {\n\t\t\tcontinue\n\t\t}\n\n\t\tfmt.Println(line)\n\t}\n}\n"
  },
  {
    "path": "helpers/changelog/main_test.go",
    "content": "//go:build linux\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestMain(t *testing.T) {\n\t// Create a temporary file\n\ttmpfile, err := os.CreateTemp(\"\", \"changelog_test_*.md\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer os.Remove(tmpfile.Name())\n\n\t// Write test data to the temporary file\n\tcontent := `# Changelog\n## [1.0.1] - 2021-01-01\n### Added\n- New feature\n\n## [1.0.0] - 2020-12-31\n### Added\n- Initial release\n`\n\tif _, err := tmpfile.Write([]byte(content)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := tmpfile.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Override the global variable filename\n\tfilename = tmpfile.Name()\n\n\t// Capture the output\n\told := os.Stdout\n\tr, w, _ := os.Pipe()\n\tos.Stdout = w\n\n\tmain()\n\n\tw.Close()\n\tos.Stdout = old\n\n\tvar output strings.Builder\n\tscanner := bufio.NewScanner(r)\n\tfor scanner.Scan() {\n\t\toutput.WriteString(scanner.Text() + \"\\n\")\n\t}\n\n\texpected := `## [1.0.1] - 2021-01-01\n### Added\n- New feature\n\n`\n\tif output.String() != expected {\n\t\tt.Errorf(\"expected %q, got %q\", expected, output.String())\n\t}\n}\n"
  },
  {
    "path": "helpers/gitutils/gitutils.go",
    "content": "package gitutils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar Verbose = false\n\nfunc InitGitDirWithRemote(t *testing.T, baseDir string) string {\n\tt.Helper()\n\n\tremoteDir := filepath.Join(baseDir, \"remote\")\n\tInitGitBare(t, remoteDir)\n\n\tcmd := exec.Command(\"git\", \"clone\", remoteDir, \"repo\")\n\tcmd.Dir = baseDir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\trequire.NoError(t, cmd.Run())\n\n\tdir := filepath.Join(baseDir, \"repo\")\n\tPopulateGitDir(t, dir)\n\n\treturn dir\n}\n\nfunc initGitWithArgs(t *testing.T, dir string, extraArgs ...string) string {\n\tt.Helper()\n\n\t// make sure the directory exists\n\trequire.NoError(t, os.MkdirAll(dir, 0o755))\n\n\t// git init -b master\n\targs := []string{\n\t\t\"init\",\n\t\t\"-b\",\n\t\t\"master\",\n\t}\n\targs = append(args, extraArgs...)\n\tcmd := exec.Command(\"git\", args...)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\trequire.NoError(t, cmd.Run())\n\n\treturn dir\n}\n\nfunc InitGitDir(t *testing.T, dir string) string {\n\tt.Helper()\n\n\tdir = initGitWithArgs(t, dir)\n\tPopulateGitDir(t, dir)\n\n\treturn dir\n}\n\nfunc InitGitBare(t *testing.T, dir string) string {\n\tt.Helper()\n\n\treturn initGitWithArgs(t, dir, \"--bare\")\n}\n\nfunc PopulateGitDir(t *testing.T, dir string) {\n\tt.Helper()\n\t// Create a file in the repo so we have something to commit and create a root commit from.\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"README.md\"), []byte(\"test content\"), 0o644))\n\n\t// Add the file to the index.\n\tcmd := exec.Command(\"git\", \"add\", \"README.md\")\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\trequire.NoError(t, cmd.Run())\n\n\t// Commit the file.\n\tcmd = exec.Command(\"git\", \"commit\", \"-m\", \"Initial commit\")\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\trequire.NoError(t, cmd.Run())\n}\n\nfunc IsGitClean(dir string) bool {\n\tif sv := os.Getenv(\"GOPASS_FORCE_CLEAN\"); sv != \"\" {\n\t\treturn true\n\t}\n\n\tcmd := exec.Command(\"git\", \"diff\", \"--stat\")\n\tcmd.Dir = dir\n\tbuf, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif strings.TrimSpace(string(buf)) != \"\" {\n\t\tfmt.Printf(\"❌ Git in %s is not clean: %q\\n\", dir, string(buf))\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc GitCoMaster(dir string) error {\n\tcmd := exec.Command(\"git\", \"checkout\", \"master\")\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run()\n}\n\nfunc GitCoBranch(dir, branch string) error {\n\tcmd := exec.Command(\"git\", \"checkout\", \"-b\", branch)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run()\n}\n\nfunc GitDelBranch(dir, branch string) error {\n\tcmd := exec.Command(\"git\", \"branch\", \"-D\", branch)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run()\n}\n\nfunc GitPom(dir string) error {\n\tcmd := exec.Command(\"git\", \"pull\", \"origin\", \"master\")\n\tcmd.Dir = dir\n\t// hide long pull output unless an error occurs\n\tbuf := &bytes.Buffer{}\n\tcmd.Stdout = buf\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Println(buf.String())\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc GitAdd(dir string, files ...string) error {\n\targs := []string{\"add\"}\n\targs = append(args, files...)\n\n\tcmd := exec.Command(\"git\", args...)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run()\n}\n\nfunc GitCommitAndPush(dir, tag string) error {\n\tcmd := exec.Command(\"git\", \"commit\", \"-a\", \"-s\", \"-m\", \"Update to \"+tag)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes: %w\", err)\n\t}\n\n\tcmd = exec.Command(\"git\", \"push\", \"origin\", \"master\")\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to push changes: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GitCommit(dir, commitMsg string, files ...string) error {\n\targs := []string{\"add\"}\n\targs = append(args, files...)\n\n\tcmd := exec.Command(\"git\", args...)\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\tcmd.Dir = dir\n\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\tif err := cmd.Run(); err != nil {\n\t\treturn err\n\t}\n\n\tcmd = exec.Command(\"git\", \"commit\", \"-s\", \"-m\", commitMsg)\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\tcmd.Dir = dir\n\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\n\treturn cmd.Run()\n}\n\nfunc GitPush(remote, branch string) error {\n\tif remote == \"\" {\n\t\tremote = \"origin\"\n\t}\n\tif branch == \"\" {\n\t\tbranch = \"master\"\n\t}\n\n\tcmd := exec.Command(\"git\", \"push\", remote, branch)\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run()\n}\n\nfunc GitTagAndPush(dir string, tag string) error {\n\tcmd := exec.Command(\"git\", \"tag\", \"-m\", \"'Tag \"+tag+\"'\", tag)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes: %w\", err)\n\t}\n\n\tcmd = exec.Command(\"git\", \"push\", \"origin\", tag)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to push changes: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GitHasTag(dir string, tag string) bool {\n\tcmd := exec.Command(\"git\", \"rev-parse\", tag)\n\tcmd.Dir = dir\n\tcmd.Stderr = os.Stderr\n\n\tif Verbose {\n\t\tcmd.Stdout = os.Stdout\n\t\tfmt.Printf(\"Running command: %s\\n\", cmd)\n\t}\n\n\treturn cmd.Run() == nil\n}\n"
  },
  {
    "path": "helpers/man/main.go",
    "content": "// Copyright 2021 The gopass Authors. All rights reserved.\n// Use of this source code is governed by the MIT license,\n// that can be found in the LICENSE file.\n\n// Man implements a man(1) documentation generator that is run as part of the\n// release helper to generate an up to date manpage for Gopass.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\tap \"github.com/gopasspw/gopass/internal/action\"\n\t\"github.com/gopasspw/gopass/internal/action/pwgen\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar (\n\tfilename           = \"VERSION\"\n\tstdout   io.Writer = os.Stdout\n)\n\nfunc main() {\n\t// this is a workaround for the man helper getting accidentally\n\t// installed into my $GOBIN dir and me not being able to figure out\n\t// why. So instead of being greeted with an ugly panic message\n\t// every now and then when I need to open a man page I decided\n\t// to rather have a little bit of code to automate this away.\n\tif len(os.Args) > 0 && os.Args[0] == \"man\" {\n\t\tmanPath, err := lookPath(\"man\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tcmd := exec.Command(manPath, os.Args[1:]...)\n\t\tcmd.Stderr = os.Stderr\n\t\tcmd.Stdin = os.Stdin\n\t\tcmd.Stdout = os.Stdout\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tos.Exit(cmd.ProcessState.ExitCode())\n\t\t}\n\n\t\treturn\n\t}\n\n\tvs, err := os.ReadFile(filename)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion := semver.MustParse(strings.TrimSpace(string(vs)))\n\n\taction, err := ap.New(config.New(), version)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcmds := action.GetCommands()\n\tcmds = append(cmds, pwgen.GetCommands()...)\n\tsort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })\n\n\tdata := &payload{\n\t\tSectionNumber: 1,\n\t\tDatePretty:    time.Now().UTC().Format(\"January 2006\"),\n\t\tVersion:       version.String(),\n\t\tSectionName:   \"User Commands\",\n\t\tCommands:      cmds,\n\t\tFlags:         getFlags(ap.ShowFlags()),\n\t}\n\tfuncMap := template.FuncMap{\n\t\t\"flags\": getFlags,\n\t}\n\tif err := template.Must(template.New(\"man\").Funcs(funcMap).Parse(manTpl)).Execute(stdout, data); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc getFlags(flags []cli.Flag) []flag {\n\tsort.Slice(flags, func(i, j int) bool { return flags[i].Names()[0] < flags[j].Names()[0] })\n\n\tout := make([]flag, 0, len(flags))\n\tfor _, f := range flags {\n\t\tswitch v := f.(type) {\n\t\tcase *cli.BoolFlag:\n\t\t\tout = append(out, flag{\n\t\t\t\tName:        v.Name,\n\t\t\t\tAliases:     append([]string{v.Name}, v.Aliases...),\n\t\t\t\tDescription: v.Usage,\n\t\t\t})\n\t\tcase *cli.IntFlag:\n\t\t\tout = append(out, flag{\n\t\t\t\tName:        v.Name,\n\t\t\t\tAliases:     append([]string{v.Name}, v.Aliases...),\n\t\t\t\tDescription: v.Usage,\n\t\t\t})\n\t\tcase *cli.StringFlag:\n\t\t\tout = append(out, flag{\n\t\t\t\tName:        v.Name,\n\t\t\t\tAliases:     append([]string{v.Name}, v.Aliases...),\n\t\t\t\tDescription: v.Usage,\n\t\t\t})\n\t\t}\n\t}\n\treturn out\n}\n\ntype flag struct {\n\tName        string\n\tAliases     []string\n\tDescription string\n}\n\ntype payload struct {\n\tSectionNumber int    // 1\n\tDatePretty    string // July 2020\n\tVersion       string // 1.12.1\n\tSectionName   string // User Commands\n\tCommands      []*cli.Command\n\tFlags         []flag\n}\n\n// from https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/os/exec/lp_unix.go\nfunc lookPath(file string) (string, error) {\n\tcurPath, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpath := os.Getenv(\"PATH\")\n\tfor _, dir := range filepath.SplitList(path) {\n\t\tif dir == \"\" {\n\t\t\t// Unix shell semantics: path element \"\" means \".\"\n\t\t\tdir = \".\"\n\t\t}\n\t\tpath := filepath.Join(dir, file)\n\t\t// do not call ourselves\n\t\tif path == curPath {\n\t\t\tcontinue\n\t\t}\n\t\tif err := findExecutable(path); err == nil {\n\t\t\treturn path, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"%s: executable file not found in $PATH\", file)\n}\n\nfunc findExecutable(file string) error {\n\td, err := os.Stat(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m := d.Mode(); !m.IsDir() && m&0o111 != 0 {\n\t\treturn nil\n\t}\n\treturn fs.ErrPermission\n}\n\nvar manTpl = `\n.TH GOPASS \"{{ .SectionNumber }}\" \"{{ .DatePretty }}\" \"gopass (github.com/gopasspw/gopass) {{ .Version }}\" \"{{ .SectionName }}\"\n.SH NAME\ngopass - The standard Unix password manager\n.SH SYNOPSIS\n.B gopass\n[\\fI\\,global options\\/\\fR] \\fI\\,command\\/\\fR [\\fI\\,command options\\/\\fR] [\\fI,arguments\\/\\fR...]\n.SH GLOBAL OPTIONS\n{{ range $flag := .Flags }}\n.TP{{ range $alias := $flag.Aliases }}\n\\fB{{ if (gt (len $alias) 1) }}\\-{{ end }}\\-{{ $alias }}\\fR,{{ end }}\n{{ $flag.Description }}{{ end }}\n.SH COMMANDS\n{{ range $cmd := .Commands }}\n.SS {{ $cmd.Name }}\n{{ $cmd.Usage }}\n\n{{ $cmd.Description }}\n{{- if $cmd.Flags }}\n\n.B Flags\n{{- range $flag := $cmd.Flags | flags }}\n.TP{{ range $alias := $flag.Aliases }}\n\\fB{{ if (gt (len $alias) 1) }}\\-{{ end }}\\-{{ $alias }}\\fR,{{ end }}\n{{ $flag.Description }}{{ end }}\n{{- end }}\n{{- end}}\n\n.SH \"REPORTING BUGS\"\nReport bugs to <https://github.com/gopasspw/gopass/issues/new>\n.SH \"COPYRIGHT\"\nCopyright \\(co 2021 Gopass Authors\nThis program is free software; you may redistribute it under the terms of\nthe MIT license. This program has absolutely no warranty.\n`\n"
  },
  {
    "path": "helpers/man/main_test.go",
    "content": "//go:build linux\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestMain(t *testing.T) {\n\t// Setup temporary directory for testing\n\ttempDir := t.TempDir()\n\tfilename = filepath.Join(tempDir, \"VERSION\")\n\terr := os.WriteFile(filename, []byte(\"1.0.0\"), 0o644)\n\tassert.NoError(t, err)\n\n\t// Test man-page generation\n\tbuf := &bytes.Buffer{}\n\tstdout = buf\n\tdefer func() { stdout = os.Stderr }()\n\n\tmain()\n\n\tassert.Contains(t, buf.String(), \"1.0.0\")\n\tassert.Contains(t, buf.String(), \"gopass\")\n\t// TODO: Validate man format.\n}\n\nfunc TestGetFlags(t *testing.T) {\n\tflags := []cli.Flag{\n\t\t&cli.BoolFlag{Name: \"boolFlag\", Usage: \"A boolean flag\"},\n\t\t&cli.IntFlag{Name: \"intFlag\", Usage: \"An integer flag\"},\n\t\t&cli.StringFlag{Name: \"stringFlag\", Usage: \"A string flag\"},\n\t}\n\n\texpected := []flag{\n\t\t{Name: \"boolFlag\", Aliases: []string{\"boolFlag\"}, Description: \"A boolean flag\"},\n\t\t{Name: \"intFlag\", Aliases: []string{\"intFlag\"}, Description: \"An integer flag\"},\n\t\t{Name: \"stringFlag\", Aliases: []string{\"stringFlag\"}, Description: \"A string flag\"},\n\t}\n\n\tresult := getFlags(flags)\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestLookPath(t *testing.T) {\n\t// Test finding an executable in PATH\n\tpath, err := lookPath(\"ls\")\n\tassert.NoError(t, err)\n\tassert.NotEmpty(t, path)\n\n\t// Test not finding an executable\n\t_, err = lookPath(\"nonexistent\")\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "helpers/modinfo/main.go",
    "content": "// modinfo a small helper to print the build info and module versions.\n//\n// Test builds don't have build info, so this will only work in a real build.\npackage main\n\nimport (\n\t\"fmt\"\n\trd \"runtime/debug\"\n\n\t_ \"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nfunc main() {\n\tinfo, ok := rd.ReadBuildInfo()\n\tif !ok {\n\t\tpanic(\"could not read build info\")\n\t}\n\n\tfmt.Printf(\"Build Info: %+v\\n\", info)\n\n\tfor _, v := range []string{\n\t\t\"github.com/blang/semver/v4\",\n\t\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\",\n\t} {\n\t\tmv := debug.ModuleVersion(v)\n\t\tfmt.Printf(\"Module Version: %s %s\\n\", v, mv)\n\t}\n}\n"
  },
  {
    "path": "helpers/msipkg/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\nvar (\n\twixTpl = `<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n  <Product Id=\"*\" Name=\"gopass\" UpgradeCode=\"{{ .UpgradeCode }}\" Language=\"1033\" Codepage=\"1252\" Version=\"{{ .Version }}\" Manufacturer=\"gopass\">\n    <Property Id=\"PREVIOUSVERSIONSINSTALLED\" Secure=\"yes\"/>\n    <Upgrade Id=\"{{ .UpgradeCode }}\">\n      <UpgradeVersion Minimum=\"0.0.0\" Property=\"PREVIOUSVERSIONSINSTALLED\" IncludeMinimum=\"yes\" IncludeMaximum=\"no\"/>\n    </Upgrade>\n    <InstallExecuteSequence>\n      <RemoveExistingProducts Before=\"InstallInitialize\"/>\n    </InstallExecuteSequence>\n    <Package InstallerVersion=\"200\" Compressed=\"yes\" Comments=\"Windows Installer Package\" InstallScope=\"perUser\"/>\n    <Media Id=\"1\" Cabinet=\"app.cab\" EmbedCab=\"yes\"/>\n    <Icon Id=\"icon.ico\" SourceFile=\"{{ .Icon }}\"/>\n    <Property Id=\"ARPPRODUCTICON\" Value=\"icon.ico\"/>\n    <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n      <Directory Id=\"LocalAppDataFolder\">\n        <Directory Id=\"INSTALLDIR\" Name=\"gopass\">\n          <Component Id=\"gopass.exe\" Guid=\"*\">\n            <File Id=\"gopass.exe\" Source=\"{{ .Binary }}\" Name=\"gopass.exe\"/>\n            <Shortcut Id=\"StartMenuShortcut\" Advertise=\"no\" Icon=\"icon.ico\" Name=\"gopass\" Directory=\"ProgramMenuFolder\" WorkingDirectory=\"INSTALLDIR\" Description=\"\"/>\n            <Shortcut Id=\"DesktopShortcut\" Advertise=\"no\" Icon=\"icon.ico\" Name=\"gopass\" Directory=\"DesktopFolder\" WorkingDirectory=\"INSTALLDIR\" Description=\"\"/>\n          </Component>\n        </Directory>\n      </Directory>\n    </Directory>\n    <Feature Id=\"App\" Level=\"1\">\n      <ComponentRef Id=\"gopass.exe\"/>\n    </Feature>\n  </Product>\n</Wix>\n`\n\tupgradeCode = \"6c1bd458-7d1b-4311-848d-d0fe1a65af66\"\n\ticon        = \"docs/logo.ico\"\n\tsource      = \"dist/gopass_windows_amd64_v1/gopass.exe\"\n)\n\nconst logo = `\n   __     _    _ _      _ _   ___   ___\n /'_ '\\ /'_'\\ ( '_'\\  /'_' )/',__)/',__)\n( (_) |( (_) )| (_) )( (_| |\\__, \\\\__, \\\n'\\__  |'\\___/'| ,__/''\\__,_)(____/(____/\n( )_) |       | |\n \\___/'       (_)\n`\n\nfunc main() {\n\t// render template to temp dir\n\t// run: wixl /tmp/file.xml -o gopass-ARCH.msi --arch x86|x64\n\tctx := context.Background()\n\n\tfmt.Print(logo)\n\tfmt.Println()\n\tfmt.Println(\"🌟 Creating gopass Windows MSI package.\")\n\n\tcurVer, err := versionFile()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println()\n\tfmt.Printf(\"✅ Current version is: %s\\n\", curVer.String())\n\n\ttd, err := os.MkdirTemp(\"\", \"gopass-\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer os.RemoveAll(td)\n\n\ttmpl, err := template.New(\"wix\").Parse(wixTpl)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\twCfg := filepath.Join(td, \"wix.xml\")\n\tfh, err := os.Create(wCfg)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := tmpl.Execute(fh, struct {\n\t\tUpgradeCode string\n\t\tVersion     string\n\t\tIcon        string\n\t\tBinary      string\n\t}{\n\t\tUpgradeCode: upgradeCode,\n\t\tVersion:     curVer.String(),\n\t\tIcon:        icon,\n\t\tBinary:      source,\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\tfh.Close()\n\n\tfmt.Printf(\"✅ Wrote Wix XML config to: %s\\n\", wCfg)\n\n\tmsiPkg := fmt.Sprintf(\"dist/gopass-%s-windows-%s.msi\", \"x64\", curVer.String())\n\tcmd := exec.CommandContext(ctx, \"wixl\", wCfg, \"-o\", msiPkg, \"--arch\", \"x64\")\n\tbuf := &bytes.Buffer{}\n\tcmd.Stdout = buf\n\tcmd.Stderr = buf\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Printf(\"wixl failed: %s\", buf.String())\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"✅ Created MSI package at: %s\\n\", msiPkg)\n\tfmt.Println()\n}\n\nfunc versionFile() (semver.Version, error) {\n\tbuf, err := os.ReadFile(\"VERSION\")\n\tif err != nil {\n\t\treturn semver.Version{}, err\n\t}\n\n\treturn semver.Parse(strings.TrimSpace(string(buf)))\n}\n"
  },
  {
    "path": "helpers/postrel/main.go",
    "content": "// Copyright 2021 The gopass Authors. All rights reserved.\n// Use of this source code is governed by the MIT license,\n// that can be found in the LICENSE file.\n\n// Postrel is a helper that's supposed to be run after a release has been completed.\n// It will update the gopasspw.github.io website and create a new GitHub milestone.\n// Since it depends on the artifacts generated by the autorelease GitHub action we\n// can't run it as part of the release helper.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/google/go-github/v61/github\"\n\t\"github.com/gopasspw/gopass/helpers/gitutils\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"golang.org/x/mod/modfile\"\n\t\"golang.org/x/oauth2\"\n)\n\nvar verTmpl = `package main\n\nimport (\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\nfunc getVersion() semver.Version {\n\tsv, err := semver.Parse(strings.TrimPrefix(version, \"v\"))\n\tif err == nil {\n\t\treturn sv\n\t}\n\n\treturn semver.Version{\n\t\tMajor: {{ .Major }},\n\t\tMinor: {{ .Minor }},\n\t\tPatch: {{ .Patch }},\n\t\tPre: []semver.PRVersion{\n\t\t\t{VersionStr: \"git\"},\n\t\t},\n\t\tBuild: []string{\"HEAD\"},\n\t}\n}\n`\n\nconst logo = `\n   __     _    _ _      _ _   ___   ___\n /'_ '\\ /'_'\\ ( '_'\\  /'_' )/',__)/',__)\n( (_) |( (_) )| (_) )( (_| |\\__, \\\\__, \\\n'\\__  |'\\___/'| ,__/''\\__,_)(____/(____/\n( )_) |       | |\n \\___/'       (_)\n`\n\nfunc main() {\n\tgitutils.Verbose = true\n\tctx := context.Background()\n\n\tfmt.Print(logo)\n\tfmt.Println()\n\tfmt.Println(\"🌟 Performing post-release cleanup.\")\n\n\tcurVer, err := versionFile()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tnextVer := curVer\n\tnextVer.IncrementPatch()\n\n\thtmlDir := \"../gopasspw.github.io\"\n\tif h := os.Getenv(\"GOPASS_HTMLDIR\"); h != \"\" {\n\t\thtmlDir = h\n\t}\n\n\tmustCheckEnv()\n\n\tghCl, err := newGHClient(ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println()\n\tfmt.Printf(\"✅ Current version is: %s\\n\", curVer.String())\n\tfmt.Printf(\"✅ New version milestone will be: %s\\n\", nextVer.String())\n\tfmt.Printf(\"✅ Expecting HTML in: %s\\n\", htmlDir)\n\tfmt.Println()\n\tfmt.Println(\"❓ Do you want to continue? (press any key to continue or Ctrl+C to abort)\")\n\tfmt.Scanln()\n\n\t// create a new GitHub milestone\n\tfmt.Println(\"☝  Creating new GitHub Milestone(s) ...\")\n\tif err := ghCl.createMilestones(ctx, nextVer); err != nil {\n\t\tfmt.Printf(\"Failed to create GitHub milestones: %s\\n\", err)\n\t}\n\n\t// update gopass integrations\n\tui, err := newIntegrationsUpdater(ghCl.client, curVer)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to create integrations updater: %s\\n\", err)\n\t} else {\n\t\tui.update(ctx)\n\t}\n\n\tfmt.Println(\"💎🙌 Done 🚀🚀🚀🚀🚀🚀\")\n}\n\nfunc mustCheckEnv() {\n\twant := []string{\"GITHUB_TOKEN\", \"GITHUB_USER\", \"GITHUB_FORK\"}\n\tfor _, e := range want {\n\t\tif sv := os.Getenv(e); sv == \"\" {\n\t\t\tpanic(\"Please set: \" + fmt.Sprintf(\"%v\", want))\n\t\t}\n\t}\n}\n\ntype ghClient struct {\n\tclient *github.Client\n\torg    string\n\trepo   string\n}\n\nfunc newGHClient(ctx context.Context) (*ghClient, error) {\n\tpat := os.Getenv(\"GITHUB_TOKEN\")\n\tif pat == \"\" {\n\t\treturn nil, fmt.Errorf(\"❌ Please set GITHUB_TOKEN\")\n\t}\n\n\tts := oauth2.StaticTokenSource(\n\t\t&oauth2.Token{AccessToken: pat},\n\t)\n\ttc := oauth2.NewClient(ctx, ts)\n\tclient := github.NewClient(tc)\n\n\treturn &ghClient{\n\t\tclient: client,\n\t\torg:    \"gopasspw\",\n\t\trepo:   \"gopass\",\n\t}, nil\n}\n\nfunc (g *ghClient) createMilestones(ctx context.Context, v semver.Version) error {\n\tms, _, err := g.client.Issues.ListMilestones(ctx, g.org, g.repo, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// create a milestone for the next patch version\n\tif err := g.createMilestone(ctx, v.String(), 1, ms); err != nil {\n\t\treturn err\n\t}\n\n\t// create a milestone for the next+1 patch version\n\tv.IncrementPatch()\n\tif err := g.createMilestone(ctx, v.String(), 2, ms); err != nil {\n\t\treturn err\n\t}\n\n\t// create a milestone for the next minor version\n\tv.IncrementMinor()\n\tv.Patch = 0\n\n\treturn g.createMilestone(ctx, v.String(), 90, ms)\n}\n\nfunc (g *ghClient) createMilestone(ctx context.Context, title string, offset int, ms []*github.Milestone) error {\n\tfor _, m := range ms {\n\t\tif *m.Title == title {\n\t\t\tfmt.Printf(\"❌ Milestone %s exists\\n\", title)\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t_, _, err := g.client.Issues.CreateMilestone(ctx, g.org, g.repo, &github.Milestone{\n\t\tTitle: &title,\n\t\tDueOn: &github.Timestamp{Time: time.Now().Add(time.Duration(offset) * 30 * 24 * time.Hour)},\n\t})\n\tif err == nil {\n\t\tfmt.Printf(\"✅ Milestone %s created\\n\", title)\n\t}\n\n\treturn err\n}\n\nfunc runCmd(dir string, args ...string) error {\n\tcmd := exec.Command(args[0], args[1:]...)\n\tcmd.Dir = dir\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Run()\n}\n\nfunc versionFile() (semver.Version, error) {\n\tbuf, err := os.ReadFile(\"VERSION\")\n\tif err != nil {\n\t\treturn semver.Version{}, err\n\t}\n\n\treturn semver.Parse(strings.TrimSpace(string(buf)))\n}\n\nfunc goVersion(v string) string {\n\tv = strings.TrimPrefix(v, \"go\")\n\tsv, err := semver.ParseTolerant(v)\n\tif err != nil {\n\t\t// if we can't parse the version, we assume it's a dev version\n\t\t// and return the current major.minor version\n\t\tif len(v) > 3 {\n\t\t\tsv, err = semver.ParseTolerant(v[:3])\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fmt.Sprintf(\"%d.%d\", sv.Major, sv.Minor)\n}\n\ntype inUpdater struct {\n\tgithub *github.Client\n\tv      semver.Version\n\tgoVer  string // go version as major.minor (for use in go.mod and GH workflows)\n}\n\nfunc newIntegrationsUpdater(client *github.Client, v semver.Version) (*inUpdater, error) {\n\tbuf, err := os.ReadFile(\"go.mod\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmodfile, err := modfile.Parse(\"go.mod\", buf, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &inUpdater{\n\t\tgithub: client,\n\t\tv:      v,\n\t\tgoVer:  goVersion(modfile.Go.Version),\n\t}, nil\n}\n\nfunc (u *inUpdater) update(ctx context.Context) {\n\tfor _, upd := range []string{\n\t\t\"git-credential-gopass\",\n\t\t\"gopass-hibp\",\n\t\t\"gopass-jsonapi\",\n\t\t\"gopass-summon-provider\",\n\t} {\n\t\tfmt.Println()\n\t\tfmt.Println(\"------------------------------\")\n\t\tfmt.Println()\n\t\tfmt.Printf(\"🌟 Updating: %s ...\\n\", upd)\n\t\tfmt.Println()\n\t\tif err := u.doUpdate(ctx, upd); err != nil {\n\t\t\tfmt.Printf(\"❌ Updating %s failed: %s\\n\", upd, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tfmt.Printf(\"✅ Integration %s is up to date.\\n\", upd)\n\t}\n}\n\nfunc (u *inUpdater) doUpdate(ctx context.Context, dir string) error {\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath := filepath.Join(filepath.Dir(cwd), dir)\n\n\ttag := fmt.Sprintf(\"v%s\", u.v.String())\n\t// check if the release is already tagged\n\tif gitutils.GitHasTag(path, tag) {\n\t\tfmt.Printf(\"✅ Integration %s has tag %s already.\\n\", dir, tag)\n\n\t\treturn nil\n\t}\n\tfmt.Printf(\"✅ [%s] %s is not tagged, yet.\\n\", dir, tag)\n\n\t// make sure we're at head\n\tif !gitutils.IsGitClean(path) {\n\t\treturn fmt.Errorf(\"git not clean at %s\", path)\n\t}\n\tfmt.Printf(\"✅ [%s] Git is clean.\", dir)\n\n\t// git pull origin master\n\tif err := gitutils.GitPom(path); err != nil {\n\t\treturn fmt.Errorf(\"failed to fetch changes at %s: %s\", path, err)\n\t}\n\n\t// make upgrade\n\tif err := runCmd(path, \"make\", \"upgrade\"); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] make upgrade.\\n\", dir)\n\n\t// go get github.com/gopasspw/gopass@tag\n\tif err := runCmd(path, \"go\", \"get\", \"github.com/gopasspw/gopass@\"+tag); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] updated gopass dependency.\\n\", dir)\n\n\t// sync .golangci.yml ?\n\tif err := fsutil.CopyFile(filepath.Join(cwd, \".golangci.yml\"), filepath.Join(path, \".golangci.yml\")); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] synced .golangci.yml.\\n\", dir)\n\n\t// update go.mod\n\tif err := runCmd(path, \"go\", \"mod\", \"edit\", \"-go=\"+u.goVer); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] updated Go version in go.mod to %s.\\n\", dir, u.goVer)\n\n\t// go mod tidy\n\tif err := runCmd(path, \"go\", \"mod\", \"tidy\"); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] go mod tidy.\\n\", dir)\n\n\t// update workflows\n\tif err := u.updateWorkflows(ctx, path); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] updated workflows.\\n\", dir)\n\n\t// update VERSION\n\tif err := os.WriteFile(filepath.Join(path, \"VERSION\"), []byte(u.v.String()+\"\\n\"), 0o644); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] wrote VERSION.\\n\", dir)\n\n\t// update version.go\n\tif err := u.writeVersionGo(path); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] wrote version.go.\\n\", dir)\n\n\t// update CHANGELOG.md\n\tif err := u.updateChangelog(ctx, path); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] wrote CHANGELOG.md.\\n\", dir)\n\n\t// git commit\n\tif err := gitutils.GitCommitAndPush(path, tag); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] committed.\\n\", dir)\n\n\t// git tag v\n\tif err := gitutils.GitTagAndPush(path, tag); err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"✅ [%s] tagged.\\n\", dir)\n\n\treturn nil\n}\n\nfunc (u *inUpdater) updateWorkflows(ctx context.Context, dir string) error {\n\tfilepath.Walk(filepath.Join(dir, \".github\", \"workflows\"), func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Failed to walk %s: %s\\n\", path, err)\n\n\t\t\treturn nil\n\t\t}\n\t\tif info.IsDir() {\n\t\t\t// fmt.Printf(\"Skipping dir %s\\n\", path)\n\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasSuffix(path, \".yml\") {\n\t\t\t// fmt.Printf(\"Skipping file %s\\n\", path)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn u.updateWorkflow(ctx, path)\n\t})\n\n\treturn nil\n}\n\nvar goVersionRE = regexp.MustCompile(`go-version:\\s+\\d+\\.\\d+`)\n\nfunc (u *inUpdater) updateWorkflow(_ context.Context, path string) error {\n\tbuf, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstr := goVersionRE.ReplaceAllString(string(buf), \"go-version: \"+u.goVer)\n\t// no change, no write\n\tif str == string(buf) {\n\t\t// fmt.Printf(\"No changes in %s\\n\", path)\n\n\t\treturn nil\n\t}\n\n\tfmt.Printf(\"Wrote %s\\n\", path)\n\n\treturn os.WriteFile(path, []byte(str), 0o644)\n}\n\ntype tplPayload struct {\n\tMajor uint64\n\tMinor uint64\n\tPatch uint64\n}\n\nfunc (u *inUpdater) writeVersionGo(path string) error {\n\ttmpl, err := template.New(\"version\").Parse(verTmpl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfn := filepath.Join(path, \"version.go\")\n\tfh, err := os.Create(fn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close()\n\n\treturn tmpl.Execute(fh, tplPayload{\n\t\tMajor: u.v.Major,\n\t\tMinor: u.v.Minor,\n\t\tPatch: u.v.Patch,\n\t})\n}\n\nfunc (u *inUpdater) updateChangelog(_ context.Context, dir string) error {\n\tfn := filepath.Join(dir, \"CHANGELOG.md\")\n\n\tbuf, err := os.ReadFile(fn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"## %s\\n\", u.v.String())\n\tfmt.Fprintln(&sb)\n\tfmt.Fprintf(&sb, \"- Bump dependencies to gopass release v%s\\n\", u.v.String())\n\tfmt.Fprintln(&sb)\n\n\t_, err = sb.Write(buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := os.WriteFile(fn, []byte(sb.String()), 0o644); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc checksum(url string) (string, string, error) {\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\ts2 := sha256.New()\n\ts5 := sha512.New()\n\tw := io.MultiWriter(s2, s5)\n\n\t_, err = io.Copy(w, resp.Body)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn fmt.Sprintf(\"%x\", s2.Sum(nil)), fmt.Sprintf(\"%x\", s5.Sum(nil)), nil\n}\n\nfunc updateBuild(path string, m map[string]*string) error {\n\tfin, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fin.Close()\n\n\tnpath := path + \".new\"\n\tfout, err := os.Create(npath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fout.Close()\n\n\ts := bufio.NewScanner(fin)\nSCAN:\n\tfor s.Scan() {\n\t\tline := s.Text()\n\t\tfor match, repl := range m {\n\t\t\tif strings.HasPrefix(line, match) {\n\t\t\t\tif repl != nil {\n\t\t\t\t\tfmt.Fprintln(fout, *repl)\n\t\t\t\t}\n\n\t\t\t\tcontinue SCAN\n\t\t\t}\n\t\t}\n\t\tfmt.Fprintln(fout, line)\n\t}\n\n\treturn os.Rename(npath, path)\n}\n\ntype repo struct {\n\tver semver.Version // gopass version\n\turl string         // gopass download url\n\tdir string         // repo dir\n\tmsg string\n\trem string // remote\n}\n\nfunc (r *repo) branch() string {\n\treturn fmt.Sprintf(\"gopass-%s\", r.ver.String())\n}\n\nfunc (r *repo) commitMsg() string {\n\tif r.msg != \"\" {\n\t\treturn r.msg\n\t}\n\n\treturn \"gopass: update to \" + r.ver.String() + \"\\nNote: This is an auto-generated change as part of the gopass release process.\\n\"\n}\n\nfunc (r *repo) updatePrepare() error {\n\tfmt.Println(\"🌟 Running prepare ...\")\n\n\t// git co master\n\tif err := r.gitCoMaster(); err != nil {\n\t\treturn fmt.Errorf(\"git checkout master failed: %w\", err)\n\t}\n\tif !r.isGitClean() {\n\t\treturn fmt.Errorf(\"git is dirty\")\n\t}\n\t// git pull origin master\n\tif err := r.gitPom(); err != nil {\n\t\treturn fmt.Errorf(\"git pull origin master failed: %w\", err)\n\t}\n\t// git co -b gopass-VER\n\tif err := r.gitBranch(); err == nil {\n\t\treturn nil\n\t}\n\n\t// git branch -d gopass-VER\n\tif err := r.gitBranchDel(); err != nil {\n\t\treturn fmt.Errorf(\"git branch -d failed: %w\", err)\n\t}\n\n\treturn r.gitBranch()\n}\n\nfunc (r *repo) updateFinalize(path string) error {\n\tfmt.Println(\"🌟 Running finalize ...\")\n\n\t// git commit -m 'gopass: update to VER'\n\tif err := r.gitCommit(path); err != nil {\n\t\treturn fmt.Errorf(\"git commit %s failed: %w\", path, err)\n\t}\n\t// git push myfork gopass-VER\n\treturn r.gitPush(r.rem)\n}\n\nfunc (r *repo) gitCoMaster() error {\n\treturn gitutils.GitCoMaster(r.dir)\n}\n\nfunc (r *repo) gitBranch() error {\n\treturn gitutils.GitCoBranch(r.dir, r.branch())\n}\n\nfunc (r *repo) gitBranchDel() error {\n\treturn gitutils.GitDelBranch(r.dir, r.branch())\n}\n\nfunc (r *repo) gitPom() error {\n\treturn gitutils.GitPom(r.dir)\n}\n\nfunc (r *repo) gitPush(remote string) error {\n\treturn gitutils.GitPush(remote, r.branch())\n}\n\nfunc (r *repo) gitCommit(files ...string) error {\n\treturn gitutils.GitCommit(r.dir, r.commitMsg(), files...)\n}\n\nfunc (r *repo) isGitClean() bool {\n\treturn gitutils.IsGitClean(r.dir)\n}\n\nfunc strp(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "helpers/postrel/main_test.go",
    "content": "//go:build linux || darwin\n\npackage main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/helpers/gitutils\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// Test mustCheckEnv function\nfunc TestMustCheckEnv(t *testing.T) {\n\tos.Setenv(\"GITHUB_TOKEN\", \"mock-token\")\n\tos.Setenv(\"GITHUB_USER\", \"mock-user\")\n\tos.Setenv(\"GITHUB_FORK\", \"mock-fork\")\n\n\tassert.NotPanics(t, mustCheckEnv)\n}\n\n// Test createMilestones function\n// TODO: Add test for createMilestones function\n// func TestCreateMilestones(t *testing.T) {\n// \tctx := t.Context()\n// \tghCl := newMockGHClient(ctx)\n// \tversion := semver.MustParse(\"1.2.3\")\n\n// \terr := ghCl.createMilestones(ctx, version)\n// \tassert.NoError(t, err)\n// }\n\n// Test versionFile function\nfunc TestVersionFile(t *testing.T) {\n\tdir := t.TempDir()\n\terr := os.WriteFile(filepath.Join(dir, \"VERSION\"), []byte(\"1.2.3\"), 0o644)\n\tassert.NoError(t, err)\n\n\tos.Chdir(dir)\n\tversion, err := versionFile()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"1.2.3\", version.String())\n}\n\n// Test goVersion function\nfunc TestGoVersion(t *testing.T) {\n\tfor _, v := range []string{\"1.15\", \"1.16\", \"1.17\", \"1.25rc2\"} {\n\t\tt.Logf(\"Testing goVersion with %s\", v)\n\t\tassert.NotEmpty(t, goVersion(v), v)\n\t}\n}\n\n// Test updateWorkflows function\nfunc TestUpdateWorkflows(t *testing.T) {\n\tdir := t.TempDir()\n\tgitutils.InitGitDir(t, dir)\n\n\terr := os.MkdirAll(filepath.Join(dir, \".github\", \"workflows\"), 0o755)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \".github\", \"workflows\", \"test.yml\"), []byte(\"go-version: 1.15\"), 0o644)\n\tassert.NoError(t, err)\n\n\tupdater := &inUpdater{\n\t\tgoVer: \"1.16\",\n\t}\n\n\terr = updater.updateWorkflows(t.Context(), dir)\n\tassert.NoError(t, err)\n\n\tcontent, err := os.ReadFile(filepath.Join(dir, \".github\", \"workflows\", \"test.yml\"))\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(content), \"go-version: 1.16\")\n}\n"
  },
  {
    "path": "helpers/proxy/Dockerfile.debian",
    "content": "FROM debian:bookworm\n\nRUN apt update && apt install -y \\\n    sudo \\\n    curl \\\n    iproute2 \\\n    iputils-ping \\\n    vim\nRUN curl -L -q https://packages.gopass.pw/repos/gopass/gopass-archive-keyring.gpg | sudo tee /usr/share/keyrings/gopass-archive-keyring.gpg\nADD helpers/proxy/apt.debughttp /etc/apt/apt.conf.d/99debughttp\nADD helpers/proxy/gopass.sources /etc/apt/sources.list.d/gopass.sources\n\nCMD /bin/bash\n"
  },
  {
    "path": "helpers/proxy/README-3111.md",
    "content": "# Debugging Issue 3111\n\n- Run the proxy on the host: `go run helpers/proxy/main.go`\n  - Turn off the firewall / open the port!\n- Modify apt.debughttp and replace the HOST with the IP of the Docker host\n- `docker build -t debian:gopass -f helpers/proxy/Dockerfile.debian .`\n- `docker run --rm -ti debian:gopass`\n- Inside the container:\n- `apt update`\n"
  },
  {
    "path": "helpers/proxy/apt.debughttp",
    "content": "Debug::Acquire::http \"true\";\nDebug::Acquire::https \"true\";\nDebug::pkgAcquire \"true\";\nAcquire::http::Proxy \"http://HOST:8080/\";\n"
  },
  {
    "path": "helpers/proxy/gopass.sources",
    "content": "Types: deb\nURIs: https://packages.gopass.pw/repos/gopass\nSuites: stable\nArchitectures: all amd64 arm64 armhf\nComponents: main\nSigned-By: /usr/share/keyrings/gopass-archive-keyring.gpg\n"
  },
  {
    "path": "helpers/proxy/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar (\n\tlisten = flag.String(\"listen\", \":8080\", \"Address to listen on\")\n\t// List of common text-based MIME types\n\ttextContentTypes = map[string]bool{\n\t\t\"text/plain\":             true,\n\t\t\"text/html\":              true,\n\t\t\"text/css\":               true,\n\t\t\"text/xml\":               true,\n\t\t\"text/csv\":               true,\n\t\t\"text/javascript\":        true,\n\t\t\"text/markdown\":          true,\n\t\t\"application/json\":       true,\n\t\t\"application/xml\":        true,\n\t\t\"application/ld+json\":    true,\n\t\t\"application/javascript\": true, // RFC 4329, though text/javascript is more common\n\t\t\"application/xhtml+xml\":  true,\n\t\t\"application/atom+xml\":   true,\n\t\t\"application/rss+xml\":    true,\n\t\t\"image/svg+xml\":          true, // SVG is XML-based and human-readable\n\t}\n)\n\n// isClientOrServerError checks if the status code is a 4xx or 5xx error.\nfunc isClientOrServerError(statusCode int) bool {\n\treturn statusCode >= 400 && statusCode <= 599\n}\n\n// isTextContentType checks if the given Content-Type string indicates a textual format.\nfunc isTextContentType(contentTypeHeader string) bool {\n\t// Normalize: to lowercase and take only the main type/subtype, ignore parameters like charset\n\tmediaType := strings.ToLower(strings.Split(contentTypeHeader, \";\")[0])\n\n\t// Check if the media type is in the predefined list of text content types\n\tif textContentTypes[mediaType] {\n\t\treturn true\n\t}\n\n\t// Catch-all for other \"text/*\" types\n\tif strings.HasPrefix(mediaType, \"text/\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc modifyResponse(resp *http.Response) error {\n\tlog.Printf(\"Response from upstream for %s: Status %s\\n\", resp.Request.URL, resp.Status)\n\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tif isClientOrServerError(resp.StatusCode) || isTextContentType(contentType) {\n\t\tstatusStr := \"SUCCESSFUL\"\n\t\tif resp.StatusCode >= 400 {\n\t\t\tstatusStr = \"FAILED\"\n\t\t}\n\t\tlog.Printf(\"Upstream request to %s %s with status: %s. Dumping request and response.\\n\", resp.Request.URL, statusStr, resp.Status)\n\t\tdump, err := httputil.DumpRequestOut(resp.Request, true) // true to include body\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error dumping %s request for %s: %v\\n\", statusStr, resp.Request.URL, err)\n\t\t\t// Fallback logging if dump fails\n\t\t\tfmt.Fprintf(os.Stdout, \"---- BEGIN %s UPSTREAM REQUEST (DUMP FAILED: %v) ----\\nURL: %s\\nMethod: %s\\nHeaders: %v\\n---- END %s UPSTREAM REQUEST ----\\n\", statusStr, err, resp.Request.URL, resp.Request.Method, resp.Request.Header, statusStr)\n\t\t\treturn nil // Allow the original error response to proceed if dump fails\n\t\t}\n\t\tfmt.Fprintf(os.Stdout, \"---- BEGIN %s UPSTREAM REQUEST DUMP (URL: %s) ----\\n%s\\n---- END %s UPSTREAM REQUEST DUMP ----\\n\", statusStr, resp.Request.URL, string(dump), statusStr)\n\t\t// DumpResponse reads and replaces resp.Body with a new ReadCloser.\n\t\tdump, err = httputil.DumpResponse(resp, true) // true to include body\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error dumping %s response for %s: %v\\n\", statusStr, resp.Request.URL, err)\n\t\t\t// Fallback logging if dump fails\n\t\t\tfmt.Fprintf(os.Stdout, \"---- BEGIN %s UPSTREAM RESPONSE (DUMP FAILED: %v) ----\\nURL: %s\\nStatus: %s\\n---- END %s UPSTREAM RESPONSE ----\\n\", statusStr, err, resp.Request.URL, resp.Status, statusStr)\n\t\t\treturn nil // Allow the original error response to proceed if dump fails\n\t\t}\n\t\tfmt.Fprintf(os.Stdout, \"---- BEGIN %s UPSTREAM RESPONSE DUMP (URL: %s) ----\\n%s\\n---- END %s UPSTREAM RESPONSE DUMP ----\\n\", statusStr, resp.Request.URL, string(dump), statusStr)\n\t\t// The body is already replaced by DumpResponse, so ReverseProxy can send it.\n\t} else {\n\t\t// For other statuses (1xx informational, 3xx redirection), just log.\n\t\t// Redirections are typically handled by the client (browser) based on the headers.\n\t\t// ReverseProxy will pass these responses through.\n\t\tlog.Printf(\"Response from %s with status %s (not dumping content for this type).\\n\", resp.Request.URL, resp.Status)\n\t}\n\n\treturn nil // No error modifying response, let ReverseProxy handle it\n}\n\nfunc main() {\n\tdirector := func(req *http.Request) {\n\t\t// httputil.ReverseProxy will automatically set X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto.\n\t\t// It uses req.URL.Host as the destination.\n\t\t// We need to ensure req.URL.Scheme and req.URL.Host are correctly set.\n\t\t// if req.URL.Scheme == \"\" {\n\t\t// \treq.URL.Scheme = \"http\" // Assume http if not specified\n\t\t// }\n\t\treq.URL.Scheme = \"https\" // Force HTTPS\n\t\t// The Host header is already set by the client to the target server.\n\t\t// ReverseProxy uses req.URL.Host for dialing.\n\t\t// Ensure req.URL.Host is set to the target. It usually is from r.RequestURI.\n\t\tlog.Printf(\"Proxying %s request for: %s\\n\", req.Method, req.URL.String())\n\t}\n\n\terrorHandler := func(w http.ResponseWriter, r *http.Request, err error) {\n\t\tlog.Printf(\"Proxy error for %s: %v\\n\", r.URL, err)\n\t\t// This error means the upstream was unreachable (e.g., connection refused, DNS lookup failure).\n\t\t// There is no HTTP response from upstream to dump.\n\t\tfmt.Fprintf(os.Stdout, \"---- FAILED UPSTREAM ATTEMPT ----\\nURL: %s\\nError: %v\\n---- END FAILED UPSTREAM ATTEMPT ----\\n\", r.URL, err)\n\t\thttp.Error(w, fmt.Sprintf(\"Upstream server error: %v\", err), http.StatusBadGateway)\n\t}\n\n\treverseProxy := &httputil.ReverseProxy{\n\t\tDirector:       director,\n\t\tModifyResponse: modifyResponse,\n\t\tErrorHandler:   errorHandler,\n\t\tTransport:      http.DefaultTransport, // You can customize transport (e.g., for timeouts)\n\t}\n\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method == http.MethodConnect {\n\t\t\thttp.Error(w, \"CONNECT method not supported\", http.StatusMethodNotAllowed)\n\t\t\treturn\n\t\t} else {\n\t\t\treverseProxy.ServeHTTP(w, r)\n\t\t}\n\t})\n\n\tlog.Printf(\"Starting HTTP proxy on %s\\n\", *listen)\n\tserver := &http.Server{\n\t\tAddr:         *listen,\n\t\tHandler:      handler,\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t}\n\n\tif err := server.ListenAndServe(); err != nil {\n\t\tlog.Fatalf(\"Failed to start proxy server: %v\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "helpers/release/main.go",
    "content": "// Copyright 2021 The gopass Authors. All rights reserved.\n// Use of this source code is governed by the MIT license,\n// that can be found in the LICENSE file.\n\n// Release is the first part of the gopass release automation. It's supposed\n// to be run by a member of the gopass team. It will ensure that the repository\n// is in a clean state and make it trivial to trigger a new release.\n// You can run it without any parameters and as long as you pay close attention\n// to the output it will be a breeze.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/helpers/gitutils\"\n)\n\nvar (\n\tsleep   = time.Second\n\tissueRE = regexp.MustCompile(`#(\\d+)\\b`)\n\t// Supported formats:\n\t// [TAG] description\n\t// TAG: description\n\tsubjectRE = regexp.MustCompile(`^(\\[\\w+\\]\\s+.*|\\S+:\\s.*)$`)\n\tverTmpl   = `package main\n\nimport (\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\nfunc getVersion() semver.Version {\n\tsv, err := semver.Parse(strings.TrimPrefix(version, \"v\"))\n\tif err == nil {\n\t\treturn sv\n\t}\n\n\treturn semver.Version{\n\t\tMajor: {{ .Major }},\n\t\tMinor: {{ .Minor }},\n\t\tPatch: {{ .Patch }},\n\t\tPre: []semver.PRVersion{\n\t\t\t{VersionStr: \"git\"},\n\t\t},\n\t\tBuild: []string{\"{{ .Build }}\"},\n\t}\n}\n`\n)\n\nconst logo = `\n   __     _    _ _      _ _   ___   ___\n /'_ '\\ /'_'\\ ( '_'\\  /'_' )/',__)/',__)\n( (_) |( (_) )| (_) )( (_| |\\__, \\\\__, \\\n'\\__  |'\\___/'| ,__/''\\__,_)(____/(____/\n( )_) |       | |\n \\___/'       (_)\n\n `\n\nfunc main() {\n\tfmt.Println(logo)\n\tfmt.Println()\n\tfmt.Println(\"🌟 Preparing a new gopass release.\")\n\tfmt.Println(\"☝  Checking pre-conditions ...\")\n\n\tprevVer, nextVer := getVersions()\n\n\t// - check that workdir is clean\n\tif !isGitClean() {\n\t\tpanic(\"❌ git is dirty\")\n\t}\n\tfmt.Println(\"✅ git is clean\")\n\n\tif len(nextVer.Pre) < 1 {\n\t\t// - check out master\n\t\tif err := gitCoMaster(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfmt.Println(\"✅ Switched to master branch\")\n\t\t// - pull from origin\n\t\tif err := gitPom(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfmt.Println(\"✅ Fetched changes for master\")\n\t}\n\t// - check that workdir is clean\n\tif !isGitClean() {\n\t\tpanic(\"git is dirty\")\n\t}\n\tfmt.Println(\"✅ git is still clean\")\n\n\tfmt.Println()\n\tfmt.Printf(\"✅ New version will be: %s\\n\", nextVer.String())\n\tfmt.Println()\n\tfmt.Println(\"❓ Do you want to continue? (press any key to continue or Ctrl+C to abort)\")\n\tfmt.Scanln()\n\n\t// - update deps and run tests\n\tif err := updateDeps(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// - update VERSION\n\tif err := writeVersion(nextVer); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"✅ Wrote VERSION\")\n\ttime.Sleep(sleep)\n\t// - update version.go\n\tif err := writeVersionGo(nextVer); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"✅ Wrote version.go\")\n\ttime.Sleep(sleep)\n\t// - update CHANGELOG.md\n\tif err := writeChangelog(prevVer, nextVer); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"✅ Updated CHANGELOG.md\")\n\ttime.Sleep(sleep)\n\t// - update shell completions\n\tif err := updateCompletion(); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"✅ Updated shell completions\")\n\ttime.Sleep(sleep)\n\t// - update man page\n\tif err := updateManpage(); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(\"✅ Updated man page\")\n\ttime.Sleep(sleep)\n\n\t// - create PR\n\t//   git checkout -b release/vX.Y.Z\n\tif err := gitCoRel(nextVer); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"✅ Created branch release/v%s\\n\", nextVer.String())\n\ttime.Sleep(sleep)\n\n\t// commit changes\n\tif err := gitCommit(nextVer); err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"✅ Committed changes to release/v%s\\n\", nextVer.String())\n\ttime.Sleep(sleep)\n\n\tfmt.Println(\"🏁 Preparation finished\")\n\ttime.Sleep(sleep)\n\n\tfmt.Printf(\"⚠ Prepared release of gopass %s.\\n\", nextVer.String())\n\ttime.Sleep(sleep)\n\n\tfmt.Printf(\"⚠ Run 'git push <remote> release/v%s' to push this branch and open a PR against gopasspw/gopass master.\\n\", nextVer.String())\n\ttime.Sleep(sleep)\n\n\tfmt.Printf(\"⚠ Get the PR merged and run 'git tag -s v%s && git push origin v%s' to kick off the release process.\\n\", nextVer.String(), nextVer.String())\n\ttime.Sleep(sleep)\n\tfmt.Println()\n\n\tfmt.Println(\"💎🙌 Done 🚀🚀🚀🚀🚀🚀\")\n}\n\nfunc getVersions() (semver.Version, semver.Version) {\n\tnextVerFlag := \"\"\n\tif len(os.Args) > 1 && !strings.HasPrefix(os.Args[1], \"-test.\") {\n\t\tnextVerFlag = strings.TrimSpace(strings.TrimPrefix(os.Args[1], \"v\"))\n\t}\n\tprevVerFlag := \"\"\n\tif len(os.Args) > 2 && !strings.HasPrefix(os.Args[2], \"-test.\") {\n\t\tprevVerFlag = strings.TrimSpace(strings.TrimPrefix(os.Args[2], \"v\"))\n\t}\n\n\t// obtain the last tagged version from git\n\tgitVer, err := gitVersion()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// read the version file to get the last committed version\n\tvfVer, err := versionFile()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprevVer := gitVer\n\tif prevVerFlag != \"\" {\n\t\tprevVer = semver.MustParse(prevVerFlag)\n\t}\n\n\tif gitVer.NE(vfVer) {\n\t\tfmt.Printf(\"git version: %q != VERSION: %q\\n\", gitVer.String(), vfVer.String())\n\t\tif prevVerFlag == \"\" && len(vfVer.Pre) < 1 {\n\t\t\tusage()\n\t\t\tpanic(\"version mismatch\")\n\t\t}\n\t}\n\n\tnextVer := prevVer\n\tif nextVerFlag != \"\" {\n\t\tnextVer = semver.MustParse(nextVerFlag)\n\t\tif nextVer.LTE(prevVer) {\n\t\t\tusage()\n\t\t\tpanic(\"next version must be greather than the previous version\")\n\t\t}\n\t} else {\n\t\tnextVer.IncrementPatch()\n\t\tif len(vfVer.Pre) > 0 {\n\t\t\tnextVer = vfVer\n\t\t\tnextVer.Pre = nil\n\t\t}\n\t}\n\n\tfmt.Printf(`☝ Version overview\n  Git (latest tag):  %q\n  VERSION:           %q\n  Next version flag: %q\n  Prev version flag: %q\n\nWill use\n  Previous: %q\n  Next:     %q\n`,\n\t\tgitVer,\n\t\tvfVer,\n\t\tprevVerFlag,\n\t\tnextVerFlag,\n\t\tprevVer,\n\t\tnextVer)\n\n\treturn prevVer, nextVer\n}\n\nfunc updateDeps() error {\n\tif sv := os.Getenv(\"GOPASS_NOUPGRADE\"); sv == \"\" {\n\t\tcmd := exec.Command(\"make\", \"upgrade\")\n\t\tcmd.Stderr = os.Stderr\n\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif sv := os.Getenv(\"GOPASS_NOTEST\"); sv != \"\" {\n\t\tfmt.Printf(\"⚠ GOPASS_NOTEST=%v, skipping 'make gha-linux'\", sv)\n\n\t\treturn nil\n\t}\n\n\ttd := os.TempDir()\n\tfn := filepath.Join(td, \"gopass-release.log\")\n\tfh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY, 0o600)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd := exec.Command(\"make\", \"gha-linux\")\n\tcmd.Stderr = io.MultiWriter(fh, os.Stderr)\n\tcmd.Stdout = fh\n\tcmd.Env = []string{\n\t\t\"LANG=en_US.UTF-8\",\n\t\t\"PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/bin:\" + os.Getenv(\"GOBIN\"),\n\t}\n\n\tfor _, v := range os.Environ() {\n\t\tif strings.HasPrefix(v, \"GO\") {\n\t\t\tcmd.Env = append(cmd.Env, v)\n\t\t}\n\t\tif strings.HasPrefix(v, \"HOME=\") {\n\t\t\tcmd.Env = append(cmd.Env, v)\n\t\t}\n\t}\n\n\tif err := cmd.Run(); err != nil {\n\t\t_ = fh.Close()\n\t\tfmt.Printf(\"⚠ 'make gha-linux' failed. Please see the log at %s!\", fn)\n\n\t\treturn err\n\t}\n\n\t// remove the log, we don't need it anymore\n\t_ = fh.Close()\n\t_ = os.RemoveAll(fn)\n\n\treturn nil\n}\n\nfunc gitCoMaster() error {\n\treturn gitutils.GitCoMaster(\".\")\n}\n\nfunc gitPom() error {\n\treturn gitutils.GitPom(\".\")\n}\n\nfunc gitCoRel(v semver.Version) error {\n\treturn gitutils.GitCoBranch(\".\", \"release/v\"+v.String())\n}\n\nfunc gitCommit(v semver.Version) error {\n\targs := []string{\n\t\t\"CHANGELOG.md\",\n\t\t\"VERSION\",\n\t\t\"version.go\",\n\t\t\"gopass.1\",\n\t\t\"*.completion\",\n\t\t\"go.mod\",\n\t\t\"go.sum\",\n\t\t\"pkg/pwgen/pwrules/pwrules_gen.go\",\n\t}\n\treturn gitutils.GitCommit(\".\", \"Tag v\"+v.String(), args...)\n}\n\nfunc writeChangelog(prev, next semver.Version) error {\n\tcl, err := changelogEntries(prev)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// prepend the new changelog entries by first writing the\n\t// new content in a new file ...\n\tfh, err := os.Create(\"CHANGELOG.new\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close()\n\n\tofh, err := os.Open(\"CHANGELOG.md\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer ofh.Close()\n\n\tscanner := bufio.NewScanner(ofh)\n\n\tvar written bool\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\t// insert the new section before the last entry\n\t\tif strings.HasPrefix(line, \"## \") && !written {\n\t\t\tfmt.Fprintf(fh, \"## %s / %s\\n\\n\", next.String(), time.Now().UTC().Format(\"2006-01-02\"))\n\t\t\tfor _, e := range cl {\n\t\t\t\tfmt.Fprint(fh, \"* \")\n\t\t\t\tfmt.Fprintln(fh, e)\n\t\t\t}\n\t\t\tfmt.Fprintln(fh)\n\n\t\t\twritten = true\n\t\t}\n\n\t\t// all existing lines are just copied over\n\t\tfmt.Fprintln(fh, line)\n\t}\n\n\t// renaming the new file to the old file\n\treturn os.Rename(\"CHANGELOG.new\", \"CHANGELOG.md\")\n}\n\nfunc updateCompletion() error {\n\tcmd := exec.Command(\"make\", \"completion\")\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Run()\n}\n\nfunc updateManpage() error {\n\tcmd := exec.Command(\"make\", \"man\")\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Run()\n}\n\nfunc writeVersion(v semver.Version) error {\n\treturn os.WriteFile(\"VERSION\", []byte(v.String()+\"\\n\"), 0o644)\n}\n\ntype tplPayload struct {\n\tMajor uint64\n\tMinor uint64\n\tPatch uint64\n\tBuild string\n}\n\nfunc writeVersionGo(v semver.Version) error {\n\ttmpl, err := template.New(\"version\").Parse(verTmpl)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfh, err := os.Create(\"version.go\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close()\n\n\tbuild := \"HEAD\"\n\tif sv, err := gitCommitHash(); err == nil {\n\t\tbuild = sv\n\t}\n\n\treturn tmpl.Execute(fh, tplPayload{\n\t\tMajor: v.Major,\n\t\tMinor: v.Minor,\n\t\tPatch: v.Patch,\n\t\tBuild: build,\n\t})\n}\n\nfunc isGitClean() bool {\n\treturn gitutils.IsGitClean(\".\")\n}\n\nfunc versionFile() (semver.Version, error) {\n\tbuf, err := os.ReadFile(\"VERSION\")\n\tif err != nil {\n\t\treturn semver.Version{}, err\n\t}\n\n\treturn semver.Parse(strings.TrimSpace(string(buf)))\n}\n\nfunc gitCommitHash() (string, error) {\n\tbuf, err := exec.Command(\"git\", \"rev-parse\", \"--short\", \"HEAD\").CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(string(buf)), nil\n}\n\nfunc gitVersion() (semver.Version, error) {\n\tbuf, err := exec.Command(\"git\", \"tag\", \"--sort=version:refname\").CombinedOutput()\n\tif err != nil {\n\t\treturn semver.Version{}, err\n\t}\n\n\tlines := strings.Split(strings.TrimSpace(string(buf)), \"\\n\")\n\tif len(lines) < 1 {\n\t\treturn semver.Version{}, fmt.Errorf(\"no output\")\n\t}\n\n\tfor i := len(lines); i > 0; i-- {\n\t\tsv := strings.TrimPrefix(lines[i-1], \"v\")\n\t\tv, err := semver.Parse(sv)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(v.Pre) > 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn v, nil\n\t}\n\n\treturn semver.Version{}, fmt.Errorf(\"no valid version found\")\n}\n\nfunc changelogEntries(since semver.Version) ([]string, error) {\n\t// set up a custom output format for the git log command to make it easier to parse here.\n\tgitSep := \"@@@GIT-SEP@@@\"\n\tgitDelim := \"@@@GIT-DELIM@@@\"\n\t// full hash - subject - body\n\t// note: we don't use the hash at the moment\n\tprettyFormat := gitSep + \"%H\" + gitDelim + \"%s\" + gitDelim + \"%b\" + gitSep\n\targs := []string{\n\t\t\"log\",\n\t\t\"v\" + since.String() + \"..HEAD\",\n\t\t\"--pretty=\" + prettyFormat,\n\t}\n\tbuf, err := exec.Command(\"git\", args...).CombinedOutput()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run git %+v with error %w: %s\", args, err, string(buf))\n\t}\n\n\t// gitSep separates each commit from the next\n\tnotes := make([]string, 0, 10)\n\tcommits := strings.SplitSeq(string(buf), gitSep)\n\tfor commit := range commits {\n\t\tcommit := strings.TrimSpace(commit)\n\t\tif commit == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// inside each commit gitDelim seaparates each field from each other\n\t\t// p[0] - full hash\n\t\t// p[1] - subject\n\t\t// p[2] - body (might be empty)\n\t\tp := strings.Split(commit, gitDelim)\n\t\tif len(p) < 3 {\n\t\t\t// invalid entry, shouldn't happen\n\t\t\tcontinue\n\t\t}\n\n\t\tsubject := strings.TrimSpace(p[1])\n\n\t\t// extract github issue numbers from the subject\n\t\tissues := []string{}\n\t\tif m := issueRE.FindStringSubmatch(strings.TrimSpace(subject)); len(m) > 1 {\n\t\t\tissues = append(issues, m[1])\n\t\t}\n\n\t\t// try to extract the release note from the subject\n\t\tif m := subjectRE.FindStringSubmatch(subject); len(m) > 1 {\n\t\t\tnotes = append(notes, m[1])\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// if no suitable subject was parsed, try to parse the body as well\n\t\tfor line := range strings.SplitSeq(p[2], \"\\n\") {\n\t\t\tline := strings.TrimSpace(line)\n\n\t\t\tif m := issueRE.FindStringSubmatch(line); len(m) > 1 {\n\t\t\t\tissues = append(issues, m[1])\n\t\t\t}\n\n\t\t\tif !strings.HasPrefix(line, \"RELEASE_NOTES=\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp := strings.Split(line, \"=\")\n\t\t\tif len(p) < 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tval := p[1]\n\t\t\tif strings.ToLower(val) == \"n/a\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(issues) > 0 {\n\t\t\t\tval += \" (#\" + strings.Join(issues, \", #\") + \")\"\n\t\t\t}\n\t\t\tnotes = append(notes, val)\n\t\t}\n\t}\n\n\tsort.Strings(notes)\n\n\treturn notes, nil\n}\n\nfunc usage() {\n\tfmt.Printf(\"Usage: %s [next version] [prev version]\\n\", \"go run helpers/release/main.go\")\n}\n"
  },
  {
    "path": "helpers/release/main_test.go",
    "content": "//go:build linux\n\npackage main\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/helpers/gitutils\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestGetVersions tests the getVersions function.\nfunc TestGetVersions(t *testing.T) {\n\t// Create a temporary directory for the test.\n\ttempDir := t.TempDir()\n\n\tdir := gitutils.InitGitDirWithRemote(t, tempDir)\n\t// Change the working directory to the temporary directory.\n\tos.Chdir(dir)\n\n\t// Create a mock VERSION file.\n\terr := os.WriteFile(\"VERSION\", []byte(\"1.2.3\\n\"), 0o644)\n\tassert.NoError(t, err)\n\n\t// Create a git tag.\n\trequire.NoError(t, gitutils.GitTagAndPush(dir, \"v1.2.3\"))\n\n\t// Call the getVersions function.\n\tprevVer, nextVer := getVersions()\n\n\t// Assert the versions.\n\tassert.Equal(t, \"1.2.3\", prevVer.String())\n\tassert.Equal(t, \"1.2.4\", nextVer.String())\n}\n\n// TestWriteVersion tests the writeVersion function.\nfunc TestWriteVersion(t *testing.T) {\n\t// Create a temporary directory for the test.\n\ttempDir := t.TempDir()\n\t// Change the working directory to the temporary directory.\n\tos.Chdir(tempDir)\n\n\t// Call the writeVersion function.\n\terr := writeVersion(semver.MustParse(\"1.2.3\"))\n\tassert.NoError(t, err)\n\n\t// Read the VERSION file.\n\tdata, err := os.ReadFile(\"VERSION\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"1.2.3\\n\", string(data))\n}\n\n// TestWriteVersionGo tests the writeVersionGo function.\nfunc TestWriteVersionGo(t *testing.T) {\n\t// Create a temporary directory for the test.\n\ttempDir := t.TempDir()\n\t// Change the working directory to the temporary directory.\n\tos.Chdir(tempDir)\n\n\t// Call the writeVersionGo function.\n\terr := writeVersionGo(semver.MustParse(\"1.2.3\"))\n\tassert.NoError(t, err)\n\n\t// Read the version.go file.\n\tdata, err := os.ReadFile(\"version.go\")\n\tassert.NoError(t, err)\n\tassert.Contains(t, string(data), \"Major: 1\")\n\tassert.Contains(t, string(data), \"Minor: 2\")\n\tassert.Contains(t, string(data), \"Patch: 3\")\n}\n"
  },
  {
    "path": "internal/action/action.go",
    "content": "package action\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/reminder\"\n\t\"github.com/gopasspw/gopass/internal/store/root\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar (\n\tstdin  io.Reader = os.Stdin\n\tstdout io.Writer = os.Stdout\n\tstderr io.Writer = os.Stderr //nolint:unused\n)\n\n// Action knows everything to run gopass CLI actions.\ntype Action struct {\n\tName    string\n\tStore   *root.Store\n\tcfg     *config.Config\n\tversion semver.Version\n\trem     *reminder.Store\n}\n\n// New returns a new Action wrapper.\nfunc New(cfg *config.Config, sv semver.Version) (*Action, error) {\n\treturn newAction(cfg, sv, true)\n}\n\nfunc newAction(cfg *config.Config, sv semver.Version, remind bool) (*Action, error) {\n\tname := \"gopass\"\n\tif len(os.Args) > 0 {\n\t\tname = filepath.Base(os.Args[0])\n\t}\n\n\tact := &Action{\n\t\tName:    name,\n\t\tcfg:     cfg,\n\t\tversion: sv,\n\t\tStore:   root.New(cfg),\n\t}\n\n\tif remind {\n\t\tr, err := reminder.New()\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to init reminder: %s\", err)\n\t\t} else {\n\t\t\t// only populate the reminder variable on success, the implementation.\n\t\t\t// can handle being called on a nil pointer.\n\t\t\tact.rem = r\n\t\t}\n\t}\n\n\treturn act, nil\n}\n\n// String implement fmt.Stringer.\nfunc (s *Action) String() string {\n\treturn s.Store.String()\n}\n"
  },
  {
    "path": "internal/action/action_test.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc newMock(ctx context.Context, path string) (*Action, error) {\n\tcfg := config.NewInMemory()\n\tif err := cfg.SetPath(path); err != nil {\n\t\treturn nil, err\n\t}\n\tctx = cfg.WithConfig(ctx)\n\n\tif !backend.HasCryptoBackend(ctx) {\n\t\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\t}\n\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\tact, err := newAction(cfg, semver.Version{}, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\tc := cli.NewContext(cli.NewApp(), fs, nil)\n\tc.Context = ctx\n\tif err := act.IsInitialized(c); err != nil {\n\t\t// we still return the action since this might be expected sometimes\n\t\treturn act, err\n\t}\n\n\treturn act, nil\n}\n\nfunc TestAction(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\n\tactName := \"action.test\"\n\n\tif runtime.GOOS == \"windows\" {\n\t\tactName = \"action.test.exe\"\n\t}\n\n\tassert.Equal(t, actName, act.Name)\n\n\tassert.Contains(t, act.String(), u.StoreDir(\"\"))\n\tassert.Empty(t, act.Store.Mounts())\n}\n\nfunc TestNew(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\ttd := t.TempDir()\n\tcfg := config.NewInMemory()\n\tsv := semver.Version{}\n\n\tt.Run(\"init a new store\", func(t *testing.T) {\n\t\t_, err := New(cfg, sv)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"init an existing plain store\", func(t *testing.T) {\n\t\trequire.NoError(t, cfg.SetPath(filepath.Join(td, \"store\")))\n\t\trequire.NoError(t, os.MkdirAll(cfg.Path(), 0o700))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(cfg.Path(), plain.IDFile), []byte(\"foobar\"), 0o600))\n\t\t_, err := New(cfg, sv)\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/action/aliases.go",
    "content": "package action\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// AliasesPrint prints all configured aliases for password generation rules.\nfunc (s *Action) AliasesPrint(c *cli.Context) error {\n\tout.Printf(c.Context, \"Configured aliases:\")\n\taliases := pwrules.AllAliases(c.Context)\n\tkeys := make([]string, 0, len(aliases))\n\tfor k := range aliases {\n\t\tkeys = append(keys, k)\n\t}\n\n\tsort.Strings(keys)\n\tfor _, k := range keys {\n\t\tout.Printf(c.Context, \"- %s -> %s\", k, strings.Join(aliases[k], \", \"))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/aliases_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAliases(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\trequire.NoError(t, act.AliasesPrint(gptest.CliCtx(ctx, t)))\n}\n"
  },
  {
    "path": "internal/action/audit.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/audit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Audit validates passwords against common flaws.\nfunc (s *Action) Audit(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\t_ = s.rem.Reset(\"audit\")\n\tout.Print(ctx, \"Auditing passwords for common flaws ...\")\n\n\tt, err := s.Store.Tree(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to get store tree: %s\", err)\n\t}\n\n\tif filter := c.Args().First(); filter != \"\" {\n\t\tsubtree, err := t.FindFolder(filter)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to find subtree: %s\", err)\n\t\t}\n\t\tdebug.Log(\"subtree for %q: %+v\", filter, subtree)\n\t\tt = subtree\n\t}\n\n\tlist := t.List(tree.INF)\n\n\tif len(list) < 1 {\n\t\tout.Printf(ctx, \"No secrets found\")\n\n\t\treturn nil\n\t}\n\n\tvar excludes string\n\tst := s.Store.Storage(ctx, c.Args().First())\n\tif buf, err := st.Get(ctx, \".gopass-audit-ignore\"); err == nil && buf != nil {\n\t\texcludes = string(buf)\n\t}\n\tnList := audit.FilterExcludes(excludes, list)\n\tif len(nList) < len(list) {\n\t\tout.Warningf(ctx, \"Excluding %d secrets based on .gopass-audit-ignore\", len(list)-len(nList))\n\t}\n\n\ta := audit.New(c.Context, s.Store)\n\tr, err := a.Batch(ctx, nList)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to audit password store: %s\", err)\n\t}\n\n\tif p := c.String(\"template\"); p != \"\" && fsutil.IsFile(p) {\n\t\tr.Template = p\n\t}\n\n\tswitch c.String(\"format\") {\n\tcase \"html\":\n\t\treturn saveReport(ctx, r.RenderHTML, c.String(\"output-file\"), \"html\")\n\tcase \"csv\":\n\t\treturn saveReport(ctx, r.RenderCSV, c.String(\"output-file\"), \"csv\")\n\tdefault:\n\t\tvar err error\n\t\tif c.Bool(\"full\") {\n\t\t\tdebug.Log(\"Printing full report\")\n\t\t\terr = r.PrintResults(ctx)\n\t\t}\n\t\tif c.Bool(\"summary\") {\n\t\t\tdebug.Log(\"Printing summary\")\n\n\t\t\tnerr := r.PrintSummary(ctx)\n\t\t\t// do not overwrite err if it is already set\n\t\t\tif err == nil {\n\t\t\t\terr = nerr\n\t\t\t}\n\t\t}\n\t\tif !c.Bool(\"full\") && !c.Bool(\"summary\") {\n\t\t\tout.Warning(ctx, \"No output format specified. Use `--full` or `--summary` to specify.\")\n\t\t}\n\n\t\treturn err\n\t}\n}\n\nfunc saveReport(ctx context.Context, f func(io.Writer) error, path, suffix string) error {\n\tif path == \"\" {\n\t\tout.Noticef(ctx, \"No output filename given. Will use a random file name. Use `--output-file` to specify.\")\n\t}\n\n\tfn, err := writeReport(f, path)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to write report to %s: %s\", fn, err)\n\t}\n\n\tif !strings.HasSuffix(fn, \".\"+suffix) {\n\t\tnfn := fn + \".\" + suffix\n\t\tif err := os.Rename(fn, fn+\".\"+suffix); err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"failed to rename report to %s: %s\", nfn, err)\n\t\t}\n\t\tfn = nfn\n\t}\n\n\tout.Noticef(ctx, \"Wrote report to %s\", fn)\n\n\treturn nil\n}\n\nfunc writeReport(f func(io.Writer) error, path string) (string, error) {\n\tfh, err := openReport(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\tif err := f(fh); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fh.Name(), nil\n}\n\nfunc openReport(path string) (*os.File, error) {\n\tif path == \"\" {\n\t\treturn os.CreateTemp(\"\", \"gopass-report\")\n\t}\n\n\treturn os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)\n}\n"
  },
  {
    "path": "internal/action/audit_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAudit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"expect audit to complains on very weak passwords\", func(t *testing.T) {\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"123\")\n\t\trequire.NoError(t, act.Store.Set(ctx, \"bar\", sec))\n\t\trequire.NoError(t, act.Store.Set(ctx, \"baz\", sec))\n\n\t\trequire.Error(t, act.Audit(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"full\": \"true\"})), buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"test with filter\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.Error(t, act.Audit(c))\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"test empty store\", func(t *testing.T) {\n\t\tfor _, v := range []string{\"foo\", \"bar\", \"baz\"} {\n\t\t\trequire.NoError(t, act.Store.Delete(ctx, v))\n\t\t}\n\t\trequire.NoError(t, act.Audit(gptest.CliCtx(ctx, t)))\n\t\tassert.Contains(t, \"No secrets found\", buf.String())\n\t\tbuf.Reset()\n\t})\n}\n"
  },
  {
    "path": "internal/action/binary.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar binstdin = os.Stdin\n\n// Cat prints to or reads from STDIN/STDOUT.\n// If the content is piped to stdin, it is written to the secret.\n// Otherwise, the secret content is printed to stdout.\nfunc (s *Action) Cat(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.NoName, nil, \"Usage: %s cat <NAME>\", c.App.Name)\n\t}\n\n\t// handle pipe to stdin.\n\tinfo, err := binstdin.Stat()\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"failed to stat stdin: %s\", err)\n\t}\n\n\t// if content is piped to stdin, read and save it.\n\tif info.Mode()&os.ModeCharDevice == 0 {\n\t\tdebug.Log(\"Reading from STDIN ...\")\n\t\tcontent := &bytes.Buffer{}\n\n\t\tif written, err := io.Copy(content, binstdin); err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"Failed to copy after %d bytes: %s\", written, err)\n\t\t}\n\n\t\tsec, err := secFromBytes(name, \"STDIN\", content.Bytes())\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"Failed to parse secret from STDIN: %v\", err)\n\t\t}\n\t\tif err = s.Store.Set(\n\t\t\tctxutil.WithCommitMessage(ctx, \"Read secret from STDIN\"),\n\t\t\tname,\n\t\t\tsec,\n\t\t); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to write secret from STDIN: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tbuf, err := s.binaryGet(ctx, name)\n\tif err != nil {\n\t\treturn exit.Error(exit.Decrypt, err, \"failed to read secret: %s\", err)\n\t}\n\tdebug.Log(\"read %d decoded bytes from secret %s\", len(buf), name)\n\n\tfmt.Fprint(stdout, string(buf))\n\n\treturn nil\n}\n\nfunc secFromBytes(dst, src string, in []byte) (gopass.Secret, error) {\n\tdebug.Log(\"Read %d bytes from %s to %s\", len(in), src, dst)\n\n\tsec := secrets.NewAKV()\n\tif err := sec.Set(\"Content-Disposition\", fmt.Sprintf(\"attachment; filename=\\\"%s\\\"\", filepath.Base(src))); err != nil {\n\t\tdebug.Log(\"Failed to set Content-Disposition: %q\", err)\n\t}\n\tif err := sec.Set(\"Content-Transfer-Encoding\", \"Base64\"); err != nil {\n\t\tdebug.Log(\"Failed to set Content-Transfer-Encoding: %q\", err)\n\t}\n\n\tvar written int\n\tencoder := base64.NewEncoder(base64.StdEncoding, sec)\n\tn, err := encoder.Write(in)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to write to base64 encoder: %v\", err)\n\n\t\treturn sec, err\n\t}\n\twritten += n\n\n\tif err := encoder.Close(); err != nil {\n\t\tdebug.Log(\"Failed to finalize base64 payload: %v\", err)\n\n\t\treturn sec, err\n\t}\n\tn, err = sec.Write([]byte(\"\\n\"))\n\tif err != nil {\n\t\tdebug.Log(\"Failed to write to secret: %v\", err)\n\n\t\treturn sec, err\n\t}\n\twritten += n\n\n\tdebug.Log(\"Wrote %d bytes of Base64 encoded bytes to secret\", written)\n\n\treturn sec, nil\n}\n\n// BinaryCopy copies either from the filesystem to the store or from the store\n// to the filesystem.\nfunc (s *Action) BinaryCopy(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tfrom := c.Args().Get(0)\n\tto := c.Args().Get(1)\n\n\t// argument checking is in s.binaryCopy.\n\tif err := s.binaryCopy(ctx, c, from, to, false); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"%s\", err)\n\t}\n\n\treturn nil\n}\n\n// BinaryMove works like Copy but will remove (shred/wipe) the source\n// after a successful copy. Mostly useful for securely moving secrets into\n// the store if they are no longer needed / wanted on disk afterwards.\nfunc (s *Action) BinaryMove(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tfrom := c.Args().Get(0)\n\tto := c.Args().Get(1)\n\n\t// argument checking is in s.binaryCopy.\n\tif err := s.binaryCopy(ctx, c, from, to, true); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"%s\", err)\n\t}\n\n\treturn nil\n}\n\n// isFilePath returns true if the given string is likely a file path.\nfunc isFilePath(s string) bool {\n\t// this heuristic tries to detect filepaths that are not valid secret names.\n\t// this should trigger in case a file and secret names are mixed up.\n\tif strings.HasPrefix(s, \"/\") || strings.HasPrefix(s, \"./\") || strings.HasPrefix(s, \"../\") {\n\t\treturn true\n\t}\n\n\treturn fsutil.IsFile(s)\n}\n\n// isInStore returns true if the given file is in the store or a mounted substore.\nfunc (s *Action) isInStore(fn string) bool {\n\tfp, err := filepath.Abs(fn)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(fp, s.Store.Path()) {\n\t\treturn true\n\t}\n\tfor _, mp := range s.Store.Mounts() {\n\t\tmp, err := filepath.Abs(mp)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(fp, mp) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// binaryCopy implements the control flow for copy and move. We support two\n// workflows:.\n// 1. From the filesystem to the store.\n// 2. From the store to the filesystem.\n//\n// Copying secrets in the store must be done through the regular copy command.\nfunc (s *Action) binaryCopy(ctx context.Context, c *cli.Context, from, to string, deleteSource bool) error {\n\tif from == \"\" || to == \"\" {\n\t\top := \"copy\"\n\t\tif deleteSource {\n\t\t\top = \"move\"\n\t\t}\n\n\t\treturn fmt.Errorf(\"usage: %s fs%s from to\", c.App.Name, op)\n\t}\n\n\tswitch {\n\tcase isFilePath(from) && isFilePath(to):\n\t\t// copying from on file to another file is not supported.\n\t\treturn fmt.Errorf(\"ambiguity detected. Only from or to can be a file. Use cp to copy between files\")\n\tcase s.Store.Exists(ctx, from) && s.Store.Exists(ctx, to):\n\t\t// copying from one secret to another secret is not supported.\n\t\treturn fmt.Errorf(\"ambiguity detected. Either from or to must be a file. Use gopass cp to copy between secrets\")\n\tcase isFilePath(from) && !isFilePath(to):\n\t\tif s.isInStore(from) {\n\t\t\tout.Warningf(ctx, \"Ambiguity detected. Source %q is in the store. Use --force if intended\", from)\n\t\t\tif !c.Bool(\"force\") {\n\t\t\t\treturn fmt.Errorf(\"ambiguity detected. Source is in the store\")\n\t\t\t}\n\t\t}\n\n\t\treturn s.binaryCopyFromFileToStore(ctx, from, to, deleteSource)\n\tcase !isFilePath(from):\n\t\tif s.isInStore(to) {\n\t\t\tout.Warningf(ctx, \"Ambiguity detected. Destination %q is in the store. Use --force if intended\", to)\n\t\t\tif !c.Bool(\"force\") {\n\t\t\t\treturn fmt.Errorf(\"ambiguity detected. Destination is in the store\")\n\t\t\t}\n\t\t}\n\n\t\treturn s.binaryCopyFromStoreToFile(ctx, from, to, deleteSource)\n\tdefault:\n\t\treturn fmt.Errorf(\"ambiguity detected. Unhandled case. Please report a bug\")\n\t}\n}\n\nfunc (s *Action) binaryCopyFromFileToStore(ctx context.Context, from, to string, deleteSource bool) error {\n\t// if the source is a file the destination must not to avoid ambiguities.\n\t// if necessary this can be resolved by using a absolute path for the file\n\t// and a relative one for the secret.\n\n\t// copy from FS to store.\n\tbuf, err := os.ReadFile(from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read file from %q: %w\", from, err)\n\t}\n\n\tsec, err := secFromBytes(to, from, buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse secret from input: %w\", err)\n\t}\n\tif err := s.Store.Set(\n\t\tctxutil.WithCommitMessage(ctx, fmt.Sprintf(\"Copied data from %s to %s\", from, to)), to, sec); err != nil {\n\t\treturn fmt.Errorf(\"failed to save buffer to store: %w\", err)\n\t}\n\n\tif !deleteSource {\n\t\treturn nil\n\t}\n\n\t// it's important that we return if the validation fails, because\n\t// in that case we don't want to shred our (only) copy of this data!.\n\tif err := s.binaryValidate(ctx, buf, to); err != nil {\n\t\treturn fmt.Errorf(\"failed to validate written data: %w\", err)\n\t}\n\tif err := fsutil.Shred(from, 8); err != nil {\n\t\treturn fmt.Errorf(\"failed to shred data: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) binaryCopyFromStoreToFile(ctx context.Context, from, to string, deleteSource bool) error {\n\t// if the source is no file we assume it's a secret and to is a filename\n\t// (which may already exist or not).\n\n\t// copy from store to FS.\n\tbuf, err := s.binaryGet(ctx, from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read data from %q: %w\", from, err)\n\t}\n\tif err := os.WriteFile(to, buf, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write data to %q: %w\", to, err)\n\t}\n\n\tif !deleteSource {\n\t\treturn nil\n\t}\n\n\t// as before: if validation of the written data fails, we MUST NOT\n\t// delete the (only) source.\n\tif err := s.binaryValidate(ctx, buf, from); err != nil {\n\t\treturn fmt.Errorf(\"failed to validate the written data: %w\", err)\n\t}\n\tif err := s.Store.Delete(ctx, from); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete %q from the store: %w\", from, err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) binaryValidate(ctx context.Context, buf []byte, name string) error {\n\th := sha256.New()\n\t_, _ = h.Write(buf)\n\tfileSum := hex.EncodeToString(h.Sum(nil))\n\th.Reset()\n\n\tdebug.Log(\"in: %s - %q\", fileSum, string(buf))\n\n\tvar err error\n\tbuf, err = s.binaryGet(ctx, name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read %q from the store: %w\", name, err)\n\t}\n\t_, _ = h.Write(buf)\n\tstoreSum := hex.EncodeToString(h.Sum(nil))\n\n\tdebug.Log(\"store: %s - %q\", storeSum, string(buf))\n\n\tif fileSum != storeSum {\n\t\treturn fmt.Errorf(\"hashsum mismatch (file: %s, store: %s)\", fileSum, storeSum)\n\t}\n\n\treturn nil\n}\n\nfunc isBase64Encoded(sec gopass.Secret) bool {\n\tfor _, k := range []string{\n\t\t\"Content-Transfer-Encoding\",\n\t\t\"content-transfer-encoding\",\n\t} {\n\t\tcte, _ := sec.Get(k)\n\t\tif strings.ToLower(cte) == \"base64\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (s *Action) binaryGet(ctx context.Context, name string) ([]byte, error) {\n\tsec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read %q from the store: %w\", name, err)\n\t}\n\n\tif !isBase64Encoded(sec) {\n\t\tdebug.Log(\"handling non-base64 secret\")\n\n\t\t// need to use sec.Bytes() otherwise the first line is missing.\n\t\treturn sec.Bytes(), nil\n\t}\n\n\tdebug.Log(\"decoding Base64 encoded secret\")\n\tbody := sec.Body()\n\tbuf, err := base64.StdEncoding.DecodeString(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encode to base64: %w\", err)\n\t}\n\n\tdebug.Log(\"decoded %d Base64 chars into %d bytes\", len(body), len(buf))\n\tif len(buf) < 1 {\n\t\tdebug.Log(\"body:\\n%v\", body)\n\t}\n\n\treturn buf, nil\n}\n\n// Sum decodes binary content and computes the SHA256 checksum.\n// It prints the checksum to stdout.\nfunc (s *Action) Sum(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s sha256 name\", c.App.Name)\n\t}\n\n\tbuf, err := s.binaryGet(ctx, name)\n\tif err != nil {\n\t\treturn exit.Error(exit.Decrypt, err, \"failed to read secret: %s\", err)\n\t}\n\n\th := sha256.New()\n\t_, _ = h.Write(buf)\n\tout.Printf(ctx, \"%x\", h.Sum(nil))\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/binary_test.go",
    "content": "package action\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBinary(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.Error(t, act.Cat(gptest.CliCtx(ctx, t)))\n\trequire.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t)))\n\trequire.Error(t, act.BinaryMove(gptest.CliCtx(ctx, t)))\n\trequire.Error(t, act.Sum(gptest.CliCtx(ctx, t)))\n}\n\nfunc TestBinaryCat(t *testing.T) {\n\ttSize := 1024\n\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tinfile := filepath.Join(u.Dir, \"input.txt\")\n\twriteBinfile(t, infile, tSize)\n\n\tt.Run(\"populate store\", func(t *testing.T) {\n\t\trequire.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, \"bar\", true))\n\t})\n\n\tt.Run(\"binary cat bar\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Cat(gptest.CliCtx(ctx, t, \"bar\")))\n\t})\n\n\tstdinfile := filepath.Join(u.Dir, \"stdin\")\n\tt.Run(\"binary cat baz from stdin\", func(t *testing.T) {\n\t\twriteBinfile(t, stdinfile, tSize)\n\n\t\tfd, err := os.Open(stdinfile)\n\t\trequire.NoError(t, err)\n\t\tbinstdin = fd\n\t\tdefer func() {\n\t\t\tbinstdin = os.Stdin\n\t\t\t_ = fd.Close()\n\t\t}()\n\n\t\trequire.NoError(t, act.Cat(gptest.CliCtx(ctx, t, \"baz\")))\n\t})\n\n\tt.Run(\"compare output\", func(t *testing.T) {\n\t\tbuf, err := os.ReadFile(stdinfile)\n\t\trequire.NoError(t, err)\n\t\tsec, err := act.binaryGet(ctx, \"baz\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, string(buf), string(sec))\n\t})\n}\n\nfunc TestBinaryCatSizes(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tfor tSize := 1024; tSize < bufio.MaxScanTokenSize*2; tSize += 1024 {\n\t\t// cat stdinfile | gopass cat baz\n\t\tstdinfile := filepath.Join(u.Dir, \"stdin\")\n\t\twriteBinfile(t, stdinfile, tSize)\n\n\t\tfd, err := os.Open(stdinfile)\n\t\trequire.NoError(t, err)\n\n\t\tcatFn := func() {\n\t\t\tbinstdin = fd\n\t\t\tdefer func() {\n\t\t\t\tbinstdin = os.Stdin\n\t\t\t\t_ = fd.Close()\n\t\t\t}()\n\n\t\t\trequire.NoError(t, act.Cat(gptest.CliCtx(ctx, t, \"baz\")))\n\t\t}\n\t\tcatFn()\n\n\t\t// gopass cat baz and compare output with input, they should match\n\t\tbuf, err := os.ReadFile(stdinfile)\n\t\trequire.NoError(t, err)\n\t\tsec, err := act.binaryGet(ctx, \"baz\")\n\t\trequire.NoError(t, err)\n\n\t\tif string(buf) != string(sec) {\n\t\t\tt.Fatalf(\"Input and output mismatch at tSize %d\", tSize)\n\t\t}\n\t\tt.Logf(\"Input and Output match at tSize %d\", tSize)\n\t}\n}\n\nfunc TestBinaryCopy(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tt.Run(\"copy textfile\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tinfile := filepath.Join(u.Dir, \"input.txt\")\n\t\trequire.NoError(t, os.WriteFile(infile, []byte(\"0xDEADBEEF\\n\"), 0o644))\n\t\trequire.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, \"txt\", true))\n\t})\n\n\tinfile := filepath.Join(u.Dir, \"input.raw\")\n\toutfile := filepath.Join(u.Dir, \"output.raw\")\n\tt.Run(\"copy binary file\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\twriteBinfile(t, infile, 1024)\n\t\trequire.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, \"bar\", true))\n\t})\n\n\tt.Run(\"binary copy bar tempdir/bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.BinaryCopy(gptest.CliCtx(ctx, t, \"bar\", outfile)))\n\t})\n\n\tt.Run(\"binary copy tempdir/bar tempdir/bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t, outfile, outfile)))\n\t})\n\n\tt.Run(\"binary copy bar bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t, \"bar\", \"bar\")))\n\t})\n\n\tt.Run(\"binary move tempdir/bar bar2\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.BinaryMove(gptest.CliCtx(ctx, t, outfile, \"bar2\")))\n\t})\n\n\tt.Run(\"binary move bar2 tempdir/bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.BinaryMove(gptest.CliCtx(ctx, t, \"bar2\", outfile)))\n\t})\n}\n\nfunc TestBinarySum(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tinfile := filepath.Join(u.Dir, \"input.raw\")\n\n\tt.Run(\"populate store\", func(t *testing.T) {\n\t\twriteBinfile(t, infile, 1024)\n\t\trequire.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, \"bar\", true))\n\t})\n\n\tt.Run(\"binary sum bar\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Sum(gptest.CliCtx(ctx, t, \"bar\")))\n\t\tbuf.Reset()\n\t})\n}\n\nfunc TestBinaryGet(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tdata := []byte(\"1\\n2\\n3\\n\")\n\trequire.NoError(t, act.insertStdin(ctx, \"x\", data, false))\n\n\tout, err := act.binaryGet(ctx, \"x\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, data, out)\n}\n\nfunc writeBinfile(t *testing.T, fn string, size int) {\n\tt.Helper()\n\n\t// tests should be predicable\n\tlr := rand.New(rand.NewSource(42))\n\n\tbuf := make([]byte, size)\n\tn, err := lr.Read(buf)\n\trequire.NoError(t, err)\n\tassert.Equal(t, size, n)\n\trequire.NoError(t, os.WriteFile(fn, buf, 0o644))\n}\n"
  },
  {
    "path": "internal/action/clihelper.go",
    "content": "package action\n\nimport (\n\t\"strings\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype argList []string\n\nfunc (a argList) Get(n int) string {\n\tif len(a) > n {\n\t\treturn a[n]\n\t}\n\n\treturn \"\"\n}\n\nfunc parseArgs(c *cli.Context) (argList, map[string]string) {\n\targs := make(argList, 0, c.Args().Len())\n\tkvps := make(map[string]string, c.Args().Len())\n\tif c.Args().Len() == 1 {\n\t\t// If there is only one arg, assume it is\n\t\t// the secret name, so don't attempt to\n\t\t// parse into args and kvps\n\t\targs = append(args, c.Args().Get(0))\n\n\t\treturn args, kvps\n\t}\nOUTER:\n\tfor _, arg := range c.Args().Slice() {\n\t\tfor _, sep := range []string{\":\", \"=\"} {\n\t\t\tif !strings.Contains(arg, sep) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp := strings.Split(arg, sep)\n\t\t\tif len(p) < 2 {\n\t\t\t\targs = append(args, arg)\n\n\t\t\t\tcontinue OUTER\n\t\t\t}\n\t\t\tkey := p[0]\n\t\t\tkvps[key] = strings.Join(p[1:], \":\")\n\n\t\t\tcontinue OUTER\n\t\t}\n\t\targs = append(args, arg)\n\t}\n\n\treturn args, kvps\n}\n"
  },
  {
    "path": "internal/action/clihelper_test.go",
    "content": "package action\n\nimport (\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestParseArgs(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname   string\n\t\targIn  []string\n\t\targOut argList\n\t\tkvOut  map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"no args\",\n\t\t},\n\t\t{\n\t\t\tname:   \"secret\",\n\t\t\targIn:  []string{\"foo/bar\"},\n\t\t\targOut: argList{\"foo/bar\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"secret with colon\",\n\t\t\targIn:  []string{\"foo/bar:test\"},\n\t\t\targOut: argList{\"foo/bar:test\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with key\",\n\t\t\targIn:  []string{\"foo/bar\", \"baz\"},\n\t\t\targOut: argList{\"foo/bar\", \"baz\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with k/v (=)\",\n\t\t\targIn:  []string{\"foo/bar\", \"baz=bam\"},\n\t\t\targOut: argList{\"foo/bar\"},\n\t\t\tkvOut:  map[string]string{\"baz\": \"bam\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with k/v (:)\",\n\t\t\targIn:  []string{\"foo/bar\", \"baz:bam\"},\n\t\t\targOut: argList{\"foo/bar\"},\n\t\t\tkvOut:  map[string]string{\"baz\": \"bam\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with k/v (mixed)\",\n\t\t\targIn:  []string{\"foo/bar\", \"baz:bam\", \"foo=zen\"},\n\t\t\targOut: argList{\"foo/bar\"},\n\t\t\tkvOut:  map[string]string{\"baz\": \"bam\", \"foo\": \"zen\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with k/v (mixed order)\",\n\t\t\targIn:  []string{\"foo:bar\", \"foo/bar\", \"baz:bam\"},\n\t\t\targOut: argList{\"foo/bar\"},\n\t\t\tkvOut:  map[string]string{\"foo\": \"bar\", \"baz\": \"bam\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"with k/v (=) key and length\",\n\t\t\targIn:  []string{\"foo/bar\", \"baz=bam\", \"baz\", \"42\"},\n\t\t\targOut: argList{\"foo/bar\", \"baz\", \"42\"},\n\t\t\tkvOut:  map[string]string{\"baz\": \"bam\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.argOut == nil {\n\t\t\t\ttc.argOut = argList{}\n\t\t\t}\n\n\t\t\tif tc.kvOut == nil {\n\t\t\t\ttc.kvOut = map[string]string{}\n\t\t\t}\n\n\t\t\tapp := cli.NewApp()\n\t\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\t\trequire.NoError(t, fs.Parse(tc.argIn), tc.name)\n\t\t\targs, kvps := parseArgs(cli.NewContext(app, fs, nil))\n\t\t\tassert.Equal(t, tc.argOut, args, tc.name)\n\t\t\tassert.Equal(t, tc.kvOut, kvps, tc.name)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/action/clone.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store/root\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Clone will fetch and mount a new password store from a git repo.\n// It can also be used to clone a new password store to a submount.\nfunc (s *Action) Clone(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.IsSet(\"crypto\") {\n\t\tvar err error\n\t\tctx, err = backend.WithCryptoBackendString(ctx, c.String(\"crypto\"))\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to set crypto backend: %s\", err)\n\t\t}\n\t}\n\n\tif c.IsSet(\"storage\") {\n\t\tvar err error\n\t\tctx, err = backend.WithStorageBackendString(ctx, c.String(\"storage\"))\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to set storage backend: %s\", err)\n\t\t}\n\t}\n\n\tpath := c.String(\"path\")\n\n\tif c.Args().Len() < 1 {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s clone repo [mount]\", s.Name)\n\t}\n\n\t// gopass clone [--crypto=foo] [--path=/some/store] git://foo/bar team0.\n\trepo := c.Args().Get(0)\n\tmount := \"\"\n\tif c.Args().Len() > 1 {\n\t\tmount = c.Args().Get(1)\n\t}\n\n\tout.Printf(ctx, logo)\n\tout.Printf(ctx, \"🌟 Welcome to gopass!\")\n\tout.Printf(ctx, \"🌟 Cloning an existing password store from %q ...\", repo)\n\n\tif name := termio.DetectName(ctx, c); name != \"\" {\n\t\tctx = ctxutil.WithUsername(ctx, name)\n\t}\n\tif email := termio.DetectEmail(ctx, c); email != \"\" {\n\t\tctx = ctxutil.WithEmail(ctx, email)\n\t}\n\n\t// age: only native keys\n\t// \"[ssh] types should only be used for compatibility with existing keys,\n\t// and native X25519 keys should be preferred otherwise.\"\n\t// https://pkg.go.dev/filippo.io/age@v1.0.0/agessh#pkg-overview.\n\tctx = age.WithOnlyNative(ctx, true)\n\t// gpg: only trusted keys\n\t// only list \"usable\" / properly trused and signed GPG keys by requesting\n\t// always trust is false. Ignored for other backends. See\n\t// https://www.gnupg.org/gph/en/manual/r1554.html.\n\tctx = gpg.WithAlwaysTrust(ctx, false)\n\n\tif err := s.clone(ctx, repo, mount, path); err != nil {\n\t\treturn err\n\t}\n\n\t// need to re-initialize the root store or it's already initialized\n\t// and won't properly set up crypto according to our context.\n\ts.Store = root.New(s.cfg)\n\tinited, err := s.Store.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to check store status: %s\", err)\n\t}\n\n\tif !inited {\n\t\tout.Errorf(ctx, \"Failed to clone\")\n\n\t\treturn nil\n\t}\n\n\tif !c.Bool(\"check-keys\") {\n\t\treturn nil\n\t}\n\n\treturn s.cloneCheckDecryptionKeys(ctx, mount)\n}\n\n// storageBackendOrDefault will return a storage backend that can be clone,\n// i.e. specifically backend.FS can't be cloned.\nfunc storageBackendOrDefault(ctx context.Context, repo string) backend.StorageBackend {\n\t// first try to get it from the context.\n\tif be := backend.GetStorageBackend(ctx); be != backend.FS {\n\t\treturn be\n\t}\n\n\tif strings.HasSuffix(repo, \".fossil\") {\n\t\treturn backend.FossilFS\n\t}\n\n\tif strings.HasSuffix(repo, \".git\") {\n\t\treturn backend.GitFS\n\t}\n\n\tdebug.Log(\"falling back to the default storage backend for clone (GitFS)\")\n\n\treturn backend.GitFS\n}\n\nfunc (s *Action) clone(ctx context.Context, repo, mount, path string) error {\n\tif path == \"\" {\n\t\tpath = config.PwStoreDir(mount)\n\t}\n\n\tinited, err := s.Store.IsInitialized(ctxutil.WithGitInit(ctx, false))\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialized stores: %s\", err)\n\t}\n\n\tif mount == \"\" && inited {\n\t\treturn exit.Error(exit.AlreadyInitialized, nil, \"Can not clone %s to the root store, as this store is already initialized. Please try cloning to a submount: `%s clone %s sub`\", repo, s.Name, repo)\n\t}\n\n\t// make sure the parent directory exists.\n\tif parentPath := filepath.Dir(path); !fsutil.IsDir(parentPath) {\n\t\tif err := os.MkdirAll(parentPath, 0o700); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to create parent directory for clone: %s\", err)\n\t\t}\n\t}\n\n\t// clone repo.\n\tsb := storageBackendOrDefault(ctx, repo)\n\tout.Noticef(ctx, \"Cloning %s repository %q to %q ...\", sb, repo, path)\n\t_, err = backend.Clone(ctx, sb, repo, path)\n\tif err != nil {\n\t\treturn exit.Error(exit.Git, err, \"failed to clone repo %q to %q: %s\", repo, path, err)\n\t}\n\n\t// add mount.\n\tdebug.Log(\"Mounting cloned repo %q at %q\", path, mount)\n\tif err := s.cloneAddMount(ctx, mount, path); err != nil {\n\t\treturn err\n\t}\n\n\t// try to init repo config.\n\tout.Noticef(ctx, \"Configuring %s repository ...\", sb)\n\n\t// ask for config values.\n\tusername, email, err := s.cloneGetGitConfig(ctx, mount)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// initialize repo config.\n\tif err := s.Store.RCSInitConfig(ctx, mount, username, email); err != nil {\n\t\tdebug.Log(\"Stacktrace: %+v\\n\", err)\n\t\tout.Errorf(ctx, \"Failed to configure %s: %s\", sb, err)\n\t}\n\n\tif mount != \"\" {\n\t\tmount = \" \" + mount\n\t}\n\n\tout.Printf(ctx, \"Your password store is ready to use! Have a look around: `%s list%s`\\n\", s.Name, mount)\n\n\treturn nil\n}\n\nfunc (s *Action) cloneCheckDecryptionKeys(ctx context.Context, mount string) error {\n\tcrypto := s.getCryptoFor(ctx, mount)\n\tif crypto == nil {\n\t\treturn fmt.Errorf(\"can not continue without crypto\")\n\t}\n\tdebug.Log(\"Crypto Backend initialized as: %s\", crypto.Name())\n\n\t// check for existing GPG/Age keypairs (private/secret keys). We need at least\n\t// one useable key pair. If none exists try to create one.\n\tif !s.initHasUseablePrivateKeys(ctx, crypto) {\n\t\tout.Printf(ctx, \"🔐 No useable cryptographic keys. Generating new key pair\")\n\t\tif crypto.Name() == \"gpgcli\" {\n\t\t\tout.Printf(ctx, \"🕰 Key generation may take up to a few minutes\")\n\t\t}\n\t\tif err := s.initGenerateIdentity(ctx, crypto, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create new private key: %w\", err)\n\t\t}\n\t\tout.Printf(ctx, \"🔐 Cryptographic keys generated\")\n\t}\n\n\tdebug.Log(\"We have useable private keys\")\n\n\trecpSet := set.New(s.Store.ListRecipients(ctx, mount)...)\n\tids, err := crypto.ListIdentities(ctx)\n\tif err != nil {\n\t\tout.Warningf(ctx, \"Failed to check decryption keys: %s\", err)\n\n\t\treturn nil\n\t}\n\n\tidSet := set.New(ids...)\n\t// Check whether any of our usable keys are in recpSet\n\tif _, found := recpSet.Choose(idSet.Contains); found {\n\t\tout.Noticef(ctx, \"Found valid decryption keys. You can now decrypt your passwords.\")\n\n\t\treturn nil\n\t}\n\n\tvar exported bool\n\tif sub, err := s.Store.GetSubStore(mount); err == nil {\n\t\tdebug.Log(\"exporting public keys: %v\", idSet.Elements())\n\t\texported, err = sub.UpdateExportedPublicKeys(ctx)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to export missing public keys: %w\", err)\n\t\t}\n\t} else {\n\t\tdebug.Log(\"failed to get sub store: %s\", err)\n\t}\n\n\tout.Noticef(ctx, \"Please ask the owner of the password store to add one of your keys: %s\", strings.Join(idSet.Elements(), \", \"))\n\tif exported {\n\t\tout.Noticef(ctx, \"The missing keys were exported to the password store. Run `gopass sync` to push them.\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) cloneAddMount(ctx context.Context, mount, path string) error {\n\tif mount == \"\" {\n\t\treturn nil\n\t}\n\n\tinited, err := s.Store.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialize store: %s\", err)\n\t}\n\n\tif !inited {\n\t\treturn exit.Error(exit.NotInitialized, nil, \"Root-Store is not initialized. Clone or init root store first\")\n\t}\n\n\tif err := s.Store.AddMount(ctx, mount, path); err != nil {\n\t\treturn exit.Error(exit.Mount, err, \"Failed to add mount: %s\", err)\n\t}\n\tout.Printf(ctx, \"Mounted password store %s at mount point `%s` ...\", path, mount)\n\n\treturn nil\n}\n\nfunc (s *Action) cloneGetGitConfig(ctx context.Context, name string) (string, string, error) {\n\tout.Printf(ctx, \"🎩 Gathering information for the git repository ...\")\n\t// for convenience, set defaults to user-selected values from available private keys.\n\t// NB: discarding returned error since this is merely a best-effort look-up for convenience.\n\tusername, email, _ := cui.AskForGitConfigUser(ctx, s.Store.Crypto(ctx, name))\n\tif username == \"\" {\n\t\tusername = termio.DetectName(ctx, nil)\n\t\tvar err error\n\t\tusername, err = termio.AskForString(ctx, \"🚶 What is your name?\", username)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", exit.Error(exit.IO, err, \"Failed to read user input: %s\", err)\n\t\t}\n\t}\n\n\tif email == \"\" {\n\t\temail = termio.DetectEmail(ctx, nil)\n\t\tvar err error\n\t\temail, err = termio.AskForString(ctx, \"📧 What is your email?\", email)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", exit.Error(exit.IO, err, \"Failed to read user input: %s\", err)\n\t\t}\n\t}\n\n\treturn username, email, nil\n}\n"
  },
  {
    "path": "internal/action/clone_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\tgit \"github.com/gopasspw/gopass/internal/backend/storage/gitfs\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// aGitRepo creates and initializes a small git repo.\nfunc aGitRepo(ctx context.Context, t *testing.T, u *gptest.Unit, name string) string {\n\tt.Helper()\n\n\tgd := filepath.Join(u.Dir, name)\n\trequire.NoError(t, os.MkdirAll(gd, 0o700))\n\n\t_, err := git.New(gd)\n\trequire.Error(t, err)\n\n\tidf := filepath.Join(gd, \".gpg-id\")\n\trequire.NoError(t, os.WriteFile(idf, []byte(\"0xDEADBEEF\"), 0o600))\n\n\tgr, err := git.Init(ctx, gd, \"Nobody\", \"foo.bar@example.org\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, gr)\n\n\treturn gd\n}\n\nfunc TestClone(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"no args\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tc := gptest.CliCtx(ctx, t)\n\t\trequire.Error(t, act.Clone(c))\n\t})\n\n\tt.Run(\"clone to initialized store\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.clone(ctx, \"/tmp/non-existing-repo.git\", \"\", filepath.Join(u.Dir, \"store\")))\n\t})\n\n\tt.Run(\"clone to mount\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tgd := aGitRepo(ctx, t, u, \"other-repo\")\n\t\trequire.NoError(t, act.clone(ctx, gd, \"gd\", filepath.Join(u.Dir, \"mount\")))\n\t})\n}\n\nfunc TestCloneBackendIsStoredForMount(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tcfg := config.NewInMemory()\n\trequire.NoError(t, cfg.SetPath(u.StoreDir(\"\")))\n\n\tact, err := newAction(cfg, semver.Version{}, false)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tc := gptest.CliCtx(ctx, t)\n\trequire.NoError(t, act.IsInitialized(c))\n\n\trepo := aGitRepo(ctx, t, u, \"my-project\")\n\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"check-keys\": \"false\"}, repo, \"the-project\")\n\trequire.NoError(t, act.Clone(c))\n\n\trequire.Contains(t, act.cfg.Mounts(), \"the-project\")\n}\n\nfunc TestCloneGetGitConfig(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tr1 := gptest.UnsetVars(termio.NameVars...)\n\tdefer r1()\n\tr2 := gptest.UnsetVars(termio.EmailVars...)\n\tdefer r2()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tname, email, err := act.cloneGetGitConfig(ctx, \"foobar\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0xDEADBEEF\", name)\n\tassert.Equal(t, \"0xDEADBEEF\", email)\n}\n\nfunc TestCloneCheckDecryptionKeys(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tcfg := config.NewInMemory()\n\trequire.NoError(t, cfg.SetPath(u.StoreDir(\"\")))\n\n\tact, err := newAction(cfg, semver.Version{}, false)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tc := gptest.CliCtx(ctx, t)\n\trequire.NoError(t, act.IsInitialized(c))\n\n\trepo := aGitRepo(ctx, t, u, \"my-project\")\n\n\tif runtime.GOOS != \"linux\" {\n\t\tt.Skip(\"TODO: not working on non-linux builders, yet\")\n\t}\n\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"check-keys\": \"true\"}, repo, \"the-project\")\n\trequire.NoError(t, act.Clone(c))\n\n\trequire.Contains(t, act.cfg.Mounts(), \"the-project\")\n}\n"
  },
  {
    "path": "internal/action/commands.go",
    "content": "package action\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// ShowFlags returns the flags for the show command.\n// Exported to re-use in main for the default command.\nfunc ShowFlags() []cli.Flag {\n\treturn []cli.Flag{\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"yes\",\n\t\t\tAliases: []string{\"y\"},\n\t\t\tUsage:   \"Always answer yes to yes/no questions\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"clip\",\n\t\t\tAliases: []string{\"c\"},\n\t\t\tUsage:   \"Copy the password value into the clipboard\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"alsoclip\",\n\t\t\tAliases: []string{\"C\"},\n\t\t\tUsage:   \"Copy the password and show everything\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  \"qr\",\n\t\t\tUsage: \"Print the password as a QR Code\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  \"qrbody\",\n\t\t\tUsage: \"Print the body as a QR Code\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"unsafe\",\n\t\t\tAliases: []string{\"u\", \"force\", \"f\"},\n\t\t\tUsage:   \"Display unsafe content (e.g. the password) even if safecontent is enabled\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"safe\",\n\t\t\tAliases: []string{\"s\"},\n\t\t\tUsage:   \"Do not display unsafe content (e.g. the password) even if safecontent is disabled\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"password\",\n\t\t\tAliases: []string{\"o\"},\n\t\t\tUsage:   \"Display only the password. Takes precedence over all other flags.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    \"revision\",\n\t\t\tAliases: []string{\"r\"},\n\t\t\tUsage:   \"Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    \"noparsing\",\n\t\t\tAliases: []string{\"n\"},\n\t\t\tUsage:   \"Do not parse the output.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  \"nosync\",\n\t\t\tUsage: \"Disable auto-sync\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  \"chars\",\n\t\t\tUsage: \"Print specific characters from the secret\",\n\t\t},\n\t}\n}\n\n// GetCommands returns the cli commands exported by this module.\n// It also includes any commands provided by the crypto and storage backends.\nfunc (s *Action) GetCommands() []*cli.Command {\n\tcmds := []*cli.Command{\n\t\t{\n\t\t\tName:        \"alias\",\n\t\t\tUsage:       \"Print domain aliases\",\n\t\t\tDescription: \"Print defined domain aliases.\",\n\t\t\tAction:      s.AliasesPrint,\n\t\t},\n\t\t{\n\t\t\tName:      \"audit\",\n\t\t\tUsage:     \"Decrypt all secrets and scan for weak or leaked passwords\",\n\t\t\tArgsUsage: \"[filter]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command decrypts all secrets and checks for common flaws and (optionally) \" +\n\t\t\t\t\"against a list of previously leaked passwords.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.Audit,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"format\",\n\t\t\t\t\tUsage: \"Output format. text, csv or html. Default: text\",\n\t\t\t\t\tValue: \"text\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"output-file\",\n\t\t\t\t\tAliases: []string{\"o\"},\n\t\t\t\t\tUsage:   \"Output filename. Used for csv and html\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"template\",\n\t\t\t\t\tUsage: \"HTML template. If not set use the built-in default.\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"full\",\n\t\t\t\t\tUsage: \"Print full details of all findings. Default: false\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"summary\",\n\t\t\t\t\tUsage: \"Print a summary of the audit results. Default: true (print summary)\",\n\t\t\t\t\tValue: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"cat\",\n\t\t\tUsage:     \"Decode and print content of a binary secret to stdout, or encode and insert from stdin\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command is similar to the way cat works on the command line. \" +\n\t\t\t\t\"It can either be used to retrieve the decoded content of a secret \" +\n\t\t\t\t\"similar to 'cat file' or vice versa to encode the content from STDIN \" +\n\t\t\t\t\"to a secret.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Cat,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:      \"clone\",\n\t\t\tUsage:     \"Clone a password store from a git repository\",\n\t\t\tArgsUsage: \"[git-repo] [mount-point]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command clones an existing password store from a git remote to \" +\n\t\t\t\t\"a local password store. Can be either used to initialize a new root store \" +\n\t\t\t\t\"or to add a new mounted sub-store. \" +\n\t\t\t\t\"\" +\n\t\t\t\t\"Needs at least one argument (git URL) to clone from. \" +\n\t\t\t\t\"Accepts a second argument (mount location) to clone and mount a sub-store, e.g. \" +\n\t\t\t\t\"'gopass clone git@example.com/store.git foo/bar'\",\n\t\t\tAction: s.Clone,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"path\",\n\t\t\t\t\tUsage: \"Path to clone the repo to\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"crypto\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select crypto backend %v\", backend.CryptoRegistry.BackendNames()),\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"storage\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select storage backend %v\", set.Filter(backend.StorageRegistry.BackendNames(), \"fs\")),\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"check-keys\",\n\t\t\t\t\tUsage: \"Check for valid decryption keys. Generate new keys if none are found.\",\n\t\t\t\t\tValue: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"config\",\n\t\t\tUsage:     \"Display and edit the configuration file\",\n\t\t\tArgsUsage: \"[key [value]]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command allows for easy printing and editing of the configuration. \" +\n\t\t\t\t\"Without argument, the entire config is printed. \" +\n\t\t\t\t\"With a single argument, a single key can be printed. \" +\n\t\t\t\t\"With two arguments a setting specified by key can be set to value.\",\n\t\t\tAction: s.Config,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"store\",\n\t\t\t\t\tUsage: \"Set options to a specific store\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tBashComplete: s.ConfigComplete,\n\t\t},\n\t\t{\n\t\t\tName:        \"convert\",\n\t\t\tUsage:       \"Convert a store to different backends\",\n\t\t\tDescription: \"Convert a store to a different set of backends\",\n\t\t\tAction:      s.Convert,\n\t\t\tBefore:      s.IsInitialized,\n\t\t\tHidden:      true,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"store\",\n\t\t\t\t\tUsage: \"Specify which store to convert\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"move\",\n\t\t\t\t\tValue: true,\n\t\t\t\t\tUsage: \"Replace store?\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"crypto\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Which crypto backend? %v\", backend.CryptoRegistry.BackendNames()),\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"storage\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Which storage backend? %v\", backend.StorageRegistry.BackendNames()),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"copy\",\n\t\t\tAliases:   []string{\"cp\"},\n\t\t\tUsage:     \"Copy secrets from one location to another\",\n\t\t\tArgsUsage: \"[from] [to]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command copies an existing secret in the store to another location. \" +\n\t\t\t\t\"This also works across different sub-stores. If the source is a directory it will \" +\n\t\t\t\t\"automatically copy recursively. In that case, the source directory is re-created \" +\n\t\t\t\t\"at the destination if no trailing slash is found, otherwise the contents are \" +\n\t\t\t\t\"flattened (similar to rsync).\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Copy,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Force to copy the secret and overwrite existing one\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"commit-message\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"create\",\n\t\t\tAliases:   []string{\"new\"},\n\t\t\tUsage:     \"Easy creation of new secrets\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command starts a wizard to aid in creation of new secrets.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.Create,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"store\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Which store to use\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Force path selection\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"delete\",\n\t\t\tUsage:     \"Remove one or many secrets from the store\",\n\t\t\tArgsUsage: \"[secret [key]]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command removes secrets. It can work recursively on folders. \" +\n\t\t\t\t\"Recursing across stores is purposefully not supported.\",\n\t\t\tAliases:      []string{\"remove\", \"rm\"},\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Delete,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"recursive\",\n\t\t\t\t\tAliases: []string{\"r\"},\n\t\t\t\t\tUsage:   \"Recursive delete files and folders\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Force to delete the secret\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"commit-message\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"edit\",\n\t\t\tUsage:     \"Edit new or existing secrets\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Use this command to insert a new secret or edit an existing one using \" +\n\t\t\t\t\"your $EDITOR. It will attempt to create a secure temporary directory \" +\n\t\t\t\t\"for storing your secret while the editor is accessing it. Please make \" +\n\t\t\t\t\"sure your editor doesn't leak sensitive data to other locations while \" +\n\t\t\t\t\"editing.\\n\" +\n\t\t\t\t\"Note: If $EDITOR is not set we will try 'editor'. If that's not available \" +\n\t\t\t\t\"either we fall back to 'vi'. Consider using 'update-alternatives --config editor \" +\n\t\t\t\t\"to change the defaults.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Edit,\n\t\t\tAliases:      []string{\"set\"},\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"editor\",\n\t\t\t\t\tAliases: []string{\"e\"},\n\t\t\t\t\tUsage:   \"Use this editor binary\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"create\",\n\t\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\t\tUsage:   \"Create a new secret if none found\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"commit-message\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:         \"env\",\n\t\t\tUsage:        \"Run a subprocess with a pre-populated environment\",\n\t\t\tArgsUsage:    \"[secret] [command and args...]\",\n\t\t\tDescription:  \"This command runs a sub process with the environment populated from the keys of a secret.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Env,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"keep-case\",\n\t\t\t\t\tAliases: []string{\"kc\"},\n\t\t\t\t\tValue:   false,\n\t\t\t\t\tUsage:   \"Do not capitalize the environment variable and instead retain the original capitalization\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"find\",\n\t\t\tUsage:     \"Search for secrets\",\n\t\t\tArgsUsage: \"<pattern>\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command will first attempt a simple pattern match on the name of the \" +\n\t\t\t\t\"secret.  If there is an exact match it will be shown directly; if there are \" +\n\t\t\t\t\"multiple matches, a selection will be shown.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Find,\n\t\t\tAliases:      []string{\"search\"},\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"unsafe\",\n\t\t\t\t\tAliases: []string{\"u\", \"force\", \"f\"},\n\t\t\t\t\tUsage:   \"In the case of an exact match, display the password even if safecontent is enabled\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"regex\",\n\t\t\t\t\tAliases: []string{\"r\"},\n\t\t\t\t\tUsage:   \"Interpret pattern as regular expression\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"fsck\",\n\t\t\tUsage:     \"Check store integrity, clean up artifacts and possibly re-write secrets\",\n\t\t\tArgsUsage: \"[filter]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Check the integrity of the given sub-store or all stores if none are specified. \" +\n\t\t\t\t\"Will automatically fix all issues found, i.e. it will change permissions, re-write secrets and remove outdated configs.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Fsck,\n\t\t\tBashComplete: s.MountsComplete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"decrypt\",\n\t\t\t\t\tUsage: \"Decrypt and reencrypt during fsck.\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"store\",\n\t\t\t\t\tUsage: \"Limit fsck to this mount point\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"fscopy\",\n\t\t\tUsage:     \"Copy files from or to the password store\",\n\t\t\tArgsUsage: \"[from] [to]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command either reads a file from the filesystem and writes the \" +\n\t\t\t\t\"encoded and encrypted version in the store or it decrypts and decodes \" +\n\t\t\t\t\"a secret and writes the result to a file. Either source or destination \" +\n\t\t\t\t\"must be a file and the other one a secret. If you want the source to \" +\n\t\t\t\t\"be securely removed after copying, use 'gopass fsmove'\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.BinaryCopy,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:      \"fsmove\",\n\t\t\tUsage:     \"Move files from or to the password store\",\n\t\t\tArgsUsage: \"[from] [to]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command either reads a file from the filesystem and writes the \" +\n\t\t\t\t\"encoded and encrypted version in the store or it decrypts and decodes \" +\n\t\t\t\t\"a secret and writes the result to a file. Either source or destination \" +\n\t\t\t\t\"must be a file and the other one a secret. The source will be wiped \" +\n\t\t\t\t\"from disk or from the store after it has been copied successfully \" +\n\t\t\t\t\"and validated. If you don't want the source to be removed use \" +\n\t\t\t\t\"'gopass fscopy'\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.BinaryMove,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:      \"generate\",\n\t\t\tUsage:     \"Generate a new password\",\n\t\t\tArgsUsage: \"[secret [key [length]|length]]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Dialog to generate a new password and write it into a new or existing secret. \" +\n\t\t\t\t\"By default, the new password will replace the first line of an existing secret (or create a new one).\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Generate,\n\t\t\tBashComplete: s.CompleteGenerate,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"clip\",\n\t\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\t\tUsage:   \"Copy the generated password to the clipboard\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"print\",\n\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\tUsage:   \"Print the generated password to the terminal\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Force to overwrite existing password\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"edit\",\n\t\t\t\t\tAliases: []string{\"e\"},\n\t\t\t\t\tUsage:   \"Open secret for editing after generating a password\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"symbols\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Use symbols in the password\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"generator\",\n\t\t\t\t\tAliases: []string{\"g\"},\n\t\t\t\t\tUsage:   \"Choose a password generator, use one of: cryptic, memorable, xkcd or external. Default: cryptic\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"strict\",\n\t\t\t\t\tUsage: \"Require strict character class rules\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force-regen\",\n\t\t\t\t\tAliases: []string{\"t\"},\n\t\t\t\t\tUsage:   \"Force full re-generation, incl. evaluation of templates. Will overwrite the entire secret!\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"sep\",\n\t\t\t\t\tAliases: []string{\"xkcdsep\", \"xs\"},\n\t\t\t\t\tUsage:   \"Word separator for generated passwords. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised.\",\n\t\t\t\t\tValue:   \"\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"lang\",\n\t\t\t\t\tAliases: []string{\"xkcdlang\", \"xl\"},\n\t\t\t\t\tUsage:   \"Language to generate password from, currently only en (english, default) or de are supported\",\n\t\t\t\t\tValue:   \"en\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"commit-message\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"grep\",\n\t\t\tUsage:     \"Search for secrets files containing search-string when decrypted.\",\n\t\t\tArgsUsage: \"[needle]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command decrypts all secrets and performs a pattern matching on the \" +\n\t\t\t\t\"content.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.Grep,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"regexp\",\n\t\t\t\t\tAliases: []string{\"r\"},\n\t\t\t\t\tUsage:   \"Interpret pattern as RE2 regular expression\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"history\",\n\t\t\tUsage:     \"Show password history\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tAliases:   []string{\"hist\"},\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Display the change history for a secret\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.History,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"password\",\n\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\tUsage:   \"Include passwords in output\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"init\",\n\t\t\tUsage:     \"Initialize new password store.\",\n\t\t\tArgsUsage: \"[gpg-id]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Initialize new password storage and use gpg-id for encryption.\",\n\t\t\tAction: s.Init,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"path\",\n\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\tUsage:   \"Set the sub-store path to operate on\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"store\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Set the name of the sub-store\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"crypto\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select crypto backend %v\", backend.CryptoRegistry.BackendNames()),\n\t\t\t\t\tValue: \"gpgcli\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"storage\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select storage backend %v\", backend.StorageRegistry.BackendNames()),\n\t\t\t\t\tValue: \"gitfs\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"insert\",\n\t\t\tUsage:     \"Insert a new secret\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Insert a new secret. Optionally, echo the secret back to the console during entry. \" +\n\t\t\t\t\"Or, optionally, the entry may be multiline. \" +\n\t\t\t\t\"Prompt before overwriting existing secret unless forced.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Insert,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"echo\",\n\t\t\t\t\tAliases: []string{\"e\"},\n\t\t\t\t\tUsage:   \"Display secret while typing\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"multiline\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Insert using $EDITOR\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Overwrite any existing secret and do not prompt to confirm recipients\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"append\",\n\t\t\t\t\tAliases: []string{\"a\"},\n\t\t\t\t\tUsage:   \"Append data read from STDIN to existing data\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"commit-message\",\n\t\t\t\t\tUsage: \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"link\",\n\t\t\tUsage:     \"Create a symlink\",\n\t\t\tArgsUsage: \"[from] [to]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command creates a symlink from one entry in a mounted store to another entry. \" +\n\t\t\t\t\"Important: Does not cross mounts!\",\n\t\t\tAliases:      []string{\"ln\", \"symlink\"},\n\t\t\tHidden:       true,\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Link,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:      \"list\",\n\t\t\tUsage:     \"List existing secrets\",\n\t\t\tArgsUsage: \"[prefix]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command will list all existing secrets. Provide a folder prefix to list \" +\n\t\t\t\t\"only certain subfolders of the store.\",\n\t\t\tAliases:      []string{\"ls\"},\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.List,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:    \"limit\",\n\t\t\t\t\tAliases: []string{\"l\"},\n\t\t\t\t\tUsage:   \"Display no more than this many levels of the tree\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"flat\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Print a flat list\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"folders\",\n\t\t\t\t\tAliases: []string{\"d\"},\n\t\t\t\t\tUsage:   \"Print a flat list of folders\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"strip-prefix\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Strip this prefix from filtered entries\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"merge\",\n\t\t\tUsage:     \"Merge multiple secrets into one\",\n\t\t\tArgsUsage: \"[to] [from]...\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command implements a merge workflow to help deduplicate \" +\n\t\t\t\t\"secrets. It requires exactly one destination (may already exist) \" +\n\t\t\t\t\"and at least one source (must exist, can be multiple). gopass will \" +\n\t\t\t\t\"then merge all entries into one, drop into an editor, save the result \" +\n\t\t\t\t\"and remove all merged entries.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Merge,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"delete\",\n\t\t\t\t\tAliases: []string{\"d\"},\n\t\t\t\t\tUsage:   \"Remove merged entries\",\n\t\t\t\t\tValue:   true,\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Skip editor, merge entries unattended\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"move\",\n\t\t\tAliases:   []string{\"mv\"},\n\t\t\tUsage:     \"Move secrets from one location to another\",\n\t\t\tArgsUsage: \"[from] [to]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command moves a secret from one path to another. This also works \" +\n\t\t\t\t\"across different sub-stores. If the source is a directory, the source directory \" +\n\t\t\t\t\"is re-created at the destination if no trailing slash is found, otherwise the \" +\n\t\t\t\t\"contents are flattened (similar to rsync).\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Move,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"force\",\n\t\t\t\t\tAliases: []string{\"f\"},\n\t\t\t\t\tUsage:   \"Force to move the secret and overwrite existing one\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"commit-message\",\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tUsage:   \"Set the commit message\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"interactive-commit\",\n\t\t\t\t\tAliases: []string{\"i\"},\n\t\t\t\t\tUsage:   \"Open an editor for the commit message\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"mounts\",\n\t\t\tUsage: \"Edit mounted stores\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command displays all mounted password stores. It offers several \" +\n\t\t\t\t\"subcommands to create or remove mounts.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.MountsPrint,\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:    \"add\",\n\t\t\t\t\tAliases: []string{\"mount\"},\n\t\t\t\t\tUsage:   \"Mount a password store\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command allows for mounting an existing or new password store \" +\n\t\t\t\t\t\t\"at any path in an existing root store.\" +\n\t\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\t\t\"For example: gopass mounts add /path/to/existing/store\" +\n\t\t\t\t\t\t\"\\n\" +\n\t\t\t\t\t\t\"This will mount the store at /path/to/existing/store with the alias 'store'.\" +\n\t\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\t\t\"Or with a custom alias: gopass mounts add secondary-store /path/to/existing/store\" +\n\t\t\t\t\t\t\"\\n\\n\" +\n\t\t\t\t\t\t\"Learn more: https://github.com/gopasspw/gopass/blob/master/docs/commands/mounts.md\",\n\t\t\t\t\tBefore: s.IsInitialized,\n\t\t\t\t\tAction: s.MountAdd,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:    \"create\",\n\t\t\t\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\t\t\t\tUsage:   \"Create a new store at this location\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"remove\",\n\t\t\t\t\tAliases: []string{\"rm\", \"unmount\", \"umount\"},\n\t\t\t\t\tUsage:   \"Umount an mounted password store\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command allows to unmount an mounted password store. This will \" +\n\t\t\t\t\t\t\"only updated the configuration and not delete the password store.\",\n\t\t\t\t\tBefore:       s.IsInitialized,\n\t\t\t\t\tAction:       s.MountRemove,\n\t\t\t\t\tBashComplete: s.MountsComplete,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"versions\",\n\t\t\t\t\tAliases: []string{\"version\"},\n\t\t\t\t\tUsage:   \"Display mount provider versions\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command displays version information of important external \" +\n\t\t\t\t\t\t\"commands used by the configured password store mounts.\",\n\t\t\t\t\tBefore: s.IsInitialized,\n\t\t\t\t\tAction: s.MountsVersions,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"otp\",\n\t\t\tUsage:     \"Generate time- or hmac-based tokens\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tAliases:   []string{\"totp\", \"hotp\"},\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Tries to parse an OTP URL (otpauth://). URL can be TOTP or HOTP. \" +\n\t\t\t\t\"The URL can be provided on its own line or on a key value line with a key named 'totp'.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.OTP,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"alsoclip\",\n\t\t\t\t\tAliases: []string{\"C\"},\n\t\t\t\t\tUsage:   \"Copy the time-based token and show it\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"clip\",\n\t\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\t\tUsage:   \"Copy the time-based token into the clipboard\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"qr\",\n\t\t\t\t\tAliases: []string{\"q\"},\n\t\t\t\t\tUsage:   \"Write QR code to FILE\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"chained\",\n\t\t\t\t\tAliases: []string{\"p\"},\n\t\t\t\t\tUsage:   \"chain the token to the password\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"password\",\n\t\t\t\t\tAliases: []string{\"o\"},\n\t\t\t\t\tUsage:   \"Only display the token\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"snip\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Scan screen content to insert a OTP QR code into provided entry\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"process\",\n\t\t\tUsage: \"Process a template file\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command processes a template file. It will read the template file \" +\n\t\t\t\t\"and replace all variables with their values.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.Process,\n\t\t},\n\t\t{\n\t\t\tName:      \"rcs\",\n\t\t\tUsage:     \"Run a RCS command inside a password store\",\n\t\t\tArgsUsage: \"[init|push|pull]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"If the password store is a git repository, execute a git command \" +\n\t\t\t\t\"specified by git-command-args.\",\n\t\t\tHidden: true,\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:        \"init\",\n\t\t\t\t\tUsage:       \"Init RCS repo\",\n\t\t\t\t\tDescription: \"Create and initialize a new RCS repo in the store\",\n\t\t\t\t\tBefore:      s.IsInitialized,\n\t\t\t\t\tAction:      s.RCSInit,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"store\",\n\t\t\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"name\",\n\t\t\t\t\t\t\tAliases: []string{\"username\"},\n\t\t\t\t\t\t\tUsage:   \"Git Author Name\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:    \"email\",\n\t\t\t\t\t\t\tAliases: []string{\"useremail\"},\n\t\t\t\t\t\t\tUsage:   \"Git Author Email\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"storage\",\n\t\t\t\t\t\t\tUsage: fmt.Sprintf(\"Select storage backend %v\", set.Filter(backend.StorageRegistry.BackendNames(), \"fs\")),\n\t\t\t\t\t\t\tValue: \"gitfs\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:        \"status\",\n\t\t\t\t\tUsage:       \"RCS status\",\n\t\t\t\t\tDescription: \"Show the RCS status\",\n\t\t\t\t\tBefore:      s.IsInitialized,\n\t\t\t\t\tAction:      s.RCSStatus,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"store\",\n\t\t\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"recipients\",\n\t\t\tUsage: \"Edit recipient permissions\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command displays all existing recipients for all mounted stores. \" +\n\t\t\t\t\"The subcommands allow adding or removing recipients.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.RecipientsPrint,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"pretty\",\n\t\t\t\t\tUsage: \"Pretty print recipients\",\n\t\t\t\t\tValue: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:    \"ack\",\n\t\t\t\t\tAliases: []string{\"acknowledge\"},\n\t\t\t\t\tUsage:   \"Update recipients.hash\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command updates the value of recipients.hash. \" +\n\t\t\t\t\t\t\"This should only be run after manually validating any \" +\n\t\t\t\t\t\t\"changes to the recipients list. \",\n\t\t\t\t\tBefore: s.IsInitialized,\n\t\t\t\t\tAction: s.RecipientsAck,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"store\",\n\t\t\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"add\",\n\t\t\t\t\tAliases: []string{\"authorize\"},\n\t\t\t\t\tUsage:   \"Add any number of Recipients to any store\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command adds any number of recipients to any existing store. \" +\n\t\t\t\t\t\t\"If none are given it will display a list of usable public keys. \" +\n\t\t\t\t\t\t\"After adding the recipient to the list it will re-encrypt the whole \" +\n\t\t\t\t\t\t\"affected store to make sure the recipient has access to all existing \" +\n\t\t\t\t\t\t\"secrets.\",\n\t\t\t\t\tBefore: s.IsInitialized,\n\t\t\t\t\tAction: s.RecipientsAdd,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"store\",\n\t\t\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:  \"force\",\n\t\t\t\t\t\t\tUsage: \"Force adding non-existing keys\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"remove\",\n\t\t\t\t\tAliases: []string{\"rm\", \"deauthorize\"},\n\t\t\t\t\tUsage:   \"Remove any number of Recipients from any store\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"This command removes any number of recipients from any existing store. \" +\n\t\t\t\t\t\t\"If no recipients are provided, it will show a list of existing recipients \" +\n\t\t\t\t\t\t\"to choose from. It will refuse to remove the current user's key from the \" +\n\t\t\t\t\t\t\"store to avoid losing access. After removing the keys it will re-encrypt \" +\n\t\t\t\t\t\t\"all existing secrets. Please note that the removed recipients will still \" +\n\t\t\t\t\t\t\"be able to decrypt old revisions of the password store and any local \" +\n\t\t\t\t\t\t\"copies they might have. The only way to reliably remove a recipient is to \" +\n\t\t\t\t\t\t\"rotate all existing secrets.\",\n\t\t\t\t\tBefore:       s.IsInitialized,\n\t\t\t\t\tAction:       s.RecipientsRemove,\n\t\t\t\t\tBashComplete: s.RecipientsComplete,\n\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\tName:  \"store\",\n\t\t\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\t\tName:  \"force\",\n\t\t\t\t\t\t\tUsage: \"Force adding non-existing keys\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"reorg\",\n\t\t\tUsage:     \"Reorganize a password store by editing a text file\",\n\t\t\tArgsUsage: \"[prefix]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command lists all the secrets in a text file, line by line, and then opens it in an editor. \" +\n\t\t\t\t\"Once the user saves and leaves the editor, gopass will read the temp file, calculate the necessary moves and show a diff and a confirmation prompt. \" +\n\t\t\t\t\"Once the user acknowledges that it will reorganize the secrets and create a meaningful commit message.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Reorg,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:  \"setup\",\n\t\t\tUsage: \"Initialize a new password store\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command is automatically invoked if gopass is started without any \" +\n\t\t\t\t\"existing password store. This command exists so users can be provided with \" +\n\t\t\t\t\"simple one-command setup instructions.\",\n\t\t\tAction: s.Setup,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"remote\",\n\t\t\t\t\tUsage: \"URL to a git remote, will attempt to join this team\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"alias\",\n\t\t\t\t\tUsage: \"Local mount point for the given remote\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"create\",\n\t\t\t\t\tUsage: \"Create a new team (default: false, i.e. join an existing team)\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"name\",\n\t\t\t\t\tUsage: \"Firstname and Lastname for unattended GPG key generation\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"email\",\n\t\t\t\t\tUsage: \"EMail for unattended GPG key generation\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"crypto\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select crypto backend %v\", backend.CryptoRegistry.BackendNames()),\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"storage\",\n\t\t\t\t\tUsage: fmt.Sprintf(\"Select storage backend %v\", backend.StorageRegistry.BackendNames()),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"show\",\n\t\t\tUsage:     \"Display the content of a secret\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Show an existing secret and optionally put its first line on the clipboard. \" +\n\t\t\t\t\"If put on the clipboard, it will be cleared after 45 seconds.\",\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Show,\n\t\t\tBashComplete: s.Complete,\n\t\t\tFlags:        ShowFlags(),\n\t\t},\n\t\t{\n\t\t\tName:      \"sum\",\n\t\t\tUsage:     \"Compute the SHA256 checksum\",\n\t\t\tArgsUsage: \"[secret]\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command decodes an Base64 encoded secret and computes the SHA256 checksum \" +\n\t\t\t\t\"over the decoded data. This is useful to verify the integrity of an \" +\n\t\t\t\t\"inserted secret.\",\n\t\t\tAliases:      []string{\"sha\", \"sha256\"},\n\t\t\tBefore:       s.IsInitialized,\n\t\t\tAction:       s.Sum,\n\t\t\tBashComplete: s.Complete,\n\t\t},\n\t\t{\n\t\t\tName:  \"sync\",\n\t\t\tUsage: \"Sync all local stores with their remotes\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Sync all local stores with their git remotes, if any, and check \" +\n\t\t\t\t\"any possibly affected gpg keys.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.Sync,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"store\",\n\t\t\t\t\tAliases: []string{\"s\"},\n\t\t\t\t\tUsage:   \"Select the store to sync\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"templates\",\n\t\t\tUsage: \"Edit templates\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"List existing templates in the password store and allow for editing \" +\n\t\t\t\t\"and creating them.\",\n\t\t\tBefore: s.IsInitialized,\n\t\t\tAction: s.TemplatesPrint,\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:         \"show\",\n\t\t\t\t\tUsage:        \"Show a secret template.\",\n\t\t\t\t\tDescription:  \"Display an existing template\",\n\t\t\t\t\tAliases:      []string{\"cat\"},\n\t\t\t\t\tBefore:       s.IsInitialized,\n\t\t\t\t\tAction:       s.TemplatePrint,\n\t\t\t\t\tBashComplete: s.TemplatesComplete,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"edit\",\n\t\t\t\t\tUsage:        \"Edit secret templates.\",\n\t\t\t\t\tDescription:  \"Edit an existing or new template\",\n\t\t\t\t\tAliases:      []string{\"create\", \"new\"},\n\t\t\t\t\tBefore:       s.IsInitialized,\n\t\t\t\t\tAction:       s.TemplateEdit,\n\t\t\t\t\tBashComplete: s.TemplatesComplete,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"remove\",\n\t\t\t\t\tAliases:      []string{\"rm\"},\n\t\t\t\t\tUsage:        \"Remove secret templates.\",\n\t\t\t\t\tDescription:  \"Remove an existing template\",\n\t\t\t\t\tBefore:       s.IsInitialized,\n\t\t\t\t\tAction:       s.TemplateRemove,\n\t\t\t\t\tBashComplete: s.TemplatesComplete,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:        \"unclip\",\n\t\t\tUsage:       \"Internal command to clear clipboard\",\n\t\t\tDescription: \"Clear the clipboard if the content matches the checksum.\",\n\t\t\tAction:      s.Unclip,\n\t\t\tHidden:      true,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.IntFlag{\n\t\t\t\t\tName:  \"timeout\",\n\t\t\t\t\tUsage: \"Time to wait\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:  \"force\",\n\t\t\t\t\tUsage: \"Clear clipboard even if checksum mismatches\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"update\",\n\t\t\tUsage: \"Check for updates\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command checks for gopass updates at GitHub and automatically \" +\n\t\t\t\t\"downloads and installs any missing update.\",\n\t\t\tAction: s.Update,\n\t\t},\n\t\t{\n\t\t\tName:  \"version\",\n\t\t\tUsage: \"Display version\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"This command displays version and build time information.\",\n\t\t\tAction: s.Version,\n\t\t},\n\t}\n\n\t// crypto and storage backends can add their own commands if they need to\n\tfor _, be := range backend.CryptoRegistry.Backends() {\n\t\tbc, ok := be.(commander)\n\t\tif !ok {\n\t\t\t// Backend does not implement commander interface\n\n\t\t\tcontinue\n\t\t}\n\t\tnc := bc.Commands()\n\t\tdebug.V(2).Log(\"Backend %s added %d commands\", be, len(nc))\n\t\tcmds = append(cmds, nc...)\n\t}\n\n\tfor _, be := range backend.StorageRegistry.Backends() {\n\t\tbc, ok := be.(storeCommander)\n\t\tif !ok {\n\t\t\t// Backend does not implement commander interface\n\n\t\t\tcontinue\n\t\t}\n\t\tnc := bc.Commands(s.IsInitialized, func(alias string) (string, error) {\n\t\t\tsub, err := s.Store.GetSubStore(alias)\n\t\t\tif err != nil || sub == nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to get sub store for %s: %w\", alias, err)\n\t\t\t}\n\n\t\t\treturn sub.Path(), nil\n\t\t})\n\t\tdebug.V(2).Log(\"Backend %s added %d commands\", be, len(nc))\n\t\tcmds = append(cmds, nc...)\n\t}\n\n\treturn cmds\n}\n\ntype commander interface {\n\tCommands() []*cli.Command\n}\n\ntype storeCommander interface {\n\tCommands(func(*cli.Context) error, func(string) (string, error)) []*cli.Command\n}\n"
  },
  {
    "path": "internal/action/commands_test.go",
    "content": "package action\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc testCommand(t *testing.T, cmd *cli.Command) {\n\tt.Helper()\n\n\tif len(cmd.Subcommands) < 1 {\n\t\tassert.NotNil(t, cmd.Action, cmd.Name)\n\t}\n\n\tassert.NotEmpty(t, cmd.Usage, \"Required usage for command %s\", cmd.Name)\n\tassert.NotEmpty(t, cmd.Description, \"Required description for command %s\", cmd.Name)\n\n\tfor _, flag := range cmd.Flags {\n\t\tswitch v := flag.(type) {\n\t\tcase *cli.StringFlag:\n\t\t\tassert.NotContains(t, v.Name, \",\")\n\t\t\tassert.NotEmpty(t, v.Usage)\n\t\tcase *cli.BoolFlag:\n\t\t\tassert.NotContains(t, v.Name, \",\")\n\t\t\tassert.NotEmpty(t, v.Usage)\n\t\t}\n\t}\n\n\tfor _, scmd := range cmd.Subcommands {\n\t\ttestCommand(t, scmd)\n\t}\n}\n\nfunc TestCommands(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\n\tfor _, cmd := range act.GetCommands() {\n\t\tt.Run(cmd.Name, func(t *testing.T) {\n\t\t\ttestCommand(t, cmd)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/action/completion.go",
    "content": "package action\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\n\tfishcomp \"github.com/gopasspw/gopass/internal/completion/fish\"\n\tzshcomp \"github.com/gopasspw/gopass/internal/completion/zsh\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar escapeRegExp = regexp.MustCompile(`('|\"|\\s|\\(|\\)|\\<|\\>|\\&|\\;|\\#|\\\\|\\||\\*|\\?)`)\n\n// bashEscape Escape special characters with `\\`.\nfunc bashEscape(s string) string {\n\treturn escapeRegExp.ReplaceAllStringFunc(s, func(c string) string {\n\t\tif c == `\\` {\n\t\t\treturn `\\\\\\\\`\n\t\t}\n\n\t\tif c == `'` {\n\t\t\treturn `\\` + c\n\t\t}\n\n\t\tif c == `\"` {\n\t\t\treturn `\\\\\\` + c\n\t\t}\n\n\t\treturn `\\\\` + c\n\t})\n}\n\n// Complete prints a list of all password names to os.Stdout, for bash completion.\nfunc (s *Action) Complete(c *cli.Context) {\n\tctx := ctxutil.WithGlobalFlags(c)\n\t_, err := s.Store.IsInitialized(ctx) // important to make sure the structs are not nil.\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Store not initialized: %s\", err)\n\n\t\treturn\n\t}\n\tlist, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, v := range list {\n\t\tfmt.Fprintln(stdout, bashEscape(v))\n\t}\n}\n\n// CompletionOpenBSDKsh returns an OpenBSD ksh script used for auto completion.\nfunc (s *Action) CompletionOpenBSDKsh(a *cli.App) error {\n\tout := `\nPASS_LIST=$(gopass ls -f)\nset -A complete_gopass -- $PASS_LIST %s\n`\n\n\tif a == nil {\n\t\treturn fmt.Errorf(\"can not parse command options\")\n\t}\n\n\topts := make([]string, 0, len(a.Commands))\n\tfor _, opt := range a.Commands {\n\t\topts = append(opts, opt.Name)\n\t\tif len(opt.Aliases) > 0 {\n\t\t\topts = append(opts, strings.Join(opt.Aliases, \" \"))\n\t\t}\n\t}\n\n\tfmt.Fprintf(stdout, out, strings.Join(opts, \" \"))\n\n\treturn nil\n}\n\n// CompletionBash returns a bash script used for auto completion.\nfunc (s *Action) CompletionBash(c *cli.Context) error {\n\tout := `_gopass_bash_autocomplete() {\n     local cur opts base\n     COMPREPLY=()\n     cur=\"${COMP_WORDS[COMP_CWORD]}\"\n     # Use error handling to prevent crashes from invalid flags\n     opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion 2>/dev/null ) || opts=\"\"\n     local IFS=$'\\n'\n     COMPREPLY=( $(compgen -W \"${opts}\" -- ${cur}) )\n     return 0\n }\n\n`\n\tout += \"complete -F _gopass_bash_autocomplete \" + s.Name\n\tif runtime.GOOS == \"windows\" {\n\t\tout += \"\\ncomplete -F _gopass_bash_autocomplete \" + s.Name + \".exe\"\n\t}\n\tfmt.Fprintln(stdout, out)\n\n\treturn nil\n}\n\n// CompletionFish returns an autocompletion script for fish.\nfunc (s *Action) CompletionFish(a *cli.App) error {\n\tif a == nil {\n\t\treturn fmt.Errorf(\"app is nil\")\n\t}\n\tcomp, err := fishcomp.GetCompletion(a)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Fprintln(stdout, comp)\n\n\treturn nil\n}\n\n// CompletionZSH returns a zsh completion script.\nfunc (s *Action) CompletionZSH(a *cli.App) error {\n\tcomp, err := zshcomp.GetCompletion(a)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Fprintln(stdout, comp)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/completion_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestBashEscape(t *testing.T) {\n\tt.Run(\"bash escape\", func(t *testing.T) {\n\t\texpected := `a\\\\<\\\\>\\\\|\\\\\\\\and\\\\ sometimes\\\\?\\\\*\\\\(\\\\)\\\\&\\\\;\\\\#`\n\t\tif escaped := bashEscape(`a<>|\\and sometimes?*()&;#`); escaped != expected {\n\t\t\tt.Errorf(\"Expected %q, but got %q\", expected, escaped)\n\t\t}\n\t})\n\n\tt.Run(\"bash escape single quote\", func(t *testing.T) {\n\t\texpected := `good\\\\ ol\\'\\\\ days`\n\t\tif escaped := bashEscape(`good ol' days`); escaped != expected {\n\t\t\tt.Errorf(\"Expected %q, but got %q\", expected, escaped)\n\t\t}\n\t})\n\n\tt.Run(\"bash escape double quote\", func(t *testing.T) {\n\t\texpected := `my\\\\ \\\\\\\"bad\\\\\\\"\\\\ password`\n\t\tif escaped := bashEscape(`my \"bad\" password`); escaped != expected {\n\t\t\tt.Errorf(\"Expected %q, but got %q\", expected, escaped)\n\t\t}\n\t})\n}\n\nfunc TestComplete(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tapp := cli.NewApp()\n\tapp.Commands = []*cli.Command{\n\t\t{\n\t\t\tName:    \"test\",\n\t\t\tAliases: []string{\"foo\", \"bar\"},\n\t\t},\n\t}\n\n\tt.Run(\"complete foo\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tact.Complete(gptest.CliCtx(ctx, t))\n\t\tassert.Equal(t, \"foo\\n\", buf.String())\n\t})\n\n\tt.Run(\"bash completion\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.NoError(t, act.CompletionBash(nil))\n\t\tassert.Contains(t, buf.String(), \"action.test\")\n\t})\n\n\tt.Run(\"fish completion\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.NoError(t, act.CompletionFish(app))\n\t\tassert.Contains(t, buf.String(), \"action.test\")\n\t\trequire.Error(t, act.CompletionFish(nil))\n\t})\n\n\tt.Run(\"zsh completion\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.NoError(t, act.CompletionZSH(app))\n\t\tassert.Contains(t, buf.String(), \"action.test\")\n\t\trequire.Error(t, act.CompletionZSH(nil))\n\t})\n\n\tt.Run(\"openbsdksh completion\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.NoError(t, act.CompletionOpenBSDKsh(app))\n\t\tassert.Contains(t, buf.String(), \"complete_gopass\")\n\t\trequire.Error(t, act.CompletionOpenBSDKsh(nil))\n\t})\n}\n"
  },
  {
    "path": "internal/action/config.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Config handles changes to the gopass configuration.\n// It can be used to print the whole config, a single key, or to set a new\n// value for a given key.\nfunc (s *Action) Config(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tif c.Args().Len() < 1 {\n\t\ts.printConfigValues(ctx, store)\n\n\t\treturn nil\n\t}\n\n\tif c.Args().Len() == 1 {\n\t\ts.printConfigValues(ctx, store, c.Args().Get(0))\n\n\t\treturn nil\n\t}\n\n\tif c.Args().Len() > 2 {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s config key value\", s.Name)\n\t}\n\n\t// sub-stores need to have been initialized so we can update their local configs.\n\t// special case: we can always update the global config. NB: IsInitialized initializes the store if nil.\n\tif inited, err := s.Store.IsInitialized(ctx); err != nil || (store != \"\" && !inited) {\n\t\treturn exit.Error(exit.Unknown, err, \"Store %s seems uninitialized or cannot be initialized\", store)\n\t}\n\n\tif err := s.setConfigValue(ctx, store, c.Args().Get(0), c.Args().Get(1)); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Error setting config value: %s\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) printConfigValues(ctx context.Context, store string, needles ...string) {\n\tfor _, k := range set.SortedFiltered(s.cfg.Keys(store), func(e string) bool {\n\t\treturn contains(needles, e)\n\t}) {\n\t\tv := s.cfg.GetM(store, k)\n\t\t// if only a single key is requested, print only the value\n\t\t// useful for scriping, e.g. `$ cd $(gopass config path)`.\n\t\tif len(needles) == 1 {\n\t\t\tout.Printf(ctx, \"%s\", v)\n\n\t\t\tcontinue\n\t\t}\n\t\tout.Printf(ctx, \"%s = %s\", k, v)\n\t}\n}\n\nfunc contains(haystack []string, needle string) bool {\n\tif len(haystack) < 1 {\n\t\treturn true\n\t}\n\n\treturn slices.Contains(haystack, needle)\n}\n\nfunc (s *Action) setConfigValue(ctx context.Context, store, key, value string) error {\n\tdebug.Log(\"setting %s to %s for %q\", key, value, store)\n\n\tlevel, err := s.cfg.SetWithLevel(store, key, value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to set config value %q: %w\", key, err)\n\t}\n\n\t// in case of a non-local config change we don't need to track changes in the store\n\tif level != config.Local && level != config.Worktree {\n\t\tdebug.Log(\"did not set local config (was config level %d) in store %q, skipping commit phase\", level, store)\n\t\ts.printConfigValues(ctx, store, key)\n\n\t\treturn nil\n\t}\n\n\tst := s.Store.Storage(ctx, store)\n\tif st == nil {\n\t\treturn fmt.Errorf(\"storage not available\")\n\t}\n\n\tconfigFile := \"config\"\n\tif level == config.Worktree {\n\t\tconfigFile = \"config.worktree\"\n\t}\n\n\t// notice that the cfg.Set above should have created the local config file.\n\tif !st.Exists(ctx, configFile) {\n\t\treturn fmt.Errorf(\"local config file %s didn't exist in store, this is unexpected\", configFile)\n\t}\n\n\tif err := st.TryAdd(ctx, configFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to Add local config %q: %w\", configFile, err)\n\t}\n\tdebug.Log(\"Added local config for commit\")\n\n\tif err := st.TryCommit(ctx, \"Update config\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit local config: %w\", err)\n\t}\n\tdebug.Log(\"Committed local config\")\n\n\ts.printConfigValues(ctx, store, key)\n\n\treturn nil\n}\n\nfunc (s *Action) configKeys() []string {\n\treturn s.cfg.Keys(\"\")\n}\n\n// ConfigComplete will print the list of valid config keys for bash completion.\nfunc (s *Action) ConfigComplete(c *cli.Context) {\n\tfor _, k := range s.configKeys() {\n\t\tfmt.Fprintln(stdout, k)\n\t}\n}\n"
  },
  {
    "path": "internal/action/config_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"display config\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t)\n\t\trequire.NoError(t, act.Config(c))\n\t\twant := `age.agent-enabled = false\nage.agent-timeout = 0\ncore.autoimport = true\ncore.autopush = true\ncore.autosync = true\ncore.cliptimeout = 45\ncore.exportkeys = true\ncore.follow-references = false\ncore.nopager = true\ncore.notifications = true\ngenerate.autoclip = true\n`\n\t\twant += \"mounts.path = \" + u.StoreDir(\"\") + \"\\n\" +\n\t\t\t\"pwgen.xkcd-lang = en\\n\"\n\t\tassert.Equal(t, want, buf.String())\n\t})\n\n\tt.Run(\"set valid config value\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.NoError(t, act.setConfigValue(ctx, \"\", \"core.nopager\", \"true\"))\n\n\t\t// should print accepted config value\n\t\tassert.Equal(t, \"true\", strings.TrimSpace(buf.String()), \"action.setConfigValue\")\n\t})\n\n\tt.Run(\"set invalid config value\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\trequire.Error(t, act.setConfigValue(ctx, \"\", \"foobar\", \"true\"))\n\t})\n\n\tt.Run(\"print single config value\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tact.printConfigValues(ctx, \"\", \"core.nopager\")\n\n\t\twant := \"true\"\n\t\tassert.Equal(t, want, strings.TrimSpace(buf.String()), \"action.printConfigValues\")\n\t})\n\n\tt.Run(\"print all config values\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tact.printConfigValues(ctx, \"\")\n\t\twant := `age.agent-enabled = false\nage.agent-timeout = 0\ncore.autoimport = true\ncore.autopush = true\ncore.autosync = true\ncore.cliptimeout = 45\ncore.exportkeys = true\ncore.follow-references = false\ncore.nopager = true\ncore.notifications = true\ngenerate.autoclip = true\n`\n\t\twant += \"mounts.path = \" + u.StoreDir(\"\") + \"\\n\" +\n\t\t\t\"pwgen.xkcd-lang = en\\n\"\n\n\t\tassert.Equal(t, want, buf.String(), \"action.printConfigValues\")\n\t})\n\n\tt.Run(\"show autoimport value\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"core.autoimport\")\n\t\trequire.NoError(t, act.Config(c))\n\t\tassert.Equal(t, \"true\", strings.TrimSpace(buf.String()))\n\t})\n\n\tt.Run(\"disable autoimport\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"core.autoimport\", \"false\")\n\t\trequire.NoError(t, act.Config(c))\n\t\tassert.Equal(t, \"false\", strings.TrimSpace(buf.String()))\n\t})\n\n\tt.Run(\"complete config items\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tact.ConfigComplete(gptest.CliCtx(ctx, t))\n\t\twant := `age.agent-enabled\nage.agent-timeout\ncore.autoimport\ncore.autopush\ncore.autosync\ncore.cliptimeout\ncore.exportkeys\ncore.follow-references\ncore.nopager\ncore.notifications\ngenerate.autoclip\nmounts.path\npwgen.xkcd-lang\n`\n\t\tassert.Equal(t, want, buf.String())\n\t})\n\n\tt.Run(\"set autoimport to invalid value\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"autoimport\", \"false\", \"42\")\n\t\trequire.Error(t, act.Config(c))\n\t})\n}\n"
  },
  {
    "path": "internal/action/context.go",
    "content": "package action\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyClip contextKey = iota\n\tctxKeyPasswordOnly\n\tctxKeyPrintQR\n\tctxKeyRevision\n\tctxKeyKey\n\tctxKeyOnlyClip\n\tctxKeyAlsoClip\n\tctxKeyPrintChars\n\tctxKeyWithQRBody\n)\n\n// WithClip returns a context with the value for clip (for copy to clipboard)\n// set.\nfunc WithClip(ctx context.Context, clip bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyClip, clip)\n}\n\n// IsClip returns the value of clip or the default (false).\nfunc IsClip(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyClip).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithAlsoClip returns a context with the value for alsoclip (copy to\n// clipboard and print to stdout) set.\nfunc WithAlsoClip(ctx context.Context, clip bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyAlsoClip, clip)\n}\n\n// IsAlsoClip returns the value for alsoclip or the default (false).\nfunc IsAlsoClip(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyAlsoClip).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithOnlyClip returns a context with the value for clip (for copy to clipboard)\n// set.\nfunc WithOnlyClip(ctx context.Context, clip bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyOnlyClip, clip)\n}\n\n// IsOnlyClip returns the value of clip or the default (false).\nfunc IsOnlyClip(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyOnlyClip).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithPasswordOnly returns a context with the value of password only set.\nfunc WithPasswordOnly(ctx context.Context, pw bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPasswordOnly, pw)\n}\n\n// IsPasswordOnly returns the value of password only or the default (false).\nfunc IsPasswordOnly(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyPasswordOnly).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithPrintQR returns a context with the value of print QR set.\nfunc WithPrintQR(ctx context.Context, qr bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPrintQR, qr)\n}\n\n// IsPrintQR returns the value of print QR or the default (false).\nfunc IsPrintQR(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyPrintQR).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithRevision returns a context withe the value of revision set.\nfunc WithRevision(ctx context.Context, rev string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyRevision, rev)\n}\n\n// HasRevision returns true if a value for revision was set in this context.\nfunc HasRevision(ctx context.Context) bool {\n\tsv, ok := ctx.Value(ctxKeyRevision).(string)\n\n\treturn ok && sv != \"\"\n}\n\n// GetRevision returns the revison set in this context or an empty string.\nfunc GetRevision(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyRevision).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n\n// WithKey returns a context with the key set.\nfunc WithKey(ctx context.Context, sv string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyKey, sv)\n}\n\n// HasKey returns true if the key is set.\nfunc HasKey(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyKey).(string)\n\n\treturn ok\n}\n\n// GetKey returns the value of key or the default (empty string).\nfunc GetKey(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n\n// WithPrintChars returns the context with the print chars set.\nfunc WithPrintChars(ctx context.Context, c []int) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPrintChars, c)\n}\n\n// GetPrintChars returns a map of all character positions to print.\nfunc GetPrintChars(ctx context.Context) []int {\n\tmv, ok := ctx.Value(ctxKeyPrintChars).([]int)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn mv\n}\n\n// WithQRBody returns the context with the value of with QR body set.\nfunc WithQRBody(ctx context.Context, qr bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyWithQRBody, qr)\n}\n\n// IsQRBody returns the value of with QR body or the default (false).\nfunc IsQRBody(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyWithQRBody).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n"
  },
  {
    "path": "internal/action/context_test.go",
    "content": "package action\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWithClip(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tif IsClip(ctx) {\n\t\tt.Errorf(\"Should be false\")\n\t}\n\n\tif !IsClip(WithClip(ctx, true)) {\n\t\tt.Errorf(\"Should be true\")\n\t}\n}\n\nfunc TestWithPasswordOnly(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tif IsPasswordOnly(ctx) {\n\t\tt.Errorf(\"Should be false\")\n\t}\n\n\tif !IsPasswordOnly(WithPasswordOnly(ctx, true)) {\n\t\tt.Errorf(\"Should be true\")\n\t}\n}\n\nfunc TestWithPrintQR(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsPrintQR(ctx))\n\tassert.True(t, IsPrintQR(WithPrintQR(ctx, true)))\n}\n\nfunc TestWithRevision(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, GetRevision(ctx))\n\tassert.Equal(t, \"foo\", GetRevision(WithRevision(ctx, \"foo\")))\n\tassert.False(t, HasRevision(ctx))\n\tassert.True(t, HasRevision(WithRevision(ctx, \"foo\")))\n}\n\nfunc TestWithKey(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, GetKey(ctx))\n\tassert.Equal(t, \"foo\", GetKey(WithKey(ctx, \"foo\")))\n}\n\nfunc TestWithOnlyClip(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsOnlyClip(ctx))\n\tassert.True(t, IsOnlyClip(WithOnlyClip(ctx, true)))\n}\n\nfunc TestWithAlsoClip(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsAlsoClip(ctx))\n\tassert.True(t, IsAlsoClip(WithAlsoClip(ctx, true)))\n}\n"
  },
  {
    "path": "internal/action/convert.go",
    "content": "package action\n\nimport (\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Convert converts a store to a different set of backends.\nfunc (s *Action) Convert(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tctx = age.WithOnlyNative(ctx, true)\n\n\tstore := c.String(\"store\")\n\tmove := c.Bool(\"move\")\n\n\tsub, err := s.Store.GetSubStore(store)\n\tif err != nil {\n\t\treturn exit.Error(exit.NotFound, err, \"mount %q not found: %s\", store, err)\n\t}\n\n\t// we know it's a valid mount at this point\n\tctx = config.WithMount(ctx, store)\n\n\toldStorage := sub.Storage().Name()\n\n\tstorage, err := backend.StorageRegistry.Backend(oldStorage)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"unknown source storage backend %q: %s\", oldStorage, err)\n\t}\n\n\tif sv := c.String(\"storage\"); sv != \"\" {\n\t\tvar err error\n\t\tstorage, err = backend.StorageRegistry.Backend(sv)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Usage, err, \"unknown destination storage backend %q: %s\", storage, err)\n\t\t}\n\t}\n\n\toldCrypto := sub.Crypto().Name()\n\n\tcrypto, err := backend.CryptoRegistry.Backend(oldCrypto)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"unknown source crypto backend %q: %s\", oldCrypto, err)\n\t}\n\n\tif sv := c.String(\"crypto\"); sv != \"\" {\n\t\tvar err error\n\t\tcrypto, err = backend.CryptoRegistry.Backend(sv)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Usage, err, \"unknown destination crypto backend %q: %s\", sv, err)\n\t\t}\n\t}\n\n\tif oldCrypto == crypto.String() && oldStorage == storage.String() {\n\t\tout.Notice(ctx, \"No conversion needed. Source and destination match.\")\n\n\t\treturn nil\n\t}\n\n\tif oldCrypto != crypto.String() {\n\t\tdebug.Log(\"attempting to convert crypto from %q to %q\", oldCrypto, crypto.String())\n\n\t\tcbe, err := backend.NewCrypto(ctx, crypto)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := s.initCheckPrivateKeys(ctx, cbe); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tout.Printf(ctx, \"Crypto %q has private keys\", crypto.String())\n\t}\n\n\tout.Noticef(ctx, \"Converting %q. Crypto: %q -> %q, Storage: %q -> %q\", store, oldCrypto, crypto, oldStorage, storage)\n\tok, err := termio.AskForBool(ctx, \"Continue?\", false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif ctxutil.IsInteractive(ctx) && !ok {\n\t\tout.Notice(ctx, \"Aborted\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.Store.Convert(ctx, store, crypto, storage, move); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to convert store %q: %s\", store, err)\n\t}\n\n\tout.OKf(ctx, \"Successfully converted %q\", store)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/convert_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConvert(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(s string, b bool) ([]byte, error) {\n\t\treturn []byte(\"foo\"), nil\n\t})\n\tctx = ctxutil.WithPasswordPurgeCallback(ctx, func(s string) {})\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// first add another entry in a subdir\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.Convert(gptest.CliCtxWithFlags(ctx, t, map[string]string{\n\t\t\"move\":    \"true\",\n\t\t\"storage\": \"fs\",\n\t\t\"crypto\":  \"age\",\n\t})))\n\t// TODO: validate converted store. t.Logf(\"Buffer: %s\", buf.String()).\n}\n"
  },
  {
    "path": "internal/action/copy.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Copy the contents of a file to another one.\nfunc (s *Action) Copy(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tforce := c.Bool(\"force\")\n\n\tif c.Args().Len() != 2 {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s cp <FROM> <TO>\", s.Name)\n\t}\n\n\tfrom := c.Args().Get(0)\n\tto := c.Args().Get(1)\n\n\t// Check for custom commit message\n\tcommitMsg := fmt.Sprintf(\"Copied %s to %s\", from, to)\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\treturn s.copy(ctx, from, to, force)\n}\n\nfunc (s *Action) copy(ctx context.Context, from, to string, force bool) error {\n\tif !s.Store.Exists(ctx, from) && !s.Store.IsDir(ctx, from) {\n\t\treturn exit.Error(exit.NotFound, nil, \"%s does not exist\", from)\n\t}\n\n\tisSourceDir := s.Store.IsDir(ctx, from)\n\thasTrailingSlash := strings.HasSuffix(to, \"/\")\n\n\tif isSourceDir && hasTrailingSlash {\n\t\treturn s.copyFlattenDir(ctx, from, to, force)\n\t}\n\n\treturn s.copyRegular(ctx, from, to, force)\n}\n\nfunc (s *Action) copyFlattenDir(ctx context.Context, from, to string, force bool) error {\n\tentries, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list entries in %q\", from)\n\t}\n\n\tfromPrefix := from\n\tif !strings.HasSuffix(fromPrefix, \"/\") {\n\t\tfromPrefix += \"/\"\n\t}\n\n\tfor _, entry := range entries {\n\t\tif strings.HasPrefix(entry, fromPrefix) {\n\t\t\ttoPath := filepath.Join(to, filepath.Base(entry))\n\n\t\t\tif err := s.copyRegular(ctx, entry, toPath, force); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) copyRegular(ctx context.Context, from, to string, force bool) error {\n\tif !force {\n\t\tif s.Store.Exists(ctx, to) && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"%s already exists. Overwrite it?\", to)) {\n\t\t\treturn exit.Error(exit.Aborted, nil, \"not overwriting your current secret\")\n\t\t}\n\t}\n\n\tif err := s.Store.Copy(ctx, from, to); err != nil {\n\t\treturn exit.Error(exit.IO, err, \"failed to copy from %q to %q\", from, to)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/copy_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCopy(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tcolor.NoColor = true\n\n\t// copy foo bar\n\tc := gptest.CliCtx(ctx, t, \"foo\", \"bar\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy foo bar (again, should fail)\n\t{\n\t\tctx := ctxutil.WithAlwaysYes(ctx, false)\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t\tc.Context = ctx\n\t\trequire.Error(t, act.Copy(c))\n\t\tbuf.Reset()\n\t}\n\n\t// copy not-found still-not-there\n\tc = gptest.CliCtx(ctx, t, \"not-found\", \"still-not-there\")\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy\n\tc = gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// insert bam/baz\n\trequire.NoError(t, act.insertStdin(ctx, \"bam/baz\", []byte(\"foobar\"), false))\n\trequire.NoError(t, act.insertStdin(ctx, \"bam/zab\", []byte(\"barfoo\"), false))\n\n\t// recursive copy: bam/ -> zab\n\tc = gptest.CliCtx(ctx, t, \"bam\", \"zab\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t)))\n\twant := `gopass\n├── bam/\n│   ├── baz\n│   └── zab\n├── bar\n├── foo\n└── zab/\n    ├── baz\n    └── zab\n\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\tctx = ctxutil.WithTerminal(ctx, false)\n\trequire.NoError(t, act.show(ctx, c, \"zab/zab\", false))\n\tassert.Equal(t, \"barfoo\\n\", buf.String())\n\tbuf.Reset()\n}\n\nfunc TestCopyGpg(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\tu := gptest.NewGUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tcolor.NoColor = true\n\n\t// generate foo\n\tc := gptest.CliCtx(ctx, t, \"foo\")\n\trequire.NoError(t, act.Generate(c))\n\tbuf.Reset()\n\n\t// copy foo bar\n\tc = gptest.CliCtx(ctx, t, \"foo\", \"bar\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy foo bar (again, should fail)\n\t{\n\t\tctx := ctxutil.WithAlwaysYes(ctx, false)\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t\tc.Context = ctx\n\t\trequire.Error(t, act.Copy(c))\n\t\tbuf.Reset()\n\t}\n\n\t// copy not-found still-not-there\n\tc = gptest.CliCtx(ctx, t, \"not-found\", \"still-not-there\")\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy\n\tc = gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// insert bam/baz\n\trequire.NoError(t, act.insertStdin(ctx, \"bam/baz\", []byte(\"foobar\"), false))\n\trequire.NoError(t, act.insertStdin(ctx, \"bam/zab\", []byte(\"barfoo\"), false))\n\n\t// recursive copy: bam/ -> zab\n\tc = gptest.CliCtx(ctx, t, \"bam\", \"zab\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t)))\n\twant := `gopass\n├── bam/\n│   ├── baz\n│   └── zab\n├── bar\n├── foo\n└── zab/\n    ├── baz\n    └── zab\n\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\tctx = ctxutil.WithTerminal(ctx, false)\n\trequire.NoError(t, act.show(WithPasswordOnly(ctx, true), c, \"zab/zab\", false))\n\tassert.Equal(t, \"barfoo\", buf.String())\n\tbuf.Reset()\n}\n\nfunc TestCopyWithTrailingSlash(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tcolor.NoColor = true\n\n\t// generate foo\n\tc := gptest.CliCtx(ctx, t, \"foo\")\n\trequire.NoError(t, act.Generate(c))\n\tbuf.Reset()\n\n\t// copy foo bar\n\tc = gptest.CliCtx(ctx, t, \"foo\", \"bar\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy foo bar (again, should fail)\n\t{\n\t\tctx := ctxutil.WithAlwaysYes(ctx, false)\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t\tc.Context = ctx\n\t\trequire.Error(t, act.Copy(c))\n\t\tbuf.Reset()\n\t}\n\n\t// copy not-found still-not-there\n\tc = gptest.CliCtx(ctx, t, \"not-found\", \"still-not-there\")\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// copy\n\tc = gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// Create a directory structure\n\trequire.NoError(t, act.insertStdin(ctx, \"secret/some/zab\", []byte(\"secret\"), false))\n\trequire.NoError(t, act.insertStdin(ctx, \"secret/baz\", []byte(\"another\"), false))\n\n\t// Test copying a directory with trailing slash\n\tc = gptest.CliCtx(ctx, t, \"secret/\", \"new/\")\n\trequire.NoError(t, act.Copy(c))\n\tbuf.Reset()\n\n\t// Verify the result\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t)))\n\twant := `gopass\n├── bar\n├── foo\n├── new/\n│   ├── baz\n│   └── zab\n└── secret/\n    ├── baz\n    └── some/\n        └── zab\n\n`\n\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// Verify content of copied files\n\tctx = ctxutil.WithTerminal(ctx, false)\n\trequire.NoError(t, act.show(ctx, c, \"new/zab\", false))\n\tassert.Equal(t, \"secret\\n\", buf.String())\n\tbuf.Reset()\n\n\trequire.NoError(t, act.show(ctx, c, \"new/baz\", false))\n\tassert.Equal(t, \"another\\n\", buf.String())\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/create.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/create\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/clipboard\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Create displays the password creation wizard.\nfunc (s *Action) Create(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tout.Printf(ctx, \"🌟 Welcome to the secret creation wizard (gopass create)!\")\n\tout.Printf(ctx, \"🧪 Hint: Use 'gopass edit -c' for more control!\")\n\n\twiz, err := create.New(ctx, s.Store.Storage(ctx, c.String(\"store\")))\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialize wizard\")\n\t}\n\n\tacts := wiz.Actions(s.Store, s.createPrintOrCopy)\n\t// this should usually not happen because we initialize the templates if none\n\t// exist.\n\tif len(acts) < 1 {\n\t\treturn exit.Error(exit.Unknown, nil, \"no wizard actions available\")\n\t}\n\t// no need to ask if there is only one action available.\n\tif len(acts) == 1 {\n\t\treturn acts.Run(ctx, c, 0)\n\t}\n\n\tact, sel := cui.GetSelection(ctx, \"Please select the type of secret you would like to create\", acts.Selection())\n\tswitch act {\n\tcase \"default\":\n\t\tfallthrough\n\tcase \"show\":\n\t\treturn acts.Run(ctx, c, sel)\n\tdefault:\n\t\treturn exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n}\n\n// createPrintOrCopy will display the created password (or copy to clipboard).\nfunc (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, password string, genPw bool) error {\n\tif !genPw {\n\t\treturn nil\n\t}\n\n\tif c.Bool(\"print\") {\n\t\tfmt.Fprintf(out.Stdout, \"The generated password for %s is:\\n%s\\n\", name, password)\n\n\t\treturn nil\n\t}\n\n\tif err := clipboard.CopyTo(ctx, name, []byte(password), config.Int(ctx, \"core.cliptimeout\")); err != nil {\n\t\treturn exit.Error(exit.IO, err, \"failed to copy to clipboard: %s\", err)\n\t}\n\n\treturn hook.InvokeRoot(ctx, \"create.post-hook\", name, s.Store)\n}\n"
  },
  {
    "path": "internal/action/create_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tclipboard.ForceUnsupported = true\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"core.notifications\", \"false\"))\n\trequire.NoError(t, act.cfg.Set(\"\", \"core.cliptimeout\", \"1\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// create\n\tc := gptest.CliCtx(ctx, t)\n\n\trequire.Error(t, act.Create(c))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/delete.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Delete a secret file with its content.\nfunc (s *Action) Delete(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\trecursive := c.Bool(\"recursive\")\n\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s rm name\", s.Name)\n\t}\n\n\tif recursive {\n\t\tif len(c.Args().Tail()) > 1 {\n\t\t\treturn exit.Error(exit.Usage, nil, \"Deleting multiple keys is not supported in recursive mode\")\n\t\t}\n\n\t\treturn s.deleteRecursive(ctx, name, c.Bool(\"force\"))\n\t}\n\n\tif s.Store.IsDir(ctx, name) && !s.Store.Exists(ctx, name) {\n\t\treturn exit.Error(exit.Usage, nil, \"Cannot remove %q: Is a directory. Use 'gopass rm -r %s' to delete\", name, name)\n\t}\n\n\t// specifying a key is optional.\n\tkey := c.Args().Get(1)\n\n\t// multiple secrets, so not a key\n\tif len(c.Args().Tail()) > 1 {\n\t\tkey = \"\"\n\t}\n\n\t// Check for custom commit message\n\tcommitMsg := fmt.Sprintf(\"Deleted %s\", name)\n\tif key != \"\" {\n\t\tcommitMsg = fmt.Sprintf(\"Deleted key %s from %s\", key, name)\n\t}\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\tnames := append([]string{name}, c.Args().Tail()...)\n\n\tif key != \"\" && s.Store.Exists(ctx, key) {\n\t\treturn exit.Error(exit.Unsupported, nil, \"Key %q clashes with a secret of this name, use 'gopass edit %s' to delete\", key, name)\n\t}\n\n\tif !s.Store.Exists(ctx, name) {\n\t\treturn exit.Error(exit.NotFound, nil, \"Secret %q does not exist\", name)\n\t}\n\n\tif !c.Bool(\"force\") { // don't check if it's force anyway.\n\t\tqStr := fmt.Sprintf(\"☠ Are you sure you would like to delete %q?\", names)\n\t\tif key != \"\" {\n\t\t\tqStr = fmt.Sprintf(\"☠ Are you sure you would like to delete %q from %q?\", key, name)\n\t\t}\n\t\tif (s.Store.Exists(ctx, name) || s.Store.IsDir(ctx, name)) && key == \"\" && !termio.AskForConfirmation(ctx, qStr) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// deletes a single key from a YAML doc.\n\tif key != \"\" {\n\t\tdebug.Log(\"removing key %q from %q\", key, name)\n\n\t\treturn s.deleteKeyFromYAML(ctx, name, key)\n\t}\n\n\tfor _, name := range names {\n\t\tdebug.Log(\"removing entry %q\", name)\n\t\tif err := s.Store.Delete(ctx, name); err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"Can not delete %q: %s\", name, err)\n\t\t}\n\n\t\tif err := hook.InvokeRoot(ctx, \"delete.post-hook\", name, s.Store); err != nil {\n\t\t\treturn exit.Error(exit.Hook, err, \"Hook failed for %s: %s\", name, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) deleteRecursive(ctx context.Context, name string, force bool) error {\n\tif !force { // don't check if it's force anyway.\n\t\tif (s.Store.Exists(ctx, name) || s.Store.IsDir(ctx, name)) && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"☠ Are you sure you would like to recursively delete %q?\", name)) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdebug.Log(\"pruning %q\", name)\n\tif err := s.Store.Prune(ctx, name); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to prune %q: %s\", name, err)\n\t}\n\tdebug.Log(\"pruned %q\", name)\n\n\treturn nil\n}\n\n// deleteKeyFromYAML deletes a single key from YAML.\nfunc (s *Action) deleteKeyFromYAML(ctx context.Context, name, key string) error {\n\tsec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"Can not delete key %q from %q: %s\", key, name, err)\n\t}\n\n\tsec.Del(key)\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.IO, err, \"Can not delete key %q from %q: %s\", key, name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the YAML file does't seem to have the key to be deleted\")\n\t}\n\n\treturn hook.Invoke(ctx, \"delete.post-hook\", name, key)\n}\n"
  },
  {
    "path": "internal/action/delete_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDelete(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\t// delete\n\tc := gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.Delete(c))\n\tbuf.Reset()\n\n\t// delete foo\n\tc = gptest.CliCtx(ctx, t, \"foo\")\n\trequire.NoError(t, act.Delete(c))\n\tbuf.Reset()\n\n\t// delete foo bar\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\t_, err = sec.Write([]byte(\"---\\nbar: zab\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, act.Store.Set(ctx, \"foo\", sec))\n\n\tc = gptest.CliCtx(ctx, t, \"foo\", \"bar\")\n\trequire.NoError(t, act.Delete(c))\n\tbuf.Reset()\n\n\t// delete -r foo\n\trequire.NoError(t, act.Store.Set(ctx, \"foo\", sec))\n\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"recursive\": \"true\"}, \"foo\")\n\trequire.NoError(t, act.Delete(c))\n\tbuf.Reset()\n\n\t// reject recursive flag when a key is given\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"recursive\": \"true\"}, \"foo\", \"bar\")\n\trequire.Error(t, act.Delete(c))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.Store.Set(ctx, \"sec/1\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"sec/2\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"sec/3\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"sec/4\", sec))\n\n\t// warn if key matching a secret is given\n\tc = gptest.CliCtx(ctx, t, \"sec/1\", \"sec/2\")\n\trequire.Error(t, act.Delete(c))\n\tbuf.Reset()\n\n\t// remove multiple secrets\n\tc = gptest.CliCtx(ctx, t, \"sec/1\", \"sec/2\", \"sec/3\", \"sec/4\")\n\trequire.NoError(t, act.Delete(c))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/doc.go",
    "content": "// Package action implements all the handlers that are available as subcommands\n// for gopass.\npackage action\n"
  },
  {
    "path": "internal/action/edit.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/audit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Edit the content of a password file.\nfunc (s *Action) Edit(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tctx = ctxutil.WithFollowRef(ctx, false)\n\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s edit secret\", s.Name)\n\t}\n\n\tif err := hook.Invoke(ctx, \"edit.pre-hook\", name); err != nil {\n\t\treturn exit.Error(exit.Hook, err, \"edit.pre-hook failed: %s\", err)\n\t}\n\n\tif err := s.edit(ctx, c, name); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to edit %q: %s\", name, err)\n\t}\n\n\treturn hook.InvokeRoot(ctx, \"edit.post-hook\", name, s.Store)\n}\n\nfunc (s *Action) edit(ctx context.Context, c *cli.Context, name string) error {\n\ted := editor.Path(c)\n\n\t// get existing content or generate new one from a template.\n\tname, content, changed, err := s.editGetContent(ctx, name, c.Bool(\"create\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// invoke the editor to let the user edit the content.\n\tnewContent, err := editor.Invoke(ctx, ed, content)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke editor: %s\", err)\n\t}\n\n\t// Check for custom commit message\n\tcommitMsg := fmt.Sprintf(\"Edited with %s\", ed)\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\treturn s.editUpdate(ctx, name, content, newContent, changed, ed)\n}\n\nfunc (s *Action) editUpdate(ctx context.Context, name string, content, nContent []byte, changed bool, ed string) error {\n\t// If content is equal, nothing changed, exiting.\n\tif bytes.Equal(content, nContent) && !changed {\n\t\treturn nil\n\t}\n\n\tnSec := secrets.ParseAKV(nContent)\n\n\t// if the secret has a password, we check its strength.\n\tif pw := nSec.Password(); pw != \"\" {\n\t\taudit.Single(ctx, pw)\n\t}\n\n\t// write result (back) to store.\n\tif err := s.Store.Set(ctx, name, nSec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to encrypt secret %s: %s\", name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"The new value of the password is equal to its current value. Not writing it again.\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) editGetContent(ctx context.Context, name string, create bool) (string, []byte, bool, error) {\n\tif !s.Store.Exists(ctx, name) && !create && !config.Bool(ctx, \"edit.auto-create\") {\n\t\tvar err error\n\t\tname, err = s.editFindName(ctx, name)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, false, err\n\t\t}\n\t}\n\n\tif err := s.Store.CheckRecipients(ctx, name); err != nil {\n\t\treturn name, nil, false, exit.Error(exit.Recipients, err, \"invalid recipients detected for %q: %s\", name, err)\n\t}\n\n\t// edit existing entry.\n\tif s.Store.Exists(ctx, name) {\n\t\t// we make sure we are not parsing the content of the file when editing.\n\t\tsec, err := s.Store.Get(ctxutil.WithShowParsing(ctx, false), name)\n\t\tif err != nil {\n\t\t\treturn name, nil, false, exit.Error(exit.Decrypt, err, \"failed to decrypt %s: %s\", name, err)\n\t\t}\n\n\t\treturn name, sec.Bytes(), false, nil\n\t}\n\n\tif !create {\n\t\tout.Warningf(ctx, \"Entry %s not found. Creating new secret ...\", name)\n\t}\n\n\t// load template if it exists.\n\tpwLength, _ := config.DefaultPasswordLengthFromEnv(ctx)\n\tif content, found := s.renderTemplate(ctx, name, []byte(pwgen.GeneratePassword(pwLength, false))); found {\n\t\treturn name, content, true, nil\n\t}\n\n\t// new entry, no template.\n\treturn name, nil, false, nil\n}\n\nfunc (s *Action) editFindName(ctx context.Context, name string) (string, error) {\n\tnewName := \"\"\n\t// capture only the name of the selected secret.\n\tcb := func(ctx context.Context, c *cli.Context, selectedName string, recurse bool) error {\n\t\tnewName = selectedName\n\n\t\treturn nil\n\t}\n\tif err := s.find(ctx, nil, name, cb, false); err != nil {\n\t\tdebug.Log(\"failed to find secret %s: %s\", name, err)\n\n\t\treturn name, err\n\t}\n\n\tcont, err := termio.AskForBool(ctx, fmt.Sprintf(\"Secret does not exist %q. Found possible match in %q. Edit existing entry?\", name, newName), true)\n\tif err != nil {\n\t\treturn name, err\n\t}\n\n\tif cont {\n\t\treturn newName, nil\n\t}\n\n\treturn name, nil\n}\n"
  },
  {
    "path": "internal/action/edit_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEdit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// edit\n\trequire.Error(t, act.Edit(gptest.CliCtx(ctx, t)))\n\tbuf.Reset()\n\n\t// edit foo (existing)\n\trequire.Error(t, act.Edit(gptest.CliCtx(ctx, t, \"foo\")))\n\tbuf.Reset()\n\n\t// edit bar (new)\n\trequire.Error(t, act.Edit(gptest.CliCtx(ctx, t, \"foo\")))\n\tbuf.Reset()\n}\n\nfunc TestEditUpdate(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tcontent := []byte(\"foobar\")\n\t// no changes\n\trequire.NoError(t, act.editUpdate(ctx, \"foo\", content, content, false, \"test\"))\n\tbuf.Reset()\n\n\t// changes\n\tnContent := []byte(\"barfoo\")\n\trequire.NoError(t, act.editUpdate(ctx, \"foo\", content, nContent, false, \"test\"))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/env.go",
    "content": "package action\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Env implements the env subcommand. It populates the environment of a subprocess with\n// a set of environment variables corresponding to the secret subtree specified on the\n// command line.\nfunc (s *Action) Env(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\targs := c.Args().Tail()\n\tkeepCase := c.Bool(\"keep-case\")\n\n\tif len(args) == 0 {\n\t\treturn exit.Error(exit.Usage, nil, \"Missing subcommand to execute\")\n\t}\n\n\tif !s.Store.Exists(ctx, name) && !s.Store.IsDir(ctx, name) {\n\t\treturn exit.Error(exit.NotFound, nil, \"Secret %s not found\", name)\n\t}\n\n\tkeys := make([]string, 0, 1)\n\tif s.Store.IsDir(ctx, name) {\n\t\tdebug.Log(\"%q is a dir, adding it's entries\", name)\n\n\t\tl, err := s.Store.Tree(ctx)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.List, err, \"failed to list store: %s\", err)\n\t\t}\n\n\t\tsubtree, err := l.FindFolder(name)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.NotFound, nil, \"Entry %q not found\", name)\n\t\t}\n\n\t\tfor _, e := range subtree.List(tree.INF) {\n\t\t\tdebug.Log(\"found key: %s\", e)\n\t\t\tkeys = append(keys, e)\n\t\t}\n\t} else {\n\t\tkeys = append(keys, name)\n\t}\n\n\tenv := make([]string, 0, 1)\n\tfor _, key := range keys {\n\t\tdebug.Log(\"exporting to environment key: %s\", key)\n\t\tsec, err := s.Store.Get(ctx, key)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get entry for env prefix %q: %w\", name, err)\n\t\t}\n\t\tenvKey := path.Base(key)\n\t\tif !keepCase {\n\t\t\tenvKey = strings.ToUpper(envKey)\n\t\t}\n\t\tenv = append(env, fmt.Sprintf(\"%s=%s\", envKey, sec.Password()))\n\t}\n\n\tcmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), env...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = stdout\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "internal/action/env_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEnvLeafHappyPath(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\t// Command-line would be: \"gopass env foo env\", where \"foo\" is an existing\n\t// secret with value \"secret\". We expect to see the key/value in the output\n\t// of the /usr/bin/env utility in the form \"BAZ=secret\".\n\tpw := pwgen.GeneratePassword(24, false)\n\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(pw), false))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.Env(gptest.CliCtx(ctx, t, \"baz\", \"env\")))\n\tassert.Contains(t, buf.String(), fmt.Sprintf(\"BAZ=%s\\n\", pw))\n}\n\nfunc TestEnvLeafHappyPathKeepCase(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\t// Command-line would be: \"gopass env --keep-case BaZ env\", where\n\t// \"foo\" is an existing secret with value \"secret\". We expect to see the\n\t// key/value in the output of the /usr/bin/env utility in the form\n\t// \"BaZ=secret\".\n\tpw := pwgen.GeneratePassword(24, false)\n\trequire.NoError(t, act.insertStdin(ctx, \"BaZ\", []byte(pw), false))\n\tbuf.Reset()\n\n\tflags := make(map[string]string, 1)\n\tflags[\"keep-case\"] = \"true\"\n\trequire.NoError(t, act.Env(gptest.CliCtxWithFlags(ctx, t, flags, \"BaZ\", \"env\")))\n\tassert.Contains(t, buf.String(), fmt.Sprintf(\"BaZ=%s\\n\", pw))\n}\n\nfunc TestEnvSecretNotFound(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\t// Command-line would be: \"gopass env non-existing true\".\n\trequire.EqualError(t, act.Env(gptest.CliCtx(ctx, t, \"non-existing\", \"true\")),\n\t\t\"Secret non-existing not found\")\n}\n\nfunc TestEnvProgramNotFound(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\twanted := \"exec: \\\"non-existing\\\": executable file not found in \"\n\tif runtime.GOOS == \"windows\" {\n\t\twanted += \"%PATH%\"\n\t} else {\n\t\twanted += \"$PATH\"\n\t}\n\n\t// Command-line would be: \"gopass env foo non-existing\".\n\trequire.EqualError(t, act.Env(gptest.CliCtx(ctx, t, \"foo\", \"non-existing\")),\n\t\twanted)\n}\n\n// Crash regression.\nfunc TestEnvProgramNotSpecified(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\t// Command-line would be: \"gopass env foo\".\n\trequire.EqualError(t, act.Env(gptest.CliCtx(ctx, t, \"foo\")),\n\t\t\"Missing subcommand to execute\")\n}\n"
  },
  {
    "path": "internal/action/exit/errors.go",
    "content": "package exit\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst (\n\t// OK means no error (status code 0).\n\tOK = iota\n\t// Unknown is used if we can't determine the exact exit cause.\n\tUnknown\n\t// Usage is used if there was some kind of invocation error.\n\tUsage\n\t// Aborted is used if the user willingly aborted an action.\n\tAborted\n\t// Unsupported is used if an operation is not supported by gopass.\n\tUnsupported\n\t// AlreadyInitialized is used if someone is trying to initialize.\n\t// an already initialized store.\n\tAlreadyInitialized\n\t// NotInitialized is used if someone is trying to use an unitialized.\n\t// store.\n\tNotInitialized\n\t// Git is used if any git errors are encountered.\n\tGit\n\t// Mount is used if a substore mount operation fails.\n\tMount\n\t// NoName is used when no name was provided for a named entry.\n\tNoName\n\t// NotFound is used if a requested secret is not found.\n\tNotFound\n\t// Decrypt is used when reading/decrypting a secret failed.\n\tDecrypt\n\t// Encrypt is used when writing/encrypting of a secret fails.\n\tEncrypt\n\t// List is used when listing the store content fails.\n\tList\n\t// Audit is used when audit report possible issues.\n\tAudit\n\t// Fsck is used when the integrity check fails.\n\tFsck\n\t// Config is used when config errors occur.\n\tConfig\n\t// Recipients is used when a recipient operation fails.\n\tRecipients\n\t// IO is used for misc. I/O errors.\n\tIO\n\t// GPG is used for misc. gpg errors.\n\tGPG\n\t// Hook is used for Hook failures.\n\tHook\n)\n\n// Error returns a user friendly CLI error.\nfunc Error(exitCode int, err error, format string, args ...any) error {\n\tmsg := fmt.Sprintf(format, args...)\n\tif err != nil {\n\t\tdebug.LogN(1, \"%s - stacktrace: %+v\", msg, err)\n\t}\n\n\treturn cli.Exit(msg, exitCode)\n}\n"
  },
  {
    "path": "internal/action/exit/errors_test.go",
    "content": "package exit\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestError(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\trequire.Error(t, Error(Unknown, fmt.Errorf(\"test\"), \"test\"))\n\tassert.NotContains(t, buf.String(), \"Stacktrace\")\n}\n"
  },
  {
    "path": "internal/action/find.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/schollz/closestmatch\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Find runs find without fuzzy search.\nfunc (s *Action) Find(c *cli.Context) error {\n\treturn s.findCmd(c, nil, false)\n}\n\n// FindFuzzy runs find with fuzzy search.\nfunc (s *Action) FindFuzzy(c *cli.Context) error {\n\treturn s.findCmd(c, s.show, true)\n}\n\nfunc (s *Action) findCmd(c *cli.Context, cb showFunc, fuzzy bool) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.IsSet(\"clip\") {\n\t\tctx = WithOnlyClip(ctx, c.Bool(\"clip\"))\n\t\tctx = WithClip(ctx, c.Bool(\"clip\"))\n\t}\n\n\tif c.IsSet(\"unsafe\") {\n\t\tctx = ctxutil.WithForce(ctx, c.Bool(\"unsafe\"))\n\t}\n\n\tif !c.Args().Present() {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s find <pattern>\", s.Name)\n\t}\n\n\treturn s.find(ctx, c, c.Args().First(), cb, fuzzy)\n}\n\n// see action.show - context, cli context, name, key, rescurse.\ntype showFunc func(context.Context, *cli.Context, string, bool) error\n\nfunc (s *Action) find(ctx context.Context, c *cli.Context, needle string, cb showFunc, fuzzy bool) error {\n\t// get all existing entries.\n\thaystack, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list store: %s\", err)\n\t}\n\n\t// filter our the ones from the haystack matching the needle.\n\tchoices, err := filter(haystack, needle, c.Bool(\"regex\"))\n\tif err != nil {\n\t\treturn exit.Error(exit.Usage, err, \"%s\", err)\n\t}\n\n\t// if we have an exact match print it.\n\tif len(choices) == 1 {\n\t\tif cb == nil {\n\t\t\tout.Printf(ctx, choices[0])\n\n\t\t\treturn nil\n\t\t}\n\t\tout.OKf(ctx, \"Found exact match in %q\", choices[0])\n\n\t\treturn cb(ctx, c, choices[0], false)\n\t}\n\n\t// if we don't have a match yet try a fuzzy search.\n\tif len(choices) < 1 && fuzzy {\n\t\t// try fuzzy match.\n\t\tcm := closestmatch.New(haystack, []int{2})\n\t\tchoices = cm.ClosestN(needle, 5)\n\t}\n\n\t// if there are still no results we abort.\n\tif len(choices) < 1 {\n\t\treturn exit.Error(exit.NotFound, nil, \"no results found\")\n\t}\n\n\t// do not invoke wizard if not printing to terminal or if\n\t// gopass find/search was invoked directly (for scripts).\n\tif !ctxutil.IsTerminal(ctx) || (c != nil && c.Command.Name == \"find\") {\n\t\tfor _, value := range choices {\n\t\t\tout.Printf(ctx, value)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn s.findSelection(ctx, c, choices, needle, cb)\n}\n\n// findSelection runs a wizard that lets the user select an entry.\nfunc (s *Action) findSelection(ctx context.Context, c *cli.Context, choices []string, needle string, cb showFunc) error {\n\tif cb == nil {\n\t\treturn fmt.Errorf(\"callback is nil\")\n\t}\n\tif len(choices) < 1 {\n\t\treturn fmt.Errorf(\"out of options\")\n\t}\n\n\tsort.Strings(choices)\n\tact, sel := cui.GetSelection(ctx, \"Found secrets - Please select an entry\", choices)\n\tdebug.Log(\"Action: %s - Selection: %d\", act, sel)\n\n\tswitch act {\n\tcase \"default\":\n\t\t// display or copy selected entry.\n\t\tfmt.Fprintln(stdout, choices[sel])\n\n\t\treturn cb(ctx, c, choices[sel], false)\n\tcase \"copy\":\n\t\t// display selected entry.\n\t\tfmt.Fprintln(stdout, choices[sel])\n\n\t\treturn cb(WithClip(ctx, true), c, choices[sel], false)\n\tcase \"show\":\n\t\t// display selected entry.\n\t\tfmt.Fprintln(stdout, choices[sel])\n\n\t\treturn cb(WithClip(ctx, false), c, choices[sel], false)\n\tcase \"sync\":\n\t\t// run sync and re-run show/find workflow.\n\t\tif err := s.Sync(c); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn cb(ctx, c, needle, true)\n\tcase \"edit\":\n\t\t// edit selected entry.\n\t\tfmt.Fprintln(stdout, choices[sel])\n\n\t\treturn s.edit(ctx, c, choices[sel])\n\tdefault:\n\t\treturn exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n}\n\nfunc filter(l []string, needle string, reMatch bool) ([]string, error) {\n\tchoices := make([]string, 0, 10)\n\n\tif reMatch {\n\t\tcompiledRE, err := regexp.Compile(needle)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, value := range l {\n\t\t\tif compiledRE.MatchString(value) {\n\t\t\t\tchoices = append(choices, value)\n\t\t\t}\n\t\t}\n\n\t\treturn choices, nil\n\t}\n\n\tfor _, value := range l {\n\t\tif strings.Contains(strings.ToLower(value), strings.ToLower(needle)) {\n\t\t\tchoices = append(choices, value)\n\t\t}\n\t}\n\n\treturn choices, nil\n}\n"
  },
  {
    "path": "internal/action/find_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestFind(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\tcolor.NoColor = true\n\n\tactName := \"action.test\"\n\tif runtime.GOOS == \"windows\" {\n\t\tactName = \"action.test.exe\"\n\t}\n\n\t// find\n\tc := gptest.CliCtx(ctx, t)\n\tif err := act.FindFuzzy(c); err == nil || err.Error() != fmt.Sprintf(\"Usage: %s find <pattern>\", actName) {\n\t\tt.Errorf(\"Should fail: %s\", err)\n\t}\n\n\t// find fo (with fuzzy search)\n\tc = gptest.CliCtxWithFlags(ctx, t, nil, \"fo\")\n\trequire.NoError(t, act.FindFuzzy(c))\n\tassert.Contains(t, strings.TrimSpace(buf.String()), \"Found exact match in \\\"foo\\\"\\nsecret\")\n\tbuf.Reset()\n\n\t// find fo (no fuzzy search)\n\tc = gptest.CliCtxWithFlags(ctx, t, nil, \"fo\")\n\trequire.NoError(t, act.Find(c))\n\tassert.Equal(t, \"foo\", strings.TrimSpace(buf.String()))\n\tbuf.Reset()\n\n\t// testing the safecontent case\n\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"true\"))\n\tc.Context = ctx\n\trequire.NoError(t, act.FindFuzzy(c))\n\tbuf.Reset()\n\n\t// testing with the clip flag set\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"clip\": \"true\"}, \"fo\")\n\trequire.NoError(t, act.FindFuzzy(c))\n\tout := strings.TrimSpace(buf.String())\n\tassert.Contains(t, out, \"Found exact match in \\\"foo\\\"\")\n\tbuf.Reset()\n\n\t// safecontent case with force flag set\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"unsafe\": \"true\"}, \"fo\")\n\trequire.NoError(t, act.FindFuzzy(c))\n\tout = strings.TrimSpace(buf.String())\n\tassert.Contains(t, out, \"Found exact match in \\\"foo\\\"\\nsecret\")\n\tbuf.Reset()\n\n\t// stopping with the safecontent tests\n\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"false\"))\n\n\t// find yo\n\tc = gptest.CliCtx(ctx, t, \"yo\")\n\trequire.Error(t, act.FindFuzzy(c))\n\tbuf.Reset()\n\n\t// add some secrets\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"foo\")\n\t_, err = sec.Write([]byte(\"bar\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/zab\", sec))\n\tbuf.Reset()\n\n\t// find bar\n\tc = gptest.CliCtx(ctx, t, \"bar\")\n\trequire.NoError(t, act.FindFuzzy(c))\n\tassert.Equal(t, \"bar/baz\\nbar/zab\", strings.TrimSpace(buf.String()))\n\tbuf.Reset()\n\n\t// find w/o callback\n\tc = gptest.CliCtx(ctx, t)\n\trequire.NoError(t, act.find(ctx, c, \"foo\", nil, false))\n\tassert.Equal(t, \"foo\", strings.TrimSpace(buf.String()))\n\tbuf.Reset()\n\n\t// findSelection w/o callback\n\tc = gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.findSelection(ctx, c, []string{\"foo\", \"bar\"}, \"fo\", nil))\n\n\t// findSelection w/o options\n\tc = gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.findSelection(ctx, c, nil, \"fo\", func(_ context.Context, _ *cli.Context, _ string, _ bool) error { return nil }))\n\n\t// Test regex matching\n\t// Add a secret with a pattern that can be matched by a regex\n\trequire.NoError(t, act.Store.Set(ctx, \"test/regex123\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"test/no-match\", sec))\n\tbuf.Reset()\n\n\t// find using regex pattern\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"regex\": \"true\"}, \"regex.*\")\n\trequire.NoError(t, act.Find(c))\n\tassert.Equal(t, \"test/regex123\", strings.TrimSpace(buf.String()))\n\tbuf.Reset()\n\n\t// find using regex pattern with no match\n\tc = gptest.CliCtxWithFlags(ctx, t, map[string]string{\"regex\": \"true\"}, \"nomatch.*\")\n\trequire.Error(t, act.Find(c))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/fsck.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config/legacy\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Fsck checks the store integrity.\nfunc (s *Action) Fsck(c *cli.Context) error {\n\t_ = s.rem.Reset(\"fsck\")\n\n\tfilter := c.Args().First()\n\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.IsSet(\"decrypt\") {\n\t\tctx = leaf.WithFsckDecrypt(ctx, c.Bool(\"decrypt\"))\n\t}\n\n\tout.Printf(ctx, \"Checking password store integrity ...\")\n\n\t// clean up any previous config locations.\n\tfor _, oldCfg := range append(legacy.ConfigLocations(), filepath.Join(appdir.UserHome(), \".gopass.yml\")) {\n\t\tif fsutil.IsFile(oldCfg) {\n\t\t\tif err := os.Remove(oldCfg); err != nil {\n\t\t\t\tout.Errorf(ctx, \"Failed to remove old gopass config %s: %s\", oldCfg, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// display progress bar.\n\tpwList, err := s.fsckEntries(ctx, filter)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbar := termio.NewProgressBar(int64(len(pwList)) + 1)\n\tbar.Hidden = ctxutil.IsHidden(ctx)\n\tctx = ctxutil.WithProgressCallback(ctx, func() {\n\t\tbar.Inc()\n\t})\n\tctx = out.AddPrefix(ctx, \"\\n\")\n\n\t// the main work in done by the sub stores.\n\tif err := s.Store.Fsck(ctx, c.String(\"store\"), filter); err != nil {\n\t\treturn exit.Error(exit.Fsck, err, \"fsck found errors: %s\", err)\n\t}\n\tbar.Done()\n\n\treturn nil\n}\n\nfunc (s *Action) fsckEntries(ctx context.Context, filter string) ([]string, error) {\n\tt, err := s.Store.Tree(ctx)\n\tif err != nil {\n\t\treturn nil, exit.Error(exit.Unknown, err, \"failed to list stores: %s\", err)\n\t}\n\n\tif filter == \"\" {\n\t\treturn t.List(tree.INF), nil\n\t}\n\n\tif s.Store.Exists(ctx, filter) {\n\t\treturn []string{filter}, nil\n\t}\n\n\t// We restrict ourselves to the filter.\n\tt, err = t.FindFolder(filter)\n\tif err != nil {\n\t\treturn nil, exit.Error(exit.NotFound, nil, \"Entry %q not found\", filter)\n\t}\n\n\treturn t.List(tree.INF), nil\n}\n"
  },
  {
    "path": "internal/action/fsck_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/can\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFsck(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\tcolor.NoColor = true\n\n\t// generate foo/bar\n\tc := gptest.CliCtx(ctx, t, \"foo/bar\", \"24\")\n\trequire.NoError(t, act.Generate(c), \"gopass generate foo/bar 24\")\n\tbuf.Reset()\n\n\t// fsck\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctx, t)))\n\toutput := strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tassert.Contains(t, output, \"extra recipients on foo: [0xFEEDBEEF]\")\n\tbuf.Reset()\n\n\t// fsck (hidden)\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctxutil.WithHidden(ctx, true), t)))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.NotContains(t, output, \"Checking password store integrity ...\")\n\tassert.NotContains(t, output, \"extra recipients on foo: [0xFEEDBEEF]\")\n\tbuf.Reset()\n\n\t// fsck --decrypt\n\trequire.NoError(t, act.Fsck(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"decrypt\": \"true\"})))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tassert.Contains(t, output, \"extra recipients on foo: [0xFEEDBEEF]\")\n\tbuf.Reset()\n\n\t// fsck foo\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctx, t, \"foo\")))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tassert.Contains(t, output, \"extra recipients on foo: [0xFEEDBEEF]\")\n\tbuf.Reset()\n}\n\nfunc TestFsckGpg(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\tu := gptest.NewGUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\tcolor.NoColor = true\n\n\tsub, err := act.Store.GetSubStore(\"\")\n\trequire.NoError(t, err)\n\trequire.NoError(t, sub.ImportMissingPublicKeys(ctx, can.KeyID()))\n\n\t// generate foo/bar\n\tc := gptest.CliCtx(ctx, t, \"foo/bar\", \"24\")\n\trequire.NoError(t, act.Generate(c), \"gopass generate foo/bar 24\")\n\tbuf.Reset()\n\n\t// fsck\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctx, t)))\n\toutput := strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tbuf.Reset()\n\n\t// fsck (hidden)\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctxutil.WithHidden(ctx, true), t)))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.NotContains(t, output, \"Checking password store integrity ...\")\n\tassert.NotContains(t, output, \"Extra recipients on foo: [0xFEEDBEEF]\")\n\tbuf.Reset()\n\n\t// fsck --decrypt\n\trequire.NoError(t, act.Fsck(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"decrypt\": \"true\"})))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tbuf.Reset()\n\n\t// fsck foo\n\trequire.NoError(t, act.Fsck(gptest.CliCtx(ctx, t, \"foo\")))\n\toutput = strings.TrimSpace(buf.String())\n\tassert.Contains(t, output, \"Checking password store integrity ...\")\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/generate.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/clipboard\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/xkcdgen\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar reNumber = regexp.MustCompile(`^\\d+$`)\n\n// Generate and save a password.\nfunc (s *Action) Generate(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tctx = WithClip(ctx, c.Bool(\"clip\"))\n\tforce := c.Bool(\"force\")\n\tedit := c.Bool(\"edit\") // nolint:ifshort\n\n\targs, kvps := parseArgs(c)\n\tname := args.Get(0)\n\tkey, length := keyAndLength(args)\n\n\tctx = ctxutil.WithForce(ctx, force)\n\n\t// ask for name of the secret if it wasn't provided already.\n\tif name == \"\" {\n\t\tvar err error\n\t\tname, err = termio.AskForString(ctx, \"Which name do you want to use?\", \"\")\n\t\tif err != nil || name == \"\" {\n\t\t\treturn exit.Error(exit.NoName, err, \"please provide a password name\")\n\t\t}\n\t}\n\n\t// Check for custom commit message\n\tcommitMsg := \"Generated Password\"\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\t// ask for confirmation before overwriting existing entry.\n\tif !force { // don't check if it's force anyway.\n\t\tif s.Store.Exists(ctx, name) && key == \"\" && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"An entry already exists for %s. Overwrite the current password?\", name)) {\n\t\t\treturn exit.Error(exit.Aborted, nil, \"user aborted. not overwriting your current password\")\n\t\t}\n\t}\n\n\tmp := s.Store.MountPoint(name)\n\tctx = config.WithMount(ctx, mp)\n\n\t// generate password.\n\tpassword, err := s.generatePassword(ctx, c, length, name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// display or copy to clipboard.\n\tif err := s.generateCopyOrPrint(ctx, c, name, key, password); err != nil {\n\t\treturn err\n\t}\n\n\t// write generated password to store.\n\tctx, err = s.generateSetPassword(ctx, name, key, password, kvps, c.Bool(\"force-regen\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// if requested launch editor to add more data to the generated secret.\n\tif edit {\n\t\tc.Context = ctx\n\t\tif err := s.Edit(c); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to edit %q: %s\", name, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc keyAndLength(args argList) (string, string) {\n\tkey := args.Get(1)\n\tlength := args.Get(2)\n\n\t// generate can be called with one positional arg or two.\n\t// * one - the desired length for the \"master\" secret itself\n\t// * two - the key in a YAML doc and the length for a secret generated for this\n\t//   key only.\n\tif length == \"\" && key != \"\" && reNumber.MatchString(key) {\n\t\tlength = key\n\t\tkey = \"\"\n\t}\n\n\treturn key, length\n}\n\n// generateCopyOrPrint will print the password to the screen or copy to the\n// clipboard.\nfunc (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, key, password string) error {\n\tentry := name\n\tif key != \"\" {\n\t\tentry += \" \" + key\n\t}\n\n\tout.OKf(ctx, \"Password for entry %q generated\", entry)\n\n\t// copy to clipboard if:\n\t// - explicitly requested with -c\n\t// - autoclip=true, but only if output is not being redirected.\n\tif IsClip(ctx) || (config.AsBool(s.cfg.Get(\"generate.autoclip\")) && ctxutil.IsTerminal(ctx)) {\n\t\tif err := clipboard.CopyTo(ctx, name, []byte(password), config.AsInt(s.cfg.Get(\"core.cliptimeout\"))); err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"failed to copy to clipboard: %s\", err)\n\t\t}\n\t\t// if autoclip is on and we're not printing the password to the terminal\n\t\t// at least leave a notice that we did indeed copy it.\n\t\tif config.AsBool(s.cfg.Get(\"generate.autoclip\")) && !c.Bool(\"print\") {\n\t\t\tout.Print(ctx, \"Copied to clipboard\")\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif !c.Bool(\"print\") {\n\t\tout.Printf(ctx, \"Not printing secrets by default. Use 'gopass show %s' to display the password.\", entry)\n\n\t\treturn nil\n\t}\n\n\tif c.IsSet(\"print\") && !c.Bool(\"print\") && config.Bool(ctx, \"show.safecontent\") {\n\t\tdebug.Log(\"safecontent suppressing printing\")\n\n\t\treturn nil\n\t}\n\n\tout.Printf(\n\t\tctx,\n\t\t\"⚠ The generated password is:\\n\\n%s\\n\",\n\t\tout.Secret(password),\n\t)\n\n\treturn nil\n}\n\n// hasRuleForSecret extracts the domain from the secret name and checks if there is a\n// password rule for it. If so, it returns the domain and the rule. If the domain is\n// empty no rule is found.\nfunc hasPwRuleForSecret(ctx context.Context, name string) (string, pwrules.Rule) {\n\t// trim elements from the end of the path until we find a domain or the root.\n\tfor name != \"\" && name != \".\" {\n\t\td := path.Base(name)\n\t\tif r, found := pwrules.LookupRule(ctx, d); found {\n\t\t\treturn d, r\n\t\t}\n\t\tname = path.Dir(name)\n\t}\n\n\treturn \"\", pwrules.Rule{}\n}\n\n// generatePassword will run through the password generation steps.\nfunc (s *Action) generatePassword(ctx context.Context, c *cli.Context, length, name string) (string, error) {\n\tif domain, rule := hasPwRuleForSecret(ctx, name); domain != \"\" && !c.Bool(\"force\") {\n\t\treturn s.generatePasswordForRule(ctx, length, domain, rule)\n\t}\n\n\tcfg, mp := config.FromContext(ctx)\n\n\tgenerator := cfg.GetM(mp, \"generate.generator\")\n\tif c.IsSet(\"generator\") {\n\t\tgenerator = c.String(\"generator\")\n\t}\n\n\tif generator == \"xkcd\" {\n\t\treturn s.generatePasswordXKCD(ctx, c, length)\n\t}\n\n\tsymbols := false\n\tif c.IsSet(\"symbols\") {\n\t\tsymbols = c.Bool(\"symbols\")\n\t} else {\n\t\tif cfg.GetM(mp, \"generate.symbols\") != \"\" {\n\t\t\tsymbols = config.AsBool(cfg.GetM(mp, \"generate.symbols\"))\n\t\t}\n\t}\n\n\tvar pwlen int\n\tif length == \"\" {\n\t\tpwlength, err := getPwLengthFromEnvOrAskUser(ctx)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tpwlen = pwlength\n\t} else {\n\t\tiv, err := strconv.Atoi(length)\n\t\tif err != nil {\n\t\t\treturn \"\", exit.Error(exit.Usage, err, \"password length must be a number\")\n\t\t}\n\t\tpwlen = iv\n\t}\n\n\tif pwlen < 1 {\n\t\treturn \"\", exit.Error(exit.Usage, nil, \"password length must not be zero\")\n\t}\n\n\tswitch generator {\n\tcase \"memorable\":\n\t\tif isStrict(ctx, c) {\n\t\t\treturn pwgen.GenerateMemorablePassword(pwlen, symbols, true), nil\n\t\t}\n\n\t\treturn pwgen.GenerateMemorablePassword(pwlen, symbols, false), nil\n\tcase \"external\":\n\t\treturn pwgen.GenerateExternal(pwlen)\n\tdefault:\n\t\tif isStrict(ctx, c) {\n\t\t\treturn pwgen.GeneratePasswordWithAllClasses(pwlen, symbols)\n\t\t}\n\n\t\treturn pwgen.GeneratePassword(pwlen, symbols), nil\n\t}\n}\n\n// getPwLengthFromEnvOrAskUser either determines the password length through an\n// environment variable or asks the user to set one.\n// This function assumes that if the length is set via the environment variable,\n// the user has already made a conscious decision and does not need to be asked\n// again.\nfunc getPwLengthFromEnvOrAskUser(ctx context.Context) (int, error) {\n\tvar pwlen int\n\tcandidateLength, isCustom := config.DefaultPasswordLengthFromEnv(ctx)\n\tif !isCustom {\n\t\tquestion := \"How long should the password be?\"\n\t\tiv, err := termio.AskForInt(ctx, question, candidateLength)\n\t\tif err != nil {\n\t\t\treturn 0, exit.Error(exit.Usage, err, \"password length must be a number\")\n\t\t}\n\t\tpwlen = iv\n\t} else {\n\t\tpwlen = candidateLength\n\t}\n\n\treturn pwlen, nil\n}\n\n// generatePasswordForRule validates the user-provided password length against\n// the rule for the domain and condtionally prompts the user for a correct\n// length if the initial value is invalid.\nfunc (s *Action) generatePasswordForRule(ctx context.Context, length, domain string, rule pwrules.Rule) (string, error) {\n\tout.Noticef(ctx, \"Using password rules for %s ...\", domain)\n\n\tvar iv int\n\tvar err error\n\n\tif iv, err = strconv.Atoi(length); err != nil {\n\t\treturn \"\", exit.Error(exit.Usage, err, \"password length must be a number\")\n\t}\n\n\tif iv < rule.Minlen || iv > rule.Maxlen {\n\t\tdebug.Log(\n\t\t\t\"pw length %s does not match rule {min: %d, max: %d}, prompting for another one\",\n\t\t\tlength, rule.Minlen, rule.Maxlen,\n\t\t)\n\n\t\tquestion := fmt.Sprintf(\n\t\t\t\"How long should the password be? (min: %d, max: %d)\",\n\t\t\trule.Minlen, rule.Maxlen,\n\t\t)\n\n\t\tvar sv string\n\n\t\tif sv, err = termio.AskForString(ctx, question, strconv.Itoa(rule.Maxlen)); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// recursively prompt the user until a valid length is provided\n\t\treturn s.generatePasswordForRule(ctx, sv, domain, rule)\n\t}\n\n\tpw := pwgen.NewCrypticForDomain(ctx, iv, domain).Password()\n\tif pw == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed to generate password for %s\", domain)\n\t}\n\n\treturn pw, nil\n}\n\n// generatePasswordXKCD walks through the steps necessary to create an XKCD-style\n// password.\nfunc (s *Action) generatePasswordXKCD(ctx context.Context, c *cli.Context, length string) (string, error) {\n\tsep := config.String(c.Context, \"pwgen.xkcd-sep\")\n\tif c.IsSet(\"sep\") {\n\t\tsep = c.String(\"sep\")\n\t}\n\tlang := config.String(c.Context, \"pwgen.xkcd-lang\")\n\tif c.IsSet(\"lang\") {\n\t\tlang = c.String(\"lang\")\n\t}\n\tcapitalize := config.Bool(c.Context, \"pwgen.xkcd-capitalize\")\n\tif c.IsSet(\"xkcdcapitalize\") {\n\t\tcapitalize = c.Bool(\"xkcdcapitalize\")\n\t}\n\tnum := config.Bool(c.Context, \"pwgen.xkcd-numbers\")\n\tif c.IsSet(\"xkcdnumbers\") {\n\t\tnum = c.Bool(\"xkcdnumbers\")\n\t}\n\n\tpwlen := config.Int(c.Context, \"pwgen.xkcd-len\")\n\tswitch {\n\tcase length != \"\":\n\t\t// using the command line supplied value\n\t\tiv, err := strconv.Atoi(length)\n\t\tif err != nil {\n\t\t\treturn \"\", exit.Error(exit.Usage, err, \"password length must be a number: %s\", err)\n\t\t}\n\t\tpwlen = iv\n\tcase pwlen < 1:\n\t\t// no config value, nothing on the command line: ask the user\n\t\tquestion := \"How many words should be combined to a password?\"\n\t\tiv, err := termio.AskForInt(ctx, question, config.DefaultXKCDLength)\n\t\tif err != nil {\n\t\t\treturn \"\", exit.Error(exit.Usage, err, \"password length must be a number\")\n\t\t}\n\t\tpwlen = iv\n\tdefault:\n\t\t// no-op, using the config value\n\t}\n\n\tif pwlen < 1 {\n\t\treturn \"\", exit.Error(exit.Usage, nil, \"password length must not be zero\")\n\t}\n\n\treturn xkcdgen.RandomLengthDelim(pwlen, sep, lang, capitalize, num)\n}\n\n// generateSetPassword will update or create a secret.\nfunc (s *Action) generateSetPassword(ctx context.Context, name, key, password string, kvps map[string]string, regen bool) (context.Context, error) {\n\t// set a single key in an entry.\n\tif key != \"\" {\n\t\tsec, err := s.Store.Get(ctx, name)\n\t\tif err != nil {\n\t\t\treturn ctx, exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t\t}\n\n\t\tsetMetadata(sec, kvps)\n\t\t_ = sec.Set(key, password)\n\t\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\t\treturn ctx, exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t\t\t}\n\t\t\tout.Errorf(ctx, \"Password generation somehow obtained the same password as before: you might want to check your system's entropy pool\")\n\t\t}\n\n\t\treturn ctx, nil\n\t}\n\n\t// replace password in existing secret. we might be asked to skip the\n\t// check to enforce possibly re-evaluating templates.\n\tif !regen && s.Store.Exists(ctx, name) {\n\t\tctx, err := s.generateReplaceExisting(ctx, name, key, password, kvps)\n\t\tif err == nil {\n\t\t\treturn ctx, nil\n\t\t}\n\n\t\tout.Errorf(ctx, \"Failed to read existing secret. Creating anew. Error: %s\", err.Error())\n\t}\n\n\t// generate a completely new secret.\n\tvar sec gopass.Secret\n\tsec = secrets.New()\n\tsec.SetPassword(password)\n\tif u := hasChangeURL(ctx, name); u != \"\" {\n\t\t_ = sec.Set(\"password-change-url\", u)\n\t}\n\n\tif content, found := s.renderTemplate(ctx, name, []byte(password)); found {\n\t\tnSec := secrets.NewAKV()\n\t\tif _, err := nSec.Write(content); err == nil {\n\t\t\tsec = nSec\n\t\t} else {\n\t\t\tdebug.Log(\"failed to handle template: %s\", err)\n\t\t}\n\t}\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn ctx, exit.Error(exit.Encrypt, err, \"failed to create %q: %s\", name, err)\n\t\t}\n\t\tout.Errorf(ctx, \"Password generation somehow obtained the same password as before: you might want to check your system's entropy pool\")\n\t}\n\n\treturn ctx, nil\n}\n\nfunc hasChangeURL(ctx context.Context, name string) string {\n\tp := strings.Split(name, \"/\")\n\tfor i := len(p) - 1; i > 0; i-- {\n\t\tif u := pwrules.LookupChangeURL(ctx, p[i]); u != \"\" {\n\t\t\treturn u\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc (s *Action) generateReplaceExisting(ctx context.Context, name, key, password string, kvps map[string]string) (context.Context, error) {\n\tsec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn ctx, exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t}\n\n\tsetMetadata(sec, kvps)\n\tsec.SetPassword(password)\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn ctx, exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t\t}\n\t\tout.Errorf(ctx, \"Password generation somehow obtained the same password as before: you might want to check your system's entropy pool\")\n\t}\n\n\treturn ctx, nil\n}\n\nfunc setMetadata(sec gopass.Secret, kvps map[string]string) {\n\tfor k, v := range kvps {\n\t\tdebug.Log(\"setting %s to %s\", k, v)\n\t\t_ = sec.Set(k, v)\n\t}\n}\n\n// CompleteGenerate implements the completion heuristic for the generate command.\nfunc (s *Action) CompleteGenerate(c *cli.Context) {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.Args().Len() < 1 {\n\t\treturn\n\t}\n\tneedle := c.Args().Get(0) // nolint:ifshort\n\n\t_, err := s.Store.IsInitialized(ctx) // important to make sure the structs are not nil.\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Store not initialized: %s\", err)\n\n\t\treturn\n\t}\n\n\tlist, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif strings.Contains(needle, \"/\") {\n\t\tlist = filterPrefix(uniq(extractEmails(list)), path.Base(needle))\n\t} else {\n\t\tlist = filterPrefix(uniq(extractDomains(list)), needle)\n\t}\n\n\tfor _, v := range list {\n\t\tfmt.Fprintln(stdout, bashEscape(v))\n\t}\n}\n\nfunc extractEmails(list []string) []string {\n\tresults := make([]string, 0, len(list))\n\tfor _, e := range list {\n\t\te = path.Base(e)\n\t\tif strings.Contains(e, \"@\") || strings.Contains(e, \"_\") {\n\t\t\tresults = append(results, e)\n\t\t}\n\t}\n\n\treturn results\n}\n\nvar reDomain = regexp.MustCompile(`^(?i)([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}$`)\n\nfunc extractDomains(list []string) []string {\n\tresults := make([]string, 0, len(list))\n\tfor _, e := range list {\n\t\te = path.Base(e)\n\t\tif reDomain.MatchString(e) {\n\t\t\tresults = append(results, e)\n\t\t}\n\t}\n\n\treturn results\n}\n\nfunc uniq(in []string) []string {\n\tset := make(map[string]struct{}, len(in))\n\tfor _, e := range in {\n\t\tset[e] = struct{}{}\n\t}\n\n\tout := make([]string, 0, len(set))\n\tfor k := range set {\n\t\tout = append(out, k)\n\t}\n\n\tsort.Strings(out)\n\n\treturn out\n}\n\nfunc filterPrefix(in []string, prefix string) []string {\n\tout := make([]string, 0, len(in))\n\tfor _, e := range in {\n\t\tif strings.HasPrefix(e, prefix) {\n\t\t\tout = append(out, e)\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc isStrict(ctx context.Context, c *cli.Context) bool {\n\tcfg, mp := config.FromContext(ctx)\n\n\tif c.Bool(\"strict\") {\n\t\treturn true\n\t}\n\n\t// if the config option is not set, GetBoolM will return false by default\n\treturn config.AsBool(cfg.GetM(mp, \"generate.strict\"))\n}\n"
  },
  {
    "path": "internal/action/generate_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestRuleLookup(t *testing.T) {\n\tdomain, _ := hasPwRuleForSecret(config.NewContextInMemory(), \"foo/gopass.pw\")\n\tassert.Empty(t, domain)\n}\n\nfunc TestGenerate(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\tcolor.NoColor = true\n\n\t// generate\n\tt.Run(\"generate\", func(t *testing.T) {\n\t\trequire.Error(t, act.Generate(gptest.CliCtx(ctx, t)))\n\t\tbuf.Reset()\n\t})\n\n\t// generate foobar\n\tt.Run(\"generate foobar\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtx(ctx, t, \"foobar\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate foobar\n\t// should succeed because of always yes\n\tt.Run(\"generate foobar again\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtx(ctx, t, \"foobar\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --edit foobar\n\tt.Run(\"generate --edit foobar\", func(t *testing.T) {\n\t\tif testing.Short() || runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"edit\": \"true\", \"editor\": \"/usr/bin/env cat\"}, \"foobar\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar\n\tt.Run(\"generate --force foobar\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"foobar\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar 32\n\tt.Run(\"generate --force foobar 32\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"foobar\", \"32\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --symbols foobar 32\n\tt.Run(\"generate --force --symbols foobar 32\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"print\": \"true\", \"symbols\": \"true\"}, \"foobar\", \"32\")))\n\t\tpassIsAlphaNum(t, buf.String(), false)\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --symbols=false foobar 32\n\tt.Run(\"generate --force --symbols=False foobar 32\", func(t *testing.T) {\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"print\": \"true\", \"symbols\": \"false\"}, \"foobar\", \"32\")))\n\t\tpassIsAlphaNum(t, buf.String(), true)\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --xkcd foobar 32\n\tt.Run(\"generate --force --xkcd foobar 32\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"xkcd\": \"true\", \"lang\": \"en\"}, \"foobar\", \"32\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --xkcd foobar baz 32\n\tt.Run(\"generate --force --xkcd foobar baz 32\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"xkcd\": \"true\", \"lang\": \"en\"}, \"foobar\", \"baz\", \"32\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --xkcd foobar baz\n\tt.Run(\"generate --force --xkcd foobar baz\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"xkcd\": \"true\", \"lang\": \"en\"}, \"foobar\", \"baz\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force --xkcd --print foobar baz\n\tt.Run(\"generate --force --xkcd --print foobar baz\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"xkcd\": \"true\", \"print\": \"true\", \"lang\": \"en\"}, \"foobar\", \"baz\")))\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar 24 w/ autoclip and output redirection\n\tt.Run(\"generate --force foobar 24\", func(t *testing.T) {\n\t\tov := act.cfg.Get(\"generate.autoclip\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", ov))\n\t\t}()\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"true\"))\n\t\tctx := ctxutil.WithTerminal(ctx, false)\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"foobar\", \"24\")))\n\t\tassert.Contains(t, buf.String(), \"Not printing secrets by default\")\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar 24 w/ autoclip and no output redirection\n\tt.Run(\"generate --force foobar 24\", func(t *testing.T) {\n\t\tov := act.cfg.Get(\"generate.autoclip\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", ov))\n\t\t}()\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"true\"))\n\t\tctx := ctxutil.WithTerminal(ctx, true)\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"foobar\", \"24\")))\n\t\tassert.Contains(t, buf.String(), \"Copied to clipboard\")\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar w/ pw length set via env variable (42 chars)\n\tt.Run(\"generate --force foobar\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_PW_DEFAULT_LENGTH\", \"42\")\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"print\": \"true\", \"symbols\": \"false\"}, \"foobar\")))\n\t\tlines := strings.Split(strings.TrimSpace(buf.String()), \"\\n\")\n\t\tassert.Len(t, lines[3], 42)\n\t\tbuf.Reset()\n\t})\n\n\t// generate --force foobar w/ pw length set via env variable to invalid value, fallback mechanism\n\tt.Run(\"generate --force foobar\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_PW_DEFAULT_LENGTH\", \"0\")\n\n\t\tif testing.Short() {\n\t\t\tt.Skip(\"skipping test in short mode.\")\n\t\t}\n\n\t\trequire.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\", \"print\": \"true\", \"symbols\": \"false\"}, \"foobar\")))\n\t\tlines := strings.Split(strings.TrimSpace(buf.String()), \"\\n\")\n\t\tassert.Len(t, lines[3], 24) // 24 = default value used as fallback\n\t\tbuf.Reset()\n\t})\n}\n\nfunc passIsAlphaNum(t *testing.T, buf string, want bool) {\n\tt.Helper()\n\n\treAlphaNum := regexp.MustCompile(`^[A-Za-z0-9]+$`)\n\tlines := strings.Split(strings.TrimSpace(buf), \"\\n\")\n\tif len(lines) < 1 {\n\t\tt.Errorf(\"buffer empty (no lines)\")\n\t}\n\tline := strings.TrimSpace(lines[len(lines)-1])\n\tif reAlphaNum.MatchString(line) != want {\n\t\tt.Errorf(\"buffer did not match alpha num re: %s (%s)\", line, buf)\n\t}\n}\n\nfunc TestKeyAndLength(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin     []string\n\t\tkey    string\n\t\tlength string\n\t}{\n\t\t{\n\t\t\tin:     []string{\"32\"},\n\t\t\tkey:    \"\",\n\t\t\tlength: \"32\",\n\t\t},\n\t\t{\n\t\t\tin:     []string{\"baz\"},\n\t\t\tkey:    \"baz\",\n\t\t\tlength: \"\",\n\t\t},\n\t\t{\n\t\t\tin:     []string{\"baz\", \"32\"},\n\t\t\tkey:    \"baz\",\n\t\t\tlength: \"32\",\n\t\t},\n\t\t{\n\t\t\tin:     []string{},\n\t\t\tkey:    \"\",\n\t\t\tlength: \"\",\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%v\", tc.in), func(t *testing.T) {\n\t\t\tapp := cli.NewApp()\n\t\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\t\trequire.NoError(t, fs.Parse(append([]string{\"foobar\"}, tc.in...)))\n\t\t\tc := cli.NewContext(app, fs, nil)\n\t\t\targs, _ := parseArgs(c)\n\t\t\tk, l := keyAndLength(args)\n\t\t\tassert.Equal(t, tc.key, k, \"Key from %+v\", tc.in)\n\t\t\tassert.Equal(t, tc.length, l, \"Length from %+v\", tc.in)\n\t\t})\n\t}\n}\n\nfunc TestExtractEmails(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin  []string\n\t\tout []string\n\t}{\n\t\t{\n\t\t\tout: []string{},\n\t\t},\n\t\t{\n\t\t\tin:  []string{\"some/mount/gmail.com/john.doe@example.org\", \"example.com/user@example.org\"},\n\t\t\tout: []string{\"john.doe@example.org\", \"user@example.org\"},\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%v\", tc.in), func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.out, extractEmails(tc.in))\n\t\t})\n\t}\n}\n\nfunc TestExtractDomains(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin  []string\n\t\tout []string\n\t}{\n\t\t{\n\t\t\tout: []string{},\n\t\t},\n\t\t{\n\t\t\tin:  []string{\"websites/gmail.com\", \"live.com\", \"some/mount/websites/web.de\"},\n\t\t\tout: []string{\"gmail.com\", \"live.com\", \"web.de\"},\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%v\", tc.in), func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.out, extractDomains(tc.in))\n\t\t})\n\t}\n}\n\nfunc TestUniq(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin  []string\n\t\tout []string\n\t}{\n\t\t{\n\t\t\tout: []string{},\n\t\t},\n\t\t{\n\t\t\tin:  []string{\"foo\", \"foo\", \"bar\"},\n\t\t\tout: []string{\"bar\", \"foo\"},\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%v\", tc.in), func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.out, uniq(tc.in))\n\t\t})\n\t}\n}\n\nfunc TestFilterPrefix(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin     []string\n\t\tprefix string\n\t\tout    []string\n\t}{\n\t\t{\n\t\t\tout: []string{},\n\t\t},\n\t\t{\n\t\t\tin:     []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tprefix: \"foo\",\n\t\t\tout:    []string{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tin:     []string{\"foo/bar\", \"foo/baz\", \"bar/foo\"},\n\t\t\tprefix: \"foo/\",\n\t\t\tout:    []string{\"foo/bar\", \"foo/baz\"},\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%v\", tc.in), func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.out, filterPrefix(tc.in, tc.prefix))\n\t\t})\n\t}\n}\n\n// NOTE: Do not use t.Parallel because environment variables are being used\n// which can leak into other tests that run in parallel.\nfunc TestDefaultLengthFromEnv(t *testing.T) {\n\tconst pwLengthEnvName = \"GOPASS_PW_DEFAULT_LENGTH\"\n\n\tctx := config.NewContextInMemory()\n\n\tt.Run(\"use default value if no environment variable is set\", func(t *testing.T) {\n\t\tactual, isCustom := config.DefaultPasswordLengthFromEnv(ctx)\n\t\texpected := config.DefaultPasswordLength\n\t\tassert.Equal(t, expected, actual)\n\t\tassert.False(t, isCustom)\n\t})\n\n\tt.Run(\"interpretetion of various inputs for environment variable\", func(t *testing.T) {\n\t\tfor _, tc := range []struct {\n\t\t\tin       string\n\t\t\texpected int\n\t\t\tcustom   bool\n\t\t}{\n\t\t\t{in: \"42\", expected: 42, custom: true},\n\t\t\t{in: \"1\", expected: 1, custom: true},\n\t\t\t{in: \"0\", expected: config.DefaultPasswordLength, custom: false},\n\t\t\t{in: \"abc\", expected: config.DefaultPasswordLength, custom: false},\n\t\t\t{in: \"-1\", expected: config.DefaultPasswordLength, custom: false},\n\t\t} {\n\t\t\tt.Setenv(pwLengthEnvName, tc.in)\n\t\t\tactual, isCustom := config.DefaultPasswordLengthFromEnv(ctx)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t\tassert.Equal(t, isCustom, tc.custom)\n\t\t}\n\t})\n}\n\nfunc TestHasPwRuleForSecret(t *testing.T) {\n\tctx := t.Context()\n\n\twantRule := pwrules.Rule{\n\t\tMinlen:   8,\n\t\tMaxlen:   63,\n\t\tRequired: []string{\"digit\", \"lower\", \"upper\"},\n\t\tAllowed:  []string{\"ascii-printable\"},\n\t\tExact:    false,\n\t}\n\tfor _, tc := range []struct {\n\t\tname   string\n\t\tinput  string\n\t\tdomain string\n\t\twant   pwrules.Rule\n\t}{\n\t\t{\n\t\t\tname:   \"domain only\",\n\t\t\tinput:  \"websites/apple.com\",\n\t\t\tdomain: \"apple.com\",\n\t\t\twant:   wantRule,\n\t\t},\n\t\t{\n\t\t\tname:   \"domain and username\",\n\t\t\tinput:  \"websites/apple.com/gopass\",\n\t\t\tdomain: \"apple.com\",\n\t\t\twant:   wantRule,\n\t\t},\n\t\t{\n\t\t\tname:   \"domain and email\",\n\t\t\tinput:  \"websites/apple.com/gopass@gopass.pw\",\n\t\t\tdomain: \"apple.com\",\n\t\t\twant:   wantRule,\n\t\t},\n\t\t{\n\t\t\tname:   \"domain and user that looks like a domain\",\n\t\t\tinput:  \"websites/apple.com/gopass.pw\",\n\t\t\tdomain: \"apple.com\",\n\t\t\twant:   wantRule,\n\t\t},\n\t\t{\n\t\t\tname:   \"empty input\",\n\t\t\tinput:  \"\",\n\t\t\tdomain: \"\",\n\t\t\twant:   pwrules.Rule{},\n\t\t},\n\t\t{\n\t\t\t// domains are search for starting from the end of the string, so\n\t\t\t// the the further right the domain is, the more specific it is.\n\t\t\tname:   \"double domains\",\n\t\t\tinput:  \"websites/apple.com/google.com\",\n\t\t\tdomain: \"google.com\",\n\t\t\twant: pwrules.Rule{\n\t\t\t\tMinlen:    8,\n\t\t\t\tMaxlen:    0,\n\t\t\t\tRequired:  []string{},\n\t\t\t\tAllowed:   []string{\"\", \"digit\", \"lower\", \"upper\"},\n\t\t\t\tMaxconsec: 0,\n\t\t\t\tExact:     false,\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdomain, rule := hasPwRuleForSecret(ctx, tc.input)\n\t\t\tassert.Equal(t, tc.domain, domain, tc.name)\n\t\t\tassert.Equal(t, tc.want, rule, tc.name)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/action/git.go",
    "content": "package action\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Git passes the git command to the underlying backend.\nfunc (s *Action) Git(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\n\tsub, err := s.Store.GetSubStore(store)\n\tif err != nil || sub == nil {\n\t\treturn exit.Error(exit.Git, err, \"failed to get sub store %s: %s\", store, err)\n\t}\n\n\targs := c.Args().Slice()\n\tout.Noticef(ctx, \"Running 'git %s' in %s...\", strings.Join(args, \" \"), sub.Path())\n\tcmd := exec.CommandContext(ctx, \"git\", args...)\n\tcmd.Dir = sub.Path()\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "internal/action/grep.go",
    "content": "package action\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Grep searches a string inside the content of all files.\nfunc (s *Action) Grep(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif !c.Args().Present() {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s grep arg\", s.Name)\n\t}\n\n\t// get the search term.\n\tneedle := c.Args().First()\n\n\thaystack, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list store: %s\", err)\n\t}\n\n\tmatchFn := func(haystack string) bool {\n\t\treturn strings.Contains(haystack, needle)\n\t}\n\n\tif c.Bool(\"regexp\") {\n\t\tre, err := regexp.Compile(needle)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Usage, err, \"failed to compile regexp %q: %s\", needle, err)\n\t\t}\n\t\tmatchFn = re.MatchString\n\t}\n\n\tvar matches int\n\tvar errors int\n\tfor _, v := range haystack {\n\t\tsec, err := s.Store.Get(ctx, v)\n\t\tif err != nil {\n\t\t\tout.Errorf(ctx, \"failed to decrypt %s: %v\", v, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif matchFn(string(sec.Bytes())) {\n\t\t\tout.Printf(ctx, \"%s matches\", color.BlueString(v))\n\t\t}\n\t}\n\n\tif errors > 0 {\n\t\tout.Warningf(ctx, \"%d secrets failed to decrypt\", errors)\n\t}\n\tout.Printf(ctx, \"\\nScanned %d secrets. %d matches, %d errors\", len(haystack), matches, errors)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/grep_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGrep(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tc := gptest.CliCtx(ctx, t, \"foo\")\n\tt.Run(\"empty store\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Grep(c))\n\t})\n\n\tt.Run(\"add some secret\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"foobar\")\n\t\t_, err := sec.Write([]byte(\"foobar\"))\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, act.Store.Set(ctx, \"foo\", sec))\n\t})\n\n\tt.Run(\"should find existing\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Grep(c))\n\t})\n\n\tt.Run(\"RE2\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"regexp\": \"true\"}, \"f..bar\")\n\t\trequire.NoError(t, act.Grep(c))\n\t})\n}\n"
  },
  {
    "path": "internal/action/history.go",
    "content": "package action\n\nimport (\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// History displays the history of a given secret.\nfunc (s *Action) History(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().Get(0)\n\tshowPassword := c.Bool(\"password\")\n\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s history <NAME>\", s.Name)\n\t}\n\n\tif !s.Store.Exists(ctx, name) {\n\t\treturn exit.Error(exit.NotFound, nil, \"Secret not found\")\n\t}\n\n\trevs, err := s.Store.ListRevisions(ctx, name)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to get revisions: %s\", err)\n\t}\n\n\tfor _, rev := range revs {\n\t\tpw := \"\"\n\t\tif showPassword {\n\t\t\t_, sec, err := s.Store.GetRevision(ctx, name, rev.Hash)\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"Failed to get revision %q of %q: %s\", rev.Hash, name, err)\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tpw = \" - \" + sec.Password()\n\t\t\t}\n\t\t}\n\t\tout.Printf(ctx, \"%s - %s <%s> - %s - %s%s\\n\", rev.Hash, rev.AuthorName, rev.AuthorEmail, rev.Date.Format(time.RFC3339), rev.Subject, pw)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/history_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHistory(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tr1 := gptest.UnsetVars(termio.NameVars...)\n\tr2 := gptest.UnsetVars(termio.EmailVars...)\n\tdefer r1()\n\tdefer r2()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\n\tcfg := config.NewInMemory()\n\trequire.NoError(t, cfg.SetPath(u.StoreDir(\"\")))\n\n\tact, err := newAction(cfg, semver.Version{}, false)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tt.Run(\"can initialize\", func(t *testing.T) {\n\t\trequire.NoError(t, act.IsInitialized(gptest.CliCtx(ctx, t)))\n\t})\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"init git\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.rcsInit(ctx, \"\", \"foo bar\", \"foo.bar@example.org\"))\n\t\tt.Logf(\"init git: %s\", buf.String())\n\t})\n\n\tt.Run(\"insert bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Insert(gptest.CliCtx(ctx, t, \"bar\")))\n\t})\n\n\tt.Run(\"history bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.History(gptest.CliCtx(ctx, t, \"bar\")))\n\t})\n\n\tt.Run(\"history --password bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.History(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"password\": \"true\"}, \"bar\")))\n\t})\n}\n"
  },
  {
    "path": "internal/action/init.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst logo = `\n   __     _    _ _      _ _   ___   ___\n /'_ '\\ /'_'\\ ( '_'\\  /'_' )/',__)/',__)\n( (_) |( (_) )| (_) )( (_| |\\__, \\\\__, \\\n'\\__  |'\\___/'| ,__/''\\__,_)(____/(____/\n( )_) |       | |\n \\___/'       (_)\n`\n\n// IsInitialized returns an error if the store is not properly\n// prepared.\nfunc (s *Action) IsInitialized(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tinited, err := s.Store.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialize store: %s\", err)\n\t}\n\n\tif inited {\n\t\tdebug.Log(\"Store is fully initialized and ready to go\\n\\nAll systems go. 🚀\\n\")\n\t\tname := c.Args().First()\n\t\t// setting the mount point here is not enough when we're using the REPL mode\n\t\tctx = config.WithMount(ctx, s.Store.MountPoint(name))\n\t\ts.printReminder(ctx)\n\t\tif c.Command.Name != \"sync\" && !c.Bool(\"nosync\") {\n\t\t\t_ = s.autoSync(ctx)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"Store needs to be initialized.\\n\\nAbort. Abort. Abort. 🚫\\n\")\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn exit.Error(exit.NotInitialized, nil, \"password-store is not initialized. Try '%s init'\", s.Name)\n\t}\n\n\tout.Printf(ctx, logo)\n\tout.Printf(ctx, \"🌟 Welcome to gopass!\")\n\tout.Noticef(ctx, \"No existing configuration found.\")\n\n\tcontSetup, err := termio.AskForBool(ctx, \"❓ Do you want to continue to setup?\", false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif contSetup {\n\t\treturn s.Setup(c)\n\t}\n\n\tout.Printf(ctx, \"☝ Please run 'gopass setup'\")\n\n\treturn exit.Error(exit.NotInitialized, err, \"not initialized\")\n}\n\n// Init a new password store with a first gpg id.\nfunc (s *Action) Init(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tpath := c.String(\"path\")\n\talias := c.String(\"store\")\n\n\tctx, err := initParseContext(ctx, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\tout.Printf(ctx, \"🍭 Initializing a new password store ...\")\n\n\tif name := termio.DetectName(c.Context, c); name != \"\" {\n\t\tctx = ctxutil.WithUsername(ctx, name)\n\t}\n\n\tif email := termio.DetectEmail(c.Context, c); email != \"\" {\n\t\tctx = ctxutil.WithEmail(ctx, email)\n\t}\n\n\tinited, err := s.Store.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialized store: %s\", err)\n\t}\n\n\tif inited {\n\t\tout.Errorf(ctx, \"Store is already initialized!\")\n\t}\n\n\tif err := s.init(ctx, alias, path, c.Args().Slice()...); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to initialize store: %s\", err)\n\t}\n\n\treturn nil\n}\n\nfunc initParseContext(ctx context.Context, c *cli.Context) (context.Context, error) {\n\tif c.IsSet(\"crypto\") {\n\t\tvar err error\n\t\tctx, err = backend.WithCryptoBackendString(ctx, c.String(\"crypto\"))\n\t\tif err != nil {\n\t\t\treturn ctx, exit.Error(exit.Unknown, err, \"Failed to set crypto backend: %s\", err)\n\t\t}\n\t}\n\n\tif c.IsSet(\"storage\") {\n\t\tvar err error\n\t\tctx, err = backend.WithStorageBackendString(ctx, c.String(\"storage\"))\n\t\tif err != nil {\n\t\t\treturn ctx, exit.Error(exit.Unknown, err, \"Failed to set storage backend: %s\", err)\n\t\t}\n\t}\n\n\tif !backend.HasCryptoBackend(ctx) {\n\t\tdebug.Log(\"Using default Crypto Backend (GPGCLI)\")\n\t\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\t}\n\n\tif !backend.HasStorageBackend(ctx) {\n\t\tdebug.Log(\"Using default storage backend (GitFS)\")\n\t\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\t}\n\n\tsb := backend.GetStorageBackend(ctx)\n\tif sb == backend.CryptFS {\n\t\tout.Warning(ctx, \"⚠ CryptFS is an experimental backend. Use at your own risk! ⚠\")\n\t}\n\tif sb == backend.JJFS {\n\t\tout.Warning(ctx, \"⚠ JJFS is an experimental backend. Use at your own risk! ⚠\")\n\t}\n\n\treturn ctx, nil\n}\n\nfunc (s *Action) init(ctx context.Context, alias, path string, keys ...string) error {\n\tif path == \"\" {\n\t\tif alias != \"\" {\n\t\t\tpath = config.PwStoreDir(alias)\n\t\t} else {\n\t\t\tpath = s.Store.Path()\n\t\t}\n\t}\n\tpath = fsutil.CleanPath(path)\n\n\tdebug.Log(\"Initializing Store %q in %q for %+v\", alias, path, keys)\n\n\tout.Printf(ctx, \"🔑 Searching for usable private Keys ...\")\n\tdebug.Log(\"Checking private keys for: %+v\", keys)\n\tcrypto := s.getCryptoFor(ctx, alias)\n\n\t// private key selection doesn't matter for plain. save one question.\n\t// TODO should ask the backend\n\tif crypto.Name() == \"plain\" {\n\t\tkeys, _ = crypto.ListIdentities(ctx)\n\t}\n\n\tif len(keys) < 1 {\n\t\tif crypto.Name() != \"age\" {\n\t\t\tout.Notice(ctx, \"Hint: Use 'gopass init <subkey> to use subkeys!'\")\n\t\t}\n\t\tnk, err := cui.AskForPrivateKey(ctx, crypto, \"🎮 Please select a private key for encrypting secrets:\")\n\t\tif err != nil {\n\t\t\tout.Noticef(ctx, \"Hint: Use 'gopass setup --crypto %s' to be guided through an initial setup instead of 'gopass init'\", crypto.Name())\n\n\t\t\treturn fmt.Errorf(\"failed to read user input: %w\", err)\n\t\t}\n\t\tkeys = []string{nk}\n\t}\n\n\tdebug.Log(\"Initializing sub store - Alias: %q - Path: %q - Keys: %+v\", alias, path, keys)\n\tif err := s.Store.Init(ctx, alias, path, keys...); err != nil {\n\t\treturn fmt.Errorf(\"failed to init store %q at %q: %w\", alias, path, err)\n\t}\n\n\tif alias != \"\" && path != \"\" {\n\t\tdebug.Log(\"Mounting sub store %q -> %q\", alias, path)\n\t\tif err := s.Store.AddMount(ctx, alias, path); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add mount %q: %w\", alias, err)\n\t\t}\n\t}\n\n\tif backend.HasStorageBackend(ctx) {\n\t\tbn := backend.StorageBackendName(backend.GetStorageBackend(ctx))\n\t\tdebug.Log(\"Initializing RCS (%s) ...\", bn)\n\t\tif err := s.rcsInit(ctx, alias, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx)); err != nil {\n\t\t\tdebug.Log(\"Stacktrace: %+v\\n\", err)\n\t\t\tout.Errorf(ctx, \"❌ Failed to init Version Control (%s): %s\", bn, err)\n\t\t}\n\t\tdebug.Log(\"RCS initialized as %s\", s.Store.Storage(ctx, alias).Name())\n\t} else {\n\t\tdebug.Log(\"not initializing RCS backend ...\")\n\t}\n\n\tout.Printf(ctx, \"🏁 Password store %s initialized for:\", path)\n\ts.printRecipients(ctx, alias)\n\n\treturn nil\n}\n\nfunc (s *Action) printRecipients(ctx context.Context, alias string) {\n\tcrypto := s.Store.Crypto(ctx, alias)\n\tfor _, recipient := range s.Store.ListRecipients(ctx, alias) {\n\t\tif kl, err := crypto.FindRecipients(ctx, recipient); err == nil && len(kl) > 0 {\n\t\t\trecipient = crypto.FormatKey(ctx, kl[0], \"\")\n\t\t}\n\t\tout.Printf(ctx, \"📩 \"+recipient)\n\t}\n}\n\nfunc (s *Action) getCryptoFor(ctx context.Context, name string) backend.Crypto {\n\treturn s.Store.Crypto(ctx, name)\n}\n"
  },
  {
    "path": "internal/action/init_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\tctx = backend.WithStorageBackend(ctx, backend.FS)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tc := gptest.CliCtx(ctx, t, \"foo.bar@example.org\")\n\trequire.NoError(t, act.IsInitialized(c))\n\trequire.Error(t, act.Init(c))\n\trequire.NoError(t, act.Setup(c))\n\n\tcrypto := act.Store.Crypto(ctx, \"\")\n\trequire.NotNil(t, crypto)\n\tassert.Equal(t, \"plain\", crypto.Name())\n\tassert.True(t, act.initHasUseablePrivateKeys(ctx, crypto))\n\trequire.Error(t, act.initGenerateIdentity(ctx, crypto, \"foo bar\", \"foo.bar@example.org\"))\n\tbuf.Reset()\n\n\tact.printRecipients(ctx, \"\")\n\tassert.Contains(t, buf.String(), \"0xDEADBEEF\")\n\tbuf.Reset()\n\n\t// un-initialize the store\n\trequire.NoError(t, os.Remove(filepath.Join(u.StoreDir(\"\"), plain.IDFile)))\n\trequire.Error(t, act.IsInitialized(c))\n\tbuf.Reset()\n}\n\nfunc TestInitParseContext(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname  string\n\t\tflags map[string]string\n\t\tcheck func(context.Context) error\n\t}{\n\t\t{\n\t\t\tname:  \"crypto age\",\n\t\t\tflags: map[string]string{\"crypto\": \"age\"},\n\t\t\tcheck: func(ctx context.Context) error {\n\t\t\t\tif be := backend.GetCryptoBackend(ctx); be != backend.Age {\n\t\t\t\t\treturn fmt.Errorf(\"wrong backend: %d\", be)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tcheck: func(ctx context.Context) error {\n\t\t\t\tif backend.GetStorageBackend(ctx) != backend.GitFS {\n\t\t\t\t\treturn fmt.Errorf(\"wrong backend\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := gptest.CliCtxWithFlags(config.NewContextInMemory(), t, tc.flags)\n\t\t\tctx, err := initParseContext(c.Context, c)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, tc.check(ctx), tc.name)\n\t\t\tbuf.Reset()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/action/insert.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/audit\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Insert a string as content to a secret file.\nfunc (s *Action) Insert(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\techo := c.Bool(\"echo\")\n\tmultiline := c.Bool(\"multiline\")\n\tforce := c.Bool(\"force\")\n\tappending := c.Bool(\"append\")\n\n\targs, kvps := parseArgs(c)\n\tname := args.Get(0)\n\tkey := args.Get(1)\n\n\tif name == \"\" {\n\t\treturn exit.Error(exit.NoName, nil, \"Usage: %s insert name\", s.Name)\n\t}\n\n\treturn s.insert(ctx, c, name, key, echo, multiline, force, appending, kvps)\n}\n\nfunc (s *Action) insert(ctx context.Context, c *cli.Context, name, key string, echo, multiline, force, appending bool, kvps map[string]string) error {\n\tvar content []byte\n\n\t// Check for custom commit message\n\tcommitMsg := \"Inserted user supplied password\"\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\t// if content is piped to stdin, read and save it.\n\tif ctxutil.IsStdin(ctx) {\n\t\tbuf := &bytes.Buffer{}\n\n\t\tif written, err := io.Copy(buf, stdin); err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"failed to copy after %d bytes: %s\", written, err)\n\t\t}\n\n\t\tcontent = buf.Bytes()\n\t}\n\n\t// update to a single YAML entry.\n\tif key != \"\" {\n\t\treturn s.insertYAML(ctx, name, key, content, kvps)\n\t}\n\n\tif ctxutil.IsStdin(ctx) {\n\t\tif !force && !appending && s.Store.Exists(ctx, name) {\n\t\t\treturn exit.Error(exit.Aborted, nil, \"not overwriting your current secret\")\n\t\t}\n\n\t\treturn s.insertStdin(ctx, name, content, appending)\n\t}\n\n\t// don't check if it's force anyway.\n\tif !force && s.Store.Exists(ctx, name) && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"An entry already exists for %s. Overwrite it?\", name)) {\n\t\treturn exit.Error(exit.Aborted, nil, \"not overwriting your current secret\")\n\t}\n\n\t// if multi-line input is requested start an editor.\n\tif multiline && ctxutil.IsInteractive(ctx) {\n\t\treturn s.insertMultiline(ctx, c, name)\n\t}\n\n\t// if echo mode is requested use a simple string input function.\n\tif echo {\n\t\tctx = termio.WithPassPromptFunc(ctx, func(ctx context.Context, prompt string) (string, error) {\n\t\t\treturn termio.AskForString(ctx, prompt, \"\")\n\t\t})\n\t}\n\n\tpw, err := termio.AskForPassword(ctx, fmt.Sprintf(\"password for %s\", name), true)\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"failed to ask for password: %s\", err)\n\t}\n\n\treturn s.insertSingle(ctx, name, pw, kvps)\n}\n\nfunc (s *Action) insertStdin(ctx context.Context, name string, content []byte, appendTo bool) error {\n\tvar sec gopass.Secret = secrets.ParseAKV(content)\n\n\tif appendTo && s.Store.Exists(ctx, name) {\n\t\tvar err error\n\t\tsec, err = s.insertStdinAppend(ctx, name, content)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to set %q: %s\", name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) insertStdinAppend(ctx context.Context, name string, content []byte) (gopass.Secret, error) {\n\teSec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn nil, exit.Error(exit.Decrypt, err, \"failed to decrypt existing secret: %s\", err)\n\t}\n\n\tsecW, ok := eSec.(io.Writer)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%T is not an io.Writer\", eSec)\n\t}\n\n\tif _, err := secW.Write(content); err != nil {\n\t\treturn nil, exit.Error(exit.Encrypt, err, \"failed to write %q: %q\", content, err)\n\t}\n\n\tdebug.Log(\"wrote to secretWriter\")\n\n\treturn eSec, nil\n}\n\nfunc (s *Action) insertSingle(ctx context.Context, name, pw string, kvps map[string]string) error {\n\tsec, err := s.insertGetSecret(ctx, name, pw)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsetMetadata(sec, kvps)\n\n\t// we only update the pw if the kvps were not set or if it's non-empty, because otherwise we were updating the kvps.\n\tif pw != \"\" || len(kvps) == 0 {\n\t\tsec.SetPassword(pw)\n\t\taudit.Single(ctx, pw)\n\t}\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to write secret %q: %s\", name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) insertGetSecret(ctx context.Context, name, pw string) (gopass.Secret, error) {\n\tif s.Store.Exists(ctx, name) {\n\t\tsec, err := s.Store.Get(ctx, name)\n\t\tif err != nil {\n\t\t\treturn nil, exit.Error(exit.Decrypt, err, \"failed to decrypt existing secret: %s\", err)\n\t\t}\n\n\t\treturn sec, nil\n\t}\n\n\tcontent, found := s.renderTemplate(ctx, name, []byte(pw))\n\t// no template found\n\tif !found {\n\t\treturn secrets.New(), nil\n\t}\n\n\t// render template into a new secret\n\tsec := secrets.NewAKV()\n\tif _, err := sec.Write(content); err != nil {\n\t\tdebug.Log(\"failed to handle template: %s\", err)\n\n\t\treturn secrets.New(), nil\n\t}\n\n\treturn sec, nil\n}\n\n// insertYAML will overwrite existing keys.\nfunc (s *Action) insertYAML(ctx context.Context, name, key string, content []byte, kvps map[string]string) error {\n\tdebug.Log(\"insertYAML: %s - %s -> %s\", name, key, content)\n\tif ctxutil.IsInteractive(ctx) {\n\t\tpw, err := termio.AskForString(ctx, name+\":\"+key, \"\")\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"failed to ask for user input: %s\", err)\n\t\t}\n\t\tcontent = []byte(pw)\n\t}\n\n\tvar sec gopass.Secret\n\tif s.Store.Exists(ctx, name) {\n\t\tvar err error\n\t\tsec, err = s.Store.Get(ctx, name)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t\t}\n\t\tdebug.Log(\"using existing secret %s\", name)\n\t} else {\n\t\tsec = secrets.New()\n\t\tdebug.Log(\"creating new secret %s\", name)\n\t}\n\n\tsetMetadata(sec, kvps)\n\n\tdebug.Log(\"setting %s to %s\", key, string(content))\n\tif err := sec.Set(key, string(content)); err != nil {\n\t\treturn exit.Error(exit.Usage, err, \"failed set key %q of %q: %q\", key, name, err)\n\t}\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to set key %q of %q: %s\", key, name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) insertMultiline(ctx context.Context, c *cli.Context, name string) error {\n\tbuf := []byte{}\n\tif s.Store.Exists(ctx, name) {\n\t\tvar err error\n\t\tsec, err := s.Store.Get(ctx, name)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Decrypt, err, \"failed to decrypt existing secret: %s\", err)\n\t\t}\n\t\tbuf = sec.Bytes()\n\t}\n\ted := editor.Path(c)\n\tcontent, err := editor.Invoke(ctx, ed, buf)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to start editor: %s\", err)\n\t}\n\n\tsec := secrets.NewAKV()\n\tn, err := sec.Write(content)\n\tif err != nil || n < 0 {\n\t\tout.Errorf(ctx, \"WARNING: Invalid secret: %s of len %d\", err, n)\n\t}\n\n\tif err := s.Store.Set(ctx, name, sec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to store secret %q: %s\", name, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/insert_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInsert(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"true\"))\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"insert bar\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Insert(gptest.CliCtx(ctx, t, \"bar\")))\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert bar baz\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Insert(gptest.CliCtx(ctx, t, \"bar\", \"baz\")))\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/o newline\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(\"foobar\"), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, \"foobar\\n\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/ newline\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(\"foobar\\n\"), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, \"foobar\\n\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/ yaml\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(\"foobar\\n---\\nuser: name\\nother: meh\"), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, \"foobar\\n---\\nother: meh\\nuser: name\\n\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/ k-v\", func(t *testing.T) {\n\t\tin := \"foobar\\ninvalid key-value\\nOther: meh\\nUser: name\\nbody text\\n\"\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(in), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, in, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert zab#key\", func(t *testing.T) {\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"true\"))\n\t\trequire.NoError(t, act.insertYAML(ctx, \"zabkey\", \"key\", []byte(\"foobar\"), nil))\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"zabkey\", false))\n\t\tassert.Contains(t, buf.String(), \"key: foobar\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert --multiline bar baz\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Insert(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"multiline\": \"true\"}, \"bar\", \"baz\")))\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert key:value\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Insert(gptest.CliCtxWithFlags(ctx, t, nil, \"keyvaltest\", \"baz:val\")))\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"keyvaltest\", false))\n\t\tassert.Contains(t, buf.String(), \"baz: val\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/ yaml and input parsing and safecontent\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(\"foobar\\n---\\nuser: name\\nother: 0123\"), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, \"other: 83\\nuser: name\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"insert baz via stdin w/ yaml\", func(t *testing.T) {\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"false\"))\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz\", []byte(\"foobar\\n---\\nuser: name\\nother: 0123\"), false))\n\t\tbuf.Reset()\n\n\t\trequire.NoError(t, act.show(ctx, gptest.CliCtx(ctx, t), \"baz\", false))\n\t\tassert.Equal(t, \"foobar\\n---\\nother: 83\\nuser: name\\n\", buf.String())\n\t\tbuf.Reset()\n\t})\n}\n\nfunc TestInsertStdin(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithStdin(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"generate.autoclip\", \"false\"))\n\n\tbuf := &bytes.Buffer{}\n\tibuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdin = ibuf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdin = os.Stdin\n\t}()\n\n\tibuf.WriteString(\"foobar\")\n\trequire.Error(t, act.insert(ctx, gptest.CliCtx(ctx, t), \"foo\", \"\", false, false, false, false, nil))\n\tibuf.Reset()\n\tbuf.Reset()\n\n\t// force\n\tibuf.WriteString(\"foobar\")\n\trequire.NoError(t, act.insert(ctx, gptest.CliCtx(ctx, t), \"foo\", \"\", false, false, true, false, nil))\n\tibuf.Reset()\n\tbuf.Reset()\n\n\t// append\n\tibuf.WriteString(\"foobar\")\n\trequire.NoError(t, act.insert(ctx, gptest.CliCtx(ctx, t), \"foo\", \"\", false, false, false, true, nil))\n\tibuf.Reset()\n\tbuf.Reset()\n\n\t// echo\n\tibuf.WriteString(\"foobar\")\n\trequire.NoError(t, act.insert(ctx, gptest.CliCtx(ctx, t), \"bar\", \"\", true, false, false, false, nil))\n\tibuf.Reset()\n\tbuf.Reset()\n\n\t// multiline\n\tibuf.WriteString(\"foobar\")\n\trequire.NoError(t, act.insert(ctx, gptest.CliCtx(ctx, t), \"baz\", \"\", false, true, false, false, nil))\n\tibuf.Reset()\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/link.go",
    "content": "package action\n\nimport (\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Link creates a symlink.\nfunc (s *Action) Link(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tfrom := c.Args().Get(0)\n\tto := c.Args().Get(1)\n\n\tif from == \"\" || to == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: link <from> <to>\")\n\t}\n\n\treturn s.Store.Link(ctx, from, to)\n}\n"
  },
  {
    "path": "internal/action/link_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLink(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// first add another entry in a subdir\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.Link(gptest.CliCtx(ctx, t, \"bar/baz\", \"other/linkdest\")))\n\n\t// original secret should be equal to the linkdest\n\toSec, err := act.Store.Get(ctx, \"bar/baz\")\n\trequire.NoError(t, err)\n\n\tlSec, err := act.Store.Get(ctx, \"other/linkdest\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, oSec.Bytes(), lSec.Bytes())\n\n\t// update the original, linkdest should still be the same\n\toSec.SetPassword(\"456\")\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", oSec))\n\tbuf.Reset()\n\n\toSec, err = act.Store.Get(ctx, \"bar/baz\")\n\trequire.NoError(t, err)\n\n\tlSec, err = act.Store.Get(ctx, \"other/linkdest\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, oSec.Bytes(), lSec.Bytes())\n}\n"
  },
  {
    "path": "internal/action/list.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\tshellquote \"github.com/kballard/go-shellquote\"\n\t\"github.com/noborus/ov/oviewer\"\n\t\"github.com/urfave/cli/v2\"\n\t\"golang.org/x/term\"\n)\n\n// List all secrets as a tree. If the filter argument is non-empty\n// display only those that have this prefix.\nfunc (s *Action) List(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tfilter := c.Args().First()\n\tflat := c.Bool(\"flat\")\n\tstripPrefix := c.Bool(\"strip-prefix\")\n\tfolders := c.Bool(\"folders\")\n\n\t// print the path if the argument is a direct hit.\n\tif s.Store.Exists(ctx, filter) && !s.Store.IsDir(ctx, filter) {\n\t\tfmt.Println(filter)\n\n\t\treturn nil\n\t}\n\n\t// we only support listing folders in flat mode currently.\n\tif folders {\n\t\tflat = true\n\t}\n\n\tl, err := s.Store.Tree(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list store: %s\", err)\n\t}\n\n\t// set limit to infinite by default unless it's set with the flag\n\tlimit := tree.INF\n\tif c.IsSet(\"limit\") {\n\t\tlimit = c.Int(\"limit\")\n\t}\n\n\treturn s.listFiltered(ctx, l, limit, flat, folders, stripPrefix, filter)\n}\n\nfunc (s *Action) listFiltered(ctx context.Context, l *tree.Root, limit int, flat, folders, stripPrefix bool, filter string) error {\n\tsep := leaf.Sep\n\n\tif filter == \"\" || filter == sep {\n\t\t// We list all entries then.\n\t\tstripPrefix = true\n\t} else {\n\t\t// If the filter ends with a separator, we still want to find the content of that folder.\n\t\tif strings.HasSuffix(filter, sep) {\n\t\t\tfilter = filter[:len(filter)-1]\n\t\t}\n\t\t// To avoid shadowing l since we need it outside of the else scope.\n\t\tvar err error\n\t\t// We restrict ourselves to the filter.\n\t\tl, err = l.FindFolder(filter)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.NotFound, nil, \"Entry %q not found\", filter)\n\t\t}\n\t\tl.SetName(filter + sep)\n\t}\n\n\tif flat {\n\t\tlistOver := l.List\n\t\tif folders {\n\t\t\tlistOver = l.ListFolders\n\t\t}\n\t\tfor _, e := range listOver(limit) {\n\t\t\tif stripPrefix {\n\t\t\t\te = strings.TrimPrefix(e, filter+sep)\n\t\t\t}\n\t\t\tfmt.Fprintln(stdout, e)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// we may need to redirect stdout for the pager support.\n\tso, buf := redirectPager(ctx, l)\n\n\tfmt.Fprintln(so, l.Format(limit))\n\tif buf != nil {\n\t\tif err := s.pager(ctx, buf); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke pager: %s\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// redirectPager returns a redirected io.Writer if the output would exceed\n// the terminal size.\nfunc redirectPager(ctx context.Context, subtree *tree.Root) (io.Writer, *bytes.Buffer) {\n\tif config.Bool(ctx, \"core.nopager\") {\n\t\treturn stdout, nil\n\t}\n\t_, rows, err := term.GetSize(0)\n\tif err != nil {\n\t\treturn stdout, nil\n\t}\n\tif subtree == nil || subtree.Len() < rows {\n\t\treturn stdout, nil\n\t}\n\tif pager := os.Getenv(\"PAGER\"); pager == \"\" {\n\t\treturn stdout, nil\n\t}\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\n\treturn buf, buf\n}\n\n// pager invokes the default pager with the given content.\nfunc (s *Action) pager(ctx context.Context, buf io.Reader) error {\n\tif config.Bool(ctx, \"output.internal-pager\") {\n\t\tov, err := oviewer.NewRoot(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn ov.Run()\n\t}\n\n\tpager := os.Getenv(\"PAGER\")\n\tif pager == \"\" {\n\t\tfmt.Fprintln(stdout, buf)\n\n\t\treturn nil\n\t}\n\n\targs, err := shellquote.Split(pager)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to split pager command: %w\", err)\n\t}\n\n\tcmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\tcmd.Stdin = buf\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "internal/action/list_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestList(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\tcolor.NoColor = true\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t)))\n\twant := `gopass\n└── foo\n\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// add foo/bar and list folder foo\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"foo/bar\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t, \"foo\")))\n\twant = `foo/\n└── bar\n\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// list --flat foo\n\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"flat\": \"true\"}, \"foo\")))\n\twant = `foo/bar\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// list --folders\n\n\t// add more folders and subfolders\n\tsec = secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, act.Store.Set(ctx, \"foo/zen/bar\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"foo2/bar2\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"folders\": \"true\"})))\n\twant = `foo/\nfoo/zen/\nfoo2/\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// add shadowed entry\n\tsec = secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, act.Store.Set(ctx, \"foo/zen\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"flat\": \"true\"})))\n\twant = `foo\nfoo/bar\nfoo/zen\nfoo/zen/bar\nfoo2/bar2\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t, \"foo\")))\n\twant = `foo/\n├── bar\n└── zen/ (shadowed)\n    └── bar\n\n`\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\t// list not-present\n\trequire.Error(t, act.List(gptest.CliCtx(ctx, t, \"not-present\")))\n\tbuf.Reset()\n}\n\nfunc TestListLimit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\tcolor.NoColor = true\n\n\trequire.NoError(t, act.List(gptest.CliCtx(ctx, t)))\n\twant := `gopass\n└── foo\n\n`\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, act.Store.Set(ctx, \"foo/bar\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"foo/zen/baz/bar\", sec))\n\trequire.NoError(t, act.Store.Set(ctx, \"foo2/bar2\", sec))\n\tassert.Equal(t, want, buf.String())\n\tbuf.Reset()\n\n\tt.Run(\"folders-limit-0\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"folders\": \"true\", \"limit\": \"0\"})))\n\t\twant = `foo/\nfoo2/\n`\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"folders-limit-1\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"folders\": \"true\", \"limit\": \"1\"})))\n\t\twant = `foo/\nfoo/zen/\nfoo2/\n`\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"folders-limit--1\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"folders\": \"true\", \"limit\": \"-1\"})))\n\t\twant = `foo/\nfoo/zen/\nfoo/zen/baz/\nfoo2/\n`\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"flat-limit--1\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"flat\": \"true\", \"limit\": \"-1\"})))\n\t\twant = `foo\nfoo/bar\nfoo/zen/baz/bar\nfoo2/bar2\n`\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"folders-limit-0\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"flat\": \"true\", \"limit\": \"0\"})))\n\t\twant = `foo\nfoo2/\n`\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"folders-limit-2\", func(t *testing.T) {\n\t\trequire.NoError(t, act.List(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"flat\": \"true\", \"limit\": \"2\"})))\n\t\twant = `foo\nfoo/bar\nfoo/zen/baz/\nfoo2/bar2\n`\n\n\t\tassert.Equal(t, want, buf.String())\n\t\tbuf.Reset()\n\t})\n}\n\nfunc TestRedirectPager(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tvar buf *bytes.Buffer\n\tvar subtree *tree.Root\n\n\tcfg := config.NewInMemory()\n\tctx = cfg.WithConfig(ctx)\n\n\t// no pager\n\trequire.NoError(t, cfg.Set(\"\", \"core.nopager\", \"true\"))\n\tso, buf := redirectPager(ctx, subtree)\n\tassert.Nil(t, buf)\n\tassert.NotNil(t, so)\n\n\t// no term\n\trequire.NoError(t, cfg.Set(\"\", \"core.nopager\", \"false\"))\n\tso, buf = redirectPager(ctx, subtree)\n\tassert.Nil(t, buf)\n\tassert.NotNil(t, so)\n}\n"
  },
  {
    "path": "internal/action/merge.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/audit\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Merge implements the merge subcommand that allows merging multiple entries.\nfunc (s *Action) Merge(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tto := c.Args().First()\n\tfrom := c.Args().Tail()\n\n\tif to == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"usage: %s merge <to> <from> [<from>]\", s.Name)\n\t}\n\n\tif len(from) < 1 {\n\t\treturn exit.Error(exit.Usage, nil, \"usage: %s merge <to> <from> [<from>]\", s.Name)\n\t}\n\n\ted := editor.Path(c)\n\n\tcontent := &bytes.Buffer{}\n\tfor _, k := range c.Args().Slice() {\n\t\tif !s.Store.Exists(ctx, k) {\n\t\t\tcontinue\n\t\t}\n\t\tsec, err := s.Store.Get(ctxutil.WithShowParsing(ctx, false), k)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Decrypt, err, \"failed to decrypt: %s: %s\", k, err)\n\t\t}\n\n\t\t_, err = content.WriteString(\"\\n# Secret: \" + k + \"\\n\")\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to write: %s\", err)\n\t\t}\n\n\t\t_, err = content.Write(sec.Bytes())\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to write: %s\", err)\n\t\t}\n\t}\n\n\tnewContent := content.Bytes()\n\tif !c.Bool(\"force\") {\n\t\tvar err error\n\t\t// invoke the editor to let the user edit the content\n\t\tnewContent, err = editor.Invoke(ctx, ed, content.Bytes())\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke editor: %s\", err)\n\t\t}\n\n\t\t// If content is equal, nothing changed, exiting\n\t\tif bytes.Equal(content.Bytes(), newContent) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tnSec := secrets.ParseAKV(newContent)\n\n\t// if the secret has a password, we check it's strength\n\tif pw := nSec.Password(); pw != \"\" && !c.Bool(\"force\") {\n\t\taudit.Single(ctx, pw)\n\t}\n\n\t// write result (back) to store\n\tif err := s.Store.Set(ctxutil.WithCommitMessage(ctx, fmt.Sprintf(\"Merged %+v\", c.Args().Slice())), to, nSec); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn exit.Error(exit.Encrypt, err, \"failed to encrypt secret %s: %s\", to, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\tif !c.Bool(\"delete\") {\n\t\treturn nil\n\t}\n\n\t// wait until the previous commit is done\n\t// This wouldn't be necessary if we could handle merging and deleting\n\t// in a single commit, but then we'd need to expose additional implementation\n\t// details of the underlying VCS. Or create some kind of transaction on top\n\t// of the Git wrapper.\n\tif err := queue.GetQueue(ctx).Idle(time.Minute); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, old := range from {\n\t\tif !s.Store.Exists(ctx, old) {\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"deleting merged entry %s\", old)\n\t\tif err := s.Store.Delete(ctx, old); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to delete %s: %s\", old, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/merge_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMerge(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// first add two entries\n\tvar sec gopass.Secret\n\tsec = secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", sec))\n\tbuf.Reset()\n\n\tsec = secrets.NewAKV()\n\tsec.SetPassword(\"456\")\n\trequire.NoError(t, sec.Set(\"bar\", \"baz\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/zab\", sec))\n\tbuf.Reset()\n\n\trequire.NoError(t, act.Merge(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"bar/baz\", \"bar/zab\")))\n\n\tsec, err = act.Store.Get(ctx, \"bar/baz\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"\\n# Secret: bar/baz\\n123\\nbar: zab\\n\\n# Secret: bar/zab\\n456\\nbar: baz\\n\", string(sec.Bytes()))\n}\n"
  },
  {
    "path": "internal/action/mount.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/store/root\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// MountRemove removes an existing mount.\nfunc (s *Action) MountRemove(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.Args().Len() != 1 {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s mount remove [alias]\", s.Name)\n\t}\n\n\tif err := s.Store.RemoveMount(ctx, c.Args().Get(0)); err != nil {\n\t\tout.Errorf(ctx, \"Failed to remove mount: %s\", err)\n\t}\n\n\tout.Printf(ctx, \"Password Store %s umounted\", c.Args().Get(0))\n\n\treturn nil\n}\n\n// MountsPrint prints all existing mounts.\nfunc (s *Action) MountsPrint(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif len(s.Store.Mounts()) < 1 {\n\t\tout.Printf(ctx, \"No mounts\")\n\n\t\treturn nil\n\t}\n\n\troot := tree.New(color.GreenString(fmt.Sprintf(\"gopass (%s)\", s.Store.Path())))\n\tmounts := s.Store.Mounts()\n\tmps := s.Store.MountPoints()\n\tsort.Sort(store.ByPathLen(mps))\n\tfor _, alias := range mps {\n\t\tpath := mounts[alias]\n\t\tif err := root.AddMount(alias, path); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to add mount to tree: %s\", err)\n\t\t}\n\t}\n\tdebug.Log(\"MountsPrint - %+v - %+v\", mounts, mps)\n\n\tfmt.Fprintln(stdout, root.Format(tree.INF))\n\n\treturn nil\n}\n\n// MountsComplete will print a list of existings mount points for bash\n// completion.\nfunc (s *Action) MountsComplete(*cli.Context) {\n\tfor alias := range s.Store.Mounts() {\n\t\tfmt.Fprintln(stdout, alias)\n\t}\n}\n\n// MountAdd adds a new mount.\nfunc (s *Action) MountAdd(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tvar alias, localPath string\n\tswitch c.Args().Len() {\n\tcase 0:\n\t\treturn exit.Error(exit.Usage, nil, \"usage: %s mounts add <local path> OR %s mounts add <alias> <local path>\", s.Name, s.Name)\n\tcase 1:\n\t\tlocalPath = c.Args().Get(0)\n\t\talias = filepath.Base(localPath)\n\tdefault:\n\t\talias = c.Args().Get(0)\n\t\tlocalPath = c.Args().Get(1)\n\t}\n\n\tif s.Store.Exists(ctx, alias) {\n\t\tout.Warningf(ctx, \"shadowing %s entry\", alias)\n\t}\n\n\tif c.Bool(\"create\") && !set.New(alias).IsSubset(set.New(s.Store.MountPoints()...)) {\n\t\tdebug.Log(\"creating new mount %s at %s\", alias, localPath)\n\n\t\treturn s.init(ctx, alias, localPath)\n\t}\n\n\tif err := s.Store.AddMount(ctx, alias, localPath); err != nil {\n\t\tvar aerr *root.AlreadyMountedError\n\t\tif errors.As(err, &aerr) {\n\t\t\tout.Printf(ctx, \"Store is already mounted\")\n\n\t\t\treturn nil\n\t\t}\n\t\tvar nerr *root.NotInitializedError\n\t\tif errors.As(err, &nerr) {\n\t\t\tout.Printf(ctx, \"Mount %s is not yet initialized. Please use 'gopass init --store %s' instead\", nerr.Alias(), nerr.Alias())\n\n\t\t\treturn nerr\n\t\t}\n\n\t\treturn exit.Error(exit.Mount, err, \"failed to add mount %q to %q: %s\", alias, localPath, err)\n\t}\n\n\tout.Printf(ctx, \"Mounted %s as %s\", alias, localPath)\n\n\treturn nil\n}\n\n// MountsVersions prints the backend versions for each mount.\nfunc (s *Action) MountsVersions(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tcryptoVer := versionInfo(ctx, s.Store.Crypto(ctx, \"\"))\n\tstorageVer := versionInfo(ctx, s.Store.Storage(ctx, \"\"))\n\n\ttpl := \"%-10s - %10s - %10s\\n\"\n\tfmt.Fprintf(stdout, tpl, \"<root>\", cryptoVer, storageVer)\n\n\t// report all used crypto, sync and fs backends.\n\tfor _, mp := range s.Store.MountPoints() {\n\t\tcv := versionInfo(ctx, s.Store.Crypto(ctx, mp))\n\t\tsv := versionInfo(ctx, s.Store.Storage(ctx, mp))\n\n\t\tfmt.Fprintf(stdout, tpl, mp, cv, sv)\n\t}\n\n\tfmt.Fprintln(stdout)\n\tfmt.Fprintf(stdout, \"Available Crypto Backends: %s\\n\", strings.Join(backend.CryptoRegistry.BackendNames(), \", \"))\n\tfmt.Fprintf(stdout, \"Available Storage Backends: %s\\n\", strings.Join(backend.StorageRegistry.BackendNames(), \", \"))\n\n\treturn nil\n}\n\ntype versioner interface {\n\tName() string\n\tVersion(context.Context) semver.Version\n}\n\nfunc versionInfo(ctx context.Context, v versioner) string {\n\tif v == nil {\n\t\treturn \"<none>\"\n\t}\n\n\treturn fmt.Sprintf(\"%s %s\", v.Name(), v.Version(ctx))\n}\n"
  },
  {
    "path": "internal/action/mount_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMounts(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"print mounts\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.MountsPrint(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"complete mounts\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tact.MountsComplete(gptest.CliCtx(ctx, t))\n\t\tassert.Empty(t, buf.String())\n\t})\n\n\tt.Run(\"remove no non-existing mount\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.MountRemove(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"remove non-existing mount\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.MountRemove(gptest.CliCtx(ctx, t, \"foo\")))\n\t})\n\n\tt.Run(\"add non-existing mount\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.MountAdd(gptest.CliCtx(ctx, t, \"foo\", filepath.Join(u.Dir, \"mount1\"))))\n\t})\n\n\tt.Run(\"add some mounts\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, u.InitStore(\"mount1\"))\n\t\trequire.NoError(t, u.InitStore(\"mount2\"))\n\t\trequire.NoError(t, act.Store.AddMount(ctx, \"mount1\", u.StoreDir(\"mount1\")))\n\t\trequire.NoError(t, act.Store.AddMount(ctx, \"mount2\", u.StoreDir(\"mount2\")))\n\t})\n\n\tt.Run(\"print mounts\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.MountsPrint(gptest.CliCtx(ctx, t)))\n\t})\n}\n"
  },
  {
    "path": "internal/action/move.go",
    "content": "package action\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Move the content from one secret to another.\nfunc (s *Action) Move(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tif c.Args().Len() != 2 {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s mv old-path new-path\", s.Name)\n\t}\n\n\tfrom := c.Args().Get(0)\n\tto := c.Args().Get(1)\n\n\tif !c.Bool(\"force\") {\n\t\tif s.Store.Exists(ctx, to) && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"%s already exists. Overwrite it?\", to)) {\n\t\t\treturn exit.Error(exit.Aborted, nil, \"not overwriting your current secret\")\n\t\t}\n\t}\n\n\t// Check for custom commit message\n\tcommitMsg := fmt.Sprintf(\"Moved %s to %s\", from, to)\n\tif c.IsSet(\"commit-message\") {\n\t\tcommitMsg = c.String(\"commit-message\")\n\t}\n\tif c.Bool(\"interactive-commit\") {\n\t\tcommitMsg = \"\"\n\t}\n\tctx = ctxutil.WithCommitMessage(ctx, commitMsg)\n\n\tif err := s.Store.Move(ctx, from, to); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"%s\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/move_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMove(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tt.Run(\"move foo to bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Move(gptest.CliCtx(ctx, t, \"foo\", \"bar\")))\n\t})\n}\n"
  },
  {
    "path": "internal/action/otp.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/clipboard\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/otp\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/mattn/go-tty\"\n\t\"github.com/pquerna/otp/hotp\"\n\t\"github.com/pquerna/otp/totp\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// OTP implements OTP token handling for TOTP and HOTP.\nfunc (s *Action) OTP(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s otp <NAME>\", s.Name)\n\t}\n\n\tqrf := c.String(\"qr\")\n\tclip := config.Bool(ctx, \"otp.onlyclip\")\n\tif c.IsSet(\"clip\") {\n\t\tclip = c.Bool(\"clip\")\n\t}\n\talsoClip := config.Bool(ctx, \"otp.autoclip\")\n\tif c.IsSet(\"alsoclip\") {\n\t\talsoClip = c.Bool(\"alsoclip\")\n\t}\n\tchained := c.Bool(\"chained\")\n\tpw := c.Bool(\"password\")\n\tsnip := c.Bool(\"snip\")\n\n\tif snip {\n\t\tqr, err := otp.ParseScreen(ctx)\n\t\tif err != nil || len(qr) == 0 {\n\t\t\treturn err\n\t\t}\n\n\t\tchoice, err := termio.AskForBool(ctx, \"(Over)writing otpauth URL in key 'otpauth'?\", true)\n\t\tif err != nil || !choice {\n\t\t\treturn err\n\t\t}\n\t\terr = s.insertYAML(ctxutil.WithInteractive(ctx, false), name, \"otpauth\", []byte(qr), nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tout.Print(ctx, \"Value written, carrying on to display OTP value from it.\")\n\t}\n\n\treturn s.otp(ctx, name, qrf, clip, pw, true, chained, alsoClip)\n}\n\nfunc tickingBar(ctx context.Context, expiresAt time.Time, bar *termio.ProgressBar) {\n\tticker := time.NewTicker(1 * time.Second)\n\tdefer ticker.Stop()\n\tfor tt := range ticker.C {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn // returning not to leak the goroutine.\n\t\tdefault:\n\t\t\t// we don't want to block if not cancelled.\n\t\t}\n\t\tif tt.After(expiresAt) {\n\t\t\treturn\n\t\t}\n\t\tbar.Inc()\n\t}\n}\n\nfunc waitForKeyPress(ctx context.Context, cancel context.CancelFunc) (func(), func()) {\n\ttty1, err := tty.Open()\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Unexpected error opening tty: %v\", err)\n\t\tcancel()\n\t}\n\n\treturn func() {\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn // returning not to leak the goroutine.\n\t\t\t\tdefault:\n\t\t\t\t}\n\n\t\t\t\tr, err := tty1.ReadRune()\n\t\t\t\tif err != nil {\n\t\t\t\t\tout.Errorf(ctx, \"Unexpected error opening tty: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif r == 'q' || r == 'x' || err != nil {\n\t\t\t\t\tcancel()\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}, func() {\n\t\t\t_ = tty1.Close()\n\t\t}\n}\n\n// nolint: cyclop\nfunc (s *Action) otp(ctx context.Context, name, qrf string, clip, pw, recurse, chained, alsoClip bool) error {\n\tsec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn s.otpHandleError(ctx, name, qrf, clip, pw, recurse, chained, alsoClip, err)\n\t}\n\n\touterCtx := ctx\n\tctx = config.WithMount(ctx, s.Store.MountPoint(name))\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tskip := ctxutil.IsHidden(ctx) || pw || qrf != \"\" || !ctxutil.IsTerminal(ctx) || !ctxutil.IsInteractive(ctx) || clip\n\tif !skip {\n\t\t// let us monitor key presses for cancellation:.\n\t\trunFn, cleanupFn := waitForKeyPress(ctx, cancel)\n\t\tgo runFn()\n\t\tdefer cleanupFn()\n\t}\n\n\t// only used for the HOTP case as a fallback\n\tvar counter uint64 = 1\n\tif sv, found := sec.Get(\"counter\"); found && sv != \"\" {\n\t\tif iv, err := strconv.ParseUint(sv, 10, 64); iv != 0 && err == nil {\n\t\t\tcounter = iv\n\t\t}\n\t}\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tdefault:\n\t\t}\n\n\t\ttwo, err := otp.Calculate(name, sec)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"No OTP entry found for %s: %s\", name, err)\n\t\t}\n\n\t\tvar token string\n\t\tswitch two.Type() {\n\t\tcase \"totp\":\n\t\t\ttoken, err = totp.GenerateCodeCustom(two.Secret(), time.Now(), totp.ValidateOpts{\n\t\t\t\tPeriod:    uint(two.Period()),\n\t\t\t\tSkew:      1,\n\t\t\t\tDigits:    two.Digits(),\n\t\t\t\tAlgorithm: two.Algorithm(),\n\t\t\t\tEncoder:   two.Encoder(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to compute OTP token for %s: %s\", name, err)\n\t\t\t}\n\t\tcase \"hotp\":\n\t\t\ttoken, err = hotp.GenerateCodeCustom(two.Secret(), counter, hotp.ValidateOpts{\n\t\t\t\tDigits:    two.Digits(),\n\t\t\t\tAlgorithm: two.Algorithm(),\n\t\t\t\tEncoder:   two.Encoder(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to compute OTP token for %s: %s\", name, err)\n\t\t\t}\n\t\t\tcounter++\n\t\t\t_ = sec.Set(\"counter\", strconv.Itoa(int(counter)))\n\t\t\t// using outerCtx here because we want to save the counter even if the user cancels.\n\t\t\tif err := s.Store.Set(outerCtx, name, sec); err != nil {\n\t\t\t\tout.Errorf(outerCtx, \"Failed to persist counter value: %s\", err)\n\t\t\t}\n\t\t\tdebug.Log(\"Saved counter as %d\", counter)\n\t\t}\n\n\t\tnow := time.Now()\n\t\texpiresAt := now.Add(time.Duration(two.Period()) * time.Second).Truncate(time.Duration(two.Period()) * time.Second)\n\t\tsecondsLeft := int(time.Until(expiresAt).Seconds())\n\t\tbar := termio.NewProgressBar(int64(secondsLeft))\n\t\tbar.Hidden = skip\n\n\t\tdebug.Log(\"OTP period: %ds\", two.Period())\n\n\t\tif chained {\n\t\t\ttoken = fmt.Sprintf(\"%s%s\", sec.Password(), token)\n\t\t}\n\t\tif clip || alsoClip {\n\t\t\tif err := clipboard.CopyTo(ctx, fmt.Sprintf(\"token for %s\", name), []byte(token), config.AsInt(s.cfg.Get(\"core.cliptimeout\"))); err != nil {\n\t\t\t\treturn exit.Error(exit.IO, err, \"failed to copy to clipboard: %s\", err)\n\t\t\t}\n\t\t\tif clip {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tout.Printf(ctx, \"%s\", token)\n\n\t\t// In \"QR-Code\" mode just create the image file and then exit.\n\t\tif qrf != \"\" {\n\t\t\treturn otp.WriteQRFile(two, qrf)\n\t\t}\n\n\t\t// If we are in \"password mode\", not interacting with a terminal or Stdout is attached to a pipe,\n\t\t// we are done.\n\t\tif skip {\n\t\t\treturn nil\n\t\t}\n\n\t\t// if not then we want to print a progress bar with the expiry time.\n\t\tout.Warningf(ctx, \"([q] to stop. -o flag to avoid.) This OTP password still lasts for:\", nil)\n\n\t\tif bar.Hidden {\n\t\t\tcancel()\n\t\t} else {\n\t\t\tbar.Set(0)\n\t\t\tgo tickingBar(ctx, expiresAt, bar)\n\t\t}\n\n\t\t// let us wait until next OTP code:.\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tbar.Done()\n\t\t\t\tcancel()\n\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\ttime.Sleep(time.Millisecond * 500)\n\t\t\t}\n\t\t\tif time.Now().After(expiresAt) {\n\t\t\t\tbar.Done()\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *Action) otpHandleError(ctx context.Context, name, qrf string, clip, pw, recurse, chained, alsoClip bool, err error) error {\n\tif !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to retrieve secret %q: %s\", name, err)\n\t}\n\n\tout.Printf(ctx, \"Entry %q not found. Starting search...\", name)\n\tcb := func(ctx context.Context, c *cli.Context, name string, recurse bool) error {\n\t\treturn s.otp(ctx, name, qrf, clip, pw, false, chained, alsoClip)\n\t}\n\tif err := s.find(ctx, nil, name, cb, false); err != nil {\n\t\treturn exit.Error(exit.NotFound, err, \"%s\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/otp_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gokyle/twofactor\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOTP(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"display non-otp secret\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.OTP(gptest.CliCtx(ctx, t, \"foo\")))\n\t})\n\n\tt.Run(\"create and display valid OTP\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"foo\")\n\t\t_, err := sec.Write([]byte(twofactor.GenerateGoogleTOTP().URL(\"foo\") + \"\\n\"))\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, act.Store.Set(ctx, \"bar\", sec))\n\n\t\trequire.NoError(t, act.OTP(gptest.CliCtx(ctx, t, \"bar\")))\n\n\t\t// add some unrelated body material, it should still work\n\t\t_, err = sec.Write([]byte(\"more body content, unrelated to otp\"))\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, act.Store.Set(ctx, \"bar\", sec))\n\n\t\trequire.NoError(t, act.OTP(gptest.CliCtx(ctx, t, \"bar\")))\n\t})\n\n\tt.Run(\"copy to clipboard\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.otp(ctx, \"bar\", \"\", true, false, false, false, false))\n\t})\n\n\tt.Run(\"copy to clipboard chained\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.otp(ctx, \"bar\", \"\", true, false, false, true, false))\n\t})\n\n\tt.Run(\"write QR file\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tfn := filepath.Join(u.Dir, \"qr.png\")\n\t\trequire.NoError(t, act.OTP(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"qr\": fn}, \"bar\")))\n\t\tassert.FileExists(t, fn)\n\t})\n}\n"
  },
  {
    "path": "internal/action/process.go",
    "content": "package action\n\nimport (\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tpl\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Process is a command to process a template and replace secrets contained in it.\nfunc (s *Action) Process(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tfile := c.Args().First()\n\tif file == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s process <FILE>\", s.Name)\n\t}\n\n\tbuf, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"Failed to read file: %s\", file)\n\t}\n\n\tobuf, err := tpl.Execute(ctx, string(buf), file, nil, s.Store)\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"Failed to process file: %s\", file)\n\t}\n\n\tout.Print(ctx, string(obuf))\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/process_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProcess(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tsec := secrets.New()\n\trequire.NoError(t, sec.Set(\"username\", \"admin\"))\n\tsec.SetPassword(\"hunter2\")\n\trequire.NoError(t, act.Store.Set(ctx, \"server/local/mysql\", sec))\n\n\tinfile := filepath.Join(u.Dir, \"my.cnf.tpl\")\n\terr = os.WriteFile(infile, []byte(`[client]\nhost=127.0.0.1\nport=3306\nuser={{ getval \"server/local/mysql\" \"username\" }}\npassword={{ getpw \"server/local/mysql\" }}`), 0o644)\n\trequire.NoError(t, err)\n\n\tt.Run(\"process template\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\terr := act.Process(gptest.CliCtx(ctx, t, infile))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, `[client]\nhost=127.0.0.1\nport=3306\nuser=admin\npassword=hunter2\n`, buf.String(), \"processed template\")\n\t})\n}\n"
  },
  {
    "path": "internal/action/pwgen/commands.go",
    "content": "package pwgen\n\nimport (\n\t\"github.com/urfave/cli/v2\"\n)\n\n// GetCommands returns the pwgen subcommand.\nfunc GetCommands() []*cli.Command {\n\treturn []*cli.Command{\n\t\t{\n\t\t\tName:        \"pwgen\",\n\t\t\tUsage:       \"Generate passwords\",\n\t\t\tDescription: \"Print any number of password to the console. The optional length parameter specifies the length of each password.\",\n\t\t\tArgsUsage:   \"[length]\",\n\t\t\tAction:      Pwgen,\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"no-numerals\",\n\t\t\t\t\tAliases: []string{\"0\"},\n\t\t\t\t\tUsage:   \"Do not include numerals in the generated passwords.\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"no-capitalize\",\n\t\t\t\t\tAliases: []string{\"A\"},\n\t\t\t\t\tUsage:   \"Do not include capital letter in the generated passwords.\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"ambiguous\",\n\t\t\t\t\tAliases: []string{\"B\"},\n\t\t\t\t\tUsage:   \"Do not include characters that could be easily confused with each other, like '1' and 'l' or '0' and 'O'\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"symbols\",\n\t\t\t\t\tAliases: []string{\"y\"},\n\t\t\t\t\tUsage:   \"Include at least one symbol in the password.\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"one-per-line\",\n\t\t\t\t\tAliases: []string{\"1\"},\n\t\t\t\t\tUsage:   \"Print one password per line\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"xkcd\",\n\t\t\t\t\tAliases: []string{\"x\"},\n\t\t\t\t\tUsage:   \"Use multiple random english words combined to a password. By default, space is used as separator and all words are lowercase\",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"sep\",\n\t\t\t\t\tAliases: []string{\"xkcdsep\", \"xs\"},\n\t\t\t\t\tUsage:   \"Word separator for generated xkcd style password. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised. This flag implies -xkcd\",\n\t\t\t\t\tValue:   \" \",\n\t\t\t\t},\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"lang\",\n\t\t\t\t\tAliases: []string{\"xkcdlang\", \"xl\"},\n\t\t\t\t\tUsage:   \"Language to generate password from, currently only en (english, default) or de are supported\",\n\t\t\t\t\tValue:   \"en\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"xkcdcapitalize\",\n\t\t\t\t\tAliases: []string{\"xc\"},\n\t\t\t\t\tUsage:   \"Capitalize first letter of each word in generated xkcd style password. This flag implies -xkcd\",\n\t\t\t\t},\n\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\tName:    \"xkcdnumbers\",\n\t\t\t\t\tAliases: []string{\"xn\"},\n\t\t\t\t\tUsage:   \"Add a random number to the end of the generated xkcd style password. This flag implies -xkcd\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/action/pwgen/commands_test.go",
    "content": "package pwgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc testCommand(t *testing.T, cmd *cli.Command) {\n\tt.Helper()\n\n\tif len(cmd.Subcommands) < 1 {\n\t\tassert.NotNil(t, cmd.Action, cmd.Name)\n\t}\n\n\tassert.NotEmpty(t, cmd.Usage)\n\tassert.NotEmpty(t, cmd.Description)\n\n\tfor _, flag := range cmd.Flags {\n\t\tswitch v := flag.(type) {\n\t\tcase *cli.StringFlag:\n\t\t\tassert.NotContains(t, v.Name, \",\")\n\t\t\tassert.NotEmpty(t, v.Usage)\n\t\tcase *cli.BoolFlag:\n\t\t\tassert.NotContains(t, v.Name, \",\")\n\t\t\tassert.NotEmpty(t, v.Usage)\n\t\t}\n\t}\n\n\tfor _, scmd := range cmd.Subcommands {\n\t\ttestCommand(t, scmd)\n\t}\n}\n\nfunc TestCommands(t *testing.T) {\n\t// necessary for setting up the env\n\tu := gptest.NewGUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tfor _, cmd := range GetCommands() {\n\t\ttestCommand(t, cmd)\n\t}\n}\n"
  },
  {
    "path": "internal/action/pwgen/pwgen.go",
    "content": "// Package pwgen implements the subcommands to operate the stand alone password generator.\n// The reason why it's not part of the action package is that we did try to split that\n// but ran into issues and undid most of that work - except this package. If this bothers\n// you feel free to propose a PR to move it back into the action package.\npackage pwgen\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/xkcdgen\"\n\t\"github.com/urfave/cli/v2\"\n\t\"golang.org/x/term\"\n)\n\n// Pwgen handles the pwgen subcommand.\nfunc Pwgen(c *cli.Context) error {\n\tpwLen := 12\n\tif lenStr := c.Args().Get(0); lenStr != \"\" {\n\t\ti, err := strconv.Atoi(lenStr)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Usage, err, \"Failed to convert password length arg: %s\", err)\n\t\t}\n\t\tif i > 0 {\n\t\t\tpwLen = i\n\t\t}\n\t}\n\n\tpwNum := 10\n\tif numStr := c.Args().Get(1); numStr != \"\" {\n\t\ti, err := strconv.Atoi(numStr)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Usage, err, \"Failed to convert password number arg: %s\", err)\n\t\t}\n\t\tif i > 0 {\n\t\t\tpwNum = i\n\t\t}\n\t}\n\n\tif c.Bool(\"xkcd\") || c.Bool(\"xkcdcapitalize\") || c.Bool(\"xkcdnumbers\") {\n\t\treturn xkcdGen(c, pwLen, pwNum)\n\t}\n\n\treturn pwGen(c, pwLen, pwNum)\n}\n\nfunc xkcdGen(c *cli.Context, length, num int) error {\n\tsep := config.String(c.Context, \"pwgen.xkcd-sep\")\n\tif c.IsSet(\"sep\") {\n\t\tsep = c.String(\"sep\")\n\t}\n\tlang := config.String(c.Context, \"pwgen.xkcd-lang\")\n\tif c.IsSet(\"lang\") {\n\t\tlang = c.String(\"lang\")\n\t}\n\tif length < 1 {\n\t\tlength = config.Int(c.Context, \"pwgen.xkcd-len\")\n\t\tif length < 1 {\n\t\t\tlength = 4\n\t\t}\n\t}\n\tcapitalize := config.Bool(c.Context, \"pwgen.xkcd-capitalize\")\n\tif c.IsSet(\"xkcdcapitalize\") {\n\t\tcapitalize = c.Bool(\"xkcdcapitalize\")\n\t}\n\tnumbers := config.Bool(c.Context, \"pwgen.xkcd-numbers\")\n\tif c.IsSet(\"xkcdnumbers\") {\n\t\tnumbers = c.Bool(\"xkcdnumbers\")\n\t}\n\n\tfor range num {\n\t\ts, err := xkcdgen.RandomLengthDelim(length, sep, lang, capitalize, numbers)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tout.Print(c.Context, s)\n\t}\n\n\treturn nil\n}\n\nfunc pwGen(c *cli.Context, pwLen, pwNum int) error {\n\tctx := c.Context\n\n\tperLine := numPerLine(pwLen)\n\tif c.Bool(\"one-per-line\") {\n\t\tperLine = 1\n\t}\n\n\tcharset := pwgen.CharAlphaNum\n\n\tswitch {\n\tcase c.Bool(\"no-numerals\") && c.Bool(\"no-capitalize\"):\n\t\tcharset = pwgen.Lower\n\tcase c.Bool(\"no-numerals\"):\n\t\tcharset = pwgen.CharAlpha\n\tcase c.Bool(\"no-capitalize\"):\n\t\tcharset = pwgen.Digits + pwgen.Lower\n\t}\n\n\tif c.Bool(\"ambiguous\") {\n\t\tcharset = pwgen.Prune(charset, pwgen.Ambiq)\n\t}\n\n\tif c.Bool(\"symbols\") {\n\t\tcharset += pwgen.Syms\n\t}\n\n\tfor range pwNum {\n\t\tfor range perLine {\n\t\t\tctx := out.WithNewline(ctx, false)\n\t\t\tout.Print(ctx, pwgen.GeneratePasswordCharset(pwLen, charset))\n\t\t\tout.Print(ctx, \" \")\n\t\t}\n\t\tout.Print(ctx, \"\")\n\t}\n\n\treturn nil\n}\n\nfunc numPerLine(pwLen int) int {\n\tcols, _, err := term.GetSize(0)\n\tif err != nil {\n\t\treturn 1\n\t}\n\n\treturn cols / (pwLen + 1)\n}\n"
  },
  {
    "path": "internal/action/pwgen/pwgen_test.go",
    "content": "package pwgen\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPwgen(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\trequire.NoError(t, Pwgen(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"one-per-line\": \"true\"}, \"24\", \"1\")))\n\tassert.GreaterOrEqual(t, len(buf.Bytes()), 24, buf.String())\n}\n"
  },
  {
    "path": "internal/action/rcs.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\tsi \"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// RCSInit initializes a git repo including basic configuration.\nfunc (s *Action) RCSInit(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tun := termio.DetectName(c.Context, c)\n\tue := termio.DetectEmail(c.Context, c)\n\n\tif c.IsSet(\"storage\") {\n\t\tvar err error\n\t\tctx, err = backend.WithStorageBackendString(ctx, c.String(\"storage\"))\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"Failed to set storage backend: %s\", err)\n\t\t}\n\t}\n\n\t// default to git.\n\tif !backend.HasStorageBackend(ctx) {\n\t\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\t}\n\n\tif err := s.rcsInit(ctx, store, un, ue); err != nil {\n\t\treturn exit.Error(exit.Git, err, \"failed to initialize %s: %s\", backend.StorageBackendName(backend.GetStorageBackend(ctx)), err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) rcsInit(ctx context.Context, store, un, ue string) error {\n\tbe := backend.GetStorageBackend(ctx)\n\t// TODO this should rather ask s.Store if it HasRCSInit or something.\n\tif be == backend.FS {\n\t\tdebug.V(1).Log(\"No RCS init for FS backend\")\n\n\t\treturn nil\n\t}\n\n\tbn := backend.StorageBackendName(be)\n\tuserName, userEmail := s.getUserData(ctx, store, un, ue)\n\tdebug.V(1).Log(\"Initializing RCS backend %s for %q with user %s / %s\", bn, store, userName, userEmail)\n\n\tif err := s.Store.RCSInit(ctx, store, userName, userEmail); err != nil {\n\t\tif errors.Is(err, backend.ErrNotSupported) {\n\t\t\tdebug.Log(\"RCSInit not supported for backend %s in %q\", bn, store)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif gtv := os.Getenv(\"GPG_TTY\"); gtv == \"\" {\n\t\t\tout.Printf(ctx, \"Git initialization failed. You may want to try to 'export GPG_TTY=$(tty)' and start over.\")\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to run RCS init: %w\", err)\n\t}\n\n\tout.Printf(ctx, \"Initialized %s repository (%s) for %s / %s...\", be, bn, un, ue)\n\n\treturn nil\n}\n\nfunc (s *Action) getUserData(ctx context.Context, store, name, email string) (string, string) {\n\tif name != \"\" && email != \"\" {\n\t\tdebug.Log(\"Username: %s, Email: %s (provided)\", name, email)\n\n\t\treturn name, email\n\t}\n\n\t// for convenience, set defaults to user-selected values from available private keys.\n\t// NB: discarding returned error since this is merely a best-effort look-up for convenience.\n\tuserName, userEmail, _ := cui.AskForGitConfigUser(ctx, s.Store.Crypto(ctx, store))\n\n\tif name == \"\" {\n\t\tif userName == \"\" {\n\t\t\tuserName = termio.DetectName(ctx, nil)\n\t\t}\n\n\t\tvar err error\n\t\tname, err = termio.AskForString(ctx, color.CyanString(\"Please enter a user name for password store git config\"), userName)\n\t\tif err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to ask for user input: %s\", err)\n\t\t}\n\t}\n\n\tif email == \"\" {\n\t\tif userEmail == \"\" {\n\t\t\tuserEmail = termio.DetectEmail(ctx, nil)\n\t\t}\n\n\t\tvar err error\n\t\temail, err = termio.AskForString(ctx, color.CyanString(\"Please enter an email address for password store git config\"), userEmail)\n\t\tif err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to ask for user input: %s\", err)\n\t\t}\n\t}\n\n\tdebug.Log(\"Username: %s, Email: %s (detected)\", name, email)\n\n\treturn name, email\n}\n\n// RCSAddRemote adds a new git remote.\nfunc (s *Action) RCSAddRemote(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tremote := c.Args().Get(0)\n\turl := c.Args().Get(1)\n\n\tif remote == \"\" || url == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s git remote add <REMOTE> <URL>\", s.Name)\n\t}\n\n\treturn s.Store.RCSAddRemote(ctx, store, remote, url)\n}\n\n// RCSRemoveRemote removes a git remote.\nfunc (s *Action) RCSRemoveRemote(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tremote := c.Args().Get(0)\n\n\tif remote == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s git remote rm <REMOTE>\", s.Name)\n\t}\n\n\treturn s.Store.RCSRemoveRemote(ctx, store, remote)\n}\n\n// RCSPull pulls from a git remote.\nfunc (s *Action) RCSPull(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\torigin := c.Args().Get(0)\n\tbranch := c.Args().Get(1)\n\n\treturn s.Store.RCSPull(ctx, store, origin, branch)\n}\n\n// RCSPush pushes to a git remote.\nfunc (s *Action) RCSPush(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\torigin := c.Args().Get(0)\n\tbranch := c.Args().Get(1)\n\n\tif err := s.Store.RCSPush(ctx, store, origin, branch); err != nil {\n\t\tif errors.Is(err, si.ErrGitNoRemote) {\n\t\t\tout.Noticef(ctx, \"No Git remote. Not pushing\")\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn exit.Error(exit.Git, err, \"Failed to push to remote\")\n\t}\n\tout.OKf(ctx, \"Pushed to git remote\")\n\n\treturn nil\n}\n\n// RCSStatus prints the rcs status.\nfunc (s *Action) RCSStatus(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\n\treturn s.Store.RCSStatus(ctx, store)\n}\n"
  },
  {
    "path": "internal/action/rcs_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\t// git init\n\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"name\": \"foobar\", \"email\": \"foo.bar@example.org\"})\n\trequire.NoError(t, act.RCSInit(c))\n\tbuf.Reset()\n\n\t// getUserData\n\tname, email := act.getUserData(ctx, \"\", \"\", \"\")\n\tassert.Equal(t, \"0xDEADBEEF\", name)\n\tassert.Equal(t, \"0xDEADBEEF\", email)\n\n\t// GitAddRemote\n\trequire.Error(t, act.RCSAddRemote(c))\n\tbuf.Reset()\n\n\t// GitRemoveRemote\n\trequire.Error(t, act.RCSRemoveRemote(c))\n\tbuf.Reset()\n\n\t// GitPull\n\trequire.Error(t, act.RCSPull(c))\n\tbuf.Reset()\n\n\t// GitPush\n\trequire.NoError(t, act.RCSPush(c))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/recipients.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar removalWarning = `\n\n\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n@   WARNING: REMOVING A USER WILL NOT REVOKE ACCESS FROM OLD REVISONS!   @\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\nTHE USER %s WILL STILL BE ABLE TO ACCESS ANY OLD COPY OF THE STORE AND\nANY OLD REVISION THEY HAD ACCESS TO.\n\nANY CREDENTIALS THIS USER HAD ACCESS TO NEED TO BE CONSIDERED COMPROMISED\nAND SHOULD BE REVOKED.\n\nThis feature is only meant for revoking access to any added or changed\ncredentials.\n\n`\n\n// RecipientsPrint prints all recipients per store.\nfunc (s *Action) RecipientsPrint(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tout.Printf(ctx, \"Hint: run 'gopass sync' to import any missing public keys\")\n\n\tt, err := s.Store.RecipientsTree(ctx, c.Bool(\"pretty\"))\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list recipients: %s\", err)\n\t}\n\n\tfmt.Fprintln(stdout, t.Format(tree.INF))\n\n\treturn nil\n}\n\nfunc (s *Action) recipientsList(ctx context.Context) []string {\n\tt, err := s.Store.RecipientsTree(ctxutil.WithHidden(ctx, true), false)\n\tif err != nil {\n\t\tdebug.Log(\"failed to list recipients: %s\", err)\n\n\t\treturn nil\n\t}\n\n\treturn t.List(tree.INF)\n}\n\n// RecipientsComplete will print a list of recipients for bash\n// completion.\nfunc (s *Action) RecipientsComplete(c *cli.Context) {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif err := s.IsInitialized(c); err != nil {\n\t\tdebug.Log(\"IsInitialized returned error: %s\", err)\n\n\t\treturn\n\t}\n\n\tfor _, v := range s.recipientsList(ctx) {\n\t\tfmt.Fprintln(stdout, v)\n\t}\n}\n\n// RecipientsAck updates `recipients.hash`.\nfunc (s *Action) RecipientsAck(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\treturn s.Store.SaveRecipients(ctxutil.WithHidden(ctx, true), true)\n}\n\n// RecipientsAdd adds new recipients.\nfunc (s *Action) RecipientsAdd(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tforce := c.Bool(\"force\")\n\tadded := 0\n\n\t// select store.\n\tif store == \"\" {\n\t\tstore = cui.AskForStore(ctx, s.Store)\n\t}\n\n\tif err := s.Store.CheckRecipients(ctx, store); err != nil && !force {\n\t\tout.Errorf(ctx, \"%s. Please remove expired keys or extend their validity. See https://go.gopass.pw/faq#expired-recipients\", err.Error())\n\n\t\treturn exit.Error(exit.Recipients, err, \"recipients invalid: %q\", err)\n\t}\n\n\tcrypto := s.Store.Crypto(ctx, store)\n\n\t// select recipient.\n\trecipients := c.Args().Slice()\n\tif len(recipients) < 1 {\n\t\tout.Notice(ctx, \"Fetching available recipients. Please wait...\")\n\n\t\tdebug.Log(\"no recipients given, asking for selection\")\n\t\tr, err := s.recipientsSelectForAdd(ctx, store)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trecipients = r\n\t}\n\n\tdebug.Log(\"adding recipients: %+v\", recipients)\n\tfor _, r := range recipients {\n\t\tkeys, err := crypto.FindRecipients(ctx, r)\n\t\tif err != nil {\n\t\t\tout.Warningf(ctx, \"Failed to list public key %q: %s\", r, err)\n\t\t\tvar imported bool\n\t\t\tif sub, err := s.Store.GetSubStore(store); err == nil {\n\t\t\t\tctx = config.WithMount(ctx, store)\n\t\t\t\tif err := sub.ImportMissingPublicKeys(ctx, r); err != nil {\n\t\t\t\t\tout.Warningf(ctx, \"Failed to import missing public keys: %s\", err)\n\t\t\t\t}\n\t\t\t\timported = err == nil\n\t\t\t}\n\t\t\tif !force && !imported {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkeys = []string{r}\n\t\t}\n\t\tif len(keys) < 1 && !force && crypto.Name() == \"gpgcli\" {\n\t\t\tout.Printf(ctx, \"Warning: No matching valid key found. If the key is in your keyring you may need to validate it.\")\n\t\t\tout.Printf(ctx, \"If this is your key: gpg --edit-key %s; trust (set to ultimate); quit\", r)\n\t\t\tout.Printf(ctx, \"If this is not your key: gpg --edit-key %s; lsign; trust; save; quit\", r)\n\t\t\tout.Printf(ctx, \"You may need to run 'gpg --update-trustdb' afterwards\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"found recipients for %q: %+v\", r, keys)\n\n\t\tif !force && !termio.AskForConfirmation(ctx, fmt.Sprintf(\"Do you want to add %q (key %q) as a recipient to the store %q?\", crypto.FormatKey(ctx, r, \"\"), r, store)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := s.Store.AddRecipient(ctx, store, r); err != nil {\n\t\t\treturn exit.Error(exit.Recipients, err, \"failed to add recipient %q: %s\", r, err)\n\t\t}\n\t\tadded++\n\t}\n\tif added < 1 {\n\t\treturn exit.Error(exit.Unknown, nil, \"no key added\")\n\t}\n\n\tout.Printf(ctx, \"\\nAdded %d recipients\", added)\n\tout.Printf(ctx, \"You need to run 'gopass sync' to push these changes\")\n\n\treturn nil\n}\n\n// RecipientsRemove removes recipients.\nfunc (s *Action) RecipientsRemove(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tstore := c.String(\"store\")\n\tforce := c.Bool(\"force\")\n\tremoved := 0\n\n\t// select store if none is given.\n\tif !c.IsSet(\"store\") {\n\t\tstore = cui.AskForStore(ctx, s.Store)\n\t}\n\n\t// get the crypto backend from the store so we can perform the correct\n\t// recipient checks.\n\tcrypto := s.Store.Crypto(ctx, store)\n\n\t// ask to select a recipient if none are given.\n\trecipients := c.Args().Slice()\n\tif len(recipients) < 1 {\n\t\trs, err := s.recipientsSelectForRemoval(ctx, store)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trecipients = rs\n\t}\n\n\tknownRecipients := s.Store.ListRecipients(ctx, store)\n\n\t// try to remove all given recipients.\n\tfor _, r := range recipients {\n\t\t// check and warn if the user is trying to remove themselves\n\t\tkl, err := crypto.FindIdentities(ctx, r)\n\t\tif err == nil && len(kl) > 0 {\n\t\t\tif !termio.AskForConfirmation(ctx, fmt.Sprintf(\"Do you want to remove yourself (%s) from the recipients?\", r)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// if a literal recipient (e.g. ID) is given just remove that w/o any kind of lookups.\n\t\tif set.Contains(knownRecipients, r) {\n\t\t\tdebug.Log(\"Removing %q from %q (direct)\", r, store)\n\t\t\tif err := s.Store.RemoveRecipient(ctx, store, r); err != nil {\n\t\t\t\treturn exit.Error(exit.Recipients, err, \"failed to remove recipient %q: %s\", r, err)\n\t\t\t}\n\n\t\t\tremoved++\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// look up the full key ID of the given recipient (could be email or any kind of short ID)\n\t\tkeys, err := crypto.FindRecipients(ctx, r)\n\t\tif err != nil {\n\t\t\tout.Printf(ctx, \"WARNING: Failed to list public key %q: %s\", r, err)\n\t\t\tout.Printf(ctx, \"Hint: You can use `--force` to remove unknown keys.\")\n\t\t\tif !force {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tkeys = []string{r}\n\t\t}\n\t\tdebug.Log(\"FindRecipients translated %q into %v\", r, keys)\n\n\t\tif len(keys) < 1 && !force {\n\t\t\tout.Printf(ctx, \"Warning: No matching valid key found. If the key is in your keyring you may need to validate it.\")\n\t\t\tout.Printf(ctx, \"If this is your key: gpg --edit-key %s; trust (set to ultimate); quit\", r)\n\t\t\tout.Printf(ctx, \"If this is not your key: gpg --edit-key %s; lsign; trust; save; quit\", r)\n\t\t\tout.Printf(ctx, \"You may need to run 'gpg --update-trustdb' afterwards\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\trecp := r //nolint:copyloopvar\n\t\tif len(keys) > 0 {\n\t\t\tif nr := crypto.Fingerprint(ctx, keys[0]); nr != \"\" {\n\t\t\t\tdebug.Log(\"Fingerprint translated %q into %q\", keys[0], nr)\n\t\t\t\trecp = nr\n\t\t\t}\n\t\t}\n\n\t\tdebug.Log(\"Removing %q from %q (indirect)\", recp, store)\n\t\tif err := s.Store.RemoveRecipient(ctx, store, recp); err != nil {\n\t\t\treturn exit.Error(exit.Recipients, err, \"failed to remove recipient %q: %s\", recp, err)\n\t\t}\n\n\t\tfmt.Fprintf(stdout, removalWarning, r)\n\t\tremoved++\n\t}\n\n\tif removed < 1 {\n\t\treturn exit.Error(exit.Unknown, nil, \"no key removed\")\n\t}\n\n\tout.Printf(ctx, \"\\nRemoved %d recipients\", removed)\n\tout.Printf(ctx, \"You need to run 'gopass sync' to push these changes\")\n\n\treturn nil\n}\n\nfunc (s *Action) recipientsSelectForRemoval(ctx context.Context, store string) ([]string, error) {\n\tcrypto := s.Store.Crypto(ctx, store)\n\n\tids := s.Store.ListRecipients(ctx, store)\n\tchoices := make([]string, 0, len(ids))\n\tfor _, id := range ids {\n\t\tchoices = append(choices, crypto.FormatKey(ctx, id, \"\"))\n\t}\n\n\tif len(choices) < 1 {\n\t\treturn nil, nil\n\t}\n\n\tact, sel := cui.GetSelection(ctx, \"Remove recipient -\", choices)\n\tswitch act {\n\tcase \"default\":\n\t\tfallthrough\n\tcase \"show\":\n\t\treturn []string{ids[sel]}, nil\n\tdefault:\n\t\treturn nil, exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n}\n\nfunc (s *Action) recipientsSelectForAdd(ctx context.Context, store string) ([]string, error) {\n\tcrypto := s.Store.Crypto(ctx, store)\n\n\tchoices := []string{}\n\tkl, _ := crypto.FindRecipients(ctx)\n\tfor _, key := range kl {\n\t\tchoices = append(choices, crypto.FormatKey(ctx, key, \"\"))\n\t}\n\n\tif len(choices) < 1 {\n\t\treturn nil, nil\n\t}\n\n\tact, sel := cui.GetSelection(ctx, \"Add Recipient -\", choices)\n\tswitch act {\n\tcase \"default\":\n\t\tfallthrough\n\tcase \"show\":\n\t\treturn []string{kl[sel]}, nil\n\tdefault:\n\t\treturn nil, exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n}\n"
  },
  {
    "path": "internal/action/recipients_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecipients(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"print recipients tree\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsPrint(gptest.CliCtx(ctx, t)))\n\n\t\thint := `Hint: run 'gopass sync' to import any missing public keys`\n\t\twant := `gopass\n└── 0xDEADBEEF`\n\n\t\tassert.Contains(t, buf.String(), hint)\n\t\tassert.Contains(t, buf.String(), want)\n\t})\n\n\tt.Run(\"complete recipients\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tact.RecipientsComplete(gptest.CliCtx(ctx, t))\n\t\twant := \"0xDEADBEEF\\n\"\n\t\tassert.Equal(t, want, buf.String())\n\t})\n\n\tt.Run(\"add recipients w/o args\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.RecipientsAdd(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"remove recipients w/o args\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.RecipientsRemove(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"add recipient 0xFEEDBEEF\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtx(ctx, t, \"0xFEEDBEEF\")))\n\t})\n\n\tt.Run(\"add recipient 0xBEEFFEED\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtx(ctx, t, \"0xBEEFFEED\")))\n\t})\n\n\tt.Run(\"remove recipient 0xDEADBEEF\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsRemove(gptest.CliCtx(ctx, t, \"0xDEADBEEF\")))\n\t})\n}\n\nfunc TestRecipientsGpg(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\tu := gptest.NewGUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tt.Run(\"print recipients tree\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsPrint(gptest.CliCtx(ctx, t)))\n\n\t\thint := `Hint: run 'gopass sync' to import any missing public keys`\n\t\twant := `gopass\n└── BE73F104`\n\n\t\tassert.Contains(t, buf.String(), hint)\n\t\tassert.Contains(t, buf.String(), want)\n\t})\n\n\tt.Run(\"complete recipients\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tact.RecipientsComplete(gptest.CliCtx(ctx, t))\n\t\twant := \"BE73F104\\n\"\n\t\tassert.Equal(t, want, buf.String())\n\t})\n\n\tt.Run(\"add recipients w/o args\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.RecipientsAdd(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"remove recipients w/o args\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.RecipientsRemove(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"add recipient 0xFEEDBEEF\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtx(ctx, t, \"0xFEEDBEEF\")))\n\t})\n\n\tt.Run(\"add recipient 0xBEEFFEED\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"0xBEEFFEED\")))\n\t})\n\n\tt.Run(\"remove recipient 0x82EBD945BE73F104\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsRemove(gptest.CliCtx(ctx, t, \"0x82EBD945BE73F104\")))\n\t})\n\n\tt.Run(\"add recipient 0xFEEDFEED\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"0xFEEDFEED\")))\n\t})\n\n\tt.Run(\"remove recipient 0xFEEDFEED\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsRemove(gptest.CliCtx(ctx, t, \"0xFEEDFEED\")))\n\t})\n\n\tt.Run(\"add recipient 0xFEEDFEED\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsAdd(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"0xFEEDFEED\")))\n\t})\n\n\tt.Run(\"remove recipient 0xFEEDFEED (force)\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.RecipientsRemove(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"force\": \"true\"}, \"0xFEEDFEED\")))\n\t})\n}\n"
  },
  {
    "path": "internal/action/reminder.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/env\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n)\n\nfunc (s *Action) printReminder(ctx context.Context) {\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn\n\t}\n\n\tif !ctxutil.IsTerminal(ctx) {\n\t\treturn\n\t}\n\n\tif sv := os.Getenv(\"GOPASS_NO_REMINDER\"); sv != \"\" || config.Bool(ctx, \"core.noreminder\") {\n\t\treturn\n\t}\n\n\t// this might be printed along other reminders\n\tif s.rem.Overdue(\"env\") {\n\t\tmsg, err := env.Check(ctx)\n\t\tif err != nil {\n\t\t\tout.Warningf(ctx, \"Failed to check environment: %s\", err)\n\t\t}\n\t\tif msg != \"\" {\n\t\t\tout.Warningf(ctx, \"%s\", msg)\n\t\t}\n\t\t_ = s.rem.Reset(\"env\")\n\t}\n\n\t// Note: We only want to print one reminder per day (at most).\n\t// So we intentionally return after printing one, leaving the others\n\t// for the following days.\n\tif s.rem.Overdue(\"update\") {\n\t\tout.Notice(ctx, \"You haven't checked for updates in a while. Run 'gopass version' or 'gopass update' to check.\")\n\n\t\treturn\n\t}\n\n\tif s.rem.Overdue(\"fsck\") {\n\t\tout.Notice(ctx, \"You haven't run 'gopass fsck' in a while.\")\n\n\t\treturn\n\t}\n\n\tif s.rem.Overdue(\"audit\") {\n\t\tout.Notice(ctx, \"You haven't run 'gopass audit' in a while.\")\n\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/action/reorg.go",
    "content": "package action\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Reorg is the action that allows to reorganize a part of the store.\nfunc (s *Action) Reorg(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tprefix := c.Args().Get(0)\n\n\t// list secrets\n\tsecrets, err := s.Store.List(ctx, -1)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list secrets: %s\", err)\n\t}\n\n\t// filter by prefix\n\tvar initialSecrets []string\n\tif prefix != \"\" {\n\t\tfor _, secret := range secrets {\n\t\t\tif strings.HasPrefix(secret, prefix) {\n\t\t\t\tinitialSecrets = append(initialSecrets, secret)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tinitialSecrets = secrets\n\t}\n\n\tif len(initialSecrets) == 0 {\n\t\tout.Printf(ctx, \"No secrets found to reorganize.\")\n\n\t\treturn nil\n\t}\n\n\t// get initial content\n\tinitialContent := []byte(strings.Join(initialSecrets, \"\\n\") + \"\\n\")\n\n\t// open editor\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn exit.Error(exit.Unsupported, nil, \"reorg is not supported in non-interactive mode\")\n\t}\n\teditorPath := editor.Path(c)\n\tmodifiedContent, err := editor.Invoke(ctx, editorPath, initialContent)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke editor: %s\", err)\n\t}\n\n\t// parse modified secrets\n\tvar modifiedSecrets []string\n\tscanner := bufio.NewScanner(bytes.NewReader(modifiedContent))\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif line != \"\" {\n\t\t\tmodifiedSecrets = append(modifiedSecrets, line)\n\t\t}\n\t}\n\n\treturn s.ReorgAfterEdit(ctx, initialSecrets, modifiedSecrets)\n}\n\n// ReorgAfterEdit performs the reorganization after the user has edited the list of secrets.\nfunc (s *Action) ReorgAfterEdit(ctx context.Context, initialSecrets, modifiedSecrets []string) error {\n\tif len(initialSecrets) != len(modifiedSecrets) {\n\t\treturn exit.Error(exit.Usage, nil, \"number of secrets must not be changed. Aborting.\")\n\t}\n\n\t// calculate moves\n\tmoves := make(map[string]string)\n\tfor i, oldSecret := range initialSecrets {\n\t\tnewSecret := modifiedSecrets[i]\n\t\tif oldSecret != newSecret {\n\t\t\tmoves[oldSecret] = newSecret\n\t\t}\n\t}\n\n\tif len(moves) == 0 {\n\t\tout.Printf(ctx, \"No changes detected.\")\n\n\t\treturn nil\n\t}\n\n\t// validate moves\n\tif err := s.validateMoves(ctx, moves); err != nil {\n\t\treturn exit.Error(exit.Usage, err, \"failed to validate moves: %s\", err)\n\t}\n\n\t// display diff and ask for confirmation\n\tout.Printf(ctx, \"The following moves will be performed:\")\n\tfor from, to := range moves {\n\t\tout.Printf(ctx, \"  - %s -> %s\", from, to)\n\t}\n\n\tif !termio.AskForConfirmation(ctx, \"Do you want to proceed?\") {\n\t\treturn exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n\n\t// disable automatic commits\n\tctx = ctxutil.WithGitCommit(ctx, false)\n\n\t// execute moves\n\tfor from, to := range moves {\n\t\tif err := s.Store.Move(ctx, from, to); err != nil {\n\t\t\treturn exit.Error(exit.Unknown, err, \"failed to move %s to %s: %s\", from, to, err)\n\t\t}\n\t\tout.Printf(ctx, \"Moved %s to %s\", from, to)\n\t}\n\n\t// get storage from the first move\n\tvar storage backend.Storage\n\tfor from := range moves {\n\t\tstorage = s.Store.Storage(ctx, from)\n\n\t\tbreak\n\t}\n\tif storage == nil {\n\t\treturn exit.Error(exit.Git, nil, \"failed to get storage backend\")\n\t}\n\n\t// commit changes\n\tif err := storage.TryCommit(ctx, \"Reorganized secrets\"); err != nil {\n\t\treturn exit.Error(exit.Git, err, \"failed to commit changes: %s\", err)\n\t}\n\n\tout.Printf(ctx, \"Successfully reorganized secrets.\")\n\n\treturn nil\n}\n\nfunc (s *Action) validateMoves(ctx context.Context, moves map[string]string) error {\n\tdestinations := make(map[string]string, len(moves))\n\tfor from, to := range moves {\n\t\t// check for duplicate destinations\n\t\tif existingFrom, found := destinations[to]; found {\n\t\t\treturn fmt.Errorf(\"duplicate destination %q for %q and %q\", to, from, existingFrom)\n\t\t}\n\t\tdestinations[to] = from\n\n\t\t// check for cross-mount moves\n\t\tfromMount := s.Store.MountPoint(from)\n\t\ttoMount := s.Store.MountPoint(to)\n\t\tif fromMount != toMount {\n\t\t\treturn fmt.Errorf(\"moving secrets across mounts is not supported: %s (%s) -> %s (%s)\", from, fromMount, to, toMount)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/reorg_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReorg(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithTerminal(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tt.Run(\"move foo to bar\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\n\t\t// create a secret\n\t\tsec := secrets.NewAKVWithData(\"foo\", nil, \"\", false)\n\t\trequire.NoError(t, act.Store.Set(ctxutil.WithGitCommit(ctx, false), \"foo\", sec))\n\t\tbuf.Reset()\n\n\t\tinitial := []string{\"foo\"}\n\t\tmodified := []string{\"bar\"}\n\n\t\trequire.NoError(t, act.ReorgAfterEdit(ctx, initial, modified))\n\n\t\t// check that foo is now bar\n\t\t_, err := act.Store.Get(ctx, \"bar\")\n\t\trequire.NoError(t, err)\n\n\t\t// check that foo is gone\n\t\t_, err = act.Store.Get(ctx, \"foo\")\n\t\trequire.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/action/repl.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/ergochat/readline\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\tshellquote \"github.com/kballard/go-shellquote\"\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype mindFlagsCompleter struct {\n\tbase readline.AutoCompleter\n}\n\nfunc (f *mindFlagsCompleter) Do(line []rune, pos int) ([][]rune, int) {\n\t// Get the portion of the line up to the cursor and split to tokens\n\torigInput := string(line[:pos])\n\ttokens := strings.Fields(origInput)\n\tfiltered := make([]string, 0, len(tokens))\n\tfor _, tok := range tokens {\n\t\t// Skip flag tokens\n\t\tif strings.HasPrefix(tok, \"-\") {\n\t\t\tcontinue\n\t\t}\n\t\tfiltered = append(filtered, tok)\n\t}\n\tsanitized := strings.Join(filtered, \" \")\n\t// Add additional space if had one on original line\n\tif len(origInput) > 0 && origInput[len(origInput)-1] == ' ' {\n\t\tsanitized += \" \"\n\t}\n\n\treturn f.base.Do([]rune(sanitized), len([]rune(sanitized)))\n}\n\nfunc (s *Action) entriesForCompleter(ctx context.Context) ([]*readline.PrefixCompleter, error) {\n\targs := []*readline.PrefixCompleter{}\n\tlist, err := s.Store.List(ctx, tree.INF)\n\tif err != nil {\n\t\treturn args, err\n\t}\n\tfor _, v := range list {\n\t\targs = append(args, readline.PcItem(v))\n\t}\n\n\treturn args, nil\n}\n\nfunc (s *Action) replCompleteRecipients(ctx context.Context, cmd *cli.Command) []*readline.PrefixCompleter {\n\tsubCmds := []*readline.PrefixCompleter{}\n\tif cmd.Name == \"remove\" {\n\t\tfor _, r := range s.recipientsList(ctx) {\n\t\t\tsubCmds = append(subCmds, readline.PcItem(r))\n\t\t}\n\t}\n\targs := []*readline.PrefixCompleter{}\n\targs = append(args, readline.PcItem(cmd.Name, subCmds...))\n\tfor _, alias := range cmd.Aliases {\n\t\targs = append(args, readline.PcItem(alias, subCmds...))\n\t}\n\n\treturn args\n}\n\nfunc (s *Action) replCompleteTemplates(ctx context.Context, cmd *cli.Command) []*readline.PrefixCompleter {\n\tsubCmds := []*readline.PrefixCompleter{}\n\tfor _, r := range s.templatesList(ctx) {\n\t\tsubCmds = append(subCmds, readline.PcItem(r))\n\t}\n\targs := []*readline.PrefixCompleter{}\n\targs = append(args, readline.PcItem(cmd.Name, subCmds...))\n\tfor _, alias := range cmd.Aliases {\n\t\targs = append(args, readline.PcItem(alias, subCmds...))\n\t}\n\n\treturn args\n}\n\nfunc (s *Action) prefixCompleter(c *cli.Context) *readline.PrefixCompleter {\n\tsecrets, err := s.entriesForCompleter(c.Context)\n\tif err != nil {\n\t\tdebug.Log(\"failed to list secrets: %s\", err)\n\t}\n\tcmds := []*readline.PrefixCompleter{}\n\tfor _, cmd := range c.App.Commands {\n\t\tif cmd.Hidden {\n\t\t\tcontinue\n\t\t}\n\t\tsubCmds := []*readline.PrefixCompleter{}\n\t\tswitch cmd.Name {\n\t\tcase \"config\":\n\t\t\tfor _, k := range s.configKeys() {\n\t\t\t\tsubCmds = append(subCmds, readline.PcItem(k))\n\t\t\t}\n\t\tcase \"recipients\":\n\t\t\tsubCmds = append(subCmds, s.replCompleteRecipients(c.Context, cmd)...)\n\t\tcase \"templates\":\n\t\t\tsubCmds = append(subCmds, s.replCompleteTemplates(c.Context, cmd)...)\n\t\tcase \"cat\":\n\t\t\tfallthrough\n\t\tcase \"delete\":\n\t\t\tfallthrough\n\t\tcase \"edit\":\n\t\t\tfallthrough\n\t\tcase \"generate\":\n\t\t\tfallthrough\n\t\tcase \"history\":\n\t\t\tfallthrough\n\t\tcase \"list\":\n\t\t\tfallthrough\n\t\tcase \"move\":\n\t\t\tfallthrough\n\t\tcase \"otp\":\n\t\t\tfallthrough\n\t\tcase \"show\":\n\t\t\tsubCmds = append(subCmds, secrets...)\n\t\tdefault:\n\t\t}\n\t\tfor _, scmd := range cmd.Subcommands {\n\t\t\tsubCmds = append(subCmds, readline.PcItem(scmd.Name))\n\t\t}\n\t\tcmds = append(cmds, readline.PcItem(cmd.Name, subCmds...))\n\t\tfor _, alias := range cmd.Aliases {\n\t\t\tcmds = append(cmds, readline.PcItem(alias, subCmds...))\n\t\t}\n\t}\n\n\treturn readline.NewPrefixCompleter(cmds...)\n}\n\n// REPL implements a read-execute-print-line shell\n// with readline support and autocompletion.\nfunc (s *Action) REPL(c *cli.Context) error {\n\tc.App.ExitErrHandler = func(c *cli.Context, err error) {\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\t\tout.Errorf(c.Context, \"%s\", err)\n\t}\n\n\tout.Printf(c.Context, logo)\n\tout.Printf(c.Context, \"🌟 Welcome to gopass!\")\n\tout.Printf(c.Context, \"⚠ This is the built-in shell. Type 'help' for a list of commands.\")\n\n\trl, err := readline.New(\"gopass> \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\t_ = rl.Close()\n\t}()\n\nREAD:\n\tfor {\n\t\t// check for context cancellation\n\t\tselect {\n\t\tcase <-c.Done():\n\t\t\treturn fmt.Errorf(\"user aborted\")\n\t\tdefault:\n\t\t}\n\n\t\t// we need to update the completer on every loop since\n\t\t// the list of secrets may have changed, e.g. due to\n\t\t// the user adding a new secret.\n\t\tcfg := rl.GetConfig()\n\t\tcfg.AutoComplete = &mindFlagsCompleter{base: s.prefixCompleter(c)}\n\t\tif err := rl.SetConfig(cfg); err != nil {\n\t\t\tdebug.Log(\"Failed to set readline config: %s\", err)\n\n\t\t\tbreak\n\t\t}\n\n\t\tline, err := rl.Readline()\n\t\tif err != nil {\n\t\t\tdebug.Log(\"Readline error: %s\", err)\n\n\t\t\tbreak\n\t\t}\n\t\targs, err := shellquote.Split(line)\n\t\tif err != nil {\n\t\t\tout.Printf(c.Context, \"Error: %s\", err)\n\n\t\t\tcontinue\n\t\t}\n\t\tif len(args) < 1 {\n\t\t\tcontinue\n\t\t}\n\t\tswitch strings.ToLower(args[0]) {\n\t\tcase \"quit\":\n\t\t\tbreak READ\n\t\tcase \"lock\":\n\t\t\ts.replLock(c.Context)\n\n\t\t\tcontinue\n\t\tcase \"clear\":\n\t\t\trl.ClearScreen()\n\n\t\t\tcontinue\n\t\tdefault:\n\t\t}\n\n\t\tif err := c.App.RunContext(c.Context, append([]string{\"gopass\"}, args...)); err != nil {\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) replLock(ctx context.Context) {\n\tif err := s.Store.Lock(); err != nil {\n\t\tout.Errorf(ctx, \"Failed to lock stores: %s\", err)\n\n\t\treturn\n\t}\n\tout.OKf(ctx, \"Locked\")\n}\n"
  },
  {
    "path": "internal/action/repl_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/ergochat/readline\"\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestREPL(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\trl, err := readline.NewEx(&readline.Config{\n\t\tPrompt: \"gopass> \",\n\t\tStdin:  bytes.NewBufferString(\"help\\nquit\\n\"),\n\t})\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\t_ = rl.Close()\n\t}()\n\n\terr = act.REPL(gptest.CliCtx(ctx, t))\n\trequire.NoError(t, err)\n\tassert.Contains(t, buf.String(), \"help\")\n}\n\nfunc TestEntriesForCompleter(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tcompleters, err := act.entriesForCompleter(ctx)\n\trequire.NoError(t, err)\n\tassert.Len(t, completers, 1)\n}\n\nfunc TestReplCompleteRecipients(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tcmd := &cli.Command{\n\t\tName: \"remove\",\n\t}\n\n\tcompleters := act.replCompleteRecipients(ctx, cmd)\n\tassert.Len(t, completers, 1)\n}\n\nfunc TestReplCompleteTemplates(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t\tstdout = os.Stdout\n\t}()\n\n\tcmd := &cli.Command{\n\t\tName: \"templates\",\n\t}\n\n\tcompleters := act.replCompleteTemplates(ctx, cmd)\n\tassert.Len(t, completers, 1)\n}\n"
  },
  {
    "path": "internal/action/setup.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\tgpgcli \"github.com/gopasspw/gopass/internal/backend/crypto/gpg/cli\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store/root\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/xkcdgen\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Setup will invoke the onboarding / setup wizard.\nfunc (s *Action) Setup(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tremote := c.String(\"remote\")\n\tteam := c.String(\"alias\")\n\tcreate := c.Bool(\"create\")\n\n\tctx, err := initParseContext(ctx, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tout.Printf(ctx, logo)\n\tout.Printf(ctx, \"🌟 Welcome to gopass!\")\n\tout.Printf(ctx, \"🌟 Initializing a new password store ...\")\n\tif backend.HasCryptoBackend(ctx) {\n\t\tout.Printf(ctx, \"🔐 Using crypto backend: %s\", backend.GetCryptoBackend(ctx))\n\t}\n\tif backend.HasStorageBackend(ctx) {\n\t\tout.Printf(ctx, \"💾 Using storage backend: %s\", backend.GetStorageBackend(ctx))\n\t}\n\n\tif name := termio.DetectName(ctx, c); name != \"\" {\n\t\tctx = ctxutil.WithUsername(ctx, name)\n\t}\n\n\tif email := termio.DetectEmail(ctx, c); email != \"\" {\n\t\tctx = ctxutil.WithEmail(ctx, email)\n\t}\n\n\t// age: only native keys\n\t// \"[ssh] types should only be used for compatibility with existing keys,\n\t// and native X25519 keys should be preferred otherwise.\"\n\t// https://pkg.go.dev/filippo.io/age@v1.0.0/agessh#pkg-overview.\n\tctx = age.WithOnlyNative(ctx, true)\n\t// gpg: only trusted keys\n\t// only list \"usable\" / properly trused and signed GPG keys by requesting\n\t// always trust is false. Ignored for other backends. See\n\t// https://www.gnupg.org/gph/en/manual/r1554.html.\n\tctx = gpg.WithAlwaysTrust(ctx, false)\n\n\t// need to re-initialize the root store or it's already initialized\n\t// and won't properly set up crypto according to our context.\n\ts.Store = root.New(s.cfg)\n\tinited, err := s.Store.IsInitialized(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to check store status: %s\", err)\n\t}\n\n\tif inited {\n\t\tout.Errorf(ctx, \"Store is already initialized. Aborting wizard to avoid overwriting existing data.\")\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"Starting Onboarding Wizard - remote: %s - team: %s - create: %t - name: %s - email: %s\", remote, team, create, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx))\n\n\tcrypto := s.getCryptoFor(ctx, team)\n\tif crypto == nil {\n\t\treturn fmt.Errorf(\"can not continue without crypto\")\n\t}\n\tdebug.Log(\"Crypto Backend initialized as: %s\", crypto.Name())\n\n\tif err := s.initCheckPrivateKeys(ctx, crypto); err != nil {\n\t\treturn fmt.Errorf(\"failed to check private keys: %w\", err)\n\t}\n\n\t// if a git remote is given, clone it and exit\n\tif remote != \"\" && team == \"\" {\n\t\tif err := s.clone(ctx, remote, \"\", \"\"); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to clone remote %q: %w\", remote, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// if a git remote and a team name are given attempt unattended team setup.\n\tif remote != \"\" && team != \"\" {\n\t\tif create {\n\t\t\treturn s.initCreateTeam(ctx, team, remote)\n\t\t}\n\n\t\treturn s.initJoinTeam(ctx, team, remote)\n\t}\n\n\tif team == \"\" && create {\n\t\treturn fmt.Errorf(\"can not create a team without a team name\")\n\t}\n\n\t// assume local setup by default, remotes can be added easily later.\n\tif err := s.initLocal(ctx, remote); err != nil {\n\t\tdebug.Log(\"Setup failed. initLocal error: %s\", err)\n\n\t\treturn err\n\t}\n\n\tdebug.Log(\"Setup finished. All systems go. 🚀\")\n\n\treturn nil\n}\n\nfunc (s *Action) initCheckPrivateKeys(ctx context.Context, crypto backend.Crypto) error {\n\t// check for existing GPG/Age keypairs (private/secret keys). We need at least\n\t// one useable key pair. If none exists try to create one.\n\tif !s.initHasUseablePrivateKeys(ctx, crypto) {\n\t\tout.Printf(ctx, \"🔐 No useable cryptographic keys. Generating new key pair\")\n\t\tif crypto.Name() == \"gpgcli\" {\n\t\t\tout.Printf(ctx, \"🕰 Key generation may take up to a few minutes\")\n\t\t}\n\t\tif err := s.initGenerateIdentity(ctx, crypto, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create new private key: %w\", err)\n\t\t}\n\t\tout.Printf(ctx, \"🔐 Cryptographic keys generated\")\n\t}\n\n\tdebug.V(1).Log(\"We have useable private keys\")\n\n\treturn nil\n}\n\nfunc (s *Action) initGenerateIdentity(ctx context.Context, crypto backend.Crypto, name, email string) error {\n\tout.Printf(ctx, \"🧪 Creating cryptographic key pair (%s) ...\", crypto.Name())\n\n\tif crypto.Name() == gpgcli.Name {\n\t\tvar err error\n\n\t\tout.Printf(ctx, \"🎩 Gathering information for the %s key pair ...\", crypto.Name())\n\t\tname, err = termio.AskForString(ctx, \"🚶 What is your name?\", name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\temail, err = termio.AskForString(ctx, \"📧 What is your email?\", email)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif strings.TrimSpace(email) == \"\" {\n\t\t\treturn fmt.Errorf(\"⛔️ Please enter a valid email address to proceed\")\n\t\t}\n\t}\n\n\tpassphrase := xkcdgen.Random()\n\tpwGenerated := true\n\t// support fully automated setup (e.g. for tests)\n\t//nolint:nestif\n\tif ctxutil.HasPasswordCallback(ctx) {\n\t\tpw, err := ctxutil.GetPasswordCallback(ctx)(\"\", true)\n\t\tif err == nil {\n\t\t\tpassphrase = string(pw)\n\t\t}\n\t\tpwGenerated = false\n\t} else {\n\t\twant, err := termio.AskForBool(ctx, \"⚠ Do you want to enter a passphrase? (otherwise we generate one for you)\", false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif want {\n\t\t\tpwGenerated = false\n\t\t\tsv, err := termio.AskForPassword(ctx, \"passphrase for your new keypair\", true)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to read passphrase: %w\", err)\n\t\t\t}\n\t\t\tpassphrase = sv\n\t\t}\n\t}\n\n\tif crypto.Name() == \"gpgcli\" {\n\t\t// Note: This issue shouldn't matter much past Linux Kernel 5.6,\n\t\t// eventually we might want to remove this notice. Only applies to\n\t\t// GPG.\n\t\tout.Printf(ctx, \"⏳ This can take a long time. If you get impatient see https://go.gopass.pw/entropy\")\n\t\tif want, err := termio.AskForBool(ctx, \"Continue?\", true); err != nil || !want {\n\t\t\treturn fmt.Errorf(\"user aborted: %w\", err)\n\t\t}\n\t}\n\n\tif pwGenerated {\n\t\tout.Printf(ctx, color.MagentaString(\"Passphrase: \")+passphrase)\n\t\tout.Noticef(ctx, \"You need to remember this very well!\")\n\n\t\t// Prompt to confirm that the user noted their passphrase\n\t\tif want, err := termio.AskForBool(ctx, \"Did you save your passphrase?\", true); err != nil || !want {\n\t\t\treturn fmt.Errorf(\"user did not confirm saving the passphrase: %w\", err)\n\t\t}\n\t}\n\n\tif _, err := crypto.GenerateIdentity(ctx, name, email, passphrase); err != nil {\n\t\treturn fmt.Errorf(\"failed to create new private key: %w\", err)\n\t}\n\n\tout.OKf(ctx, \"Key pair for %s generated\", crypto.Name())\n\n\tout.Notice(ctx, \"🔐 We need to unlock your newly created private key now! Please enter the passphrase you just generated.\")\n\n\t// avoid the gpg cache or we won't find the newly created key\n\tkl, err := crypto.ListIdentities(gpg.WithUseCache(ctx, false))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list private keys: %w\", err)\n\t}\n\n\tif len(kl) > 1 {\n\t\tout.Notice(ctx, \"More than one private key detected. Make sure to use the correct one!\")\n\t}\n\n\tif len(kl) < 1 {\n\t\treturn fmt.Errorf(\"failed to create a usable key pair\")\n\t}\n\n\t// we can export the generated key to the current directory for convenience.\n\tif err := s.initExportPublicKey(ctx, crypto, kl[0]); err != nil {\n\t\treturn err\n\t}\n\tout.OKf(ctx, \"Key pair %s validated\", kl[0])\n\n\treturn nil\n}\n\ntype keyExporter interface {\n\tExportPublicKey(ctx context.Context, id string) ([]byte, error)\n}\n\nfunc (s *Action) initExportPublicKey(ctx context.Context, crypto backend.Crypto, key string) error {\n\texp, ok := crypto.(keyExporter)\n\tif !ok {\n\t\tdebug.Log(\"crypto backend %T can not export public keys\", crypto)\n\n\t\treturn nil\n\t}\n\n\tfn := key + \".pub.key\"\n\twant, err := termio.AskForBool(ctx, fmt.Sprintf(\"Do you want to export your public key to %q?\", fn), false)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !want {\n\t\treturn nil\n\t}\n\n\tpk, err := exp.ExportPublicKey(ctx, key)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to export public key: %w\", err)\n\t}\n\n\tif err := os.WriteFile(fn, pk, 0o6444); err != nil {\n\t\tout.Errorf(ctx, \"❌ Failed to export public key %q: %q\", fn, err)\n\n\t\treturn err\n\t}\n\tout.Printf(ctx, \"✴ Public key exported to %q\", fn)\n\n\treturn nil\n}\n\nfunc (s *Action) initHasUseablePrivateKeys(ctx context.Context, crypto backend.Crypto) bool {\n\tdebug.Log(\"checking for existing, usable identities / private keys for %s\", crypto.Name())\n\tkl, err := crypto.ListIdentities(ctx)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tdebug.Log(\"available private keys: %q for %s\", kl, crypto.Name())\n\n\treturn len(kl) > 0\n}\n\nfunc (s *Action) initSetupGitRemote(ctx context.Context, team, remote string) error {\n\tvar err error\n\tremote, err = termio.AskForString(ctx, \"Please enter the git remote for your shared store\", remote)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read user input: %w\", err)\n\t}\n\n\t// omit RCS output.\n\tctx = ctxutil.WithHidden(ctx, true)\n\tif err := s.Store.RCSAddRemote(ctx, team, \"origin\", remote); err != nil {\n\t\treturn fmt.Errorf(\"failed to add git remote: %w\", err)\n\t}\n\t// initial pull, in case the remote is non-empty.\n\tif err := s.Store.RCSPull(ctx, team, \"origin\", \"\"); err != nil {\n\t\tdebug.Log(\"Initial git pull failed: %s\", err)\n\t}\n\tif err := s.Store.RCSPush(ctx, team, \"origin\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push to git remote: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// initLocal will initialize a local store, useful for local-only setups or as\n// part of team setups to create the root store.\nfunc (s *Action) initLocal(ctx context.Context, remote string) error {\n\tpath := \"\"\n\tif s.Store != nil {\n\t\tpath = s.Store.Path()\n\t}\n\n\tout.Printf(ctx, \"🌟 Configuring your password store ...\")\n\tif err := s.init(ctxutil.WithHidden(ctx, true), \"\", path); err != nil {\n\t\treturn fmt.Errorf(\"failed to init local store: %w\", err)\n\t}\n\n\tif backend.GetStorageBackend(ctx) == backend.GitFS {\n\t\tdebug.Log(\"configuring git remotes\")\n\t\tif want, err := termio.AskForBool(ctx, \"❓ Do you want to add a git remote?\", false); (err == nil && want) || remote != \"\" {\n\t\t\tout.Printf(ctx, \"Configuring the git remote ...\")\n\t\t\tif err := s.initSetupGitRemote(ctx, \"\", remote); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to setup git remote: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\t// TODO remotes for fossil, etc.\n\n\t// detect and add mount a for passage\n\tif err := s.initDetectPassage(ctx); err != nil {\n\t\tout.Warningf(ctx, \"Failed to add passage mount: %s\", err)\n\t}\n\n\tout.OKf(ctx, \"Configuration written\")\n\n\treturn nil\n}\n\nfunc (s *Action) initDetectPassage(ctx context.Context) error {\n\tpIds := age.PassageIDFile()\n\tif !fsutil.IsFile(pIds) {\n\t\tdebug.Log(\"no passage identities found at %s\", pIds)\n\n\t\treturn nil\n\t}\n\n\tpDir := filepath.Dir(pIds)\n\n\tif err := s.Store.AddMount(ctx, \"passage\", pDir); err != nil {\n\t\treturn fmt.Errorf(\"failed to mount passage dir: %w\", err)\n\t}\n\n\tout.OKf(ctx, \"Detected passage store at %s. Mounted below passage/.\", pDir)\n\n\treturn nil\n}\n\n// initCreateTeam will create a local root store and a shared team store.\nfunc (s *Action) initCreateTeam(ctx context.Context, team, remote string) error {\n\tvar err error\n\n\tout.Printf(ctx, \"Creating a new team ...\")\n\tif err := s.initLocal(ctx, \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to create local store: %w\", err)\n\t}\n\n\t// name of the new team.\n\tteam, err = termio.AskForString(ctx, out.Prefix(ctx)+\"Please enter the name of your team (may contain slashes)\", team)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read user input: %w\", err)\n\t}\n\tctx = out.AddPrefix(ctx, \"[\"+team+\"] \")\n\n\tout.Printf(ctx, \"Initializing your shared store ...\")\n\tif err := s.init(ctxutil.WithHidden(ctx, true), team, \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to init shared store: %w\", err)\n\t}\n\tout.OKf(ctx, \"Done. Initialized the store.\")\n\n\tout.Printf(ctx, \"Configuring the git remote ...\")\n\tif err := s.initSetupGitRemote(ctx, team, remote); err != nil {\n\t\treturn fmt.Errorf(\"failed to setup git remote: %w\", err)\n\t}\n\tout.OKf(ctx, \"Done. Created Team %q\", team)\n\n\treturn nil\n}\n\n// initJoinTeam will create a local root store and clone an existing store to\n// a mount.\nfunc (s *Action) initJoinTeam(ctx context.Context, team, remote string) error {\n\tvar err error\n\n\tout.Printf(ctx, \"Joining existing team ...\")\n\tif err := s.initLocal(ctx, \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to create local store: %w\", err)\n\t}\n\n\t// name of the existing team.\n\tteam, err = termio.AskForString(ctx, out.Prefix(ctx)+\"Please enter the name of your team (may contain slashes)\", team)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx = out.AddPrefix(ctx, \"[\"+team+\"]\")\n\n\tout.Printf(ctx, \"Configuring git remote ...\")\n\tremote, err = termio.AskForString(ctx, out.Prefix(ctx)+\"Please enter the git remote for your shared store\", remote)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tout.Printf(ctx, \"Cloning from the git remote ...\")\n\tif err := s.clone(ctxutil.WithHidden(ctx, true), remote, team, \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to clone repo: %w\", err)\n\t}\n\tout.OKf(ctx, \"Done. Joined Team %q\", team)\n\tout.Noticef(ctx, \"You still need to request access to decrypt secrets!\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/setup_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSetupAgeGitFS(t *testing.T) {\n\tu := gptest.NewUnitTester(t) //nolint:staticcheck\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.Age)\n\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {\n\t\treturn []byte(\"foobar\"), nil\n\t})\n\tctx = ctxutil.WithPasswordPurgeCallback(ctx, func(s string) {}) //nolint:staticcheck\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.ErrorContains(t, err, \"not initialized\")\n\trequire.NotNil(t, act)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\t// remove existing config and store, we want to start fresh\n\trequire.NoError(t, os.RemoveAll(u.StoreDir(\"\")))\n\trequire.NoError(t, os.Remove(u.GPConfig()))\n\n\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"storage\": \"gitfs\", \"crypto\": \"age\"})\n\trequire.Error(t, act.IsInitialized(c))\n\trequire.NoError(t, act.Setup(c))\n\tassert.Contains(t, buf.String(), \"Welcome to gopass\")\n\n\tcrypto := act.Store.Crypto(ctx, \"\")\n\trequire.NotNil(t, crypto)\n\tassert.Equal(t, \"age\", crypto.Name())\n\tassert.True(t, act.initHasUseablePrivateKeys(ctx, crypto))\n\trequire.NoError(t, act.initGenerateIdentity(ctx, crypto, \"foo bar\", \"foo.bar@example.org\"))\n\tbuf.Reset()\n\n\tact.printRecipients(ctx, \"\")\n\tassert.Contains(t, buf.String(), \"age1\")\n\tbuf.Reset()\n}\n\nfunc TestSetupPlainFS(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\tctx = backend.WithStorageBackend(ctx, backend.FS)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tc := gptest.CliCtx(ctx, t, \"foo.bar@example.org\")\n\trequire.NoError(t, act.IsInitialized(c))\n\tbuf.Reset()\n\n\trequire.Error(t, act.Init(c))\n\tassert.Contains(t, buf.String(), \"already initialized\")\n\tbuf.Reset()\n\n\t// this will abort because the store is already initialized\n\trequire.NoError(t, act.Setup(c))\n\tassert.Contains(t, buf.String(), \"already initialized\")\n\tbuf.Reset()\n\n\tcrypto := act.Store.Crypto(ctx, \"\")\n\trequire.NotNil(t, crypto)\n\tassert.Equal(t, \"plain\", crypto.Name())\n\tassert.True(t, act.initHasUseablePrivateKeys(ctx, crypto))\n\trequire.Error(t, act.initGenerateIdentity(ctx, crypto, \"foo bar\", \"foo.bar@example.org\"))\n\tbuf.Reset()\n\n\tact.printRecipients(ctx, \"\")\n\tassert.Contains(t, buf.String(), \"0xDEADBEEF\")\n\tbuf.Reset()\n\n\t// un-initialize the store\n\trequire.NoError(t, os.Remove(filepath.Join(u.StoreDir(\"\"), plain.IDFile)))\n\trequire.Error(t, act.IsInitialized(c))\n\tbuf.Reset()\n\n\t// remove existing config and store\n\trequire.NoError(t, os.RemoveAll(u.StoreDir(\"\")))\n\trequire.NoError(t, os.Remove(u.GPConfig()))\n\n\t// re-initialize the store, i.e. test that a fresh setup with plain and fs works\n\trequire.NoError(t, act.Setup(c))\n\tassert.Contains(t, buf.String(), \"Welcome to gopass\")\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/show.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/notify\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/clipboard\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/gopasspw/gopass/pkg/qrcon\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc showParseArgs(c *cli.Context) context.Context {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tif c.IsSet(\"clip\") {\n\t\tctx = WithOnlyClip(ctx, c.Bool(\"clip\"))\n\t}\n\n\tif c.IsSet(\"unsafe\") {\n\t\tctx = ctxutil.WithForce(ctx, c.Bool(\"unsafe\"))\n\t}\n\n\tif c.IsSet(\"safe\") {\n\t\tcfg, _ := config.FromContext(ctx)\n\t\t_ = cfg.SetEnv(\"show.safecontent\", strconv.FormatBool(c.Bool(\"safe\")))\n\t\tctx = cfg.WithConfig(ctx)\n\t}\n\n\tif c.IsSet(\"qr\") {\n\t\tctx = WithPrintQR(ctx, c.Bool(\"qr\"))\n\t}\n\tif c.IsSet(\"qrbody\") {\n\t\tctx = WithQRBody(ctx, c.Bool(\"qrbody\"))\n\t}\n\n\tif c.IsSet(\"password\") {\n\t\tctx = WithPasswordOnly(ctx, c.Bool(\"password\"))\n\t}\n\n\tif c.IsSet(\"revision\") {\n\t\tctx = WithRevision(ctx, c.String(\"revision\"))\n\t}\n\n\tctx = WithAlsoClip(ctx, config.Bool(ctx, \"show.autoclip\"))\n\tif c.IsSet(\"alsoclip\") {\n\t\tctx = WithAlsoClip(ctx, c.Bool(\"alsoclip\"))\n\t}\n\n\tif c.IsSet(\"noparsing\") {\n\t\tctx = ctxutil.WithShowParsing(ctx, !c.Bool(\"noparsing\"))\n\t}\n\n\tif c.IsSet(\"chars\") {\n\t\tiv := []int{}\n\t\tfor v := range strings.SplitSeq(c.String(\"chars\"), \",\") {\n\t\t\tv = strings.TrimSpace(v)\n\t\t\tif v == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ti, err := strconv.Atoi(v)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tiv = append(iv, i)\n\t\t}\n\t\tctx = WithPrintChars(ctx, iv)\n\t}\n\tctx = WithClip(ctx, IsOnlyClip(ctx) || IsAlsoClip(ctx))\n\n\treturn ctx\n}\n\n// Show the content of a secret file.\nfunc (s *Action) Show(c *cli.Context) error {\n\tname := c.Args().First()\n\n\tctx := showParseArgs(c)\n\n\tif key := c.Args().Get(1); key != \"\" {\n\t\tdebug.Log(\"Adding key to ctx: %s\", key)\n\t\tctx = WithKey(ctx, key)\n\t}\n\n\tif err := s.show(ctx, c, name, true); err != nil {\n\t\treturn exit.Error(exit.Decrypt, err, \"%s\", err)\n\t}\n\n\treturn hook.InvokeRoot(ctx, \"show.post-hook\", name, s.Store, GetKey(ctx))\n}\n\n// show displays the given secret/key.\nfunc (s *Action) show(ctx context.Context, c *cli.Context, name string, recurse bool) error {\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"Usage: %s show [name]\", s.Name)\n\t}\n\n\tif s.Store.IsDir(ctx, name) && !s.Store.Exists(ctx, name) {\n\t\treturn s.List(c)\n\t}\n\n\tif s.Store.IsDir(ctx, name) && ctxutil.IsTerminal(ctx) && !IsPasswordOnly(ctx) {\n\t\tout.Warningf(ctx, \"%s is a secret and a folder. Use 'gopass show %s' to display the secret and 'gopass list %s' to show the content of the folder\", name, name, name)\n\t}\n\n\tmp := s.Store.MountPoint(name)\n\tctx = config.WithMount(ctx, mp)\n\n\tif HasRevision(ctx) {\n\t\treturn s.showHandleRevision(ctx, c, name, GetRevision(ctx))\n\t}\n\n\tsec, err := s.Store.Get(ctx, name)\n\tif err != nil {\n\t\treturn s.showHandleError(ctx, c, name, recurse, err)\n\t}\n\n\treturn s.showHandleOutput(ctx, name, sec)\n}\n\n// showHandleRevision displays a single revision.\nfunc (s *Action) showHandleRevision(ctx context.Context, c *cli.Context, name, revision string) error {\n\trevision, err := s.parseRevision(ctx, name, revision)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to get revisions: %s\", err)\n\t}\n\n\tctx, sec, err := s.Store.GetRevision(ctx, name, revision)\n\tif err != nil {\n\t\treturn s.showHandleError(ctx, c, name, false, err)\n\t}\n\n\treturn s.showHandleOutput(ctx, name, sec)\n}\n\nfunc (s *Action) parseRevision(ctx context.Context, name, revision string) (string, error) {\n\tdebug.Log(\"Revision: %s\", revision)\n\tif !strings.HasPrefix(revision, \"-\") {\n\t\treturn revision, nil\n\t}\n\n\trevStr := strings.TrimPrefix(revision, \"-\")\n\toffset, err := strconv.Atoi(revStr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdebug.Log(\"Offset: %d\", offset)\n\trevs, err := s.Store.ListRevisions(ctx, name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(revs) < offset {\n\t\tdebug.Log(\"Not enough revisions (%d)\", len(revs))\n\n\t\treturn revStr, nil\n\t}\n\n\trevision = revs[len(revs)-offset].Hash\n\tdebug.Log(\"Found %s for offset %d\", revision, offset)\n\n\treturn revision, nil\n}\n\nfunc (s *Action) showHandleOutputChars(ctx context.Context, pw string, chars []int) error {\n\tfor _, c := range chars {\n\t\tif c > len(pw) || c-1 < 0 {\n\t\t\tdebug.Log(\"Invalid char: %d\", c)\n\n\t\t\tcontinue\n\t\t}\n\t\tout.Printf(ctx, \"%d: %s\", c, out.Secret(pw[c-1]))\n\t}\n\n\treturn nil\n}\n\n// showHandleOutput displays a secret.\nfunc (s *Action) showHandleOutput(ctx context.Context, name string, sec gopass.Secret) error {\n\tpw, body, err := s.showGetContent(ctx, sec)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif chars := GetPrintChars(ctx); len(chars) > 0 {\n\t\treturn s.showHandleOutputChars(ctx, pw, chars)\n\t}\n\n\tif pw == \"\" && body == \"\" {\n\t\tif config.Bool(ctx, \"show.safecontent\") && !ctxutil.IsForce(ctx) {\n\t\t\tout.Warning(ctx, \"show.safecontent=true. Use -f to display password, if any\")\n\t\t}\n\n\t\treturn exit.Error(exit.NotFound, store.ErrEmptySecret, \"%v\", store.ErrEmptySecret)\n\t}\n\n\tif IsPrintQR(ctx) && IsQRBody(ctx) {\n\t\tif err := s.showPrintQR(name, body); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\tif IsPrintQR(ctx) && pw != \"\" {\n\t\tif err := s.showPrintQR(name, pw); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif (IsClip(ctx) || IsAlsoClip(ctx)) && pw != \"\" {\n\t\tif err := clipboard.CopyTo(ctx, name, []byte(pw), config.AsInt(s.cfg.Get(\"core.cliptimeout\"))); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif body == \"\" {\n\t\treturn nil\n\t}\n\n\t// do not output when the clip flag is set\n\tif IsOnlyClip(ctx) {\n\t\treturn nil\n\t}\n\n\tctx = out.WithNewline(ctx, ctxutil.IsTerminal(ctx))\n\tif ctxutil.IsTerminal(ctx) && !IsPasswordOnly(ctx) {\n\t\theader := fmt.Sprintf(\"Secret: %s\\n\", name)\n\t\tif HasKey(ctx) {\n\t\t\theader += fmt.Sprintf(\"Key: %s\\n\", GetKey(ctx))\n\t\t}\n\t\tout.Print(ctx, header)\n\t}\n\n\t// output the actual secret, newlines are handled by ctx and Print.\n\tout.Print(ctx, out.Secret(body))\n\n\treturn nil\n}\n\nfunc (s *Action) showGetContent(ctx context.Context, sec gopass.Secret) (string, string, error) {\n\t// YAML key.\n\tif HasKey(ctx) {\n\t\tkey := GetKey(ctx)\n\t\tvalues, found := sec.Values(key)\n\t\tif !found {\n\t\t\treturn \"\", \"\", exit.Error(exit.NotFound, store.ErrNoKey, \"%v\", store.ErrNoKey)\n\t\t}\n\t\tval := strings.Join(values, \"\\n\")\n\n\t\treturn val, val, nil\n\t}\n\n\tpw := sec.Password()\n\t// fallback for old MIME secrets.\n\tfullBody := strings.TrimPrefix(string(sec.Bytes()), secrets.Ident+\"\\n\")\n\n\tif IsQRBody(ctx) {\n\t\treturn pw, fullBody, nil\n\t}\n\t// first line of the secret only.\n\tif IsPrintQR(ctx) || IsOnlyClip(ctx) {\n\t\treturn pw, \"\", nil\n\t}\n\tif IsPasswordOnly(ctx) {\n\t\tif pw == \"\" && fullBody != \"\" {\n\t\t\treturn \"\", \"\", exit.Error(exit.NotFound, store.ErrNoPassword, \"%v\", store.ErrNoPassword)\n\t\t}\n\n\t\treturn pw, pw, nil\n\t}\n\n\t// everything but the first line.\n\tif config.Bool(ctx, \"show.safecontent\") && !ctxutil.IsForce(ctx) && ctxutil.IsShowParsing(ctx) {\n\t\tbody := showSafeContent(sec)\n\t\tif IsAlsoClip(ctx) {\n\t\t\treturn pw, body, nil\n\t\t}\n\n\t\treturn \"\", body, nil\n\t}\n\n\t// everything (default).\n\treturn pw, fullBody, nil\n}\n\nfunc showSafeContent(sec gopass.Secret) string {\n\tvar sb strings.Builder\n\tfor i, k := range sec.Keys() {\n\t\tsb.WriteString(k)\n\t\tsb.WriteString(\": \")\n\t\t// check if this key should be obstructed.\n\t\tif isUnsafeKey(k, sec) {\n\t\t\tdebug.V(1).Log(\"obstructing unsafe key %s\", k)\n\t\t\tsb.WriteString(randAsterisk())\n\t\t} else {\n\t\t\tv, found := sec.Values(k)\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsb.WriteString(strings.Join(v, \"\\n\"+k+\": \"))\n\t\t}\n\t\t// we only add a final new line if the body is non-empty.\n\t\tif sec.Body() != \"\" || i < len(sec.Keys())-1 {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tfor l := range strings.SplitSeq(sec.Body(), \"\\n\") {\n\t\tif strings.HasPrefix(l, \"otpauth://\") {\n\t\t\tsb.WriteString(fmt.Sprintf(\"\\notpauth://%s\", randAsterisk()))\n\n\t\t\tcontinue\n\t\t}\n\t\tsb.WriteString(l)\n\t}\n\n\treturn sb.String()\n}\n\nfunc isUnsafeKey(key string, sec gopass.Secret) bool {\n\tduks := []string{\"hotp\", \"otpauth\", \"password\", \"totp\"}\n\tif slices.Contains(duks, key) {\n\t\treturn true\n\t}\n\n\tuks, found := sec.Get(\"unsafe-keys\")\n\tif !found || uks == \"\" {\n\t\treturn false\n\t}\n\n\tfor uk := range strings.SplitSeq(uks, \",\") {\n\t\tuk = strings.TrimSpace(uk)\n\t\tif uk == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.EqualFold(uk, key) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc randAsterisk() string {\n\t// we could also have a random number of asterisks but testing becomes painful.\n\treturn strings.Repeat(\"*\", 5)\n}\n\n// hasAliasDomain will try to find a possible alias mapping for the secret\n// name given. Given a name like `websites/foo.com/username` it will deconstruct\n// this name from the end (i.e. username -> foo.com -> websites) and check\n// each of these against the built-in and custom alias tables. If an alias\n// if found (e.g. foo.de -> foo.com) this element will be replaced and an lookup\n// is attempted (e.g. `websites/foo.de/username`).\nfunc (s *Action) hasAliasDomain(ctx context.Context, name string) string {\n\tp := strings.Split(name, \"/\")\n\tfor i := len(p) - 1; i > 0; i-- {\n\t\td := p[i]\n\t\tfor _, alias := range pwrules.LookupAliases(ctx, d) {\n\t\t\tsn := append(p[0:i], alias)\n\t\t\tsn = append(sn, p[i+1:]...)\n\t\t\taliasName := strings.Join(sn, \"/\")\n\t\t\tif s.Store.Exists(ctx, aliasName) {\n\t\t\t\treturn aliasName\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// showHandleError handles errors retrieving secrets.\nfunc (s *Action) showHandleError(ctx context.Context, c *cli.Context, name string, recurse bool, err error) error {\n\tif !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) {\n\t\tif IsClip(ctx) {\n\t\t\t_ = notify.Notify(ctx, \"gopass - error\", fmt.Sprintf(\"failed to retrieve secret %q: %s\", name, err))\n\t\t}\n\n\t\treturn exit.Error(exit.Unknown, err, \"failed to retrieve secret %q: %s\", name, err)\n\t}\n\n\tif newName := s.hasAliasDomain(ctx, name); newName != \"\" {\n\t\treturn s.show(ctx, nil, newName, false)\n\t}\n\n\tif IsClip(ctx) {\n\t\t_ = notify.Notify(ctx, \"gopass - warning\", fmt.Sprintf(\"Entry %q not found. Starting search...\", name))\n\t}\n\n\tout.Warningf(ctx, \"Entry %q not found. Starting search...\", name)\n\tc.Context = ctx\n\tif err := s.FindFuzzy(c); err != nil {\n\t\tif IsClip(ctx) {\n\t\t\t_ = notify.Notify(ctx, \"gopass - error\", err.Error())\n\t\t}\n\n\t\treturn exit.Error(exit.NotFound, err, \"%s\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) showPrintQR(name, pw string) error {\n\tqr, err := qrcon.QRCode(pw)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to encode %q as QR: %s\", name, err)\n\t}\n\tfmt.Fprintln(stdout, qr)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/show_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestShowMulti(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// first add another entry in a subdir\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"123\")\n\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\trequire.NoError(t, act.Store.Set(ctx, \"bar/baz\", sec))\n\tbuf.Reset()\n\n\tt.Run(\"show foo\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"secret\")\n\t})\n\n\tt.Run(\"show --sync foo\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"sync\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"secret\")\n\t})\n\n\tt.Run(\"show dir\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Equal(t, \"bar/\\n└── baz\\n\\n\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"true\"))\n\n\tt.Run(\"show twoliner with safecontent enabled\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar/baz\")\n\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"bar: zab\")\n\t\tassert.NotContains(t, buf.String(), \"password: ***\")\n\t\tassert.NotContains(t, buf.String(), \"123\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show foo with safecontent enabled, should error out\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, buf.String(), \"secret\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show foo with safecontent enabled, with the force flag\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"unsafe\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"secret\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show twoliner with safecontent enabled, but with the clip flag, which should copy just the secret\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"clip\": \"true\"}, \"bar/baz\")\n\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, buf.String(), \"123\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show entry with unsafe keys\", func(t *testing.T) {\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"123\")\n\t\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\t\trequire.NoError(t, sec.Set(\"foo\", \"baz\"))\n\t\trequire.NoError(t, sec.Set(\"hello\", \"world\"))\n\t\trequire.NoError(t, sec.Set(\"unsafe-keys\", \"foo, bar\"))\n\t\trequire.NoError(t, act.Store.Set(ctx, \"unsafe/keys\", sec))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"unsafe/keys\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"*****\")\n\t\tassert.NotContains(t, buf.String(), \"zab\")\n\t\tassert.NotContains(t, buf.String(), \"baz\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"copy a key with the clip flag without showing any output\", func(t *testing.T) {\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"123\")\n\t\trequire.NoError(t, sec.Set(\"bar\", \"zab\"))\n\t\trequire.NoError(t, act.Store.Set(ctx, \"clipped/keys\", sec))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"clip\": \"true\"}, \"clipped/keys\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, buf.String(), \"bar\")\n\t\tassert.NotContains(t, buf.String(), \"zab\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show entry with otpauth field with safecontent enabled\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"otpauth\", []byte(\"123\\n---\\notpauth://totp/WEBSITE:@USER?secret=SECRET&issuer=GoPass\"), false))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"otpauth\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"otpauth://*****\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show entry with otp as keys field with safecontent enabled\", func(t *testing.T) {\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"123\")\n\t\trequire.NoError(t, sec.Set(\"otpauth\", \"otpauth://totp/WEBSITE:@USER?secret=SECRET&issuer=GoPass\"))\n\t\trequire.NoError(t, sec.Set(\"totp\", \"otpauth://totp/WEBSITE:@USER?secret=SECRET&issuer=GoPass\"))\n\t\trequire.NoError(t, sec.Set(\"hotp\", \"otpauth://totp/WEBSITE:@USER?secret=SECRET&issuer=GoPass\"))\n\t\trequire.NoError(t, act.Store.Set(ctx, \"otpauthKeys\", sec))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"otpauthKeys\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"otpauth: *****\")\n\t\tassert.Contains(t, buf.String(), \"hotp: *****\")\n\t\tassert.Contains(t, buf.String(), \"totp: *****\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show twoliner with safecontent enabled\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar/baz\")\n\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"bar: zab\")\n\t\tassert.NotContains(t, buf.String(), \"password: ***\")\n\t\tassert.NotContains(t, buf.String(), \"123\")\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show twoliner with safecontent enabled\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar/baz\")\n\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, buf.String(), \"bar: zab\")\n\t\t// password should not show up neither be obstructed\n\t\tassert.NotContains(t, buf.String(), \"123\")\n\t\tassert.NotContains(t, buf.String(), \"***\")\n\t\tbuf.Reset()\n\t})\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"false\"))\n\n\tt.Run(\"show key \", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar/baz\", \"bar\")\n\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Equal(t, \"zab\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show nonexisting key\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"bar/baz\", \"nonexisting\")\n\n\t\trequire.Error(t, act.Show(c))\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show keys with mixed case\", func(t *testing.T) {\n\t\trequire.NoError(t, act.insertStdin(ctx, \"baz2\", []byte(\"foobar\\nOther: meh\\nuser: name\\nbody text\"), false))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"baz2\", \"Other\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Equal(t, \"meh\", buf.String())\n\t\tbuf.Reset()\n\t})\n\n\tt.Run(\"show value with format strings\", func(t *testing.T) {\n\t\tpw := \"some-chars-are-odd-%s-%p-%q\"\n\n\t\trequire.NoError(t, act.insertStdin(ctx, \"printf\", []byte(pw), false))\n\t\tbuf.Reset()\n\n\t\tc := gptest.CliCtx(ctx, t, \"printf\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Equal(t, pw+\"\\n\", buf.String())\n\t\tassert.NotContains(t, buf.String(), \"MISSING\")\n\t\tbuf.Reset()\n\t})\n}\n\nfunc TestShowAutoClip(t *testing.T) {\n\t// make sure we consistently get the unsupported error message\n\tov := clipboard.ForceUnsupported\n\tdefer func() {\n\t\tclipboard.ForceUnsupported = ov\n\t}()\n\tclipboard.ForceUnsupported = true\n\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tstdoutBuf := &bytes.Buffer{}\n\tstderrBuf := &bytes.Buffer{}\n\tout.Stdout = stdoutBuf\n\tstdout = stdoutBuf\n\tout.Stderr = stderrBuf\n\tstderr = stderrBuf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t\tstderr = os.Stderr\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\t// terminal=false\n\tctx = ctxutil.WithTerminal(ctx, false)\n\n\t// gopass show foo\n\t// -> w/o terminal\n\t// -> Print password\n\t// for use in scripts\n\tt.Run(\"gopass show foo\", func(t *testing.T) {\n\t\t// terminal=false\n\t\tctx = ctxutil.WithTerminal(ctx, false)\n\t\t// initialize context with config values, also detects if we're running in a terminal\n\t\tctx = act.Store.WithStoreConfig(ctx)\n\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -c foo\n\t// -> Copy to clipboard\n\tt.Run(\"gopass show -c foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"clip\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.NotContains(t, stdoutBuf.String(), \"secret\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -C foo\n\t// -> Copy to clipboard AND print\n\tt.Run(\"gopass show -C foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"alsoclip\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"second\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -f foo\n\t// -> ONLY print\n\tt.Run(\"gopass show -f foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"unsafe\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"second\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show foo\n\t// -> Copy to clipboard\n\tt.Run(\"gopass show foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -c foo\n\t// -> Copy to clipboard and DO NOT print\n\tt.Run(\"gopass show -c foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"clip\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.NotContains(t, stdoutBuf.String(), \"secret\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -C foo\n\t// -> Copy to clipboard AND print\n\tt.Run(\"gopass show -C foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"alsoclip\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"second\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show -f foo\n\t// -> ONLY Print\n\tt.Run(\"gopass show -f foo\", func(t *testing.T) {\n\t\tc := gptest.CliCtxWithFlags(ctx, t, map[string]string{\"unsafe\": \"true\"}, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.NotContains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"secret\")\n\t\tassert.Contains(t, stdoutBuf.String(), \"second\")\n\t\tstdoutBuf.Reset()\n\t\tstderrBuf.Reset()\n\t})\n\n\t// gopass show foo with show.autoclip and show.safecontent true\n\t// -> ONLY Copy to clipboard\n\tt.Run(\"show foo with safecontent and autoclip enabled\", func(t *testing.T) {\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"show.autoclip\", \"true\"))\n\t\trequire.NoError(t, act.cfg.Set(\"\", \"show.safecontent\", \"true\"))\n\t\tc := gptest.CliCtx(ctx, t, \"foo\")\n\t\trequire.NoError(t, act.Show(c))\n\t\tassert.Contains(t, stderrBuf.String(), \"WARNING\")\n\t\tassert.NotContains(t, stdoutBuf.String(), \"secret\")\n\t\tstdoutBuf.Reset()\n\t})\n}\n\nfunc TestShowHandleRevision(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"show foo\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tc := gptest.CliCtx(ctx, t)\n\t\trequire.NoError(t, act.showHandleRevision(ctx, c, \"foo\", \"HEAD\"))\n\t})\n}\n\nfunc TestShowHandleError(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// show foo\n\tc := gptest.CliCtx(ctx, t)\n\trequire.Error(t, act.showHandleError(ctx, c, \"foo\", false, fmt.Errorf(\"test\")))\n\tbuf.Reset()\n}\n\nfunc TestShowPrintQR(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx) //nolint:ineffassign,staticcheck\n\n\tcolor.NoColor = true\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\trequire.NoError(t, act.showPrintQR(\"foo\", \"bar\"))\n\tbuf.Reset()\n}\n\nfunc TestShowHasAliasDomain(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"foo\")\n\trequire.NoError(t, act.Store.Set(ctx, \"websites/foo.de/user\", sec))\n\n\trequire.NoError(t, act.cfg.Set(\"\", \"domain-alias.foo.de.insteadof\", \"foo.com\"))\n\n\talias := act.hasAliasDomain(ctx, \"websites/foo.com/user\")\n\tassert.Equal(t, \"websites/foo.de/user\", alias)\n}\n"
  },
  {
    "path": "internal/action/sync.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/diff\"\n\t\"github.com/gopasspw/gopass/internal/notify\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n\t\"github.com/xhit/go-str2duration/v2\"\n)\n\nvar (\n\tautosyncInterval = time.Duration(3*24) * time.Hour\n\tautosyncLastRun  time.Time\n)\n\nfunc init() {\n\tsv := os.Getenv(\"GOPASS_AUTOSYNC_INTERVAL\")\n\tif sv == \"\" {\n\t\treturn\n\t}\n\n\tdebug.Log(\"GOPASS_AUTOSYNC_INTERVAL is deprecated. Please use autosync.interval\")\n\n\tiv, err := strconv.Atoi(sv)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tautosyncInterval = time.Duration(iv*24) * time.Hour\n}\n\n// Sync all stores with their remotes.\nfunc (s *Action) Sync(c *cli.Context) error {\n\treturn s.sync(ctxutil.WithGlobalFlags(c), c.String(\"store\"), false)\n}\n\nfunc (s *Action) autoSync(ctx context.Context) error {\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn nil\n\t}\n\n\tif !ctxutil.IsTerminal(ctx) {\n\t\treturn nil\n\t}\n\n\tif sv := os.Getenv(\"GOPASS_NO_AUTOSYNC\"); sv != \"\" {\n\t\tout.Warning(ctx, \"GOPASS_NO_AUTOSYNC is deprecated. Please set core.autosync = false.\")\n\n\t\treturn nil\n\t}\n\n\tif !config.Bool(ctx, \"core.autosync\") {\n\t\treturn nil\n\t}\n\n\tls := s.rem.LastSeen(\"autosync\")\n\tdebug.Log(\"autosync - last seen: %s\", ls)\n\tsyncInterval := autosyncInterval\n\n\tif intervalStr := s.cfg.Get(\"autosync.interval\"); intervalStr != \"\" {\n\t\tif _, err := strconv.Atoi(intervalStr); err == nil {\n\t\t\tintervalStr += \"d\"\n\t\t}\n\t\tif duration, err := str2duration.ParseDuration(intervalStr); err != nil {\n\t\t\tout.Warningf(ctx, \"failed to parse autosync.interval %q: %q\", intervalStr, err)\n\t\t} else {\n\t\t\tsyncInterval = duration\n\t\t}\n\t}\n\tdebug.Log(\"autosync - interval: %s\", syncInterval)\n\n\tif time.Since(ls) > syncInterval {\n\t\terr := s.sync(ctx, \"\", true)\n\t\tif err != nil {\n\t\t\tautosyncLastRun = time.Now()\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) sync(ctx context.Context, store string, isAutosync bool) error {\n\t// we just did a full sync, no need to run it again\n\tif time.Since(autosyncLastRun) < 10*time.Second {\n\t\tdebug.Log(\"skipping sync. last sync %ds ago\", time.Since(autosyncLastRun))\n\n\t\treturn nil\n\t}\n\n\t// check if user asked for single store/remote sync or all remote sync\n\tif store == \"\" {\n\t\tout.Printf(ctx, \"🚥 Syncing with all remotes ...\")\n\t}\n\n\tnumEntries := 0\n\tif l, err := s.Store.Tree(ctx); err == nil {\n\t\tnumEntries = len(l.List(tree.INF))\n\t}\n\tnumMPs := 0\n\n\tmps := s.Store.MountPoints()\n\tmps = append([]string{\"\"}, mps...)\n\n\t// sync all stores (root and all mounted sub stores).\n\tfor _, mp := range mps {\n\t\tif store != \"\" {\n\t\t\tout.Printf(ctx, \"🚥 Syncing with store/remote %q...\", store)\n\t\t\tif store != \"<root>\" && mp != store {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif store == \"<root>\" && mp != \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tnumMPs++\n\t\t_ = s.syncMount(ctx, mp, isAutosync)\n\t}\n\n\tif numMPs > 0 {\n\t\tout.OKf(ctx, \"All done\")\n\t} else {\n\t\tout.Printf(ctx, \"⚠️ No remotes were found\")\n\t}\n\n\t// If we just sync'ed all stores we can reset the auto-sync interval\n\tif store == \"\" {\n\t\t_ = s.rem.Reset(\"autosync\")\n\t}\n\n\t// Calculate number of changed entries.\n\t// This is a rough estimate as additions and deletions.\n\t// might cancel each other out.\n\tif l, err := s.Store.Tree(ctx); err == nil {\n\t\tnumEntries = len(l.List(tree.INF)) - numEntries\n\t}\n\tdiff := \"\"\n\tif numEntries > 0 {\n\t\tdiff = fmt.Sprintf(\" Added %d entries\", numEntries)\n\t} else if numEntries < 0 {\n\t\tdiff = fmt.Sprintf(\" Removed %d entries\", -1*numEntries)\n\t}\n\n\tif numEntries != 0 {\n\t\tctx = config.WithMount(ctx, store)\n\t\t_ = notify.Notify(ctx, \"gopass - sync\", fmt.Sprintf(\"Finished. Synced %d remotes.%s\", numMPs, diff))\n\t}\n\n\treturn nil\n}\n\n// syncMount syncs a single mount.\nfunc (s *Action) syncMount(ctx context.Context, mp string, isAutosync bool) error {\n\tif isAutosync {\n\t\t// using GetM here to get the value for this mount, it might be different\n\t\t// from the global value\n\t\tif as := s.cfg.GetM(mp, \"core.autosync\"); as == \"false\" {\n\t\t\tdebug.Log(\"not syncing %s, autosync is disabled for this mount\", mp)\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tctxno := out.WithNewline(ctx, false)\n\tname := mp\n\tif mp == \"\" {\n\t\tname = \"<root>\"\n\t}\n\tout.Printf(ctxno, color.GreenString(\"[%s] \", name))\n\n\tsub, err := s.Store.GetSubStore(mp)\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Failed to get sub store %q: %s\", name, err)\n\n\t\treturn fmt.Errorf(\"failed to get sub stores (%w)\", err)\n\t}\n\n\tif sub == nil {\n\t\tout.Errorf(ctx, \"Failed to get sub stores '%s: nil'\", name)\n\n\t\treturn fmt.Errorf(\"failed to get sub stores (nil)\")\n\t}\n\n\tl, err := sub.List(ctx, \"\")\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Failed to list store: %s\", err)\n\t}\n\n\tout.Printf(ctxno, \"\\n   \"+color.GreenString(\"%s pull and push ... \", sub.Storage().Name()))\n\n\tswitch err := sub.Storage().Push(ctx, \"\", \"\"); {\n\tcase err == nil:\n\t\tdebug.Log(\"Push succeeded\")\n\t\tout.Printf(ctxno, color.GreenString(\"OK\"))\n\tcase errors.Is(err, store.ErrGitNoRemote):\n\t\tout.Printf(ctx, \"Skipped (no remote)\")\n\t\tdebug.Log(\"Failed to push %q to its remote: %s\", name, err)\n\n\t\treturn err\n\tcase errors.Is(err, backend.ErrNotSupported):\n\t\tout.Printf(ctxno, \"Skipped (not supported)\")\n\tcase errors.Is(err, store.ErrGitNotInit):\n\t\tout.Printf(ctxno, \"Skipped (no Git repo)\")\n\tdefault: // any other error\n\t\tout.Errorf(ctx, \"Failed to push %q to its remote: %s\", name, err)\n\n\t\treturn err\n\t}\n\n\tln, err := sub.List(ctx, \"\")\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Failed to list store: %s\", err)\n\t}\n\tsyncPrintDiff(ctxno, l, ln)\n\n\texportKeys := config.AsBool(s.cfg.GetM(mp, \"core.exportkeys\"))\n\tdebug.Log(\"Syncing Mount %s. Exportkeys: %t\", mp, exportKeys)\n\tif err := syncImportKeys(ctxno, sub, name); err != nil {\n\t\treturn err\n\t}\n\tif exportKeys {\n\t\tif err := syncExportKeys(ctxno, sub, name); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tout.Printf(ctx, \"\\n   \"+color.GreenString(\"done\"))\n\n\treturn nil\n}\n\nfunc syncImportKeys(ctx context.Context, sub *leaf.Store, name string) error {\n\t// import keys.\n\tif err := sub.ImportMissingPublicKeys(ctx); err != nil {\n\t\tout.Errorf(ctx, \"Failed to import missing public keys for %q: %s\", name, err)\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc syncExportKeys(ctx context.Context, sub *leaf.Store, name string) error {\n\t// export keys.\n\texported, err := sub.UpdateExportedPublicKeys(ctx)\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Failed to export missing public keys for %q: %s\", name, err)\n\n\t\treturn err\n\t}\n\n\t// only run second push if we did export any keys.\n\tif !exported {\n\t\treturn nil\n\t}\n\n\tif err := sub.Storage().Push(ctx, \"\", \"\"); err != nil {\n\t\tout.Errorf(ctx, \"Failed to push %q to its remote: %s\", name, err)\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc syncPrintDiff(ctxno context.Context, l, r []string) {\n\tadded, removed := diff.Stat(l, r)\n\tdebug.Log(\"diff - added: %d - removed: %d\", added, removed)\n\tif added > 0 {\n\t\tout.Printf(ctxno, color.GreenString(\" (Added %d entries)\", added))\n\t}\n\tif removed > 0 {\n\t\tout.Printf(ctxno, color.GreenString(\" (Removed %d entries)\", removed))\n\t}\n\tif added < 1 && removed < 1 {\n\t\tout.Printf(ctxno, color.GreenString(\" (no changes)\"))\n\t}\n}\n"
  },
  {
    "path": "internal/action/sync_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSync(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Sync(gptest.CliCtx(ctx, t)))\n\t})\n\n\tt.Run(\"sync --store=root\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Sync(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"store\": \"root\"})))\n\t})\n}\n"
  },
  {
    "path": "internal/action/templates.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tpl\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst (\n\ttemplateExample = `{{ .Content }}\n\n# This is an example of the available template operations\n# Predefined variables:\n# - .Content: The secret payload, usually a generated password\n# - .Name: The name of this secret\n# - .Path: The path to this secret\n# - .Dir: The dir of this secret\n#\n# Available Template functions:\n# - md5sum: e.g. {{ .Content | md5sum }}\n# - sha1sum: e.g. {{ .Content | sha1sum }}\n# - blake3 e.g. {{ .Content | blake3 }}\n# - md5crypt: e.g. {{ .Content | md5crypt }}\n# - ssha: e.g. {{ .Content | ssha }}\n# - ssha256: e.g. {{ .Content | ssha256 }}\n# - ssha512: e.g. {{ .Content | ssha512 }}\n# - get \"key\": e.g. {{ get \"path/to/some/other/secret\" | md5sum }}\n`\n)\n\n// TemplatesPrint will pretty-print a tree of templates.\nfunc (s *Action) TemplatesPrint(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tt, err := s.Store.TemplateTree(ctx)\n\tif err != nil {\n\t\treturn exit.Error(exit.List, err, \"failed to list templates: %s\", err)\n\t}\n\tfmt.Fprintln(stdout, t.Format(tree.INF))\n\n\treturn nil\n}\n\n// TemplatePrint will lookup and print a single template.\nfunc (s *Action) TemplatePrint(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\n\tcontent, err := s.Store.GetTemplate(ctx, name)\n\tif err != nil {\n\t\treturn exit.Error(exit.IO, err, \"failed to retrieve template: %s\", err)\n\t}\n\n\tfmt.Fprintln(stdout, string(content))\n\n\treturn nil\n}\n\n// TemplateEdit will load and existing or new template into an\n// editor.\nfunc (s *Action) TemplateEdit(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\n\tvar content []byte\n\tif s.Store.HasTemplate(ctx, name) {\n\t\tvar err error\n\t\tcontent, err = s.Store.GetTemplate(ctx, name)\n\t\tif err != nil {\n\t\t\treturn exit.Error(exit.IO, err, \"failed to retrieve template: %s\", err)\n\t\t}\n\t} else {\n\t\tcontent = []byte(templateExample)\n\t}\n\n\ted := editor.Path(c)\n\tnContent, err := editor.Invoke(ctx, ed, content)\n\tif err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke editor %s: %s\", ed, err)\n\t}\n\n\t// If content is equal, nothing changed, exiting.\n\tif bytes.Equal(content, nContent) {\n\t\treturn nil\n\t}\n\n\treturn s.Store.SetTemplate(ctx, name, nContent)\n}\n\n// TemplateRemove will remove a single template.\nfunc (s *Action) TemplateRemove(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tname := c.Args().First()\n\tif name == \"\" {\n\t\treturn exit.Error(exit.Usage, nil, \"usage: %s templates remove [name]\", s.Name)\n\t}\n\n\tif !s.Store.HasTemplate(ctx, name) {\n\t\treturn exit.Error(exit.NotFound, nil, \"template %q not found\", name)\n\t}\n\n\treturn s.Store.RemoveTemplate(ctx, name)\n}\n\nfunc (s *Action) templatesList(ctx context.Context) []string {\n\tt, err := s.Store.TemplateTree(ctx)\n\tif err != nil {\n\t\tdebug.Log(\"failed to list templates: %s\", err)\n\n\t\treturn nil\n\t}\n\n\treturn t.List(tree.INF)\n}\n\n// TemplatesComplete prints a list of all templates for bash completion.\nfunc (s *Action) TemplatesComplete(c *cli.Context) {\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tfor _, v := range s.templatesList(ctx) {\n\t\tfmt.Fprintln(stdout, v)\n\t}\n}\n\nfunc (s *Action) renderTemplate(ctx context.Context, name string, content []byte) ([]byte, bool) {\n\ttName, tmpl, found := s.Store.LookupTemplate(ctx, name)\n\tif !found {\n\t\tdebug.Log(\"No template found for %s\", name)\n\n\t\treturn content, false\n\t}\n\n\ttmplStr := strings.TrimSpace(string(tmpl))\n\tif tmplStr == \"\" {\n\t\tdebug.Log(\"Skipping empty template %q, for %s\", tName, name)\n\n\t\treturn content, false\n\t}\n\n\t// load template if it exists.\n\tnc, err := tpl.Execute(ctx, string(tmpl), name, content, s.Store)\n\tif err != nil {\n\t\tfmt.Fprintf(stdout, \"failed to execute template %q: %s\\n\", tName, err)\n\n\t\treturn content, false\n\t}\n\n\tout.Printf(ctx, \"Note: Using template %s\", tName)\n\n\treturn nc, true\n}\n"
  },
  {
    "path": "internal/action/templates_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTemplates(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tcolor.NoColor = true\n\tdefer func() {\n\t\tstdout = os.Stdout\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"display empty template tree\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.TemplatesPrint(gptest.CliCtx(ctx, t, \"foo\")))\n\t\tassert.Equal(t, \"gopass\\n\\n\", buf.String())\n\t})\n\n\tt.Run(\"add template\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.Store.SetTemplate(ctx, \"foo\", []byte(\"foobar\")))\n\t\trequire.NoError(t, act.TemplatesPrint(gptest.CliCtx(ctx, t, \"foo\")))\n\t\twant := `gopass\n└── foo\n\n`\n\t\tassert.Contains(t, buf.String(), want)\n\t})\n\n\tt.Run(\"complete templates\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\tact.TemplatesComplete(gptest.CliCtx(ctx, t, \"foo\"))\n\t\tassert.Equal(t, \"foo\\n\", buf.String())\n\t})\n\n\tt.Run(\"print template\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.TemplatePrint(gptest.CliCtx(ctx, t, \"foo\")))\n\t\tassert.Equal(t, \"foobar\\n\", buf.String())\n\t})\n\n\tt.Run(\"edit template\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.Error(t, act.TemplateEdit(gptest.CliCtx(ctx, t, \"foo\")))\n\t})\n\n\tt.Run(\"remove template\", func(t *testing.T) {\n\t\tdefer buf.Reset()\n\t\trequire.NoError(t, act.TemplateRemove(gptest.CliCtx(ctx, t, \"foo\")))\n\t})\n}\n"
  },
  {
    "path": "internal/action/unclip.go",
    "content": "package action\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/clipboard\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Unclip tries to erase the content of the clipboard.\nfunc (s *Action) Unclip(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tforce := c.Bool(\"force\")\n\ttimeout := c.Int(\"timeout\")\n\tname := os.Getenv(\"GOPASS_UNCLIP_NAME\")\n\tchecksum := os.Getenv(\"GOPASS_UNCLIP_CHECKSUM\")\n\n\ttime.Sleep(time.Second * time.Duration(timeout))\n\n\tmp := s.Store.MountPoint(name)\n\tctx = config.WithMount(ctx, mp)\n\n\tif err := clipboard.Clear(ctx, name, checksum, force); err != nil {\n\t\treturn exit.Error(exit.IO, err, \"Failed to clear clipboard: %s\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/unclip_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnclip(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, act)\n\tctx = act.cfg.WithConfig(ctx)\n\n\tt.Run(\"unlcip should fail\", func(t *testing.T) {\n\t\trequire.Error(t, act.Unclip(gptest.CliCtxWithFlags(ctx, t, map[string]string{\"timeout\": \"0\"})))\n\t})\n}\n"
  },
  {
    "path": "internal/action/update.go",
    "content": "package action\n\nimport (\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/updater\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Update will start the interactive update assistant.\nfunc (s *Action) Update(c *cli.Context) error {\n\t_ = s.rem.Reset(\"update\")\n\n\tctx := ctxutil.WithGlobalFlags(c)\n\n\tif s.version.String() == \"0.0.0+HEAD\" {\n\t\tout.Errorf(ctx, \"Can not check version against HEAD\")\n\n\t\treturn nil\n\t}\n\n\tout.Printf(ctx, \"⚒ Checking for available updates ...\")\n\tif err := updater.Update(ctx, s.version); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"Failed to update gopass: %s\", err)\n\t}\n\n\tout.OKf(ctx, \"gopass is up to date\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/action/update_test.go",
    "content": "package action\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"runtime\"\n\t\"testing\"\n\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/updater\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testUpdateJSON = `{\n    \"id\": 8979833,\n    \"name\": \"1.6.6 / 2017-12-20\",\n    \"tag_name\": \"v1.6.6\",\n    \"draft\": false,\n    \"prerelease\": false,\n    \"published_at\": \"2017-12-20T14:38:21Z\",\n    \"assets\": [\n      {\n       \"browser_download_url\": \"%s/gopass.tar.gz\",\n       \"id\": 5676623,\n       \"name\": \"gopass-1.6.6-%s-%s.tar.gz\"\n      },\n      {\n       \"browser_download_url\": \"%s/SHA256SUMS\",\n       \"id\": 5676624,\n       \"name\": \"gopass-1.6.6_SHA256SUMS\"\n      },\n      {\n       \"browser_download_url\": \"%s/SHA256SUMS.sig\",\n       \"id\": 5676625,\n       \"name\": \"gopass-1.6.6_SHA256SUMS.sig\"\n      }\n    ]\n  }`\n\nfunc TestUpdate(t *testing.T) {\n\tupdater.UpdateMoveAfterQuit = false\n\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\n\t// github release download mock\n\tghdl := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tgzw := gzip.NewWriter(w)\n\t\tdefer func() {\n\t\t\t_ = gzw.Close()\n\t\t}()\n\n\t\ttw := tar.NewWriter(gzw)\n\t\tdefer func() {\n\t\t\t_ = tw.Close()\n\t\t}()\n\n\t\tbody := \"foobar\"\n\t\thdr := &tar.Header{\n\t\t\tTypeflag: tar.TypeReg,\n\t\t\tName:     \"gopass\",\n\t\t\tMode:     0o600,\n\t\t\tSize:     int64(len(body)),\n\t\t}\n\t\tif err := tw.WriteHeader(hdr); err != nil {\n\t\t\thttp.Error(w, \"Internal Server Error\", http.StatusInternalServerError)\n\n\t\t\treturn\n\t\t}\n\t\tif _, err := tw.Write([]byte(body)); err != nil {\n\t\t\thttp.Error(w, \"Internal Server Error\", http.StatusInternalServerError)\n\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer ghdl.Close()\n\n\t// github api mock\n\tghapi := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tjson := fmt.Sprintf(testUpdateJSON, ghdl.URL, runtime.GOOS, runtime.GOARCH, ghdl.URL, ghdl.URL)\n\t\tfmt.Fprint(w, json)\n\t}))\n\tdefer ghapi.Close()\n\n\tupdater.BaseURL = ghapi.URL + \"/%s/%s\"\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\t// This should not fail, but then we need to provide valid signatures\n\trequire.Error(t, act.Update(gptest.CliCtx(ctx, t)))\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/action/version.go",
    "content": "package action\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/updater\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/protect\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Version prints the gopass version.\nfunc (s *Action) Version(c *cli.Context) error {\n\tctx := ctxutil.WithGlobalFlags(c)\n\tversion := make(chan string, 1)\n\tgo s.checkVersion(ctx, version)\n\n\tcli.VersionPrinter(c)\n\n\tselect {\n\tcase vi := <-version:\n\t\tif vi != \"\" {\n\t\t\tfmt.Fprintln(stdout, vi)\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tout.Errorf(ctx, \"Version check timed out\")\n\tcase <-ctx.Done():\n\t\treturn exit.Error(exit.Aborted, nil, \"user aborted\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Action) checkVersion(ctx context.Context, u chan string) {\n\tmsg := \"\"\n\tdefer func() {\n\t\tu <- msg\n\t}()\n\n\tif disabled := os.Getenv(\"CHECKPOINT_DISABLE\"); disabled != \"\" {\n\t\tdebug.Log(\"remote version check disabled by CHECKPOINT_DISABLE\")\n\n\t\treturn\n\t}\n\n\t// NB: \"updater.check\" isn't supported as a local config option, hence no mount point here\n\tif cfg, _ := config.FromContext(ctx); cfg.IsSet(\"updater.check\") && !config.AsBool(cfg.Get(\"updater.check\")) {\n\t\tdebug.Log(\"remote version check disabled by updater.check = false\")\n\n\t\treturn\n\t}\n\n\t// force checking for updates, mainly for testing.\n\tforce := os.Getenv(\"GOPASS_FORCE_CHECK\") != \"\"\n\n\tif !force && strings.HasSuffix(s.version.String(), \"+HEAD\") {\n\t\t// chan not check version against HEAD.\n\t\tdebug.Log(\"remote version check disabled for dev version\")\n\n\t\treturn\n\t}\n\n\tif !force && protect.ProtectEnabled {\n\t\t// chan not check version\n\t\t// against pledge(2)'d OpenBSD.\n\t\tdebug.Log(\"remote version check disabled for pledge(2)'d version\")\n\n\t\treturn\n\t}\n\n\tr, err := updater.FetchLatestRelease(ctx)\n\tif err != nil {\n\t\tmsg = color.RedString(\"\\nError checking latest version: %s\", err)\n\n\t\treturn\n\t}\n\n\tif !r.Version.GT(s.version) {\n\t\t_ = s.rem.Reset(\"update\")\n\t\tdebug.Log(\"gopass is up-to-date (local: %q, GitHub: %q)\", s.version, r.Version)\n\n\t\treturn\n\t}\n\n\tnotice := fmt.Sprintf(\"\\nYour version (%s) of gopass is out of date!\\nThe latest version is %s.\\n\", s.version, r.Version.String())\n\tnotice += \"You can update by downloading from https://www.gopass.pw/#install\"\n\tif err := updater.IsUpdateable(ctx); err == nil {\n\t\tnotice += \" by running 'gopass update'\"\n\t}\n\tnotice += \" or via your package manager\"\n\tmsg = color.YellowString(notice)\n}\n"
  },
  {
    "path": "internal/action/version_test.go",
    "content": "package action\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestVersion(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, err := newMock(ctx, u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tstdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tstdout = os.Stdout\n\t}()\n\n\tcli.VersionPrinter = func(*cli.Context) {\n\t\tout.Printf(ctx, \"gopass version 0.0.0-test\")\n\t}\n\n\tt.Run(\"print fixed version\", func(t *testing.T) {\n\t\trequire.NoError(t, act.Version(gptest.CliCtx(ctx, t)))\n\t})\n}\n"
  },
  {
    "path": "internal/audit/audit.go",
    "content": "// Package audit contains the password-strength auditing implementation. It reads all decrypted\n// passwords and applies different heuristics and external password strength checks to determine\n// the quality of the password (i.e. the first line of the secret - only!).\npackage audit\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"maps\"\n\t\"path\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass-hibp/pkg/hibp/api\"\n\t\"github.com/gopasspw/gopass-hibp/pkg/hibp/dump\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/hashsum\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/muesli/crunchy\"\n)\n\ntype secretGetter interface {\n\tGet(context.Context, string) (gopass.Secret, error)\n\tListRevisions(context.Context, string) ([]backend.Revision, error)\n\tConcurrency() int\n}\n\ntype validator struct {\n\tName        string\n\tDescription string\n\tValidate    func(string, gopass.Secret) error\n}\n\n// DefaultExpiration is the default expiration time for secrets.\nvar DefaultExpiration = time.Hour * 24 * 365\n\ntype Auditor struct {\n\ts   secretGetter\n\tr   *ReportBuilder\n\tpcb func()\n\tv   []validator\n}\n\nfunc New(ctx context.Context, s secretGetter) *Auditor {\n\ta := &Auditor{\n\t\ts:   s,\n\t\tr:   newReport(),\n\t\tpcb: func() {},\n\t}\n\n\tcv := crunchy.NewValidator()\n\ta.v = []validator{\n\t\t{\n\t\t\tName:        \"crunchy\",\n\t\t\tDescription: \"github.com/muesli/crunchy\",\n\t\t\tValidate: func(_ string, sec gopass.Secret) error {\n\t\t\t\treturn cv.Check(sec.Password())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:        \"equals-name\",\n\t\t\tDescription: \"Checks for passwords the match the secret name\",\n\t\t\tValidate: func(name string, sec gopass.Secret) error {\n\t\t\t\tif name == sec.Password() || path.Base(name) == sec.Password() {\n\t\t\t\t\treturn fmt.Errorf(\"password equals name\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n\n\tif config.Bool(ctx, \"audit.hibp-use-api\") {\n\t\ta.v = append(a.v, validator{\n\t\t\tName:        \"hibp\",\n\t\t\tDescription: \"Checks passwords against the HIBPv2 API. See https://haveibeenpwned.com/\",\n\t\t\tValidate: func(_ string, sec gopass.Secret) error {\n\t\t\t\tif sec.Password() == \"\" {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tnumFound, err := api.Lookup(hashsum.SHA1Hex(sec.Password()))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't check HIBPv2 API: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif numFound > 0 {\n\t\t\t\t\treturn fmt.Errorf(\"password contained in at least %d public data breaches (HIBP API)\", numFound)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\t}\n\n\treturn a\n}\n\n// Batch runs a password strength audit on multiple secrets. Expiration is in days.\nfunc (a *Auditor) Batch(ctx context.Context, secrets []string) (*Report, error) {\n\tout.Printf(ctx, \"Checking %d secrets. This may take some time ...\\n\", len(secrets))\n\n\ta.r = newReport()\n\tpending := make(chan string, 1024)\n\n\t// It would be nice to parallelize this operation and limit the maxJobs to\n\t// runtime.NumCPU(), but sadly this causes various problems with multiple\n\t// gnupg jobs running in parallel. See the entire discussion here:\n\t//\n\t// https://github.com/gopasspw/gopass/pull/245\n\t//\n\tmaxJobs := a.s.Concurrency()\n\tif maxVal := config.Int(ctx, \"audit.concurrency\"); maxVal > 0 {\n\t\tif maxJobs > maxVal {\n\t\t\tmaxJobs = maxVal\n\t\t}\n\t}\n\n\t// Spawn workers that run the auditing of all secrets concurrently.\n\tdebug.Log(\"launching %d audit workers\", maxJobs)\n\n\tdone := make(chan struct{}, maxJobs)\n\tfor range maxJobs {\n\t\tgo a.audit(ctx, pending, done)\n\t}\n\n\tgo func() {\n\t\tfor _, secret := range secrets {\n\t\t\tpending <- secret\n\t\t}\n\t\tclose(pending)\n\t}()\n\n\tbar := termio.NewProgressBar(int64(len(secrets)))\n\tbar.Hidden = ctxutil.IsHidden(ctx)\n\ta.pcb = func() {\n\t\tbar.Inc()\n\t}\n\n\tfor range maxJobs {\n\t\t<-done\n\t}\n\tbar.Done()\n\n\tif err := a.checkHIBP(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn a.r.Finalize(), nil\n}\n\nfunc (a *Auditor) audit(ctx context.Context, secrets <-chan string, done chan struct{}) {\n\tfor secret := range secrets {\n\t\t// check for context cancelation.\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tcontinue\n\t\tdefault:\n\t\t}\n\n\t\ta.auditSecret(ctx, secret)\n\t\ta.pcb()\n\t}\n\tdone <- struct{}{}\n}\n\nfunc (a *Auditor) auditSecret(ctx context.Context, secret string) {\n\tdebug.Log(\"Auditing %q\", secret)\n\n\t// handle old passwords\n\trevs, err := a.s.ListRevisions(ctx, secret)\n\tif err != nil {\n\t\ta.r.AddFinding(secret, \"error-revisions\", err.Error(), \"error\")\n\t}\n\tif len(revs) > 0 {\n\t\ta.r.SetAge(secret, time.Since(revs[0].Date))\n\t}\n\n\tsec, err := a.s.Get(ctx, secret)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to check %s: %s\", secret, err)\n\n\t\ta.r.AddFinding(secret, \"error-read\", err.Error(), \"error\")\n\t\tif sec != nil {\n\t\t\ta.r.AddPassword(secret, sec.Password())\n\t\t}\n\n\t\treturn\n\t}\n\n\t// do not check empty secrets.\n\tif sec.Password() == \"\" {\n\t\tdebug.Log(\"Skipping empty secret %s\", secret)\n\n\t\treturn\n\t}\n\n\t// add the password for the duplicate check\n\ta.r.AddPassword(secret, sec.Password())\n\n\t// pass the secret to all validators.\n\tvar wg sync.WaitGroup\n\tfor _, v := range a.v {\n\t\twg.Go(func() {\n\t\t\tif err := v.Validate(secret, sec); err != nil {\n\t\t\t\ta.r.AddFinding(secret, v.Name, err.Error(), \"warning\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ta.r.AddFinding(secret, v.Name, \"ok\", \"none\")\n\t\t})\n\t}\n\twg.Wait()\n}\n\nfunc (a *Auditor) checkHIBP(ctx context.Context) error {\n\tif config.Bool(ctx, \"audit.hibp-use-api\") {\n\t\t// no need to check the dumps if we already checked the API\n\t\treturn nil\n\t}\n\n\t// if the user has set up the path to an HIBP dump we can continue.\n\tfn := config.String(ctx, \"audit.hibp-dump-file\")\n\tif fn == \"\" || !fsutil.IsFile(fn) {\n\t\tdebug.Log(\"audit.hibp-dump-file not pointing to a valid dump file\")\n\n\t\treturn nil\n\t}\n\n\t// if creating the scanner fails the dump file is most likely invalid.\n\tscanner, err := dump.New(fn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tout.Notice(ctx, \"Starting HIBP check (slow) ...\")\n\n\t// look up all known sha1sums. The LookupBatch method will sort the\n\t// input so we don't need to.\n\tmatches := scanner.LookupBatch(ctx, slices.Collect(maps.Keys(a.r.sha1sums)))\n\tfor _, m := range matches {\n\t\t// map any match back to the secret(s).\n\t\tsecs, found := a.r.sha1sums[m]\n\t\tif !found {\n\t\t\t// should not happen\n\t\t\tcontinue\n\t\t}\n\n\t\t// add a breach warning to each of these secrets.\n\t\tfor _, sec := range secs.Elements() {\n\t\t\ta.r.AddFinding(sec, \"hibp\", \"Found in at least one public data breach (HIBP Dump)\", \"warning\")\n\t\t}\n\t}\n\n\tfor name, sr := range a.r.secrets {\n\t\tif sr.Findings == nil {\n\t\t\tsr.Findings = make(map[string]Finding, 1)\n\t\t}\n\t\tif _, found := sr.Findings[\"hibp\"]; !found {\n\t\t\tsr.Findings[\"hibp\"] = Finding{\n\t\t\t\tSeverity: \"none\",\n\t\t\t\tMessage:  \"ok\",\n\t\t\t}\n\t\t\ta.r.secrets[name] = sr\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/audit/audit_test.go",
    "content": "package audit\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype mockSecretGetter struct{}\n\nfunc (m *mockSecretGetter) Get(ctx context.Context, name string) (gopass.Secret, error) {\n\tsec := secrets.New()\n\tsec.SetPassword(\"password\")\n\n\treturn sec, nil\n}\n\nfunc (m *mockSecretGetter) ListRevisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\treturn []backend.Revision{\n\t\t{Date: time.Now().Add(-time.Hour * 24 * 365)},\n\t}, nil\n}\n\nfunc (m *mockSecretGetter) Concurrency() int {\n\treturn 1\n}\n\nfunc TestNewAuditor(t *testing.T) {\n\tctx := t.Context()\n\ts := &mockSecretGetter{}\n\ta := New(ctx, s)\n\n\tassert.NotNil(t, a)\n\tassert.Equal(t, s, a.s)\n\tassert.NotNil(t, a.r)\n\tassert.NotNil(t, a.v)\n}\n\nfunc TestBatch(t *testing.T) {\n\tctx := t.Context()\n\ts := &mockSecretGetter{}\n\ta := New(ctx, s)\n\n\tsecrets := []string{\"secret1\", \"secret2\"}\n\treport, err := a.Batch(ctx, secrets)\n\n\trequire.NoError(t, err)\n\tassert.NotNil(t, report)\n\tassert.Len(t, secrets, len(report.Secrets))\n}\n\nfunc TestAuditSecret(t *testing.T) {\n\tctx := t.Context()\n\ts := &mockSecretGetter{}\n\ta := New(ctx, s)\n\n\tsecret := \"secret1\"\n\ta.auditSecret(ctx, secret)\n\n\tassert.Contains(t, a.r.secrets, secret)\n}\n\nfunc TestCheckHIBP(t *testing.T) {\n\tctx := t.Context()\n\ts := &mockSecretGetter{}\n\ta := New(ctx, s)\n\n\terr := a.checkHIBP(ctx)\n\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/audit/excludes.go",
    "content": "package audit\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\ntype res []*regexp.Regexp\n\nfunc (r res) Matches(s string) bool {\n\tfor _, re := range r {\n\t\tif re.MatchString(s) {\n\t\t\tdebug.Log(\"Matched %s against %s\", s, re.String())\n\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// FilterExcludes filters the given list of secrets against the given exclude patterns (RE2 syntax).\nfunc FilterExcludes(excludes string, in []string) []string {\n\tdebug.Log(\"Filtering %d secrets against %d exclude patterns\", len(in), strings.Count(excludes, \"\\n\"))\n\n\tres := make(res, 0, 10)\n\tfor line := range strings.SplitSeq(excludes, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\t\tre, err := regexp.Compile(line)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to compile exclude pattern %q: %s\", line, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"Adding exclude pattern %q\", re.String())\n\t\tres = append(res, re)\n\t}\n\n\t// shortcut if we have no excludes\n\tif len(res) < 1 {\n\t\treturn in\n\t}\n\n\t// check all secrets against all excludes\n\tout := make([]string, 0, len(in))\n\tfor _, s := range in {\n\t\tif res.Matches(s) {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, s)\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "internal/audit/excludes_test.go",
    "content": "package audit\n\nimport (\n\t\"testing\"\n)\n\nfunc TestFilterExcludes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texcludes string\n\t\tin       []string\n\t\twant     []string\n\t}{\n\t\t{\n\t\t\tname:     \"no excludes\",\n\t\t\texcludes: \"\",\n\t\t\tin:       []string{\"secret1\", \"secret2\"},\n\t\t\twant:     []string{\"secret1\", \"secret2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude one secret\",\n\t\t\texcludes: \"secret1\",\n\t\t\tin:       []string{\"secret1\", \"secret2\"},\n\t\t\twant:     []string{\"secret2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude all secrets\",\n\t\t\texcludes: \"secret1\\nsecret2\",\n\t\t\tin:       []string{\"secret1\", \"secret2\"},\n\t\t\twant:     []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude with comment\",\n\t\t\texcludes: \"# this is a comment\\nsecret1\",\n\t\t\tin:       []string{\"secret1\", \"secret2\"},\n\t\t\twant:     []string{\"secret2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude with empty lines\",\n\t\t\texcludes: \"\\nsecret1\\n\\n\",\n\t\t\tin:       []string{\"secret1\", \"secret2\"},\n\t\t\twant:     []string{\"secret2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude with regex\",\n\t\t\texcludes: \"secret.*\",\n\t\t\tin:       []string{\"secret1\", \"secret2\", \"other\"},\n\t\t\twant:     []string{\"other\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := FilterExcludes(tt.excludes, tt.in)\n\t\t\tif len(got) != len(tt.want) {\n\t\t\t\tt.Errorf(\"FilterExcludes() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tfor i := range got {\n\t\t\t\tif got[i] != tt.want[i] {\n\t\t\t\t\tt.Errorf(\"FilterExcludes() = %v, want %v\", got, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/audit/output.go",
    "content": "package audit\n\nimport (\n\t\"context\"\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/tpl\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\nfunc (r *Report) PrintResults(ctx context.Context) error {\n\tif r == nil {\n\t\tout.Warning(ctx, \"Empty report\")\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"Printing results for %d secrets\", len(r.Secrets))\n\n\tvar failed bool\n\tfor _, name := range set.SortedKeys(r.Secrets) {\n\t\ts := r.Secrets[name]\n\t\tvar sb strings.Builder\n\t\tsb.WriteString(fmt.Sprintf(\"%s (age: %s) \", name, s.HumanizeAge()))\n\t\tif !s.HasFindings() {\n\t\t\tsb.WriteString(\"OK\")\n\t\t\tout.OK(ctx, sb.String())\n\n\t\t\tcontinue\n\t\t}\n\t\tsb.WriteString(\"Potentially weak. \")\n\t\tfor k, v := range s.Findings {\n\t\t\tif v.Severity == \"error\" || v.Severity == \"warning\" {\n\t\t\t\tfailed = true\n\t\t\t}\n\n\t\t\tswitch v.Severity {\n\t\t\tcase \"error\":\n\t\t\t\tfallthrough\n\t\t\tcase \"warning\":\n\t\t\t\tsb.WriteString(fmt.Sprintf(\"%s: %s. \", k, v.Message))\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tout.Warning(ctx, sb.String())\n\t}\n\n\tif failed {\n\t\treturn fmt.Errorf(\"weak password or duplicates detected\")\n\t}\n\n\treturn nil\n}\n\nfunc (r *Report) PrintSummary(ctx context.Context) error {\n\tif r == nil {\n\t\tout.Warning(ctx, \"Empty report\")\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"Printing summary for %d findings\", len(r.Findings))\n\n\tfor _, name := range set.SortedKeys(r.Findings) {\n\t\tf := r.Findings[name]\n\t\tif f.Len() < 1 {\n\t\t\tcontinue\n\t\t}\n\t\t// TODO add details about the analyzer, not just the name\n\t\tout.Printf(ctx, \"Analyzer %s found issues: \", name)\n\t\tfor _, v := range set.Sorted(f.Elements()) {\n\t\t\tout.Printf(ctx, \"- %s\", v)\n\t\t}\n\t}\n\n\tif len(r.Findings) > 0 {\n\t\treturn fmt.Errorf(\"weak password or duplicates detected\")\n\t}\n\n\treturn nil\n}\n\nfunc (r *Report) RenderCSV(w io.Writer) error {\n\tcw := csv.NewWriter(w)\n\n\tcs := set.New[string]()\n\tfor _, v := range r.Secrets {\n\t\tfor k := range v.Findings {\n\t\t\tcs.Add(k)\n\t\t}\n\t}\n\tcats := cs.Elements()\n\tsort.Strings(cats)\n\n\tfor _, name := range set.SortedKeys(r.Secrets) {\n\t\tsec := r.Secrets[name]\n\n\t\trec := make([]string, 0, len(cats)+2)\n\t\trec = append(rec, name)\n\t\trec = append(rec, sec.Age.String())\n\t\tfor _, cat := range cats {\n\t\t\tif f, found := sec.Findings[cat]; found {\n\t\t\t\trec = append(rec, f.Message)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trec = append(rec, \"ok\")\n\t\t}\n\n\t\tif err := cw.Write(rec); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tcw.Flush()\n\n\treturn cw.Error()\n}\n\nfunc (r *Report) RenderHTML(w io.Writer) error {\n\ttplStr := htmlTpl\n\n\tif r.Template != \"\" {\n\t\tif buf, err := os.ReadFile(r.Template); err == nil {\n\t\t\ttplStr = string(buf)\n\t\t} else {\n\t\t\tdebug.Log(\"failed to load custom template from %s: %s\", r.Template, err)\n\t\t}\n\t}\n\n\ttmpl, err := template.New(\"report\").Funcs(tpl.PublicFuncMap()).Parse(tplStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse template: %w\", err)\n\t}\n\n\tif err := tmpl.Execute(w, getHTMLPayload(r)); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute template: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc getHTMLPayload(r *Report) *htmlPayload {\n\th := &htmlPayload{\n\t\tToday:      time.Now().UTC(),\n\t\tNum:        len(r.Secrets),\n\t\tDuration:   r.Duration,\n\t\tCategories: make([]string, 0, 24),\n\t\tSecrets:    make(map[string]SecretReport, len(r.Secrets)),\n\t}\n\n\tcs := set.New[string]()\n\tfor _, v := range r.Secrets {\n\t\tfor k := range v.Findings {\n\t\t\tcs.Add(k)\n\t\t}\n\t}\n\th.Categories = cs.Elements()\n\tsort.Strings(h.Categories)\n\n\tfor k, v := range r.Secrets {\n\t\tsr := SecretReport{\n\t\t\tName:     v.Name,\n\t\t\tAge:      v.Age,\n\t\t\tFindings: make(map[string]Finding, len(v.Findings)),\n\t\t}\n\t\tfor _, cat := range h.Categories {\n\t\t\tif f, found := v.Findings[cat]; found {\n\t\t\t\tsr.Findings[cat] = f\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsr.Findings[cat] = Finding{\n\t\t\t\tSeverity: \"none\",\n\t\t\t\tMessage:  \"ok\",\n\t\t\t}\n\t\t}\n\t\th.Secrets[k] = sr\n\t}\n\n\treturn h\n}\n\ntype htmlPayload struct {\n\tToday      time.Time\n\tNum        int\n\tDuration   time.Duration\n\tCategories []string\n\tSecrets    map[string]SecretReport\n}\n\nvar htmlTpl = `<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>gopass audit report generated on {{ .Today | date }}</title>\n  <style>\n#findings {\n  font-family: Arial, Helvetica, sans-serif;\n  border-collapse: collapse;\n  width: 100%;\n}\n#findings td, #findings th {\n  border: 1px solid #ddd;\n  padding: 8px;\n}\n#findings tr:nth-child(even){\n  background-color: #f3f3f3;\n}\n#findings tr:hover {\n  background-color: #ddd;\n}\n#findings th {\n  padding-top: 12px;\n  padding-bottom: 12px;\n  text-align: left;\n  background-color: #03995D;\n  color: white;\n}\n  </style>\n</head>\n<body>\n\nAudited {{ .Num }} secrets in {{ .Duration | roundDuration }} on {{ .Today | date }}.<br />\n\n<table id=\"findings\">\n  <thead>\n  <th>Secret</th>\n{{ $cats := .Categories}}\n{{- range .Categories }}\n<th>{{ . }}</th>\n{{ end }}\n  </thead>\n{{- range .Secrets }}\n  <tr>\n    <td>{{ .Name }}</td>\n{{- range .Findings }}\n    <td class=\"{{ .Severity }}\">\n        <div title=\"{{ .Message }}\">{{ .Message | truncate 120 }}</div>\n    </td>\n{{- end }}\n  </tr>\n{{- end }}\n</table>\n</body>\n</html>\n`\n"
  },
  {
    "path": "internal/audit/output_test.go",
    "content": "package audit\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHTML(t *testing.T) {\n\tr := newReport()\n\n\tr.AddPassword(\"foo\", \"bar\")\n\tr.SetAge(\"foo\", time.Hour)\n\tr.AddFinding(\"foo\", \"duplicate\", \"found duplicates\", \"warning\")\n\tr.AddFinding(\"foo\", \"hibp-api\", \"found match on HIBP\", \"warning\")\n\n\tsr := r.Finalize()\n\tout := &bytes.Buffer{}\n\trequire.NoError(t, sr.RenderHTML(out))\n\ttoday := time.Now().UTC().Format(\"2006-01-02\")\n\tassert.Equal(t, fmt.Sprintf(`<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>gopass audit report generated on %s</title>\n  <style>\n#findings {\n  font-family: Arial, Helvetica, sans-serif;\n  border-collapse: collapse;\n  width: 100%%;\n}\n#findings td, #findings th {\n  border: 1px solid #ddd;\n  padding: 8px;\n}\n#findings tr:nth-child(even){\n  background-color: #f3f3f3;\n}\n#findings tr:hover {\n  background-color: #ddd;\n}\n#findings th {\n  padding-top: 12px;\n  padding-bottom: 12px;\n  text-align: left;\n  background-color: #03995D;\n  color: white;\n}\n  </style>\n</head>\n<body>\n\nAudited 1 secrets in 0s on %s.<br />\n\n<table id=\"findings\">\n  <thead>\n  <th>Secret</th>\n\n<th>duplicate</th>\n\n<th>hibp-api</th>\n\n  </thead>\n  <tr>\n    <td>foo</td>\n    <td class=\"warning\">\n        <div title=\"found duplicates\">found duplicates</div>\n    </td>\n    <td class=\"warning\">\n        <div title=\"found match on HIBP\">found match on HIBP</div>\n    </td>\n  </tr>\n</table>\n</body>\n</html>\n`, today, today), out.String())\n}\n"
  },
  {
    "path": "internal/audit/report.go",
    "content": "package audit\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/hashsum\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\ntype Finding struct {\n\tSeverity string\n\tMessage  string\n}\n\ntype SecretReport struct {\n\tName string\n\t// analyzer -> finding details\n\tFindings map[string]Finding\n\tAge      time.Duration\n}\n\nfunc (s *SecretReport) HasFindings() bool {\n\tfor _, f := range s.Findings {\n\t\tif f.Severity != \"none\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (s *SecretReport) HumanizeAge() string {\n\tif s.Age < 24*time.Hour {\n\t\treturn fmt.Sprintf(\"%d hours\", int(s.Age.Hours()))\n\t}\n\tdays := int(s.Age.Hours() / 24)\n\tif days < 30 {\n\t\treturn fmt.Sprintf(\"%d days\", days)\n\t}\n\tmonths := days / 30\n\tif months < 12 {\n\t\treturn fmt.Sprintf(\"%d months\", months)\n\t}\n\tyears := months / 12\n\n\treturn fmt.Sprintf(\"%d years\", years)\n}\n\ntype Report struct {\n\t// secret name -> report\n\tSecrets map[string]SecretReport\n\n\t// finding -> secrets\n\tFindings map[string]set.Set[string]\n\n\tTemplate string\n\tDuration time.Duration\n}\n\ntype ReportBuilder struct {\n\t// protects all below\n\tsync.Mutex\n\n\t// secret name -> report\n\tsecrets map[string]SecretReport\n\t// finding -> secrets\n\tfindings map[string]set.Set[string]\n\n\t// SHA512(password) -> secret names\n\tduplicates map[string]set.Set[string]\n\n\t// HIBP\n\t// SHA1(password) -> secret names\n\tsha1sums map[string]set.Set[string]\n\n\tt0 time.Time\n}\n\nfunc (r *ReportBuilder) AddPassword(name, pw string) {\n\tif name == \"\" || pw == \"\" {\n\t\treturn\n\t}\n\n\tr.Lock()\n\tdefer r.Unlock()\n\n\ts256 := hashsum.SHA256Hex(pw)\n\td := r.duplicates[s256]\n\td.Add(name)\n\tr.duplicates[s256] = d\n\n\ts1 := hashsum.SHA1Hex(pw)\n\ts := r.sha1sums[s1]\n\ts.Add(name)\n\tr.sha1sums[s1] = s\n}\n\nfunc (r *ReportBuilder) AddFinding(secret, finding, message, severity string) {\n\tif secret == \"\" || finding == \"\" || message == \"\" || severity == \"\" {\n\t\treturn\n\t}\n\n\tr.Lock()\n\tdefer r.Unlock()\n\n\t// record individual findings\n\ts := r.secrets[secret]\n\ts.Name = secret\n\tif s.Findings == nil {\n\t\ts.Findings = make(map[string]Finding, 4)\n\t}\n\tf := s.Findings[finding]\n\tf.Message = message\n\tf.Severity = severity\n\ts.Findings[finding] = f\n\tr.secrets[secret] = s\n\n\tdebug.Log(\"Secret %q has finding %q: %s with severity %s\", secret, finding, message, severity)\n\tif severity == \"none\" {\n\t\treturn\n\t}\n\n\t// record secrets per finding, for the summary\n\tss := r.findings[finding]\n\tss.Add(secret)\n\tr.findings[finding] = ss\n}\n\nfunc (r *ReportBuilder) SetAge(name string, age time.Duration) {\n\tif name == \"\" {\n\t\treturn\n\t}\n\n\tr.Lock()\n\tdefer r.Unlock()\n\n\ts := r.secrets[name]\n\ts.Name = name\n\ts.Age = age\n\tr.secrets[name] = s\n}\n\nfunc newReport() *ReportBuilder {\n\treturn &ReportBuilder{\n\t\tsecrets:    make(map[string]SecretReport, 512),\n\t\tfindings:   make(map[string]set.Set[string], 512),\n\t\tduplicates: make(map[string]set.Set[string], 512),\n\t\tsha1sums:   make(map[string]set.Set[string], 512),\n\t\tt0:         time.Now().UTC(),\n\t}\n}\n\n// Finalize computes the duplicates.\nfunc (r *ReportBuilder) Finalize() *Report {\n\tfor k, s := range r.secrets {\n\t\tfor _, secs := range r.duplicates {\n\t\t\tif secs.Len() < 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !secs.Contains(k) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif s.Findings == nil {\n\t\t\t\ts.Findings = make(map[string]Finding, 1)\n\t\t\t}\n\t\t\ts.Findings[\"duplicates\"] = Finding{\n\t\t\t\tSeverity: \"warning\",\n\t\t\t\tMessage:  fmt.Sprintf(\"Duplicates detected. Shared with: %+v\", secs.Difference(set.New(k))),\n\t\t\t}\n\t\t}\n\t\tr.secrets[k] = s\n\t}\n\n\tret := &Report{\n\t\tSecrets:  make(map[string]SecretReport, len(r.secrets)),\n\t\tFindings: make(map[string]set.Set[string], len(r.findings)),\n\t\tDuration: time.Since(r.t0),\n\t}\n\n\tfor k := range r.secrets {\n\t\tret.Secrets[k] = r.secrets[k]\n\t}\n\n\tfor k := range r.findings {\n\t\tret.Findings[k] = r.findings[k]\n\t}\n\n\tdebug.Log(\"Finalized report: %d secrets, %d findings\", len(ret.Secrets), len(ret.Findings))\n\n\treturn ret\n}\n"
  },
  {
    "path": "internal/audit/report_test.go",
    "content": "package audit\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFinalize(t *testing.T) {\n\tr := newReport()\n\tr.AddPassword(\"foo\", \"bar\")\n\tr.AddPassword(\"baz\", \"bar\")\n\tr.AddPassword(\"zab\", \"bar\")\n\tr.AddPassword(\"foo\", \"bar\")\n\tr.AddFinding(\"foo\", \"foo\", \"bar\", \"warning\")\n\tr.AddFinding(\"bar\", \"foo\", \"bar\", \"warning\")\n\n\tsr := r.Finalize()\n\tassert.NotNil(t, sr)\n}\n"
  },
  {
    "path": "internal/audit/single.go",
    "content": "package audit\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/muesli/crunchy\"\n)\n\n// Single runs a password strength audit on a single password.\nfunc Single(ctx context.Context, password string) {\n\tvalidator := crunchy.NewValidator()\n\tif err := validator.Check(password); err != nil {\n\t\tout.Printf(ctx, fmt.Sprintf(\"Warning: %s\", err))\n\t}\n}\n"
  },
  {
    "path": "internal/backend/context.go",
    "content": "package backend\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyCryptoBackend contextKey = iota\n\tctxKeyStorageBackend\n)\n\n// CryptoBackendName returns the name of the given backend.\nfunc CryptoBackendName(cb CryptoBackend) string {\n\tif name, err := CryptoRegistry.BackendName(cb); err == nil {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n\n// WithCryptoBackendString returns a context with the given crypto backend set.\nfunc WithCryptoBackendString(ctx context.Context, be string) (context.Context, error) {\n\tcb, err := CryptoRegistry.Backend(be)\n\tif err != nil {\n\t\treturn ctx, err\n\t}\n\n\treturn WithCryptoBackend(ctx, cb), nil\n}\n\n// WithCryptoBackend returns a context with the given crypto backend set.\nfunc WithCryptoBackend(ctx context.Context, be CryptoBackend) context.Context {\n\treturn context.WithValue(ctx, ctxKeyCryptoBackend, be)\n}\n\n// HasCryptoBackend returns true if a value for crypto backend has been set in the context.\nfunc HasCryptoBackend(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyCryptoBackend).(CryptoBackend)\n\n\treturn ok\n}\n\n// GetCryptoBackend returns the selected crypto backend or the default (GPGCLI).\nfunc GetCryptoBackend(ctx context.Context) CryptoBackend {\n\tbe, ok := ctx.Value(ctxKeyCryptoBackend).(CryptoBackend)\n\tif !ok {\n\t\treturn GPGCLI\n\t}\n\n\treturn be\n}\n\n// WithStorageBackendString returns a context with the given store backend set.\nfunc WithStorageBackendString(ctx context.Context, sb string) (context.Context, error) {\n\tbe, err := StorageRegistry.Backend(sb)\n\tif err != nil {\n\t\treturn WithStorageBackend(ctx, FS), err\n\t}\n\n\treturn WithStorageBackend(ctx, be), nil\n}\n\n// WithStorageBackend returns a context with the given store backend set.\nfunc WithStorageBackend(ctx context.Context, sb StorageBackend) context.Context {\n\treturn context.WithValue(ctx, ctxKeyStorageBackend, sb)\n}\n\n// GetStorageBackend returns the store backend or the default (FS).\nfunc GetStorageBackend(ctx context.Context) StorageBackend {\n\tbe, ok := ctx.Value(ctxKeyStorageBackend).(StorageBackend)\n\tif !ok {\n\t\treturn FS\n\t}\n\n\treturn be\n}\n\n// HasStorageBackend returns true if a value for store backend was set.\nfunc HasStorageBackend(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyStorageBackend).(StorageBackend)\n\n\treturn ok\n}\n\n// StorageBackendName returns the name of the given backend.\nfunc StorageBackendName(sb StorageBackend) string {\n\tif name, err := StorageRegistry.BackendName(sb); err == nil {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/backend/context_test.go",
    "content": "package backend\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCryptoBackend(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.Equal(t, GPGCLI, GetCryptoBackend(ctx))\n\tctx1, err := WithCryptoBackendString(ctx, \"gpgcli\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, GPGCLI, GetCryptoBackend(ctx1))\n\tassert.Equal(t, GPGCLI, GetCryptoBackend(WithCryptoBackend(ctx, GPGCLI)))\n\tassert.True(t, HasCryptoBackend(WithCryptoBackend(ctx, GPGCLI)))\n}\n\nfunc TestStorageBackend(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.Equal(t, \"fs\", StorageBackendName(FS))\n\tassert.Equal(t, FS, GetStorageBackend(ctx))\n\tctx1, err := WithStorageBackendString(ctx, \"fs\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, FS, GetStorageBackend(ctx1))\n\tassert.Equal(t, FS, GetStorageBackend(WithStorageBackend(ctx, FS)))\n\tassert.True(t, HasStorageBackend(WithStorageBackend(ctx, FS)))\n}\n\nfunc TestComposite(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = WithCryptoBackend(ctx, Age)\n\tctx = WithStorageBackend(ctx, FS)\n\n\tassert.Equal(t, Age, GetCryptoBackend(ctx))\n\tassert.Equal(t, FS, GetStorageBackend(ctx))\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/age.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"filippo.io/age\"\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age/agent\"\n\t\"github.com/gopasspw/gopass/internal/cache\"\n\t\"github.com/gopasspw/gopass/internal/cache/ghssh\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\t// Ext is the file extension for age encrypted secrets.\n\tExt = \"age\"\n\t// IDFile is the name for age recipients.\n\tIDFile = \".age-recipients\"\n)\n\ntype githubSSHCacher interface {\n\tListKeys(ctx context.Context, user string) ([]string, error)\n\tString() string\n}\n\n// Age is an age backend.\ntype Age struct {\n\tidentity   string\n\tghCache    githubSSHCacher\n\taskPass    *askPass\n\trecpCache  *cache.OnDisk\n\tsshKeyPath string // custom SSH key or directory path\n}\n\n// New creates a new Age backend.\nfunc New(ctx context.Context, sshKeyPath string) (*Age, error) {\n\tghc, err := ghssh.New()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trc, err := cache.NewOnDisk(\"age-identity-recipients\", 30*time.Hour)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ta := &Age{\n\t\tghCache:    ghc,\n\t\trecpCache:  rc,\n\t\tidentity:   filepath.Join(appdir.UserConfig(), \"age\", \"identities\"),\n\t\taskPass:    newAskPass(ctx),\n\t\tsshKeyPath: sshKeyPath,\n\t}\n\ta.tryStartAgent(ctx)\n\n\tdebug.Log(\"age initialized (ghc: %s, recipients: %s, identity: %s, sshKeyPath: %s)\", a.ghCache.String(), a.recpCache.String(), a.identity, a.sshKeyPath)\n\n\treturn a, nil\n}\n\nfunc (a *Age) tryStartAgent(ctx context.Context) {\n\tif !config.Bool(ctx, \"age.agent-enabled\") {\n\t\tdebug.Log(\"age agent disabled\")\n\n\t\treturn\n\t}\n\n\tclient := agent.NewClient()\n\tif err := client.Ping(); err == nil {\n\t\tdebug.Log(\"age agent already running\")\n\n\t\treturn\n\t}\n\n\tdebug.Log(\"age agent not running, starting it...\")\n\tif err := startAgent(ctx); err != nil {\n\t\tdebug.Log(\"failed to start age agent: %s\", err)\n\n\t\treturn\n\t}\n\n\tbo := backoff.NewExponentialBackOff()\n\tbo.InitialInterval = 25 * time.Millisecond\n\tbo.MaxElapsedTime = 3 * time.Second\n\top := func() error {\n\t\treturn client.Ping()\n\t}\n\tif err := backoff.Retry(op, bo); err != nil {\n\t\tdebug.Log(\"failed to ping age agent after starting: %s\", err)\n\n\t\treturn\n\t}\n\n\t// send identities to agent\n\tids, err := a.getAllIdentities(ctx)\n\tif err != nil {\n\t\tdebug.Log(\"failed to get identities: %s\", err)\n\n\t\treturn\n\t}\n\n\tidStrs := make([]string, 0, len(ids))\n\tfor _, id := range ids {\n\t\tidStrs = append(idStrs, fmt.Sprintf(\"%s\", id))\n\t}\n\n\tif err := client.SendIdentities(strings.Join(idStrs, \"\\n\")); err != nil {\n\t\tdebug.Log(\"failed to send identities to agent: %s\", err)\n\t}\n\n\t// set timeout\n\tif timeout := config.AsInt(config.String(ctx, \"age.agent-timeout\")); timeout > 0 {\n\t\tif err := client.SetTimeout(timeout); err != nil {\n\t\t\tdebug.Log(\"failed to set agent timeout: %s\", err)\n\t\t}\n\t}\n}\n\n// Initialized returns nil.\nfunc (a *Age) Initialized(ctx context.Context) error {\n\tif a == nil {\n\t\treturn fmt.Errorf(\"Age not initialized\")\n\t}\n\n\treturn nil\n}\n\n// Name returns age.\nfunc (a *Age) Name() string {\n\treturn \"age\"\n}\n\n// Version returns the version of the age dependency being used.\nfunc (a *Age) Version(ctx context.Context) semver.Version {\n\treturn debug.ModuleVersion(\"filippo.io/age\")\n}\n\n// Ext returns the extension.\nfunc (a *Age) Ext() string {\n\treturn Ext\n}\n\n// IDFile return the recipients file.\nfunc (a *Age) IDFile() string {\n\treturn IDFile\n}\n\n// Concurrency returns 1 for `age` since otherwise it prompts for the identity password for each worker.\nfunc (a *Age) Concurrency() int {\n\treturn 1\n}\n\n// GetFingerprint returns the fingerprint of a key.\nfunc (a *Age) GetFingerprint(ctx context.Context, key []byte) (string, error) {\n\treturn string(key), nil\n}\n\n// Lock flushes the password cache.\nfunc (a *Age) Lock() {\n\ta.askPass.Lock()\n}\n\nfunc (a *Age) identitiesToString(ids []age.Identity) (string, error) {\n\tvar sb strings.Builder\n\tfor _, id := range ids {\n\t\tfmt.Fprintln(&sb, id)\n\t}\n\n\treturn sb.String(), nil\n}\n\n// String implements fmt.Stringer.\nfunc (a *Age) String() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"Age(\")\n\tif a == nil {\n\t\tsb.WriteString(\"<nil>)\")\n\n\t\treturn sb.String()\n\t}\n\tsb.WriteString(\"Identity: \")\n\tsb.WriteString(a.identity)\n\tif a.sshKeyPath != \"\" {\n\t\tsb.WriteString(\", SSHKeyPath: \")\n\t\tsb.WriteString(a.sshKeyPath)\n\t}\n\tsb.WriteString(\")\")\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/age_test.go",
    "content": "package age\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n}\n\nfunc TestInitialized(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\terr = a.Initialized(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc TestName(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\tname := a.Name()\n\tassert.Equal(t, \"age\", name)\n}\n\nfunc TestVersion(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\tversion := a.Version(ctx)\n\texpectedVersion := debug.ModuleVersion(\"filippo.io/age\")\n\tassert.Equal(t, expectedVersion, version)\n}\n\nfunc TestExt(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\text := a.Ext()\n\tassert.Equal(t, Ext, ext)\n}\n\nfunc TestIDFile(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\tidFile := a.IDFile()\n\tassert.Equal(t, IDFile, idFile)\n}\n\nfunc TestConcurrency(t *testing.T) {\n\tctx := t.Context()\n\ta, err := New(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, a)\n\n\tconcurrency := a.Concurrency()\n\tassert.Equal(t, 1, concurrency)\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent/agent.go",
    "content": "// Package agent implements the gopass age-agent.\npackage agent\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"filippo.io/age\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\tsocketName = \"gopass-age-agent.sock\"\n)\n\n// Agent is a gopass age agent.\ntype Agent struct {\n\tsocketPath string\n\tlistener   net.Listener\n\n\tmux        sync.Mutex\n\tidentities []age.Identity\n\tlocked     bool\n\ttimer      *time.Timer\n\ttimeout    time.Duration\n}\n\n// New creates a new agent.\nfunc New() (*Agent, error) {\n\tsocketDir := appdir.UserRuntime()\n\tif err := os.MkdirAll(socketDir, 0o700); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create socket directory: %w\", err)\n\t}\n\n\tsocketPath := filepath.Join(socketDir, socketName)\n\n\treturn &Agent{\n\t\tsocketPath: socketPath,\n\t\tlocked:     false,\n\t\ttimeout:    0,\n\t}, nil\n}\n\n// Run starts the agent.\nfunc (a *Agent) Run(ctx context.Context) error {\n\t// listen on the socket\n\tl, err := net.Listen(\"unix\", a.socketPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to listen on socket: %w\", err)\n\t}\n\tif err := os.Chmod(a.socketPath, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to set socket permissions: %w\", err)\n\t}\n\ta.listener = l\n\tdefer func() {\n\t\t_ = a.listener.Close()\n\t}()\n\n\tdebug.Log(\"agent listening on %s\", a.socketPath)\n\n\t// handle signals\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\tsig := <-sigChan\n\t\tdebug.Log(\"received signal %s, shutting down\", sig)\n\t\ta.Shutdown(ctx)\n\t}()\n\n\t// accept connections\n\tfor {\n\t\tconn, err := a.listener.Accept()\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdebug.Log(\"failed to accept connection: %s\", err)\n\n\t\t\tcontinue\n\t\t}\n\t\tgo a.handleConnection(ctx, conn)\n\t}\n}\n\n// Shutdown stops the agent.\nfunc (a *Agent) Shutdown(ctx context.Context) {\n\tif a.listener != nil {\n\t\t_ = a.listener.Close()\n\t}\n\tif err := os.Remove(a.socketPath); err != nil {\n\t\tdebug.Log(\"failed to remove socket file: %s\", err)\n\t}\n\n\tdebug.Log(\"agent shut down\")\n}\n\nfunc (a *Agent) handleConnection(ctx context.Context, conn net.Conn) {\n\tdefer func() {\n\t\t_ = conn.Close()\n\t}()\n\n\tscanner := bufio.NewScanner(conn)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tdebug.Log(\"received: %s\", line)\n\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmd := parts[0]\n\t\targs := parts[1:]\n\n\t\tswitch cmd {\n\t\tcase \"ping\":\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\tcase \"status\":\n\t\t\ta.mux.Lock()\n\t\t\tlocked := a.locked\n\t\t\ta.mux.Unlock()\n\t\t\tif locked {\n\t\t\t\tfmt.Fprintln(conn, \"OK locked\")\n\t\t\t} else {\n\t\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\t\t}\n\t\tcase \"identities\":\n\t\t\tif len(args) < 1 {\n\t\t\t\tfmt.Fprintln(conn, \"ERR missing identities\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tids, err := age.ParseIdentities(strings.NewReader(strings.Join(args, \"\\n\")))\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintln(conn, \"ERR failed to parse identities: \"+err.Error())\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta.mux.Lock()\n\t\t\ta.identities = ids\n\t\t\ta.mux.Unlock()\n\t\t\tdebug.Log(\"loaded %d identities\", len(ids))\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\tcase \"decrypt\":\n\t\t\tif len(args) != 1 {\n\t\t\t\tfmt.Fprintln(conn, \"ERR missing ciphertext\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tciphertext, err := base64.StdEncoding.DecodeString(args[0])\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintln(conn, \"ERR failed to decode ciphertext: \"+err.Error())\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tplaintext, err := a.decrypt(ciphertext)\n\t\t\tif err != nil {\n\t\t\t\tif err.Error() == \"agent is locked\" {\n\t\t\t\t\tfmt.Fprintln(conn, \"ERR agent is locked\")\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Fprintln(conn, \"ERR failed to decrypt: \"+err.Error())\n\t\t\t\t}\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfmt.Fprintln(conn, \"OK \"+base64.StdEncoding.EncodeToString(plaintext))\n\t\tcase \"lock\":\n\t\t\t// clear all identities from memory\n\t\t\ta.mux.Lock()\n\t\t\ta.identities = nil\n\t\t\ta.locked = true\n\t\t\ta.mux.Unlock()\n\n\t\t\tdebug.Log(\"cleared identities from memory and locked agent\")\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\tcase \"unlock\":\n\t\t\ta.mux.Lock()\n\t\t\ta.locked = false\n\t\t\ta.mux.Unlock()\n\n\t\t\tdebug.Log(\"unlocked agent\")\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\tcase \"set-timeout\":\n\t\t\tif len(args) != 1 {\n\t\t\t\tfmt.Fprintln(conn, \"ERR missing timeout\")\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttimeout, err := strconv.Atoi(args[0])\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintln(conn, \"ERR failed to parse timeout: \"+err.Error())\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta.setTimeout(time.Duration(timeout) * time.Second)\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\tcase \"quit\":\n\t\t\tfmt.Fprintln(conn, \"OK\")\n\t\t\tgo a.Shutdown(ctx)\n\n\t\t\treturn\n\t\tdefault:\n\t\t\tfmt.Fprintln(conn, \"ERR unknown command\")\n\t\t}\n\t}\n}\n\nfunc (a *Agent) setTimeout(timeout time.Duration) {\n\ta.mux.Lock()\n\tdefer a.mux.Unlock()\n\n\ta.timeout = timeout\n\tif a.timer != nil {\n\t\ta.timer.Stop()\n\t}\n\tif a.timeout > 0 {\n\t\ta.timer = time.AfterFunc(a.timeout, func() {\n\t\t\ta.lock()\n\t\t})\n\t}\n}\n\nfunc (a *Agent) lock() {\n\ta.mux.Lock()\n\tdefer a.mux.Unlock()\n\n\ta.identities = nil\n\ta.locked = true\n\tif a.timer != nil {\n\t\ta.timer.Stop()\n\t}\n\tdebug.Log(\"cleared identities from memory and locked agent\")\n}\n\nfunc (a *Agent) decrypt(ciphertext []byte) ([]byte, error) {\n\ta.mux.Lock()\n\tdefer a.mux.Unlock()\n\tif a.locked {\n\t\treturn nil, fmt.Errorf(\"agent is locked\")\n\t}\n\tif a.timer != nil {\n\t\ta.timer.Reset(a.timeout)\n\t}\n\tout := &bytes.Buffer{}\n\tf := bytes.NewReader(ciphertext)\n\tr, err := age.Decrypt(f, a.identities...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt: %w\", err)\n\t}\n\n\tif _, err := io.Copy(out, r); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write plaintext to buffer: %w\", err)\n\t}\n\n\treturn out.Bytes(), nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent/agent_test.go",
    "content": "package agent\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"filippo.io/age\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAgent(t *testing.T) {\n\tctx := t.Context()\n\tctx = termio.WithPassPromptFunc(ctx, func(ctx context.Context, prompt string) (string, error) {\n\t\treturn \"test\", nil\n\t})\n\n\t// start agent\n\ta, err := New()\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\t_ = a.Run(ctx)\n\t}()\n\tdefer a.Shutdown(ctx)\n\n\t// wait for it to be ready\n\ttime.Sleep(time.Second)\n\n\t// create client\n\tc := NewClient()\n\trequire.NoError(t, c.Ping())\n\n\t// test decrypt\n\tid, err := age.GenerateX25519Identity()\n\trequire.NoError(t, err)\n\n\tplaintext := []byte(\"hello world\")\n\tbuf := &bytes.Buffer{}\n\twc, err := age.Encrypt(buf, id.Recipient())\n\trequire.NoError(t, err)\n\t_, _ = wc.Write(plaintext)\n\trequire.NoError(t, wc.Close())\n\tciphertext := buf.Bytes()\n\n\trequire.NoError(t, c.SendIdentities(id.String()))\n\n\tdecrypted, err := c.Decrypt(ciphertext)\n\trequire.NoError(t, err)\n\trequire.Equal(t, plaintext, decrypted)\n\n\t// test lock\n\trequire.NoError(t, c.Lock())\n\n\t// test quit\n\trequire.NoError(t, c.Quit())\n}\n\nfunc TestAgentAutoLock(t *testing.T) {\n\tctx := t.Context()\n\tctx = termio.WithPassPromptFunc(ctx, func(ctx context.Context, prompt string) (string, error) {\n\t\treturn \"test\", nil\n\t})\n\n\t// start agent\n\ta, err := New()\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\t_ = a.Run(ctx)\n\t}()\n\tdefer a.Shutdown(ctx)\n\n\t// wait for it to be ready\n\ttime.Sleep(time.Second)\n\n\t// create client\n\tc := NewClient()\n\trequire.NoError(t, c.Ping())\n\n\t// set timeout\n\trequire.NoError(t, c.SetTimeout(1))\n\n\t// test decrypt\n\tid, err := age.GenerateX25519Identity()\n\trequire.NoError(t, err)\n\n\tplaintext := []byte(\"hello world\")\n\tbuf := &bytes.Buffer{}\n\twc, err := age.Encrypt(buf, id.Recipient())\n\trequire.NoError(t, err)\n\t_, _ = wc.Write(plaintext)\n\trequire.NoError(t, wc.Close())\n\tciphertext := buf.Bytes()\n\n\trequire.NoError(t, c.SendIdentities(id.String()))\n\n\tdecrypted, err := c.Decrypt(ciphertext)\n\trequire.NoError(t, err)\n\trequire.Equal(t, plaintext, decrypted)\n\n\t// wait for auto-lock\n\ttime.Sleep(2 * time.Second)\n\n\t// check if locked\n\t_, err = c.Decrypt(ciphertext)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"agent is locked\")\n\n\t// test quit\n\trequire.NoError(t, c.Quit())\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent/client.go",
    "content": "package agent\n\nimport (\n\t\"bufio\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Client is a client for the age agent.\ntype Client struct {\n\tsocketPath string\n}\n\n// NewClient creates a new client.\nfunc NewClient() *Client {\n\treturn &Client{\n\t\tsocketPath: filepath.Join(appdir.UserRuntime(), socketName),\n\t}\n}\n\nfunc (c *Client) connect() (net.Conn, error) {\n\tif err := c.checkSocketSecurity(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebug.Log(\"connecting to agent at %s\", c.socketPath)\n\tconn, err := net.Dial(\"unix\", c.socketPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to connect to agent: %w\", err)\n\t}\n\n\tdebug.Log(\"connected to agent at %s\", c.socketPath)\n\n\treturn conn, nil\n}\n\nfunc (c *Client) send(cmd string) (string, error) {\n\tconn, err := c.connect()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() {\n\t\t_ = conn.Close()\n\t}()\n\n\tif _, err := fmt.Fprintln(conn, cmd); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to send command to agent: %w\", err)\n\t}\n\n\tresp, err := bufio.NewReader(conn).ReadString('\\n')\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read response from agent: %w\", err)\n\t}\n\n\tresp = strings.TrimSpace(resp)\n\tif strings.HasPrefix(resp, \"ERR\") {\n\t\treturn \"\", fmt.Errorf(\"agent error: %s\", strings.TrimPrefix(resp, \"ERR \"))\n\t}\n\n\treturn strings.TrimPrefix(resp, \"OK \"), nil\n}\n\n// Ping pings the agent.\nfunc (c *Client) Ping() error {\n\t_, err := c.send(\"ping\")\n\n\treturn err\n}\n\n// Status returns the agent's status.\nfunc (c *Client) Status() (string, error) {\n\treturn c.send(\"status\")\n}\n\n// SendIdentities sends the identities to the agent.\nfunc (c *Client) SendIdentities(ids string) error {\n\t_, err := c.send(\"identities \" + ids)\n\n\treturn err\n}\n\n// Decrypt decrypts the given ciphertext.\nfunc (c *Client) Decrypt(ciphertext []byte) ([]byte, error) {\n\tresp, err := c.send(\"decrypt \" + base64.StdEncoding.EncodeToString(ciphertext))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn base64.StdEncoding.DecodeString(resp)\n}\n\n// Remove removes a passphrase from the agent.\nfunc (c *Client) Remove(key string) error {\n\t_, err := c.send(\"remove \" + key)\n\n\treturn err\n}\n\n// Lock locks the agent.\nfunc (c *Client) Lock() error {\n\t_, err := c.send(\"lock\")\n\n\treturn err\n}\n\n// Unlock unlocks the agent.\nfunc (c *Client) Unlock() error {\n\t_, err := c.send(\"unlock\")\n\n\treturn err\n}\n\n// SetTimeout sets the agent's timeout.\nfunc (c *Client) SetTimeout(timeout int) error {\n\t_, err := c.send(\"set-timeout \" + strconv.Itoa(timeout))\n\n\treturn err\n}\n\n// Quit quits the agent.\nfunc (c *Client) Quit() error {\n\t_, err := c.send(\"quit\")\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent/client_unix.go",
    "content": "//go:build !windows\n\npackage agent\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc (c *Client) checkSocketSecurity() error {\n\tinfo, err := os.Stat(c.socketPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat socket: %w\", err)\n\t}\n\n\t// Check socket permissions.\n\tif info.Mode()&os.ModePerm != 0o600 {\n\t\treturn fmt.Errorf(\"incorrect socket permissions: %v\", info.Mode().Perm())\n\t}\n\n\t// Check socket ownership.\n\tstat, ok := info.Sys().(*syscall.Stat_t)\n\tif !ok {\n\t\treturn fmt.Errorf(\"failed to get socket system info\")\n\t}\n\n\tif stat.Uid != uint32(os.Getuid()) {\n\t\treturn fmt.Errorf(\"socket owned by wrong user: %d\", stat.Uid)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent/client_windows.go",
    "content": "//go:build windows\n\npackage agent\n\nfunc (c *Client) checkSocketSecurity() error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent_starter_unix.go",
    "content": "//go:build !windows\n\npackage age\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc startAgent(_ context.Context) error {\n\tcmd := exec.Command(os.Args[0], \"age\", \"agent\", \"start\")\n\tcmd.Env = os.Environ()\n\tcmd.Stderr = os.Stderr\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetpgid: true,\n\t}\n\n\treturn cmd.Start()\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/agent_starter_windows.go",
    "content": "//go:build windows\n\npackage age\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n)\n\nfunc startAgent(_ context.Context) error {\n\tcmd := exec.Command(os.Args[0], \"age\", \"agent\", \"start\")\n\tcmd.Env = os.Environ()\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Start()\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/askpass.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/cache\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/pinentry/cli\"\n\t\"github.com/twpayne/go-pinentry/v4\"\n\t\"github.com/zalando/go-keyring\"\n)\n\ntype cacher interface {\n\tGet(string) (string, bool)\n\tSet(string, string)\n\tRemove(string)\n\tPurge()\n}\n\ntype osKeyring struct {\n\tknownKeys map[string]bool\n}\n\nfunc newOsKeyring() *osKeyring {\n\treturn &osKeyring{\n\t\tknownKeys: make(map[string]bool),\n\t}\n}\n\nfunc (o *osKeyring) Get(key string) (string, bool) {\n\tsec, err := keyring.Get(\"gopass\", key)\n\tif err != nil {\n\t\tdebug.Log(\"failed to get %s from OS keyring: %w\", key, err)\n\n\t\treturn \"\", false\n\t}\n\to.knownKeys[name] = true\n\n\treturn sec, true\n}\n\nfunc (o *osKeyring) Set(name, value string) {\n\tif err := keyring.Set(\"gopass\", name, value); err != nil {\n\t\tdebug.Log(\"failed to set %s: %w\", name, err)\n\t}\n\to.knownKeys[name] = true\n}\n\nfunc (o *osKeyring) Remove(name string) {\n\tif err := keyring.Delete(\"gopass\", name); err != nil {\n\t\tdebug.Log(\"failed to remove %s from keyring: %s\", name, err)\n\n\t\treturn\n\t}\n\to.knownKeys[name] = false\n}\n\nfunc (o *osKeyring) Purge() {\n\t// purge all known keys. only useful for the REPL case.\n\t// Does not persist across restarts.\n\tfor k, v := range o.knownKeys {\n\t\tif !v {\n\t\t\tcontinue\n\t\t}\n\t\tif err := keyring.Delete(\"gopass\", k); err != nil {\n\t\t\tdebug.Log(\"failed to remove %s from keyring: %s\", k, err)\n\t\t}\n\t}\n}\n\ntype askPass struct {\n\ttesting bool\n\tcache   cacher\n}\n\nfunc newAskPass(ctx context.Context) *askPass {\n\ta := &askPass{\n\t\tcache: cache.NewInMemTTL[string, string](time.Hour, 24*time.Hour),\n\t}\n\n\tif config.Bool(ctx, \"age.usekeychain\") {\n\t\tif err := keyring.Set(\"gopass\", \"sentinel\", \"empty\"); err == nil {\n\t\t\tdebug.V(1).Log(\"using OS keychain to cache age credentials\")\n\t\t\ta.cache = newOsKeyring()\n\t\t}\n\t}\n\n\treturn a\n}\n\nfunc (a *askPass) Ping(_ context.Context) error {\n\treturn nil\n}\n\nfunc (a *askPass) Passphrase(key string, reason string, repeat bool) (string, error) {\n\tif value, found := a.cache.Get(key); found || a.testing {\n\t\tdebug.V(1).Log(\"Read value for %s from cache\", key)\n\n\t\treturn value, nil\n\t}\n\tdebug.V(1).Log(\"Value for %s not found in cache\", key)\n\n\tpw, err := a.getPassphrase(reason, repeat)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"pinentry error: %w\", err)\n\t}\n\n\tdebug.V(1).Log(\"Updated value for %s in cache\", key)\n\ta.cache.Set(key, pw)\n\n\treturn pw, nil\n}\n\nfunc (a *askPass) getPassphrase(reason string, repeat bool) (string, error) {\n\topts := []pinentry.ClientOption{\n\t\tpinentry.WithBinaryNameFromGnuPGAgentConf(),\n\t\tpinentry.WithDesc(strings.TrimSuffix(reason, \":\") + \".\"),\n\t\tpinentry.WithGPGTTY(),\n\t\tpinentry.WithPrompt(\"Passphrase:\"),\n\t\tpinentry.WithTitle(\"gopass\"),\n\t}\n\tif repeat {\n\t\topts = append(opts, pinentry.WithRepeat(\"Confirm\"))\n\t} else {\n\t\topts = append(opts,\n\t\t\tpinentry.WithOption(pinentry.OptionAllowExternalPasswordCache),\n\t\t\tpinentry.WithKeyInfo(\"gopass/age-identities\"),\n\t\t)\n\t}\n\n\tp, err := pinentry.NewClient(opts...)\n\tif err != nil {\n\t\tdebug.Log(\"Pinentry not found: %q\", err)\n\t\t// use CLI fallback\n\t\tpf := cli.New()\n\t\tif repeat {\n\t\t\t_ = pf.Set(\"REPEAT\")\n\t\t}\n\n\t\treturn pf.GetPIN()\n\t}\n\tdefer func() {\n\t\t_ = p.Close()\n\t}()\n\n\tresult, err := p.GetPIN()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"pinentry error: %w\", err)\n\t}\n\n\treturn result.PIN, nil\n}\n\nfunc (a *askPass) Remove(key string) {\n\ta.cache.Remove(key)\n}\n\nfunc (a *askPass) Lock() {\n\ta.cache.Purge()\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/clientUI.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\n\t\"filippo.io/age/plugin\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\nvar pluginTerminalUI = &plugin.ClientUI{\n\tDisplayMessage: func(name, message string) error {\n\t\tout.Printf(context.Background(), \"%s plugin: %s\", name, message)\n\n\t\treturn nil\n\t},\n\tRequestValue: func(name, message string, _ bool) (string, error) {\n\t\tvar err error\n\t\tdefer func() {\n\t\t\tif err != nil {\n\t\t\t\tout.Warningf(context.Background(), \"could not read value for age-plugin-%s: %v\", name, err)\n\t\t\t}\n\t\t}()\n\t\tsecret, err := termio.AskForPassword(context.Background(), \"secret\", false)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn secret, nil\n\t},\n\tConfirm: func(name, message, yes, no string) (bool, error) {\n\t\trep, _ := cui.GetSelection(context.Background(), message, []string{yes, no})\n\t\tif rep == yes {\n\t\t\treturn true, nil\n\t\t}\n\n\t\treturn false, nil\n\t},\n\n\tWaitTimer: func(name string) {\n\t\tout.Printf(context.Background(), \"waiting on %s plugin...\", name)\n\t},\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/commands.go",
    "content": "package age\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"filippo.io/age\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age/agent\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n//nolint:cyclop\nfunc (l loader) Commands() []*cli.Command {\n\treturn []*cli.Command{\n\t\t{\n\t\t\tName:   name,\n\t\t\tHidden: false,\n\t\t\tUsage:  \"age commands\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Built-in commands for the age backend.\\n\" +\n\t\t\t\t\"These allow limited interactions with the gopass specific age identities.\\n \" +\n\t\t\t\t\"Added identities are automatically added as recipient to your secrets when encrypting, but not to\" +\n\t\t\t\t\"your recipients, make sure to keep your recipients and identities in sync as you want to.\\n\" +\n\t\t\t\t\"All age identities, including plugin ones should be supported. We also still support github\" +\n\t\t\t\t\"identities despite them being deprecated by age, we do so by falling back to the ssh identities\" +\n\t\t\t\t\"for these and keeping a local cache of ssh keys for a given github identity.\",\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    \"age-ssh-key-path\",\n\t\t\t\t\tUsage:   \"Custom path to SSH key or directory for age backend\",\n\t\t\t\t\tEnvVars: []string{\"GOPASS_SSH_DIR\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t{\n\t\t\t\t\tName:  \"agent\",\n\t\t\t\t\tUsage: \"Manage the age agent\",\n\t\t\t\t\tDescription: \"Manage the age agent, this will start a background process that will cache your age identities in memory and provide them to gopass on demand. \" +\n\t\t\t\t\t\t\"This is optional, but recommended if you use age identities that require a password or are managed by a plugin.\",\n\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\tif err := cli.ShowSubcommandHelp(c); err != nil {\n\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to show help\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn exit.Error(exit.Usage, nil, \"Please specify a subcommand\")\n\t\t\t\t\t},\n\t\t\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        \"start\",\n\t\t\t\t\t\t\tUsage:       \"Start the age agent\",\n\t\t\t\t\t\t\tDescription: \"Start the age agent\",\n\t\t\t\t\t\t\tAction:      l.agent,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        \"stop\",\n\t\t\t\t\t\t\tUsage:       \"Stop the age agent\",\n\t\t\t\t\t\t\tDescription: \"Stop the age agent\",\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\t\t\tclient := agent.NewClient()\n\t\t\t\t\t\t\t\tif err := client.Quit(); err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to stop agent: %s\", err)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tout.Printf(ctx, \"Age agent asked to stop\")\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        \"status\",\n\t\t\t\t\t\t\tUsage:       \"Check if the age agent is running, this will return 0 if the agent is running and 1 otherwise\",\n\t\t\t\t\t\t\tDescription: \"Check if the age agent is running, this will return 0 if the agent is running and 1 otherwise\",\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\t\t\tclient := agent.NewClient()\n\t\t\t\t\t\t\t\tstatus, err := client.Status()\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\tout.Printf(ctx, \"Age agent is not running\")\n\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"agent not running\")\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tout.Printf(ctx, \"Age agent is running\")\n\t\t\t\t\t\t\t\tif status == \"locked\" {\n\t\t\t\t\t\t\t\t\tout.Printf(ctx, \" (locked)\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        \"unlock\",\n\t\t\t\t\t\t\tUsage:       \"Unlock the age agent\",\n\t\t\t\t\t\t\tDescription: \"Unlock the age agent, this will allow gopass to ask for your password again when decrypting\",\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tclient := agent.NewClient()\n\t\t\t\t\t\t\t\tif err := client.Unlock(); err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to unlock agent: %s\", err)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tout.Printf(c.Context, \"Age agent unlocked\")\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        \"lock\",\n\t\t\t\t\t\t\tUsage:       \"Lock the age agent\",\n\t\t\t\t\t\t\tDescription: \"Lock the age agent\",\n\t\t\t\t\t\t\tAction:      l.lock,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"identities\",\n\t\t\t\t\tUsage: \"List age identities used for decryption and encryption\",\n\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\"List identities\",\n\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\tsshKeyPath := config.String(ctx, \"age.ssh-key-path\")\n\t\t\t\t\t\tif sv := c.String(\"age-ssh-key-path\"); sv != \"\" {\n\t\t\t\t\t\t\tsshKeyPath = sv\n\t\t\t\t\t\t}\n\t\t\t\t\t\ta, err := New(ctx, sshKeyPath)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to create age backend\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tids, err := a.IdentityRecipients(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to get age identities\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(ids) < 1 {\n\t\t\t\t\t\t\tout.Notice(ctx, \"No identities found\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor _, id := range recipientsToString(ids) {\n\t\t\t\t\t\t\tout.Print(ctx, out.Secret(id))\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tSubcommands: []*cli.Command{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"add\",\n\t\t\t\t\t\t\tUsage: \"Add an existing age identity\",\n\t\t\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\t\t\"Add an existing age identity, interactively\",\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\t\t\tsshKeyPath := config.String(ctx, \"age.ssh-key-path\")\n\t\t\t\t\t\t\t\tif sv := c.String(\"age-ssh-key-path\"); sv != \"\" {\n\t\t\t\t\t\t\t\t\tsshKeyPath = sv\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ta, err := New(ctx, sshKeyPath)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to create age backend\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tidS, recEncm := c.Args().Get(0), c.Args().Get(1)\n\n\t\t\t\t\t\t\t\tif len(idS) < 1 {\n\t\t\t\t\t\t\t\t\tidS, err = termio.AskForPassword(ctx, \"the age identity starting in AGE-\", false)\n\t\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to read age identity\")\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif len(recEncm) < 1 && !strings.HasPrefix(idS, \"AGE-SECRET-KEY-1\") {\n\t\t\t\t\t\t\t\t\trecEncm, err = termio.AskForString(ctx, \"Provide the corresponding age recipient\", \"\")\n\t\t\t\t\t\t\t\t\tif err != nil || recEncm == \"\" {\n\t\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to read corresponding age recipient\")\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif strings.HasPrefix(recEncm, \"AGE-\") {\n\t\t\t\t\t\t\t\t\t\tout.Warning(ctx, \"You have provided an identity as a recipient, recipients should start in 'age1', this might not be properly supported and might leak secret data in our identity recipient cache\")\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tid, err := parseIdentity(idS + \"|\" + recEncm)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to parse age identity\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\terr = a.addIdentity(ctx, id)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to save age identity\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\trec := IdentityToRecipient(id)\n\t\t\t\t\t\t\t\tout.Noticef(ctx, \"New age identities are not automatically added to your recipient list, consider adding it using 'gopass recipients add %s'\", rec)\n\t\t\t\t\t\t\t\tout.Warning(ctx, \"If you do not add this recipient to the recipient list, make sure to re-encrypt using 'gopass fsck --decrypt' to properly support this identity\")\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"keygen\",\n\t\t\t\t\t\t\tUsage: \"Generate a new age identity\",\n\t\t\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\t\t\"Generate a new age identity\",\n\t\t\t\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t\t\t\t&cli.StringFlag{\n\t\t\t\t\t\t\t\t\tName:  \"password\",\n\t\t\t\t\t\t\t\t\tUsage: \"Password for the new key\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\t\t\tsshKeyPath := config.String(ctx, \"age.ssh-key-path\")\n\t\t\t\t\t\t\t\tif sv := c.String(\"age-ssh-key-path\"); sv != \"\" {\n\t\t\t\t\t\t\t\t\tsshKeyPath = sv\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ta, err := New(ctx, sshKeyPath)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to create age backend\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tpw := c.String(\"password\")\n\t\t\t\t\t\t\t\tif pw == \"\" {\n\t\t\t\t\t\t\t\t\tpw, err = termio.AskForPassword(ctx, \"Enter password for new key\", true)\n\t\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\trec, err := a.GenerateIdentity(ctx, \"\", \"\", pw)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to generate age identity\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tout.Printf(ctx, \"New age identity created: %s\", rec)\n\t\t\t\t\t\t\t\tout.Notice(ctx, \"New age identities are not automatically added to your recipient list, consider adding it using 'gopass recipients add age1...'\")\n\t\t\t\t\t\t\t\tout.Warning(ctx, \"If you do not add this recipient to the recipient list, make sure to re-encrypt using 'gopass fsck --decrypt' to properly support this identity\")\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"remove\",\n\t\t\t\t\t\t\tAliases: []string{\"rm\"},\n\t\t\t\t\t\t\tUsage:   \"Remove an identity\",\n\t\t\t\t\t\t\tDescription: \"\" +\n\t\t\t\t\t\t\t\t\"Remove all identity matching the argument\",\n\t\t\t\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\t\t\t\t\tsshKeyPath := config.String(ctx, \"age.ssh-key-path\")\n\t\t\t\t\t\t\t\tif sv := c.String(\"age-ssh-key-path\"); sv != \"\" {\n\t\t\t\t\t\t\t\t\tsshKeyPath = sv\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ta, err := New(ctx, sshKeyPath)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to create age backend\")\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tvictim := c.Args().First()\n\t\t\t\t\t\t\t\tif len(victim) == 0 {\n\t\t\t\t\t\t\t\t\treturn exit.Error(exit.Usage, err, \"missing argument to remove\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tids, _ := a.Identities(ctx)\n\t\t\t\t\t\t\t\tnewIds := make([]string, 0, len(ids))\n\n\t\t\t\t\t\t\t\tdebug.Log(\"ranging over %d age identities\", len(ids))\n\t\t\t\t\t\t\t\tfor _, id := range ids {\n\t\t\t\t\t\t\t\t\t// we only need to care about X25519 and plugin/wrapped identities here because\n\t\t\t\t\t\t\t\t\t// SSH identities are considered external and are not managed by gopass.\n\t\t\t\t\t\t\t\t\t// Users should use ssh-keygen and such to deal with them.\n\t\t\t\t\t\t\t\t\t// At least we definitely don't want to remove them.\n\t\t\t\t\t\t\t\t\tswitch x := id.(type) {\n\t\t\t\t\t\t\t\t\tcase *age.X25519Identity:\n\t\t\t\t\t\t\t\t\t\tif x.Recipient().String() == victim {\n\t\t\t\t\t\t\t\t\t\t\tdebug.Log(\"will remove X25519Identity %s\", x.Recipient())\n\n\t\t\t\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcase *wrappedIdentity:\n\t\t\t\t\t\t\t\t\t\tskip := false\n\t\t\t\t\t\t\t\t\t\t// to avoid fuzzy matching, let's match on entire parts\n\t\t\t\t\t\t\t\t\t\tfor part := range strings.SplitSeq(x.String(), \"|\") {\n\t\t\t\t\t\t\t\t\t\t\tif part == victim {\n\t\t\t\t\t\t\t\t\t\t\t\tskip = true\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tif skip {\n\t\t\t\t\t\t\t\t\t\t\tdebug.Log(\"will remove Plugin Identity %s\", x)\n\n\t\t\t\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tnewIds = append(newIds, fmt.Sprintf(\"%s\", id))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif len(newIds) != len(ids) {\n\t\t\t\t\t\t\t\t\tout.Warning(ctx, \"Make sure to run 'gopass fsck --decrypt' to re-encrypt your secrets without including that identity if it's not in your recipient list.\")\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tout.Notice(ctx, \"no matching identity found in list\")\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// we invalidate our recipient id cache when we remove an identity, if there's one\n\t\t\t\t\t\t\t\tif err := a.recpCache.Remove(idRecpCacheKey); err != nil {\n\t\t\t\t\t\t\t\t\tdebug.Log(\"error invalidating age id recipient cache: %s\", err)\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn a.saveIdentities(ctx, newIds, false)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:        \"lock\",\n\t\t\t\t\tUsage:       \"Lock the age agent\",\n\t\t\t\t\tDescription: \"Lock the age agent, this will remove all cached identities from memory and require you to re-enter any passwords for your identities when decrypting\",\n\t\t\t\t\tAction:      l.lock,\n\t\t\t\t\tHidden:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (l loader) agent(c *cli.Context) error {\n\tout.Printf(c.Context, \"Starting age agent ...\")\n\n\tag, err := agent.New()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn ag.Run(c.Context)\n}\n\nfunc (l loader) lock(c *cli.Context) error {\n\tclient := agent.NewClient()\n\tif err := client.Lock(); err != nil {\n\t\treturn exit.Error(exit.Unknown, err, \"failed to lock agent: %s\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/context.go",
    "content": "package age\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyOnlyNative contextKey = iota\n)\n\n// WithOnlyNative will return a context with the flag for only native set.\nfunc WithOnlyNative(ctx context.Context, at bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyOnlyNative, at)\n}\n\n// IsOnlyNative will return the value of the only native flag or the default\n// (false).\nfunc IsOnlyNative(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyOnlyNative).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/context_test.go",
    "content": "package age\n\nimport (\n\t\"testing\"\n)\n\nfunc TestWithOnlyNative(t *testing.T) {\n\tctx := t.Context()\n\tctx = WithOnlyNative(ctx, true)\n\n\tval := ctx.Value(ctxKeyOnlyNative)\n\tif val == nil {\n\t\tt.Errorf(\"Expected value to be set, got nil\")\n\t}\n\n\tboolVal, ok := val.(bool)\n\tif !ok {\n\t\tt.Errorf(\"Expected value to be of type bool, got %T\", val)\n\t}\n\n\tif !boolVal {\n\t\tt.Errorf(\"Expected value to be true, got false\")\n\t}\n}\n\nfunc TestIsOnlyNative(t *testing.T) {\n\tctx := t.Context()\n\n\t// Test default value\n\tif IsOnlyNative(ctx) {\n\t\tt.Errorf(\"Expected default value to be false, got true\")\n\t}\n\n\t// Test set value\n\tctx = WithOnlyNative(ctx, true)\n\tif !IsOnlyNative(ctx) {\n\t\tt.Errorf(\"Expected value to be true, got false\")\n\t}\n\n\t// Test reset value\n\tctx = WithOnlyNative(ctx, false)\n\tif IsOnlyNative(ctx) {\n\t\tt.Errorf(\"Expected value to be false, got true\")\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/decrypt.go",
    "content": "package age\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"filippo.io/age\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age/agent\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Decrypt will attempt to decrypt the given payload.\nfunc (a *Age) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {\n\tif config.Bool(ctx, \"age.agent-enabled\") {\n\t\tplaintext, err := a.decryptWithAgent(ctx, ciphertext)\n\t\tif err == nil {\n\t\t\treturn plaintext, nil\n\t\t}\n\t\tdebug.Log(\"failed to decrypt with agent: %s\", err)\n\t\tdebug.Log(\"falling back to direct decryption\")\n\t}\n\n\tif !ctxutil.HasPasswordCallback(ctx) {\n\t\tdebug.Log(\"no password callback found, redirecting to askPass\")\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\t\tpw, err := a.askPass.Passphrase(prompt, fmt.Sprintf(\"to load the keyring at %s\", a.identity), false)\n\n\t\t\treturn []byte(pw), err\n\t\t})\n\t\tctx = ctxutil.WithPasswordPurgeCallback(ctx, a.askPass.Remove)\n\t}\n\n\tids, err := a.getAllIds(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn a.decrypt(ciphertext, ids...)\n}\n\nfunc (a *Age) decryptWithAgent(ctx context.Context, ciphertext []byte) ([]byte, error) {\n\tclient := agent.NewClient()\n\tplaintext, err := client.Decrypt(ciphertext)\n\tif err == nil {\n\t\treturn plaintext, nil\n\t}\n\n\tif !strings.Contains(err.Error(), \"agent is locked\") {\n\t\tdebug.Log(\"failed to decrypt with agent: %s\", err)\n\n\t\treturn nil, err\n\t}\n\n\tdebug.Log(\"agent is locked, trying to unlock\")\n\t// unlock the agent\n\tif err := client.Unlock(); err != nil {\n\t\tdebug.Log(\"failed to unlock agent: %s\", err)\n\t}\n\t// get identities\n\tids, err := a.getAllIds(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// send identities to agent\n\tsIds, err := a.identitiesToString(ids)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := client.SendIdentities(sIds); err != nil {\n\t\tdebug.Log(\"failed to send identities to agent: %s\", err)\n\t}\n\t// set timeout\n\tif timeout := config.AsInt(config.String(ctx, \"age.agent-timeout\")); timeout > 0 {\n\t\tif err := client.SetTimeout(timeout); err != nil {\n\t\t\tdebug.Log(\"failed to set agent timeout: %s\", err)\n\t\t}\n\t}\n\t// retry decryption\n\treturn client.Decrypt(ciphertext)\n}\n\nfunc (a *Age) decrypt(ciphertext []byte, ids ...age.Identity) ([]byte, error) {\n\tdebug.V(1).Log(\"decrypting with %d ids\", len(ids))\n\n\tout := &bytes.Buffer{}\n\tf := bytes.NewReader(ciphertext)\n\tr, err := age.Decrypt(f, ids...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt: %w\", err)\n\t}\n\tn, err := io.Copy(out, r)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write plaintext to buffer: %w\", err)\n\t}\n\tdebug.V(1).Log(\"Decrypted %d bytes of ciphertext to %d bytes of plaintext\", len(ciphertext), n)\n\n\treturn out.Bytes(), nil\n}\n\n// decryptFile is used to decrypt a scrypt encrypted age keyring/identity file.\nfunc (a *Age) decryptFile(ctx context.Context, filename string) ([]byte, error) {\n\tciphertext, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdebug.V(1).Log(\"read %d bytes from %s\", len(ciphertext), filename)\n\n\tpw, err := ctxutil.GetPasswordCallback(ctx)(filename, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// debug.Log(\"deriving scrypt identity from password: %q\", out.Secret(pw))\n\tid, err := age.NewScryptIdentity(string(pw))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplaintext, err := a.decrypt(ciphertext, id)\n\tif err != nil {\n\t\tctxutil.GetPasswordPurgeCallback(ctx)(filename)\n\t}\n\n\treturn plaintext, err\n}\n\nfunc (a *Age) getAllIds(ctx context.Context) ([]age.Identity, error) {\n\tids, err := a.getAllIdentities(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidl := make([]age.Identity, 0, len(ids))\n\tfor _, id := range ids {\n\t\tidl = append(idl, id)\n\t}\n\n\treturn idl, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/encrypt.go",
    "content": "package age\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\n\t\"filippo.io/age\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Encrypt will encrypt the given payload.\nfunc (a *Age) Encrypt(ctx context.Context, plaintext []byte, recipients []string) ([]byte, error) {\n\t// add our own public keys to the recipients to ensure we can decrypt it later.\n\tidRecps, err := a.IdentityRecipients(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch identity recipients for encryption: %w\", err)\n\t}\n\t// parse the most specific recipients file and add it to the final\n\t// recipients, too.\n\trecp, err := a.parseRecipients(ctx, recipients)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse recipients file for encryption: %w\", err)\n\t}\n\n\t// dedupe also order recipients so that native ones are first\n\trecp = dedupe(append(recp, idRecps...))\n\n\treturn a.encrypt(plaintext, recp...)\n}\n\n// dedupe the recipients, only works for native age recipients.\nfunc dedupe(recp []age.Recipient) []age.Recipient {\n\tout := make([]age.Recipient, 0, len(recp))\n\tset := make(map[string]age.Recipient, len(recp))\n\tfor _, r := range recp {\n\t\tk, ok := r.(fmt.Stringer)\n\t\t// handle non-native recipients.\n\t\tif !ok {\n\t\t\tout = append(out, r)\n\n\t\t\tcontinue\n\t\t}\n\t\tset[k.String()] = r\n\t}\n\n\tfor _, r := range set {\n\t\tout = append(out, r)\n\t}\n\n\t// we make sure they are sorted so that age1 identities are first,\n\t// because age by default tries to decrypt in the order of the stanzas,\n\t// and if we do have a native identity on our machine, we probably want to\n\t// use that first before using a hardware token which might require a PIN.\n\tslices.SortFunc(out, func(a, b age.Recipient) int {\n\t\ti, oka := a.(fmt.Stringer)\n\t\tj, okb := b.(fmt.Stringer)\n\n\t\t// handle non-native recipients such as SSH, we want them at the bottom\n\t\tif !oka {\n\t\t\treturn -1\n\t\t}\n\t\tif !okb {\n\t\t\treturn -1\n\t\t}\n\t\t// yubikey identities are typically longer\n\t\treturn len(i.String()) - len(j.String())\n\t})\n\tdebug.Log(\"in: %+v - out: %+v\", recp, out)\n\n\treturn out\n}\n\nfunc (a *Age) encrypt(plaintext []byte, recp ...age.Recipient) ([]byte, error) {\n\tout := &bytes.Buffer{}\n\tw, err := age.Encrypt(out, recp...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tn, err := w.Write(plaintext)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := w.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\tdebug.Log(\"Encrypted %d bytes of plaintext to %d bytes of ciphertext for %q\", n, out.Len(), recp)\n\n\treturn out.Bytes(), nil\n}\n\nfunc (a *Age) encryptFile(ctx context.Context, filename string, plaintext []byte, confirm bool) error {\n\tpw, err := ctxutil.GetPasswordCallback(ctx)(filename, confirm)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tid, err := age.NewScryptRecipient(string(pw))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbuf, err := a.encrypt(plaintext, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.WriteFile(filename, buf, 0o600)\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/encrypt_test.go",
    "content": "package age\n\nimport (\n\t\"crypto/ed25519\"\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestDedupe(t *testing.T) {\n\tt.Parallel()\n\n\ti1, err := age.GenerateX25519Identity()\n\trequire.NoError(t, err)\n\n\ti2, err := age.GenerateX25519Identity()\n\trequire.NoError(t, err)\n\n\ti3pub, _, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\ti3ssh, err := ssh.NewPublicKey(i3pub)\n\trequire.NoError(t, err)\n\ti3, err := agessh.NewEd25519Recipient(i3ssh)\n\trequire.NoError(t, err)\n\n\tin := []age.Recipient{i1.Recipient(), i2.Recipient(), i2.Recipient(), i3, i3}\n\tout := dedupe(in)\n\twant := []age.Recipient{i3, i3, i1.Recipient(), i2.Recipient()}\n\n\tsort.Sort(Recipients(out))\n\tsort.Sort(Recipients(want))\n\tassert.Equal(t, want, out)\n}\n\ntype Recipients []age.Recipient\n\nfunc (r Recipients) Len() int {\n\treturn len(r)\n}\n\nfunc (r Recipients) Swap(i, j int) {\n\tr[i], r[j] = r[j], r[i]\n}\n\nfunc (r Recipients) Less(i, j int) bool {\n\treturn fmt.Sprintf(\"%s\", r[i]) < fmt.Sprintf(\"%s\", r[j])\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/identities.go",
    "content": "package age\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"filippo.io/age/plugin\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar idRecpCacheKey = \"identity\"\n\n// wrappedIdentity is a struct that allows us to wrap an `age.Identity` (typically\n// a `plugin.Identity` in order to keep track of its corresponding `age.Recipient`\n// and its bech32 encoding, since the `age`  plugin system doesn't provide a way\n// to easily derive a plugin `Recipient` from a given `Identity`.\n// It is very important to instantiate the recipient when instantiating a\n// wrappedIdentity.\ntype wrappedIdentity struct {\n\tid       age.Identity\n\trec      age.Recipient\n\tencoding string\n}\n\nfunc (w *wrappedIdentity) Recipient() age.Recipient { return w.rec }\nfunc (w *wrappedIdentity) String() string           { return w.encoding }\n\n// SafeStr is implemented in order to avoid logging potentially sensitive data,\n// since an `age.Identity` typically contains secret key material.\nfunc (w *wrappedIdentity) SafeStr() string {\n\tif len(w.encoding) < 12 {\n\t\treturn \"(elided)\"\n\t} else {\n\t\t// we return the first 12 char which are typically \"AGE-PLUGIN-x\" where\n\t\t// x is the first letter of the plugin name\n\t\treturn w.encoding[:12]\n\t}\n}\n\n// Unwrap simply delegates the unwrapping process to its wrapped identity.\nfunc (w *wrappedIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\treturn w.id.Unwrap(stanzas)\n}\n\n// wrappedRecipient is meant to wrap an `age.Recipient`, typically a plugin one,\n// in order to keep track of its corresponding bech32 encoding since plugins don't\n// support deriving a recipient and its encoding from a given identity.\ntype wrappedRecipient struct {\n\trec      age.Recipient\n\tencoding string\n}\n\nfunc (w *wrappedRecipient) String() string { return w.encoding }\n\n// Wrap simply delegates the wrapping process to its wrapped recipient.\nfunc (w *wrappedRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\treturn w.rec.Wrap(fileKey)\n}\n\n// Identities returns all identities, used for decryption.\nfunc (a *Age) Identities(ctx context.Context) ([]age.Identity, error) {\n\tif !ctxutil.HasPasswordCallback(ctx) {\n\t\tdebug.V(1).Log(\"no password callback found, redirecting to askPass\")\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, confirm bool) ([]byte, error) {\n\t\t\tpw, err := a.askPass.Passphrase(prompt, fmt.Sprintf(\"to read the age keyring from %s\", a.identity), confirm)\n\n\t\t\treturn []byte(pw), err\n\t\t})\n\t\tctx = ctxutil.WithPasswordPurgeCallback(ctx, a.askPass.Remove)\n\t}\n\n\tdebug.V(1).Log(\"reading native identities from %s\", a.identity)\n\tbuf, err := a.decryptFile(ctx, a.identity)\n\tif err != nil {\n\t\tdebug.Log(\"failed to decrypt existing identities from %s: %s\", a.identity, err)\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, fmt.Errorf(\"failed to decrypt %s: %w\", a.identity, err)\n\t\t}\n\n\t\treturn nil, nil\n\t}\n\n\tids, err := parseIdentities(bytes.NewReader(buf))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebug.V(1).Log(\"read %d native identities from %s\", len(ids), a.identity)\n\n\treturn ids, nil\n}\n\n// parseIdentity is mostly like `age` parseIdentity, except that it implements\n// our custom format we use with wrapped identities to store the encoding of\n// both the plugin identity and its corresponding recipient.\n// Custom format: `<age identity>\"|\"<age recipient>`\n// This custom format allows us to keep track of a given identity's recipient\n// and prevents us from storing secret identity data in our recipient cache.\nfunc parseIdentity(s string) (age.Identity, error) {\n\tswitch {\n\tcase strings.HasPrefix(s, \"AGE-PLUGIN-\"):\n\t\t// sp will have a length at least 1 and will contain either the full string\n\t\t// or the first part before | and the second part will be in sp[1].\n\t\tsp := strings.Split(s, \"|\")\n\t\tid, err := plugin.NewIdentity(sp[0], pluginTerminalUI)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse plugin identity: %w\", err)\n\t\t}\n\t\tvar rec age.Recipient\n\t\tif len(sp) == 2 {\n\t\t\trec = &wrappedRecipient{\n\t\t\t\trec:      id.Recipient(),\n\t\t\t\tencoding: sp[1],\n\t\t\t}\n\t\t} else {\n\t\t\trec = id.Recipient()\n\t\t}\n\n\t\treturn &wrappedIdentity{\n\t\t\tid:       id,\n\t\t\tencoding: s,\n\t\t\trec:      rec,\n\t\t}, nil\n\tcase strings.HasPrefix(s, \"AGE-SECRET-KEY-1\"):\n\t\tsp := strings.Split(s, \"|\")\n\n\t\treturn age.ParseX25519Identity(sp[0])\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown identity type\")\n\t}\n}\n\n// parseIdentities is like age.ParseIdentities, but supports plugin identities,\n// it is a copy of https://github.com/FiloSottile/age/blob/2214a556f60400ad19f2ca43d3cbbb4a5a0fe5ab/cmd/age/parse.go#L123-L126\nfunc parseIdentities(f io.Reader) ([]age.Identity, error) {\n\tconst privateKeySizeLimit = 1 << 24 // 16 MiB\n\tvar ids []age.Identity\n\tscanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))\n\tvar n int\n\tfor scanner.Scan() {\n\t\tn++\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ti, err := parseIdentity(line)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error at line %d: %w\", n, err)\n\t\t}\n\t\tids = append(ids, i)\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read secret keys file: %w\", err)\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil, fmt.Errorf(\"no secret keys found\")\n\t}\n\n\treturn ids, nil\n}\n\n// IdentityRecipients returns a slice of recipients derived from our identities.\n// Since the identity file is encrypted we try to use a cached copy of the recipients\n// derived from the identities.\nfunc (a *Age) IdentityRecipients(ctx context.Context) ([]age.Recipient, error) {\n\tif ids := a.cachedIDRecipients(); len(ids) > 0 {\n\t\tdebug.Log(\"successfully retrieved identities from cache\")\n\n\t\treturn ids, nil\n\t}\n\n\tids, err := a.Identities(ctx)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tvar r []age.Recipient\n\tfor _, id := range ids {\n\t\tif rec := IdentityToRecipient(id); rec != nil {\n\t\t\tr = append(r, rec)\n\t\t}\n\t}\n\tdebug.Log(\"got %d recipients from %d age identities\", len(r), len(ids))\n\n\tif err := a.recpCache.Set(idRecpCacheKey, recipientsToString(r)); err != nil {\n\t\tdebug.Log(\"failed to cache identity recipients: %s\", err)\n\t}\n\n\treturn r, nil\n}\n\nfunc IdentityToRecipient(id age.Identity) age.Recipient {\n\tswitch id := id.(type) {\n\tcase *age.X25519Identity:\n\t\tdebug.Log(\"parsed age identity as X25519Identity\")\n\n\t\treturn id.Recipient()\n\tcase *wrappedIdentity:\n\t\tdebug.Log(\"parsed age identity as wrappedIdentity\")\n\n\t\treturn id.Recipient()\n\tcase *plugin.Identity:\n\t\tdebug.Log(\"parsed age identity as plugin.Identity\")\n\n\t\treturn id.Recipient()\n\tcase *agessh.RSAIdentity:\n\t\tdebug.Log(\"parsed age identity as RSAIdentity\")\n\n\t\treturn id.Recipient()\n\tcase *agessh.Ed25519Identity:\n\t\tdebug.Log(\"parsed age identity as Ed25519Identity\")\n\n\t\treturn id.Recipient()\n\tcase *agessh.EncryptedSSHIdentity:\n\t\tdebug.Log(\"parsed age identity as encrypted SSHIdentity\")\n\n\t\treturn id.Recipient()\n\tdefault:\n\t\tdebug.Log(\"unexpected age identity type: %T\", id)\n\n\t\treturn nil\n\t}\n}\n\n// GenerateIdentity creates a new identity.\nfunc (a *Age) GenerateIdentity(ctx context.Context, _ string, _ string, pw string) (string, error) {\n\t// we don't check if the password callback is set, since it could only be\n\t// set through an env variable, and here pw can only be set through an\n\t// actual user input.\n\tif pw != \"\" {\n\t\tdebug.Log(\"age GenerateIdentity using provided pw\")\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, confirm bool) ([]byte, error) {\n\t\t\treturn []byte(pw), nil\n\t\t})\n\t}\n\n\tid, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := a.addIdentity(ctx, id); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn id.Recipient().String(), nil\n}\n\n// ListIdentities lists all identities.\nfunc (a *Age) ListIdentities(ctx context.Context) ([]string, error) {\n\tdebug.Log(\"checking existing identities\")\n\tids, err := a.getAllIdentities(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidStr := make([]string, 0, len(ids))\n\tfor k := range ids {\n\t\tidStr = append(idStr, k)\n\t}\n\n\tsort.Strings(idStr)\n\n\treturn idStr, nil\n}\n\n// FindIdentities returns all usable identities (native only).\nfunc (a *Age) FindIdentities(ctx context.Context, keys ...string) ([]string, error) {\n\tids, err := a.IdentityRecipients(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmatches := make([]string, 0, len(ids))\nOUTER:\n\tfor _, k := range keys {\n\t\tfor _, r := range recipientsToString(ids) {\n\t\t\tif r == k {\n\t\t\t\tmatches = append(matches, k)\n\t\t\t\tdebug.Log(\"found matching recipient %s\", k)\n\n\t\t\t\tcontinue OUTER\n\t\t\t}\n\t\t}\n\t\tdebug.Log(\"%s not found in %q\", k, ids)\n\t}\n\n\tsort.Strings(matches)\n\n\treturn matches, nil\n}\n\nfunc (a *Age) cachedIDRecipients() []age.Recipient {\n\tif a.recpCache.ModTime(idRecpCacheKey).Before(modTime(a.identity)) {\n\t\tdebug.Log(\"identity cache expired\")\n\t\tif err := a.recpCache.Remove(idRecpCacheKey); err != nil {\n\t\t\tdebug.Log(\"error invalidating age id recipient cache: %s\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trecps, err := a.recpCache.Get(idRecpCacheKey)\n\tif err != nil {\n\t\tdebug.Log(\"failed to get recipients from cache: %s\", err)\n\n\t\treturn nil\n\t}\n\n\trs, err := a.parseRecipients(context.Background(), recps)\n\tif err != nil {\n\t\tdebug.Log(\"cachedIDRecipients failed to parse some age recipients: %s\", err)\n\t}\n\n\treturn rs\n}\n\nfunc (a *Age) addIdentity(ctx context.Context, id age.Identity) error {\n\t// we invalidate our recipient id cache when we add a new identity\n\tif err := a.recpCache.Remove(idRecpCacheKey); err != nil {\n\t\tdebug.Log(\"error invalidating age id recipient cache: %s\", err)\n\t}\n\n\tids, _ := a.Identities(ctx)\n\n\tids = append(ids, id)\n\n\treturn a.saveIdentities(ctx, identitiesToString(ids), true)\n}\n\nfunc (a *Age) saveIdentities(ctx context.Context, ids []string, newFile bool) error {\n\t// only force a password prompt if running interactively\n\t// TODO: this doesn't really cut it. the purpose is to avoid a password prompt\n\t// from popping up during tests. but no combination of existing flags really\n\t// does convey that correctly. I think we need to cleanup and document the\n\t// different flags conveyed by ctxutil.\n\t//\n\t// Note: if running in a test, we don't want to prompt for a password and just fail.\n\t// Not perfect but we don't support password-less age, yet.\n\t// TODO(#2108): remove this hack\n\tif !ctxutil.HasPasswordCallback(ctx) && !ctxutil.IsAlwaysYes(ctx) {\n\t\tdebug.Log(\"no password callback found, redirecting to askPass\")\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, confirm bool) ([]byte, error) {\n\t\t\tpw, err := a.askPass.Passphrase(prompt, fmt.Sprintf(\"to save the age keyring to %s\", a.identity), confirm)\n\n\t\t\treturn []byte(pw), err\n\t\t})\n\t\tctx = ctxutil.WithPasswordPurgeCallback(ctx, a.askPass.Remove)\n\t}\n\n\t// ensure directory exists.\n\tif err := os.MkdirAll(filepath.Dir(a.identity), 0o700); err != nil {\n\t\tdebug.Log(\"failed to create directory for the keyring at %s: %s\", a.identity, err)\n\n\t\treturn fmt.Errorf(\"failed to create directory for %s: %w\", a.identity, err)\n\t}\n\n\tif err := a.encryptFile(ctx, a.identity, []byte(strings.Join(ids, \"\\n\")), newFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to write encrypted identity to %s: %w\", a.identity, err)\n\t}\n\n\tdebug.Log(\"saved %d identities to %s\", len(ids), a.identity)\n\n\treturn nil\n}\n\nfunc (a *Age) getAllIdentities(ctx context.Context) (map[string]age.Identity, error) {\n\tdebug.V(1).Log(\"checking native identities\")\n\tnative, err := a.getNativeIdentities(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdebug.V(1).Log(\"got %d native identities\", len(native))\n\n\tif IsOnlyNative(ctx) {\n\t\tdebug.V(1).Log(\"returning only native identities\")\n\n\t\treturn native, nil\n\t}\n\n\tdebug.V(1).Log(\"checking ssh identities\")\n\tssh, err := a.getSSHIdentities(ctx)\n\tif err != nil {\n\t\tif errors.Is(err, ErrNoSSHDir) {\n\t\t\treturn native, nil\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tdebug.V(1).Log(\"got %d ssh identities\", len(ssh))\n\n\t// merge both.\n\tmaps.Copy(native, ssh)\n\tdebug.V(1).Log(\"got %d merged identities\", len(native))\n\n\tps, err := a.getPassageIdentities(ctx)\n\tif err != nil {\n\t\tdebug.V(1).Log(\"unable to load passage identities: %s\", err)\n\t}\n\n\t// merge\n\tmaps.Copy(native, ps)\n\n\treturn native, nil\n}\n\nfunc (a *Age) getPassageIdentities(_ context.Context) (map[string]age.Identity, error) {\n\tfn := PassageIDFile()\n\tfh, err := os.Open(fn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open %s: %w\", fn, err)\n\t}\n\tdefer func() { _ = fh.Close() }()\n\n\tids, err := age.ParseIdentities(fh)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// TODO(gh/2059) support encrypted passage identities\n\n\treturn idMap(ids), nil\n}\n\n// PassageIDFile returns the location of the passage identities file.\nfunc PassageIDFile() string {\n\treturn filepath.Join(appdir.UserHome(), \".passage\", \"identities\")\n}\n\nfunc (a *Age) getNativeIdentities(ctx context.Context) (map[string]age.Identity, error) {\n\tids, err := a.Identities(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn idMap(ids), nil\n}\n\nfunc idMap(ids []age.Identity) map[string]age.Identity {\n\tm := make(map[string]age.Identity)\n\tfor _, id := range ids {\n\t\tswitch i := id.(type) {\n\t\tcase *age.X25519Identity:\n\t\t\tm[i.Recipient().String()] = id\n\n\t\t\tcontinue\n\t\tcase *wrappedIdentity:\n\t\t\tm[i.String()] = id\n\n\t\tdefault:\n\t\t\tdebug.Log(\"unknown Identity type: %T\", id)\n\t\t}\n\t}\n\n\treturn m\n}\n\nfunc recipientsToString(recps []age.Recipient) []string {\n\tr := make([]string, 0, len(recps))\n\tfor _, recp := range recps {\n\t\tr = append(r, fmt.Sprintf(\"%s\", recp))\n\t}\n\n\treturn r\n}\n\nfunc identitiesToString(ids []age.Identity) []string {\n\tr := make([]string, 0, len(ids))\n\tfor _, id := range ids {\n\t\tr = append(r, fmt.Sprintf(\"%s\", id))\n\t}\n\n\treturn r\n}\n\nfunc modTime(path string) time.Time {\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\tdebug.Log(\"failed to stat %s: %s\", path, err)\n\n\t\treturn time.Time{}\n\t}\n\n\treturn fi.ModTime()\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/identities_test.go",
    "content": "package age\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseIdentity(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tencoding     string\n\t\texpectedType any\n\t\tshouldFail   bool\n\t}{\n\t\t{\n\t\t\t\"plugin id\",\n\t\t\t\"AGE-PLUGIN-YUBIKEY-1GKZKJQYZL98RLMC67F9PJ\",\n\t\t\t&wrappedIdentity{},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"native age id\",\n\t\t\t\"AGE-SECRET-KEY-1RLNPSS8EV69RL40DKHUFUPU9SNWHUYYJQQMF3ZXQ7S4F3PTPS8EQ2RWVNA\",\n\t\t\t&age.X25519Identity{},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"invalid age id\",\n\t\t\t\"AGE-NONSECRET-KEY-TEST\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"invalid bech32 plugin\",\n\t\t\t\"AGE-PLUGIN-YUBIKEY-1GKKJQYZL98RLM7FJ\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttID, err := parseIdentity(tt.encoding)\n\t\t\tif tt.shouldFail {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.IsType(t, tt.expectedType, tID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentityAndRecipient(t *testing.T) {\n\ttestID, err := age.GenerateX25519Identity()\n\trequire.NoError(t, err)\n\n\tpluginID, err := plugin.NewIdentity(\"AGE-PLUGIN-YUBIKEY-1GKZKJQYZL98RLMC67F9PJ\", nil)\n\trequire.NoError(t, err)\n\tpluginRec, err := plugin.NewRecipient(\"age1yubikey1qt2r3tfk7wvlykudm7ew28dqqm3h8ln9zfsxsq4lcd2w8rh4n4hhz46ur24\", nil)\n\trequire.NoError(t, err)\n\twrRec := &wrappedRecipient{\n\t\trec:      pluginRec,\n\t\tencoding: \"age0yubikey1qt2r3tfk7wvlykudm7ew28dqqm3h8ln9zfsxsq4lcd2w8rh4n4hhz46ur24\",\n\t}\n\ttests := []struct {\n\t\tname string\n\t\tid   age.Identity\n\t\twant age.Recipient\n\t}{\n\t\t{\n\t\t\t\"native identity\",\n\t\t\ttestID,\n\t\t\ttestID.Recipient(),\n\t\t},\n\t\t{\n\t\t\t\"wrapped native id\",\n\t\t\t&wrappedIdentity{\n\t\t\t\tid:       testID,\n\t\t\t\trec:      testID.Recipient(),\n\t\t\t\tencoding: testID.String(),\n\t\t\t},\n\t\t\ttestID.Recipient(),\n\t\t},\n\t\t{\n\t\t\t\"wrapped plugin id\",\n\t\t\t&wrappedIdentity{\n\t\t\t\tid:       pluginID,\n\t\t\t\trec:      wrRec,\n\t\t\t\tencoding: \"AGE-PLUGIN-YUBIKEY-1GKZKJQYZL98RLMC67F9PJ\",\n\t\t\t},\n\t\t\twrRec,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equalf(t, tt.want, IdentityToRecipient(tt.id), \"IdentityToRecipient(%v)\", tt.id)\n\t\t\t// ensure recipient strings aren't equal identity strings\n\t\t\tassert.NotEqual(t, fmt.Sprintf(\"%s\", tt.id), fmt.Sprintf(\"%s\", tt.want))\n\t\t\t// ensure Parsing works on the String of the id:\n\t\t\t_, err = parseIdentity(fmt.Sprintf(\"%s\", tt.id))\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/keyring.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// OldIDFile is the old file name for the recipients.\nvar OldIDFile = \".age-ids\"\n\n// OldKeyringPath is the old file name for the keyring.\n// Must be a func to allow us to honor GOPASS_HOMEDIR in tests.\n// Otherwise it would be read at init time and setting GOPASS_HOMEDIR\n// later would have no effect.\nfunc OldKeyringPath() string {\n\treturn filepath.Join(appdir.UserConfig(), \"age-keyring.age\")\n}\n\nfunc migrate(ctx context.Context, s backend.Storage) error {\n\tout.Noticef(ctx, \"Attempting to migrate age backend. You will need to unlock your identities keyring.\")\n\n\tif s.Exists(ctx, OldIDFile) && s.Exists(ctx, IDFile) {\n\t\tout.Warningf(ctx, \"Both %s and %s exist. Removing the old one (%s).\", OldIDFile, IDFile, OldIDFile)\n\t\tif err := s.Delete(ctx, OldIDFile); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to remove %s: %s\", OldIDFile, err)\n\t\t} else {\n\t\t\tout.OKf(ctx, \"Removed the old IDFile at %s\", OldIDFile)\n\t\t}\n\t}\n\n\tif s.Exists(ctx, OldIDFile) {\n\t\tout.Noticef(ctx, \"Found %s. Migrating to %s.\", OldIDFile, IDFile)\n\t\tbuf, err := s.Get(ctx, OldIDFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := s.Set(ctx, IDFile, buf); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to rename %s to %s: %s\", OldIDFile, IDFile, err)\n\t\t}\n\n\t\tdebug.Log(\"Renamed the old IDFile at %s to %s\", OldIDFile, IDFile)\n\t} else {\n\t\tdebug.Log(\"Old IDFile %s does not exist, nothing to do\", OldIDFile)\n\t}\n\n\t// create a new instance so we can use decryptFile.\n\ta, err := New(ctx, config.String(ctx, \"age.ssh-key-path\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toldKeyring := OldKeyringPath()\n\tif !ctxutil.HasPasswordCallback(ctx) {\n\t\tdebug.Log(\"no password callback found, redirecting to askPass\")\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\t\tpw, err := a.askPass.Passphrase(prompt, fmt.Sprintf(\"to load the age keyring at %s\", oldKeyring), false)\n\n\t\t\treturn []byte(pw), err\n\t\t})\n\t\tctx = ctxutil.WithPasswordPurgeCallback(ctx, a.askPass.Remove)\n\t}\n\n\tif fsutil.IsFile(oldKeyring) && fsutil.IsFile(a.identity) {\n\t\tout.Warningf(ctx, \"Both %s and %s exist. Keeping both. Recover any identities from %s as needed.\", oldKeyring, a.identity, oldKeyring)\n\n\t\treturn nil\n\t}\n\tif !fsutil.IsFile(oldKeyring) {\n\t\tdebug.Log(\"old keyring %s does not exist, nothing to do\", oldKeyring)\n\n\t\t// nothing to do.\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"loading old identities from %s\", oldKeyring)\n\tids, err := a.loadIdentitiesFromKeyring(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"writing new identities to %s\", a.identity)\n\tif err := a.saveIdentities(ctx, ids, false); err != nil {\n\t\treturn err\n\t}\n\n\treturn os.Remove(oldKeyring)\n}\n\n// Keyring is an age keyring.\n// Deprecated: Only used for backwards compatibility. Will be removed soon.\ntype Keyring []Keypair\n\n// Keypair is a public / private keypair.\n// Deprecated: Only used for backwards compatibility. Will be removed soon.\ntype Keypair struct {\n\tName     string `json:\"name\"`\n\tEmail    string `json:\"email\"`\n\tIdentity string `json:\"identity\"`\n}\n\nfunc (a *Age) loadIdentitiesFromKeyring(ctx context.Context) ([]string, error) {\n\toldKeyring := OldKeyringPath()\n\tbuf, err := a.decryptFile(ctx, oldKeyring)\n\tif err != nil {\n\t\tdebug.Log(\"can't decrypt keyring at %s: %s\", oldKeyring, err)\n\n\t\treturn nil, fmt.Errorf(\"can not decrypt old keyring at %s: %w\", oldKeyring, err)\n\t}\n\n\tvar kr Keyring\n\tif err := json.Unmarshal(buf, &kr); err != nil {\n\t\tdebug.Log(\"can't parse keyring at %s: %s\", oldKeyring, err)\n\n\t\treturn nil, fmt.Errorf(\"can not parse old keyring at %s: %w\", oldKeyring, err)\n\t}\n\n\t// remove invalid IDs.\n\tvalid := make([]string, 0, len(kr))\n\tfor _, k := range kr {\n\t\tif k.Identity == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tvalid = append(valid, k.Identity)\n\t}\n\tdebug.Log(\"loaded keyring with %d valid entries from %s\", len(kr), oldKeyring)\n\n\treturn valid, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/loader.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nconst (\n\tname = \"age\"\n)\n\nfunc init() {\n\tbackend.CryptoRegistry.Register(backend.Age, name, &loader{})\n}\n\ntype loader struct{}\n\nfunc (l loader) New(ctx context.Context) (backend.Crypto, error) {\n\tdebug.Log(\"Using Crypto Backend: %s\", name)\n\n\treturn New(ctx, config.String(ctx, \"age.ssh-key-path\"))\n}\n\nfunc (l loader) Handles(ctx context.Context, s backend.Storage) error {\n\t// OldKeyring is meant to be in the config folder, not necessarily in the store\n\toldKeyring := OldKeyringPath()\n\tif s.Exists(ctx, OldIDFile) || fsutil.IsNonEmptyFile(oldKeyring) {\n\t\tdebug.Log(\"Starting migration of age backend. Found ID File at %s = %t. Migrating to %s. Found Keyring at %s = %t\", OldIDFile, s.Exists(ctx, OldIDFile), IDFile, oldKeyring, fsutil.IsNonEmptyFile(oldKeyring))\n\n\t\tif err := migrate(ctx, s); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to migrate age backend: %s\", err)\n\n\t\t\treturn err\n\t\t}\n\t\tout.OKf(ctx, \"Migrated age backend to new format\")\n\t}\n\tif s.Exists(ctx, IDFile) {\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"No age ID file %q found in %s\", IDFile, s.Path())\n\n\treturn fmt.Errorf(\"not supported\")\n}\n\nfunc (l loader) Priority() int {\n\treturn 10\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/loader_test.go",
    "content": "package age\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/store/mockstore/inmem\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLoader_New(t *testing.T) {\n\tctx := t.Context()\n\tl := loader{}\n\n\tcrypto, err := l.New(ctx)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, crypto)\n}\n\nfunc TestLoader_Handles(t *testing.T) {\n\tctx := t.Context()\n\tl := loader{}\n\ts := inmem.New()\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t// Test case where OldIDFile or OldKeyring exists\n\tt.Run(\"OldIDFile or OldKeyring exists\", func(t *testing.T) {\n\t\trequire.NoError(t, s.Set(ctx, OldIDFile, []byte(\"test\")))\n\t\terr := l.Handles(ctx, s)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, s.Delete(ctx, OldIDFile))\n\t})\n\n\t// Test case where IDFile exists\n\tt.Run(\"IDFile exists\", func(t *testing.T) {\n\t\trequire.NoError(t, s.Set(ctx, OldIDFile, []byte(\"test\")))\n\t\trequire.NoError(t, s.Set(ctx, IDFile, []byte(\"test\")))\n\t\terr := l.Handles(ctx, s)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, s.Delete(ctx, OldIDFile))\n\t\trequire.NoError(t, s.Delete(ctx, IDFile))\n\t})\n\n\t// Test case where IDFile exists\n\tt.Run(\"IDFile exists\", func(t *testing.T) {\n\t\trequire.NoError(t, s.Set(ctx, IDFile, []byte(\"test\")))\n\t\terr := l.Handles(ctx, s)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, s.Delete(ctx, IDFile))\n\t})\n\n\t// Test case where neither OldIDFile nor IDFile exists\n\tt.Run(\"neither OldIDFile nor IDFile exists\", func(t *testing.T) {\n\t\terr := l.Handles(ctx, s)\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestLoader_Priority(t *testing.T) {\n\tl := loader{}\n\tassert.Equal(t, 10, l.Priority())\n}\n\nfunc TestLoader_String(t *testing.T) {\n\tl := loader{}\n\tassert.Equal(t, name, l.String())\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/recipients.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"filippo.io/age/plugin\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// FindRecipients returns all list of usable recipient key IDs matching the search strings.\n// For native age keys this is a no-op since they are self-contained (i.e. the ID is the full key already).\n// But for SSH keys, especially GitHub indirections, an extra step is necessary.\nfunc (a *Age) FindRecipients(ctx context.Context, search ...string) ([]string, error) {\n\trs := set.New[string]()\n\n\tfor _, key := range search {\n\t\tswitch {\n\t\tcase strings.HasPrefix(key, \"github:\"):\n\t\t\t// look up any \"github:<username>\" style public SSH keys\n\t\t\tpks, err := a.ghCache.ListKeys(ctx, strings.TrimPrefix(key, \"github:\"))\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"Failed to get key %s from github: %s\", key, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trs.Add(pks...)\n\t\tcase strings.HasPrefix(key, \"ssh-\"):\n\t\t\t// add ssh public keys as-is\n\t\t\trs.Add(key)\n\t\tcase strings.HasPrefix(key, \"age1\"):\n\t\t\t// add any regular age public keys as-is\n\t\t\trs.Add(key)\n\t\tdefault:\n\t\t\tdebug.Log(\"ignoring unknown key: %s\", key)\n\t\t}\n\t}\n\n\tdebug.Log(\"found usable keys for %q: %q \", search, rs)\n\n\treturn rs.Elements(), nil\n}\n\nfunc (a *Age) parseRecipients(ctx context.Context, recipients []string) ([]age.Recipient, error) {\n\tret := make([]age.Recipient, 0, len(recipients))\n\tfor _, r := range recipients {\n\t\tswitch {\n\t\tcase strings.HasPrefix(r, \"age1\"):\n\t\t\tid, err := age.ParseX25519Recipient(r)\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"Failed to parse recipient %q as X25519: %s\", r, err)\n\n\t\t\t\tpid, err := plugin.NewRecipient(r, pluginTerminalUI)\n\t\t\t\tif err != nil {\n\t\t\t\t\tdebug.Log(\"Failed to parse recipient %q as an age plugin: %s\", out.Secret(r), err)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tret = append(ret, &wrappedRecipient{rec: pid, encoding: r})\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tret = append(ret, id)\n\n\t\tcase strings.HasPrefix(r, \"ssh-\"):\n\t\t\tid, err := agessh.ParseRecipient(r)\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"Failed to parse recipient %q as SSH: %s\", r, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tret = append(ret, id)\n\n\t\tcase strings.HasPrefix(r, \"github:\"):\n\t\t\tout.Warning(ctx, \"github recipient support has been removed from age, consider switching to native keys\")\n\t\t\tpks, err := a.ghCache.ListKeys(ctx, strings.TrimPrefix(r, \"github:\"))\n\t\t\tif err != nil {\n\t\t\t\treturn ret, err\n\t\t\t}\n\t\t\tfor _, pk := range pks {\n\t\t\t\tid, err := agessh.ParseRecipient(pk)\n\t\t\t\tif err != nil {\n\t\t\t\t\tdebug.Log(\"Failed to parse GitHub recipient %q for key %q: %s\", r, pk, err)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tret = append(ret, id)\n\t\t\t}\n\n\t\t// a special case to support the case where plugin users decided to use the plugin identity itself as a recipient\n\t\t// when running the `gopass age identities add` command.\n\t\tcase strings.HasPrefix(r, \"AGE-PLUGIN\"):\n\t\t\tpid, err := plugin.NewIdentity(r, pluginTerminalUI)\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"Failed to parse identity as an age plugin: %s\", err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tret = append(ret, &wrappedRecipient{rec: pid.Recipient(), encoding: r})\n\n\t\tdefault:\n\t\t\tdebug.Log(\"Unknown age recipient %q failed parsing\", out.Secret(r))\n\t\t}\n\t}\n\n\treturn ret, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/recipients_test.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFindRecipients(t *testing.T) {\n\tctx := t.Context()\n\ta := &Age{\n\t\tghCache: &mockGHCache{},\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tsearch   []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"github key\",\n\t\t\tsearch:   []string{\"github:username\"},\n\t\t\texpected: []string{\"ssh-rsa AAAAB3Nza...\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"ssh key\",\n\t\t\tsearch:   []string{\"ssh-rsa AAAAB3Nza...\"},\n\t\t\texpected: []string{\"ssh-rsa AAAAB3Nza...\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"age key\",\n\t\t\tsearch:   []string{\"age1qxy2z...\"},\n\t\t\texpected: []string{\"age1qxy2z...\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown key\",\n\t\t\tsearch:   []string{\"unknown:key\"},\n\t\t\texpected: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trecipients, err := a.FindRecipients(ctx, tt.search...)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, recipients)\n\t\t})\n\t}\n}\n\nfunc TestParseRecipients(t *testing.T) {\n\tctx := t.Context()\n\ta := &Age{\n\t\tghCache: &mockGHCache{},\n\t}\n\n\t// both age and ssh keys are valid, throw away keys generated for this test case.\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"valid age key\",\n\t\t\tinput:    []string{\"age1zf3t7aw2rv39fmcddc469nhtj6lm22kn5kh0gy4fv3a7ds3r29rsr69l89\"},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid ssh key\",\n\t\t\tinput:    []string{\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDHnOMnKBLlwDOHWh0EEk8r/VdeLaPe7hOwdS/040YVU04PXG7U0YFoXRe1GP6KtM0SrIOIl2S3QrCc7m1cwZSBBPE3rVprrCShaIG2Wn57nTPv2kb9Qtqlc8nMXBOKITCfLmtuzN39n7E7T0EZGrThocrvcNCsPLdrc8Nd0I+eVidgN215DeWhDB4X0pJmScMRSWOmFgnPEPBpDcHvly9wTT+Iv8V7mvGiVKYBHFBA73lCpLS1+LWa+7GXJkKsLbZtBgOQKj9txmwRMkQCecrBAN3z5skdAQc1XPTc3Nihzw6FnPAe69hmjgVl8YTSdmojxbpaJwLvpkR9/Gv5w9ZH/VYM2lhmhCoXTVTLWDGIbxEG3tjEhB7dfVVEcLRod33X2f1LIzhC5lW+dIwVV9IprJooCAtNnHy06DNpQNE/2YTTjCtUSx+DX+ZLEHaGQ2QXlaARXnUfNgM+ct8VAGRL/UkQnqGDE7NgQ4U6JfsohWfR8QXrEkAvLzctmw2AHc8=\"},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown key\",\n\t\t\tinput:    []string{\"unknown:key\"},\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trecipients, err := a.parseRecipients(ctx, tt.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, recipients, tt.expected)\n\t\t})\n\t}\n}\n\ntype mockGHCache struct{}\n\nfunc (m *mockGHCache) ListKeys(ctx context.Context, user string) ([]string, error) {\n\treturn []string{\"ssh-rsa AAAAB3Nza...\"}, nil\n}\n\nfunc (m *mockGHCache) String() string {\n\treturn \"mockGHCache\"\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/ssh.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nvar (\n\tsshCache map[string]age.Identity\n\t// ErrNoSSHDir signals that no SSH dir was found. Callers\n\t// are usually expected to ignore this.\n\tErrNoSSHDir = errors.New(\"no ssh directory\")\n)\n\n// getSSHIdentities returns all SSH identities available for the current user.\nfunc (a *Age) getSSHIdentities(ctx context.Context) (map[string]age.Identity, error) {\n\tif sshCache != nil {\n\t\tdebug.Log(\"using sshCache\")\n\n\t\treturn sshCache, nil\n\t}\n\n\tids := make(map[string]age.Identity, 10) // preallocate some space for the cache\n\tsshDirs := make([]string, 0, 2)\n\n\tsshDir, err := getSSHDir()\n\tif err != nil {\n\t\tdebug.Log(\"no .ssh directory found at %s.\", sshDir)\n\t}\n\tif sshDir != \"\" {\n\t\tdebug.Log(\"found .ssh directory at %s\", sshDir)\n\t\tsshDirs = append(sshDirs, sshDir)\n\t}\n\t// also check the SSH key path, if set\n\tif a.sshKeyPath != \"\" { //nolint:nestif\n\t\tdebug.Log(\"using custom SSH key path %s\", a.sshKeyPath)\n\t\tif fsutil.IsDir(a.sshKeyPath) {\n\t\t\tsshDirs = append(sshDirs, a.sshKeyPath)\n\t\t} else if fsutil.IsFile(a.sshKeyPath) {\n\t\t\tdebug.Log(\"using custom SSH key file %s\", a.sshKeyPath)\n\t\t\trecp, id, err := a.parseSSHIdentity(ctx, a.sshKeyPath)\n\t\t\tif err != nil {\n\t\t\t\tdebug.Log(\"unable to parse custom SSH key %s: %s\", a.sshKeyPath, err)\n\t\t\t} else {\n\t\t\t\tdebug.Log(\"found custom SSH identity %s\", recp)\n\t\t\t\tids[recp] = id\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(sshDirs) < 1 {\n\t\treturn nil, fmt.Errorf(\"no SSH identities found: %w\", ErrNoSSHDir)\n\t}\n\n\tdebug.Log(\"searching for SSH identities in %d directories: %s\", len(sshDirs), strings.Join(sshDirs, \", \"))\n\n\tfor _, sshDir := range sshDirs {\n\t\tdebug.Log(\"searching for SSH identities in %s\", sshDir)\n\t\tfiles, err := os.ReadDir(sshDir)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"unable to read .ssh dir %s: %s\", sshDir, err)\n\n\t\t\treturn nil, fmt.Errorf(\"no identities found: %w\", ErrNoSSHDir)\n\t\t}\n\n\t\tfor _, file := range files {\n\t\t\tfn := filepath.Join(sshDir, file.Name())\n\t\t\tif !strings.HasSuffix(fn, \".pub\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trecp, id, err := a.parseSSHIdentity(ctx, fn)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tids[recp] = id\n\t\t}\n\t}\n\tsshCache = ids\n\tdebug.Log(\"returned %d SSH Identities\", len(ids))\n\n\treturn ids, nil\n}\n\nfunc getSSHDir() (string, error) {\n\tpreferredPath := os.Getenv(\"GOPASS_SSH_DIR\")\n\tsshDir := filepath.Join(preferredPath, \".ssh\")\n\tif preferredPath != \"\" && fsutil.IsDir(sshDir) {\n\t\treturn preferredPath, nil\n\t}\n\n\t// notice that this respects the GOPASS_HOMEDIR env variable, and won't\n\t// find a .ssh folder in your home directory if you set GOPASS_HOMEDIR\n\tuhd := appdir.UserHome()\n\tsshDir = filepath.Join(uhd, \".ssh\")\n\tif fsutil.IsDir(sshDir) {\n\t\treturn sshDir, nil\n\t}\n\n\treturn \"\", ErrNoSSHDir\n}\n\n// parseSSHIdentity parses a SSH public key file and returns the recipient and the identity.\nfunc (a *Age) parseSSHIdentity(ctx context.Context, pubFn string) (string, age.Identity, error) {\n\tprivFn := strings.TrimSuffix(pubFn, \".pub\")\n\t_, err := os.Stat(privFn)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tpubBuf, err := os.ReadFile(pubFn)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tprivBuf, err := os.ReadFile(privFn)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tpubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubBuf) //nolint:dogsled\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\trecp := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(pubkey)), \"\\n\")\n\tid, err := agessh.ParseIdentity(privBuf)\n\tif err != nil {\n\t\t// handle encrypted SSH identities here.\n\t\tvar perr *ssh.PassphraseMissingError\n\t\tif errors.As(err, &perr) {\n\t\t\tid, err := agessh.NewEncryptedSSHIdentity(pubkey, privBuf, func() ([]byte, error) {\n\t\t\t\treturn ctxutil.GetPasswordCallback(ctx)(pubFn, false)\n\t\t\t})\n\n\t\t\treturn recp, id, err\n\t\t}\n\n\t\treturn \"\", nil, err\n\t}\n\n\treturn recp, id, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/age/unsupported.go",
    "content": "package age\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// FormatKey returns the key id.\nfunc (a *Age) FormatKey(ctx context.Context, id, tpl string) string {\n\treturn id\n}\n\n// Fingerprint returns the id.\nfunc (a *Age) Fingerprint(ctx context.Context, id string) string {\n\treturn id\n}\n\n// ListRecipients is not supported for the age backend.\nfunc (a *Age) ListRecipients(context.Context) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not implemented\")\n}\n\n// ReadNamesFromKey is not supported for the age backend.\nfunc (a *Age) ReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not implemented\")\n}\n\n// RecipientIDs is not supported for the age backend.\nfunc (a *Age) RecipientIDs(ctx context.Context, buf []byte) ([]string, error) {\n\treturn nil, fmt.Errorf(\"reading recipient IDs is not supported by the age backend by design\")\n}\n"
  },
  {
    "path": "internal/backend/crypto/age.go",
    "content": "package crypto\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/crypto/age\" // registers age backend\n"
  },
  {
    "path": "internal/backend/crypto/doc.go",
    "content": "// Package crypto provides a pluggable crypto backend for gopass.\n\npackage crypto\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/decrypt.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Decrypt will try to decrypt the given file.\nfunc (g *GPG) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {\n\tctx, cancel := context.WithTimeout(ctx, Timeout)\n\tdefer cancel()\n\n\targs := append(g.args, \"--decrypt\")\n\t// Useful information may appear there\n\tif debug.IsEnabled() {\n\t\targs = append(args, \"--verbose\", \"--verbose\")\n\t}\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\tcmd.Stdin = bytes.NewReader(ciphertext)\n\t// If gopass-jsonapi is used, there is no way to reach this os.Stderr, so\n\t// we write this stderr to the log file as well.\n\tcmd.Stderr = io.MultiWriter(os.Stderr, debug.LogWriter)\n\n\tdebug.V(1).Log(\"Running %s %+v\", cmd.Path, cmd.Args)\n\tstdout, err := cmd.Output()\n\tif err != nil {\n\t\tdebug.Log(\"GPG decrypt failed: %s %+v: %+v\", cmd.Path, cmd.Args, err)\n\t}\n\n\treturn stdout, err\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/encrypt.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Encrypt will encrypt the given content for the recipients. If alwaysTrust is true\n// the trust-model will be set to always as to avoid (annoying) \"unusable public key\"\n// errors when encrypting.\nfunc (g *GPG) Encrypt(ctx context.Context, plaintext []byte, recipients []string) ([]byte, error) {\n\tctx, cancel := context.WithTimeout(ctx, Timeout)\n\tdefer cancel()\n\n\targs := append(g.args, \"--encrypt\")\n\tif gpg.IsAlwaysTrust(ctx) {\n\t\t// changing the trustmodel is possibly dangerous. A user should always\n\t\t// explicitly opt-in to do this\n\t\targs = append(args, \"--trust-model=always\")\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tif len(recipients) == 0 {\n\t\treturn buf.Bytes(), errors.New(\"recipients list is empty\")\n\t}\n\tvar badRecipients []string\n\tfor _, r := range recipients {\n\t\tkl, err := g.listKeys(ctx, \"public\", r)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"Failed to check key %s. Adding anyway. %s\", err)\n\t\t} else if len(kl.UseableKeys(gpg.IsAlwaysTrust(ctx))) < 1 {\n\t\t\tbadRecipients = append(badRecipients, r)\n\t\t\terrmsg := fmt.Sprintf(\"Not using invalid key %q for encryption. Check its expiration date, its encryption capabilities and trust.\", r)\n\t\t\tdebug.Log(errmsg)\n\t\t\tout.Printf(ctx, errmsg)\n\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"adding recipient %s\", r)\n\t\targs = append(args, \"--recipient\", r)\n\t}\n\tif len(badRecipients) == len(recipients) {\n\t\treturn buf.Bytes(), errors.New(\"no valid and trusted recipients were found\")\n\t}\n\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\tcmd.Stdin = bytes.NewReader(plaintext)\n\t// the encrypted blob as an hexdump and errors are printed to the log file, and to stdout\n\thexLogger := hex.Dumper(debug.LogWriter)\n\tcmd.Stdout = io.MultiWriter(buf, hexLogger)\n\tcmd.Stderr = io.MultiWriter(os.Stderr, debug.LogWriter)\n\n\tdebug.V(1).Log(\"%s %+v\", cmd.Path, cmd.Args)\n\terr := cmd.Run()\n\t_ = hexLogger.Close()\n\tif err != nil {\n\t\tdebug.Log(\"GPG encrypt failed: %s %+v: %+v\", cmd.Path, cmd.Args, err)\n\t}\n\n\treturn buf.Bytes(), err\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/encrypt_test.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/tests/can\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEncryptDecrypt(t *testing.T) {\n\tif testing.Short() || runtime.GOOS != \"linux\" { // not working on darwin right now, can't test on windows\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\t// necessary for setting up the env\n\tu := gptest.NewGUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\n\tg, err := New(ctx, Config{\n\t\tUmask: fsutil.Umask(),\n\t\tArgs:  gpgconf.GPGOpts(),\n\t})\n\trequire.NoError(t, err)\n\n\t// import keys so GPG4Win can find them\n\tel := can.EmbeddedKeyRing()\n\tfor _, e := range el {\n\t\tbuf := &bytes.Buffer{}\n\t\trequire.NoError(t, e.Serialize(buf))\n\n\t\trequire.NoError(t, g.ImportPublicKey(ctx, buf.Bytes()))\n\t}\n\n\tplaintext := []byte(\"plaintext\")\n\tciphertext, err := g.Encrypt(ctx, plaintext, []string{can.KeyID()})\n\trequire.NoError(t, err)\n\n\tplaintext2, err := g.Decrypt(ctx, ciphertext)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, plaintext, plaintext2)\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/generate.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"regexp\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar gpgRevocsRE = regexp.MustCompile(`.*/openpgp-revocs.d/([0-9A-F]{40})\\.rev`)\n\n// GenerateIdentity will create a new GPG keypair in batch mode.\nfunc (g *GPG) GenerateIdentity(ctx context.Context, name, email, passphrase string) (string, error) {\n\tbuf := &bytes.Buffer{}\n\t// https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;h=de0f21ccba60c3037c2a155156202df1cd098507;hb=refs/heads/STABLE-BRANCH-1-4#l716\n\t_, _ = buf.WriteString(`%echo Generating a RSA/RSA key pair\nKey-Type: RSA\nKey-Length: 2048\nSubkey-Type: RSA\nSubkey-Length: 2048\nExpire-Date: 0\n`)\n\t_, _ = buf.WriteString(\"Name-Real: \" + name + \"\\n\")\n\t_, _ = buf.WriteString(\"Name-Email: \" + email + \"\\n\")\n\t_, _ = buf.WriteString(\"Passphrase: \" + passphrase + \"\\n\")\n\n\targs := []string{\"--batch\", \"--gen-key\"}\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\tcmd.Stdin = bytes.NewReader(buf.Bytes())\n\n\tout := &bytes.Buffer{}\n\tcmd.Stdout = out\n\tcmd.Stderr = out\n\n\tdebug.Log(\"%s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to run command: '%s %+v': %q - %w\", cmd.Path, cmd.Args, out.String(), err)\n\t}\n\n\tg.privKeys = nil\n\tg.pubKeys = nil\n\n\t// try to parse key id from the output\n\tfor line := range bytes.SplitSeq(out.Bytes(), []byte{'\\n'}) {\n\t\tif !gpgRevocsRE.Match(line) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatches := gpgRevocsRE.FindSubmatch(line)\n\t\tif len(matches) == 2 {\n\t\t\tkeyID := string(matches[1])\n\t\t\tdebug.Log(\"Generated new GPG key: %s\", keyID)\n\n\t\t\treturn keyID, nil\n\t\t}\n\t}\n\n\t// Ignoring this failure since we're usually not using the Key ID directly.\n\tdebug.Log(\"Failed to find key ID in output: %q\", out.String())\n\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/gpg.go",
    "content": "// Package cli implements a GPG CLI crypto backend.\npackage cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\tlru \"github.com/hashicorp/golang-lru/v2\"\n)\n\nvar (\n\t// defaultArgs contains the default GPG args for non-interactive use. Note: Do not use '--batch'\n\t// as this will disable (necessary) passphrase questions!\n\tdefaultArgs = []string{\"--quiet\", \"--yes\", \"--compress-algo=none\", \"--no-encrypt-to\", \"--no-auto-check-trustdb\"}\n\t// Ext is the file extension used by this backend.\n\tExt = \"gpg\"\n\t// IDFile is the name of the recipients file used by this backend.\n\tIDFile = \".gpg-id\"\n\t// Name is the name of this backend.\n\tName = \"gpg\"\n\t// Timeout is the time allow for gpg invocations to complete.\n\tTimeout = time.Minute\n)\n\n// GPG is a gpg wrapper.\ntype GPG struct {\n\tbinary    string\n\targs      []string\n\tpubKeys   gpg.KeyList\n\tprivKeys  gpg.KeyList\n\tlistCache *lru.TwoQueueCache[string, gpg.KeyList]\n\tthrowKids bool\n}\n\n// Config is the gpg wrapper config.\ntype Config struct {\n\tBinary string\n\tArgs   []string\n\tUmask  int\n}\n\n// New creates a new GPG wrapper.\nfunc New(ctx context.Context, cfg Config) (*GPG, error) {\n\t// ensure created files don't have group or world perms set\n\t// this setting should be inherited by sub-processes\n\tgpgconf.Umask(cfg.Umask)\n\n\t// make sure GPG_TTY is set (if possible)\n\tif gt := os.Getenv(\"GPG_TTY\"); gt == \"\" {\n\t\tif t := gpgconf.TTY(); t != \"\" {\n\t\t\t_ = os.Setenv(\"GPG_TTY\", t)\n\t\t}\n\t}\n\n\tgcfg, err := gpgconf.Config()\n\tif err != nil {\n\t\tdebug.Log(\"failed to read GPG config: %s\", err)\n\t}\n\t_, hasThrowKids := gcfg[\"throw-keyids\"]\n\n\tg := &GPG{\n\t\tbinary:    \"gpg\",\n\t\targs:      append(defaultArgs, cfg.Args...),\n\t\tthrowKids: hasThrowKids,\n\t}\n\n\tcache, err := lru.New2Q[string, gpg.KeyList](1024)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize the LRU cache: %w\", err)\n\t}\n\tg.listCache = cache\n\n\tbin, err := gpgconf.Binary(ctx, cfg.Binary)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to detect binary: %w\", err)\n\t}\n\n\tg.binary = bin\n\tdebug.Log(\"binary detected as %s\", bin)\n\n\treturn g, nil\n}\n\n// Initialized always returns nil.\nfunc (g *GPG) Initialized(ctx context.Context) error {\n\treturn nil\n}\n\n// Name returns gpg.\nfunc (g *GPG) Name() string {\n\treturn Name\n}\n\n// Ext returns gpg.\nfunc (g *GPG) Ext() string {\n\treturn Ext\n}\n\n// IDFile returns .gpg-id.\nfunc (g *GPG) IDFile() string {\n\treturn IDFile\n}\n\n// Concurrency returns 1 to avoid concurrency issues\n// with many GPG setups.\nfunc (g *GPG) Concurrency() int {\n\treturn 1\n}\n\n// Binary returns the GPG binary location.\nfunc (g *GPG) Binary() string {\n\tif g == nil {\n\t\treturn \"\"\n\t}\n\n\treturn g.binary\n}\n\n// String implements fmt.Stringer.\nfunc (g *GPG) String() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"gpgcli(\")\n\tif g == nil {\n\t\tsb.WriteString(\"<nil>)\")\n\n\t\treturn sb.String()\n\t}\n\tsb.WriteString(\"binary:\")\n\tsb.WriteString(g.binary)\n\tsb.WriteString(\",args: [\")\n\tsb.WriteString(strings.Join(g.args, \" \"))\n\tsb.WriteString(\"])\")\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/gpg_others_test.go",
    "content": "//go:build !windows\n\npackage cli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEncrypt(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tg := &GPG{}\n\tg.binary = \"true\"\n\n\t_, err := g.Encrypt(ctx, []byte(\"foo\"), nil)\n\t// No recipients are configured so it will fail\n\trequire.Error(t, err)\n}\n\nfunc TestDecrypt(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tg := &GPG{}\n\tg.binary = \"true\"\n\n\t_, err := g.Decrypt(ctx, []byte(\"foo\"))\n\trequire.NoError(t, err)\n}\n\nfunc TestGenerateIdentity(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tg := &GPG{}\n\tg.binary = \"true\"\n\n\t_, err := g.GenerateIdentity(ctx, \"foo\", \"foo@bar.com\", \"bar\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/gpg_test.go",
    "content": "package cli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGPG(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GNUPGHOME\", td)\n\n\tctx := config.NewContextInMemory()\n\n\tvar err error\n\tvar g *GPG\n\n\tassert.Empty(t, g.Binary())\n\n\tg, err = New(ctx, Config{})\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, g.Binary())\n\n\t_, err = g.ListRecipients(ctx)\n\trequire.NoError(t, err)\n\n\t_, err = g.ListIdentities(ctx)\n\trequire.NoError(t, err)\n\n\t_, err = g.RecipientIDs(ctx, []byte{})\n\trequire.Error(t, err)\n\n\trequire.NoError(t, g.Initialized(ctx))\n\tassert.Equal(t, \"gpg\", g.Name())\n\tassert.Equal(t, \"gpg\", g.Ext())\n\tassert.Equal(t, \".gpg-id\", g.IDFile())\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/gpg_windows_test.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEncrypt(t *testing.T) {\n\tt.Parallel()\n\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\n\tg := &GPG{}\n\tg.binary = \"rundll32\"\n\n\t_, err := g.Encrypt(ctx, []byte(\"foo\"), nil)\n\n\t// No recipients are configured so it will fail\n\trequire.Error(t, err)\n\tcancel()\n}\n\nfunc TestDecrypt(t *testing.T) {\n\tt.Parallel()\n\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\n\tg := &GPG{}\n\tg.binary = \"rundll32\"\n\n\t_, err := g.Decrypt(ctx, []byte(\"foo\"))\n\trequire.NoError(t, err)\n\tcancel()\n}\n\nfunc TestGenerateIdentity(t *testing.T) {\n\tt.Parallel()\n\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\n\tg := &GPG{}\n\tg.binary = \"rundll32\"\n\n\t_, err := g.GenerateIdentity(ctx, \"foo\", \"foo@bar.com\", \"bar\")\n\trequire.NoError(t, err)\n\tcancel()\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/identities.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// ListIdentities returns a parsed list of GPG secret keys.\nfunc (g *GPG) ListIdentities(ctx context.Context) ([]string, error) {\n\tif g.privKeys == nil {\n\t\tkl, err := g.listKeys(ctx, \"secret\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tg.privKeys = kl\n\t}\n\n\tif gpg.IsAlwaysTrust(ctx) {\n\t\treturn g.privKeys.Recipients(), nil\n\t}\n\n\treturn g.privKeys.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients(), nil\n}\n\n// FindIdentities searches for the given private keys.\nfunc (g *GPG) FindIdentities(ctx context.Context, search ...string) ([]string, error) {\n\tkl, err := g.listKeys(ctx, \"secret\", search...)\n\tif err != nil || kl == nil {\n\t\treturn nil, err\n\t}\n\n\tif gpg.IsAlwaysTrust(ctx) {\n\t\treturn kl.Recipients(), nil\n\t}\n\n\treturn kl.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients(), nil\n}\n\nfunc (g *GPG) findKey(ctx context.Context, id string) (gpg.Key, bool) {\n\tdebug.Log(\"finding key %q\", id)\n\tkl, _ := g.listKeys(ctx, \"secret\", id)\n\tif len(kl) >= 1 {\n\t\treturn kl[0], true\n\t}\n\n\tkl, _ = g.listKeys(ctx, \"public\", id)\n\tif len(kl) >= 1 {\n\t\treturn kl[0], true\n\t}\n\n\treturn gpg.Key{\n\t\tFingerprint: id,\n\t}, false\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/keyring.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg/colons\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// listKey lists all keys of the given type and matching the search strings.\nfunc (g *GPG) listKeys(ctx context.Context, typ string, search ...string) (gpg.KeyList, error) {\n\tdebug.Log(\"listing %s keys for %v\", typ, search)\n\tctx, cancel := context.WithTimeout(ctx, Timeout)\n\tdefer cancel()\n\n\targs := []string{\"--with-colons\", \"--with-fingerprint\", \"--fixed-list-mode\", \"--list-\" + typ + \"-keys\"}\n\targs = append(args, search...)\n\tif e, found := g.listCache.Get(strings.Join(args, \",\")); found && gpg.UseCache(ctx) {\n\t\tdebug.Log(\"listed cached keys: %q\", strings.Join(e.Recipients(), \",\"))\n\n\t\treturn e, nil\n\t}\n\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\terrBuf := bytes.Buffer{}\n\tcmd.Stderr = &errBuf\n\n\tdebug.V(1).Log(\"%s %+v\\n\", cmd.Path, cmd.Args)\n\tcmdout, err := cmd.Output()\n\tif err != nil {\n\t\tif bytes.Contains(cmdout, []byte(\"secret key not available\")) || bytes.Contains(errBuf.Bytes(), []byte(\"No secret key\")) {\n\t\t\tdebug.Log(\"secret key not available for %v\", search)\n\n\t\t\treturn gpg.KeyList{}, nil\n\t\t}\n\t\terrStr := fmt.Errorf(\"%w: %s|%s\", err, cmdout, errBuf.String())\n\t\tdebug.Log(\"cmd error listing %s keys: %q\", typ, errStr)\n\n\t\treturn gpg.KeyList{}, errStr\n\t}\n\n\tkl := colons.Parse(bytes.NewBuffer(cmdout))\n\tg.listCache.Add(strings.Join(args, \",\"), kl)\n\n\tdebug.Log(\"listed non-cached keys: %q\", strings.Join(kl.Recipients(), \",\"))\n\n\treturn kl, nil\n}\n\n// Fingerprint returns the fingerprint.\nfunc (g *GPG) Fingerprint(ctx context.Context, id string) string {\n\tk, found := g.findKey(ctx, id)\n\tif !found {\n\t\treturn \"\"\n\t}\n\n\treturn k.Fingerprint\n}\n\n// FormatKey formats the details of a key id\n// Examples:\n// - NameFromKey: {{ .Name }}\n// - EmailFromKey: {{ .Email }}.\nfunc (g *GPG) FormatKey(ctx context.Context, id, tpl string) string {\n\tif tpl == \"\" {\n\t\tk, found := g.findKey(ctx, id)\n\t\tif !found {\n\t\t\treturn \"\"\n\t\t}\n\n\t\treturn k.OneLine()\n\t}\n\n\ttmpl, err := template.New(tpl).Parse(tpl)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar gid gpg.Identity\n\tk, found := g.findKey(ctx, id)\n\tif found {\n\t\tgid = k.Identity()\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tif err := tmpl.Execute(buf, gid); err != nil {\n\t\tdebug.Log(\"Failed to render template %q: %s\", tpl, err)\n\n\t\treturn \"\"\n\t}\n\n\treturn buf.String()\n}\n\n// ReadNamesFromKey unmarshals and returns the names associated with the given public key.\nfunc (g *GPG) ReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error) {\n\tel, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(buf))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read key ring: %w\", err)\n\t}\n\n\tif len(el) != 1 {\n\t\treturn nil, fmt.Errorf(\"public Key must contain exactly one Entity\")\n\t}\n\n\tnames := make([]string, 0, len(el[0].Identities))\n\tfor _, v := range el[0].Identities {\n\t\tnames = append(names, v.Name)\n\t}\n\n\treturn names, nil\n}\n\n// ImportPublicKey will import a key from the given location into the keyring.\nfunc (g *GPG) ImportPublicKey(ctx context.Context, buf []byte) error {\n\tif len(buf) < 1 {\n\t\treturn fmt.Errorf(\"empty input\")\n\t}\n\n\toutBuf := &bytes.Buffer{}\n\n\targs := append(g.args, \"--import\")\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\tcmd.Stdin = bytes.NewReader(buf)\n\tcmd.Stdout = outBuf\n\tcmd.Stderr = outBuf\n\n\tdebug.Log(\"gpg.ImportPublicKey: %s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\tout.Printf(ctx, \"GPG import failed: %s\", outBuf.String())\n\n\t\treturn fmt.Errorf(\"failed to run command: '%s %+v': %w - %q\", cmd.Path, cmd.Args, err, outBuf.String())\n\t}\n\n\t// clear key cache\n\tg.privKeys = nil\n\tg.pubKeys = nil\n\n\treturn nil\n}\n\n// GetFingerprint returns the fingerprint of a key.\nfunc (g *GPG) GetFingerprint(ctx context.Context, buf []byte) (string, error) {\n\tif len(buf) < 1 {\n\t\treturn \"\", fmt.Errorf(\"empty input\")\n\t}\n\n\tel, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(buf))\n\tif err != nil {\n\t\t// maybe it's a non-armored key?\n\t\tel, err = openpgp.ReadKeyRing(bytes.NewReader(buf))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read key ring: %w\", err)\n\t\t}\n\t}\n\n\tif len(el) != 1 {\n\t\treturn \"\", fmt.Errorf(\"public Key must contain exactly one Entity\")\n\t}\n\n\treturn strings.ToUpper(hex.EncodeToString(el[0].PrimaryKey.Fingerprint[:])), nil\n}\n\n// ExportPublicKey will export the named public key to the location given.\nfunc (g *GPG) ExportPublicKey(ctx context.Context, id string) ([]byte, error) {\n\tif id == \"\" {\n\t\treturn nil, fmt.Errorf(\"id is empty\")\n\t}\n\n\targs := append(g.args, \"--armor\", \"--export\", id)\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\n\tdebug.Log(\"%s %+v\", cmd.Path, cmd.Args)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run command '%s %+v': %w\", cmd.Path, cmd.Args, err)\n\t}\n\n\tif len(out) < 1 {\n\t\treturn nil, fmt.Errorf(\"key not found\")\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/keyring_test.go",
    "content": "package cli\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst pubkey = `\n-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQINBFn18UoBEACbfzn9kr35IRXuDC+VA6Yuv7AR2EGLb1tGzvtKfG1JDECV/Npo\nRu/lwN02iW+pWt8JR/yHOaMjQzZhbH+I9nuLmBITR/V9ZiPnggCpN+uNMH6EBU7W\nTXbiVNm/3kOY9PkLFZQiBL8HCXw0qARmlU4UlwBFjFEtv5ra9gbZH4EoKoLP6uFh\nziDIjViVKUCI+Z1iJZwalu7ac63LI/mXzwrAf/uWE8fAu2WpK1xuxwxFxyUxm2yO\nc9Y+ytKCQ1/PAiOvzL96SMlQNHsuSW/8kOU/C8PhoAbwArd/Hxqi0blqPinNdfGP\nNzGwKoxak0ZEwfjMo2/uOIWQCcQm7NYsI0YcAH9+7El6ZWkkLi98lRwhLCmpffe+\nw4FanGNfVVYrwsAUS0ejGpbXNF5jv9cMjEcxQlkID1xOOFAwSmg/f2PQM0wtJdDG\nZ9/DduIOXfnf5PXdR9EZhwo9N2RRciPr8FheIZe/RZqmhUejLVp2idEPhiGDbO45\nOQak0JaPSxKRHsMwHKgNmfyO2XoJ+0ONNnyJL7Gm8cr4Lq6Zg189R4qEfNF8/JIU\n//AQotKO3y2s6oHxjo2bQIYm/xkG0++Lcq4H5FxVJTqOE9XmYTyIoehGaPuk1eqy\nto/4flXBBxy9UpTfF4cF79PvJqxHz7GNPolBscecNEG+nFbMOF6CPzG1UQARAQAB\ntDFHb3Bhc3MgQXJjaGl2ZSBTaWduaW5nIEtleSA8Z29wYXNzQGp1c3R3YXRjaC5j\nb20+iQI9BBMBCgAnBQJZ9fFKAhsDBQkSzAMABQsJCAcDBRUKCQgLBRYCAwEAAh4B\nAheAAAoJEAySIlqX9rZmyscP/Rlv+0zDOCS5c7Bwyg5EkYRCQGDzt5W6+Udu9r4H\nUenhB40XD5Ox0lU0oYSGgGLKxfPqD3/mY/6AGxZNtNsiQTKz52ire3Gs4tQXQu7j\n+w1QrQkARc9Q3+FpbYVePMe8xXx4TAbKladYZumEctLp2SYXgHbG3EekYX51gBIY\nkY6akJa/7tR37QdJkCq6Twhh621CsqyJI6lSCL6kKekUktwzV/c5XUijxAAs064Y\nsPq9Hxm7bp+c1lMtz9tP/7wTSiJ5ufRpQZ85TnmJH016IdRNj1AEu07eTpFpmqxe\n9pfsPCmRFVUwGoLTG+3yCsNyWJDRumW+mmHjpFTBX3OLxW4CI3z1fczbxmFAwr6d\ntgsiUPe2tAw5LCAluo9wZxxeQLbDw8+e5FO/r7uiXLVwyWnIm4kKKu7SEZTFyWf4\ngvr+Smlm2o4NDqjqp0TurshKZcETJuNE23v9zh+gxekEqKAjdEwjsPPhhbLAT2V3\nqkzMHejDcGOZWFjz4LFHCnAwYNKOY3dhyv12dbr4PvS6CoEZGVx2vCIkLkGzmg8I\nzcvN2gdoiiy2WtZ0b5Hd+BIRgNFTDLszm8eMgFhRHgO52c2ZunRCAFtDdUDx54O6\nEHTswZ/pFJZiH6PI//jSr+nPlxLt7fbjFRJI6deOYtW94Tw+fHkzaLWvF7UC0vz2\nrw8NuQINBFn18UoBEACwuB5KkTw8xU5m9cJSRnMQ+GfH9kc8mis/O1N+zMYc/o51\nmHOclXCCG4C68Ba1DBm3PrzWBoaiGoVFomEW7SskQKyvvPwe13lD2l88d0CUIbmx\n6wbv8ESNnH4xf1Yhl/khxZ3ecEd81DN1vVevcb6Eay5aRBxihTdeRg4J9PahL8nj\ncMOTdH3J2GiEDGwIR7oBcI0a0EOpBN5PJU9goKr3Dl8ObRwB4wV1bsHFLsifWtYn\nbOYYp+hKWRPf4CfNWEoESzMHsmx7ki8aS1EXL9aZFVEcZ67ZdTu8iDaMdmnW4el/\np+D+4PeVlZSzQBDqyCdYB0zTi+ByLpJ2MhNHMBK2pdLuIWk7vvxTQCz794cqYpEI\nP0/KN788UGJ8YYg2ab5L1YBpXqyqu2wFSGWK6q/I3u5uQsm0/T+x/n8kEt+spNcu\n66zcco+ddQ/4waKbTZdY69VGgWiRubT/dJRmsgbT4sFgLmnYrLHH/v/XFYewc2e6\nszaGAWr0P//XB/UFTEltJVSox7qWNuB2UBMmCVw/9Ow0ylt1j0Zve9NgYi7yr0Qr\nlZCzkqkGnL1L54FK6JChseC5L6gsJuzXmP2nH1LDB9+NCYHdzmAAsKsy+prTS1Zv\naty0xzK4Ds00g1EJC7LIe7Iaj8HqPIZ9sDT+PRdJRcM6Z5q9TjW1VU1iRXXqIQAR\nAQABiQIlBBgBCgAPBQJZ9fFKAhsMBQkSzAMAAAoJEAySIlqX9rZmioQP/iQGtLDG\n2pyhv79qQOn4tMwIS0urSCJhLZRI09v11gfXchI8DhmOm2re4ZNFM09vvCX+Z4EI\nSG2mofY+bB0hwiYF8YECpCNSIzlMGC8O39/0VkcTHXO8fwT8Yet9RvalI5owmiO8\nt9tZeiSBNO8f2MbWWZZuDwcQm3VJSoBR0GpWk8JhyIgfBnmefQTKH60sqbrWdTk2\n7rBFQonWacioUFx5MeNVFqaY2ixQcywlGtwzXx67bM4zfgJUr5zps1pmjwKHspxR\nnZ7twHlS5V2ccFamigoa9OW52hDZZqpkjwJxbv1WjMY410r099fd5epVklLinuzz\nl6RoSl119G/Bmyv1rLguT96ALLW+rBM/6X02XLdNzVrDOFbudh8rzAcPnN+jagb6\nr7bpPxJKUVeDsMOAFpQkXfizxIO7xUkL4nSrybanckiJ9kn54KAPq5l2W4qjvwUe\nlc9H2dcZ5BfyTxSqGq+C0fRmERQt075FegIXRWTPN2r9xnFp4r1LE184vwL+7ec0\nTuG22zcizbrw+MhuAA8gfa+dxPR+Lm/BzrRYTrjrKVNJczQi5O1h4RsBj59EnaYM\nW1w17WmlKUS9SKiFT52hKi7b3C/19WPamvDoarjglEkpOKkETUOIwA8ViI9Wa4Fm\noLGNPe8bErLNfny6AWU0Enam6a13BxwbBrtr\n=AmFu\n-----END PGP PUBLIC KEY BLOCK-----\n`\n\nfunc TestReadNamesFromKey(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tg, err := New(ctx, Config{})\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, g.Binary())\n\n\tnames, err := g.ReadNamesFromKey(ctx, []byte(pubkey))\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"Gopass Archive Signing Key <gopass@justwatch.com>\"}, names)\n}\n\nfunc TestExportPublicKey(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tg, err := New(ctx, Config{})\n\trequire.NoError(t, err)\n\n\t_, err = g.ExportPublicKey(ctx, \"foobar\")\n\trequire.Error(t, err)\n}\n\nfunc TestImport(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tg := &GPG{}\n\tg.binary = \"true\"\n\tif runtime.GOOS == \"windows\" {\n\t\tg.binary = \"rundll32\"\n\t}\n\n\trequire.NoError(t, g.ImportPublicKey(ctx, []byte(\"foobar\")))\n\n\tg.binary = \"\"\n\trequire.Error(t, g.ImportPublicKey(ctx, []byte(\"foobar\")))\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/loader.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nconst (\n\tname = \"gpgcli\"\n)\n\nfunc init() {\n\tbackend.CryptoRegistry.Register(backend.GPGCLI, name, &loader{})\n}\n\ntype loader struct{}\n\n// New implements backend.CryptoLoader.\nfunc (l loader) New(ctx context.Context) (backend.Crypto, error) {\n\tdebug.Log(\"Using Crypto Backend: %s\", name)\n\n\treturn New(ctx, Config{\n\t\tUmask:  fsutil.Umask(),\n\t\tArgs:   gpgconf.GPGOpts(),\n\t\tBinary: os.Getenv(\"GOPASS_GPG_BINARY\"),\n\t})\n}\n\nfunc (l loader) Handles(ctx context.Context, s backend.Storage) error {\n\tif s.Exists(ctx, IDFile) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"not supported\")\n}\n\nfunc (l loader) Priority() int {\n\treturn 1\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/recipients.go",
    "content": "package cli\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// ListRecipients returns a parsed list of GPG public keys.\nfunc (g *GPG) ListRecipients(ctx context.Context) ([]string, error) {\n\tif g.pubKeys == nil {\n\t\tkl, err := g.listKeys(ctx, \"public\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tg.pubKeys = kl\n\t}\n\n\tif gpg.IsAlwaysTrust(ctx) {\n\t\treturn g.pubKeys.Recipients(), nil\n\t}\n\n\treturn g.pubKeys.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients(), nil\n}\n\n// FindRecipients searches for the given public keys.\nfunc (g *GPG) FindRecipients(ctx context.Context, search ...string) ([]string, error) {\n\tkl, err := g.listKeys(ctx, \"public\", search...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif kl == nil {\n\t\treturn nil, fmt.Errorf(\"no keys found for %v\", search)\n\t}\n\n\trecp := kl.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients()\n\tif gpg.IsAlwaysTrust(ctx) {\n\t\trecp = kl.Recipients()\n\t}\n\tdebug.Log(\"recp before subkey check: %q\", recp)\n\n\t// let us support the ! syntax that enforces specific subkey usage\n\tfor _, val := range search {\n\t\tstr := strings.TrimPrefix(strings.TrimSuffix(val, \"!\"), \"0x\")\n\t\tif !strings.HasSuffix(val, \"!\") || str == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, key := range kl {\n\t\t\tfor sub := range key.SubKeys {\n\t\t\t\tif !strings.Contains(sub, str) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// we remove that key from the recp\n\t\t\t\trecp = slices.DeleteFunc(recp, func(s string) bool {\n\t\t\t\t\ts = strings.TrimPrefix(s, \"0x\")\n\t\t\t\t\t// because we use short fingerprints in recp\n\t\t\t\t\treturn strings.Contains(key.Fingerprint, s)\n\t\t\t\t})\n\t\t\t\t// and we add the specific subkey as is, keeping the !\n\t\t\t\trecp = append(recp, val)\n\n\t\t\t\t// we go to the next key in the odd case the subkey is part of multiple keys\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tdebug.Log(\"found useable keys for %q: %q (all: %q)\", search, recp, kl.Recipients())\n\n\treturn recp, nil\n}\n\n// RecipientIDs returns a list of recipient IDs for a given encrypted blob.\nfunc (g *GPG) RecipientIDs(ctx context.Context, buf []byte) ([]string, error) {\n\tctx, cancel := context.WithTimeout(ctx, Timeout)\n\tdefer cancel()\n\n\trecp := make([]string, 0, 5)\n\n\t// extract recipients from gpg output\n\targs := []string{\"--batch\", \"--list-only\", \"--list-packets\", \"--no-default-keyring\", \"--secret-keyring\", \"/dev/null\"}\n\tcmd := exec.CommandContext(ctx, g.binary, args...)\n\tcmd.Stdin = bytes.NewReader(buf)\n\n\t// switch to LANG C for more predictable output, switch back later\n\tfor _, env := range os.Environ() {\n\t\tif strings.HasPrefix(env, \"LANGUAGE=\") {\n\t\t\tcontinue\n\t\t}\n\t\tcmd.Env = append(cmd.Env, env)\n\t}\n\tcmd.Env = append(cmd.Env, \"LANGUAGE=C\")\n\n\tdebug.Log(\"%s %+v\", cmd.Path, cmd.Args)\n\n\tcmdout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\t// parse the output\n\tscanner := bufio.NewScanner(bytes.NewBuffer(cmdout))\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tdebug.Log(\"GPG Output: %s\", line)\n\t\tif !strings.HasPrefix(line, \":pubkey enc packet:\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tm := splitPacket(line)\n\t\tkeyid, found := m[\"keyid\"]\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tkl, err := g.listKeys(ctx, \"public\", keyid)\n\t\tif err != nil || len(kl) < 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\trecp = append(recp, kl[0].Fingerprint)\n\t}\n\n\tif g.throwKids {\n\t\tout.Warningf(ctx, \"gpg option throw-keyids is set. some features might not work.\")\n\t}\n\n\treturn recp, nil\n}\n\nfunc splitPacket(in string) map[string]string {\n\tm := make(map[string]string, 3)\n\tp := strings.Split(in, \":\")\n\tif len(p) < 3 {\n\t\treturn m\n\t}\n\n\tp = strings.Split(strings.TrimSpace(p[2]), \" \")\n\tfor i := 0; i+1 < len(p); i += 2 {\n\t\tm[p[i]] = strings.Trim(p[i+1], \",\")\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/recipients_test.go",
    "content": "package cli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSplitPacket(t *testing.T) {\n\tt.Parallel()\n\n\tfor in, out := range map[string]map[string]string{\n\t\t\"\": {},\n\t\t\":pubkey enc packet: version 3, algo 1, keyid 00F0FF00FFC00F0F\": {\n\t\t\t\"algo\":    \"1\",\n\t\t\t\"keyid\":   \"00F0FF00FFC00F0F\",\n\t\t\t\"version\": \"3\",\n\t\t},\n\t\t\":encrypted data packet:\": {},\n\t} {\n\t\tassert.Equal(t, out, splitPacket(in))\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/cli/version.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf\"\n)\n\n// Version will return GPG version information.\nfunc (g *GPG) Version(ctx context.Context) semver.Version {\n\treturn gpgconf.Version(ctx, g.Binary())\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/colons/parse_colons.go",
    "content": "package colons\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n)\n\nvar (\n\t// nolint:godot\n\t// John Doe (user) <john.doe@example.com>\n\treUIDComment = regexp.MustCompile(`([^(<]+)\\s+(\\([^)]+\\))\\s+<([^>]+)>`)\n\t// nolint:godot\n\t// John Doe <john.doe@example.com>\n\treUID = regexp.MustCompile(`([^(<]+)\\s+<([^>]+)>`)\n\t// nolint:godot\n\t// John Doe (user)\n\treUIDNoEmailComment = regexp.MustCompile(`([^(<]+)\\s+(\\([^)]+\\))`)\n)\n\n// http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS\n// Fields:\n// 0 - Type of record\n//     Types:\n//     pub - Public Key\n//     crt - X.509 cert\n//     crs - X.509 cert and private key\n//     sub - Subkey (Secondary Key)\n//     sec - Secret / Private Key\n//     ssb - Secret Subkey\n//     uid - User ID\n//     uat - User attribute\n//     sig - Signature\n//     rev - Revocation Signature\n//     fpr - Fingerprint (field 9)\n//     pkd - Public Key Data\n//     grp - Keygrip\n//     rvk - Revocation KEy\n//     tfs - TOFU stats\n//     tru - Trust database info\n//     spk - Signature subpacket\n//     cfg - Configuration data\n// 1 - Validity\n// 2 - Key length\n// 3 - Public Key Algo\n// 4 - KeyID\n// 5 - Creation Date (UTC)\n// 6 - Expiration Date\n// 7 - Cert S/N\n// 8 - Ownertrust\n// 9 - User-ID\n// 10 - Sign. Class\n// 11 - Key Caps.\n// 12 - Issuer cert fp\n// 13 - Flag\n// 14 - S/N of a token\n// 15 - Hash algo (2 - SHA-1, 8 - SHA-256)\n// 16 - Curve Name\n\n// Parse parses the `--with-colons` output format of GPG.\nfunc Parse(reader io.Reader) gpg.KeyList {\n\tkl := make(gpg.KeyList, 0, 100)\n\n\tscanner := bufio.NewScanner(reader)\n\n\tvar cur gpg.Key\n\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tfields := strings.Split(line, \":\")\n\n\t\tswitch fields[0] {\n\t\tcase \"pub\":\n\t\t\tfallthrough\n\t\tcase \"sec\":\n\t\t\tif cur.Fingerprint != \"\" && cur.KeyLength > 0 {\n\t\t\t\tkl = append(kl, cur)\n\t\t\t}\n\n\t\t\tvalidity := fields[1]\n\t\t\tif validity == \"\" && fields[0] == \"sec\" {\n\t\t\t\tvalidity = \"u\"\n\t\t\t}\n\n\t\t\tcur = gpg.Key{\n\t\t\t\tKeyType:        fields[0],\n\t\t\t\tValidity:       validity,\n\t\t\t\tKeyLength:      parseInt(fields[2]),\n\t\t\t\tCreationDate:   parseTS(fields[5]),\n\t\t\t\tExpirationDate: parseTS(fields[6]),\n\t\t\t\tOwnertrust:     fields[8],\n\t\t\t\tIdentities:     make(map[string]gpg.Identity, 1),\n\t\t\t\tSubKeys:        make(map[string]struct{}, 1),\n\t\t\t\tCaps:           parseKeyCaps(fields[11]),\n\t\t\t}\n\t\tcase \"sub\":\n\t\t\tfallthrough\n\t\tcase \"ssb\":\n\t\t\tcur.SubKeys[fields[4]] = struct{}{}\n\t\tcase \"fpr\":\n\t\t\tif cur.Fingerprint == \"\" {\n\t\t\t\tcur.Fingerprint = fields[9]\n\t\t\t}\n\t\tcase \"uid\":\n\t\t\tsn := fields[7]\n\t\t\tcur.Identities[sn] = parseColonIdentity(fields)\n\t\t}\n\t}\n\n\tif cur.Fingerprint != \"\" && cur.KeyLength > 0 {\n\t\tkl = append(kl, cur)\n\t}\n\n\treturn kl\n}\n\nfunc parseKeyCaps(field string) gpg.Capabilities {\n\tkeycaps := gpg.Capabilities{}\n\n\tif strings.Contains(field, \"S\") {\n\t\tkeycaps.Sign = true\n\t}\n\n\tif strings.Contains(field, \"E\") {\n\t\tkeycaps.Encrypt = true\n\t}\n\n\tif strings.Contains(field, \"C\") {\n\t\tkeycaps.Certify = true\n\t}\n\n\tif strings.Contains(field, \"A\") {\n\t\tkeycaps.Authentication = true\n\t}\n\n\tif strings.Contains(field, \"D\") {\n\t\tkeycaps.Deactivated = true\n\t}\n\n\treturn keycaps\n}\n\nfunc parseColonIdentity(fields []string) gpg.Identity {\n\tfor i, f := range fields {\n\t\tfields[i] = strings.ReplaceAll(f, \"\\\\x3a\", \":\")\n\t}\n\n\tid := fields[9]\n\tni := gpg.Identity{\n\t\tName:           id,\n\t\tCreationDate:   parseTS(fields[5]),\n\t\tExpirationDate: parseTS(fields[6]),\n\t}\n\n\tif reUIDComment.MatchString(id) {\n\t\tif m := reUIDComment.FindStringSubmatch(id); len(m) > 3 {\n\t\t\tni.Name = m[1]\n\t\t\tni.Comment = strings.Trim(m[2], \"()\")\n\t\t\tni.Email = m[3]\n\n\t\t\treturn ni\n\t\t}\n\t}\n\n\tif reUIDNoEmailComment.MatchString(id) {\n\t\tif m := reUIDNoEmailComment.FindStringSubmatch(id); len(m) > 2 {\n\t\t\tni.Name = m[1]\n\t\t\tni.Comment = strings.Trim(m[2], \"()\")\n\t\t}\n\n\t\treturn ni\n\t}\n\n\tif reUID.MatchString(id) {\n\t\tif m := reUID.FindStringSubmatch(id); len(m) > 2 {\n\t\t\tni.Name = m[1]\n\t\t\tni.Email = m[2]\n\n\t\t\treturn ni\n\t\t}\n\t}\n\n\treturn ni\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/colons/parse_colons_test.go",
    "content": "package colons\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseColonIdentity(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tin      string\n\t\tname    string\n\t\tcomment string\n\t\temail   string\n\t}{\n\t\t{\n\t\t\tin:      \"uid:-::::1460666077::780A2FDD0570B3E52E5B1E24EBB406B68526CAFD::ThisIsNotAnAlias:\",\n\t\t\tname:    \"ThisIsNotAnAlias\",\n\t\t\tcomment: \"\",\n\t\t\temail:   \"\",\n\t\t},\n\t\t{\n\t\t\tin:      \"uid:::::1441103821::AEFC3F5B6CAD79A946D7F0FF83BB8B7E10B578CA::John Doe <john.doe@example.com>:\",\n\t\t\tname:    \"John Doe\",\n\t\t\tcomment: \"\",\n\t\t\temail:   \"john.doe@example.com\",\n\t\t},\n\t\t{\n\t\t\tin:      \"uid:::::1441103821::AEFC3F5B6CAD79A946D7F0FF83BB8B7E10B578CA::John Doe (user) <john.doe@example.com>:\",\n\t\t\tname:    \"John Doe\",\n\t\t\tcomment: \"user\",\n\t\t\temail:   \"john.doe@example.com\",\n\t\t},\n\t\t{\n\t\t\tin:      \"uid:::::1441103821::AEFC3F5B6CAD79A946D7F0FF83BB8B7E10B578CA::John Doe (user):\",\n\t\t\tname:    \"John Doe\",\n\t\t\tcomment: \"user\",\n\t\t\temail:   \"\",\n\t\t},\n\t} {\n\t\tt.Run(tc.in, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgi := parseColonIdentity(strings.Split(tc.in, \":\"))\n\t\t\tassert.Equal(t, tc.name, gi.Name)\n\t\t\tassert.Equal(t, tc.comment, gi.Comment)\n\t\t\tassert.Equal(t, tc.email, gi.Email)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/colons/parse_fuzz.go",
    "content": "//go:build gofuzz\n\npackage colons\n\nimport \"bytes\"\n\nfunc Fuzz(data []byte) int {\n\tif kl := Parse(bytes.NewReader(data)); len(kl) != 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/colons/utils.go",
    "content": "package colons\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// parseTS parses the passed string as an Epoch int and returns\n// the time struct or the zero time struct.\nfunc parseTS(str string) time.Time {\n\tt := time.Time{}\n\n\tif sec, err := strconv.ParseInt(str, 10, 64); err == nil {\n\t\tt = time.Unix(sec, 0)\n\t}\n\n\treturn t\n}\n\n// parseInt parses the passed string as an int and returns it\n// or 0 on errors.\nfunc parseInt(str string) int {\n\ti := 0\n\n\tif iv, err := strconv.ParseInt(str, 10, 32); err == nil {\n\t\ti = int(iv)\n\t}\n\n\treturn i\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/context.go",
    "content": "package gpg\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyAlwaysTrust contextKey = iota\n\tctxKeyUseCache\n)\n\n// WithAlwaysTrust will return a context with the flag for always trust set.\nfunc WithAlwaysTrust(ctx context.Context, at bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyAlwaysTrust, at)\n}\n\n// IsAlwaysTrust will return the value of the always trust flag or the default\n// (false).\nfunc IsAlwaysTrust(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyAlwaysTrust).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithUseCache returns a context with the value of NoCache set.\nfunc WithUseCache(ctx context.Context, nc bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyUseCache, nc)\n}\n\n// UseCache returns true if this request should ignore the cache.\nfunc UseCache(ctx context.Context) bool {\n\tnc, ok := ctx.Value(ctxKeyUseCache).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn nc\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/context_test.go",
    "content": "package gpg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n)\n\nfunc TestAlwaysTrust(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tif IsAlwaysTrust(ctx) {\n\t\tt.Errorf(\"AlwaysTrust should be false\")\n\t}\n\n\tif !IsAlwaysTrust(WithAlwaysTrust(ctx, true)) {\n\t\tt.Errorf(\"AlwaysTrust should be true\")\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/doc.go",
    "content": "// Package gpg provides a GPG crypto backend for gopass.\n// It does not provide a full GPG implementation, but rather\n// building blocks used by other packages to provide GPG\n// support.\n\npackage gpg\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/binary.go",
    "content": "package gpgconf\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Binary returns the GPG binary location.\nfunc Binary(ctx context.Context, bin string) (string, error) {\n\tif sv := os.Getenv(\"GOPASS_GPG_BINARY\"); sv != \"\" {\n\t\tdebug.Log(\"Using GOPASS_GPG_BINARY: %s\", sv)\n\n\t\treturn sv, nil\n\t}\n\n\treturn detectBinary(ctx, bin)\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/binary_others.go",
    "content": "//go:build !windows\n\npackage gpgconf\n\nimport (\n\t\"context\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nfunc detectBinary(_ context.Context, name string) (string, error) {\n\t// user supplied binaries take precedence\n\tif name != \"\" {\n\t\treturn exec.LookPath(name)\n\t}\n\n\t// try to get the proper binary from gpgconf(1)\n\tp, err := Path(\"gpg\")\n\tif err != nil || p == \"\" || !fsutil.IsFile(p) {\n\t\tdebug.Log(\"gpgconf failed (%q), falling back to path lookup: %q\", p, err)\n\t\t// otherwise fall back to the default and try\n\t\t// to look up \"gpg\"\n\t\treturn exec.LookPath(\"gpg\")\n\t}\n\n\tdebug.V(3).Log(\"gpgconf returned %q for gpg\", p)\n\n\treturn p, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/binary_windows.go",
    "content": "//go:build windows\n\npackage gpgconf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"golang.org/x/sys/windows/registry\"\n)\n\nfunc detectBinary(ctx context.Context, bin string) (string, error) {\n\tbins, err := detectBinaryCandidates(bin)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbv := make(byVersion, 0, len(bins))\n\tfor _, b := range bins {\n\t\tdebug.V(3).Log(\"Looking for %q ...\", b)\n\t\tif p, err := exec.LookPath(b); err == nil {\n\t\t\tgb := gpgBin{\n\t\t\t\tpath: p,\n\t\t\t\tver:  Version(ctx, p),\n\t\t\t}\n\t\t\tdebug.V(1).Log(\"Found %q at %q (%s)\", b, p, gb.ver.String())\n\t\t\tbv = append(bv, gb)\n\t\t}\n\t}\n\n\tif len(bv) < 1 {\n\t\treturn \"\", errors.New(\"no gpg binary found\")\n\t}\n\n\tbinary := bv[0].path\n\tdebug.V(1).Log(\"using %q\", binary)\n\n\treturn binary, nil\n}\n\nfunc detectBinaryCandidates(bin string) ([]string, error) {\n\t// gpg.exe for GPG4Win 3.0.0; would be gpg2.exe for 2.x\n\tbins := make([]string, 0, 4)\n\n\tbins, err := searchRegistry(bin, bins)\n\tif err != nil {\n\t\treturn bins, err\n\t}\n\n\tbins, err = searchPath(bin, bins)\n\tif err != nil {\n\t\treturn bins, err\n\t}\n\n\treturn bins, nil\n}\n\nfunc searchRegistry(bin string, bins []string) ([]string, error) {\n\t// try to detect location of installed GPG4Win\n\tk, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\\GnuPG`, registry.QUERY_VALUE|registry.WOW64_32KEY)\n\tif err != nil {\n\t\treturn bins, nil\n\t}\n\n\tif v, _, err := k.GetStringValue(\"Install Directory\"); err == nil && v != \"\" {\n\t\tfor _, b := range []string{bin, \"gpg2.exe\", \"gpg.exe\"} {\n\t\t\tif b == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgpgPath := filepath.Join(v, \"bin\", b)\n\t\t\tif fsutil.IsFile(gpgPath) {\n\t\t\t\tbins = append(bins, gpgPath)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn bins, nil\n}\n\nfunc searchPath(bin string, bins []string) ([]string, error) {\n\t// try to detect location for GPG installed somewhere on the PATH\n\tfor _, b := range []string{bin, \"gpg2.exe\", \"gpg.exe\"} {\n\t\tif b == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tgpgPath, err := exec.LookPath(b)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif fsutil.IsFile(gpgPath) {\n\t\t\tbins = append(bins, gpgPath)\n\t\t}\n\t}\n\n\treturn bins, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/binary_windows_test.go",
    "content": "package gpgconf\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDetectBinaryCandidates(t *testing.T) {\n\tbins, err := detectBinaryCandidates(\"foobar\")\n\trequire.NoError(t, err)\n\t// the install locations differ depending on :\n\t// - chocolatey install path prefix\n\t// - 64bit/32bit windows\n\tvar stripped []string\n\tfor _, bin := range bins {\n\t\tstripped = append(stripped, filepath.Base(bin))\n\t}\n\tassert.Contains(t, stripped, \"gpg.exe\")\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/gpgconf.go",
    "content": "package gpgconf\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// Path returns the path to a GPG component.\nfunc Path(key string) (string, error) {\n\tbuf := &bytes.Buffer{}\n\tcmd := exec.Command(\"gpgconf\")\n\tcmd.Stdout = buf\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tkey = strings.TrimSpace(strings.ToLower(key))\n\tsc := bufio.NewScanner(buf)\n\tfor sc.Scan() {\n\t\tp := strings.Split(strings.TrimSpace(sc.Text()), \":\")\n\t\tif len(p) < 3 {\n\t\t\tcontinue\n\t\t}\n\t\tif key == p[0] {\n\t\t\treturn p[2], nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils.go",
    "content": "package gpgconf\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// GPGOpts parses extra GPG options from the environment.\nfunc GPGOpts() []string {\n\tfor _, en := range []string{\"GOPASS_GPG_OPTS\", \"PASSWORD_STORE_GPG_OPTS\"} {\n\t\tif opts := os.Getenv(en); opts != \"\" {\n\t\t\treturn strings.Fields(opts)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// gpgConfigLoc returns the location of the GPG config file.\nfunc gpgConfigLoc() string {\n\tif sv := os.Getenv(\"GNUPGHOME\"); sv != \"\" {\n\t\treturn filepath.Join(sv, \"gpg.conf\")\n\t}\n\n\tuhd, _ := os.UserHomeDir()\n\n\treturn filepath.Join(uhd, \".gnupg\", \"gpg.conf\")\n}\n\n// Config returns the GPG config file.\nfunc Config() (map[string]string, error) {\n\tfh, err := os.Open(gpgConfigLoc())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\t_ = fh.Close()\n\t}()\n\n\treturn parseGpgConfig(fh)\n}\n\nfunc parseGpgConfig(fh io.Reader) (map[string]string, error) {\n\tvals := make(map[string]string, 20)\n\tscanner := bufio.NewScanner(fh)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\t// ignore comments\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tkey, val, found := strings.Cut(line, \" \")\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\t\tvals[key] = strings.TrimSpace(val)\n\t}\n\n\treturn vals, nil\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils_linux.go",
    "content": "//go:build linux\n\npackage gpgconf\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nvar fd0 = \"/proc/self/fd/0\"\n\n// TTY returns the tty of the current process.\n// see https://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html\nfunc TTY() string {\n\tdest, err := os.Readlink(fd0)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn dest\n}\n\n// Umask sets the desired umask.\nfunc Umask(mask int) int {\n\treturn syscall.Umask(mask)\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils_linux_test.go",
    "content": "//go:build linux\n\npackage gpgconf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTTY(t *testing.T) {\n\tt.Parallel()\n\n\tfd0 = \"/tmp/foobar\"\n\tassert.Empty(t, TTY())\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils_others.go",
    "content": "//go:build !linux && !windows\n\npackage gpgconf\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc TTY() string {\n\tcmd := exec.Command(\"/usr/bin/tty\")\n\tcmd.Stdin = os.Stdin\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn string(out)\n}\n\nfunc Umask(mask int) int {\n\treturn syscall.Umask(mask)\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils_test.go",
    "content": "package gpgconf\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGpgOpts(t *testing.T) {\n\tfor _, vn := range []string{\"GOPASS_GPG_OPTS\", \"PASSWORD_STORE_GPG_OPTS\"} {\n\t\tfor in, out := range map[string][]string{\n\t\t\t\"\": nil,\n\t\t\t\"--decrypt --armor --recipient 0xDEADBEEF\": {\"--decrypt\", \"--armor\", \"--recipient\", \"0xDEADBEEF\"},\n\t\t} {\n\t\t\tt.Run(vn, func(t *testing.T) {\n\t\t\t\tt.Setenv(vn, in)\n\t\t\t\tassert.Equal(t, out, GPGOpts())\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestGPGConfig(t *testing.T) {\n\tt.Parallel()\n\n\tin := `\n#-----------------------------\n# default key\n#-----------------------------\n# The default key to sign with. If this option is not used, the default key is\n# the first key found in the secret keyring\n#default-key 0xD8692123C4065DEA5E0F3AB5249B39D24F25E3F6\n#-----------------------------\n# behavior\n#-----------------------------\n# Disable inclusion of the version string in ASCII armored output\nno-emit-version\n# Disable comment string in clear text signatures and ASCII armored messages\nno-comments\n# Display long key IDs\nkeyid-format 0xlong\n# List all keys (or the specified ones) along with their fingerprints\nwith-fingerprint\n# Display the calculated validity of user IDs during key listings\nlist-options show-uid-validity\nverify-options show-uid-validity\n# Try to use the GnuPG-Agent. With this option, GnuPG first tries to connect to\n# the agent before it asks for a passphrase.\nuse-agent\n#-----------------------------\n# keyserver\n#-----------------------------\n# This is the server that --recv-keys, --send-keys, and --search-keys will\n# communicate with to receive keys from, send keys to, and search for keys on\nkeyserver hkps://hkps.pool.sks-keyservers.net\n# Provide a certificate store to override the system default\n# Get this from https://sks-keyservers.net/sks-keyservers.netCA.pem\n#keyserver-options ca-cert-file=/usr/local/etc/ssl/certs/hkps.pool.sks-keyservers.net.pem\n# Set the proxy to use for HTTP and HKP keyservers - default to the standard\n# local Tor socks proxy\n# It is encouraged to use Tor for improved anonymity. Preferably use either a\n# dedicated SOCKSPort for GnuPG and/or enable IsolateDestPort and\n# IsolateDestAddr\n#keyserver-options http-proxy=socks5-hostname://127.0.0.1:9050\n# Don't leak DNS, see https://trac.torproject.org/projects/tor/ticket/2846\n#keyserver-options no-try-dns-srv\n# When using --refresh-keys, if the key in question has a preferred keyserver\n# URL, then disable use of that preferred keyserver to refresh the key from\nkeyserver-options no-honor-keyserver-url\n# When searching for a key with --search-keys, include keys that are marked on\n# the keyserver as revoked\nkeyserver-options include-revoked\n#-----------------------------\n# algorithm and ciphers\n#-----------------------------\n# list of personal digest preferences. When multiple digests are supported by\n# all recipients, choose the strongest one\npersonal-cipher-preferences AES256 AES192 AES CAST5\n# list of personal digest preferences. When multiple ciphers are supported by\n# all recipients, choose the strongest one\npersonal-digest-preferences SHA512 SHA384 SHA256 SHA224\n# message digest algorithm used when signing a key\ncert-digest-algo SHA512\n# This preference list is used for new keys and becomes the default for\n# \"setpref\" in the edit menu\ndefault-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed\n\ndefault-recipient-self\n# GPGConf disabled this option here at Di 26 Aug 2008 14:22:45 CEST\n# keyserver pgpkeys.pca.dfn.de\ndefault-cert-check-level 3\nno-mangle-dos-filenames\nno-secmem-warning\nuse-agent\n\n#throw-keyids\n\ndefault-key  FEEDBEEF\nutf8-strings\nencrypt-to   DEADBEEF\n`\n\tcfg, err := parseGpgConfig(bytes.NewReader([]byte(in)))\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"SHA512\", cfg[\"cert-digest-algo\"])\n\t_, found := cfg[\"throw-keyids\"]\n\tassert.False(t, found)\n\tassert.Equal(t, \"DEADBEEF\", cfg[\"encrypt-to\"])\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/utils_windows.go",
    "content": "//go:build windows\n\npackage gpgconf\n\nfunc TTY() string {\n\treturn \"\"\n}\n\nfunc Umask(mask int) int {\n\treturn -1\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/version.go",
    "content": "package gpgconf\n\nimport (\n\t\"context\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\ntype gpgBin struct {\n\tpath string\n\tver  semver.Version\n}\n\ntype byVersion []gpgBin\n\nfunc (v byVersion) Len() int {\n\treturn len(v)\n}\n\nfunc (v byVersion) Swap(i, j int) {\n\tv[i], v[j] = v[j], v[i]\n}\n\nfunc (v byVersion) Less(i, j int) bool {\n\treturn v[i].ver.LT(v[j].ver)\n}\n\n// Version return the version of the gpg binary.\nfunc Version(ctx context.Context, binary string) semver.Version {\n\tv := semver.Version{}\n\n\tcmd := exec.CommandContext(ctx, binary, \"--version\")\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn v\n\t}\n\n\tfor line := range strings.SplitSeq(string(out), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif !strings.HasPrefix(line, \"gpg \") {\n\t\t\tcontinue\n\t\t}\n\n\t\tp := strings.Fields(line)\n\t\tif len(p) < 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tsv, err := semver.Parse(p[len(p)-1])\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn sv\n\t}\n\n\treturn v\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/gpgconf/version_test.go",
    "content": "package gpgconf\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSort(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tin   []gpgBin\n\t\tout  []semver.Version\n\t}{\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin: []gpgBin{\n\t\t\t\t{\n\t\t\t\t\tpath: \"/usr/local/bin/gpg\",\n\t\t\t\t\tver:  semver.MustParse(\"1.9.1\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tpath: \"/usr/bin/gpg\",\n\t\t\t\t\tver:  semver.MustParse(\"2.4.0\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tpath: \"/usr/local/bin/gpg2\",\n\t\t\t\t\tver:  semver.MustParse(\"2.1.11\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tout: []semver.Version{\n\t\t\t\tsemver.MustParse(\"1.9.1\"),\n\t\t\t\tsemver.MustParse(\"2.1.11\"),\n\t\t\t\tsemver.MustParse(\"2.4.0\"),\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsort.Sort(byVersion(tc.in))\n\n\t\t\trequire.Len(t, tc.in, len(tc.out))\n\t\t\tfor i, v := range tc.out {\n\t\t\t\tif !tc.in[i].ver.Equals(v) {\n\t\t\t\t\tt.Errorf(\"wrong sort order at %d: %s != %s\", i, tc.in[i].ver, v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/identity.go",
    "content": "package gpg\n\nimport \"time\"\n\n// Identity is a GPG identity, one key can have many IDs.\ntype Identity struct {\n\tName           string\n\tComment        string\n\tEmail          string\n\tCreationDate   time.Time\n\tExpirationDate time.Time\n}\n\n// ID returns the GPG ID format.\nfunc (i Identity) ID() string {\n\tout := i.Name\n\n\tif i.Comment != \"\" {\n\t\tout += \" (\" + i.Comment + \")\"\n\t}\n\n\tout += \" <\" + i.Email + \">\"\n\n\treturn out\n}\n\n// String implement fmt.Stringer. This method resembles the output gpg uses\n// for user-ids.\nfunc (i Identity) String() string {\n\treturn \"uid                            \" + i.ID()\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/identity_test.go",
    "content": "package gpg\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIdentity(t *testing.T) {\n\tt.Parallel()\n\n\tid := Identity{\n\t\tName:           \"John Doe\",\n\t\tComment:        \"johnny\",\n\t\tEmail:          \"john.doe@example.org\",\n\t\tCreationDate:   time.Now(),\n\t\tExpirationDate: time.Now().Add(time.Hour),\n\t}\n\n\tassert.Equal(t, \"John Doe (johnny) <john.doe@example.org>\", id.ID())\n\tassert.Equal(t, \"uid                            \"+id.ID(), id.String())\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/key.go",
    "content": "package gpg\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Key is a GPG key (public or secret).\ntype Key struct {\n\tKeyType        string\n\tKeyLength      int\n\tValidity       string\n\tCreationDate   time.Time\n\tExpirationDate time.Time\n\tOwnertrust     string\n\tFingerprint    string\n\tIdentities     map[string]Identity\n\tSubKeys        map[string]struct{}\n\tCaps           Capabilities\n}\n\n// Capabilities of a Key.\ntype Capabilities struct {\n\tEncrypt        bool\n\tSign           bool\n\tCertify        bool\n\tAuthentication bool\n\tDeactivated    bool\n}\n\n// IsUseable returns true if GPG would assume this key is useable for encryption.\nfunc (k Key) IsUseable(alwaysTrust bool) bool {\n\tif k.Caps.Deactivated {\n\t\treturn false\n\t}\n\n\tif !k.Caps.Encrypt {\n\t\treturn false\n\t}\n\n\tif !k.ExpirationDate.IsZero() && k.ExpirationDate.Before(time.Now()) {\n\t\treturn false\n\t}\n\n\tif alwaysTrust {\n\t\treturn true\n\t}\n\n\tswitch k.Validity {\n\tcase \"m\":\n\t\treturn true\n\tcase \"f\":\n\t\treturn true\n\tcase \"u\":\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// String implement fmt.Stringer. This method produces output that is close to, but\n// not exactly the same, as the output from GPG itself.\nfunc (k Key) String() string {\n\tfp := \"\"\n\tif len(k.Fingerprint) > 24 {\n\t\tfp = k.Fingerprint[24:]\n\t}\n\n\tvar out strings.Builder\n\tout.WriteString(fmt.Sprintf(\"%s   %dD/0x%s %s\", k.KeyType, k.KeyLength, fp, k.CreationDate.Format(\"2006-01-02\")))\n\tif !k.ExpirationDate.IsZero() {\n\t\tout.WriteString(fmt.Sprintf(\" [expires: %s]\", k.ExpirationDate.Format(\"2006-01-02\")))\n\t}\n\n\tout.WriteString(\"\\n      Key fingerprint = \" + k.Fingerprint)\n\tfor _, id := range k.Identities {\n\t\tout.WriteString(fmt.Sprintf(\"\\n%s\", id))\n\t}\n\n\treturn out.String()\n}\n\n// OneLine prints a terse representation of this key on one line (includes only\n// the first identity!).\nfunc (k Key) OneLine() string {\n\tif len(k.Fingerprint) < 24 {\n\t\treturn fmt.Sprintf(\"(invalid:%s)\", k.Fingerprint)\n\t}\n\n\treturn fmt.Sprintf(\"0x%s - %s\", k.Fingerprint[24:], k.Identity().ID())\n}\n\n// Identity returns the first identity.\nfunc (k Key) Identity() Identity {\n\tids := make([]Identity, 0, len(k.Identities))\n\tfor _, i := range k.Identities {\n\t\tids = append(ids, i)\n\t}\n\n\tsort.Slice(ids, func(i, j int) bool {\n\t\treturn ids[i].CreationDate.After(ids[j].CreationDate)\n\t})\n\n\tfor _, i := range ids {\n\t\treturn i\n\t}\n\n\treturn Identity{}\n}\n\n// ID returns the short fingerprint.\nfunc (k Key) ID() string {\n\tif len(k.Fingerprint) < 25 {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"0x%s\", k.Fingerprint[24:])\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/key_list.go",
    "content": "package gpg\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// ErrKeyNotFound is returned when a key is not found.\nvar ErrKeyNotFound = fmt.Errorf(\"no matching key found\")\n\n// KeyList is a searchable slice of Keys.\ntype KeyList []Key\n\n// Recipients returns the KeyList formatted as a recipient list.\nfunc (kl KeyList) Recipients() []string {\n\tsort.Sort(kl)\n\tu := make(map[string]struct{}, len(kl))\n\n\tfor _, k := range kl {\n\t\tu[k.ID()] = struct{}{}\n\t}\n\n\tl := make([]string, 0, len(kl))\n\tfor k := range u {\n\t\tl = append(l, k)\n\t}\n\n\tsort.Strings(l)\n\n\treturn l\n}\n\n// UseableKeys returns the list of useable (valid keys).\nfunc (kl KeyList) UseableKeys(alwaysTrust bool) KeyList {\n\tnkl := make(KeyList, 0, len(kl))\n\tsort.Sort(kl)\n\n\tfor _, k := range kl {\n\t\tif !k.IsUseable(alwaysTrust) {\n\t\t\tcontinue\n\t\t}\n\n\t\tnkl = append(nkl, k)\n\t}\n\n\treturn nkl\n}\n\n// UnusableKeys returns the list of unusable keys (invalid keys).\nfunc (kl KeyList) UnusableKeys(alwaysTrust bool) KeyList {\n\tnkl := make(KeyList, 0, len(kl))\n\n\tfor _, k := range kl {\n\t\tif k.IsUseable(alwaysTrust) {\n\t\t\tcontinue\n\t\t}\n\n\t\tnkl = append(nkl, k)\n\t}\n\n\tsort.Sort(nkl)\n\n\treturn nkl\n}\n\n// FindKey will try to find the requested key.\nfunc (kl KeyList) FindKey(id string) (Key, error) {\n\tid = strings.TrimPrefix(id, \"0x\")\n\n\tfor _, k := range kl {\n\t\tif k.Fingerprint == id {\n\t\t\treturn k, nil\n\t\t}\n\n\t\tif strings.HasSuffix(k.Fingerprint, id) {\n\t\t\treturn k, nil\n\t\t}\n\n\t\tfor _, ident := range k.Identities {\n\t\t\tif ident.Name == id {\n\t\t\t\treturn k, nil\n\t\t\t}\n\n\t\t\tif ident.Email == id {\n\t\t\t\treturn k, nil\n\t\t\t}\n\t\t}\n\n\t\tfor sk := range k.SubKeys {\n\t\t\tif strings.HasSuffix(sk, id) {\n\t\t\t\treturn k, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn Key{}, ErrKeyNotFound\n}\n\nfunc (kl KeyList) Len() int {\n\treturn len(kl)\n}\n\nfunc (kl KeyList) Less(i, j int) bool {\n\treturn kl[i].Identity().Name < kl[j].Identity().Name\n}\n\nfunc (kl KeyList) Swap(i, j int) {\n\tkl[i], kl[j] = kl[j], kl[i]\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/key_list_test.go",
    "content": "package gpg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestKeyList(t *testing.T) {\n\tt.Parallel()\n\n\tkl := KeyList{\n\t\tgenTestKey(\"John\", \"johnny\", \"Doe\", \"john.doe@example.org\"),\n\t\tgenTestKey(\"Jane\", \"jane\", \"Doe\", \"jane.doe@example.org\", \"25FF1614B8F87B52FFFF99B962AF4031C82E0019\"),\n\t\tgenTestKey(\"Jim\", \"jimmy\", \"Doe\", \"jim.doe@example.org\", \"25FF1614B8F87B52FFFF99B962AF4031C82E2019\", \"z\", \"none\"),\n\t}\n\tkl[2].SubKeys = map[string]struct{}{\n\t\t\"0xDEADBEEF\": {},\n\t}\n\n\tassert.Equal(t, []string{\n\t\t\"0x62AF4031C82E0019\",\n\t\t\"0x62AF4031C82E0039\",\n\t\t\"0x62AF4031C82E2019\",\n\t}, kl.Recipients())\n\tassert.Equal(t, []string{\n\t\t\"0x62AF4031C82E0019\",\n\t\t\"0x62AF4031C82E0039\",\n\t}, kl.UseableKeys(false).Recipients())\n\tassert.Equal(t, []string{\n\t\t\"0x62AF4031C82E2019\",\n\t}, kl.UnusableKeys(false).Recipients())\n\n\t// search by email\n\tk, err := kl.FindKey(\"jim.doe@example.org\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0x62AF4031C82E2019\", k.ID())\n\n\t// search by fp\n\tk, err = kl.FindKey(\"25FF1614B8F87B52FFFF99B962AF4031C82E2019\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0x62AF4031C82E2019\", k.ID())\n\n\t// search by id\n\tk, err = kl.FindKey(\"0x62AF4031C82E2019\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0x62AF4031C82E2019\", k.ID())\n\n\t// search for non existing key\n\tk, err = kl.FindKey(\"0x62AF4091C82E2019\")\n\trequire.Error(t, err)\n\tassert.Empty(t, k.ID())\n\n\t// search by full name\n\tk, err = kl.FindKey(\"John Doe\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0x62AF4031C82E0039\", k.ID())\n\n\t// search by subkey id\n\tk, err = kl.FindKey(\"0xDEADBEEF\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0x62AF4031C82E2019\", k.ID())\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpg/key_test.go",
    "content": "package gpg\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc genTestKey(args ...string) Key {\n\tfirst := \"John\"\n\tif len(args) > 0 && args[0] != \"\" {\n\t\tfirst = args[0]\n\t}\n\n\tnick := \"johnny\"\n\tif len(args) > 1 && args[1] != \"\" {\n\t\tnick = args[1]\n\t}\n\n\tlast := \"Doe\"\n\tif len(args) > 2 && args[2] != \"\" {\n\t\tlast = args[2]\n\t}\n\n\temail := \"john.doe@example.org\"\n\tif len(args) > 3 && args[3] != \"\" {\n\t\temail = args[3]\n\t}\n\n\tfp := \"25FF1614B8F87B52FFFF99B962AF4031C82E0039\"\n\tif len(args) > 4 && args[4] != \"\" {\n\t\tfp = args[4]\n\t}\n\n\tvalidity := \"u\"\n\tif len(args) > 5 && args[5] != \"\" {\n\t\tvalidity = args[5]\n\t}\n\n\ttrust := \"ultimate\"\n\tif len(args) > 6 && args[6] != \"\" {\n\t\ttrust = args[6]\n\t}\n\n\tcreation := time.Date(2018, 1, 1, 1, 1, 1, 0, time.UTC)\n\texpiration := time.Date(2218, 1, 1, 1, 1, 1, 0, time.UTC)\n\n\treturn Key{\n\t\tKeyType:        \"sec\",\n\t\tKeyLength:      2048,\n\t\tValidity:       validity,\n\t\tCreationDate:   creation,\n\t\tExpirationDate: expiration,\n\t\tOwnertrust:     trust,\n\t\tFingerprint:    fp,\n\t\tIdentities: map[string]Identity{\n\t\t\tfmt.Sprintf(\"%s %s (%s) <%s>\", first, last, nick, email): {\n\t\t\t\tName:           fmt.Sprintf(\"%s %s\", first, last),\n\t\t\t\tComment:        nick,\n\t\t\t\tEmail:          email,\n\t\t\t\tCreationDate:   creation,\n\t\t\t\tExpirationDate: expiration,\n\t\t\t},\n\t\t},\n\t\tCaps: Capabilities{\n\t\t\tEncrypt:        true,\n\t\t\tSign:           false,\n\t\t\tCertify:        false,\n\t\t\tAuthentication: false,\n\t\t\tDeactivated:    false,\n\t\t},\n\t}\n}\n\nfunc TestKey(t *testing.T) {\n\tt.Parallel()\n\n\tk := Key{\n\t\tIdentities: map[string]Identity{},\n\t}\n\tassert.Equal(t, \"(invalid:)\", k.OneLine())\n\tassert.Empty(t, k.Identity().Name)\n\tk = genTestKey()\n\tassert.True(t, k.IsUseable(false))\n\tassert.Equal(t, \"sec   2048D/0x62AF4031C82E0039 2018-01-01 [expires: 2218-01-01]\\n      Key fingerprint = 25FF1614B8F87B52FFFF99B962AF4031C82E0039\\nuid                            John Doe (johnny) <john.doe@example.org>\", k.String())\n\tassert.Equal(t, \"0x62AF4031C82E0039 - John Doe (johnny) <john.doe@example.org>\", k.OneLine())\n\tassert.Equal(t, \"0x62AF4031C82E0039\", k.ID())\n}\n\nfunc TestIdentitySort(t *testing.T) {\n\tt.Parallel()\n\n\tcreation := time.Date(2017, 1, 1, 1, 1, 1, 0, time.UTC)\n\texpiration := time.Date(2018, 1, 1, 1, 1, 1, 0, time.UTC)\n\n\tk := genTestKey()\n\tk.Identities[\"Foo Bar\"] = Identity{\n\t\tName:           \"Foo Bar\",\n\t\tComment:        \"foo\",\n\t\tEmail:          \"foo.bar@example.com\",\n\t\tCreationDate:   creation,\n\t\tExpirationDate: expiration,\n\t}\n\tassert.Equal(t, \"0x62AF4031C82E0039 - John Doe (johnny) <john.doe@example.org>\", k.OneLine())\n}\n\nfunc TestUseability(t *testing.T) {\n\tt.Parallel()\n\n\t// invalid\n\tfor _, k := range []Key{\n\t\t{},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(-time.Second),\n\t\t\tCaps:           Capabilities{Encrypt: true},\n\t\t},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tCaps:           Capabilities{Encrypt: true},\n\t\t\tValidity:       \"z\",\n\t\t},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tCaps:           Capabilities{Deactivated: true},\n\t\t},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tCaps:           Capabilities{Encrypt: false},\n\t\t},\n\t} {\n\t\tassert.False(t, k.IsUseable(false))\n\t}\n\t// valid\n\tfor _, k := range []Key{\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tValidity:       \"m\",\n\t\t\tCaps:           Capabilities{Encrypt: true},\n\t\t},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tValidity:       \"f\",\n\t\t\tCaps:           Capabilities{Encrypt: true},\n\t\t},\n\t\t{\n\t\t\tExpirationDate: time.Now().Add(time.Hour),\n\t\t\tValidity:       \"u\",\n\t\t\tCaps:           Capabilities{Encrypt: true},\n\t\t},\n\t} {\n\t\tassert.True(t, k.IsUseable(false))\n\t}\n}\n"
  },
  {
    "path": "internal/backend/crypto/gpgcli.go",
    "content": "package crypto\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/crypto/gpg/cli\" // register gpg cli backend\n"
  },
  {
    "path": "internal/backend/crypto/plain/backend.go",
    "content": "// Package plain implements a plaintext backend\npackage plain\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar staticPrivateKeyList = gpg.KeyList{\n\tgpg.Key{\n\t\tKeyType:      \"rsa\",\n\t\tKeyLength:    2048,\n\t\tValidity:     \"u\",\n\t\tCreationDate: time.Now(),\n\t\tFingerprint:  \"000000000000000000000000DEADBEEF\",\n\t\tIdentities: map[string]gpg.Identity{\n\t\t\t\"Dead Beef <dead.beef@example.com>\": {\n\t\t\t\tName:         \"Dead Beef\",\n\t\t\t\tEmail:        \"dead.beef@example.com\",\n\t\t\t\tCreationDate: time.Now(),\n\t\t\t},\n\t\t},\n\t},\n\tgpg.Key{\n\t\tKeyType:      \"rsa\",\n\t\tKeyLength:    2048,\n\t\tValidity:     \"u\",\n\t\tCreationDate: time.Now(),\n\t\tFingerprint:  \"000000000000000000000000FEEDBEEF\",\n\t\tIdentities: map[string]gpg.Identity{\n\t\t\t\"Feed Beef <feed.beef@example.com>\": {\n\t\t\t\tName:         \"Feed Beef\",\n\t\t\t\tEmail:        \"feed.beef@example.com\",\n\t\t\t\tCreationDate: time.Now(),\n\t\t\t},\n\t\t},\n\t},\n}\n\n// Mocker is a no-op GPG mock.\ntype Mocker struct{}\n\n// New creates a new GPG mock.\nfunc New() *Mocker {\n\treturn &Mocker{}\n}\n\n// ListRecipients does nothing.\nfunc (m *Mocker) ListRecipients(context.Context) ([]string, error) {\n\treturn staticPrivateKeyList.Recipients(), nil\n}\n\n// FindRecipients does nothing.\nfunc (m *Mocker) FindRecipients(ctx context.Context, keys ...string) ([]string, error) {\n\trs := staticPrivateKeyList.Recipients()\n\tres := make([]string, 0, len(rs))\n\tfor _, r := range rs {\n\t\tfor _, needle := range keys {\n\t\t\tdebug.V(1).Log(\"checking recipient %q = %q\", r, needle)\n\t\t\tif strings.HasSuffix(r, needle) {\n\t\t\t\tres = append(res, r)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\n// ListIdentities does nothing.\nfunc (m *Mocker) ListIdentities(context.Context) ([]string, error) {\n\treturn staticPrivateKeyList.Recipients(), nil\n}\n\n// FindIdentities does nothing.\nfunc (m *Mocker) FindIdentities(ctx context.Context, keys ...string) ([]string, error) {\n\treturn m.FindRecipients(ctx, keys...)\n}\n\n// RecipientIDs does nothing.\nfunc (m *Mocker) RecipientIDs(context.Context, []byte) ([]string, error) {\n\treturn staticPrivateKeyList.Recipients(), nil\n}\n\n// Encrypt writes the input to disk unaltered.\nfunc (m *Mocker) Encrypt(ctx context.Context, content []byte, recipients []string) ([]byte, error) {\n\treturn content, nil\n}\n\n// Decrypt read the file from disk unaltered.\nfunc (m *Mocker) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {\n\treturn ciphertext, nil\n}\n\n// ImportPublicKey does nothing.\nfunc (m *Mocker) ImportPublicKey(context.Context, []byte) error {\n\treturn nil\n}\n\n// Version returns dummy version info.\nfunc (m *Mocker) Version(context.Context) semver.Version {\n\treturn debug.ModuleVersion(\"github.com/gopasspw/gopass/internal/backend/crypto/plain\")\n}\n\n// Binary always returns 'gpg'.\nfunc (m *Mocker) Binary() string {\n\treturn \"gpg\"\n}\n\n// GenerateIdentity is not implemented.\nfunc (m *Mocker) GenerateIdentity(ctx context.Context, name, email, pw string) (string, error) {\n\treturn \"\", fmt.Errorf(\"not yet implemented\")\n}\n\n// Fingerprint returns thd id.\nfunc (m *Mocker) Fingerprint(ctx context.Context, id string) string {\n\treturn id\n}\n\n// FormatKey returns the id.\nfunc (m *Mocker) FormatKey(ctx context.Context, id, tpl string) string {\n\treturn id\n}\n\n// Initialized returns nil.\nfunc (m *Mocker) Initialized(context.Context) error {\n\treturn nil\n}\n\n// Name returns plain, the name of the backend.\nfunc (m *Mocker) Name() string {\n\treturn Name\n}\n\n// Ext returns gpg.\nfunc (m *Mocker) Ext() string {\n\treturn Ext\n}\n\nconst (\n\t// Name is the name of this backend.\n\tName = \"plain\"\n\t// Ext is the file extension used by this backend.\n\tExt = \"txt\"\n\t// IDFile is the name of the recipients file used by this backend.\n\tIDFile = \".plain-id\"\n)\n\n// IDFile returns the name of the recipients file.\nfunc (m *Mocker) IDFile() string {\n\treturn IDFile\n}\n\n// ReadNamesFromKey does nothing.\nfunc (m *Mocker) ReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error) {\n\treturn []string{\"unsupported\"}, nil\n}\n\n// GetFingerprint returns an empty fingerprint.\nfunc (m *Mocker) GetFingerprint(ctx context.Context, key []byte) (string, error) {\n\treturn \"\", nil\n}\n\n// Concurrency returns the number of CPUs.\nfunc (m *Mocker) Concurrency() int {\n\treturn runtime.NumCPU()\n}\n\n// String implements fmt.Stringer.\nfunc (m *Mocker) String() string {\n\treturn \"Plaintext(Encrypt/Decrypt no-op)\"\n}\n"
  },
  {
    "path": "internal/backend/crypto/plain/backend_test.go",
    "content": "package plain\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPlain(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tm := New()\n\tkl, err := m.ListIdentities(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, kl)\n\tassert.Equal(t, \"0xDEADBEEF\", kl[0])\n\n\tkl, err = m.ListRecipients(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, kl, \"ListRecipients\")\n\n\trcs, err := m.RecipientIDs(ctx, []byte{})\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, rcs, \"RecipientIDs\")\n\n\tbuf, err := m.Encrypt(ctx, []byte(\"foobar\"), []string{\"0xDEADBEEF\"})\n\trequire.NoError(t, err)\n\n\tcontent, err := m.Decrypt(ctx, buf)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", string(content))\n\n\tassert.Equal(t, \"gpg\", m.Binary())\n\n\t_, err = m.GenerateIdentity(ctx, \"\", \"\", \"\")\n\trequire.Error(t, err)\n\n\tkl, err = m.FindRecipients(ctx)\n\trequire.NoError(t, err)\n\tassert.Empty(t, kl, \"FindRecipients()\")\n\n\tkl, err = m.FindRecipients(ctx, \"0xDEADBEEF\")\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, kl, \"FindRecipients(0xDEADBEEF)\")\n\n\t_, err = m.FindIdentities(ctx)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, m.ImportPublicKey(ctx, buf))\n\n\tassert.Empty(t, m.FormatKey(ctx, \"\", \"\"))\n\tassert.Empty(t, m.Fingerprint(ctx, \"\"))\n\trequire.NoError(t, m.Initialized(ctx))\n\tassert.Equal(t, \"plain\", m.Name())\n\tassert.Equal(t, \"txt\", m.Ext())\n\tassert.Equal(t, \".plain-id\", m.IDFile())\n\tnames, err := m.ReadNamesFromKey(ctx, nil)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"unsupported\"}, names)\n}\n\nfunc TestLoader(t *testing.T) {\n\tt.Parallel()\n\n\tl := &loader{}\n\tb, err := l.New(config.NewContextInMemory())\n\trequire.NoError(t, err)\n\tassert.Equal(t, name, l.String())\n\tassert.Equal(t, \"plain\", b.Name())\n}\n"
  },
  {
    "path": "internal/backend/crypto/plain/loader.go",
    "content": "package plain\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\tname = \"plain\"\n)\n\nfunc init() {\n\tbackend.CryptoRegistry.Register(backend.Plain, name, &loader{})\n}\n\ntype loader struct{}\n\n// New implements backend.CryptoLoader.\nfunc (l loader) New(ctx context.Context) (backend.Crypto, error) {\n\tdebug.Log(\"Using Crypto Backend: %s (NO ENCRYPTION)\", name)\n\n\treturn New(), nil\n}\n\nfunc (l loader) Handles(ctx context.Context, s backend.Storage) error {\n\tif s.Exists(ctx, IDFile) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"not supported\")\n}\n\nfunc (l loader) Priority() int {\n\treturn 1000\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/crypto/plain.go",
    "content": "package crypto\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/crypto/plain\" // register plaintext backend\n"
  },
  {
    "path": "internal/backend/crypto.go",
    "content": "package backend\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// CryptoBackend is a cryptographic backend.\ntype CryptoBackend int\n\nconst (\n\t// Plain is a no-op crypto backend.\n\tPlain CryptoBackend = iota\n\t// GPGCLI is a gpg-cli based crypto backend.\n\tGPGCLI\n\t// Age - age-encryption.org.\n\tAge\n)\n\nfunc (c CryptoBackend) String() string {\n\tif be, err := CryptoRegistry.BackendName(c); err == nil {\n\t\treturn be\n\t}\n\n\treturn \"\"\n}\n\n// Keyring is a public/private key manager.\ntype Keyring interface {\n\tListRecipients(ctx context.Context) ([]string, error)\n\tListIdentities(ctx context.Context) ([]string, error)\n\n\tFindRecipients(ctx context.Context, needles ...string) ([]string, error)\n\tFindIdentities(ctx context.Context, needles ...string) ([]string, error)\n\n\tFingerprint(ctx context.Context, id string) string\n\tFormatKey(ctx context.Context, id, tpl string) string\n\tReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error)\n\tGetFingerprint(ctx context.Context, key []byte) (string, error)\n\n\tGenerateIdentity(ctx context.Context, name, email, passphrase string) (string, error)\n}\n\n// Crypto is a crypto backend.\ntype Crypto interface {\n\tKeyring\n\n\tEncrypt(ctx context.Context, plaintext []byte, recipients []string) ([]byte, error)\n\tDecrypt(ctx context.Context, ciphertext []byte) ([]byte, error)\n\tRecipientIDs(ctx context.Context, ciphertext []byte) ([]string, error)\n\n\tName() string\n\tVersion(context.Context) semver.Version\n\tInitialized(ctx context.Context) error\n\tExt() string    // filename extension.\n\tIDFile() string // recipient IDs.\n\tConcurrency() int\n}\n\n// NewCrypto instantiates a new crypto backend.\nfunc NewCrypto(ctx context.Context, id CryptoBackend) (Crypto, error) {\n\tif be, err := CryptoRegistry.Get(id); err == nil {\n\t\treturn be.New(ctx)\n\t}\n\n\treturn nil, fmt.Errorf(\"unknown backend %d: %w\", id, ErrNotFound)\n}\n\n// DetectCrypto tries to detect the crypto backend used.\nfunc DetectCrypto(ctx context.Context, storage Storage) (Crypto, error) {\n\tif HasCryptoBackend(ctx) {\n\t\tif be, err := CryptoRegistry.Get(GetCryptoBackend(ctx)); err == nil {\n\t\t\treturn be.New(ctx)\n\t\t}\n\t}\n\n\tfor _, be := range CryptoRegistry.Prioritized() {\n\t\tdebug.Log(\"Trying %s for %s\", be, storage)\n\t\tif err := be.Handles(ctx, storage); err != nil {\n\t\t\tdebug.Log(\"failed to use crypto %s for %s\", be, storage)\n\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"Using %s for %s\", be, storage)\n\n\t\treturn be.New(ctx)\n\t}\n\tdebug.Log(\"No valid crypto provider found for %s\", storage)\n\t// TODO: this should return ErrNotSupported, but need to fix some tests for that\n\treturn nil, nil //nolint:nilnil\n}\n"
  },
  {
    "path": "internal/backend/crypto_test.go",
    "content": "package backend\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDetectCrypto(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tfile string\n\t}{\n\t\t{\n\t\t\tname: \"plain\",\n\t\t\tfile: \".plain-id\",\n\t\t},\n\t\t{\n\t\t\tname: \"gpg\",\n\t\t\tfile: \".gpg-id\",\n\t\t},\n\t\t{\n\t\t\tname: \"age\",\n\t\t\tfile: \".age-recipients\",\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tctx := config.NewContextInMemory()\n\n\t\t\tfsDir := filepath.Join(t.TempDir(), \"fs\")\n\t\t\t_ = os.RemoveAll(fsDir)\n\t\t\trequire.NoError(t, os.MkdirAll(fsDir, 0o700))\n\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(fsDir, tc.file), []byte(\"foo\"), 0o600))\n\n\t\t\tr, err := DetectStorage(ctx, fsDir)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotNil(t, r)\n\t\t\tassert.Equal(t, \"fs\", r.Name())\n\n\t\t\tc, err := DetectCrypto(ctx, r)\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t\trequire.NotNil(t, c, tc.name)\n\t\t\tassert.Equal(t, tc.name, c.Name())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/doc.go",
    "content": "// Package backend implements a registry to register differnet plugable backends for encryption and storage (incl. version control).\n// The actual backends are implemented in the subpackages. They register themselves in the registry with blank imports.\npackage backend\n"
  },
  {
    "path": "internal/backend/rcs.go",
    "content": "package backend\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// rcs is a revision control backend.\ntype rcs interface {\n\tAdd(ctx context.Context, args ...string) error\n\tCommit(ctx context.Context, msg string) error\n\tPush(ctx context.Context, remote, location string) error\n\tPull(ctx context.Context, remote, location string) error\n\n\tTryAdd(ctx context.Context, args ...string) error\n\tTryCommit(ctx context.Context, msg string) error\n\tTryPush(ctx context.Context, remote, location string) error\n\n\tInitConfig(ctx context.Context, name, email string) error\n\tAddRemote(ctx context.Context, remote, location string) error\n\tRemoveRemote(ctx context.Context, remote string) error\n\n\tRevisions(ctx context.Context, name string) ([]Revision, error)\n\tGetRevision(ctx context.Context, name, revision string) ([]byte, error)\n\n\tStatus(ctx context.Context) ([]byte, error)\n\tCompact(ctx context.Context) error\n}\n\n// Revision is a SCM revision.\ntype Revision struct {\n\tHash        string\n\tAuthorName  string\n\tAuthorEmail string\n\tDate        time.Time\n\tSubject     string\n\tBody        string\n}\n\n// Revisions implements the sort interface.\ntype Revisions []Revision\n\nfunc (r Revisions) Len() int {\n\treturn len(r)\n}\n\nfunc (r Revisions) Less(i, j int) bool {\n\treturn r[i].Date.After(r[j].Date)\n}\n\nfunc (r Revisions) Swap(i, j int) {\n\tr[i], r[j] = r[j], r[i]\n}\n\n// Clone clones an existing repository from a remote.\nfunc Clone(ctx context.Context, id StorageBackend, repo, path string) (Storage, error) {\n\tif be, err := StorageRegistry.Get(id); err == nil {\n\t\tdebug.Log(\"Cloning with %s\", be.String())\n\n\t\treturn be.Clone(ctx, repo, path)\n\t}\n\n\treturn nil, fmt.Errorf(\"unknown backend %d: %w\", id, ErrNotFound)\n}\n"
  },
  {
    "path": "internal/backend/rcs_test.go",
    "content": "package backend\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClone(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ttd := t.TempDir()\n\n\trepo := filepath.Join(td, \"repo\")\n\trequire.NoError(t, os.MkdirAll(repo, 0o700))\n\n\tstore := filepath.Join(td, \"store\")\n\trequire.NoError(t, os.MkdirAll(store, 0o700))\n\n\tcmd := exec.Command(\"git\", \"init\", repo)\n\trequire.NoError(t, cmd.Run())\n\n\tr, err := Clone(ctx, GitFS, repo, store)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, r)\n}\n\nfunc TestInitRCS(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ttd := t.TempDir()\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tout.Stderr = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t\tout.Stderr = os.Stderr\n\t}()\n\n\tgitDir := filepath.Join(td, \"git\")\n\trequire.NoError(t, os.MkdirAll(filepath.Join(gitDir, \".git\"), 0o700))\n\n\tr, err := InitStorage(ctx, GitFS, gitDir)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, r)\n}\n"
  },
  {
    "path": "internal/backend/registry.go",
    "content": "package backend\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\nvar (\n\t// CryptoRegistry is the global registry of available crypto backends.\n\tCryptoRegistry = NewRegistry[CryptoBackend, CryptoLoader]()\n\t// StorageRegistry is the global registry of available storage backends.\n\tStorageRegistry = NewRegistry[StorageBackend, StorageLoader]()\n\n\t// ErrNotFound is returned if the requested backend was not found.\n\tErrNotFound = fmt.Errorf(\"backend not found\")\n)\n\n// Prioritized is the interface for prioritized items.\ntype Prioritized interface {\n\tPriority() int\n}\n\n// CryptoLoader is the interface for creating a new crypto backend.\ntype CryptoLoader interface {\n\tfmt.Stringer\n\tPrioritized\n\tNew(context.Context) (Crypto, error)\n\tHandles(context.Context, Storage) error\n}\n\n// StorageLoader is the interface for creating a new storage backend.\ntype StorageLoader interface {\n\tfmt.Stringer\n\tPrioritized\n\tNew(context.Context, string) (Storage, error)\n\tInit(context.Context, string) (Storage, error)\n\tClone(context.Context, string, string) (Storage, error)\n\tHandles(context.Context, string) error\n}\n\n// NewRegistry returns a new registry.\nfunc NewRegistry[K comparable, V Prioritized]() *Registry[K, V] {\n\treturn &Registry[K, V]{\n\t\tbackends:      map[K]V{},\n\t\tnameToBackend: map[string]K{},\n\t\tbackendToName: map[K]string{},\n\t}\n}\n\n// Registry is a registry of backends.\ntype Registry[K comparable, V Prioritized] struct {\n\tsync.RWMutex\n\n\tbackends      map[K]V\n\tnameToBackend map[string]K\n\tbackendToName map[K]string\n}\n\nfunc (r *Registry[K, V]) Register(backend K, name string, loader V) {\n\tr.Lock()\n\tdefer r.Unlock()\n\n\tr.backends[backend] = loader\n\tr.nameToBackend[name] = backend\n\tr.backendToName[backend] = name\n}\n\nfunc (r *Registry[K, V]) BackendNames() []string {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\treturn set.SortedKeys(r.nameToBackend)\n}\n\nfunc (r *Registry[K, V]) Backends() []V {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tbes := make([]V, 0, len(r.backends))\n\tfor _, be := range r.backends {\n\t\tbes = append(bes, be)\n\t}\n\n\treturn bes\n}\n\nfunc (r *Registry[K, V]) Prioritized() []V {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tbes := maps.Values(r.backends)\n\n\treturn slices.SortedFunc(bes, func(a, b V) int {\n\t\treturn cmp.Compare(a.Priority(), b.Priority())\n\t})\n}\n\nfunc (r *Registry[K, V]) Get(key K) (V, error) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tif be, found := r.backends[key]; found {\n\t\treturn be, nil\n\t}\n\tvar zero V\n\n\treturn zero, ErrNotFound\n}\n\nfunc (r *Registry[K, V]) Backend(name string) (K, error) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tif name == \"gpg\" {\n\t\tname = \"gpgcli\"\n\t}\n\tbackend, ok := r.nameToBackend[name]\n\tif !ok {\n\t\tvar zero K\n\n\t\treturn zero, ErrNotFound\n\t}\n\n\treturn backend, nil\n}\n\nfunc (r *Registry[K, V]) BackendName(backend K) (string, error) {\n\tr.RLock()\n\tdefer r.RUnlock()\n\n\tname, ok := r.backendToName[backend]\n\tif !ok {\n\t\treturn \"\", ErrNotFound\n\t}\n\n\treturn name, nil\n}\n"
  },
  {
    "path": "internal/backend/registry_test.go",
    "content": "package backend_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype fakeCryptoLoaderHighPrio struct{}\n\nfunc (l fakeCryptoLoaderHighPrio) New(context.Context) (backend.Crypto, error) {\n\treturn plain.New(), nil\n}\n\nfunc (l fakeCryptoLoaderHighPrio) String() string {\n\treturn \"fakeCryptoLoaderHighPrio\"\n}\n\nfunc (l fakeCryptoLoaderHighPrio) Handles(context.Context, backend.Storage) error {\n\treturn nil\n}\n\nfunc (l fakeCryptoLoaderHighPrio) Priority() int {\n\treturn 2\n}\n\ntype fakeCryptoLoaderLowPrio struct{}\n\nfunc (l fakeCryptoLoaderLowPrio) New(context.Context) (backend.Crypto, error) {\n\treturn plain.New(), nil\n}\n\nfunc (l fakeCryptoLoaderLowPrio) String() string {\n\treturn \"fakeCryptoLoaderLowPrio\"\n}\n\nfunc (l fakeCryptoLoaderLowPrio) Handles(context.Context, backend.Storage) error {\n\treturn nil\n}\n\nfunc (l fakeCryptoLoaderLowPrio) Priority() int {\n\treturn 1\n}\n\nfunc TestCryptoLoader(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tbackend.CryptoRegistry.Register(backend.Plain, \"plain\", fakeCryptoLoaderHighPrio{})\n\tc, err := backend.NewCrypto(ctx, backend.Plain)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"plain\", c.Name())\n}\n\nfunc TestRegistry_BackendNames(t *testing.T) {\n\tt.Parallel()\n\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.Plain, \"plain\", fakeCryptoLoaderHighPrio{})\n\tregistry.Register(backend.GPGCLI, \"gpgcli\", fakeCryptoLoaderHighPrio{})\n\tregistry.Register(backend.Age, \"age\", fakeCryptoLoaderHighPrio{})\n\n\texpected := []string{\"age\", \"gpgcli\", \"plain\"}\n\tactual := registry.BackendNames()\n\tassert.Equal(t, expected, actual, \"backend names should be sorted\")\n}\n\nfunc TestRegistry_Backends(t *testing.T) {\n\tt.Parallel()\n\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.Plain, \"plain\", fakeCryptoLoaderHighPrio{})\n\tregistry.Register(backend.GPGCLI, \"gpgcli\", fakeCryptoLoaderHighPrio{})\n\tregistry.Register(backend.Age, \"age\", fakeCryptoLoaderLowPrio{})\n\n\t// iteration order of map is random, so it's hard to test the actual content\n\tassert.Len(t, registry.Backends(), 3, \"should return all registered backend loaders\")\n}\n\nfunc TestRegistry_Prioritized(t *testing.T) {\n\tt.Parallel()\n\n\thighPrio := fakeCryptoLoaderHighPrio{}\n\tlowPrio := fakeCryptoLoaderLowPrio{}\n\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.Plain, \"plain\", highPrio)\n\tregistry.Register(backend.GPGCLI, \"gpgcli\", lowPrio)\n\n\texpected := []backend.CryptoLoader{lowPrio, highPrio}\n\tactual := registry.Prioritized()\n\tassert.Equal(t, expected, actual, \"should return in ascending priority order\")\n}\n\nfunc TestRegistry_Get(t *testing.T) {\n\tt.Parallel()\n\n\tloader := fakeCryptoLoaderHighPrio{}\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.Plain, \"plain\", loader)\n\n\ttests := map[string]struct {\n\t\tbackend backend.CryptoBackend\n\t\twant    backend.CryptoLoader\n\t\twantErr error\n\t}{\n\t\t\"backend exists\": {\n\t\t\tbackend: backend.Plain,\n\t\t\twant:    loader,\n\t\t\twantErr: nil,\n\t\t},\n\t\t\"backend does not exist\": {\n\t\t\tbackend: backend.GPGCLI,\n\t\t\twant:    nil,\n\t\t\twantErr: backend.ErrNotFound,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv, err := registry.Get(tt.backend)\n\t\t\tassert.Equal(t, tt.want, v)\n\t\t\tassert.Equal(t, tt.wantErr, err)\n\t\t})\n\t}\n}\n\nfunc TestRegistry_Backend(t *testing.T) {\n\tt.Parallel()\n\n\tloader := fakeCryptoLoaderHighPrio{}\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.GPGCLI, \"gpgcli\", loader)\n\n\ttests := map[string]struct {\n\t\tbackendName string\n\t\twant        backend.CryptoBackend\n\t\twantErr     error\n\t}{\n\t\t\"backend name exists\": {\n\t\t\tbackendName: \"gpgcli\",\n\t\t\twant:        backend.GPGCLI,\n\t\t\twantErr:     nil,\n\t\t},\n\t\t\"backend name does not exist\": {\n\t\t\tbackendName: \"fake\",\n\t\t\twant:        0, // zero value\n\t\t\twantErr:     backend.ErrNotFound,\n\t\t},\n\t\t`special case: \"gpg\" name should be handled as \"gpgcli\"`: {\n\t\t\tbackendName: \"gpg\",\n\t\t\twant:        backend.GPGCLI,\n\t\t\twantErr:     nil,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv, err := registry.Backend(tt.backendName)\n\t\t\tassert.Equal(t, tt.want, v)\n\t\t\tassert.Equal(t, tt.wantErr, err)\n\t\t})\n\t}\n}\n\nfunc TestRegistry_BackendName(t *testing.T) {\n\tt.Parallel()\n\n\tregistry := backend.NewRegistry[backend.CryptoBackend, backend.CryptoLoader]()\n\tregistry.Register(backend.Plain, \"plain\", fakeCryptoLoaderHighPrio{})\n\n\ttests := map[string]struct {\n\t\tbackend backend.CryptoBackend\n\t\twant    string\n\t\twantErr error\n\t}{\n\t\t\"backend exists\": {\n\t\t\tbackend: backend.Plain,\n\t\t\twant:    \"plain\",\n\t\t\twantErr: nil,\n\t\t},\n\t\t\"backend does not exist\": {\n\t\t\tbackend: backend.GPGCLI,\n\t\t\twant:    \"\", // zero value\n\t\t\twantErr: backend.ErrNotFound,\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv, err := registry.BackendName(tt.backend)\n\t\t\tassert.Equal(t, tt.want, v)\n\t\t\tassert.Equal(t, tt.wantErr, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/cryptfs/crypt.go",
    "content": "// Package cryptfs implements a filename encrypting storage backend.\npackage cryptfs\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\tname = \"cryptfs\"\n\t// mappingFile is the file that contains the name mapping.\n\tmappingFile = \".gopass-mapping\"\n)\n\n// Crypt is a storage backend that encrypts filenames.\ntype Crypt struct {\n\tsub      backend.Storage\n\tcrypto   *age.Age\n\tpath     string\n\tmux      sync.RWMutex\n\tmappings map[string]string\n}\n\n// newCrypt creates a new cryptfs backend.\nfunc newCrypt(ctx context.Context, sub backend.Storage) (*Crypt, error) {\n\ta, err := age.New(ctx, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Crypt{\n\t\tsub:      sub,\n\t\tcrypto:   a,\n\t\tpath:     sub.Path(),\n\t\tmappings: make(map[string]string),\n\t}\n\n\tif err := c.loadMappings(ctx); err != nil {\n\t\tout.Warningf(ctx, \"Failed to load mappings: %s\", err)\n\t}\n\tdebug.Log(\"Loaded %d mappings\", len(c.mappings))\n\n\treturn c, nil\n}\n\nfunc (c *Crypt) hash(s string) string {\n\th := sha256.New()\n\th.Write([]byte(s))\n\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\nfunc (c *Crypt) loadMappings(ctx context.Context) error {\n\tif !c.sub.Exists(ctx, mappingFile) {\n\t\treturn nil\n\t}\n\n\tciphertext, err := c.sub.Get(ctx, mappingFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tplaintext, err := c.crypto.Decrypt(ctx, ciphertext)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn json.Unmarshal(plaintext, &c.mappings)\n}\n\nfunc (c *Crypt) saveMappings(ctx context.Context) error {\n\tplaintext, err := json.MarshalIndent(c.mappings, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecipientsFile := c.crypto.IDFile()\n\tcontent, err := c.sub.Get(ctx, recipientsFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read recipients file %s: %w\", recipientsFile, err)\n\t}\n\n\trecipients := strings.Split(strings.TrimSpace(string(content)), \"\\n\")\n\tif len(recipients) == 0 || (len(recipients) == 1 && recipients[0] == \"\") {\n\t\treturn fmt.Errorf(\"no recipients found in %s\", recipientsFile)\n\t}\n\n\tciphertext, err := c.crypto.Encrypt(ctx, plaintext, recipients)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.sub.Set(ctx, mappingFile, ciphertext)\n}\n\n// String implements fmt.Stringer.\nfunc (c *Crypt) String() string {\n\treturn name\n}\n\n// Name returns the name of the backend.\nfunc (c *Crypt) Name() string {\n\treturn name\n}\n\n// Path returns the path of the backend.\nfunc (c *Crypt) Path() string {\n\treturn c.path\n}\n\n// Version returns the version of the backend.\nfunc (c *Crypt) Version(ctx context.Context) semver.Version {\n\treturn semver.Version{Major: 1}\n}\n\n// Fsck performs a consistency check on the backend.\nfunc (c *Crypt) Fsck(ctx context.Context) error {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\t// list all files in sub-storage\n\tallFiles, err := c.sub.List(ctx, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// create a set of mapped hashes\n\tmappedHashes := make(map[string]struct{})\n\tfor _, h := range c.mappings {\n\t\tmappedHashes[h] = struct{}{}\n\t}\n\n\t// find orphans and delete them\n\tfor _, file := range allFiles {\n\t\tif file == mappingFile {\n\t\t\tcontinue\n\t\t}\n\t\tif c.sub.IsDir(ctx, file) {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := mappedHashes[file]; !ok {\n\t\t\tif err := c.sub.Delete(ctx, file); err != nil {\n\t\t\t\tout.Warningf(ctx, \"Failed to prune orphaned file %s: %s\", file, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn c.sub.Fsck(ctx)\n}\n\nfunc (c *Crypt) Prune(ctx context.Context, prefix string) error {\n\treturn c.sub.Prune(ctx, prefix)\n}\n\n// Link creates a symlink.\nfunc (c *Crypt) Link(ctx context.Context, from, to string) error {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\th, ok := c.mappings[from]\n\tif !ok {\n\t\treturn os.ErrNotExist\n\t}\n\tif _, ok := c.mappings[to]; ok {\n\t\treturn fmt.Errorf(\"destination %s already exists\", to)\n\t}\n\n\tc.mappings[to] = h\n\n\treturn c.saveMappings(ctx)\n}\n\n// rcs methods.\nfunc (c *Crypt) getCryptoExt(ctx context.Context) string {\n\tcryptoID := backend.GetCryptoBackend(ctx)\n\tloader, err := backend.CryptoRegistry.Get(cryptoID)\n\tif err != nil {\n\t\t// fallback to gpg\n\t\treturn \".gpg\"\n\t}\n\tcrypto, err := loader.New(ctx)\n\tif err != nil {\n\t\t// fallback to gpg\n\t\treturn \".gpg\"\n\t}\n\n\treturn crypto.Ext()\n}\n\nfunc (c *Crypt) pathToName(ctx context.Context, p string) (string, error) {\n\trel, err := filepath.Rel(c.path, p)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get relative path for %s: %w\", p, err)\n\t}\n\n\text := c.getCryptoExt(ctx)\n\trel = strings.TrimSuffix(rel, ext)\n\n\treturn filepath.ToSlash(rel), nil\n}\n\nfunc (c *Crypt) Add(ctx context.Context, files ...string) error {\n\tc.mux.Lock()\n\tdefer c.mux.Unlock()\n\n\thashedFiles := make([]string, 0, len(files))\n\tfor _, file := range files {\n\t\tname, err := c.pathToName(ctx, file)\n\t\tif err != nil {\n\t\t\t// not in our store?\n\t\t\tdebug.Log(\"Failed to get name for %s: %s\", file, err)\n\n\t\t\tname = file\n\t\t}\n\n\t\tdebug.Log(\"Mapping file %s to name %s\", file, name)\n\n\t\th, ok := c.mappings[name]\n\t\tif !ok {\n\t\t\t// could be a directory or a path that git understands (like '.')\n\t\t\t// pass it through and hope for the best.\n\t\t\t// This is not perfect, but should work for the common cases.\n\t\t\thashedFiles = append(hashedFiles, file)\n\n\t\t\tdebug.Log(\"No mapping for %s found, passing through\", name)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"Mapping name %s to hash %s found\", name, h)\n\t\thashedFile := filepath.Join(c.path, h)\n\t\thashedFiles = append(hashedFiles, hashedFile)\n\t}\n\t// always add the mapping file\n\thashedFiles = append(hashedFiles, filepath.Join(c.path, mappingFile))\n\n\tdebug.Log(\"Adding files to the git index: %+v\", hashedFiles)\n\n\treturn c.sub.Add(ctx, hashedFiles...)\n}\n\nfunc (c *Crypt) Commit(ctx context.Context, msg string) error {\n\treturn c.sub.Commit(ctx, msg)\n}\n\nfunc (c *Crypt) TryAdd(ctx context.Context, files ...string) error {\n\terr := c.Add(ctx, files...)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"Git not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc (c *Crypt) TryCommit(ctx context.Context, msg string) error {\n\treturn c.sub.TryCommit(ctx, msg)\n}\n\nfunc (c *Crypt) Push(ctx context.Context, remote, branch string) error {\n\treturn c.sub.Push(ctx, remote, branch)\n}\n\nfunc (c *Crypt) Pull(ctx context.Context, remote, branch string) error {\n\treturn c.sub.Pull(ctx, remote, branch)\n}\n\nfunc (c *Crypt) TryPush(ctx context.Context, remote, branch string) error {\n\treturn c.sub.TryPush(ctx, remote, branch)\n}\n\nfunc (c *Crypt) Revisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\th, ok := c.mappings[name]\n\tif !ok {\n\t\treturn nil, os.ErrNotExist\n\t}\n\n\treturn c.sub.Revisions(ctx, h)\n}\n\nfunc (c *Crypt) GetRevision(ctx context.Context, name, revision string) ([]byte, error) {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\th, ok := c.mappings[name]\n\tif !ok {\n\t\treturn nil, os.ErrNotExist\n\t}\n\n\treturn c.sub.GetRevision(ctx, h, revision)\n}\n\nfunc (c *Crypt) Status(ctx context.Context) ([]byte, error) {\n\treturn c.sub.Status(ctx)\n}\n\nfunc (c *Crypt) Compact(ctx context.Context) error {\n\treturn c.sub.Compact(ctx)\n}\n\nfunc (c *Crypt) InitConfig(ctx context.Context, name, email string) error {\n\treturn c.sub.InitConfig(ctx, name, email)\n}\n\nfunc (c *Crypt) AddRemote(ctx context.Context, remote, url string) error {\n\treturn c.sub.AddRemote(ctx, remote, url)\n}\n\nfunc (c *Crypt) RemoveRemote(ctx context.Context, remote string) error {\n\treturn c.sub.RemoveRemote(ctx, remote)\n}\n\n// IsDir returns true if the given path is a directory.\nfunc (c *Crypt) IsDir(ctx context.Context, name string) bool {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\tfor k := range c.mappings {\n\t\tif strings.HasPrefix(k, name+\"/\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// List returns a list of all secrets.\nfunc (c *Crypt) List(ctx context.Context, prefix string) ([]string, error) {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\tlist := make([]string, 0, len(c.mappings))\n\n\tif !strings.HasSuffix(prefix, \"/\") && prefix != \"\" {\n\t\tprefix += \"/\"\n\t}\n\n\tfor k := range c.mappings {\n\t\tif !strings.HasPrefix(k, prefix) {\n\t\t\tcontinue\n\t\t}\n\t\tlist = append(list, k)\n\t}\n\tsort.Strings(list)\n\n\treturn list, nil\n}\n\n// Get returns the content of a secret.\nfunc (c *Crypt) Get(ctx context.Context, name string) ([]byte, error) {\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\th, ok := c.mappings[name]\n\tif !ok {\n\t\tif c.sub.Exists(ctx, name) {\n\t\t\treturn c.sub.Get(ctx, name)\n\t\t}\n\n\t\treturn nil, os.ErrNotExist\n\t}\n\n\tdebug.Log(\"Reading content for %s from %s\", name, h)\n\n\treturn c.sub.Get(ctx, h)\n}\n\n// Set sets the content of a secret.\nfunc (c *Crypt) Set(ctx context.Context, name string, value []byte) error {\n\tc.mux.Lock()\n\tdefer c.mux.Unlock()\n\n\th, ok := c.mappings[name]\n\tif !ok {\n\t\th = c.hash(name)\n\t\tc.mappings[name] = h\n\n\t\tdebug.Log(\"New mapping: %s -> %s\", name, h)\n\t}\n\n\tdebug.Log(\"Writing content for %s to %s\", name, h)\n\tif err := c.sub.Set(ctx, h, value); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.saveMappings(ctx)\n}\n\n// Delete removes a secret.\nfunc (c *Crypt) Delete(ctx context.Context, name string) error {\n\tc.mux.Lock()\n\tdefer c.mux.Unlock()\n\n\th, ok := c.mappings[name]\n\tif !ok {\n\t\treturn os.ErrNotExist\n\t}\n\tif err := c.sub.Delete(ctx, h); err != nil {\n\t\treturn err\n\t}\n\tdelete(c.mappings, name)\n\n\treturn c.saveMappings(ctx)\n}\n\n// Exists returns true if a secret exists.\nfunc (c *Crypt) Exists(ctx context.Context, name string) bool {\n\tif c.sub.Exists(ctx, name) {\n\t\treturn true\n\t}\n\n\tc.mux.RLock()\n\tdefer c.mux.RUnlock()\n\n\t_, ok := c.mappings[name]\n\n\treturn ok\n}\n\n// Move moves a secret.\nfunc (c *Crypt) Move(ctx context.Context, from, to string, del bool) error {\n\tc.mux.Lock()\n\tdefer c.mux.Unlock()\n\n\tfromH, ok := c.mappings[from]\n\tif !ok {\n\t\treturn os.ErrNotExist\n\t}\n\tif _, ok := c.mappings[to]; ok {\n\t\treturn fmt.Errorf(\"destination %s already exists\", to)\n\t}\n\n\t// get content\n\tcontent, err := c.sub.Get(ctx, fromH)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// set new\n\ttoH := c.hash(to)\n\tif err := c.sub.Set(ctx, toH, content); err != nil {\n\t\treturn err\n\t}\n\n\t// update mapping\n\tdelete(c.mappings, from)\n\tc.mappings[to] = toH\n\tif err := c.saveMappings(ctx); err != nil {\n\t\t// try to rollback\n\t\tc.mappings[from] = fromH\n\t\tdelete(c.mappings, to)\n\n\t\treturn err\n\t}\n\n\t// delete old\n\tif err := c.sub.Delete(ctx, fromH); err != nil {\n\t\t// this is not ideal, we have two copies now.\n\t\t// Fsck should detect this.\n\t\tout.Warningf(ctx, \"Failed to delete old file %s after move: %s\", fromH, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/backend/storage/cryptfs/crypt_test.go",
    "content": "package cryptfs\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage/gitfs\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar password = \"hunter2\"\n\nfunc newTestCryptFS(ctx context.Context, t *testing.T, td string) (*Crypt, string) {\n\tt.Helper()\n\n\t// setup age identity\n\ta, err := age.New(ctx, \"\")\n\trequire.NoError(t, err)\n\n\trecp, err := a.GenerateIdentity(ctx, \"\", \"\", password)\n\trequire.NoError(t, err)\n\n\t// setup store\n\tstorePath := filepath.Join(td, \".store\")\n\trequire.NoError(t, os.MkdirAll(storePath, 0o755))\n\n\t// use fs backend for simplicity\n\tsub, err := backend.InitStorage(ctx, backend.GitFS, storePath)\n\trequire.NoError(t, err)\n\n\t// create .age-recipients file\n\terr = os.WriteFile(filepath.Join(storePath, \".age-recipients\"), []byte(recp), 0o644)\n\trequire.NoError(t, err)\n\n\t// create cryptfs\n\tcrypt, err := newCrypt(ctx, sub)\n\trequire.NoError(t, err)\n\t// save empty mapping\n\terr = crypt.saveMappings(ctx)\n\trequire.NoError(t, err)\n\n\treturn crypt, storePath\n}\n\nfunc TestSetGet(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\tsecret := []byte(\"my secret\")\n\tname := \"foo/bar\"\n\n\terr := crypt.Set(ctx, name, secret)\n\trequire.NoError(t, err)\n\n\tret, err := crypt.Get(ctx, name)\n\trequire.NoError(t, err)\n\tassert.Equal(t, secret, ret)\n}\n\nfunc TestList(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\terr := crypt.Set(ctx, \"foo/bar\", []byte(\"1\"))\n\trequire.NoError(t, err)\n\terr = crypt.Set(ctx, \"foo/baz\", []byte(\"2\"))\n\trequire.NoError(t, err)\n\terr = crypt.Set(ctx, \"qux/quux\", []byte(\"3\"))\n\trequire.NoError(t, err)\n\n\tlist, err := crypt.List(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"foo/bar\", \"foo/baz\", \"qux/quux\"}, list)\n\n\tlist, err = crypt.List(ctx, \"foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"foo/bar\", \"foo/baz\"}, list)\n}\n\nfunc TestDelete(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\terr := crypt.Set(ctx, \"foo/bar\", []byte(\"1\"))\n\trequire.NoError(t, err)\n\tassert.True(t, crypt.Exists(ctx, \"foo/bar\"))\n\n\terr = crypt.Delete(ctx, \"foo/bar\")\n\trequire.NoError(t, err)\n\tassert.False(t, crypt.Exists(ctx, \"foo/bar\"))\n}\n\nfunc TestMove(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\terr := crypt.Set(ctx, \"foo/bar\", []byte(\"1\"))\n\trequire.NoError(t, err)\n\tassert.True(t, crypt.Exists(ctx, \"foo/bar\"))\n\n\terr = crypt.Move(ctx, \"foo/bar\", \"foo/baz\", true)\n\trequire.NoError(t, err)\n\tassert.False(t, crypt.Exists(ctx, \"foo/bar\"))\n\tassert.True(t, crypt.Exists(ctx, \"foo/baz\"))\n}\n\nfunc TestIsDir(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\terr := crypt.Set(ctx, \"foo/bar\", []byte(\"1\"))\n\trequire.NoError(t, err)\n\n\tassert.True(t, crypt.IsDir(ctx, \"foo\"))\n\tassert.False(t, crypt.IsDir(ctx, \"foo/bar\"))\n}\n\nfunc TestGit(t *testing.T) {\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithGitInit(ctx, true)\n\tctx = ctxutil.WithUsername(ctx, \"test\")\n\tctx = ctxutil.WithEmail(ctx, \"test@example.com\")\n\tctx = ctxutil.WithPasswordCallback(ctx, func(prompt string, _ bool) ([]byte, error) {\n\t\treturn []byte(password), nil\n\t})\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tcrypt, _ := newTestCryptFS(ctx, t, td)\n\n\t// Set a secret\n\terr := crypt.Set(ctx, \"foo/bar\", []byte(\"1\"))\n\trequire.NoError(t, err)\n\n\t// Add and commit\n\terr = crypt.Add(ctx, crypt.Path())\n\trequire.NoError(t, err)\n\terr = crypt.Commit(ctx, \"initial commit\")\n\trequire.NoError(t, err)\n\n\t// Check revisions\n\trevs, err := crypt.Revisions(ctx, \"foo/bar\")\n\trequire.NoError(t, err)\n\tassert.Len(t, revs, 1)\n}\n"
  },
  {
    "path": "internal/backend/storage/cryptfs/loader.go",
    "content": "package cryptfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nfunc init() {\n\tbackend.StorageRegistry.Register(backend.CryptFS, \"cryptfs\", &loader{})\n}\n\ntype loader struct{}\n\n// New returns a new cryptfs storage backend.\nfunc (l *loader) New(ctx context.Context, path string) (backend.Storage, error) {\n\tsubID, err := getSubStorage(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsub, err := backend.NewStorage(ctx, subID, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn newCrypt(ctx, sub)\n}\n\n// Init initializes a new cryptfs storage backend.\nfunc (l *loader) Init(ctx context.Context, path string) (backend.Storage, error) {\n\tsubID, err := getSubStorage(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsub, err := backend.InitStorage(ctx, subID, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebug.Log(\"Initialized sub storage %s at %s\", subID, path)\n\n\tc, err := newCrypt(ctx, sub)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.saveMappings(ctx); err != nil {\n\t\tout.Warningf(ctx, \"Failed to save initial mapping: %s\", err)\n\t}\n\n\treturn c, nil\n}\n\n// Handles returns true if this backend handles the given path.\nfunc (l *loader) Handles(ctx context.Context, path string) error {\n\tif fsutil.IsFile(path + \"/\" + mappingFile) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"no mapping file found\")\n}\n\n// String returns the name of this backend.\nfunc (l *loader) String() string {\n\treturn name\n}\n\n// Priority returns the priority of this backend.\nfunc (l *loader) Priority() int {\n\treturn 7\n}\n\n// Clone clones an existing repository and initializes the cryptfs backend.\nfunc (l *loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) {\n\tsubID, err := getSubStorage(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsubLoader, err := backend.StorageRegistry.Get(subID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsub, err := subLoader.Clone(ctx, repo, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn newCrypt(ctx, sub)\n}\n\nfunc getSubStorage(ctx context.Context) (backend.StorageBackend, error) {\n\tsubStoreName := config.String(ctx, \"cryptfs.substorage\")\n\tif subStoreName == \"\" {\n\t\tsubStoreName = \"gitfs\"\n\t}\n\n\tid, err := backend.StorageRegistry.Backend(subStoreName)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to get backend ID for %q: %s\", subStoreName, err)\n\t}\n\n\treturn id, err\n}\n"
  },
  {
    "path": "internal/backend/storage/cryptfs.go",
    "content": "package storage\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/storage/cryptfs\"\n"
  },
  {
    "path": "internal/backend/storage/doc.go",
    "content": "// Package storage provides a pluggable storage backend for gopass.\n\npackage storage\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/context.go",
    "content": "package fossilfs\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyPathOverride contextKey = iota\n)\n\nfunc withPathOverride(ctx context.Context, path string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPathOverride, path)\n}\n\nfunc getPathOverride(ctx context.Context, def string) string {\n\tif sv, ok := ctx.Value(ctxKeyPathOverride).(string); ok && sv != \"\" {\n\t\treturn sv\n\t}\n\n\treturn def\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/context_test.go",
    "content": "package fossilfs\n\nimport (\n\t\"testing\"\n)\n\nfunc TestWithPathOverride(t *testing.T) {\n\tctx := t.Context()\n\tpath := \"/test/path\"\n\tctx = withPathOverride(ctx, path)\n\n\tif val, ok := ctx.Value(ctxKeyPathOverride).(string); !ok || val != path {\n\t\tt.Errorf(\"Expected path %s, but got %v\", path, val)\n\t}\n}\n\nfunc TestGetPathOverride(t *testing.T) {\n\tctx := t.Context()\n\tdefaultPath := \"/default/path\"\n\n\t// Test with no override\n\tif path := getPathOverride(ctx, defaultPath); path != defaultPath {\n\t\tt.Errorf(\"Expected default path %s, but got %s\", defaultPath, path)\n\t}\n\n\t// Test with override\n\toverridePath := \"/override/path\"\n\tctx = withPathOverride(ctx, overridePath)\n\tif path := getPathOverride(ctx, defaultPath); path != overridePath {\n\t\tt.Errorf(\"Expected override path %s, but got %s\", overridePath, path)\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/fossil.go",
    "content": "package fossilfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nconst (\n\t// CheckoutMarker is the marker file that indicates a fossil checkout.\n\tCheckoutMarker = \".fslckout\"\n)\n\n// Fossil is a storage backend for Fossil.\ntype Fossil struct {\n\tfs *fs.Store\n}\n\n// New instantiates a new Fossil store.\nfunc New(path string) (*Fossil, error) {\n\tmarker := filepath.Join(path, CheckoutMarker)\n\tif !fsutil.IsFile(marker) {\n\t\treturn nil, fmt.Errorf(\"no fossil checkout marker found at %s\", marker)\n\t}\n\n\treturn &Fossil{\n\t\tfs: fs.New(path),\n\t}, nil\n}\n\n// Clone opens a new fossil checkout.\nfunc Clone(ctx context.Context, repo, path string) (*Fossil, error) {\n\tf := &Fossil{\n\t\tfs: fs.New(path),\n\t}\n\t// we use open instead of clone, since that automatically clones, if necessary\n\targs := []string{\n\t\t\"open\", repo,\n\t\t\"--workdir\", path,\n\t}\n\t// the --repodir option only makes sense if the REPOSITORY argument is a URI that begins with http:, https:, ssh:, or file:\n\tif strings.HasPrefix(repo, \"http:\") || strings.HasPrefix(repo, \"https:\") || strings.HasPrefix(repo, \"ssh:\") || strings.HasPrefix(repo, \"file:\") {\n\t\targs = append(args, \"--repodir\", filepath.Dir(path))\n\t}\n\n\tif err := f.Cmd(withPathOverride(ctx, filepath.Dir(path)), \"Clone\", args...); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// initialize the local fossil config.\n\tif err := f.InitConfig(ctx, \"\", \"\"); err != nil {\n\t\treturn f, fmt.Errorf(\"failed to configure git: %w\", err)\n\t}\n\n\tout.Printf(ctx, \"fossil configured at %s\", f.fs.Path())\n\n\treturn f, nil\n}\n\n// Init initializes this store's fossil repo.\nfunc Init(ctx context.Context, path, _, _ string) (*Fossil, error) {\n\tf := &Fossil{\n\t\tfs: fs.New(path),\n\t}\n\t// the fossil repo may be empty (i.e. no branches, cloned from a fresh remote)\n\t// or already initialized. Only run fossil init if the folder is completely empty.\n\tif !f.IsInitialized() {\n\t\trepo := filepath.Join(filepath.Dir(path), \".\"+filepath.Base(path)+\".fossil\")\n\t\tif err := f.Cmd(ctx, \"Init\", \"init\", repo); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to initialize fossil in %s: %w\", repo, err)\n\t\t}\n\n\t\tif err := f.Cmd(ctx, \"Open\", \"open\", repo); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to open fossil in %s: %w\", repo, err)\n\t\t}\n\n\t\tout.Printf(ctx, \"fossil initialized at %s\", f.fs.Path())\n\t}\n\n\t// TODO rename to IsRCSInitialized\n\tif !ctxutil.IsGitInit(ctx) {\n\t\treturn f, nil\n\t}\n\n\t// initialize the local fossil config.\n\tif err := f.InitConfig(ctx, \"\", \"\"); err != nil {\n\t\treturn f, fmt.Errorf(\"failed to configure fossil: %w\", err)\n\t}\n\n\tout.Printf(ctx, \"fossil configured at %s\", f.fs.Path())\n\n\t// add current content of the store.\n\tif err := f.Add(ctx, f.fs.Path()); err != nil {\n\t\treturn f, fmt.Errorf(\"failed to add %q to fossil: %w\", f.fs.Path(), err)\n\t}\n\n\t// commit if there is something to commit.\n\tif !f.HasStagedChanges(ctx) {\n\t\tdebug.Log(\"No staged changes\")\n\n\t\treturn f, nil\n\t}\n\n\tif err := f.Commit(ctx, \"Add current content of password store\"); err != nil {\n\t\treturn f, fmt.Errorf(\"failed to commit changes to fossil: %w\", err)\n\t}\n\n\treturn f, nil\n}\n\nfunc (f *Fossil) captureCmd(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {\n\tbufOut := &bytes.Buffer{}\n\tbufErr := &bytes.Buffer{}\n\n\tcmd := exec.CommandContext(ctx, \"fossil\", args[0:]...)\n\tcmd.Dir = getPathOverride(ctx, f.fs.Path())\n\tcmd.Stdout = bufOut\n\tcmd.Stderr = bufErr\n\n\tdebug.Log(\"fossil.%s: %s %+v (%s)\", name, cmd.Path, cmd.Args, f.fs.Path())\n\terr := cmd.Run()\n\n\treturn bufOut.Bytes(), bufErr.Bytes(), err\n}\n\n// Cmd runs an fossil command.\nfunc (f *Fossil) Cmd(ctx context.Context, name string, args ...string) error {\n\tstdout, stderr, err := f.captureCmd(ctx, name, args...)\n\tif err != nil {\n\t\tdebug.Log(\"CMD: %s %+v\\nError: %s\\nOutput:\\n  Stdout: %q\\n  Stderr: %q\", name, args, err, string(stdout), string(stderr))\n\n\t\treturn fmt.Errorf(\"%w: %s\", err, strings.TrimSpace(string(stderr)))\n\t}\n\n\treturn nil\n}\n\n// Name returns 'fossil'.\nfunc (f *Fossil) Name() string {\n\treturn name\n}\n\n// Version returns the fossil version as major, minor and patch level.\nfunc (f *Fossil) Version(ctx context.Context) semver.Version {\n\tv := semver.Version{}\n\n\tcmd := exec.CommandContext(ctx, \"fossil\", \"version\")\n\tcmdout, err := cmd.Output()\n\tif err != nil {\n\t\tdebug.Log(\"Failed to run 'fossil version': %s\", err)\n\n\t\treturn v\n\t}\n\n\tsvStr := strings.TrimPrefix(string(cmdout), \"This is fossil version \")\n\tif p := strings.Fields(svStr); len(p) > 0 {\n\t\tsvStr = p[0]\n\t}\n\n\tsv, err := semver.ParseTolerant(svStr)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to parse %q as semver: %s\", svStr, err)\n\n\t\treturn v\n\t}\n\n\treturn sv\n}\n\n// IsInitialized returns true if this stores has an (probably) initialized Fossil checkout.\nfunc (f *Fossil) IsInitialized() bool {\n\tfn := filepath.Join(f.fs.Path(), CheckoutMarker)\n\tisFile := fsutil.IsFile(fn)\n\n\tdebug.Log(\"checking for Fossil Checkout marker at %s: %t\", fn, isFile)\n\n\treturn isFile\n}\n\n// Add adds the listed files to the fossil index.\nfunc (f *Fossil) Add(ctx context.Context, files ...string) error {\n\tif !f.IsInitialized() {\n\t\t// TODO should rename to ErrRCSNotInitialized\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tfor i := range files {\n\t\tfiles[i] = strings.TrimPrefix(files[i], f.fs.Path()+\"/\")\n\t}\n\n\targs := []string{\"add\", \"--force\", \"--dotfiles\"}\n\targs = append(args, files...)\n\n\treturn f.Cmd(ctx, \"fossilAdd\", args...)\n}\n\n// TryAdd adds the listed files to the fossil index.\nfunc (f *Fossil) TryAdd(ctx context.Context, files ...string) error {\n\terr := f.Add(ctx, files...)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"Fossil not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// HasStagedChanges returns true if there are any staged changes which can be committed.\nfunc (f *Fossil) HasStagedChanges(ctx context.Context) bool {\n\ts, err := f.getStatus(ctx)\n\tif err != nil {\n\t\t// TODO should return an error\n\t\treturn true\n\t}\n\n\treturn s.Staged().Len() > 0\n}\n\n// ListUntrackedFiles lists untracked files.\nfunc (f *Fossil) ListUntrackedFiles(ctx context.Context) []string {\n\ts, err := f.getStatus(ctx)\n\tif err != nil {\n\t\t// TODO should return an error\n\t\treturn []string{fmt.Sprintf(\"ERROR: %s\", err)}\n\t}\n\n\treturn s.Untracked().Elements()\n}\n\n// Commit creates a new fossil commit with the given commit message.\nfunc (f *Fossil) Commit(ctx context.Context, msg string) error {\n\tif !f.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tif !f.HasStagedChanges(ctx) {\n\t\treturn store.ErrGitNothingToCommit\n\t}\n\n\treturn f.Cmd(\n\t\tctx,\n\t\t\"fossilCommit\",\n\t\t\"commit\",\n\t\t\"--date-override\",\n\t\tctxutil.GetCommitTimestamp(ctx).UTC().Format(\"2006-01-02T15:04:05.000\"),\n\t\t\"--no-warnings\",\n\t\t\"-m\",\n\t\tmsg,\n\t)\n}\n\n// TryCommit calls commit and returns nil if there was nothing to commit or if the Fossil repo was not initialized.\nfunc (f *Fossil) TryCommit(ctx context.Context, msg string) error {\n\terr := f.Commit(ctx, msg)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNothingToCommit) {\n\t\tdebug.Log(\"Nothing to commit. Ignoring.\")\n\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"Fossil not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// PushPull pushes the repo to it's origin.\n// optional arguments: remote and branch.\nfunc (f *Fossil) PushPull(ctx context.Context, op, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\tif !f.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tif uf := f.ListUntrackedFiles(ctx); len(uf) > 0 {\n\t\tout.Warningf(ctx, \"Found untracked files: %+v\", uf)\n\t}\n\n\t// https://www.fossil-scm.org/home/help?cmd=sync\n\tswitch op {\n\tcase \"pull\":\n\t\tif err := f.Cmd(ctx, \"fossilPull\", op); err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\tif err := f.Cmd(ctx, \"fossilSync\", \"sync\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn f.Cmd(ctx, \"fossilUpdate\", \"update\")\n}\n\n// Push pushes to the fossil remote.\nfunc (f *Fossil) Push(ctx context.Context, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\n\treturn f.PushPull(ctx, \"push\", remote, branch)\n}\n\n// TryPush calls Push and returns nil if the Fossil repo was not initialized.\nfunc (f *Fossil) TryPush(ctx context.Context, remote, branch string) error {\n\terr := f.Push(ctx, remote, branch)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tswitch {\n\tcase errors.Is(err, store.ErrGitNotInit):\n\t\tdebug.Log(\"Fossil not initialized. Ignoring.\")\n\n\t\treturn nil\n\tcase errors.Is(err, store.ErrGitNoRemote):\n\t\tdebug.Log(\"Fossil has no remote. Ignoring.\")\n\n\t\treturn nil\n\tdefault:\n\t\treturn err\n\t}\n}\n\n// Pull pulls from the fossil remote.\nfunc (f *Fossil) Pull(ctx context.Context, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\n\treturn f.PushPull(ctx, \"pull\", remote, branch)\n}\n\n// AddRemote adds a new remote.\nfunc (f *Fossil) AddRemote(ctx context.Context, remote, url string) error {\n\treturn f.Cmd(ctx, \"fossilAddRemote\", \"remote\", \"add\", remote, url)\n}\n\n// RemoveRemote removes a remote.\nfunc (f *Fossil) RemoveRemote(ctx context.Context, remote string) error {\n\treturn f.Cmd(ctx, \"fossilRemoveRemote\", \"remote\", \"delete\", remote)\n}\n\n// Revisions will list all available revisions of the named entity.\nfunc (f *Fossil) Revisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\targs := []string{\n\t\t\"finfo\",\n\t\t\"-W\",\n\t\t\"0\",\n\t\tname,\n\t}\n\tstdout, stderr, err := f.captureCmd(ctx, \"Revisions\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\trevs := make([]backend.Revision, 0, strings.Count(string(stdout), \"\\n\"))\n\tfor line := range strings.SplitSeq(string(stdout), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tdebug.Log(\"empty line\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"Parsing line: %s\", line)\n\t\tbody := line //nolint:copyloopvar // retain full line for the body\n\t\tdate, line, found := strings.Cut(line, \" \")\n\t\tif !found {\n\t\t\tdebug.Log(\"Failed to parse date\")\n\n\t\t\tcontinue\n\t\t}\n\t\trev, line, found := strings.Cut(line, \" \")\n\t\tif !found {\n\t\t\tdebug.Log(\"Failed to parse revision\")\n\n\t\t\tcontinue\n\t\t}\n\t\trev = strings.Trim(rev, \"[]\")\n\t\tsubject, line, found := strings.Cut(line, \"(\")\n\t\tif !found {\n\t\t\tdebug.Log(\"Failed to parse subject\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\tauthor, _, found := strings.Cut(line, \",\")\n\t\tif !found {\n\t\t\tdebug.Log(\"Failed to parse author\")\n\n\t\t\tcontinue\n\t\t}\n\t\tauthor = strings.TrimPrefix(author, \"user: \")\n\n\t\tts, err := time.Parse(\"2006-01-02\", date)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"Failed to parse date %s: %s\", date, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tr := backend.Revision{\n\t\t\tHash:       rev,\n\t\t\tDate:       ts,\n\t\t\tBody:       body,\n\t\t\tSubject:    subject,\n\t\t\tAuthorName: author,\n\t\t}\n\t\trevs = append(revs, r)\n\t}\n\n\treturn revs, nil\n}\n\n// GetRevision will return the content of any revision of the named entity.\nfunc (f *Fossil) GetRevision(ctx context.Context, name, revision string) ([]byte, error) {\n\tname = strings.TrimSpace(name)\n\trevision = strings.TrimSpace(revision)\n\targs := []string{\n\t\t\"cat\",\n\t\t\"-r\",\n\t\trevision,\n\t\tname,\n\t}\n\tstdout, stderr, err := f.captureCmd(ctx, \"GetRevision\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Status return the fossil status output.\nfunc (f *Fossil) Status(ctx context.Context) ([]byte, error) {\n\tstdout, stderr, err := f.captureCmd(ctx, \"FossilStatus\", \"status\")\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\\n%s\", string(stdout), string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Compact will run fossil rebuild.\nfunc (f *Fossil) Compact(ctx context.Context) error {\n\treturn f.Cmd(ctx, \"fossilRebuild\", \"rebuild\", \"--compress\", \"--analyze\", \"--vacuum\")\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/fossil_test.go",
    "content": "//go:build fossil\n\npackage fossilfs\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tdir := t.TempDir()\n\n\tmarker := filepath.Join(dir, CheckoutMarker)\n\t_, err := os.Create(marker)\n\trequire.NoError(t, err)\n\n\tf, err := New(dir)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, f)\n}\n\nfunc TestClone(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\trepo := \"https://example.com/repo.fossil\"\n\n\tf, err := Clone(ctx, repo, dir)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, f)\n}\n\nfunc TestInit(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, f)\n}\n\nfunc TestAdd(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.Add(ctx, \"testfile\")\n\trequire.NoError(t, err)\n}\n\nfunc TestCommit(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.Commit(ctx, \"Initial commit\")\n\trequire.NoError(t, err)\n}\n\nfunc TestPush(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.Push(ctx, \"origin\", \"main\")\n\trequire.NoError(t, err)\n}\n\nfunc TestPull(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.Pull(ctx, \"origin\", \"main\")\n\trequire.NoError(t, err)\n}\n\nfunc TestAddRemote(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.AddRemote(ctx, \"origin\", \"https://example.com/repo.fossil\")\n\trequire.NoError(t, err)\n}\n\nfunc TestRemoveRemote(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.RemoveRemote(ctx, \"origin\")\n\trequire.NoError(t, err)\n}\n\nfunc TestRevisions(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\trevs, err := f.Revisions(ctx, \"testfile\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, revs)\n}\n\nfunc TestGetRevision(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\tcontent, err := f.GetRevision(ctx, \"testfile\", \"1\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, content)\n}\n\nfunc TestStatus(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\tstatus, err := f.Status(ctx)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, status)\n}\n\nfunc TestCompact(t *testing.T) {\n\tdir := t.TempDir()\n\n\tctx := t.Context()\n\tf, err := Init(ctx, dir, \"\", \"\")\n\trequire.NoError(t, err)\n\n\terr = f.Compact(ctx)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/loader.go",
    "content": "package fossilfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nconst (\n\tname = \"fossilfs\"\n)\n\nfunc init() {\n\tbackend.StorageRegistry.Register(backend.FossilFS, name, &loader{})\n}\n\ntype loader struct{}\n\nfunc (l loader) New(ctx context.Context, path string) (backend.Storage, error) {\n\treturn New(path)\n}\n\nfunc (l loader) Open(ctx context.Context, path string) (backend.Storage, error) {\n\treturn New(path)\n}\n\nfunc (l loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) {\n\treturn Clone(ctx, repo, path)\n}\n\nfunc (l loader) Init(ctx context.Context, path string) (backend.Storage, error) {\n\treturn Init(ctx, path, \"\", \"\")\n}\n\nfunc (l loader) Handles(ctx context.Context, path string) error {\n\tpath = fsutil.ExpandHomedir(path)\n\n\tmarker := filepath.Join(path, CheckoutMarker)\n\tif !fsutil.IsFile(marker) {\n\t\treturn fmt.Errorf(\"no fossil checkout marker found at %s\", marker)\n\t}\n\n\treturn nil\n}\n\nfunc (l loader) Priority() int {\n\treturn 12\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/loader_test.go",
    "content": "// Package fossilfs provides an experimental storage backend for gopass\n// based on the Fossil SCM system.\n//\n// It is not recommended for production use and is only intended for testing\n// purposes.\npackage fossilfs\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createMarker(t *testing.T, path string) {\n\tt.Helper()\n\n\t// Create a mock marker file for testing\n\trequire.NoError(t, os.MkdirAll(path, 0o700))\n\tmarker := filepath.Join(path, CheckoutMarker)\n\trequire.NoError(t, os.WriteFile(marker, []byte(\"marker\"), 0o600))\n}\n\nfunc TestLoader_New(t *testing.T) {\n\tl := loader{}\n\tctx := t.Context()\n\tpath := t.TempDir()\n\tcreateMarker(t, path)\n\n\tstorage, err := l.New(ctx, path)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, storage)\n}\n\nfunc TestLoader_Open(t *testing.T) {\n\tl := loader{}\n\tctx := t.Context()\n\tpath := t.TempDir()\n\tcreateMarker(t, path)\n\n\tstorage, err := l.Open(ctx, path)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, storage)\n}\n\nfunc TestLoader_Clone(t *testing.T) {\n\tt.Skip(\"needs fossil binary and valid remote\")\n\n\tl := loader{}\n\tctx := t.Context()\n\trepo := \"https://example.com/repo.git\"\n\tpath := t.TempDir()\n\tcreateMarker(t, path)\n\n\tstorage, err := l.Clone(ctx, repo, path)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, storage)\n}\n\nfunc TestLoader_Init(t *testing.T) {\n\tt.Skip(\"needs fossil binary\")\n\n\tl := loader{}\n\tctx := t.Context()\n\tpath := t.TempDir()\n\tcreateMarker(t, path)\n\n\tstorage, err := l.Init(ctx, path)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, storage)\n}\n\nfunc TestLoader_Handles(t *testing.T) {\n\tl := loader{}\n\tctx := t.Context()\n\ttd := t.TempDir()\n\n\terr := l.Handles(ctx, td)\n\trequire.Error(t, err)\n\n\tcreateMarker(t, td)\n\n\terr = l.Handles(ctx, td)\n\trequire.NoError(t, err)\n}\n\nfunc TestLoader_Priority(t *testing.T) {\n\tl := loader{}\n\tassert.Equal(t, 12, l.Priority())\n}\n\nfunc TestLoader_String(t *testing.T) {\n\tl := loader{}\n\tassert.Equal(t, name, l.String())\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/settings.go",
    "content": "package fossilfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nfunc (f *Fossil) fixConfig(ctx context.Context) error {\n\t// enable autosync\n\tif err := f.ConfigSet(ctx, \"autosync\", \"1\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to set fossil config autosync: %w\", err)\n\t}\n\n\t// binary-glob\n\tif err := f.ConfigSet(ctx, \"binary-glob\", \"*.age,*.gpg\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to set fossil config binary-glob: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// InitConfig initializes the fossil config.\nfunc (f *Fossil) InitConfig(ctx context.Context, _, _ string) error {\n\t// ensure a sane fossil config.\n\tif err := f.fixConfig(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to fix fossil config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// ConfigSet sets a local config value.\nfunc (f *Fossil) ConfigSet(ctx context.Context, key, value string) error {\n\treturn f.Cmd(ctx, \"fossilConfigSet\", \"settings\", \"--exact\", key, value)\n}\n\n// ConfigGet returns a given config value.\nfunc (f *Fossil) ConfigGet(ctx context.Context, key string) (string, error) {\n\tif !f.IsInitialized() {\n\t\treturn \"\", store.ErrGitNotInit\n\t}\n\n\tbuf := &strings.Builder{}\n\n\tcmd := exec.CommandContext(ctx, \"fossil\", \"settings\", \"--exact\", key)\n\tcmd.Dir = f.fs.Path()\n\tcmd.Stdout = buf\n\tcmd.Stderr = os.Stderr\n\n\tdebug.Log(\"%s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsv := strings.Fields(strings.TrimSpace(buf.String()))\n\n\treturn sv[len(sv)-1], nil\n}\n\n// ConfigList returns all fossil config settings.\nfunc (f *Fossil) ConfigList(ctx context.Context) (map[string]string, error) {\n\tif !f.IsInitialized() {\n\t\treturn nil, store.ErrGitNotInit\n\t}\n\n\tbuf := &strings.Builder{}\n\n\tcmd := exec.CommandContext(ctx, \"fossil\", \"settings\")\n\tcmd.Dir = f.fs.Path()\n\tcmd.Stdout = buf\n\tcmd.Stderr = os.Stderr\n\n\tdebug.Log(\"%s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tlines := strings.Split(buf.String(), \"\\n\")\n\tkv := make(map[string]string, len(lines))\n\tfor _, line := range lines {\n\t\tp := strings.Fields(strings.TrimSpace(line))\n\t\t// only record settings with a value\n\t\tif len(p) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\tkv[p[0]] = p[len(p)-1]\n\t}\n\n\treturn kv, nil\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/status.go",
    "content": "package fossilfs\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\ntype fossilStatus struct {\n\tExtra     set.Set[string]\n\tAdded     set.Set[string]\n\tEdited    set.Set[string]\n\tUnchanged set.Set[string]\n}\n\nfunc (f *Fossil) getStatus(ctx context.Context) (fossilStatus, error) {\n\tstdout, _, err := f.captureCmd(ctx, \"fossilStatus\", \"status\", \"--extra\", \"--all\")\n\tif err != nil {\n\t\treturn fossilStatus{}, err\n\t}\n\n\ts := fossilStatus{\n\t\tExtra:     set.New[string](),\n\t\tAdded:     set.New[string](),\n\t\tEdited:    set.New[string](),\n\t\tUnchanged: set.New[string](),\n\t}\n\tfor line := range strings.SplitSeq(string(stdout), \"\\n\") {\n\t\top, file, found := strings.Cut(line, \" \")\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\t\tswitch op {\n\t\tcase \"ADDED\":\n\t\t\ts.Added.Add(strings.TrimSpace(file))\n\t\tcase \"UNCHANGED\":\n\t\t\ts.Unchanged.Add(strings.TrimSpace(file))\n\t\tcase \"EXTRA\":\n\t\t\ts.Added.Add(strings.TrimSpace(file))\n\t\tcase \"EDITED\":\n\t\t\ts.Edited.Add(strings.TrimSpace(file))\n\t\t}\n\t}\n\n\treturn s, nil\n}\n\nfunc (fs *fossilStatus) Untracked() set.Set[string] {\n\treturn fs.Extra.Union(fs.Added).Union(fs.Edited)\n}\n\nfunc (fs *fossilStatus) Staged() set.Set[string] {\n\treturn fs.Edited.Union(fs.Added)\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/storage.go",
    "content": "package fossilfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// Get retrieves the named content.\nfunc (f *Fossil) Get(ctx context.Context, name string) ([]byte, error) {\n\treturn f.fs.Get(ctx, name)\n}\n\n// Set writes the given content.\nfunc (f *Fossil) Set(ctx context.Context, name string, value []byte) error {\n\treturn f.fs.Set(ctx, name, value)\n}\n\n// Delete removes the named entity.\nfunc (f *Fossil) Delete(ctx context.Context, name string) error {\n\treturn f.fs.Delete(ctx, name)\n}\n\n// Exists checks if the named entity exists.\nfunc (f *Fossil) Exists(ctx context.Context, name string) bool {\n\treturn f.fs.Exists(ctx, name)\n}\n\n// List returns a list of all entities\n// e.g. foo, far/bar baz/.bang\n// directory separator are normalized using `/`.\nfunc (f *Fossil) List(ctx context.Context, prefix string) ([]string, error) {\n\treturn f.fs.List(ctx, prefix)\n}\n\n// IsDir returns true if the named entity is a directory.\nfunc (f *Fossil) IsDir(ctx context.Context, name string) bool {\n\treturn f.fs.IsDir(ctx, name)\n}\n\n// Prune removes a named directory.\nfunc (f *Fossil) Prune(ctx context.Context, prefix string) error {\n\treturn f.fs.Prune(ctx, prefix)\n}\n\n// String implements fmt.Stringer.\nfunc (f *Fossil) String() string {\n\treturn fmt.Sprintf(\"fossilfs(%s,path:%s)\", f.Version(context.TODO()).String(), f.fs.Path())\n}\n\n// Path returns the path to this storage.\nfunc (f *Fossil) Path() string {\n\treturn f.fs.Path()\n}\n\n// Fsck checks the storage integrity.\nfunc (f *Fossil) Fsck(ctx context.Context) error {\n\t// ensure sane fossil config.\n\tif err := f.fixConfig(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to fix fossil config: %w\", err)\n\t}\n\n\treturn f.fs.Fsck(ctx)\n}\n\n// Link creates a symlink.\nfunc (f *Fossil) Link(ctx context.Context, from, to string) error {\n\treturn f.fs.Link(ctx, from, to)\n}\n\n// Move moves from src to dst.\nfunc (f *Fossil) Move(ctx context.Context, src, dst string, del bool) error {\n\treturn f.fs.Move(ctx, src, dst, del)\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs/storage_test.go",
    "content": "//go:build fossil\n\npackage fossilfs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFossil_Get(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tname := \"test\"\n\n\tfossil.fs.Set(ctx, name, []byte(\"content\"))\n\n\tcontent, err := fossil.Get(ctx, name)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []byte(\"content\"), content)\n}\n\nfunc TestFossil_Set(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tname := \"test\"\n\tvalue := []byte(\"content\")\n\n\terr := fossil.Set(ctx, name, value)\n\trequire.NoError(t, err)\n}\n\nfunc TestFossil_Delete(t *testing.T) {\n\tt.Skip(\"needs fossil binary\")\n\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tname := \"test\"\n\n\tfossil.fs.Set(ctx, name, []byte(\"content\"))\n\n\terr := fossil.Delete(ctx, name)\n\trequire.NoError(t, err)\n}\n\nfunc TestFossil_Exists(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tname := \"test\"\n\n\tfossil.fs.Set(ctx, name, []byte(\"content\"))\n\n\texists := fossil.Exists(ctx, name)\n\tassert.True(t, exists)\n}\n\nfunc TestFossil_List(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tprefix := \"test\"\n\n\tfossil.fs.Set(ctx, \"test/foo\", []byte(\"content\"))\n\tfossil.fs.Set(ctx, \"test/bar\", []byte(\"content\"))\n\tfossil.fs.Set(ctx, \"foo/bar\", []byte(\"content\"))\n\n\tlist, err := fossil.List(ctx, prefix)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"test/bar\", \"test/foo\"}, list)\n}\n\nfunc TestFossil_IsDir(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tname := \"test\"\n\n\tfossil.fs.Set(ctx, \"test/foo\", []byte(\"content\"))\n\n\tassert.True(t, fossil.IsDir(ctx, name))\n}\n\nfunc TestFossil_Prune(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tprefix := \"test\"\n\n\tfossil.fs.Set(ctx, \"test/foo\", []byte(\"content\"))\n\tfossil.fs.Set(ctx, \"test/bar\", []byte(\"content\"))\n\n\terr := fossil.Prune(ctx, prefix)\n\trequire.NoError(t, err)\n}\n\nfunc TestFossil_String(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\n\tstr := fossil.String()\n\tassert.Contains(t, str, \"fossilfs(\")\n\tassert.Contains(t, str, \"path:/path/to/storage\")\n}\n\nfunc TestFossil_Path(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\n\tpath := fossil.Path()\n\tassert.Equal(t, \"/path/to/storage\", path)\n}\n\nfunc TestFossil_Fsck(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\n\terr := fossil.Fsck(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc TestFossil_Link(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tfrom := \"from\"\n\tto := \"to\"\n\n\tfossil.fs.Set(ctx, \"from\", []byte(\"content\"))\n\n\terr := fossil.Link(ctx, from, to)\n\trequire.NoError(t, err)\n}\n\nfunc TestFossil_Move(t *testing.T) {\n\ttd := t.TempDir()\n\tfossil := &Fossil{fs: fs.New(td)}\n\tctx := t.Context()\n\tsrc := \"src\"\n\tdst := \"dst\"\n\tdel := true\n\n\tfossil.fs.Set(ctx, \"src\", []byte(\"content\"))\n\n\terr := fossil.Move(ctx, src, dst, del)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/backend/storage/fossilfs.go",
    "content": "package storage\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/storage/fossilfs\" // register fossilfs backend\n"
  },
  {
    "path": "internal/backend/storage/fs/fsck.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\n// Fsck checks the storage integrity.\nfunc (s *Store) Fsck(ctx context.Context) error {\n\tentries, err := s.List(ctx, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdirs := make(map[string]struct{}, len(entries))\n\tfor _, entry := range entries {\n\t\tdebug.Log(\"checking entry %q\", entry)\n\n\t\tfilename := filepath.Join(s.path, entry)\n\t\tdirs[filepath.Dir(filename)] = struct{}{}\n\n\t\tif err := s.fsckCheckFile(ctx, filename); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor dir := range dirs {\n\t\tdebug.Log(\"checking dir %q\", dir)\n\t\tif err := s.fsckCheckDir(ctx, dir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := s.fsckCheckEmptyDirs(); err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"checking root dir %q\", s.path)\n\tif err := s.fsckCheckDir(ctx, s.path); err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"checking git config\")\n\n\treturn s.InitConfig(ctx, termio.DetectName(ctx, nil), termio.DetectEmail(ctx, nil))\n}\n\nfunc (s *Store) fsckCheckFile(ctx context.Context, filename string) error {\n\tfi, err := os.Stat(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif fi.Mode().Perm()&0o177 == 0 {\n\t\treturn nil\n\t}\n\n\tout.Printf(ctx, \"Permissions too wide: %s (%s)\", filename, fi.Mode().String())\n\n\tnp := uint32(fi.Mode().Perm() & 0o600)\n\tout.Printf(ctx, \"  Fixing permissions from %s to %s\", fi.Mode().Perm().String(), os.FileMode(np).Perm().String())\n\tif err := syscall.Chmod(filename, np); err != nil {\n\t\tout.Errorf(ctx, \"  Failed to set permissions for %s to rw-------: %s\", filename, err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) fsckCheckDir(ctx context.Context, dirname string) error {\n\tfi, err := os.Stat(dirname)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// check if any group or other perms are set,\n\t// i.e. check for perms other than rwx------\n\tif fi.Mode().Perm()&0o77 != 0 {\n\t\tout.Printf(ctx, \"Permissions too wide %s on dir %s\", fi.Mode().Perm().String(), dirname)\n\n\t\tnp := uint32(fi.Mode().Perm() & 0o700)\n\t\tout.Printf(ctx, \"  Fixing permissions from %s to %s\", fi.Mode().Perm().String(), os.FileMode(np).Perm().String())\n\t\tif err := syscall.Chmod(dirname, np); err != nil {\n\t\t\tout.Errorf(ctx, \"  Failed to set permissions for %s to rwx------: %s\", dirname, err)\n\t\t}\n\t}\n\n\t// check for empty folders\n\tisEmpty, err := fsutil.IsEmptyDir(dirname)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isEmpty {\n\t\tout.Errorf(ctx, \"Folder %s is empty. Removing\", dirname)\n\n\t\treturn os.Remove(dirname)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) fsckCheckEmptyDirs() error {\n\tv := []string{}\n\tif err := filepath.Walk(s.path, func(fp string, fi os.FileInfo, ferr error) error {\n\t\tif ferr != nil {\n\t\t\treturn ferr\n\t\t}\n\t\tif !fi.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasPrefix(fi.Name(), \".\") {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif fp == s.path {\n\t\t\treturn nil\n\t\t}\n\n\t\t// add candidate\n\t\tdebug.Log(\"adding candidate %q\", fp)\n\t\tv = append(v, fp)\n\n\t\treturn nil\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// start with longest path (deepest dir)\n\tsort.Slice(v, func(i, j int) bool {\n\t\treturn len(v[i]) > len(v[j])\n\t})\n\n\tfor _, d := range v {\n\t\tif err := fsckRemoveEmptyDir(d); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc fsckRemoveEmptyDir(fp string) error {\n\tls, err := os.ReadDir(fp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(ls) > 0 {\n\t\tdebug.Log(\"dir %q is not empty (%d)\", fp, len(ls))\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"removing %q ...\", fp)\n\n\treturn os.Remove(fp)\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/fsck_test.go",
    "content": "package fs\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFsck(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\tpath := t.TempDir()\n\n\tl := &loader{}\n\ts, err := l.Init(ctx, path)\n\trequire.NoError(t, err)\n\trequire.NoError(t, l.Handles(ctx, path))\n\n\tfor _, fn := range []string{\n\t\tfilepath.Join(path, \".plain-ids\"),\n\t\tfilepath.Join(path, \"foo\", \"bar\"),\n\t\tfilepath.Join(path, \"foo\", \"zen\"),\n\t} {\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(fn), 0o777))\n\t\trequire.NoError(t, os.WriteFile(fn, []byte(fn), 0o663))\n\t}\n\n\trequire.NoError(t, s.Fsck(ctx))\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/link.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// addRel adds the required number of relative elements to go from dst back to\n// src.\nfunc addRel(src, dst string) string {\n\tfor range strings.Count(dst, \"/\") {\n\t\tsrc = \"../\" + src\n\t}\n\n\treturn src\n}\n\n// longestCommonPrefix finds the longest common prefix directory.\nfunc longestCommonPrefix(l, r string) string {\n\tvar prefix string\n\tfor i := 0; i < len(l) && i < len(r); i++ {\n\t\tif l[i] != r[i] {\n\t\t\tprefix = l[:i]\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !strings.Contains(prefix, \"/\") {\n\t\treturn prefix\n\t}\n\n\treturn prefix[:strings.LastIndex(prefix, \"/\")]\n}\n\n// Link creates a symlink, i.e. an alias to reach the same secret\n// through different names.\nfunc (s *Store) Link(ctx context.Context, from, to string) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tfrom = filepath.FromSlash(from)\n\t\tto = filepath.FromSlash(to)\n\t}\n\tfromPath := filepath.Join(s.path, from)\n\ttoPath := filepath.Join(s.path, to)\n\tprefix := longestCommonPrefix(fromPath, toPath)\n\n\tfromRel := strings.TrimPrefix(fromPath, prefix+string(filepath.Separator))\n\ttoRel := strings.TrimPrefix(toPath, prefix+string(filepath.Separator))\n\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"can not get current working directory: %w\", err)\n\t}\n\n\tdefer func() {\n\t\t_ = os.Chdir(cwd)\n\t}()\n\n\ttoDir := filepath.Dir(toPath)\n\tif err := os.MkdirAll(toDir, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination dir %q: %w\", toDir, err)\n\t}\n\n\tif err := os.Chdir(toDir); err != nil {\n\t\treturn fmt.Errorf(\"can no change to link dir %q: %w\", toDir, err)\n\t}\n\n\tlinkDst := addRel(fromRel, toRel)\n\n\tdebug.Log(\"path: %q\\n\\tfromPath:\\t%q\\n\\ttoPath:\\t\\t%q\\n\\tprefix:\\t\\t%q\\n\\tfromRel:\\t%q\\n\\ttoRel:\\t\\t%q\\n\\ttoDir:\\t\\t%q\\n\\tlinkDst:\\t%q\",\n\t\ts.path, fromPath, toPath, prefix, fromRel, toRel, toDir, linkDst)\n\n\treturn os.Symlink(linkDst, filepath.Base(toRel))\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/link_test.go",
    "content": "package fs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLongestCommonPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tSrc    string\n\t\tDst    string\n\t\tPrefix string\n\t}{\n\t\t{\n\t\t\tSrc:    \"foo/bar/baz/zab.txt\",\n\t\t\tDst:    \"foo/baz/foo.txt\",\n\t\t\tPrefix: \"foo\",\n\t\t},\n\t} {\n\t\tprefix := longestCommonPrefix(tc.Src, tc.Dst)\n\t\tassert.Equal(t, tc.Prefix, prefix)\n\t}\n}\n\nfunc TestAddRel(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tSrc string\n\t\tDst string\n\t\tOut string\n\t}{\n\t\t{\n\t\t\tSrc: \"bar/baz.txt\",\n\t\t\tDst: \"baz/foo.txt\",\n\t\t\tOut: \"../bar/baz.txt\",\n\t\t},\n\t} {\n\t\tassert.Equal(t, tc.Out, addRel(tc.Src, tc.Dst))\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/loader.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nconst (\n\tname = \"fs\"\n)\n\nfunc init() {\n\tbackend.StorageRegistry.Register(backend.FS, name, &loader{})\n}\n\ntype loader struct{}\n\n// New implements backend.StorageLoader.\nfunc (l loader) New(ctx context.Context, path string) (backend.Storage, error) {\n\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\tbe := New(path)\n\tdebug.Log(\"Using Storage Backend: %s\", be.String())\n\n\treturn be, nil\n}\n\nfunc (l loader) Init(ctx context.Context, path string) (backend.Storage, error) {\n\tif err := os.MkdirAll(path, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn l.New(ctx, path)\n}\n\n// Clone is a no-op.\nfunc (l loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) {\n\treturn l.New(ctx, path)\n}\n\n// Handles returns true if the given path is supported by this backend. Will always return\n// true if the directory exists.\nfunc (l loader) Handles(ctx context.Context, path string) error {\n\tpath = fsutil.ExpandHomedir(path)\n\n\tif fsutil.IsDir(path) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"dir not found\")\n}\n\n// Priority returns the priority of this backend. Should always be higher than\n// the more specific ones, e.g. gitfs.\nfunc (l loader) Priority() int {\n\treturn 50\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/rcs.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n)\n\n// Add does nothing.\nfunc (s *Store) Add(ctx context.Context, args ...string) error {\n\treturn store.ErrGitNotInit\n}\n\n// TryAdd does nothing.\nfunc (s *Store) TryAdd(ctx context.Context, args ...string) error {\n\treturn nil\n}\n\n// Commit does nothing.\nfunc (s *Store) Commit(ctx context.Context, msg string) error {\n\treturn store.ErrGitNotInit\n}\n\n// TryCommit does nothing.\nfunc (s *Store) TryCommit(ctx context.Context, msg string) error {\n\treturn nil\n}\n\n// Push does nothing.\nfunc (s *Store) Push(ctx context.Context, origin, branch string) error {\n\treturn store.ErrGitNotInit\n}\n\n// TryPush does nothing.\nfunc (s *Store) TryPush(ctx context.Context, origin, branch string) error {\n\treturn nil\n}\n\n// Pull does nothing.\nfunc (s *Store) Pull(ctx context.Context, origin, branch string) error {\n\treturn store.ErrGitNotInit\n}\n\n// Cmd does nothing.\nfunc (s *Store) Cmd(ctx context.Context, name string, args ...string) error {\n\treturn nil\n}\n\n// Init does nothing.\nfunc (s *Store) Init(context.Context, string, string) error {\n\treturn backend.ErrNotSupported\n}\n\n// InitConfig does nothing.\nfunc (s *Store) InitConfig(context.Context, string, string) error {\n\treturn nil\n}\n\n// AddRemote does nothing.\nfunc (s *Store) AddRemote(ctx context.Context, remote, url string) error {\n\treturn backend.ErrNotSupported\n}\n\n// RemoveRemote does nothing.\nfunc (s *Store) RemoveRemote(ctx context.Context, remote string) error {\n\treturn backend.ErrNotSupported\n}\n\n// Revisions is not implemented.\nfunc (s *Store) Revisions(context.Context, string) ([]backend.Revision, error) {\n\treturn []backend.Revision{\n\t\t{\n\t\t\tHash: \"latest\",\n\t\t\tDate: time.Now(),\n\t\t},\n\t}, backend.ErrNotSupported\n}\n\n// GetRevision only supports getting the latest revision.\nfunc (s *Store) GetRevision(ctx context.Context, name string, revision string) ([]byte, error) {\n\tif revision == \"HEAD\" || revision == \"latest\" {\n\t\treturn s.Get(ctx, name)\n\t}\n\n\treturn []byte(\"\"), backend.ErrNotSupported\n}\n\n// Status is not implemented.\nfunc (s *Store) Status(context.Context) ([]byte, error) {\n\treturn []byte(\"\"), backend.ErrNotSupported\n}\n\n// Compact is not implemented.\nfunc (s *Store) Compact(context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/rcs_test.go",
    "content": "package fs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRCS(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tpath := t.TempDir()\n\n\tg := New(path)\n\t// the fs backend does not support the RCS operations\n\trequire.Error(t, g.Add(ctx, \"foo\", \"bar\"))\n\trequire.Error(t, g.Commit(ctx, \"foobar\"))\n\trequire.Error(t, g.Push(ctx, \"foo\", \"bar\"))\n\trequire.Error(t, g.Pull(ctx, \"foo\", \"bar\"))\n\trequire.NoError(t, g.Cmd(ctx, \"foo\", \"bar\"))\n\trequire.Error(t, g.Init(ctx, \"foo\", \"bar\"))\n\trequire.NoError(t, g.InitConfig(ctx, \"foo\", \"bar\"))\n\tassert.Equal(t, \"fs\", g.Name())\n\trequire.Error(t, g.AddRemote(ctx, \"foo\", \"bar\"))\n\trevs, err := g.Revisions(ctx, \"foo\")\n\trequire.Error(t, err)\n\tassert.Len(t, revs, 1)\n\tbody, err := g.GetRevision(ctx, \"foo\", \"latest\")\n\trequire.Error(t, err)\n\tassert.Empty(t, string(body))\n\trequire.Error(t, g.RemoveRemote(ctx, \"foo\"))\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/store.go",
    "content": "// Package fs implement a password-store compatible on disk storage layout\n// with unencrypted paths.\npackage fs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// Store is a fs based store.\ntype Store struct {\n\tpath string\n}\n\n// New creates a new store.\nfunc New(dir string) *Store {\n\tif d, err := filepath.EvalSymlinks(dir); err == nil {\n\t\tdir = d\n\t}\n\n\treturn &Store{\n\t\tpath: fsutil.ExpandHomedir(dir),\n\t}\n}\n\n// Get retrieves the named content.\nfunc (s *Store) Get(ctx context.Context, name string) ([]byte, error) {\n\tif runtime.GOOS == \"windows\" {\n\t\tname = filepath.FromSlash(name)\n\t}\n\n\tpath := filepath.Join(s.path, filepath.Clean(name))\n\tdebug.V(3).Log(\"Reading %s from %s\", name, path)\n\n\treturn os.ReadFile(path)\n}\n\n// Set writes the given content.\nfunc (s *Store) Set(ctx context.Context, name string, value []byte) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tname = filepath.FromSlash(name)\n\t}\n\n\tfilename := filepath.Join(s.path, filepath.Clean(name))\n\tfiledir := filepath.Dir(filename)\n\n\tif !fsutil.IsDir(filedir) {\n\t\tif err := os.MkdirAll(filedir, 0o700); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdebug.V(3).Log(\"Writing %s to %q\", name, filename)\n\n\t// if we ever try to write a secret that is identical (in ciphertext) to the secret in store,\n\t// we might want to act differently\n\t// (for instance, by not adding/committing/pushing the secret in git,\n\t//  or by panicking in the case of password generation)\n\toldvalue, err := os.ReadFile(filename)\n\tif err == nil && bytes.Equal(oldvalue, value) {\n\t\treturn store.ErrMeaninglessWrite\n\t}\n\n\treturn os.WriteFile(filename, value, 0o644)\n}\n\n// Move moves the named entity to the new location.\nfunc (s *Store) Move(ctx context.Context, from, to string, del bool) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tfrom = filepath.FromSlash(from)\n\t\tto = filepath.FromSlash(to)\n\t}\n\n\tfromFn := filepath.Join(s.path, filepath.Clean(from))\n\ttoFn := filepath.Join(s.path, filepath.Clean(to))\n\ttoDir := filepath.Dir(toFn)\n\n\tif !fsutil.IsDir(toDir) {\n\t\tif err := os.MkdirAll(toDir, 0o700); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create directory %q: %w\", toDir, err)\n\t\t}\n\t}\n\tdebug.V(3).Log(\"Copying %q (%q) to %q (%q)\", from, fromFn, to, toFn)\n\n\tif del {\n\t\tif err := os.Rename(fromFn, toFn); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to copy %q to %q: %w\", from, to, err)\n\t\t}\n\n\t\treturn s.removeEmptyParentDirectories(fromFn)\n\t}\n\n\treturn fsutil.CopyFile(fromFn, toFn)\n}\n\n// Delete removes the named entity.\nfunc (s *Store) Delete(ctx context.Context, name string) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tname = filepath.FromSlash(name)\n\t}\n\tpath := filepath.Join(s.path, filepath.Clean(name))\n\tdebug.V(3).Log(\"Deleting %s from %s\", name, path)\n\n\tif err := os.Remove(path); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.removeEmptyParentDirectories(path)\n}\n\n// Deletes all empty parent directories up to the store root.\nfunc (s *Store) removeEmptyParentDirectories(path string) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tpath = filepath.FromSlash(path)\n\t}\n\tparent := filepath.Dir(path)\n\n\tif relpath, err := filepath.Rel(s.path, parent); err != nil {\n\t\treturn err\n\t} else if strings.HasPrefix(relpath, \".\") {\n\t\treturn nil\n\t}\n\n\tdebug.V(1).Log(\"removing empty parent dir: %q\", parent)\n\terr := os.Remove(parent)\n\tswitch {\n\tcase err == nil:\n\t\treturn s.removeEmptyParentDirectories(parent)\n\tcase notEmptyErr(err):\n\t\t// ignore when directory is non-empty.\n\t\treturn nil\n\tdefault:\n\t\treturn err\n\t}\n}\n\n// Exists checks if the named entity exists.\nfunc (s *Store) Exists(ctx context.Context, name string) bool {\n\tif runtime.GOOS == \"windows\" {\n\t\tname = filepath.FromSlash(name)\n\t}\n\tpath := filepath.Join(s.path, filepath.Clean(name))\n\tfound := fsutil.IsFile(path)\n\tdebug.V(2).Log(\"Checking if '%s' exists at %s: %t\", name, path, found)\n\n\treturn found\n}\n\n// List returns a list of all entities\n// e.g. foo, far/bar baz/.bang\n// directory separator are normalized using `/`.\nfunc (s *Store) List(ctx context.Context, prefix string) ([]string, error) {\n\tprefix = strings.TrimPrefix(prefix, \"/\")\n\tdebug.V(2).Log(\"Listing %s/%s\", s.path, prefix)\n\n\tfiles := make([]string, 0, 100)\n\tif err := walkSymlinks(s.path, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trelPath := strings.TrimPrefix(path, s.path+string(filepath.Separator)) + string(filepath.Separator)\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\trelPath = filepath.ToSlash(relPath)\n\t\t}\n\t\tif info.IsDir() && strings.HasPrefix(info.Name(), \".\") && path != s.path && !strings.HasPrefix(prefix, relPath) && filepath.Base(path) != filepath.Base(prefix) {\n\t\t\tdebug.V(3).Log(\"skipping dot dir (relPath: %s, prefix: %s)\", relPath, prefix)\n\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif path == s.path {\n\t\t\treturn nil\n\t\t}\n\t\tname := strings.TrimPrefix(path, s.path+string(filepath.Separator))\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tname = filepath.ToSlash(name)\n\t\t}\n\t\tif !strings.HasPrefix(name, prefix) {\n\t\t\treturn nil\n\t\t}\n\n\t\tfiles = append(files, name)\n\n\t\treturn nil\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\tsort.Strings(files)\n\n\treturn files, nil\n}\n\n// IsDir returns true if the named entity is a directory.\nfunc (s *Store) IsDir(ctx context.Context, name string) bool {\n\tif runtime.GOOS == \"windows\" {\n\t\tname = filepath.FromSlash(name)\n\t}\n\tpath := filepath.Join(s.path, filepath.Clean(name))\n\tisDir := fsutil.IsDir(path)\n\tdebug.V(2).Log(\"%s at %s is a directory? %t\", name, path, isDir)\n\n\treturn isDir\n}\n\n// Prune removes a named directory.\nfunc (s *Store) Prune(ctx context.Context, prefix string) error {\n\tpath := filepath.Join(s.path, filepath.Clean(prefix))\n\tdebug.Log(\"Purning %s from %s\", prefix, path)\n\n\tif err := os.RemoveAll(path); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.removeEmptyParentDirectories(path)\n}\n\n// Name returns the name of this backend.\nfunc (s *Store) Name() string {\n\treturn \"fs\"\n}\n\n// Version returns the version of this backend.\nfunc (s *Store) Version(context.Context) semver.Version {\n\treturn debug.ModuleVersion(\"github.com/gopasspw/gopass/internal/backend/storage/fs\")\n}\n\n// String implements fmt.Stringer.\nfunc (s *Store) String() string {\n\treturn fmt.Sprintf(\"fs(%s,path:%s)\", s.Version(context.TODO()).String(), s.path)\n}\n\n// Path returns the path to this storage.\nfunc (s *Store) Path() string {\n\treturn s.path\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/store_others.go",
    "content": "//go:build !windows\n\npackage fs\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc notEmptyErr(err error) bool {\n\tvar perr *os.PathError\n\tif errors.As(err, &perr) {\n\t\treturn errors.Is(perr.Err, syscall.ENOTEMPTY)\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/store_test.go",
    "content": "package fs\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSetAndGet(t *testing.T) {\n\tt.Parallel()\n\n\tinitialContent := []byte(`initial file content`)\n\totherContent := []byte(`other file content`)\n\tctx := config.NewContextInMemory()\n\n\tpath := t.TempDir()\n\n\ts := &Store{path}\n\n\tfileHasContent := func(filename string, content []byte) {\n\t\twritten, _ := s.Get(ctx, filename)\n\t\tassert.Equalf(t, content, written, \"content of file\")\n\t}\n\n\tfilename := filepath.Join(\"a\", \"b\", \"file\")\n\n\t// when file does not exist\n\tfileHasContent(filename, nil)\n\n\t// when the folder does not exist\n\t_ = s.Set(ctx, filename, initialContent)\n\tfileHasContent(filename, initialContent)\n\n\t// overwrite file\n\t_ = s.Set(ctx, filename, otherContent)\n\tfileHasContent(filename, otherContent)\n\n\t// when folder already exists, with unclean path\n\t_ = s.Set(ctx, filepath.Join(\"a\", \".\", \"b\", \"..\", \"other\"), initialContent)\n\tfileHasContent(filepath.Join(\"a\", \"other\"), initialContent)\n}\n\nfunc TestMove(t *testing.T) {\n\tt.Parallel()\n\n\tinitialContent := []byte(`initial file content`)\n\totherContent := []byte(`other file content`)\n\tctx := config.NewContextInMemory()\n\n\tpath := t.TempDir()\n\n\ts := &Store{path}\n\n\tfileHasContent := func(filename string, content []byte) {\n\t\twritten, _ := s.Get(ctx, filename)\n\t\tassert.Equalf(t, content, written, \"content of file\")\n\t}\n\n\tfilename := \"src\"\n\n\t// when file does not exist\n\tfileHasContent(filename, nil)\n\n\t// when the folder does not exist\n\t_ = s.Set(ctx, filename, initialContent)\n\tfileHasContent(filename, initialContent)\n\n\t// move file\n\trequire.NoError(t, s.Move(ctx, filename, \"dst1\", true))\n\tfileHasContent(\"dst1\", initialContent)\n\n\t// overwrite file\n\t_ = s.Set(ctx, \"dst2\", otherContent)\n\tfileHasContent(\"dst2\", otherContent)\n\n\t// move file\n\trequire.NoError(t, s.Move(ctx, \"dst1\", \"dst2\", true))\n\tfileHasContent(\"dst1\", nil)\n\tfileHasContent(\"dst2\", initialContent)\n\n\t// copy file\n\trequire.NoError(t, s.Move(ctx, \"dst2\", \"dst3\", false))\n\tfileHasContent(\"dst2\", initialContent)\n\tfileHasContent(\"dst3\", initialContent)\n}\n\nfunc TestRemoveEmptyParentDirectories(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tstoreRoot     []string\n\t\tsubdirs       []string\n\t\texpectPresent []string\n\t\texpectGone    []string\n\t\tprepare       func(string)\n\t}{\n\t\t{\n\t\t\tname:          \"only empty subdirs\",\n\t\t\tstoreRoot:     []string{\"store-root\"},\n\t\t\tsubdirs:       []string{\"a\", \"b\", \"c\"},\n\t\t\texpectPresent: []string{\"store-root\"},\n\t\t\texpectGone:    []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty subdirs, nested root\",\n\t\t\tstoreRoot:     []string{\"root-parent\", \"store-root\"},\n\t\t\tsubdirs:       []string{\"a\", \"b\"},\n\t\t\texpectPresent: []string{\"root-parent\", \"store-root\"},\n\t\t\texpectGone:    []string{\"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"file in outer dir\",\n\t\t\tstoreRoot:     []string{\"root-parent\", \"store-root\"},\n\t\t\tsubdirs:       []string{\"a\", \"b\"},\n\t\t\texpectPresent: []string{\"root-parent\", \"store-root\", \"a\", \"b\"},\n\t\t\tprepare: func(path string) {\n\t\t\t\tf, _ := os.Create(filepath.Join(path, \"some-file\"))\n\t\t\t\t_ = f.Close()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"file in parent dir\",\n\t\t\tstoreRoot:     []string{\"store-root\"},\n\t\t\tsubdirs:       []string{\"a\", \"b\"},\n\t\t\texpectPresent: []string{\"store-root\", \"a\"},\n\t\t\texpectGone:    []string{\"b\"},\n\t\t\tprepare: func(path string) {\n\t\t\t\tf, _ := os.Create(filepath.Join(path, \"..\", \"file-in-parent\"))\n\t\t\t\t_ = f.Close()\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttd := t.TempDir()\n\n\t\t\tpath := filepath.Join(append([]string{td}, test.storeRoot...)...)\n\t\t\tsubdir := filepath.Join(append([]string{path}, test.subdirs...)...)\n\n\t\t\tif err := os.MkdirAll(subdir, 0o777); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tif test.prepare != nil {\n\t\t\t\ttest.prepare(subdir)\n\t\t\t}\n\n\t\t\ts := &Store{\n\t\t\t\tpath,\n\t\t\t}\n\t\t\tif err := s.removeEmptyParentDirectories(filepath.Join(subdir, \"deletedFile\")); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tdir := td\n\t\t\tfor _, d := range test.expectPresent {\n\t\t\t\tdir = filepath.Join(dir, d)\n\t\t\t\tassert.DirExists(t, dir)\n\t\t\t}\n\t\t\tfor _, d := range test.expectGone {\n\t\t\t\tdir = filepath.Join(dir, d)\n\t\t\t\tif _, err := os.Stat(dir); err == nil || !os.IsNotExist(err) {\n\t\t\t\t\tt.Errorf(\"Directory %s should not exist\", dir)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDelete(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tlocation  []string\n\t\ttoDelete  []string\n\t\tshouldErr bool\n\t}{\n\t\t{\n\t\t\tname:     \"simple paths\",\n\t\t\tlocation: []string{\"a\", \"b\"},\n\t\t\ttoDelete: []string{\"a\", \"b\", \"secret\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"non-existent file\",\n\t\t\ttoDelete:  []string{\"a\", \"b\", \"other\"},\n\t\t\tlocation:  []string{\"a\", \"b\"},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"deletion of non-empty dir not allowed\",\n\t\t\ttoDelete:  []string{\"a\"},\n\t\t\tlocation:  []string{\"a\"},\n\t\t\tshouldErr: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"unclean path, with parent\",\n\t\t\tlocation: []string{\"a\"},\n\t\t\ttoDelete: []string{\"a\", \"..\", \"a\", \"secret\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"unclean path, with dots\",\n\t\t\tlocation: []string{\"a\"},\n\t\t\ttoDelete: []string{\".\", \"a\", \".\", \".\", \"secret\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"unclean path, with dots and parent\",\n\t\t\tlocation: []string{\"a\", \"b\"},\n\t\t\ttoDelete: []string{\".\", \"a\", \".\", \"b\", \"..\", \".\", \"b\", \"secret\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tpath := t.TempDir()\n\n\t\t\tsubdir := filepath.Join(append([]string{path}, test.location...)...)\n\t\t\tif err := os.MkdirAll(subdir, 0o777); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tfile := filepath.Join(subdir, \"secret\")\n\t\t\tif f, err := os.Create(file); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t} else {\n\t\t\t\t_ = f.Close()\n\t\t\t}\n\n\t\t\tstore := &Store{\n\t\t\t\tpath,\n\t\t\t}\n\t\t\terr := store.Delete(config.NewContextInMemory(), filepath.Join(test.toDelete...))\n\n\t\t\tif test.shouldErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"Deletion should fail\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(\"Deletion should not fail\")\n\t\t\t\t}\n\t\t\t\tif _, err = os.Stat(file); !os.IsNotExist(err) {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/store_windows.go",
    "content": "package fs\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc notEmptyErr(err error) bool {\n\treturn err.(*os.PathError).Err == syscall.ERROR_DIR_NOT_EMPTY\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/walk.go",
    "content": "package fs\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc walkSymlinks(path string, walkFn filepath.WalkFunc) error {\n\treturn walk(path, path, walkFn)\n}\n\nfunc walk(filename, linkDir string, walkFn filepath.WalkFunc) error {\n\tsWalkFn := func(path string, info fs.FileInfo, _ error) error {\n\t\tfname, err := filepath.Rel(filename, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpath = filepath.Join(linkDir, fname)\n\n\t\t// handle non-symlinks\n\t\tif info.Mode()&fs.ModeSymlink != fs.ModeSymlink {\n\t\t\treturn walkFn(path, info, err)\n\t\t}\n\n\t\t// handle symlinks\n\t\tdestPath, err := filepath.EvalSymlinks(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdestInfo, err := os.Lstat(destPath)\n\t\tif err != nil {\n\t\t\treturn walkFn(path, destInfo, err)\n\t\t}\n\n\t\tif destInfo.IsDir() {\n\t\t\treturn walk(destPath, path, walkFn)\n\t\t}\n\n\t\treturn walkFn(path, info, err)\n\t}\n\n\treturn filepath.Walk(filename, sWalkFn)\n}\n"
  },
  {
    "path": "internal/backend/storage/fs/walk_test.go",
    "content": "package fs\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWalkTooLong(t *testing.T) {\n\tt.Parallel()\n\t// Walking a path with a symlink loop should fail.\n\n\ttd := t.TempDir()\n\tstoreDir := filepath.Join(td, \"store\")\n\tfn := filepath.Join(storeDir, \"real\", \"file.txt\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(fn), 0o700))\n\trequire.NoError(t, os.WriteFile(fn, []byte(\"foobar\"), 0o600))\n\n\tptr := filepath.Join(storeDir, \"path\", \"via\", \"link\")\n\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(ptr), 0o700))\n\n\trequire.NoError(t, os.Symlink(filepath.Join(storeDir, \"path\"), filepath.Join(storeDir, \"path\", \"via\", \"loop\")))\n\n\t// test the walkFunc\n\trequire.Error(t, walkSymlinks(storeDir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif info.IsDir() && strings.HasPrefix(info.Name(), \".\") {\n\t\t\treturn fs.SkipDir\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\trPath := strings.TrimPrefix(path, storeDir)\n\t\tif rPath == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn nil\n\t}))\n}\n\nfunc TestWalkSameFile(t *testing.T) {\n\tt.Parallel()\n\t// Two files visible via different link chains should both end up in the result set.\n\n\ttd := t.TempDir()\n\tstoreDir := filepath.Join(td, \"store\")\n\tfn := filepath.Join(storeDir, \"real\", \"file.txt\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(fn), 0o700))\n\trequire.NoError(t, os.WriteFile(fn, []byte(\"foobar\"), 0o600))\n\n\tptr1 := filepath.Join(storeDir, \"path\", \"via\", \"one\", \"link\")\n\tptr2 := filepath.Join(storeDir, \"another\", \"path\", \"to\", \"this\", \"file\")\n\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(ptr1), 0o700))\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(ptr2), 0o700))\n\n\trequire.NoError(t, os.Symlink(fn, ptr1))\n\trequire.NoError(t, os.Symlink(fn, ptr2))\n\n\t// test the walkFunc\n\tseen := map[string]bool{}\n\twant := map[string]bool{\n\t\t\"another/path/to/this/file\": true,\n\t\t\"path/via/one/link\":         true,\n\t\t\"real/file.txt\":             true,\n\t}\n\n\trequire.NoError(t, walkSymlinks(storeDir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif info.IsDir() && strings.HasPrefix(info.Name(), \".\") {\n\t\t\treturn fs.SkipDir\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\trPath := strings.TrimPrefix(path, storeDir)\n\t\tif rPath == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\trPath = filepath.ToSlash(rPath) // support running this test on Windows\n\t\trPath = strings.TrimPrefix(rPath, \"/\")\n\t\tseen[rPath] = true\n\n\t\treturn nil\n\t}))\n\n\tassert.Equal(t, want, seen)\n}\n"
  },
  {
    "path": "internal/backend/storage/fs.go",
    "content": "package storage\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/storage/fs\" // register fs backend\n"
  },
  {
    "path": "internal/backend/storage/gitfs/commands.go",
    "content": "package gitfs\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Commands returns the commands that are available for the gitfs backend.\n// TODO: maybe we just want to add the Before action when populating the final\n// command slice (unless it's non-nil so backends can override it). A similar\n// approach could be taken with the Action function. We could wrap it, parse\n// \"global\" flags like store and put that into the context. A bit hacky\n// but on the other hand less ugly wrt. the function signature.\nfunc (l loader) Commands(i func(*cli.Context) error, s func(string) (string, error)) []*cli.Command {\n\treturn []*cli.Command{\n\t\t{\n\t\t\tName:  \"git\",\n\t\t\tUsage: \"Run a git command inside a password store: gopass git [--store=<store>] <git-command>\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"If the password store is a git repository, execute a git command \" +\n\t\t\t\t\"specified by git-command-args.\",\n\t\t\tHidden: true,\n\t\t\tBefore: i,\n\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\tctx := ctxutil.WithGlobalFlags(c)\n\t\t\t\tstore := c.String(\"store\")\n\n\t\t\t\tpath, err := s(store)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to get sub store %s: %s\", store, err)\n\t\t\t\t}\n\n\t\t\t\targs := c.Args().Slice()\n\t\t\t\tout.Noticef(ctx, \"Running 'git %s' in %s...\", strings.Join(args, \" \"), path)\n\t\t\t\tcmd := exec.CommandContext(ctx, \"git\", args...)\n\t\t\t\tcmd.Dir = path\n\t\t\t\tcmd.Stdout = os.Stdout\n\t\t\t\tcmd.Stderr = os.Stderr\n\t\t\t\tcmd.Stdin = os.Stdin\n\n\t\t\t\treturn cmd.Run()\n\t\t\t},\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:  \"store\",\n\t\t\t\t\tUsage: \"Store to operate on\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/config.go",
    "content": "package gitfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n)\n\nconst (\n\tfileMode = 0o600\n)\n\n// fixConfig sets up the git config for the password store in a way to simplifies some of the quirks\n// that git has. We'd prefer if that wasn't necessary but git has way too many modes of operation\n// and we need it to behave a predicatable as possible.\nfunc (g *Git) fixConfig(ctx context.Context) error {\n\t// set push default, to avoid issues with\n\t// \"fatal: The current branch master has multiple upstream branches, refusing to push\"\n\t// https://stackoverflow.com/questions/948354/default-behavior-of-git-push-without-a-branch-specified.\n\tif err := g.ConfigSet(ctx, \"push.default\", \"matching\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to set git config for push.default: %w\", err)\n\t}\n\n\tif err := g.ConfigSet(ctx, \"pull.rebase\", \"false\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to set git config for pull.rebase: %w\", err)\n\t}\n\n\t// setup for proper diffs.\n\tif err := g.ConfigSet(ctx, \"diff.gpg.binary\", \"true\"); err != nil {\n\t\tout.Errorf(ctx, \"Error while initializing git: %s\", err)\n\t}\n\tif err := g.ConfigSet(ctx, \"diff.gpg.textconv\", \"gpg --no-tty --decrypt\"); err != nil {\n\t\tout.Errorf(ctx, \"Error while initializing git: %s\", err)\n\t}\n\n\t// setup for persistent SSH connections.\n\tif sc := gitSSHCommand(); sc != \"\" {\n\t\tov, err := g.ConfigGet(ctx, \"core.sshCommand\")\n\t\t// only set sshCommand if it's not already set. Avoid overwriting user settings.\n\t\tif err != nil && ov == \"\" {\n\t\t\tif err := g.ConfigSet(ctx, \"core.sshCommand\", sc); err != nil {\n\t\t\t\tout.Errorf(ctx, \"Error while configuring persistent SSH connections: %s\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// InitConfig initialized and preparse the git config.\nfunc (g *Git) InitConfig(ctx context.Context, userName, userEmail string) error {\n\t// set commit identity.\n\tif userName != \"\" {\n\t\tif err := g.ConfigSet(ctx, \"user.name\", userName); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set git config user.name: %w\", err)\n\t\t}\n\t} else {\n\t\tout.Printf(ctx, \"Git Username not set\")\n\t}\n\tif userEmail != \"\" && strings.Contains(userEmail, \"@\") {\n\t\tif err := g.ConfigSet(ctx, \"user.email\", userEmail); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set git config user.email: %w\", err)\n\t\t}\n\t} else {\n\t\tout.Printf(ctx, \"Git Email not set\")\n\t}\n\n\t// ensure sane git config.\n\tif err := g.fixConfig(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to fix git config: %w\", err)\n\t}\n\n\tif err := os.WriteFile(filepath.Join(g.fs.Path(), \".gitattributes\"), []byte(\"*.gpg diff=gpg\\n\"), fileMode); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize git: %w\", err)\n\t}\n\tif err := g.Add(ctx, g.fs.Path()+\"/.gitattributes\"); err != nil {\n\t\tout.Warningf(ctx, \"Failed to add .gitattributes to git\")\n\t}\n\tif err := g.Commit(ctx, \"Configure git repository for gpg file diff.\"); err != nil {\n\t\tout.Warningf(ctx, \"Failed to commit .gitattributes to git\")\n\t}\n\n\treturn nil\n}\n\n// ConfigSet sets a local config value.\nfunc (g *Git) ConfigSet(ctx context.Context, key, value string) error {\n\t// return g.Cmd(ctx, \"gitConfigSet\", \"config\", \"--local\", key, value)\n\treturn g.cfg.SetLocal(key, value)\n}\n\n// ConfigGet returns a given config value.\nfunc (g *Git) ConfigGet(ctx context.Context, key string) (string, error) {\n\tif !g.IsInitialized() {\n\t\treturn \"\", store.ErrGitNotInit\n\t}\n\n\tvalue := g.cfg.Get(key)\n\tif value == \"\" {\n\t\tg.cfg.Reload()\n\n\t\tvalue = g.cfg.Get(key)\n\t}\n\n\treturn value, nil\n}\n\n// ConfigList returns all git config settings.\nfunc (g *Git) ConfigList(ctx context.Context) (map[string]string, error) {\n\tif !g.IsInitialized() {\n\t\treturn nil, store.ErrGitNotInit\n\t}\n\n\tkv := make(map[string]string, 23)\n\tfor _, k := range g.cfg.List(\"\") {\n\t\tkv[k] = g.cfg.Get(k)\n\t}\n\n\treturn kv, nil\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/config_test.go",
    "content": "package gitfs\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGitConfig(t *testing.T) {\n\tgitdir := filepath.Join(t.TempDir(), \"git\")\n\trequire.NoError(t, os.Mkdir(gitdir, 0o755))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tgit, err := Init(ctx, gitdir, \"Dead Beef\", \"dead.beef@example.org\")\n\trequire.NoError(t, err)\n\tun, err := git.ConfigGet(ctx, \"user.name\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"Dead Beef\", un)\n\n\trequire.NoError(t, git.InitConfig(ctx, \"Foo Bar\", \"foo.bar@example.org\"))\n\tun, err = git.ConfigGet(ctx, \"user.name\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"Foo Bar\", un)\n\n\trequire.NoError(t, git.ConfigSet(ctx, \"user.name\", \"foo\"))\n\tun, err = git.ConfigGet(ctx, \"user.name\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foo\", un)\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/git.go",
    "content": "// Package gitfs implements a git cli based RCS backend.\npackage gitfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gitconfig\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\ntype contextKey int\n\nconst (\n\tctxKeyPathOverride contextKey = iota\n)\n\nfunc withPathOverride(ctx context.Context, path string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPathOverride, path)\n}\n\nfunc getPathOverride(ctx context.Context, def string) string {\n\tif sv, ok := ctx.Value(ctxKeyPathOverride).(string); ok && sv != \"\" {\n\t\treturn sv\n\t}\n\n\treturn def\n}\n\n// Git is a cli based git backend.\ntype Git struct {\n\tfs  *fs.Store\n\tcfg *gitconfig.Configs\n}\n\n// New creates a new git cli based git backend.\nfunc New(path string) (*Git, error) {\n\tpath = fsutil.ExpandHomedir(path)\n\n\tgitDir := filepath.Join(path, \".git\")\n\tif !fsutil.IsDir(gitDir) {\n\t\treturn nil, fmt.Errorf(\"git repo does not exist at %s\", gitDir)\n\t}\n\n\treturn &Git{\n\t\tfs:  fs.New(path),\n\t\tcfg: gitconfig.New().LoadAll(gitDir),\n\t}, nil\n}\n\n// Clone clones an existing git repo and returns a new cli based git backend\n// configured for this clone repo.\nfunc Clone(ctx context.Context, repo, path, userName, userEmail string) (*Git, error) {\n\tg := &Git{\n\t\tfs:  fs.New(path),\n\t\tcfg: gitconfig.New(),\n\t}\n\n\tif err := g.Cmd(withPathOverride(ctx, filepath.Dir(path)), \"Clone\", \"clone\", repo, path); err != nil {\n\t\treturn nil, err\n\t}\n\n\tg.cfg.LoadAll(filepath.Join(path, \".git\"))\n\n\t// initialize the local git config.\n\tif err := g.InitConfig(ctx, userName, userEmail); err != nil {\n\t\treturn g, fmt.Errorf(\"failed to configure git: %w\", err)\n\t}\n\tout.Printf(ctx, \"git configured at %s\", g.fs.Path())\n\n\treturn g, nil\n}\n\n// Init initializes this store's git repo.\nfunc Init(ctx context.Context, path, userName, userEmail string) (*Git, error) {\n\tg := &Git{\n\t\tfs:  fs.New(path),\n\t\tcfg: gitconfig.New(),\n\t}\n\n\t// the git repo may be empty (i.e. no branches, cloned from a fresh remote)\n\t// or already initialized. Only run git init if the folder is completely empty.\n\tif !g.IsInitialized() {\n\t\tif err := g.Cmd(ctx, \"Init\", \"init\"); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to initialize git: %w\", err)\n\t\t}\n\t\tout.Printf(ctx, \"git initialized at %s\", g.fs.Path())\n\t}\n\n\tg.cfg.LoadAll(filepath.Join(path, \".git\"))\n\n\tif !ctxutil.IsGitInit(ctx) {\n\t\treturn g, nil\n\t}\n\n\t// initialize the local git config.\n\tif err := g.InitConfig(ctx, userName, userEmail); err != nil {\n\t\treturn g, fmt.Errorf(\"failed to configure git: %w\", err)\n\t}\n\tout.Printf(ctx, \"git configured at %s\", g.fs.Path())\n\n\t// add current content of the store.\n\tif err := g.Add(ctx, g.fs.Path()); err != nil {\n\t\treturn g, fmt.Errorf(\"failed to add %q to git: %w\", g.fs.Path(), err)\n\t}\n\n\t// commit if there is something to commit.\n\tif !g.HasStagedChanges(ctx) {\n\t\tdebug.Log(\"No staged changes\")\n\n\t\treturn g, nil\n\t}\n\n\tif err := g.Commit(ctx, \"Add current content of password store\"); err != nil {\n\t\treturn g, fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\treturn g, nil\n}\n\nfunc (g *Git) captureCmd(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {\n\tbufOut := &bytes.Buffer{}\n\tbufErr := &bytes.Buffer{}\n\n\tcmd := exec.CommandContext(ctx, \"git\", args[0:]...)\n\tcmd.Dir = getPathOverride(ctx, g.fs.Path())\n\tcmd.Stdout = bufOut\n\tcmd.Stderr = bufErr\n\n\tdebug.Log(\"store.%s: %s %+v (%s)\", name, cmd.Path, cmd.Args, g.fs.Path())\n\terr := cmd.Run()\n\n\treturn bufOut.Bytes(), bufErr.Bytes(), err\n}\n\n// Cmd runs an git command.\nfunc (g *Git) Cmd(ctx context.Context, name string, args ...string) error {\n\tstdout, stderr, err := g.captureCmd(ctx, name, args...)\n\tif err != nil {\n\t\tdebug.Log(\"CMD: %s %+v\\nError: %s\\nOutput:\\n  Stdout: %q\\n  Stderr: %q\", name, args, err, string(stdout), string(stderr))\n\n\t\treturn fmt.Errorf(\"%w: %s\", err, strings.TrimSpace(string(stderr)))\n\t}\n\n\treturn nil\n}\n\n// Name returns git.\nfunc (g *Git) Name() string {\n\treturn name\n}\n\n// Version returns the git version as major, minor and patch level.\nfunc (g *Git) Version(ctx context.Context) semver.Version {\n\tv := semver.Version{}\n\n\tcmd := exec.CommandContext(ctx, \"git\", \"version\")\n\tcmdout, err := cmd.Output()\n\tif err != nil {\n\t\tdebug.Log(\"Failed to run 'git version': %s\", err)\n\n\t\treturn v\n\t}\n\n\tsvStr := strings.TrimPrefix(string(cmdout), \"git version \")\n\tif p := strings.Fields(svStr); len(p) > 0 {\n\t\tsvStr = p[0]\n\t}\n\n\tsv, err := semver.ParseTolerant(svStr)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to parse %q as semver: %s\", svStr, err)\n\n\t\treturn v\n\t}\n\n\treturn sv\n}\n\nvar reLeadingNumber = regexp.MustCompile(`^\\s*(\\d+)\\D*`)\n\nfunc parseVersion(sv string) (semver.Version, error) {\n\tif sv, err := semver.ParseTolerant(sv); err == nil {\n\t\treturn sv, nil\n\t}\n\n\tparts := strings.SplitN(sv, \".\", 3)\n\tif len(parts) == 3 {\n\t\t// try to extract the number-only prefix from the patch level\n\t\t// e.g. \"windwows.2\" from \"2.42.0.windows.2\"\n\t\tmatches := reLeadingNumber.FindStringSubmatch(parts[2])\n\t\tif len(matches) > 0 {\n\t\t\tparts[2] = matches[1]\n\t\t\tsv = strings.Join(parts, \".\")\n\t\t}\n\t}\n\n\treturn semver.ParseTolerant(sv)\n}\n\n// IsInitialized returns true if this stores has an (probably) initialized .git folder.\nfunc (g *Git) IsInitialized() bool {\n\treturn fsutil.IsFile(filepath.Join(g.fs.Path(), \".git\", \"config\"))\n}\n\n// Add adds the listed files to the git index.\nfunc (g *Git) Add(ctx context.Context, files ...string) error {\n\tif !g.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tfor i := range files {\n\t\tfiles[i] = strings.TrimPrefix(files[i], g.fs.Path()+\"/\")\n\t}\n\n\targs := []string{\"add\", \"--all\", \"--force\"}\n\targs = append(args, files...)\n\n\treturn g.Cmd(ctx, \"gitAdd\", args...)\n}\n\n// TryAdd calls Add and returns nil if the git repo was not initialized.\nfunc (g *Git) TryAdd(ctx context.Context, files ...string) error {\n\terr := g.Add(ctx, files...)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"Git not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// HasStagedChanges returns true if there are any staged changes which can be committed.\nfunc (g *Git) HasStagedChanges(ctx context.Context) bool {\n\tif err := g.Cmd(ctx, \"gitDiffIndex\", \"diff-index\", \"--quiet\", \"HEAD\"); err != nil {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// ListUntrackedFiles lists untracked files.\nfunc (g *Git) ListUntrackedFiles(ctx context.Context) []string {\n\tstdout, _, err := g.captureCmd(ctx, \"gitLsFiles\", \"ls-files\", \".\", \"--exclude-standard\", \"--others\")\n\tif err != nil {\n\t\treturn []string{fmt.Sprintf(\"ERROR: %s\", err)}\n\t}\n\tuf := []string{}\n\tfor f := range strings.SplitSeq(string(stdout), \"\\n\") {\n\t\tif f == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tuf = append(uf, f)\n\t}\n\n\treturn uf\n}\n\n// Commit creates a new git commit with the given commit message.\nfunc (g *Git) Commit(ctx context.Context, msg string) error {\n\tif !g.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tif !g.HasStagedChanges(ctx) {\n\t\treturn store.ErrGitNothingToCommit\n\t}\n\n\targs := []string{\"commit\", fmt.Sprintf(\"--date=%d +00:00\", ctxutil.GetCommitTimestamp(ctx).UTC().Unix())}\n\t// if the message is empty git will open an editor\n\tif msg != \"\" {\n\t\targs = append(args, \"-m\", msg)\n\t}\n\n\treturn g.Cmd(ctx, \"gitCommit\", args...)\n}\n\n// TryCommit calls commit and returns nil if there was nothing to commit or if the git repo was not initialized.\nfunc (g *Git) TryCommit(ctx context.Context, msg string) error {\n\terr := g.Commit(ctx, msg)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNothingToCommit) {\n\t\tdebug.Log(\"Nothing to commit. Ignoring.\")\n\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"Git not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc (g *Git) defaultRemote(ctx context.Context, branch string) string {\n\topts, err := g.ConfigList(ctx)\n\tif err != nil {\n\t\treturn \"origin\"\n\t}\n\n\tremote := opts[\"branch.\"+branch+\".remote\"]\n\tif remote == \"\" {\n\t\treturn \"origin\"\n\t}\n\n\tneedle := \"remote.\" + remote + \".url\"\n\tfor k := range opts {\n\t\tif k == needle {\n\t\t\treturn remote\n\t\t}\n\t}\n\n\treturn \"origin\"\n}\n\nfunc (g *Git) defaultBranch(ctx context.Context) string {\n\tout, _, err := g.captureCmd(ctx, \"defaultBranch\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\")\n\tif err != nil || string(out) == \"\" {\n\t\t// see https://github.com/github/renaming.\n\t\treturn \"main\"\n\t}\n\n\treturn strings.TrimSpace(string(out))\n}\n\n// PushPull pushes the repo to it's origin.\n// optional arguments: remote and branch.\nfunc (g *Git) PushPull(ctx context.Context, op, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\tif !g.IsInitialized() {\n\t\tdebug.Log(\"Git in %s is not initialized. Can not push/pull\", g.Path())\n\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tif branch == \"\" {\n\t\tbranch = g.defaultBranch(ctx)\n\t}\n\n\tif remote == \"\" {\n\t\tremote = g.defaultRemote(ctx, branch)\n\t}\n\n\turlKey := \"remote.\" + remote + \".url\"\n\tif v, err := g.ConfigGet(ctx, urlKey); err != nil || v == \"\" {\n\t\tdebug.Log(\"No value for %q found in config. Keys: %+v\", urlKey, g.cfg.Keys())\n\n\t\treturn store.ErrGitNoRemote\n\t}\n\n\tif err := g.Cmd(ctx, \"gitPush\", \"pull\", remote, branch); err != nil {\n\t\tif op == \"pull\" {\n\t\t\treturn err\n\t\t}\n\t\tout.Warningf(ctx, \"Failed to pull before git push: %s\", err)\n\t}\n\n\tif op == \"pull\" {\n\t\treturn nil\n\t}\n\n\tif uf := g.ListUntrackedFiles(ctx); len(uf) > 0 {\n\t\tout.Warningf(ctx, \"Found untracked files: %+v\", uf)\n\t}\n\n\treturn g.Cmd(ctx, \"gitPush\", \"push\", remote, branch)\n}\n\n// TryPush calls Push and returns nil if the git repo was not initialized.\nfunc (g *Git) TryPush(ctx context.Context, remote, branch string) error {\n\terr := g.Push(ctx, remote, branch)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tswitch {\n\tcase errors.Is(err, store.ErrGitNotInit):\n\t\tdebug.Log(\"Git not initialized. Ignoring.\")\n\n\t\treturn nil\n\tcase errors.Is(err, store.ErrGitNoRemote):\n\t\tdebug.Log(\"Git has no remote. Ignoring.\")\n\n\t\treturn nil\n\tdefault:\n\t\treturn err\n\t}\n}\n\n// Push pushes to the git remote.\nfunc (g *Git) Push(ctx context.Context, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\n\treturn g.PushPull(ctx, \"push\", remote, branch)\n}\n\n// Pull pulls from the git remote.\nfunc (g *Git) Pull(ctx context.Context, remote, branch string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\n\treturn g.PushPull(ctx, \"pull\", remote, branch)\n}\n\n// AddRemote adds a new remote.\nfunc (g *Git) AddRemote(ctx context.Context, remote, url string) error {\n\treturn g.Cmd(ctx, \"gitAddRemote\", \"remote\", \"add\", remote, url)\n}\n\n// RemoveRemote removes a remote.\nfunc (g *Git) RemoveRemote(ctx context.Context, remote string) error {\n\treturn g.Cmd(ctx, \"gitRemoveRemote\", \"remote\", \"remove\", remote)\n}\n\n// Revisions will list all available revisions of the named entity\n// see http://blog.lost-theory.org/post/how-to-parse-git-log-output/\n// and https://git-scm.com/docs/git-log#_pretty_formats.\nfunc (g *Git) Revisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\targs := []string{\n\t\t\"log\",\n\t\t`--format=%H%x1f%an%x1f%ae%x1f%at%x1f%s%x1f%b%x1e`,\n\t\t\"--\",\n\t\tname,\n\t}\n\tstdout, stderr, err := g.captureCmd(ctx, \"Revisions\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\tso := string(stdout)\n\trevs := make([]backend.Revision, 0, strings.Count(so, \"\\x1e\"))\n\tfor rev := range strings.SplitSeq(so, \"\\x1e\") {\n\t\trev = strings.TrimSpace(rev)\n\t\tif rev == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tp := strings.Split(rev, \"\\x1f\")\n\t\tif len(p) < 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tr := backend.Revision{}\n\t\tr.Hash = p[0]\n\t\tif len(p) > 1 {\n\t\t\tr.AuthorName = p[1]\n\t\t}\n\n\t\tif len(p) > 2 {\n\t\t\tr.AuthorEmail = p[2]\n\t\t}\n\n\t\tif len(p) > 3 {\n\t\t\tif iv, err := strconv.ParseInt(p[3], 10, 64); err == nil {\n\t\t\t\tr.Date = time.Unix(iv, 0)\n\t\t\t}\n\t\t}\n\n\t\tif len(p) > 4 {\n\t\t\tr.Subject = p[4]\n\t\t}\n\n\t\tif len(p) > 5 {\n\t\t\tr.Body = p[5]\n\t\t}\n\n\t\trevs = append(revs, r)\n\t}\n\n\tdebug.Log(\"Revisions for %s: %+v\", name, revs)\n\n\treturn revs, nil\n}\n\n// GetRevision will return the content of any revision of the named entity\n// see https://git-scm.com/docs/git-log#_pretty_formats.\nfunc (g *Git) GetRevision(ctx context.Context, name, revision string) ([]byte, error) {\n\tname = strings.TrimSpace(name)\n\trevision = strings.TrimSpace(revision)\n\targs := []string{\n\t\t\"show\",\n\t\trevision + \":\" + name,\n\t}\n\tstdout, stderr, err := g.captureCmd(ctx, \"GetRevision\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Status return the git status output.\nfunc (g *Git) Status(ctx context.Context) ([]byte, error) {\n\tstdout, stderr, err := g.captureCmd(ctx, \"GitStatus\", \"status\")\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\\n%s\", string(stdout), string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Compact will run git gc.\nfunc (g *Git) Compact(ctx context.Context) error {\n\treturn g.Cmd(ctx, \"gitGC\", \"gc\", \"--aggressive\")\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/git_test.go",
    "content": "package gitfs\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGit(t *testing.T) {\n\ttd := t.TempDir()\n\n\tgitdir := filepath.Join(td, \"git\")\n\trequire.NoError(t, os.Mkdir(gitdir, 0o755))\n\tgitdir2 := filepath.Join(td, \"git2\")\n\trequire.NoError(t, os.Mkdir(gitdir2, 0o755))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tt.Run(\"init new repo\", func(t *testing.T) {\n\t\tgit, err := Init(ctx, gitdir, \"Dead Beef\", \"dead.beef@example.org\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, git)\n\n\t\tsv := git.Version(ctx)\n\t\tassert.NotEmpty(t, sv.String())\n\n\t\tassert.True(t, git.IsInitialized())\n\t\ttf := filepath.Join(gitdir, \"some-file\")\n\t\trequire.NoError(t, os.WriteFile(tf, []byte(\"foobar\"), 0o644))\n\t\trequire.NoError(t, git.Add(ctx, \"some-file\"))\n\t\tassert.True(t, git.HasStagedChanges(ctx))\n\t\trequire.NoError(t, git.Commit(ctx, \"added some-file\"))\n\t\tassert.False(t, git.HasStagedChanges(ctx))\n\n\t\trequire.Error(t, git.Push(ctx, \"origin\", \"master\"))\n\t\trequire.Error(t, git.Pull(ctx, \"origin\", \"master\"))\n\t})\n\n\tt.Run(\"open existing repo\", func(t *testing.T) {\n\t\tgit, err := New(gitdir)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, git)\n\t\tassert.Equal(t, \"gitfs\", git.Name())\n\t\trequire.NoError(t, git.AddRemote(ctx, \"foo\", \"file:///tmp/foo\"))\n\t\trequire.NoError(t, git.RemoveRemote(ctx, \"foo\"))\n\t\trequire.Error(t, git.RemoveRemote(ctx, \"foo\"))\n\t})\n\n\tt.Run(\"clone existing repo\", func(t *testing.T) {\n\t\tgit, err := Clone(ctx, gitdir, gitdir2, \"\", \"\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, git)\n\t\tassert.Equal(t, \"gitfs\", git.Name())\n\n\t\ttf := filepath.Join(gitdir2, \"some-other-file\")\n\t\trequire.NoError(t, os.WriteFile(tf, []byte(\"foobar\"), 0o644))\n\t\trequire.NoError(t, git.Add(ctx, \"some-other-file\"))\n\t\trequire.NoError(t, git.Commit(ctx, \"added some-other-file\"))\n\n\t\trevs, err := git.Revisions(ctx, \"some-other-file\")\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, revs, 1)\n\n\t\tcontent, err := git.GetRevision(ctx, \"some-other-file\", revs[0].Hash)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"foobar\", string(content))\n\t})\n}\n\nfunc TestParseVersion(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname    string\n\t\tin      string\n\t\tsv      semver.Version\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"empty\",\n\t\t\tin:      \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid\",\n\t\t\tin:      \"foo\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid\",\n\t\t\tin:   \"2.30.0\",\n\t\t\tsv:   semver.MustParse(\"2.30.0\"),\n\t\t},\n\t\t{\n\t\t\tname: \"invalid-recovered\", // GH-2686\n\t\t\tin:   \"2.42.0.windows.2\",\n\t\t\tsv:   semver.MustParse(\"2.42.0\"),\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsv, err := parseVersion(tc.in)\n\t\t\tassert.Equal(t, tc.sv, sv)\n\t\t\tif tc.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/loader.go",
    "content": "package gitfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\nconst (\n\tname = \"gitfs\"\n)\n\nfunc init() {\n\tbackend.StorageRegistry.Register(backend.GitFS, name, &loader{})\n}\n\ntype loader struct{}\n\nfunc (l loader) New(ctx context.Context, path string) (backend.Storage, error) {\n\treturn New(path)\n}\n\n// Open implements backend.RCSLoader.\nfunc (l loader) Open(ctx context.Context, path string) (backend.Storage, error) {\n\treturn New(path)\n}\n\n// Clone implements backend.RCSLoader.\nfunc (l loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) {\n\treturn Clone(ctx, repo, path, termio.DetectName(ctx, nil), termio.DetectEmail(ctx, nil))\n}\n\n// Init implements backend.RCSLoader.\nfunc (l loader) Init(ctx context.Context, path string) (backend.Storage, error) {\n\treturn Init(ctx, path, termio.DetectName(ctx, nil), termio.DetectEmail(ctx, nil))\n}\n\nfunc (l loader) Handles(ctx context.Context, path string) error {\n\tpath = fsutil.ExpandHomedir(path)\n\tif !fsutil.IsDir(filepath.Join(path, \".git\")) {\n\t\treturn fmt.Errorf(\"no .git at %s\", path)\n\t}\n\n\treturn nil\n}\n\nfunc (l loader) Priority() int {\n\treturn 11\n}\n\nfunc (l loader) String() string {\n\treturn name\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/ssh_darwin.go",
    "content": "//go:build darwin\n\npackage gitfs\n\n// gitSSHCommand returns a SSH command instructing git to use SSH\n// with persistent connections through a custom socket.\n// See https://linux.die.net/man/5/ssh_config and\n// https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand\n//\n// Note: Setting GIT_SSH_COMMAND, possibly to an empty string, will take\n// precedence over this setting.\n//\n// %C is a hash of %l%h%p%r and should avoid \"path too long for unix domain socket\"\n// errors. On MacOS this doesn't always seem to work, so we're using a hardcoded\n// /tmp instead.\nfunc gitSSHCommand() string {\n\treturn \"ssh -oControlMaster=auto -oControlPersist=600 -oControlPath=/tmp/.ssh-%C\"\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/ssh_others.go",
    "content": "//go:build !windows && !darwin\n\npackage gitfs\n\nimport \"os\"\n\n// gitSSHCommand returns a SSH command instructing git to use SSH\n// with persistent connections through a custom socket.\n// See https://linux.die.net/man/5/ssh_config and\n// https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand\n//\n// Note: Setting GIT_SSH_COMMAND, possibly to an empty string, will take\n// precedence over this setting.\n//\n// %C is a hash of %l%h%p%r and should avoid \"path too long for unix domain socket\"\n// errors. If you still encounter this error set TMPDIR to a short path, e.g. /tmp.\nfunc gitSSHCommand() string {\n\treturn \"ssh -oControlMaster=auto -oControlPersist=600 -oControlPath=\" + os.TempDir() + \"/.ssh-%C\"\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/ssh_windows.go",
    "content": "//go:build windows\n\npackage gitfs\n\nfunc gitSSHCommand() string {\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs/storage.go",
    "content": "package gitfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Get retrieves the named content.\nfunc (g *Git) Get(ctx context.Context, name string) ([]byte, error) {\n\treturn g.fs.Get(ctx, name)\n}\n\n// Set writes the given content.\nfunc (g *Git) Set(ctx context.Context, name string, value []byte) error {\n\treturn g.fs.Set(ctx, name, value)\n}\n\n// Delete removes the named entity.\nfunc (g *Git) Delete(ctx context.Context, name string) error {\n\treturn g.fs.Delete(ctx, name)\n}\n\n// Exists checks if the named entity exists.\nfunc (g *Git) Exists(ctx context.Context, name string) bool {\n\treturn g.fs.Exists(ctx, name)\n}\n\n// List returns a list of all entities\n// e.g. foo, far/bar baz/.bang\n// directory separator are normalized using `/`.\nfunc (g *Git) List(ctx context.Context, prefix string) ([]string, error) {\n\treturn g.fs.List(ctx, prefix)\n}\n\n// IsDir returns true if the named entity is a directory.\nfunc (g *Git) IsDir(ctx context.Context, name string) bool {\n\treturn g.fs.IsDir(ctx, name)\n}\n\n// Prune removes a named directory.\nfunc (g *Git) Prune(ctx context.Context, prefix string) error {\n\treturn g.fs.Prune(ctx, prefix)\n}\n\n// String implements fmt.Stringer.\nfunc (g *Git) String() string {\n\treturn fmt.Sprintf(\"gitfs(%s,path:%s)\", g.Version(context.TODO()).String(), g.fs.Path())\n}\n\n// Path returns the path to this storage.\nfunc (g *Git) Path() string {\n\treturn g.fs.Path()\n}\n\n// Fsck checks the storage integrity.\nfunc (g *Git) Fsck(ctx context.Context) error {\n\t// ensure sane git config.\n\tif err := g.fixConfig(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to fix git config: %w\", err)\n\t}\n\n\t// add any untracked files.\n\tif err := g.addUntrackedFiles(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to add untracked files: %w\", err)\n\t}\n\n\treturn g.fs.Fsck(ctx)\n}\n\nfunc (g *Git) addUntrackedFiles(ctx context.Context) error {\n\tut := g.ListUntrackedFiles(ctx)\n\tif len(ut) < 1 && !g.HasStagedChanges(ctx) {\n\t\tdebug.Log(\"no untracked or staged files found\")\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"untracked files found: %v\", ut)\n\tif err := g.Add(ctx, ut...); err != nil {\n\t\treturn fmt.Errorf(\"failed to add untracked files: %w\", err)\n\t}\n\n\treturn g.Commit(ctx, \"fsck\")\n}\n\n// Link creates a symlink.\nfunc (g *Git) Link(ctx context.Context, from, to string) error {\n\treturn g.fs.Link(ctx, from, to)\n}\n\n// Move moves from src to dst.\nfunc (g *Git) Move(ctx context.Context, src, dst string, del bool) error {\n\treturn g.fs.Move(ctx, src, dst, del)\n}\n"
  },
  {
    "path": "internal/backend/storage/gitfs.go",
    "content": "package storage\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/storage/gitfs\" // register gitfs backend\n"
  },
  {
    "path": "internal/backend/storage/jjfs/jj.go",
    "content": "// Package jjfs implements a jj cli based RCS backend.\npackage jjfs\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// JJFS is a cli based jj backend.\ntype JJFS struct {\n\tfs *fs.Store\n}\n\n// New creates a new jj cli based jj backend.\nfunc New(path string) (*JJFS, error) {\n\treturn &JJFS{\n\t\t\tfs: fs.New(path),\n\t\t},\n\t\tnil\n}\n\n// Init initializes this store's jj repo.\nfunc Init(ctx context.Context, path, userName, userEmail string) (*JJFS, error) {\n\tj := &JJFS{\n\t\tfs: fs.New(path),\n\t}\n\n\tif !j.IsInitialized() {\n\t\tif err := j.Cmd(ctx, \"Init\", \"git\", \"init\", \"--colocate\"); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to initialize jj: %w\", err)\n\t\t}\n\t\tout.Printf(ctx, \"jj initialized at %s\", j.fs.Path())\n\t}\n\n\tif err := j.Add(ctx, j.fs.Path()); err != nil {\n\t\treturn j, fmt.Errorf(\"failed to add %q to jj: %w\", j.fs.Path(), err)\n\t}\n\n\tif err := j.Commit(ctx, \"Add current content of password store\"); err != nil {\n\t\treturn j, fmt.Errorf(\"failed to commit changes to jj: %w\", err)\n\t}\n\n\treturn j, nil\n}\n\nfunc (j *JJFS) captureCmd(ctx context.Context, name string, args ...string) ([]byte, []byte, error) {\n\tbufOut := &bytes.Buffer{}\n\tbufErr := &bytes.Buffer{}\n\n\tcmd := exec.CommandContext(ctx, \"jj\", args[0:]...)\n\tcmd.Dir = j.fs.Path()\n\tcmd.Stdout = bufOut\n\tcmd.Stderr = bufErr\n\n\tdebug.Log(\"store.%s: %s %+v (%s)\", name, cmd.Path, cmd.Args, j.fs.Path())\n\terr := cmd.Run()\n\n\treturn bufOut.Bytes(), bufErr.Bytes(), err\n}\n\n// Cmd runs an jj command.\nfunc (j *JJFS) Cmd(ctx context.Context, name string, args ...string) error {\n\tstdout, stderr, err := j.captureCmd(ctx, name, args...)\n\tif err != nil {\n\t\tdebug.Log(\"CMD: %s %+v\\nError: %s\\nOutput:\\n  Stdout: %q\\n  Stderr: %q\", name, args, err, string(stdout), string(stderr))\n\n\t\treturn fmt.Errorf(\"%w: %s\", err, strings.TrimSpace(string(stderr)))\n\t}\n\n\treturn nil\n}\n\n// Name returns jj.\nfunc (j *JJFS) Name() string {\n\treturn \"jjfs\"\n}\n\n// Version returns the jj version.\nfunc (j *JJFS) Version(ctx context.Context) semver.Version {\n\tv := semver.Version{}\n\n\tstdout, _, err := j.captureCmd(ctx, \"version\", \"version\")\n\tif err != nil {\n\t\tdebug.Log(\"Failed to run 'jj version': %s\", err)\n\n\t\treturn v\n\t}\n\n\tsv, err := semver.ParseTolerant(string(stdout))\n\tif err != nil {\n\t\tdebug.Log(\"Failed to parse %q as semver: %s\", string(stdout), err)\n\n\t\treturn v\n\t}\n\n\treturn sv\n}\n\n// IsInitialized returns true if this stores has an (probably) initialized .jj folder.\nfunc (j *JJFS) IsInitialized() bool {\n\treturn fsutil.IsDir(j.fs.Path() + \"/.jj\")\n}\n\n// Add adds the listed files to the jj index.\nfunc (j *JJFS) Add(ctx context.Context, files ...string) error {\n\tif !j.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\tfor i := range files {\n\t\tfiles[i] = strings.TrimPrefix(files[i], j.fs.Path()+\"/\")\n\t}\n\n\targs := []string{\"git\", \"add\"}\n\targs = append(args, files...)\n\n\treturn j.Cmd(ctx, \"jjGitAdd\", args...)\n}\n\n// TryAdd calls Add and returns nil if the git repo was not initialized.\nfunc (j *JJFS) TryAdd(ctx context.Context, files ...string) error {\n\terr := j.Add(ctx, files...)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"JJFS not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// Commit creates a new jj commit with the given commit message.\nfunc (j *JJFS) Commit(ctx context.Context, msg string) error {\n\tif !j.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\treturn j.Cmd(ctx, \"jjCommit\", \"describe\", \"-m\", msg)\n}\n\n// TryCommit calls commit and returns nil if there was nothing to commit or if the git repo was not initialized.\nfunc (j *JJFS) TryCommit(ctx context.Context, msg string) error {\n\terr := j.Commit(ctx, msg)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNothingToCommit) {\n\t\tdebug.Log(\"Nothing to commit. Ignoring.\")\n\n\t\treturn nil\n\t}\n\tif errors.Is(err, store.ErrGitNotInit) {\n\t\tdebug.Log(\"JJFS not initialized. Ignoring.\")\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// Push pushes to the git remote.\nfunc (j *JJFS) Push(ctx context.Context, _, _ string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\tif !j.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\treturn j.Cmd(ctx, \"jjGitPush\", \"git\", \"push\")\n}\n\n// Pull pulls from the git remote.\nfunc (j *JJFS) Pull(ctx context.Context, _, _ string) error {\n\tif ctxutil.IsNoNetwork(ctx) {\n\t\tdebug.Log(\"Skipping network ops. NoNetwork=true\")\n\n\t\treturn nil\n\t}\n\tif !j.IsInitialized() {\n\t\treturn store.ErrGitNotInit\n\t}\n\n\treturn j.Cmd(ctx, \"jjGitPull\", \"git\", \"fetch\")\n}\n\n// TryPush calls Push and returns nil if the git repo was not initialized.\nfunc (j *JJFS) TryPush(ctx context.Context, remote, branch string) error {\n\terr := j.Push(ctx, remote, branch)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tswitch {\n\tcase errors.Is(err, store.ErrGitNotInit):\n\t\tdebug.Log(\"JJFS not initialized. Ignoring.\")\n\n\t\treturn nil\n\tcase errors.Is(err, store.ErrGitNoRemote):\n\t\tdebug.Log(\"JJFS has no remote. Ignoring.\")\n\n\t\treturn nil\n\tdefault:\n\t\treturn err\n\t}\n}\n\n// Revisions will list all available revisions of the named entity.\nfunc (j *JJFS) Revisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\tif !j.IsInitialized() {\n\t\treturn nil, store.ErrGitNotInit\n\t}\n\n\targs := []string{\n\t\t\"log\",\n\t\t\"--revisions\", \"@\",\n\t\t\"--template\",\n\t\t\"commit_id \\\"\\x1f\\\" author \\\"\\x1f\\\" committer.timestamp() \\\"\\x1f\\\" description \\\"\\x1e\\\"\",\n\t\t\"--\",\n\t\tname,\n\t}\n\tstdout, stderr, err := j.captureCmd(ctx, \"Revisions\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\tso := string(stdout)\n\trevs := make([]backend.Revision, 0, strings.Count(so, \"\\x1e\"))\n\tfor rev := range strings.SplitSeq(so, \"\\x1e\") {\n\t\trev = strings.TrimSpace(rev)\n\t\tif rev == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tp := strings.Split(rev, \"\\x1f\")\n\t\tif len(p) < 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tr := backend.Revision{}\n\t\tr.Hash = p[0]\n\t\tif len(p) > 1 {\n\t\t\tr.AuthorName = p[1]\n\t\t}\n\n\t\tif len(p) > 2 {\n\t\t\tif iv, err := strconv.ParseInt(p[2], 10, 64); err == nil {\n\t\t\t\tr.Date = time.Unix(iv, 0)\n\t\t\t}\n\t\t}\n\n\t\tif len(p) > 3 {\n\t\t\tr.Subject = p[3]\n\t\t}\n\n\t\trevs = append(revs, r)\n\t}\n\n\tdebug.Log(\"Revisions for %s: %+v\", name, revs)\n\n\treturn revs, nil\n}\n\n// GetRevision will return the content of any revision of the named entity.\nfunc (j *JJFS) GetRevision(ctx context.Context, name, revision string) ([]byte, error) {\n\tif !j.IsInitialized() {\n\t\treturn nil, store.ErrGitNotInit\n\t}\n\n\tname = strings.TrimSpace(name)\n\trevision = strings.TrimSpace(revision)\n\targs := []string{\n\t\t\"show\",\n\t\t\"--revision\", revision,\n\t\tname,\n\t}\n\tstdout, stderr, err := j.captureCmd(ctx, \"GetRevision\", args...)\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\", string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Status return the jj status output.\nfunc (j *JJFS) Status(ctx context.Context) ([]byte, error) {\n\tstdout, stderr, err := j.captureCmd(ctx, \"jjStatus\", \"status\")\n\tif err != nil {\n\t\tdebug.Log(\"Command failed: %s\\n%s\", string(stdout), string(stderr))\n\n\t\treturn nil, err\n\t}\n\n\treturn stdout, nil\n}\n\n// Compact will run git gc.\nfunc (j *JJFS) Compact(ctx context.Context) error {\n\treturn j.Cmd(ctx, \"jjGitGC\", \"git\", \"gc\", \"--aggressive\")\n}\n\n// ListUntrackedFiles lists untracked files.\nfunc (j *JJFS) ListUntrackedFiles(ctx context.Context) []string {\n\tstdout, _, err := j.captureCmd(ctx, \"jjStatus\", \"status\", \"--no-patch\")\n\tif err != nil {\n\t\treturn []string{fmt.Sprintf(\"ERROR: %s\", err)}\n\t}\n\tuf := []string{}\n\tfor f := range strings.SplitSeq(string(stdout), \"\\n\") {\n\t\tif f == \"\" || len(f) < 3 {\n\t\t\tcontinue\n\t\t}\n\t\tif f[0] == 'A' {\n\t\t\tuf = append(uf, strings.TrimSpace(f[2:]))\n\t\t}\n\t}\n\n\treturn uf\n}\n\n// HasStagedChanges returns true if there are any staged changes which can be committed.\nfunc (j *JJFS) HasStagedChanges(ctx context.Context) bool {\n\tstdout, _, err := j.captureCmd(ctx, \"jjStatus\", \"status\", \"--no-patch\")\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn len(strings.TrimSpace(string(stdout))) > 0\n}\n\n// AddRemote adds a new remote.\nfunc (j *JJFS) AddRemote(ctx context.Context, remote, url string) error {\n\treturn j.Cmd(ctx, \"jjGitRemoteAdd\", \"git\", \"remote\", \"add\", remote, url)\n}\n\n// RemoveRemote removes a remote.\nfunc (j *JJFS) RemoveRemote(ctx context.Context, remote string) error {\n\treturn j.Cmd(ctx, \"jjGitRemoteRemove\", \"git\", \"remote\", \"remove\", remote)\n}\n\n// InitConfig initializes the git config.\nfunc (j *JJFS) InitConfig(ctx context.Context, name, email string) error {\n\treturn nil\n}\n\n// Get returns the content of a secret.\nfunc (j *JJFS) Get(ctx context.Context, name string) ([]byte, error) {\n\treturn j.fs.Get(ctx, name)\n}\n\n// Set writes the content of a secret.\nfunc (j *JJFS) Set(ctx context.Context, name string, value []byte) error {\n\treturn j.fs.Set(ctx, name, value)\n}\n\n// Delete removes a secret.\nfunc (j *JJFS) Delete(ctx context.Context, name string) error {\n\treturn j.fs.Delete(ctx, name)\n}\n\n// Exists checks if a secret exists.\nfunc (j *JJFS) Exists(ctx context.Context, name string) bool {\n\treturn j.fs.Exists(ctx, name)\n}\n\n// List returns a list of all secrets.\nfunc (j *JJFS) List(ctx context.Context, prefix string) ([]string, error) {\n\treturn j.fs.List(ctx, prefix)\n}\n\n// IsDir returns true if the given path is a directory.\nfunc (j *JJFS) IsDir(ctx context.Context, name string) bool {\n\treturn j.fs.IsDir(ctx, name)\n}\n\n// Prune removes a directory.\nfunc (j *JJFS) Prune(ctx context.Context, prefix string) error {\n\treturn j.fs.Prune(ctx, prefix)\n}\n\n// Link creates a symlink.\nfunc (j *JJFS) Link(ctx context.Context, from, to string) error {\n\treturn j.fs.Link(ctx, from, to)\n}\n\n// Path returns the path to the storage.\nfunc (j *JJFS) Path() string {\n\treturn j.fs.Path()\n}\n\n// Fsck checks the storage for errors.\nfunc (j *JJFS) Fsck(ctx context.Context) error {\n\treturn j.fs.Fsck(ctx)\n}\n\n// Move moves a file.\nfunc (j *JJFS) Move(ctx context.Context, from, to string, del bool) error {\n\treturn j.fs.Move(ctx, from, to, del)\n}\n\nfunc (j *JJFS) String() string {\n\treturn j.fs.String()\n}\n"
  },
  {
    "path": "internal/backend/storage/jjfs/loader.go",
    "content": "// Package jjfs implements a jj cli based RCS backend.\npackage jjfs\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nfunc init() {\n\tbackend.StorageRegistry.Register(backend.JJFS, \"jjfs\", &loader{})\n}\n\ntype loader struct{}\n\nfunc (l loader) String() string {\n\treturn \"jjfs\"\n}\n\nfunc (l loader) Priority() int {\n\treturn 10\n}\n\nfunc (l loader) New(ctx context.Context, path string) (backend.Storage, error) {\n\treturn New(path)\n}\n\nfunc (l loader) Init(ctx context.Context, path string) (backend.Storage, error) {\n\treturn Init(ctx, path, \"\", \"\")\n}\n\nfunc (l loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) {\n\treturn nil, backend.ErrNotSupported\n}\n\nfunc (l loader) Handles(ctx context.Context, path string) error {\n\tif fsutil.IsDir(path + \"/.jj\") {\n\t\treturn nil\n\t}\n\n\treturn backend.ErrNotSupported\n}\n"
  },
  {
    "path": "internal/backend/storage/jjfs.go",
    "content": "// Package storage registers the jjfs backend.\npackage storage\n\nimport _ \"github.com/gopasspw/gopass/internal/backend/storage/jjfs\" // register jjfs backend\n"
  },
  {
    "path": "internal/backend/storage.go",
    "content": "package backend\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// ErrNotSupported is returned by backends for unsupported calls.\nvar ErrNotSupported = fmt.Errorf(\"not supported\")\n\n// StorageBackend is a type of storage backend.\ntype StorageBackend int\n\nconst (\n\t// FS is a filesystem-backed storage.\n\tFS StorageBackend = iota\n\t// GitFS is a filesystem-backed storage with Git.\n\tGitFS\n\t// FossilFS is a filesystem-backed storage with Fossil.\n\tFossilFS\n\t// JJ is a filesystem-backed storage with Jujutsu.\n\tJJFS\n\t// CryptFS is a filename encrypting storage.\n\tCryptFS\n)\n\nfunc (s StorageBackend) String() string {\n\tif be, err := StorageRegistry.BackendName(s); err == nil {\n\t\treturn be\n\t}\n\n\treturn \"\"\n}\n\n// Storage is an storage backend.\ntype Storage interface {\n\tfmt.Stringer\n\trcs\n\tGet(ctx context.Context, name string) ([]byte, error)\n\tSet(ctx context.Context, name string, value []byte) error\n\tDelete(ctx context.Context, name string) error\n\tExists(ctx context.Context, name string) bool\n\tMove(ctx context.Context, from, to string, del bool) error\n\tList(ctx context.Context, prefix string) ([]string, error)\n\tIsDir(ctx context.Context, name string) bool\n\tPrune(ctx context.Context, prefix string) error\n\tLink(ctx context.Context, from, to string) error\n\n\tName() string\n\tPath() string\n\tVersion(context.Context) semver.Version\n\tFsck(context.Context) error\n}\n\n// DetectStorage tries to detect the storage backend being used.\nfunc DetectStorage(ctx context.Context, path string) (Storage, error) {\n\t// The call to HasStorageBackend is important since GetStorageBackend will always return FS\n\t// if nothing is found in the context.\n\tif be, err := StorageRegistry.Get(GetStorageBackend(ctx)); HasStorageBackend(ctx) && err == nil {\n\t\tdebug.V(1).Log(\"Trying requested storage backend %q for %q\", be, path)\n\t\tst, err := be.New(ctx, path)\n\t\tif err == nil {\n\t\t\tdebug.Log(\"Successfully loaded requested storage backend %q for %q\", be, path)\n\n\t\t\treturn st, nil\n\t\t}\n\t\tdebug.Log(\"Failed to use requested storage backend %q for %s: %q\", be, path, err)\n\n\t\t// fallback to FS\n\t\tbe, err := StorageRegistry.Get(FS)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdebug.Log(\"Using fallback %q for %q\", be, path)\n\n\t\treturn be.Init(ctx, path)\n\t}\n\n\t// Nothing requested in the context. Try to detect the backend.\n\tfor _, be := range StorageRegistry.Prioritized() {\n\t\tdebug.V(1).Log(\"Trying storage backend %q for %q\", be, path)\n\t\tif err := be.Handles(ctx, path); err != nil {\n\t\t\tdebug.Log(\"failed to use %s for %s: %s\", be, path, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"Detected storage backend %q for %q\", be, path)\n\n\t\treturn be.New(ctx, path)\n\t}\n\n\t// fallback to FS\n\tbe, err := StorageRegistry.Get(FS)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdebug.Log(\"Using default fallback %q for %q\", be, path)\n\n\treturn be.Init(ctx, path)\n}\n\n// NewStorage initializes an existing storage backend.\nfunc NewStorage(ctx context.Context, id StorageBackend, path string) (Storage, error) {\n\tif be, err := StorageRegistry.Get(id); err == nil {\n\t\tdebug.Log(\"Using storage backend %q for %q\", be, path)\n\n\t\treturn be.New(ctx, path)\n\t}\n\n\treturn nil, fmt.Errorf(\"unknown backend %q: %w\", path, ErrNotFound)\n}\n\n// InitStorage initilizes a new storage location.\nfunc InitStorage(ctx context.Context, id StorageBackend, path string) (Storage, error) {\n\tif be, err := StorageRegistry.Get(id); err == nil {\n\t\tdebug.Log(\"Using %s for %s\", be, path)\n\n\t\treturn be.Init(ctx, path)\n\t}\n\n\treturn nil, fmt.Errorf(\"unknown backend %q: %w\", path, ErrNotFound)\n}\n"
  },
  {
    "path": "internal/backend/storage_test.go",
    "content": "package backend\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDetectStorage(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ttd := t.TempDir()\n\n\t// all tests involving age should set GOPASS_HOMEDIR\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {\n\t\tdebug.Log(\"static test password callback\")\n\n\t\treturn []byte(\"gopass\"), nil\n\t})\n\n\tfsDir := filepath.Join(td, \"fs\")\n\trequire.NoError(t, os.MkdirAll(fsDir, 0o700))\n\n\tt.Run(\"detect fs\", func(t *testing.T) {\n\t\tr, err := DetectStorage(ctx, fsDir)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, r)\n\t\tassert.Equal(t, \"fs\", r.Name())\n\t})\n}\n"
  },
  {
    "path": "internal/cache/disk.go",
    "content": "// Package cache proivdes a simple on disk cache for gopass.\n// It stores the data in a user specific directory.\npackage cache\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// OnDisk is a simple on disk cache.\ntype OnDisk struct {\n\tttl  time.Duration\n\tname string\n\tdir  string\n}\n\n// NewOnDisk creates a new on disk cache.\nfunc NewOnDisk(name string, ttl time.Duration) (*OnDisk, error) {\n\td := filepath.Join(appdir.UserCache(), \"gopass\", name)\n\n\treturn NewOnDiskWithDir(name, d, ttl)\n}\n\n// NewOnDiskWithDir creates a new on disk cache.\nfunc NewOnDiskWithDir(name, dir string, ttl time.Duration) (*OnDisk, error) {\n\tdebug.V(1).Log(\"New on disk cache %s created at %s\", name, dir)\n\n\to := &OnDisk{\n\t\tttl:  ttl,\n\t\tname: name,\n\t\tdir:  dir,\n\t}\n\n\treturn o, o.ensureDir()\n}\n\nfunc (o *OnDisk) ensureDir() error {\n\tif err := os.MkdirAll(o.dir, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create ondisk cache dir %s: %w\", o.dir, err)\n\t}\n\n\treturn nil\n}\n\n// String return the identity of this cache instance.\nfunc (o *OnDisk) String() string {\n\treturn fmt.Sprintf(\"OnDiskCache(name: %s, ttl: %d, dir: %s)\", o.name, o.ttl, o.dir)\n}\n\n// Get fetches an entry from the cache.\nfunc (o *OnDisk) Get(key string) ([]string, error) {\n\tkey = fsutil.CleanFilename(key)\n\tfn := filepath.Join(o.dir, key)\n\tfi, err := os.Stat(fn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to stat %s: %w\", fn, err)\n\t}\n\n\tif time.Now().After(fi.ModTime().Add(o.ttl)) {\n\t\treturn nil, fmt.Errorf(\"expired\")\n\t}\n\n\tbuf, err := os.ReadFile(fn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read file %s: %w\", fn, err)\n\t}\n\n\treturn strings.Split(string(buf), \"\\n\"), nil\n}\n\n// Set adds an entry to the cache.\nfunc (o *OnDisk) Set(key string, value []string) error {\n\t// we need to make sure not to log things here as plugin Identities' recipients\n\t// can contain secret data\n\tif err := o.ensureDir(); err != nil {\n\t\treturn err\n\t}\n\tkey = fsutil.CleanFilename(key)\n\tfn := filepath.Join(o.dir, key)\n\tif err := os.WriteFile(fn, []byte(strings.Join(value, \"\\n\")), 0o644); err != nil {\n\t\treturn fmt.Errorf(\"failed to write %s to %s: %w\", key, fn, err)\n\t}\n\n\treturn nil\n}\n\n// ModTime returns the modification time of the cache entry.\nfunc (o *OnDisk) ModTime(key string) time.Time {\n\tkey = fsutil.CleanFilename(key)\n\tfn := filepath.Join(o.dir, key)\n\tfi, err := os.Stat(fn)\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\n\treturn fi.ModTime()\n}\n\n// Remove removes an entry from the cache.\nfunc (o *OnDisk) Remove(key string) error {\n\tif err := o.ensureDir(); err != nil {\n\t\treturn err\n\t}\n\tkey = fsutil.CleanFilename(key)\n\tfn := filepath.Join(o.dir, key)\n\n\treturn os.Remove(fn)\n}\n\n// Purge removes all entries from the cache.\nfunc (o *OnDisk) Purge() error {\n\treturn os.RemoveAll(o.dir)\n}\n"
  },
  {
    "path": "internal/cache/disk_test.go",
    "content": "package cache\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOnDisk(t *testing.T) {\n\tt.Parallel()\n\n\ttd := t.TempDir()\n\n\todc, err := NewOnDiskWithDir(\"test\", td, time.Hour)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, odc.Set(\"foo\", []string{\"bar\"}))\n\tres, err := odc.Get(\"foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"bar\"}, res)\n\n\trequire.Error(t, odc.Remove(\"bar\"))\n\trequire.NoError(t, odc.Remove(\"foo\"))\n\trequire.NoError(t, odc.Purge())\n}\n\nfunc TestOnDiskExpiry(t *testing.T) {\n\tt.Parallel()\n\n\ttd := t.TempDir()\n\n\todc, err := NewOnDiskWithDir(\"test\", td, time.Second)\n\trequire.NoError(t, err)\n\trequire.NoError(t, odc.Set(\"foo\", []string{\"bar\"}))\n\tres, err := odc.Get(\"foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"bar\"}, res)\n\n\ttime.Sleep(time.Second + 100*time.Millisecond)\n\tres, err = odc.Get(\"foo\")\n\trequire.Error(t, err)\n\tassert.NotEqual(t, []string{\"bar\"}, res)\n}\n"
  },
  {
    "path": "internal/cache/ghssh/cache.go",
    "content": "package ghssh\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/cache\"\n)\n\n// Cache is a disk-backed GitHub SSH public key cache.\ntype Cache struct {\n\tdisk    *cache.OnDisk\n\tTimeout time.Duration\n}\n\n// New creates a new github cache.\nfunc New() (*Cache, error) {\n\tcDir, err := cache.NewOnDisk(\"github-ssh\", 6*time.Hour)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Cache{\n\t\tdisk:    cDir,\n\t\tTimeout: 30 * time.Second,\n\t}, nil\n}\n\nfunc (c *Cache) String() string {\n\treturn fmt.Sprintf(\"Github SSH key cache (OnDisk: %s)\", c.disk.String())\n}\n"
  },
  {
    "path": "internal/cache/ghssh/cache_test.go",
    "content": "package ghssh\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\t// Mock GOPASS_HOMEDIR to point to a temp directory\n\ttempDir := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", tempDir)\n\n\tc, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, c)\n\tassert.Equal(t, 30*time.Second, c.Timeout)\n\tassert.NotNil(t, c.disk)\n}\n\nfunc TestCache_String(t *testing.T) {\n\t// Mock GOPASS_HOMEDIR to point to a temp directory\n\ttempDir := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", tempDir)\n\n\tc, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, c)\n\n\tassert.Contains(t, c.String(), \"Github SSH key cache (OnDisk:\")\n\tassert.Contains(t, c.String(), tempDir)\n}\n"
  },
  {
    "path": "internal/cache/ghssh/github.go",
    "content": "// Package ghssh provides a cache for github ssh keys.\n// It fetches the keys from github and caches them on disk.\npackage ghssh\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar httpClient = &http.Client{\n\tTransport: &http.Transport{\n\t\tTLSClientConfig: &tls.Config{\n\t\t\t// enforce TLS 1.3\n\t\t\tMinVersion: tls.VersionTLS13,\n\t\t},\n\t},\n}\n\nvar baseURL = \"https://github.com\"\n\n// ListKeys returns the public keys for a github user. It will\n// cache results up to a configurable amount of time (default: 6h).\nfunc (c *Cache) ListKeys(ctx context.Context, user string) ([]string, error) {\n\tpk, err := c.disk.Get(user)\n\tif err != nil {\n\t\tdebug.Log(\"failed to fetch %s from cache: %s\", user, err)\n\t}\n\n\tif len(pk) > 0 {\n\t\treturn pk, nil\n\t}\n\n\tkeys, err := c.fetchKeys(ctx, user)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(keys) < 1 {\n\t\treturn nil, fmt.Errorf(\"key not found\")\n\t}\n\n\t_ = c.disk.Set(user, keys)\n\n\treturn keys, nil\n}\n\n// fetchKeys returns the public keys for a github user.\nfunc (c *Cache) fetchKeys(ctx context.Context, user string) ([]string, error) {\n\tctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n\tdefer cancel()\n\n\turl := fmt.Sprintf(\"%s/%s.keys\", baseURL, user)\n\tdebug.Log(\"fetching public keys for %s from github: %s\", user, url)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to fetch keys from %s: %s\", url, resp.Status)\n\t}\n\n\tout := make([]string, 0, 5)\n\tscanner := bufio.NewScanner(resp.Body)\n\n\tfor scanner.Scan() {\n\t\tout = append(out, strings.TrimSpace(scanner.Text()))\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "internal/cache/ghssh/github_test.go",
    "content": "package ghssh\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestListKeys(t *testing.T) {\n\t// Set GOPASS_HOMEDIR to a temp directory\n\ttempDir := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", tempDir)\n\n\t// Mock HTTP server\n\tmockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/validuser.keys\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(\"ssh-rsa AAAAB3Nza... validuser@github\\n\")) //nolint:errcheck\n\t\t} else {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t}\n\t}))\n\tdefer mockServer.Close()\n\n\toURL := baseURL\n\tbaseURL = mockServer.URL\n\tdefer func() {\n\t\tbaseURL = oURL\n\t}()\n\n\tcache, err := New()\n\trequire.NoError(t, err)\n\n\tt.Run(\"valid user\", func(t *testing.T) {\n\t\tkeys, err := cache.ListKeys(t.Context(), \"validuser\")\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, keys, 1)\n\t\tassert.Equal(t, \"ssh-rsa AAAAB3Nza... validuser@github\", keys[0])\n\t})\n\n\tt.Run(\"invalid user\", func(t *testing.T) {\n\t\tkeys, err := cache.ListKeys(t.Context(), \"invaliduser\")\n\t\trequire.Error(t, err)\n\t\tassert.Nil(t, keys)\n\t})\n}\n\nfunc TestFetchKeys(t *testing.T) {\n\t// Set GOPASS_HOMEDIR to a temp directory\n\ttempDir := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", tempDir)\n\n\t// Mock HTTP server\n\tmockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/validuser.keys\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(\"ssh-rsa AAAAB3Nza... validuser@github\\n\")) //nolint:errcheck\n\t\t} else {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t}\n\t}))\n\tdefer mockServer.Close()\n\n\toURL := baseURL\n\tbaseURL = mockServer.URL\n\tdefer func() {\n\t\tbaseURL = oURL\n\t}()\n\n\tcache, err := New()\n\trequire.NoError(t, err)\n\n\tt.Run(\"valid user\", func(t *testing.T) {\n\t\tkeys, err := cache.fetchKeys(t.Context(), \"validuser\")\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, keys, 1)\n\t\tassert.Equal(t, \"ssh-rsa AAAAB3Nza... validuser@github\", keys[0])\n\t})\n\n\tt.Run(\"invalid user\", func(t *testing.T) {\n\t\tkeys, err := cache.fetchKeys(t.Context(), \"invaliduser\")\n\t\trequire.Error(t, err)\n\t\tassert.Nil(t, keys)\n\t})\n}\n"
  },
  {
    "path": "internal/cache/inmem.go",
    "content": "package cache\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\ntype cacheEntry[V any] struct {\n\tvalue     V\n\tmaxExpire time.Time\n\texpire    time.Time\n\tcreated   time.Time\n\tnow       func() time.Time\n}\n\nfunc (ce *cacheEntry[V]) isExpired() bool {\n\tif ce.now().After(ce.maxExpire) {\n\t\treturn true\n\t}\n\n\tif ce.now().After(ce.expire) {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// InMemTTL implements a simple TTLed cache in memory. It is concurrency safe.\ntype InMemTTL[K comparable, V any] struct {\n\tsync.Mutex\n\tnow     func() time.Time\n\tttl     time.Duration\n\tmaxTTL  time.Duration\n\tentries map[K]cacheEntry[V]\n}\n\n// NewInMemTTL creates a new TTLed cache.\nfunc NewInMemTTL[K comparable, V any](ttl time.Duration, maxTTL time.Duration) *InMemTTL[K, V] {\n\treturn &InMemTTL[K, V]{\n\t\tnow:    time.Now,\n\t\tttl:    ttl,\n\t\tmaxTTL: maxTTL,\n\t}\n}\n\n// Get retrieves a single entry, extending it's TTL.\nfunc (c *InMemTTL[K, V]) Get(key K) (V, bool) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tvar zero V\n\tif c.entries == nil {\n\t\treturn zero, false\n\t}\n\n\tce, found := c.entries[key]\n\tif !found {\n\t\t// not found\n\t\treturn zero, false\n\t}\n\tif ce.isExpired() {\n\t\t// expired\n\t\treturn zero, false\n\t}\n\n\tce.expire = c.now().Add(c.ttl)\n\tc.entries[key] = ce\n\n\treturn ce.value, true\n}\n\n// purgeExpire will remove expired entries. It is called by Set.\nfunc (c *InMemTTL[K, V]) purgeExpired() {\n\tfor k, ce := range c.entries {\n\t\tif ce.isExpired() {\n\t\t\tdelete(c.entries, k)\n\t\t}\n\t}\n}\n\n// Set creates or overwrites an entry.\nfunc (c *InMemTTL[K, V]) Set(key K, value V) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif c.entries == nil {\n\t\tc.entries = make(map[K]cacheEntry[V], 10)\n\t}\n\n\tnow := c.now()\n\tc.entries[key] = cacheEntry[V]{\n\t\tvalue:     value,\n\t\tmaxExpire: now.Add(c.maxTTL),\n\t\texpire:    now.Add(c.ttl),\n\t\tcreated:   now,\n\t\tnow: func() time.Time {\n\t\t\treturn c.now()\n\t\t},\n\t}\n\n\tc.purgeExpired()\n}\n\n// Remove removes a single entry from the cache.\nfunc (c *InMemTTL[K, V]) Remove(key K) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tdelete(c.entries, key)\n}\n\n// Purge removes all entries from the cache.\nfunc (c *InMemTTL[K, V]) Purge() {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tc.entries = make(map[K]cacheEntry[V], 10)\n}\n"
  },
  {
    "path": "internal/cache/inmem_test.go",
    "content": "package cache\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc nowFunc(ns int) func() time.Time {\n\treturn func() time.Time {\n\t\treturn time.Date(2000, 1, 1, 1, 1, 1, ns, time.UTC)\n\t}\n}\n\nfunc TestTTL(t *testing.T) {\n\tt.Parallel()\n\n\tc := &InMemTTL[string, string]{\n\t\tttl:    4,\n\t\tmaxTTL: 5,\n\t}\n\n\tc.now = nowFunc(0)\n\n\tval, found := c.Get(\"foo\")\n\tassert.Empty(t, val)\n\tassert.False(t, found)\n\n\tc.Set(\"foo\", \"bar\")\n\tval, found = c.Get(\"foo\")\n\tassert.Equal(t, \"bar\", val)\n\tassert.True(t, found)\n\n\tc.now = nowFunc(4)\n\n\tval, found = c.Get(\"foo\")\n\tassert.Equal(t, \"bar\", val)\n\tassert.True(t, found)\n\n\tc.now = nowFunc(6)\n\n\tval, found = c.Get(\"foo\")\n\tassert.Empty(t, val)\n\tassert.False(t, found)\n\n\tc.Set(\"bar\", \"baz\")\n\tval, found = c.Get(\"bar\")\n\tassert.Equal(t, \"baz\", val)\n\tassert.True(t, found)\n\n\tc.Remove(\"bar\")\n\tval, found = c.Get(\"bar\")\n\tassert.Empty(t, val)\n\tassert.False(t, found)\n\n\tc.Set(\"foo\", \"bar\")\n\tc.Set(\"bar\", \"baz\")\n\tval, found = c.Get(\"bar\")\n\tassert.Equal(t, \"baz\", val)\n\tassert.True(t, found)\n\n\tc.Purge()\n\tval, found = c.Get(\"bar\")\n\tassert.Empty(t, val)\n\tassert.False(t, found)\n}\n\nfunc TestPar(t *testing.T) {\n\tt.Parallel()\n\n\tc := NewInMemTTL[int, int](time.Minute, time.Minute)\n\tc.now = nowFunc(0)\n\n\tfor i := range 32 {\n\t\tfor range 32 {\n\t\t\tt.Run(\"set\"+strconv.Itoa(i), func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tc.Set(i, i)\n\t\t\t\tiv, found := c.Get(i)\n\t\t\t\tassert.True(t, found)\n\t\t\t\tassert.Equal(t, i, iv)\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/completion/fish/completion.go",
    "content": "package fish\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"strings\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// ErrUnknownType is returned when an unknown type is encountered.\nvar ErrUnknownType = fmt.Errorf(\"unknown type\")\n\nfunc longName(name string) string {\n\t// \"If s does not contain sep and sep is not empty, Split returns a slice of length 1 whose only element is s.\"\n\t// from https://golang.org/pkg/strings/#Split.\n\treturn strings.TrimSpace(strings.Split(name, \",\")[0])\n}\n\nfunc shortName(name string) string {\n\tparts := strings.Split(name, \",\")\n\tif len(parts) < 2 {\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSpace(parts[1])\n}\n\nfunc escapePasswordName(name string) string {\n\t// Escape special characters for fish completions.\n\t// In fish, we use single quotes in the complete command, so we need to handle single quotes.\n\t// We also escape backslashes to be safe.\n\tname = strings.ReplaceAll(name, \"\\\\\", \"\\\\\\\\\")\n\t// Single quotes need to be escaped by ending the single-quoted string, adding an escaped single quote, and starting a new single-quoted string\n\t// E.g. 'hello'world' becomes 'hello'\\''world'\n\tname = strings.ReplaceAll(name, \"'\", \"'\\\\''\")\n\n\treturn name\n}\n\nfunc formatFlag(name, usage, typ string) string {\n\tswitch typ {\n\tcase \"short\":\n\t\treturn shortName(name)\n\tcase \"long\":\n\t\treturn longName(name)\n\tcase \"usage\":\n\t\treturn usage\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc formatFlagFunc(typ string) func(cli.Flag) (string, error) {\n\treturn func(f cli.Flag) (string, error) {\n\t\tswitch ft := f.(type) {\n\t\tcase *cli.BoolFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.Float64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.GenericFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.Int64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.Int64SliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.IntFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.IntSliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.StringFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.StringSliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.Uint64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tcase *cli.UintFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage, typ), nil\n\t\tdefault:\n\t\t\treturn \"\", fmt.Errorf(\"error '%T': %w\", f, ErrUnknownType)\n\t\t}\n\t}\n}\n\n// GetCompletion returns a fish completion script.\nfunc GetCompletion(a *cli.App) (string, error) {\n\ttplFuncs := template.FuncMap{\n\t\t\"formatShortFlag\":    formatFlagFunc(\"short\"),\n\t\t\"formatLongFlag\":     formatFlagFunc(\"long\"),\n\t\t\"formatFlagUsage\":    formatFlagFunc(\"usage\"),\n\t\t\"escapePasswordName\": escapePasswordName,\n\t}\n\n\ttpl, err := template.New(\"fish\").Funcs(tplFuncs).Parse(fishTemplate)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse template: %w\", err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tif err := tpl.Execute(buf, a); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to execute template: %w\", err)\n\t}\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/completion/fish/completion_escaping_test.go",
    "content": "package fish\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEscapePasswordName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"single quote\",\n\t\t\tinput:    \"password'with'quotes\",\n\t\t\texpected: \"password'\\\\''with'\\\\''quotes\",\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash\",\n\t\t\tinput:    \"path\\\\to\\\\password\",\n\t\t\texpected: \"path\\\\\\\\to\\\\\\\\password\",\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash and quote\",\n\t\t\tinput:    \"test\\\\with'quote\",\n\t\t\texpected: \"test\\\\\\\\with'\\\\''quote\",\n\t\t},\n\t\t{\n\t\t\tname:     \"colon\",\n\t\t\tinput:    \"cloudflare/api-tokens/DNS:Edit/nasvic.top\",\n\t\t\texpected: \"cloudflare/api-tokens/DNS:Edit/nasvic.top\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no special chars\",\n\t\t\tinput:    \"simple/password\",\n\t\t\texpected: \"simple/password\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := escapePasswordName(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/completion/fish/completion_test.go",
    "content": "package fish\n\nimport (\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype unknownFlag struct{}\n\nfunc (u *unknownFlag) String() string {\n\treturn \"\"\n}\n\nfunc (u *unknownFlag) Apply(*flag.FlagSet) error {\n\treturn nil\n}\n\nfunc (u *unknownFlag) GetName() string {\n\treturn \"\"\n}\n\nfunc (u *unknownFlag) IsSet() bool {\n\treturn false\n}\n\nfunc (u *unknownFlag) Names() []string {\n\treturn nil\n}\n\nfunc TestFormatFlag(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tName  string\n\t\tUsage string\n\t\tTyp   string\n\t\tOut   string\n\t}{\n\t\t{\"print, p\", \"Print\", \"short\", \"p\"},\n\t\t{\"print, p\", \"Print\", \"long\", \"print\"},\n\t\t{\"print, p\", \"Print\", \"usage\", \"Print\"},\n\t\t{\"print\", \"Print\", \"short\", \"\"},\n\t\t{\"\", \"Print\", \"long\", \"\"},\n\t\t{\"print, p\", \"Print\", \"foo\", \"\"},\n\t} {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.Out, formatFlag(tc.Name, tc.Usage, tc.Typ))\n\t\t\tt.Parallel()\n\t\t})\n\t}\n}\n\nfunc TestGetCompletion(t *testing.T) {\n\tt.Parallel()\n\n\tapp := cli.NewApp()\n\tsv, err := GetCompletion(app)\n\trequire.NoError(t, err)\n\tassert.Contains(t, sv, \"#!/usr/bin/env fish\")\n\n\tfishTemplate = \"{{.unexported}}\"\n\tsv, err = GetCompletion(app)\n\trequire.Error(t, err)\n\tassert.Contains(t, sv, \"\")\n\n\tfishTemplate = \"{{}}\"\n\tsv, err = GetCompletion(app)\n\trequire.Error(t, err)\n\tassert.Contains(t, sv, \"\")\n}\n\nfunc TestFormatflagFunc(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, flag := range []cli.Flag{\n\t\t&cli.BoolFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Float64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.GenericFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Int64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Int64SliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.IntFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.IntSliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.StringFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.StringSliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Uint64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.UintFlag{Name: \"foo\", Usage: \"bar\"},\n\t} {\n\t\tsv, err := formatFlagFunc(\"short\")(flag)\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, sv)\n\n\t\tsv, err = formatFlagFunc(\"long\")(flag)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"foo\", sv)\n\n\t\tsv, err = formatFlagFunc(\"usage\")(flag)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"bar\", sv)\n\t}\n\n\tsv, err := formatFlagFunc(\"short\")(&unknownFlag{})\n\trequire.Error(t, err)\n\tassert.Empty(t, sv)\n\n\tsv, err = formatFlagFunc(\"long\")(&unknownFlag{})\n\trequire.Error(t, err)\n\tassert.Empty(t, sv)\n\n\tsv, err = formatFlagFunc(\"usage\")(&unknownFlag{})\n\trequire.Error(t, err)\n\tassert.Empty(t, sv)\n}\n"
  },
  {
    "path": "internal/completion/fish/template.go",
    "content": "// Package fish implements a fish completion template for gopass.\npackage fish\n\n// see https://fishshell.com/docs/current/commands.html#complete\nvar fishTemplate = `#!/usr/bin/env fish\n{{ $prog := .Name -}}\nset PROG '{{ $prog }}'\n\nfunction __fish_{{ $prog }}_needs_command\n  set -l cmd (commandline -opc)\n  if [ (count $cmd) -eq 1 ] && [ $cmd[1] = $PROG ]\n    return 0\n  end\n  return 1\nend\n\nfunction __fish_{{ $prog }}_uses_command\n  set cmd (commandline -opc)\n  if [ (count $cmd) -gt 1 ]\n    if [ $argv[1] = $cmd[2] ]\n      return 0\n    end\n  end\n  return 1\nend\n\nfunction __fish_{{ $prog }}_print_gpg_keys\n  gpg2 --list-keys | grep uid | sed 's/.*<\\(.*\\)>/\\1/'\nend\n\nfunction __fish_{{ $prog }}_print_entries\n  {{ $prog }} ls --flat | sed \"s/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/'/\\\\\\\\'/g\"\nend\n\nfunction __fish_{{ $prog }}_print_dir\n  for i in ({{ $prog }} ls --flat | sed \"s/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/'/\\\\\\\\'/g\")\n\t  echo (dirname $i)\n\tend | sort -u\nend\n\n# erase any existing completions for {{ $prog }}\ncomplete -c $PROG -e\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_needs_command' -a \"(__fish_{{ $prog }}_print_entries)\"\ncomplete -c $PROG -f -s c -l clip -r -a \"(__fish_{{ $prog }}_print_entries)\"\n{{- $gflags := .Flags -}}\n{{ range .Commands }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_needs_command' -a {{ .Name }} -d 'Command: {{ .Usage }}'\n{{- $cmd := .Name -}}\n{{- if or (eq $cmd \"copy\") (eq $cmd \"cp\") (eq $cmd \"move\") (eq $cmd \"mv\") (eq $cmd \"delete\") (eq $cmd \"remove\") (eq $cmd \"rm\") (eq $cmd \"show\") (eq $cmd \"set\") (eq $cmd \"edit\") (eq $cmd \"otp\") }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_uses_command {{ $cmd }}' -a \"(__fish_{{ $prog }}_print_entries)\"{{ end -}}\n{{- if or (eq $cmd \"insert\") (eq $cmd \"generate\") (eq $cmd \"list\") (eq $cmd \"ls\") }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_uses_command {{ $cmd }}' -a \"(__fish_{{ $prog }}_print_dir)\"{{ end -}}\n{{- range .Subcommands }}\n{{- $subcmd := .Name }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_uses_command {{ $cmd }}' -a {{ $subcmd }} -d 'Subcommand: {{ .Usage }}'\n{{- range .Flags }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_uses_command {{ $cmd }} {{ $subcmd }} {{ if ne (. | formatShortFlag) \"\" }}-s {{ . | formatShortFlag }} {{ end }}-l {{ . | formatLongFlag }} -d \"{{ . | formatFlagUsage }}\"'\n{{- end }}\n{{- range $gflags }}\ncomplete -c $PROG -f -n '__fish_{{ $prog }}_uses_command {{ $cmd }} {{ $subcmd }} {{ if ne (. | formatShortFlag) \"\" }}-s {{ . | formatShortFlag }} {{ end }}-l {{ . | formatLongFlag }} -d \"{{ . | formatFlagUsage }}\"'\n{{- end }}\n{{- end }}\n{{- end }}`\n"
  },
  {
    "path": "internal/completion/zsh/completion.go",
    "content": "// Package zsh implements a zsh completion script generator.\npackage zsh\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// ErrUnknownType is returned when an unknown type is encountered.\nvar ErrUnknownType = fmt.Errorf(\"unknown type\")\n\nfunc longName(name string) string {\n\t// \"If s does not contain sep and sep is not empty, Split returns a slice of length 1 whose only element is s.\"\n\t// from https://golang.org/pkg/strings/#Split\n\treturn strings.TrimSpace(strings.Split(name, \",\")[0])\n}\n\nfunc escapePasswordName(name string) string {\n\t// Escape special characters for zsh _values command.\n\t// Must escape backslash first to avoid double-escaping.\n\t// Then escape colon (used as value:description separator) and brackets (glob chars)\n\tname = strings.ReplaceAll(name, \"\\\\\", \"\\\\\\\\\")\n\tname = strings.ReplaceAll(name, \":\", \"\\\\:\")\n\tname = strings.ReplaceAll(name, \"[\", \"\\\\[\")\n\tname = strings.ReplaceAll(name, \"]\", \"\\\\]\")\n\n\treturn name\n}\n\nfunc formatFlag(name, usage string) string {\n\t// Suare brackets must be escaped in zsh completions\n\tusage = strings.ReplaceAll(usage, \"[\", \"\\\\[\")\n\tusage = strings.ReplaceAll(usage, \"]\", \"\\\\]\")\n\n\treturn fmt.Sprintf(\"--%s[%s]\", longName(name), usage)\n}\n\nfunc formatFlagFunc() func(cli.Flag) (string, error) {\n\treturn func(f cli.Flag) (string, error) {\n\t\tswitch ft := f.(type) {\n\t\tcase *cli.BoolFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.Float64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.GenericFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.Int64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.Int64SliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.IntFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.IntSliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.StringFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.StringSliceFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.Uint64Flag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tcase *cli.UintFlag:\n\t\t\treturn formatFlag(ft.Name, ft.Usage), nil\n\t\tdefault:\n\t\t\treturn \"\", fmt.Errorf(\"error '%T': %w\", f, ErrUnknownType)\n\t\t}\n\t}\n}\n\n// GetCompletion returns a zsh completion script.\nfunc GetCompletion(a *cli.App) (string, error) {\n\ttplFuncs := template.FuncMap{\n\t\t\"formatFlag\":         formatFlagFunc(),\n\t\t\"escapePasswordName\": escapePasswordName,\n\t}\n\n\ttpl, err := template.New(\"zsh\").Funcs(tplFuncs).Parse(zshTemplate)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tif err := tpl.Execute(buf, a); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/completion/zsh/completion_escaping_test.go",
    "content": "package zsh\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEscapePasswordName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"colon\",\n\t\t\tinput:    \"cloudflare/api-tokens/DNS:Edit/nasvic.top\",\n\t\t\texpected: \"cloudflare/api-tokens/DNS\\\\:Edit/nasvic.top\",\n\t\t},\n\t\t{\n\t\t\tname:     \"brackets\",\n\t\t\tinput:    \"passwords/hostname-00[1-2].mgmt\",\n\t\t\texpected: \"passwords/hostname-00\\\\[1-2\\\\].mgmt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple special chars\",\n\t\t\tinput:    \"test:with[brackets]and:colons\",\n\t\t\texpected: \"test\\\\:with\\\\[brackets\\\\]and\\\\:colons\",\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash\",\n\t\t\tinput:    \"test\\\\path:with[special]chars\",\n\t\t\texpected: \"test\\\\\\\\path\\\\:with\\\\[special\\\\]chars\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no special chars\",\n\t\t\tinput:    \"simple/password\",\n\t\t\texpected: \"simple/password\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := escapePasswordName(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/completion/zsh/completion_test.go",
    "content": "package zsh\n\nimport (\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype unknownFlag struct{}\n\nfunc (u *unknownFlag) String() string {\n\treturn \"\"\n}\n\nfunc (u *unknownFlag) Apply(*flag.FlagSet) error {\n\treturn nil\n}\n\nfunc (u *unknownFlag) GetName() string {\n\treturn \"\"\n}\n\nfunc (u *unknownFlag) IsSet() bool {\n\treturn true\n}\n\nfunc (u *unknownFlag) Names() []string {\n\treturn []string{}\n}\n\nfunc TestFormatFlag(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname  string\n\t\tusage string\n\t\tout   string\n\t}{\n\t\t{\"print, p\", \"Print\", \"--print[Print]\"},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.out, formatFlag(tc.name, tc.usage))\n\t\t})\n\t}\n}\n\nfunc TestGetCompletion(t *testing.T) {\n\tt.Parallel()\n\n\tapp := cli.NewApp()\n\tsv, err := GetCompletion(app)\n\trequire.NoError(t, err)\n\tassert.Contains(t, sv, \"#compdef zsh.test\")\n\n\tzshTemplate = \"{{.unexported}}\"\n\tsv, err = GetCompletion(app)\n\trequire.Error(t, err)\n\tassert.Contains(t, sv, \"\")\n\n\tzshTemplate = \"{{}}\"\n\tsv, err = GetCompletion(app)\n\trequire.Error(t, err)\n\tassert.Contains(t, sv, \"\")\n}\n\nfunc TestFormatflagFunc(t *testing.T) {\n\tt.Parallel()\n\n\tff := formatFlagFunc()\n\tfor _, flag := range []cli.Flag{\n\t\t&cli.BoolFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Float64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.GenericFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Int64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Int64SliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.IntFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.IntSliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.StringFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.StringSliceFlag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.Uint64Flag{Name: \"foo\", Usage: \"bar\"},\n\t\t&cli.UintFlag{Name: \"foo\", Usage: \"bar\"},\n\t} {\n\t\tsv, err := ff(flag)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"--foo[bar]\", sv)\n\t}\n\n\tsv, err := ff(&unknownFlag{})\n\trequire.Error(t, err)\n\tassert.Empty(t, sv)\n}\n"
  },
  {
    "path": "internal/completion/zsh/template.go",
    "content": "package zsh\n\n// see http://zsh.sourceforge.net/Doc/Release/Completion-System.html\nvar zshTemplate = `{{ $prog := .Name }}#compdef {{ $prog }}\n\n_{{ $prog }} () {\n    local cmd\n    if (( CURRENT > 2)); then\n\tcmd=${words[2]}\n\tcurcontext=\"${curcontext%:*:*}:{{ $prog }}-$cmd\"\n\t(( CURRENT-- ))\n\tshift words\n\tcase \"${cmd}\" in\n{{- range .Commands }}\n\t  {{ .Name }}{{ range .Aliases }}|{{ . }}{{ end }})\n\t      {{- if .Subcommands }}\n\t      local -a subcommands\n\t      subcommands=({{ range .Subcommands }}\n\t      \"{{ .Name }}:{{ .Usage }}\"{{ end }}\n\t      )\n\t      _describe -t commands \"{{ $prog }} {{ .Name }}\" subcommands\n\t      {{- end }}\n\t      {{ if .Flags }}_arguments :{{ range .Flags }} \"{{ . | formatFlag }}\"{{ end }}{{ end }}\n\t      {{ if or (eq .Name \"insert\") (eq .Name \"generate\")  (eq .Name \"list\") }}_{{ $prog }}_complete_folders{{ end }}\n\t      {{ if or (eq .Name \"copy\") (eq .Name \"move\") (eq .Name \"delete\") (eq .Name \"show\") (eq .Name \"edit\") (eq .Name \"insert\") (eq .Name \"generate\") (eq .Name \"otp\") }}_{{ $prog }}_complete_passwords{{ end }}\n\t      ;;\n{{- end }}\n\t  *)\n\t      _{{ $prog }}_complete_passwords\n\t      ;;\n\tesac\n    else\n\tlocal -a subcommands\n\tsubcommands=({{ range .Commands }}\n\t  \"{{ .Name }}:{{ .Usage }}\"{{ end }}\n\t)\n\t_describe -t command '{{ $prog }}' subcommands\n\t_arguments : {{ range .Flags }}\"{{ . | formatFlag }}\" {{ end }}\n\t_{{ $prog }}_complete_passwords\n    fi\n}\n\n_{{ $prog }}_complete_keys () {\n    local IFS=$'\\n'\n    _values 'gpg keys' $(gpg2 --list-secret-keys --with-colons 2> /dev/null | cut -d : -f 10 | sort -u | sed '/^$/d')\n}\n\n_{{ $prog }}_complete_passwords () {\n    local IFS=$'\\n'\n    _arguments : \\\n\t\"--clip[Copy the first line of the secret into the clipboard]\"\n    _values 'passwords' $({{ $prog }} ls --flat | sed 's/\\\\/\\\\\\\\\\\\\\\\/g; s/:/\\\\\\\\:/g; s/\\[/\\\\\\\\[/g; s/\\]/\\\\\\\\]/g')\n}\n\n_{{ $prog }}_complete_folders () {\n    local -a folders\n    folders=(\"${(@f)$({{ $prog }} ls --folders --flat)}\")\n    _describe -t folders \"folders\" folders -qS /\n}\n\ncompdef _{{ $prog }} {{ $prog }}`\n"
  },
  {
    "path": "internal/config/config.go",
    "content": "// Package config provides a way to manage the configuration of gopass.\n// It handles the loading and saving of configuration files,\n// as well as the management of environment variables.\npackage config\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/gopasspw/gitconfig\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\tDefaultPasswordLength = 24\n\tDefaultXKCDLength     = 4\n)\n\nvar (\n\tenvPrefix    = \"GOPASS_CONFIG\"\n\tsystemConfig = \"/etc/gopass/config\"\n)\n\ntype Level int\n\nconst (\n\tNone Level = iota\n\tEnv\n\tWorktree\n\tLocal\n\tGlobal\n\tSystem\n\tPreset\n)\n\nfunc newGitconfig() *gitconfig.Configs {\n\tc := gitconfig.New()\n\tc.Name = \"gopass\"\n\tc.EnvPrefix = envPrefix\n\tc.GlobalConfig = os.Getenv(\"GOPASS_CONFIG\")\n\tc.SystemConfig = systemConfig\n\n\treturn c\n}\n\nvar defaults = map[string]string{\n\t\"age.agent-enabled\":      \"false\",\n\t\"age.agent-timeout\":      \"0\",\n\t\"core.autopush\":          \"true\",\n\t\"core.autosync\":          \"true\",\n\t\"core.cliptimeout\":       \"45\",\n\t\"core.exportkeys\":        \"true\",\n\t\"core.notifications\":     \"true\",\n\t\"core.follow-references\": \"false\",\n\t\"pwgen.xkcd-lang\":        \"en\",\n}\n\n// Config is a gopass config handler.\ntype Config struct {\n\troot *gitconfig.Configs\n\tcfgs map[string]*gitconfig.Configs\n}\n\n// migrationOpts is a list of config options that were used by gopass\n// and need to be migrated to a new name, it maps old name -> new name\n// the keys are used in our documentation test to spot legacy options\n// that are still used in our codebase.\nvar migrationOpts = map[string]string{\n\t// migration done in v1.15.9\n\t\"core.showsafecontent\": \"show.safecontent\",\n\t\"core.autoclip\":        \"generate.autoclip\",\n\t\"core.showautoclip\":    \"show.autoclip\",\n}\n\n// New initializes a new gopass config. It will handle legacy configs as well and legacy option names, migrating\n// them to their new location and names on a best effort basis. Any system level config or env variables options are\n// not migrated.\nfunc New() *Config {\n\tc := newWithOptions(false)\n\t// we only migrate options when we are allowed to write them\n\tc.migrateOptions(migrationOpts)\n\n\treturn c\n}\n\n// NewInMemory initializes a new config that does not allow writes. For use in tests.\n// This does not migrate legacy option names to their correct config section.\nfunc NewInMemory() *Config {\n\treturn newWithOptions(true)\n}\n\n// NewContextInMemory returns a context with a read-only config.\nfunc NewContextInMemory() context.Context {\n\treturn NewInMemory().WithConfig(context.Background())\n}\n\nfunc newWithOptions(noWrites bool) *Config {\n\tc := &Config{\n\t\tcfgs: make(map[string]*gitconfig.Configs, 42),\n\t}\n\n\t// if there is no per-user gitconfig we try to migrate\n\t// an existing config. But we will leave it around for\n\t// gopass fsck to (optionally) clean it up.\n\tif nm := os.Getenv(\"GOPASS_CONFIG_NO_MIGRATE\"); !HasGlobalConfig() && nm == \"\" {\n\t\tif err := migrateConfigs(); err != nil {\n\t\t\tdebug.Log(\"failed to migrate from old config: %s\", err)\n\t\t}\n\t}\n\n\t// load the global config to get the root path\n\tc.root = newGitconfig().LoadAll(\"\")\n\tc.root.NoWrites = noWrites\n\n\trootPath := c.root.Get(\"mounts.path\")\n\tif rootPath == \"\" {\n\t\tif err := c.SetPath(PwStoreDir(\"\")); err != nil {\n\t\t\tdebug.Log(\"failed to set path: %s\", err)\n\t\t}\n\t}\n\t// load again, this might add a per-store config from the root store\n\tc.root.LoadAll(rootPath)\n\tc.root.NoWrites = noWrites\n\n\tif rootPath := c.root.Get(\"mounts.path\"); rootPath == \"\" {\n\t\tif err := c.SetPath(PwStoreDir(\"\")); err != nil {\n\t\t\tdebug.Log(\"failed to set path: %s\", err)\n\t\t}\n\t}\n\n\t// set global defaults\n\tc.root.Preset = gitconfig.NewFromMap(defaults)\n\n\tfor _, m := range c.Mounts() {\n\t\tc.cfgs[m] = newGitconfig().LoadAll(c.MountPath(m))\n\t\tc.cfgs[m].NoWrites = noWrites\n\t}\n\n\treturn c\n}\n\n// HasGlobalConfig returns true if there is an existing global config.\nfunc HasGlobalConfig() bool {\n\treturn newGitconfig().HasGlobalConfig()\n}\n\n// IsSet returns true if the key is set in the root config.\nfunc (c *Config) IsSet(key string) bool {\n\treturn c.root.IsSet(key)\n}\n\n// IsSetM returns true if the key is set in the mount or the root config if mount is empty.\nfunc (c *Config) IsSetM(mount, key string) bool {\n\tif mount == \"\" || mount == \"<root>\" {\n\t\treturn c.root.IsSet(key)\n\t}\n\n\tif cfg := c.cfgs[mount]; cfg != nil {\n\t\treturn cfg.IsSet(key)\n\t}\n\n\treturn false\n}\n\n// Get returns the given key from the root config.\nfunc (c *Config) Get(key string) string {\n\treturn c.root.Get(key)\n}\n\n// GetAll returns all values for the given key.\nfunc (c *Config) GetAll(key string) []string {\n\treturn c.root.GetAll(key)\n}\n\n// GetGlobal returns the given key from the root global config.\n// This is typically used to prevent a local config override of sensitive config items, e.g. used for integrity checks.\nfunc (c *Config) GetGlobal(key string) string {\n\treturn c.root.GetGlobal(key)\n}\n\n// GetM returns the given key from the mount or the root config if mount is empty.\nfunc (c *Config) GetM(mount, key string) string {\n\t// env vars always win\n\tif sv, found := c.root.GetFrom(key, \"env\"); found && sv != \"\" {\n\t\treturn sv\n\t}\n\n\tif mount == \"\" || mount == \"<root>\" {\n\t\treturn c.root.Get(key)\n\t}\n\n\tif cfg := c.cfgs[mount]; cfg != nil {\n\t\treturn cfg.Get(key)\n\t}\n\n\treturn \"\"\n}\n\n// Set tries to set the key to the given value.\n// The mount option is necessary to discern between\n// the per-user (global) and possible per-directory (local)\n// config files.\n//\n//   - If mount is empty the setting will be written to the per-user config (global)\n//   - If mount has the special value \"<root>\" the setting will be written to the per-directory config of the root store (local)\n//   - If mount has any other value we will attempt to write the setting to the per-directory config of this mount.\n//   - If the mount point does not exist we will return nil.\nfunc (c *Config) Set(mount, key, value string) error {\n\t_, err := c.SetWithLevel(mount, key, value)\n\n\treturn err\n}\n\n// SetWithLevel is the same as Set, but it also returns the level at which the config was set.\n// It currently only supports global and local configs.\nfunc (c *Config) SetWithLevel(mount, key, value string) (Level, error) {\n\tif mount == \"\" {\n\t\treturn Global, c.root.SetGlobal(key, value)\n\t}\n\n\tif mount == \"<root>\" {\n\t\treturn Local, c.root.SetLocal(key, value)\n\t}\n\n\tif cfg, ok := c.cfgs[mount]; !ok {\n\t\treturn None, fmt.Errorf(\"substore %q is not initialized or doesn't exist\", mount)\n\t} else if cfg != nil {\n\t\treturn Local, cfg.SetLocal(key, value)\n\t}\n\n\treturn None, nil\n}\n\n// SetEnv overrides a key in the non-persistent layer.\nfunc (c *Config) SetEnv(key, value string) error {\n\treturn c.root.SetEnv(key, value)\n}\n\n// Path returns the root store path.\nfunc (c *Config) Path() string {\n\treturn c.Get(\"mounts.path\")\n}\n\n// MountPath returns the mount store path.\nfunc (c *Config) MountPath(mountPoint string) string {\n\treturn c.Get(mpk(mountPoint))\n}\n\n// SetPath is a shortcut to set the root store path.\nfunc (c *Config) SetPath(path string) error {\n\treturn c.Set(\"\", \"mounts.path\", path)\n}\n\n// SetMountPath is a shortcut to set a mount to a path.\nfunc (c *Config) SetMountPath(mount, path string) error {\n\treturn c.Set(\"\", mpk(mount), path)\n}\n\n// mpk for mountPathKey.\nfunc mpk(mount string) string {\n\treturn fmt.Sprintf(\"mounts.%s.path\", mount)\n}\n\n// Mounts returns all mount points from the root config.\n// Note: Any mounts in local configs are ignored.\nfunc (c *Config) Mounts() []string {\n\treturn c.root.ListSubsections(\"mounts\")\n}\n\n// Unset deletes the key from the given config.\nfunc (c *Config) Unset(mount, key string) error {\n\tif mount == \"\" {\n\t\treturn c.root.UnsetGlobal(key)\n\t}\n\n\tif mount == \"<root>\" {\n\t\treturn c.root.UnsetLocal(key)\n\t}\n\n\tif cfg := c.cfgs[mount]; cfg != nil {\n\t\treturn cfg.UnsetLocal(key)\n\t}\n\n\treturn nil\n}\n\n// Keys returns all keys in the given config.\nfunc (c *Config) Keys(mount string) []string {\n\tif mount == \"\" || mount == \"<root>\" {\n\t\treturn c.root.Keys()\n\t}\n\n\tif cfg := c.cfgs[mount]; cfg != nil {\n\t\treturn cfg.Keys()\n\t}\n\n\treturn nil\n}\n\n// migrateOptions is a best effort migration tool for when we introduce new options. It does not necessarily\n// handle worktree and env level options very well.\nfunc (c *Config) migrateOptions(migrations map[string]string) {\n\tif nm := os.Getenv(\"GOPASS_CONFIG_NO_MIGRATE\"); nm != \"\" {\n\t\treturn\n\t}\n\tvar errs []error\n\tdebug.V(2).Log(\"migrateOptions running\")\n\tfor oldK, newK := range migrations {\n\t\tfound := false\n\t\tif val := c.root.GetGlobal(oldK); val != \"\" {\n\t\t\tdebug.V(2).Log(\"migrating option in root global store: %s -> %s \", oldK, newK)\n\t\t\terrs = append(errs, c.root.SetGlobal(newK, val))\n\t\t\terrs = append(errs, c.root.UnsetGlobal(oldK))\n\t\t\tfound = true\n\t\t}\n\t\tif val := c.root.GetLocal(oldK); val != \"\" {\n\t\t\tdebug.V(2).Log(\"migrating option in <root> local store: %s -> %s \", oldK, newK)\n\t\t\terrs = append(errs, c.root.SetLocal(newK, val))\n\t\t\terrs = append(errs, c.root.UnsetLocal(oldK))\n\t\t\tfound = true\n\t\t}\n\t\tfor _, m := range c.Mounts() {\n\t\t\tif cfg := c.cfgs[m]; cfg != nil {\n\t\t\t\tif val := cfg.GetLocal(oldK); val != \"\" {\n\t\t\t\t\tdebug.V(2).Log(\"migrating option in local store %s: %s -> %s \", m, oldK, newK)\n\t\t\t\t\terrs = append(errs, cfg.SetLocal(newK, val))\n\t\t\t\t\terrs = append(errs, cfg.UnsetLocal(oldK))\n\t\t\t\t\tfound = true\n\t\t\t\t}\n\t\t\t\tif val := cfg.Get(oldK); !found && val != \"\" {\n\t\t\t\t\tdebug.V(2).Log(\"Found old option %s = %s in config, probably at the worktree or env level, \"+\n\t\t\t\t\t\t\"or maybe at the system level cannot migrate it.\", oldK, val)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif err := errors.Join(errs...); err != nil {\n\t\tdebug.Log(\"Errors encountered while migrating old options: {%v}\", err)\n\t}\n}\n\n// DefaultPasswordLengthFromEnv will determine the password length from the env variable\n// GOPASS_PW_DEFAULT_LENGTH or fallback to the hard-coded default length.\n// If the env variable is set by the user and is valid, the boolean return value\n// will be true, otherwise it will be false.\nfunc DefaultPasswordLengthFromEnv(ctx context.Context) (int, bool) {\n\tdef := DefaultPasswordLength\n\tcfg, mp := FromContext(ctx)\n\n\tif l := AsInt(cfg.GetM(mp, \"generate.length\")); l > 0 {\n\t\tdef = l\n\t}\n\n\tlengthStr, isSet := os.LookupEnv(\"GOPASS_PW_DEFAULT_LENGTH\")\n\tif !isSet {\n\t\treturn def, false\n\t}\n\tlength, err := strconv.Atoi(lengthStr)\n\tif err != nil || length < 1 {\n\t\treturn def, false\n\t}\n\n\treturn length, true\n}\n"
  },
  {
    "path": "internal/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t// this will write to the tempdir\n\tcfg := New()\n\n\tassert.False(t, cfg.IsSet(\"core.string\"))\n\trequire.NoError(t, cfg.Set(\"\", \"core.string\", \"foo\"))\n\trequire.NoError(t, cfg.Set(\"\", \"core.bool\", \"true\"))\n\trequire.NoError(t, cfg.Set(\"\", \"core.int\", \"42\"))\n\n\tassert.Equal(t, \"foo\", cfg.Get(\"core.string\"))\n\tassert.True(t, AsBool(cfg.Get(\"core.bool\")))\n\tassert.Equal(t, 42, AsInt(cfg.Get(\"core.int\")))\n\n\trequire.NoError(t, cfg.SetEnv(\"env.string\", \"foo\"))\n\tassert.Equal(t, \"foo\", cfg.Get(\"env.string\"))\n\n\t// test default values\n\tassert.Equal(t, []string{\n\t\t\"age.agent-enabled\",\n\t\t\"age.agent-timeout\",\n\t\t\"core.autopush\",\n\t\t\"core.autosync\",\n\t\t\"core.bool\",\n\t\t\"core.cliptimeout\",\n\t\t\"core.exportkeys\",\n\t\t\"core.follow-references\",\n\t\t\"core.int\",\n\t\t\"core.notifications\",\n\t\t\"core.string\",\n\t\t\"env.string\",\n\t\t\"mounts.path\",\n\t\t\"pwgen.xkcd-lang\",\n\t}, cfg.Keys(\"\"))\n\tfor key, expected := range defaults {\n\t\tassert.Equal(t, expected, cfg.Get(key))\n\t}\n\trequire.NoError(t, cfg.Set(\"\", \"pwgen.xkcd-lang\", \"de\"))\n\tassert.Equal(t, \"de\", cfg.Get(\"pwgen.xkcd-lang\"))\n\n\tctx := cfg.WithConfig(t.Context())\n\tassert.True(t, Bool(ctx, \"core.bool\"))\n\tassert.Equal(t, \"foo\", String(ctx, \"core.string\"))\n\tassert.Equal(t, 42, Int(ctx, \"core.int\"))\n\n\trequire.NoError(t, cfg.SetEnv(\"generate.length\", \"16\"))\n\tactualLength, _ := DefaultPasswordLengthFromEnv(ctx)\n\tassert.Equal(t, 16, actualLength)\n}\n\nfunc TestEnvConfig(t *testing.T) {\n\tenvs := map[string]string{\n\t\t\"GOPASS_CONFIG_COUNT\":   \"2\",\n\t\t\"GOPASS_CONFIG_KEY_0\":   \"core.autosync\",\n\t\t\"GOPASS_CONFIG_VALUE_0\": \"false\",\n\t\t\"GOPASS_CONFIG_KEY_1\":   \"show.safecontent\",\n\t\t\"GOPASS_CONFIG_VALUE_1\": \"true\",\n\t}\n\tfor k, v := range envs {\n\t\tt.Setenv(k, v)\n\t}\n\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t// this will write to the tempdir\n\tcfg := New()\n\n\tassert.Equal(t, \"false\", cfg.Get(\"core.autosync\"))\n\tassert.Equal(t, \"true\", cfg.Get(\"show.safecontent\"))\n}\n\nfunc TestInvalidEnvConfig(t *testing.T) {\n\tenvs := map[string]string{\n\t\t// notice the double _ in the middle, this is a regression test\n\t\t\"GOPASS_CONFIG__CONFIG_COUNT\":   \"1\",\n\t\t\"GOPASS_CONFIG__CONFIG_KEY_0\":   \"core.autosync\",\n\t\t\"GOPASS_CONFIG__CONFIG_VALUE_0\": \"false\",\n\t\t// old format\n\t\t\"GOPASS_CONFIG_CONFIG_COUNT\":   \"1\",\n\t\t\"GOPASS_CONFIG_CONFIG_KEY_0\":   \"core.autosync\",\n\t\t\"GOPASS_CONFIG_CONFIG_VALUE_0\": \"false\",\n\t}\n\tfor k, v := range envs {\n\t\tt.Setenv(k, v)\n\t}\n\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t// this will write to the tempdir\n\tcfg := New()\n\n\tassert.Equal(t, \"true\", cfg.Get(\"core.autosync\"))\n}\n\nfunc TestOptsMigration(t *testing.T) {\n\tt.Run(\"migrate global options\", func(t *testing.T) {\n\t\t// we use our own temp dir\n\t\ttd := t.TempDir()\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t\tcfg := New()\n\t\t// default config should not have been populated, we didn't call NewUnitTester\n\t\tassert.False(t, cfg.IsSet(\"generate.autoclip\"))\n\t\tassert.False(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"core.safecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\t\t// this will write to the tempdir\n\t\trequire.NoError(t, cfg.Set(\"\", \"core.showsafecontent\", \"true\"))\n\t\tassert.True(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.Equal(t, \"true\", cfg.root.GetGlobal(\"core.showsafecontent\"))\n\t\tassert.Empty(t, cfg.root.GetLocal(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"core.safecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\n\t\tt.Setenv(\"GOPASS_CONFIG_NO_MIGRATE\", \"\")\n\t\t// we test the migration path\n\t\t// this will read from the tempdir and should migrate the above \"old option\" to its new expected value\n\t\t// but only at the global level, not the local one since it was a global option\n\t\tcfg2 := New()\n\t\tassert.False(t, cfg2.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg2.IsSet(\"core.safecontent\"))\n\t\tassert.Empty(t, cfg2.root.GetGlobal(\"core.showsafecontent\"))\n\t\tassert.Equal(t, \"true\", cfg2.root.GetGlobal(\"show.safecontent\"))\n\t\tassert.Empty(t, cfg2.root.GetLocal(\"show.safecontent\"))\n\t})\n\n\tt.Run(\"migrated config matches test config\", func(t *testing.T) {\n\t\t// we use our own temp dir\n\t\ttd := t.TempDir()\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\t\tu := gptest.NewUnitTester(t)\n\t\tassert.NotNil(t, u)\n\t\tcfg := New()\n\t\tassert.True(t, cfg.IsSet(\"core.autoimport\"))\n\t\tassert.True(t, cfg.IsSet(\"core.cliptimeout\"))\n\t\tassert.True(t, cfg.IsSet(\"core.notifications\"))\n\t\tassert.True(t, cfg.IsSet(\"core.nopager\"))\n\t\tassert.False(t, cfg.IsSet(\"core.autoclip\"))\n\t\tassert.True(t, cfg.IsSet(\"generate.autoclip\"))\n\t\tassert.False(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"core.safecontent\"))\n\n\t\tt.Setenv(\"GOPASS_CONFIG_NO_MIGRATE\", \"\")\n\t\t// we test the migration path\n\t\tcfg = New()\n\t\tassert.True(t, cfg.IsSet(\"core.autoimport\"))\n\t\tassert.True(t, cfg.IsSet(\"core.cliptimeout\"))\n\t\tassert.True(t, cfg.IsSet(\"core.notifications\"))\n\t\tassert.True(t, cfg.IsSet(\"core.nopager\"))\n\t\tassert.False(t, cfg.IsSet(\"core.autoclip\"))\n\t\tassert.True(t, cfg.IsSet(\"generate.autoclip\"))\n\t\tassert.False(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"core.safecontent\"))\n\t})\n\n\tt.Run(\"migrate local options\", func(t *testing.T) {\n\t\t// we use our own temp dir\n\t\ttd := t.TempDir()\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t\tu := gptest.NewUnitTester(t)\n\t\tassert.NotNil(t, u)\n\n\t\tcfg := New()\n\t\t// this will write to the local config because of the <root> arg\n\t\trequire.NoError(t, cfg.Set(\"<root>\", \"core.showsafecontent\", \"true\"))\n\t\tassert.Equal(t, \"true\", cfg.root.GetLocal(\"core.showsafecontent\"))\n\t\tassert.Empty(t, cfg.root.GetGlobal(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\n\t\tt.Setenv(\"GOPASS_CONFIG_NO_MIGRATE\", \"\")\n\t\t// we test the migration path\n\t\tcfg = New()\n\t\tassert.False(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.Equal(t, \"true\", cfg.root.GetLocal(\"show.safecontent\"))\n\t\tassert.Empty(t, cfg.root.GetGlobal(\"show.safecontent\"))\n\t})\n\n\tt.Run(\"env variable are not migrated\", func(t *testing.T) {\n\t\t// we use our own temp dir\n\t\ttd := t.TempDir()\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t\tenvs := map[string]string{\n\t\t\t\"GOPASS_CONFIG_COUNT\":      \"1\",\n\t\t\t\"GOPASS_CONFIG_KEY_0\":      \"core.showsafecontent\",\n\t\t\t\"GOPASS_CONFIG_VALUE_0\":    \"true\",\n\t\t\t\"GOPASS_CONFIG_NO_MIGRATE\": \"\",\n\t\t}\n\t\tfor k, v := range envs {\n\t\t\tt.Setenv(k, v)\n\t\t}\n\n\t\tu := gptest.NewUnitTester(t)\n\t\tassert.NotNil(t, u)\n\n\t\tcfg := New()\n\t\tassert.True(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\t})\n\n\tt.Run(\"migrate submount options\", func(t *testing.T) {\n\t\t// we use our own temp dir\n\t\ttd := t.TempDir()\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t\tu := gptest.NewUnitTester(t)\n\t\tassert.NotNil(t, u)\n\t\t// we create a submount store\n\t\trequire.NoError(t, u.InitStore(\"submount\"))\n\n\t\tcfg := New()\n\t\t// we add it as a mount path to our global config\n\t\trequire.NoError(t, cfg.SetMountPath(\"submount\", u.StoreDir(\"submount\")))\n\t\t// we reload the config so the submount config is created and loaded\n\t\tcfg = New()\n\n\t\t// this will write to the local mount config\n\t\trequire.NoError(t, cfg.Set(\"submount\", \"core.showsafecontent\", \"true\"))\n\t\tassert.Equal(t, \"true\", cfg.GetM(\"submount\", \"core.showsafecontent\"))\n\t\tassert.Empty(t, cfg.Get(\"core.showsafecontent\"))\n\t\tassert.Empty(t, cfg.GetM(\"submount\", \"show.safecontent\"))\n\n\t\tt.Setenv(\"GOPASS_CONFIG_NO_MIGRATE\", \"\")\n\t\t// we test the migration path\n\t\tcfg = New()\n\t\tassert.False(t, cfg.IsSet(\"core.showsafecontent\"))\n\t\tassert.False(t, cfg.IsSet(\"show.safecontent\"))\n\t\tassert.Empty(t, cfg.GetM(\"submount\", \"core.showsafecontent\"))\n\t\tassert.Equal(t, \"true\", cfg.GetM(\"submount\", \"show.safecontent\"))\n\t})\n}\n"
  },
  {
    "path": "internal/config/config_windows.go",
    "content": "package config\n\nimport \"github.com/gopasspw/gitconfig\"\n\nfunc init() {\n\t// Disable unescaping of values. This is not strictly conformant with the\n\t// git config spec, but it avoid interpreting backslashes in paths as linebreaks\n\t// or tabs.\n\tgitconfig.CompatMode = true\n}\n"
  },
  {
    "path": "internal/config/context.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gitconfig\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\ntype contextKey int\n\nconst (\n\tctxKeyConfig contextKey = iota\n\tctxKeyMountPoint\n)\n\nfunc (c *Config) WithConfig(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, ctxKeyConfig, c)\n}\n\nfunc WithMount(ctx context.Context, mp string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyMountPoint, mp)\n}\n\n// FromContext returns a config from a context, as well as the current mount point (store name) if found.\nfunc FromContext(ctx context.Context) (*Config, string) {\n\tmount := \"\"\n\tif m, found := ctx.Value(ctxKeyMountPoint).(string); found && m != \"\" {\n\t\tmount = m\n\t}\n\n\tif c, found := ctx.Value(ctxKeyConfig).(*Config); found && c != nil {\n\t\treturn c, mount\n\t}\n\n\tdebug.Log(\"no config in context, loading anew\")\n\n\tcfg := &Config{\n\t\troot: newGitconfig().LoadAll(\"\"),\n\t}\n\tcfg.root.Preset = gitconfig.NewFromMap(defaults)\n\n\treturn cfg, mount\n}\n"
  },
  {
    "path": "internal/config/docs_test.go",
    "content": "package config\n\nimport (\n\t\"bufio\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// ignoredEnvs is a list of environment variables that are used by gopass\n// but originate from elsewhere. They should be well known and properly\n// documented already.\nvar ignoredEnvs = set.Map([]string{\n\t// keep-sorted start\n\t\"APPDATA\",\n\t\"GIT_AUTHOR_EMAIL\",\n\t\"GIT_AUTHOR_NAME\",\n\t\"GNUPGHOME\",\n\t\"GOPASS_CONFIG_NOSYSTEM\", // name assembled, tests can't catch it\n\t\"GOPASS_DEBUG_FILES\",     // indirect usage\n\t\"GOPASS_DEBUG_FUNCS\",     // indirect usage\n\t\"GOPASS_GPG_OPTS\",        // indirect usage\n\t\"GOPASS_UMASK\",           // indirect usage\n\t\"GOPATH\",\n\t\"GPG_TTY\",\n\t\"HOME\",\n\t\"LOCALAPPDATA\",\n\t\"PASSWORD_STORE_UMASK\", // indirect usage\n\t\"XDG_CACHE_HOME\",\n\t\"XDG_CONFIG_HOME\",\n\t\"XDG_DATA_HOME\",\n\t\"XDG_RUNTIME_DIR\",\n\t// keep-sorted end\n})\n\n// ignoredOptions is a list of config options that are used by gopass\n// but may not be covered easily by a regexp.\nvar ignoredOptions = set.Map([]string{\n\t// keep-sorted start\n\t\"core.post-hook\",\n\t\"core.pre-hook\",\n\t\"include.path\",\n\t\"recipients.hash\",\n\t\"user.email\",\n\t\"user.name\",\n\t// keep-sorted end\n})\n\nfunc TestConfigOptsInDocs(t *testing.T) {\n\tt.Parallel()\n\n\tdocumented := documentedOpts(t)\n\tused := usedOpts(t)\n\n\tt.Logf(\"Config options documented in doc: %+v\", documented)\n\tt.Logf(\"Config options used in the code: %+v\", used)\n\n\tfor _, k := range set.SortedKeys(documented) {\n\t\tif _, got := migrationOpts[k]; got {\n\t\t\tcontinue\n\t\t}\n\t\tif !used[k] {\n\t\t\tt.Errorf(\"Documented but not used: %s\", k)\n\t\t}\n\t}\n\tfor _, k := range set.SortedKeys(used) {\n\t\tif _, got := migrationOpts[k]; got {\n\t\t\tt.Errorf(\"Legacy option still used: %s\", k)\n\t\t}\n\t\tif !documented[k] {\n\t\t\tt.Errorf(\"Used but not documented: %s\", k)\n\t\t}\n\t}\n}\n\nfunc usedOpts(t *testing.T) map[string]bool {\n\tt.Helper()\n\n\toptRE := regexp.MustCompile(`(?:\\.Get(?:|Int|Bool|All|Global)\\(\\\"([a-z]+\\.[a-z-]+)\\\"\\)|\\.Get(?:|Int|Bool)M\\([^,]+, \\\"([a-z]+\\.[a-z-]+)\\\"\\)|config\\.(?:Bool|Int|String)\\((?:ctx|c\\.Context), \\\"([a-z]+\\.[a-z-]+)\\\"\\)|hook\\.Invoke(?:Root)?\\(ctx, \\\"([a-z]+\\.[a-z-]+)\\\")`)\n\topts := make(map[string]bool, 42)\n\n\tdir := filepath.Join(\"..\", \"..\")\n\tif err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif info.IsDir() && strings.HasPrefix(info.Name(), \".\") && path != dir {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasSuffix(info.Name(), \"_test.go\") {\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasSuffix(info.Name(), \".go\") {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn usedOptsInFile(t, path, opts, optRE)\n\t}); err != nil {\n\t\tt.Errorf(\"failed to walk %s: %s\", dir, err)\n\t}\n\n\treturn opts\n}\n\nfunc usedOptsInFile(t *testing.T, fn string, opts map[string]bool, re *regexp.Regexp) error {\n\tt.Helper()\n\n\tfh, err := os.Open(fn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\tscanner := bufio.NewScanner(fh)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif !re.MatchString(line) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfound := re.FindStringSubmatch(line)\n\t\t// t.Logf(\"found: %q\", found)\n\t\tif len(found) < 4 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor i := 1; i < 10; i++ {\n\t\t\tif found[i] == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif ignoredOptions[found[i]] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\topts[found[i]] = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc documentedOpts(t *testing.T) map[string]bool {\n\tt.Helper()\n\n\tfn := filepath.Join(\"..\", \"..\", \"docs\", \"config.md\")\n\tfh, err := os.Open(fn)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to open %s: %s\", fn, err)\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\toptRE := regexp.MustCompile(`^\\| .([a-z]+\\.[a-z-]+).`)\n\n\topts := make(map[string]bool, 42)\n\tscanner := bufio.NewScanner(fh)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif !optRE.MatchString(line) {\n\t\t\tcontinue\n\t\t}\n\t\tfound := optRE.FindStringSubmatch(line)\n\t\tif len(found) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\tif _, got := ignoredOptions[found[1]]; got {\n\t\t\tcontinue\n\t\t}\n\t\topts[found[1]] = true\n\t}\n\n\treturn opts\n}\n\nfunc TestEnvVarsInDocs(t *testing.T) {\n\tt.Parallel()\n\n\tdocumented := documentedEnvs(t)\n\tused := usedEnvs(t)\n\n\tt.Logf(\"env options documented in doc: %+v\", documented)\n\tt.Logf(\"env options used in the code: %+v\", used)\n\n\tfor _, k := range set.SortedKeys(documented) {\n\t\tif !used[k] {\n\t\t\tt.Errorf(\"Documented but not used: %s\", k)\n\t\t}\n\t}\n\tfor _, k := range set.SortedKeys(used) {\n\t\tif !documented[k] {\n\t\t\tt.Errorf(\"Used but not documented: %s\", k)\n\t\t}\n\t}\n}\n\nfunc usedEnvs(t *testing.T) map[string]bool {\n\tt.Helper()\n\n\toptRE := regexp.MustCompile(`os\\.(?:Getenv|LookupEnv)\\(\\\"([^\"]+)\\\"\\)`)\n\topts := make(map[string]bool, 42)\n\n\tdir := filepath.Join(\"..\", \"..\")\n\tif err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif info.IsDir() && strings.HasPrefix(info.Name(), \".\") && path != dir {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif info.IsDir() && (info.Name() == \"helpers\" || info.Name() == \"tests\") {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasSuffix(info.Name(), \"_test.go\") {\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasSuffix(info.Name(), \".go\") {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn usedEnvsInFile(t, path, opts, optRE)\n\t}); err != nil {\n\t\tt.Errorf(\"failed to walk %s: %s\", dir, err)\n\t}\n\n\treturn opts\n}\n\nfunc usedEnvsInFile(t *testing.T, fn string, opts map[string]bool, re *regexp.Regexp) error {\n\tt.Helper()\n\n\tfh, err := os.Open(fn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\tscanner := bufio.NewScanner(fh)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif !re.MatchString(line) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfound := re.FindStringSubmatch(line)\n\t\t// t.Logf(\"found: %q\", found)\n\t\tif len(found) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tv := found[1]\n\n\t\tif ignoredEnvs[v] {\n\t\t\tcontinue\n\t\t}\n\n\t\topts[v] = true\n\t}\n\n\treturn nil\n}\n\nfunc documentedEnvs(t *testing.T) map[string]bool {\n\tt.Helper()\n\n\tfn := filepath.Join(\"..\", \"..\", \"docs\", \"config.md\")\n\tfh, err := os.Open(fn)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to open %s: %s\", fn, err)\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\toptRE := regexp.MustCompile(`^\\| .([A-Z0-9_]+).`)\n\n\topts := make(map[string]bool, 42)\n\tscanner := bufio.NewScanner(fh)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif !optRE.MatchString(line) {\n\t\t\tcontinue\n\t\t}\n\t\tfound := optRE.FindStringSubmatch(line)\n\t\tif len(found) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tv := found[1]\n\n\t\tif ignoredEnvs[v] {\n\t\t\tcontinue\n\t\t}\n\n\t\topts[v] = true\n\t}\n\n\treturn opts\n}\n"
  },
  {
    "path": "internal/config/legacy/config.go",
    "content": "// Package legacy provides the legacy config struct for gopass.\n// It is used for backwards compatibility and should be removed in the future.\n// It is not intended for use in new code.\npackage legacy\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\t// ErrConfigNotFound is returned on load if the config was not found.\n\tErrConfigNotFound = fmt.Errorf(\"config not found\")\n\t// ErrConfigNotParsed is returned on load if the config could not be decoded.\n\tErrConfigNotParsed = fmt.Errorf(\"config not parseable\")\n)\n\n// Config is the current config struct.\ntype Config struct {\n\tAutoClip      bool              `yaml:\"autoclip\"`      // decide whether passwords are automatically copied or not.\n\tAutoImport    bool              `yaml:\"autoimport\"`    // import missing public keys w/o asking.\n\tClipTimeout   int               `yaml:\"cliptimeout\"`   // clear clipboard after seconds.\n\tExportKeys    bool              `yaml:\"exportkeys\"`    // automatically export public keys of all recipients.\n\tNoPager       bool              `yaml:\"nopager\"`       // do not invoke a pager to display long lists.\n\tNotifications bool              `yaml:\"notifications\"` // enable desktop notifications.\n\tParsing       bool              `yaml:\"parsing\"`       // allows to switch off all output parsing.\n\tPath          string            `yaml:\"path\"`\n\tSafeContent   bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tMounts        map[string]string `yaml:\"mounts\"`\n\tUseKeychain   bool              `yaml:\"keychain\"` // use OS keychain for age\n\n\tConfigPath string `yaml:\"-\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// New creates a new config with sane default values.\nfunc New() *Config {\n\treturn &Config{\n\t\tAutoImport:    false,\n\t\tClipTimeout:   45,\n\t\tExportKeys:    true,\n\t\tMounts:        make(map[string]string),\n\t\tNotifications: true,\n\t\tParsing:       true,\n\t\tPath:          PwStoreDir(\"\"),\n\t\tConfigPath:    configLocation(),\n\t}\n}\n\n// CheckOverflow implements configer. It will check for any extra config values not.\n// handled by the current struct.\nfunc (c *Config) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config will return a current config.\nfunc (c *Config) Config() *Config {\n\treturn c\n}\n\n// SetConfigValue will try to set the given key to the value in the config struct.\nfunc (c *Config) SetConfigValue(key, value string) error {\n\tif err := c.setConfigValue(key, value); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Save()\n}\n\n// setConfigValue will try to set the given key to the value in the config struct.\nfunc (c *Config) setConfigValue(key, value string) error {\n\tvalue = strings.ToLower(value)\n\to := reflect.ValueOf(c).Elem()\n\tfor i := range o.NumField() {\n\t\tjsonArg := o.Type().Field(i).Tag.Get(\"yaml\")\n\t\tif jsonArg == \"\" || jsonArg == \"-\" {\n\t\t\tcontinue\n\t\t}\n\t\tif jsonArg != key {\n\t\t\tcontinue\n\t\t}\n\t\tf := o.Field(i)\n\t\tswitch f.Kind() { //nolint:exhaustive\n\t\tcase reflect.String:\n\t\t\tf.SetString(value)\n\n\t\t\treturn nil\n\t\tcase reflect.Bool:\n\t\t\tswitch value {\n\t\t\tcase \"true\":\n\t\t\t\tfallthrough\n\t\t\tcase \"on\":\n\t\t\t\tf.SetBool(true)\n\n\t\t\t\treturn nil\n\t\t\tcase \"false\":\n\t\t\t\tfallthrough\n\t\t\tcase \"off\":\n\t\t\t\tf.SetBool(false)\n\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"not a bool: %s\", value)\n\t\t\t}\n\t\tcase reflect.Int:\n\t\t\tiv, err := strconv.Atoi(value)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to convert %q to integer: %w\", value, err)\n\t\t\t}\n\t\t\tf.SetInt(int64(iv))\n\n\t\t\treturn nil\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"unknown config option %q\", key)\n}\n\nfunc (c *Config) String() string {\n\treturn fmt.Sprintf(\"%#v\", c)\n}\n\n// Directory returns the directory this config is using.\nfunc (c *Config) Directory() string {\n\treturn filepath.Dir(c.Path)\n}\n\n// ConfigMap returns a map of stringified config values for easy printing.\nfunc (c *Config) ConfigMap() map[string]string {\n\tm := make(map[string]string, 20)\n\to := reflect.ValueOf(c).Elem()\n\tfor i := range o.NumField() {\n\t\tjsonArg := o.Type().Field(i).Tag.Get(\"yaml\")\n\t\tif jsonArg == \"\" || jsonArg == \"-\" {\n\t\t\tcontinue\n\t\t}\n\t\tf := o.Field(i)\n\t\tvar strVal string\n\t\tswitch f.Kind() { //nolint:exhaustive\n\t\tcase reflect.String:\n\t\t\tstrVal = f.String()\n\t\tcase reflect.Bool:\n\t\t\tstrVal = strconv.FormatBool(f.Bool())\n\t\tcase reflect.Int:\n\t\t\tstrVal = strconv.FormatInt(f.Int(), 10)\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tm[jsonArg] = strVal\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/config/legacy/config_test.go",
    "content": "package legacy_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config/legacy\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewConfig(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tt.Setenv(\"GOPASS_CONFIG\", filepath.Join(os.TempDir(), \".gopass.yml\"))\n\n\tcfg := legacy.New()\n\tcs := cfg.String()\n\tassert.Contains(t, cs, `&legacy.Config{AutoClip:false, AutoImport:false, ClipTimeout:45, ExportKeys:true, NoPager:false, Notifications:true,`)\n\tassert.Contains(t, cs, `SafeContent:false, Mounts:map[string]string{},`)\n\n\tcfg = &legacy.Config{\n\t\tMounts: map[string]string{\n\t\t\t\"foo\": \"\",\n\t\t\t\"bar\": \"\",\n\t\t},\n\t}\n\tcs = cfg.String()\n\tassert.Contains(t, cs, `&legacy.Config{AutoClip:false, AutoImport:false, ClipTimeout:0, ExportKeys:false, NoPager:false, Notifications:false,`)\n\tassert.Contains(t, cs, `SafeContent:false, Mounts:map[string]string{\"bar\":\"\", \"foo\":\"\"},`)\n}\n\nfunc TestSetConfigValue(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tt.Setenv(\"GOPASS_CONFIG\", filepath.Join(os.TempDir(), \".gopass.yml\"))\n\n\tcfg := legacy.New()\n\trequire.NoError(t, cfg.SetConfigValue(\"autoclip\", \"true\"))\n\trequire.NoError(t, cfg.SetConfigValue(\"cliptimeout\", \"900\"))\n\trequire.NoError(t, cfg.SetConfigValue(\"path\", \"/tmp\"))\n\trequire.Error(t, cfg.SetConfigValue(\"autoclip\", \"yo\"))\n}\n"
  },
  {
    "path": "internal/config/legacy/io.go",
    "content": "package legacy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// LoadWithFallbackRelaxed will try to load the config from one of the default.\n// locations but also accept a more recent config.\nfunc LoadWithFallbackRelaxed() *Config {\n\treturn LoadWithOptions(true, true)\n}\n\n// LoadWithFallback will try to load the config from one of the default locations.\nfunc LoadWithFallback() *Config {\n\treturn LoadWithOptions(false, true)\n}\n\n// LoadWithOptions gives more flexibility about how to load the config.\nfunc LoadWithOptions(relaxed, useDefault bool) *Config {\n\tfor _, l := range ConfigLocations() {\n\t\tif cfg := loadConfig(l, relaxed); cfg != nil {\n\t\t\treturn cfg\n\t\t}\n\t}\n\n\tif !useDefault {\n\t\treturn nil\n\t}\n\n\treturn loadDefault()\n}\n\n// Load will load the config from the default location or return a default config.\nfunc Load() *Config {\n\tif cfg := loadConfig(configLocation(), false); cfg != nil {\n\t\treturn cfg\n\t}\n\n\treturn loadDefault()\n}\n\nfunc loadConfig(l string, relaxed bool) *Config {\n\tdebug.Log(\"Trying to load config from %s\", l)\n\n\tcfg, err := load(l, relaxed)\n\tif errors.Is(err, ErrConfigNotFound) {\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"Loaded config from %s: %+v\", l, cfg)\n\n\treturn cfg\n}\n\nfunc loadDefault() *Config {\n\tcfg := New()\n\tcfg.Path = PwStoreDir(\"\")\n\tdebug.Log(\"Created new default config: %+v\", cfg)\n\n\treturn cfg\n}\n\nfunc load(cf string, relaxed bool) (*Config, error) {\n\t// deliberately using os.Stat here, a symlinked\n\t// config is OK.\n\tif _, err := os.Stat(cf); err != nil {\n\t\treturn nil, ErrConfigNotFound\n\t}\n\n\tbuf, err := os.ReadFile(cf)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error reading config from %s: %s\\n\", cf, err)\n\n\t\treturn nil, ErrConfigNotFound\n\t}\n\n\tcfg, err := decode(buf, relaxed)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error reading config from %s: %s\\n\", cf, err)\n\n\t\treturn nil, ErrConfigNotParsed\n\t}\n\n\tif cfg.Mounts == nil {\n\t\tcfg.Mounts = make(map[string]string)\n\t}\n\tcfg.ConfigPath = cf\n\n\treturn cfg, nil\n}\n\nfunc checkOverflow(m map[string]any) error {\n\tif len(m) < 1 {\n\t\treturn nil\n\t}\n\n\tkeys := set.SortedKeys(m)\n\n\treturn fmt.Errorf(\"unknown fields: %+v\", keys)\n}\n\ntype configer interface {\n\tConfig() *Config\n\tCheckOverflow() error\n}\n\nfunc decode(buf []byte, relaxed bool) (*Config, error) {\n\tmostRecent := &Config{\n\t\tAutoImport:    true,\n\t\tClipTimeout:   45,\n\t\tExportKeys:    true,\n\t\tNotifications: true,\n\t\tParsing:       true,\n\t\tPath:          PwStoreDir(\"\"),\n\t}\n\tcfgs := []configer{\n\t\t// most recent config must come first.\n\t\tmostRecent,\n\t\t&Pre1127{},\n\t\t&Pre1102{},\n\t\t&Pre193{\n\t\t\tRoot: &Pre193StoreConfig{},\n\t\t},\n\t\t&Pre182{\n\t\t\tRoot: &Pre182StoreConfig{},\n\t\t},\n\t\t&Pre140{},\n\t\t&Pre130{},\n\t}\n\n\tif relaxed {\n\t\t// most recent config must come last as well, will be tried w/o\n\t\t// overflow checks.\n\t\tcfgs = append(cfgs, mostRecent)\n\t}\n\n\tvar warn string\n\tfor i, cfg := range cfgs {\n\t\tdebug.Log(\"Trying to unmarshal config into %T\", cfg)\n\t\tif err := yaml.Unmarshal(buf, cfg); err != nil {\n\t\t\tdebug.Log(\"Loading config %T failed: %s\", cfg, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := cfg.CheckOverflow(); err != nil {\n\t\t\tdebug.Log(\"Extra elements in config: %s\", err)\n\t\t\tif i == 0 {\n\t\t\t\twarn = fmt.Sprintf(\"Failed to load config %T. Do you need to remove deprecated fields? %s\\n\", cfg, err)\n\t\t\t}\n\t\t\t// usually we are strict about extra fields, i.e. any field left\n\t\t\t// unparsed means this config failed and we try the next one.\n\t\t\tif i < len(cfgs)-1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// in relaxed mode we append an extra copy of the most recent\n\t\t\t// config to the end of the slice and might just ignore these\n\t\t\t// extra fields.\n\t\t\tif !relaxed {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdebug.Log(\"Ignoring extra config fields for fallback config (only)\")\n\t\t}\n\n\t\tdebug.Log(\"Loaded config: %T: %+v\", cfg, cfg)\n\t\tconf := cfg.Config()\n\t\tif i > 0 {\n\t\t\tdebug.Log(\"Loaded legacy config. Should rewrite config.\")\n\t\t}\n\n\t\treturn conf, nil\n\t}\n\t// We try to provide a seamless config upgrade path for users of our\n\t// released versions. But some users build gopass from the master branch\n\t// and these might run into issues when we remove config options.\n\t// Since our config parser is pedantic (it has to) we fail parsing on\n\t// unknown config options. If we remove one and the user rebuilds it's\n\t// gopass binary without changing the config, it will fail to parse the\n\t// current config and the legacy configs will likely fail as well.\n\t// But if we always display the warning users with configs from previous\n\t// releases will always see the warning. So instead we only display the\n\t// warning if parsing of the most up to date config struct fails AND\n\t// not other succeeds.\n\tif warn != \"\" {\n\t\tfmt.Fprint(os.Stderr, warn)\n\t}\n\n\treturn nil, ErrConfigNotParsed\n}\n\n// Save saves the config.\nfunc (c *Config) Save() error {\n\tbuf, err := yaml.Marshal(c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal YAML: %w\", err)\n\t}\n\n\tcfgLoc := configLocation()\n\tcfgDir := filepath.Dir(cfgLoc)\n\tif !fsutil.IsDir(cfgDir) {\n\t\tif err := os.MkdirAll(cfgDir, 0o700); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create dir %q: %w\", cfgDir, err)\n\t\t}\n\t}\n\n\tif err := os.WriteFile(cfgLoc, buf, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write config file to %q: %w\", cfgLoc, err)\n\t}\n\tdebug.Log(\"Saved config to %s: %+v\\n\", cfgLoc, c)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/config/legacy/io_test.go",
    "content": "package legacy\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigs(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tcfg  string\n\t\twant *Config\n\t}{\n\t\t{\n\t\t\tname: \"1.9.3\",\n\t\t\tcfg: `autoclip: true\nautoimport: false\ncliptimeout: 45\nexportkeys: true\nnopager: false\nnotifications: true\npath: /home/johndoe/.password-store\nsafecontent: false\nmounts:\n  foo/sub: /home/johndoe/.password-store-foo-sub\n  work: /home/johndoe/.password-store-work`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      true,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: true,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/johndoe/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"foo/sub\": \"/home/johndoe/.password-store-foo-sub\",\n\t\t\t\t\t\"work\":    \"/home/johndoe/.password-store-work\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"N+1\",\n\t\t\tcfg: `autoclip: true\nautoimport: false\ncliptimeout: 45\nexportkeys: true\nnopager: false\nfoo: bar\nnotifications: true\npath: /home/johndoe/.password-store\nsafecontent: false\nmounts:\n  foo/sub: /home/johndoe/.password-store-foo-sub\n  work: /home/johndoe/.password-store-work`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      true,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: true,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/johndoe/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"foo/sub\": \"/home/johndoe/.password-store-foo-sub\",\n\t\t\t\t\t\"work\":    \"/home/johndoe/.password-store-work\",\n\t\t\t\t},\n\t\t\t\tXXX: map[string]any{\"foo\": string(\"bar\")},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.8.2\",\n\t\t\tcfg: `root:\n  autoclip: true\n  autoimport: false\n  autosync: false\n  check_recipient_hash: false\n  cliptimeout: 45\n  concurrency: 50\n  editrecipients: true\n  exportkeys: true\n  confirm: false\n  nopager: false\n  notficiations: true\n  path: gpgcli-gitcli-fs+file:///home/johndoe/.password-store\n  safecontent: false\n  usesymbols: true\nmounts:\n  foo/sub:\n    autoclip: true\n    autoimport: false\n    autosync: false\n    check_recipient_hash: false\n    cliptimeout: 45\n    concurrency: 50\n    editrecipients: true\n    exportkeys: true\n    confirm: false\n    nopager: false\n    notficiations: true\n    path: gpgcli-gitcli-fs+file:///home/johndoe/.password-store-foo-sub\n    safecontent: false\n    usesymbols: true\n  work:\n    autoclip: true\n    autoimport: false\n    autosync: false\n    check_recipient_hash: false\n    cliptimeout: 45\n    concurrency: 50\n    editrecipients: true\n    exportkeys: true\n    confirm: false\n    nopager: false\n    notficiations: true\n    path: gpgcli-gitcli-fs+file:///home/johndoe/.password-store-work\n    safecontent: false\n    usesymbols: true\n`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      true,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/johndoe/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"foo/sub\": \"/home/johndoe/.password-store-foo-sub\",\n\t\t\t\t\t\"work\":    \"/home/johndoe/.password-store-work\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.4.0\",\n\t\t\tcfg: `root:\n  askformore: false\n  autoimport: false\n  autosync: false\n  cliptimeout: 45\n  noconfirm: false\n  nopager: false\n  path: /home/johndoe/.password-store\n  safecontent: false\nmounts:\n  foo/sub:\n    askformore: false\n    autoimport: false\n    autosync: false\n    cliptimeout: 45\n    noconfirm: false\n    nopager: false\n    path: /home/johndoe/.password-store-foo-sub\n    safecontent: false\n  work:\n    askformore: false\n    autoimport: false\n    autosync: false\n    cliptimeout: 45\n    noconfirm: false\n    nopager: false\n    path: /home/johndoe/.password-store-work\n    safecontent: false\nversion: 1.4.0`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      false,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/johndoe/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"foo/sub\": \"/home/johndoe/.password-store-foo-sub\",\n\t\t\t\t\t\"work\":    \"/home/johndoe/.password-store-work\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.3.0\",\n\t\t\tcfg: `askformore: false\nautoimport: true\nautosync: false\ncliptimeout: 45\nmounts:\n  dev: /Users/johndoe/.password-store-dev\n  ops: /Users/johndoe/.password-store-ops\n  personal: /Users/johndoe/secrets\n  teststore: /Users/johndoe/tmp/teststore\nnoconfirm: false\npath: /home/foo/.password-store\nsafecontent: true\nversion: \"1.3.0\"`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      false,\n\t\t\t\tAutoImport:    true,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/foo/.password-store\",\n\t\t\t\tSafeContent:   true,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"dev\":       \"/Users/johndoe/.password-store-dev\",\n\t\t\t\t\t\"ops\":       \"/Users/johndoe/.password-store-ops\",\n\t\t\t\t\t\"personal\":  \"/Users/johndoe/secrets\",\n\t\t\t\t\t\"teststore\": \"/Users/johndoe/tmp/teststore\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.2.0\",\n\t\t\tcfg: `alwaystrust: true\naskformore: false\nautoimport: true\nautopull: true\nautopush: true\ncliptimeout: 45\ndebug: false\nloadkeys: true\nmounts:\n  dev: /Users/johndoe/.password-store-dev\n  ops: /Users/johndoe/.password-store-ops\n  personal: /Users/johndoe/secrets\n  teststore: /Users/johndoe/tmp/teststore\nnocolor: false\nnoconfirm: false\npath: /home/foo/.password-store\npersistkeys: true\nsafecontent: true\nversion: \"1.2.0\"`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      false,\n\t\t\t\tAutoImport:    true,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/foo/.password-store\",\n\t\t\t\tSafeContent:   true,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"dev\":       \"/Users/johndoe/.password-store-dev\",\n\t\t\t\t\t\"ops\":       \"/Users/johndoe/.password-store-ops\",\n\t\t\t\t\t\"personal\":  \"/Users/johndoe/secrets\",\n\t\t\t\t\t\"teststore\": \"/Users/johndoe/tmp/teststore\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.1.0\",\n\t\t\tcfg: `alwaystrust: false\nautoimport: false\nautopull: true\nautopush: true\ncliptimeout: 45\nloadkeys: false\nmounts:\n  dev: /home/johndoe/.password-store-dev\n  ops: /home/johndoe/.password-store-ops\n  personal: /home/johndoe/secrets\n  teststore: /home/johndoe/tmp/teststore\nnocolor: false\nnoconfirm: false\npath: /home/johndoe/.password-store\npersistkeys: true\nsafecontent: false\nversion: 1.1.0`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      false,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/johndoe/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"dev\":       \"/home/johndoe/.password-store-dev\",\n\t\t\t\t\t\"ops\":       \"/home/johndoe/.password-store-ops\",\n\t\t\t\t\t\"personal\":  \"/home/johndoe/secrets\",\n\t\t\t\t\t\"teststore\": \"/home/johndoe/tmp/teststore\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tname: \"1.0.0\",\n\t\t\tcfg: `alwaystrust: false\nautoimport: false\nautopull: true\nautopush: false\ncliptimeout: 45\nloadkeys: false\nmounts:\n  dev: /Users/johndoe/.password-store-dev\n  ops: /Users/johndoe/.password-store-ops\n  personal: /Users/johndoe/secrets\n  teststore: /Users/johndoe/tmp/teststore\nnoconfirm: false\npath: /home/foo/.password-store\npersistkeys: false\nversion: \"1.0.0\"`,\n\t\t\twant: &Config{\n\t\t\t\tAutoClip:      false,\n\t\t\t\tAutoImport:    false,\n\t\t\t\tClipTimeout:   45,\n\t\t\t\tExportKeys:    true,\n\t\t\t\tNoPager:       false,\n\t\t\t\tNotifications: false,\n\t\t\t\tParsing:       true,\n\t\t\t\tPath:          \"/home/foo/.password-store\",\n\t\t\t\tSafeContent:   false,\n\t\t\t\tMounts: map[string]string{\n\t\t\t\t\t\"dev\":       \"/Users/johndoe/.password-store-dev\",\n\t\t\t\t\t\"ops\":       \"/Users/johndoe/.password-store-ops\",\n\t\t\t\t\t\"personal\":  \"/Users/johndoe/secrets\",\n\t\t\t\t\t\"teststore\": \"/Users/johndoe/tmp/teststore\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot, err := decode([]byte(tc.cfg), true)\n\t\t\trequire.NoError(t, err)\n\t\t\tif diff := cmp.Diff(tc.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"decode(%s) mismatch for:\\n%s\\n(-want +got):\\n%s\", tc.name, tc.cfg, diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst testConfig = `root:\n  askformore: true\n  autoimport: true\n  autosync: true\n  cliptimeout: 5\n  noconfirm: true\n  nopager: true\n  path: /home/johndoe/.password-store\n  safecontent: true\nmounts:\n  foo/sub:\n    askformore: false\n    autoimport: false\n    autosync: false\n    cliptimeout: 45\n    noconfirm: false\n    nopager: false\n    path: /home/johndoe/.password-store-foo-sub\n    safecontent: false\n  work:\n    askformore: false\n    autoimport: false\n    autosync: false\n    cliptimeout: 45\n    noconfirm: false\n    nopager: false\n    path: /home/johndoe/.password-store-work\n    safecontent: false\nversion: 1.4.0`\n\nfunc TestLoad(t *testing.T) {\n\ttd := os.TempDir()\n\tgcfg := filepath.Join(td, \".gopass.yml\")\n\t_ = os.Remove(gcfg)\n\tt.Setenv(\"GOPASS_CONFIG\", gcfg)\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\trequire.NoError(t, os.WriteFile(gcfg, []byte(testConfig), 0o600))\n\n\tcfg := Load()\n\tassert.True(t, cfg.SafeContent)\n}\n\nfunc TestLoadError(t *testing.T) {\n\tgcfg := filepath.Join(os.TempDir(), \".gopass-err.yml\")\n\tt.Setenv(\"GOPASS_CONFIG\", gcfg)\n\n\t_ = os.Remove(gcfg)\n\n\tcapture(t, func() error {\n\t\t_, err := load(gcfg, false)\n\t\tif err == nil {\n\t\t\treturn fmt.Errorf(\"should fail\")\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t_ = os.Remove(gcfg)\n\tcfg, err := load(gcfg, false)\n\trequire.Error(t, err)\n\n\tgcfg = filepath.Join(t.TempDir(), \"foo\", \".gopass.yml\")\n\tt.Setenv(\"GOPASS_CONFIG\", gcfg)\n\trequire.NoError(t, cfg.Save())\n}\n\nfunc TestDecodeError(t *testing.T) {\n\tgcfg := filepath.Join(os.TempDir(), \".gopass-err2.yml\")\n\tt.Setenv(\"GOPASS_CONFIG\", gcfg)\n\n\t_ = os.Remove(gcfg)\n\trequire.NoError(t, os.WriteFile(gcfg, []byte(testConfig+\"\\nfoobar: zab\\n\"), 0o600))\n\n\tcapture(t, func() error {\n\t\t_, err := load(gcfg, false)\n\t\tif err == nil {\n\t\t\treturn fmt.Errorf(\"should fail\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc capture(t *testing.T, fn func() error) string {\n\tt.Helper()\n\n\told := os.Stdout\n\n\toldcol := color.NoColor\n\tcolor.NoColor = true\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\tos.Stdout = w\n\n\tdone := make(chan string)\n\tgo func() {\n\t\tbuf := &bytes.Buffer{}\n\t\t_, _ = io.Copy(buf, r)\n\t\tdone <- buf.String()\n\t}()\n\n\terr = fn()\n\t// back to normal\n\t_ = w.Close()\n\tos.Stdout = old\n\tcolor.NoColor = oldcol\n\trequire.NoError(t, err)\n\tout := <-done\n\n\treturn strings.TrimSpace(out)\n}\n"
  },
  {
    "path": "internal/config/legacy/legacy.go",
    "content": "package legacy\n\nimport (\n\t\"maps\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// Pre1127 is a pre-1.12.7 config.\ntype Pre1127 struct {\n\tAutoClip      bool              `yaml:\"autoclip\"`      // decide whether passwords are automatically copied or not.\n\tAutoImport    bool              `yaml:\"autoimport\"`    // import missing public keys w/o asking.\n\tClipTimeout   int               `yaml:\"cliptimeout\"`   // clear clipboard after seconds.\n\tExportKeys    bool              `yaml:\"exportkeys\"`    // automatically export public keys of all recipients.\n\tNoColor       bool              `yaml:\"nocolor\"`       // do not use color when outputing text.\n\tNoPager       bool              `yaml:\"nopager\"`       // do not invoke a pager to display long lists.\n\tNotifications bool              `yaml:\"notifications\"` // enable desktop notifications.\n\tParsing       bool              `yaml:\"parsing\"`       // allows to switch off all output parsing.\n\tPath          string            `yaml:\"path\"`\n\tSafeContent   bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tMounts        map[string]string `yaml:\"mounts\"`\n\n\tConfigPath string `yaml:\"-\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// Config converts the Pre1127 config to the current config struct.\nfunc (c *Pre1127) Config() *Config {\n\tcfg := &Config{\n\t\tAutoClip:      c.AutoClip,\n\t\tAutoImport:    c.AutoImport,\n\t\tClipTimeout:   c.ClipTimeout,\n\t\tExportKeys:    c.ExportKeys,\n\t\tNoPager:       c.NoPager,\n\t\tNotifications: c.Notifications,\n\t\tParsing:       c.Parsing,\n\t\tPath:          c.Path,\n\t\tSafeContent:   c.SafeContent,\n\t\tMounts:        make(map[string]string, len(c.Mounts)),\n\t}\n\n\tmaps.Copy(cfg.Mounts, c.Mounts)\n\n\treturn cfg\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre1127) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Pre1102 is a pre-1.10.2 config.\ntype Pre1102 struct {\n\tAutoClip      bool              `yaml:\"autoclip\"`      // decide whether passwords are automatically copied or not.\n\tAutoImport    bool              `yaml:\"autoimport\"`    // import missing public keys w/o asking.\n\tClipTimeout   int               `yaml:\"cliptimeout\"`   // clear clipboard after seconds.\n\tExportKeys    bool              `yaml:\"exportkeys\"`    // automatically export public keys of all recipients.\n\tMIME          bool              `yaml:\"mime\"`          // enable gopass native MIME secrets.\n\tNoColor       bool              `yaml:\"nocolor\"`       // do not use color when outputing text.\n\tNoPager       bool              `yaml:\"nopager\"`       // do not invoke a pager to display long lists.\n\tNotifications bool              `yaml:\"notifications\"` // enable desktop notifications.\n\tPath          string            `yaml:\"path\"`\n\tSafeContent   bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tMounts        map[string]string `yaml:\"mounts\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre1102) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config converts the Pre1102 config to the current config struct.\nfunc (c *Pre1102) Config() *Config {\n\tcfg := &Config{\n\t\tAutoClip:      c.AutoClip,\n\t\tAutoImport:    c.AutoImport,\n\t\tClipTimeout:   c.ClipTimeout,\n\t\tExportKeys:    c.ExportKeys,\n\t\tNoPager:       c.NoPager,\n\t\tNotifications: c.Notifications,\n\t\tParsing:       true,\n\t\tPath:          c.Path,\n\t\tSafeContent:   c.SafeContent,\n\t\tMounts:        make(map[string]string, len(c.Mounts)),\n\t}\n\n\tmaps.Copy(cfg.Mounts, c.Mounts)\n\n\treturn cfg\n}\n\n// Pre193 is is pre-1.9.3 config.\ntype Pre193 struct {\n\tPath   string `yaml:\"-\"`\n\tRoot   *Pre193StoreConfig\n\tMounts map[string]*Pre193StoreConfig\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// Pre193StoreConfig is a pre-1.9.3 store config.\ntype Pre193StoreConfig struct {\n\tAutoClip       bool              `yaml:\"autoclip\"`   // decide whether passwords are automatically copied or not.\n\tAutoImport     bool              `yaml:\"autoimport\"` // import missing public keys w/o asking.\n\tAutoSync       bool              `yaml:\"autosync\"`   // push to git remote after commit, pull before push if necessary.\n\tCheckRecpHash  bool              `yaml:\"check_recipient_hash\"`\n\tClipTimeout    int               `yaml:\"cliptimeout\"`    // clear clipboard after seconds.\n\tConcurrency    int               `yaml:\"concurrency\"`    // allow to run multiple thread when batch processing.\n\tEditRecipients bool              `yaml:\"editrecipients\"` // edit recipients when confirming.\n\tExportKeys     bool              `yaml:\"exportkeys\"`     // automatically export public keys of all recipients.\n\tNoColor        bool              `yaml:\"nocolor\"`        // do not use color when outputing text.\n\tConfirm        bool              `yaml:\"noconfirm\"`      // do not confirm recipients when encrypting.\n\tNoPager        bool              `yaml:\"nopager\"`        // do not invoke a pager to display long lists.\n\tNotifications  bool              `yaml:\"notifications\"`  // enable desktop notifications.\n\tPath           string            `yaml:\"path\"`           // path to the root store.\n\tRecipientHash  map[string]string `yaml:\"recipient_hash\"`\n\tSafeContent    bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tUseSymbols     bool              `yaml:\"usesymbols\"`  // always use symbols when generating passwords.\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre193) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config converts the Pre193 config to the current config struct.\nfunc (c *Pre193) Config() *Config {\n\tcfg := &Config{\n\t\tAutoClip:      c.Root.AutoClip,\n\t\tAutoImport:    c.Root.AutoImport,\n\t\tClipTimeout:   c.Root.ClipTimeout,\n\t\tExportKeys:    c.Root.ExportKeys,\n\t\tNoPager:       c.Root.NoPager,\n\t\tNotifications: c.Root.Notifications,\n\t\tParsing:       true,\n\t\tPath:          c.Root.Path,\n\t\tSafeContent:   c.Root.SafeContent,\n\t\tMounts:        make(map[string]string, len(c.Mounts)),\n\t}\n\n\tif p, err := pathFromURL(c.Root.Path); err == nil {\n\t\tcfg.Path = p\n\t}\n\n\tfor k, v := range c.Mounts {\n\t\tp, err := pathFromURL(v.Path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Mounts[k] = p\n\t}\n\n\treturn cfg\n}\n\n// Pre182 is the gopass config structure before version 1.8.2.\ntype Pre182 struct {\n\tPath    string                        `yaml:\"-\"`\n\tRoot    *Pre182StoreConfig            `yaml:\"root\"`\n\tMounts  map[string]*Pre182StoreConfig `yaml:\"mounts\"`\n\tVersion string                        `yaml:\"version\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// Pre182StoreConfig is a per-store (root or mount) config.\ntype Pre182StoreConfig struct {\n\tAskForMore     bool              `yaml:\"askformore\"` // ask for more data on generate.\n\tAutoClip       bool              `yaml:\"autoclip\"`   // decide whether passwords are automatically copied or not.\n\tAutoImport     bool              `yaml:\"autoimport\"` // import missing public keys w/o asking.\n\tAutoSync       bool              `yaml:\"autosync\"`   // push to git remote after commit, pull before push if necessary.\n\tCheckRecpHash  bool              `yaml:\"check_recipient_hash\"`\n\tClipTimeout    int               `yaml:\"cliptimeout\"`    // clear clipboard after seconds.\n\tConcurrency    int               `yaml:\"concurrency\"`    // allow to run multiple thread when batch processing.\n\tEditRecipients bool              `yaml:\"editrecipients\"` // edit recipients when confirming.\n\tNoColor        bool              `yaml:\"nocolor\"`        // do not use color when outputing text.\n\tConfirm        bool              `yaml:\"noconfirm\"`      // do not confirm recipients when encrypting.\n\tNoPager        bool              `yaml:\"nopager\"`        // do not invoke a pager to display long lists.\n\tNotifications  bool              `yaml:\"notifications\"`  // enable desktop notifications.\n\tPath           string            `yaml:\"path\"`           // path to the root store.\n\tRecipientHash  map[string]string `yaml:\"recipient_hash\"`\n\tSafeContent    bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tUseSymbols     bool              `yaml:\"usesymbols\"`  // always use symbols when generating passwords.\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre182) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config converts the Pre182 config to the current config struct.\nfunc (c *Pre182) Config() *Config {\n\tcfg := &Config{\n\t\tAutoClip:      c.Root.AutoClip,\n\t\tAutoImport:    c.Root.AutoImport,\n\t\tClipTimeout:   c.Root.ClipTimeout,\n\t\tExportKeys:    true,\n\t\tNoPager:       c.Root.NoPager,\n\t\tNotifications: c.Root.Notifications,\n\t\tParsing:       true,\n\t\tPath:          c.Root.Path,\n\t\tSafeContent:   c.Root.SafeContent,\n\t\tMounts:        make(map[string]string, len(c.Mounts)),\n\t}\n\n\tif p, err := pathFromURL(c.Root.Path); err == nil {\n\t\tcfg.Path = p\n\t}\n\n\tfor k, v := range c.Mounts {\n\t\tp, err := pathFromURL(v.Path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Mounts[k] = p\n\t}\n\n\treturn cfg\n}\n\n// Pre140 is the gopass config structure before version 1.4.0.\ntype Pre140 struct {\n\tAskForMore  bool              `yaml:\"askformore\"`  // ask for more data on generate.\n\tAutoImport  bool              `yaml:\"autoimport\"`  // import missing public keys w/o asking.\n\tAutoSync    bool              `yaml:\"autosync\"`    // push to git remote after commit, pull before push if necessary.\n\tClipTimeout int               `yaml:\"cliptimeout\"` // clear clipboard after seconds.\n\tMounts      map[string]string `yaml:\"mounts,omitempty\"`\n\tConfirm     bool              `yaml:\"noconfirm\"`   // do not confirm recipients when encrypting.\n\tPath        string            `yaml:\"path\"`        // path to the root store.\n\tSafeContent bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tVersion     string            `yaml:\"version\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre140) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config converts the Pre140 config to the current config struct.\nfunc (c *Pre140) Config() *Config {\n\tcfg := &Config{\n\t\tAutoImport:  c.AutoImport,\n\t\tClipTimeout: c.ClipTimeout,\n\t\tExportKeys:  true,\n\t\tParsing:     true,\n\t\tPath:        c.Path,\n\t\tSafeContent: c.SafeContent,\n\t\tMounts:      make(map[string]string, len(c.Mounts)),\n\t}\n\n\tmaps.Copy(cfg.Mounts, c.Mounts)\n\n\treturn cfg\n}\n\n// Pre130 is the gopass config structure before version 1.3.0. Not all fields were.\n// available between 1.0.0 and 1.3.0, but this struct should cover all of them.\ntype Pre130 struct {\n\tAlwaysTrust bool              `yaml:\"alwaystrust\"` // always trust public keys when encrypting.\n\tAskForMore  bool              `yaml:\"askformore\"`  // ask for more data on generate.\n\tAutoImport  bool              `yaml:\"autoimport\"`  // import missing public keys w/o asking.\n\tAutoPull    bool              `yaml:\"autopull\"`    // pull from git before push.\n\tAutoPush    bool              `yaml:\"autopush\"`    // push to git remote after commit.\n\tClipTimeout int               `yaml:\"cliptimeout\"` // clear clipboard after seconds.\n\tDebug       bool              `yaml:\"debug\"`       // enable debug output.\n\tLoadKeys    bool              `yaml:\"loadkeys\"`    // load missing keys from store.\n\tMounts      map[string]string `yaml:\"mounts,omitempty\"`\n\tNoColor     bool              `yaml:\"nocolor\"`     // disable colors in output.\n\tConfirm     bool              `yaml:\"noconfirm\"`   // do not confirm recipients when encrypting.\n\tPath        string            `yaml:\"path\"`        // path to the root store.\n\tPersistKeys bool              `yaml:\"persistkeys\"` // store recipient keys in store.\n\tSafeContent bool              `yaml:\"safecontent\"` // avoid showing passwords in terminal.\n\tVersion     string            `yaml:\"version\"`\n\n\t// Catches all undefined files and must be empty after parsing.\n\tXXX map[string]any `yaml:\",inline\"`\n}\n\n// CheckOverflow implements configer.\nfunc (c *Pre130) CheckOverflow() error {\n\treturn checkOverflow(c.XXX)\n}\n\n// Config converts the Pre130 config to the current config struct.\nfunc (c *Pre130) Config() *Config {\n\tcfg := &Config{\n\t\tAutoImport:  c.AutoImport,\n\t\tClipTimeout: c.ClipTimeout,\n\t\tExportKeys:  true,\n\t\tParsing:     true,\n\t\tPath:        c.Path,\n\t\tSafeContent: c.SafeContent,\n\t\tMounts:      make(map[string]string, len(c.Mounts)),\n\t}\n\n\tmaps.Copy(cfg.Mounts, c.Mounts)\n\n\treturn cfg\n}\n\nfunc pathFromURL(u string) (string, error) {\n\tif !strings.Contains(u, \"://\") {\n\t\treturn u, nil\n\t}\n\n\tup, err := url.Parse(u)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn up.Path, nil\n}\n"
  },
  {
    "path": "internal/config/legacy/location.go",
    "content": "package legacy\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// configLocation returns the location of the config file\n// (a YAML file that contains values such as the path to the password store).\nfunc configLocation() string {\n\t// First, check for the \"GOPASS_CONFIG\" environment variable.\n\tif cf := os.Getenv(\"GOPASS_CONFIG\"); cf != \"\" {\n\t\treturn cf\n\t}\n\n\t// Second, check for the \"XDG_CONFIG_HOME\" environment variable\n\t// (which is part of the XDG Base Directory Specification for Linux and\n\t// other Unix-like operating sytstems)\n\treturn filepath.Join(appdir.UserConfig(), \"config.yml\")\n}\n\n// ConfigLocations returns the possible locations of gopass config files,\n// in decreasing priority.\nfunc ConfigLocations() []string {\n\tl := []string{}\n\tif cf := os.Getenv(\"GOPASS_CONFIG\"); cf != \"\" {\n\t\tl = append(l, cf)\n\t}\n\tl = append(l, filepath.Join(appdir.UserConfig(), \"config.yml\"))\n\tl = append(l, filepath.Join(appdir.UserHome(), \".config\", \"gopass\", \"config.yml\"))\n\tl = append(l, filepath.Join(appdir.UserHome(), \".gopass.yml\"))\n\n\treturn l\n}\n\n// PwStoreDir reads the password store dir from the environment\n// or returns the default location if the env is not set.\nfunc PwStoreDir(mount string) string {\n\tif mount != \"\" {\n\t\tcleanName := strings.ReplaceAll(mount, string(filepath.Separator), \"-\")\n\n\t\treturn fsutil.CleanPath(filepath.Join(appdir.UserData(), \"stores\", cleanName))\n\t}\n\t// PASSWORD_STORE_DIR support is discouraged.\n\tif d := os.Getenv(\"PASSWORD_STORE_DIR\"); d != \"\" {\n\t\tif gh := os.Getenv(\"GOPASS_HOMEDIR\"); gh == \"\" {\n\t\t\tdebug.Log(\"using value of PASSWORD_STORE_DIR: %s\", d)\n\n\t\t\treturn fsutil.CleanPath(d)\n\t\t}\n\t}\n\n\tif ld := filepath.Join(appdir.UserHome(), \".password-store\"); fsutil.IsDir(ld) {\n\t\tdebug.Log(\"re-using existing legacy dir for root store: %s\", ld)\n\n\t\treturn ld\n\t}\n\n\treturn fsutil.CleanPath(filepath.Join(appdir.UserData(), \"stores\", \"root\"))\n}\n"
  },
  {
    "path": "internal/config/legacy/location_xdg_test.go",
    "content": "//go:build !darwin && !windows\n\npackage legacy\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConfigLocations(t *testing.T) {\n\tgpcfg := filepath.Join(os.TempDir(), \"config\", \".gopass.yml\")\n\txdghome := filepath.Join(os.TempDir(), \"xdg\")\n\tgphome := filepath.Join(os.TempDir(), \"home\")\n\n\txdgcfg := filepath.Join(xdghome, \"gopass\", \"config.yml\")\n\tcurcfg := filepath.Join(gphome, \".config\", \"gopass\", \"config.yml\")\n\toldcfg := filepath.Join(gphome, \".gopass.yml\")\n\n\tt.Run(\"GOPASS_CONFIG, GOPASS_HOMEDIR set\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_CONFIG\", gpcfg)\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", gphome)\n\n\t\tassert.Equal(t, []string{gpcfg, curcfg, curcfg, oldcfg}, ConfigLocations())\n\t})\n\n\tt.Run(\"GOPASS_CONFIG, GOPASS_HOMEDIR, XDG_CONFIG_HOME set\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_CONFIG\", gpcfg)\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", gphome)\n\t\tt.Setenv(\"XDG_CONFIG_HOME\", xdghome)\n\n\t\tassert.Equal(t, []string{gpcfg, curcfg, curcfg, oldcfg}, ConfigLocations())\n\t})\n\n\tt.Run(\"XDG_CONFIG_HOME set only\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", \"\")\n\t\tt.Setenv(\"XDG_CONFIG_HOME\", xdghome)\n\t\tassert.Equal(t, xdgcfg, ConfigLocations()[0])\n\t})\n}\n"
  },
  {
    "path": "internal/config/legacy.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/config/legacy\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nfunc migrateConfigs() error {\n\tcfg := legacy.LoadWithOptions(true, false)\n\tif cfg == nil {\n\t\tdebug.V(2).Log(\"no legacy config found. not migrating.\")\n\n\t\treturn nil\n\t}\n\n\tc := newGitconfig().LoadAll(cfg.Path)\n\n\tfor k, v := range cfg.ConfigMap() {\n\t\tvar fk string\n\t\tswitch k {\n\t\tcase \"keychain\":\n\t\t\tfk = \"age.usekeychain\"\n\t\tcase \"path\":\n\t\t\tfk = \"mounts.path\"\n\t\tcase \"safecontent\":\n\t\t\tfk = \"show.safecontent\"\n\t\tcase \"autoclip\":\n\t\t\tfk = \"generate.autoclip\"\n\t\tcase \"showautoclip\":\n\t\t\tfk = \"show.autoclip\"\n\t\tdefault:\n\t\t\tfk = \"core.\" + k\n\t\t}\n\n\t\tif err := c.SetGlobal(fk, v); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write new config: %w\", err)\n\t\t}\n\t}\n\tfor alias, path := range cfg.Mounts {\n\t\tif err := c.SetGlobal(mpk(alias), path); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write new config: %w\", err)\n\t\t}\n\t}\n\n\tdebug.Log(\"migrated legacy config from %s\", cfg.ConfigPath)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/config/location.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// configLocation returns the location of the config file\n// (a YAML file that contains values such as the path to the password store).\nfunc configLocation() string {\n\t// First, check for the \"GOPASS_CONFIG\" environment variable.\n\tif cf := os.Getenv(\"GOPASS_CONFIG\"); cf != \"\" {\n\t\treturn cf\n\t}\n\n\t// Second, check for the \"XDG_CONFIG_HOME\" environment variable\n\t// (which is part of the XDG Base Directory Specification for Linux and\n\t// other Unix-like operating sytstems)\n\treturn filepath.Join(appdir.UserConfig(), \"config.yml\")\n}\n\n// PwStoreDir reads the password store dir from the environment\n// or returns the default location if the env is not set.\nfunc PwStoreDir(mount string) string {\n\tif mount != \"\" {\n\t\tcleanName := strings.ReplaceAll(mount, string(filepath.Separator), \"-\")\n\n\t\treturn fsutil.CleanPath(filepath.Join(appdir.UserData(), \"stores\", cleanName))\n\t}\n\t// PASSWORD_STORE_DIR support is discouraged.\n\tif d := os.Getenv(\"PASSWORD_STORE_DIR\"); d != \"\" {\n\t\tdebug.Log(\"using value of PASSWORD_STORE_DIR: %s\", d)\n\n\t\treturn fsutil.CleanPath(d)\n\t}\n\n\tif ld := filepath.Join(appdir.UserHome(), \".password-store\"); fsutil.IsDir(ld) {\n\t\tdebug.Log(\"re-using existing legacy dir for root store: %s\", ld)\n\n\t\treturn ld\n\t}\n\n\treturn fsutil.CleanPath(filepath.Join(appdir.UserData(), \"stores\", \"root\"))\n}\n\n// Directory returns the configuration directory for the gopass config file.\nfunc Directory() string {\n\treturn filepath.Dir(configLocation())\n}\n"
  },
  {
    "path": "internal/config/location_test.go",
    "content": "package config\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPwStoreDirNoEnv(t *testing.T) {\n\tif runtime.GOOS != \"windows\" {\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", \"/tmp\")\n\t}\n\n\tbaseDir := filepath.Join(appdir.UserHome(), \".local\", \"share\", \"gopass\", \"stores\")\n\tif runtime.GOOS == \"windows\" {\n\t\tbaseDir = filepath.Join(appdir.UserHome(), \"AppData\", \"Local\", \"gopass\", \"stores\")\n\t}\n\n\tfor in, out := range map[string]string{\n\t\t\"\":                          filepath.Join(baseDir, \"root\"),\n\t\t\"work\":                      filepath.Join(baseDir, \"work\"),\n\t\tfilepath.Join(\"foo\", \"bar\"): filepath.Join(baseDir, \"foo-bar\"),\n\t} {\n\t\tassert.Equal(t, out, PwStoreDir(in), in, \"mount \"+in)\n\t}\n}\n\nfunc TestDirectory(t *testing.T) {\n\tt.Parallel()\n\n\tloc := configLocation()\n\tdir := filepath.Dir(loc)\n\tassert.Equal(t, dir, Directory())\n}\n"
  },
  {
    "path": "internal/config/location_xdg_test.go",
    "content": "//go:build !darwin && !windows\n\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPwStoreDir(t *testing.T) {\n\tgph := filepath.Join(os.TempDir(), \"home\")\n\tt.Setenv(\"GOPASS_HOMEDIR\", gph)\n\n\tassert.Equal(t, filepath.Join(gph, \".local\", \"share\", \"gopass\", \"stores\", \"root\"), PwStoreDir(\"\"))\n\tassert.Equal(t, filepath.Join(gph, \".local\", \"share\", \"gopass\", \"stores\", \"foo\"), PwStoreDir(\"foo\"))\n\n\tpsd := filepath.Join(gph, \".password-store-test\")\n\tt.Setenv(\"PASSWORD_STORE_DIR\", psd)\n\n\tassert.Equal(t, psd, PwStoreDir(\"\"))\n\tassert.Equal(t, filepath.Join(gph, \".local\", \"share\", \"gopass\", \"stores\", \"foo\"), PwStoreDir(\"foo\"))\n\n\tt.Run(\"GOPASS_HOMEDIR takes precedence\", func(t *testing.T) {\n\t\tt.Setenv(\"XDG_DATA_HOME\", filepath.Join(os.TempDir(), \".local\", \"foo\"))\n\t\tassert.Equal(t, psd, PwStoreDir(\"\"))\n\t\tassert.Equal(t, filepath.Join(gph, \".local\", \"share\", \"gopass\", \"stores\", \"foo\"), PwStoreDir(\"foo\"))\n\t})\n\n\tt.Run(\"GOPASS_HOMEDIR unset, XDG_DATA_HOME takes precedence\", func(t *testing.T) {\n\t\tt.Setenv(\"XDG_DATA_HOME\", filepath.Join(os.TempDir(), \".local\", \"foo\"))\n\t\trequire.NoError(t, os.Unsetenv(\"GOPASS_HOMEDIR\"))\n\t\tassert.Equal(t, psd, PwStoreDir(\"\"))\n\t\tassert.Equal(t, filepath.Join(os.TempDir(), \".local\", \"foo\", \"gopass\", \"stores\", \"foo\"), PwStoreDir(\"foo\"))\n\t})\n}\n\nfunc TestConfigLocation(t *testing.T) {\n\tevs := map[string]struct {\n\t\tev  string\n\t\tloc string\n\t}{\n\t\t\"GOPASS_CONFIG\":   {ev: filepath.Join(os.TempDir(), \"gopass.yml\"), loc: filepath.Join(os.TempDir(), \"gopass.yml\")},\n\t\t\"XDG_CONFIG_HOME\": {ev: filepath.Join(os.TempDir(), \"xdg\"), loc: filepath.Join(os.TempDir(), \"xdg\", \"gopass\", \"config.yml\")},\n\t\t\"GOPASS_HOMEDIR\":  {ev: filepath.Join(os.TempDir(), \"home\"), loc: filepath.Join(os.TempDir(), \"home\", \".config\", \"gopass\", \"config.yml\")},\n\t}\n\n\tfor k := range evs {\n\t\trequire.NoError(t, os.Unsetenv(k))\n\t}\n\n\tfor k, v := range evs {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tt.Setenv(k, v.ev)\n\t\t\tassert.Equal(t, v.loc, configLocation())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/utils.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"strconv\"\n)\n\n// AsBool converts a string to a bool value.\nfunc AsBool(s string) bool {\n\treturn AsBoolWithDefault(s, false)\n}\n\n// AsBoolWithDefault converts a string to a bool value with a default value.\nfunc AsBoolWithDefault(s string, def bool) bool {\n\tif s == \"\" {\n\t\treturn def\n\t}\n\n\tswitch s {\n\tcase \"1\", \"true\", \"yes\", \"on\":\n\t\treturn true\n\tcase \"0\", \"false\", \"no\", \"off\":\n\t\treturn false\n\tdefault:\n\t\treturn def\n\t}\n}\n\n// AsInt converts a string to an integer value.\nfunc AsInt(s string) int {\n\treturn AsIntWithDefault(s, 0)\n}\n\n// AsIntWithDefault converts a string to an integer value with a default value.\nfunc AsIntWithDefault(s string, def int) int {\n\tif s == \"\" {\n\t\treturn def\n\t}\n\n\ti, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn def\n\t}\n\n\treturn i\n}\n\n// Bool returns a bool value from the config in the context.\nfunc Bool(ctx context.Context, key string) bool {\n\tcfg, mp := FromContext(ctx)\n\n\treturn AsBool(cfg.GetM(mp, key))\n}\n\n// String returns a string value from the config in the context.\nfunc String(ctx context.Context, key string) string {\n\tcfg, mp := FromContext(ctx)\n\n\treturn cfg.GetM(mp, key)\n}\n\n// Int returns an integer value from the config in the context.\nfunc Int(ctx context.Context, key string) int {\n\tcfg, mp := FromContext(ctx)\n\n\treturn AsInt(cfg.GetM(mp, key))\n}\n"
  },
  {
    "path": "internal/config/utils_test.go",
    "content": "package config\n\nimport (\n\t\"testing\"\n)\n\nfunc TestAsBoolWithDefault(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ts        string\n\t\tdef      bool\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"Empty string with default true\",\n\t\t\ts:        \"\",\n\t\t\tdef:      true,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty string with default false\",\n\t\t\ts:        \"\",\n\t\t\tdef:      false,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Valid string '1' with default true\",\n\t\t\ts:        \"1\",\n\t\t\tdef:      true,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"Valid string '0' with default true\",\n\t\t\ts:        \"0\",\n\t\t\tdef:      true,\n\t\t\texpected: false,\n\t\t},\n\t\t// Add more test cases here\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult := AsBoolWithDefault(test.s, test.def)\n\t\t\tif result != test.expected {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", test.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAsIntWithDefault(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ts        string\n\t\tdef      int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"Empty string with default 0\",\n\t\t\ts:        \"\",\n\t\t\tdef:      0,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Valid string '123' with default 0\",\n\t\t\ts:        \"123\",\n\t\t\tdef:      0,\n\t\t\texpected: 123,\n\t\t},\n\t\t{\n\t\t\tname:     \"Invalid string 'abc' with default 0\",\n\t\t\ts:        \"abc\",\n\t\t\tdef:      0,\n\t\t\texpected: 0,\n\t\t},\n\t\t// Add more test cases here\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult := AsIntWithDefault(test.s, test.def)\n\t\t\tif result != test.expected {\n\t\t\t\tt.Errorf(\"Expected %v, but got %v\", test.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/create/helpers.go",
    "content": "package create\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nfunc fmtfn(d int, n string, t string) string {\n\tstrlen := 40 - d\n\t// indent - [N] - text (trailing spaces)\n\tfmtStr := \"%\" + strconv.Itoa(d) + \"s%s %-\" + strconv.Itoa(strlen) + \"s\"\n\tdebug.Log(\"d: %d, n: %q, t: %q, strlen: %d, fmtStr: %q\", d, n, t, strlen, fmtStr)\n\n\treturn fmt.Sprintf(fmtStr, \"\", color.GreenString(\"[\"+n+\"]\"), t)\n}\n\n// extractHostname tries to extract the hostname from a URL in a filepath-safe\n// way for use in the name of a secret.\nfunc extractHostname(in string) string {\n\tif in == \"\" {\n\t\treturn \"\"\n\t}\n\t// help url.Parse by adding a scheme if one is missing. This should still\n\t// allow for any scheme, but by default we assume http (only for parsing)\n\turlStr := in\n\tif !strings.Contains(urlStr, \"://\") {\n\t\turlStr = \"http://\" + urlStr\n\t}\n\n\tu, err := url.Parse(urlStr)\n\tif err == nil {\n\t\tif ch := fsutil.CleanFilename(u.Hostname()); ch != \"\" {\n\t\t\treturn ch\n\t\t}\n\t}\n\n\treturn fsutil.CleanFilename(in)\n}\n"
  },
  {
    "path": "internal/create/helpers_test.go",
    "content": "package create\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFmtfn(t *testing.T) {\n\ttests := []struct {\n\t\td        int\n\t\tn        string\n\t\tt        string\n\t\texpected string\n\t}{\n\t\t{0, \"1\", \"test\", color.GreenString(\"[1]\") + \" test                                    \"},\n\t\t{2, \"2\", \"example\", \"  \" + color.GreenString(\"[2]\") + \" example                               \"},\n\t\t{4, \"3\", \"sample\", \"    \" + color.GreenString(\"[3]\") + \" sample                              \"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.n, func(t *testing.T) {\n\t\t\tresult := fmtfn(tt.d, tt.n, tt.t)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractHostname(t *testing.T) {\n\tt.Parallel()\n\n\tfor in, out := range map[string]string{\n\t\t\"\":                                     \"\",\n\t\t\"http://www.example.org/\":              \"www.example.org\",\n\t\t\"++#+++#jhlkadsrezu 33 553q ++++##$§&\": \"jhlkadsrezu_33_553q\",\n\t\t\"www.example.org/?foo=bar#abc\":         \"www.example.org\",\n\t\t\"a test\":                               \"a_test\",\n\t\t\"http://example.com\":                   \"example.com\",\n\t\t\"https://sub.example.com\":              \"sub.example.com\",\n\t\t\"ftp://example.com\":                    \"example.com\",\n\t\t\"example.com\":                          \"example.com\",\n\t\t\"invalid-url\":                          \"invalid-url\",\n\t} {\n\t\tassert.Equal(t, out, extractHostname(in))\n\t}\n}\n"
  },
  {
    "path": "internal/create/templates.go",
    "content": "package create\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nvar defaultTemplates = []string{\n\t`---\npriority: 0\nname: \"Website login\"\nprefix: \"websites\"\nname_from:\n  - \"url\"\n  - \"username\"\nwelcome: \"🧪 Creating Website login\"\nattributes:\n  - name: \"url\"\n    type: \"hostname\"\n    prompt: \"Website URL\"\n    min: 1\n    max: 255\n  - name: \"username\"\n    type: \"string\"\n    prompt: \"Username\"\n    min: 1\n  - name: \"password\"\n    type: \"password\"\n    prompt: \"Password for the Website\"\n`,\n\t`---\npriority: 1\nname: \"PIN Code (numerical)\"\nprefix: \"pin\"\nname_from:\n  - \"authority\"\n  - \"application\"\nwelcome: \"🧪 Creating numerical PIN\"\nattributes:\n  - name: \"authority\"\n    type: \"string\"\n    prompt: \"Authority\"\n    min: 1\n  - name: \"application\"\n    type: \"string\"\n    prompt: \"Entity\"\n    min: 1\n  - name: \"password\"\n    type: \"password\"\n    prompt: \"Pin\"\n    charset: \"0123456789\"\n    min: 1\n    max: 64\n  - name: \"comment\"\n    type: \"string\"\n`,\n}\n\ntype storageSetter interface {\n\tSet(context.Context, string, []byte) error\n\tTryAdd(context.Context, ...string) error\n\tTryCommit(context.Context, string) error\n}\n\nfunc (w *Wizard) writeTemplates(ctx context.Context, s storageSetter) error {\n\tfor _, y := range defaultTemplates {\n\t\tby := []byte(y)\n\t\ttpl := Template{}\n\t\tif err := yaml.Unmarshal(by, &tpl); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid default template. Please report a bug! %w\", err)\n\t\t}\n\n\t\tpath := fmt.Sprintf(\"%s%d-%s.yml\", tplPath, tpl.Priority, tpl.Prefix)\n\t\tif err := s.Set(ctx, path, by); err != nil {\n\t\t\tif errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\t\tdebug.Log(\"got unexpected error for %s (ignoring): %s\", path, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"failed to write default template %s: %w\", path, err)\n\t\t}\n\n\t\tif err := s.TryAdd(ctx, path); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to stage changes %s: %w\", path, err)\n\t\t}\n\n\t\tdebug.Log(\"wrote default template to %s\", path)\n\t}\n\n\tif err := s.TryCommit(ctx, \"Added default wizard templates\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/create/wizard.go",
    "content": "// Package create provides a credential creation wizard.\npackage create\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/editor\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store/root\"\n\t\"github.com/gopasspw/gopass/internal/tpl\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/martinhoefling/goxkcdpwgen/xkcdpwgen\"\n\t\"github.com/urfave/cli/v2\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst (\n\ttplPath = \".gopass/create/\"\n)\n\n// Attribute is a credential attribute that is being asked for\n// when populating a template.\ntype Attribute struct {\n\tName         string `yaml:\"name\"`\n\tType         string `yaml:\"type\"`\n\tPrompt       string `yaml:\"prompt\"`\n\tCharset      string `yaml:\"charset\"`\n\tMin          int    `yaml:\"min\"`\n\tMax          int    `yaml:\"max\"`\n\tAlwaysPrompt bool   `yaml:\"always_prompt\"` // always prompt for the crendentials\n\tStrict       bool   `yaml:\"strict\"`        // enforce character class rules (all detected classes must be present)\n}\n\n// Template is an action template for the create wizard.\ntype Template struct {\n\tName       string      `yaml:\"name\"`\n\tPriority   int         `yaml:\"priority\"`\n\tPrefix     string      `yaml:\"prefix\"`\n\tNameFrom   []string    `yaml:\"name_from\"`\n\tWelcome    string      `yaml:\"welcome\"`\n\tAttributes []Attribute `yaml:\"attributes\"`\n}\n\n// Wizard is the templateable credential creation wizard.\ntype Wizard struct {\n\tTemplates []Template\n}\n\n// New creates a new instance of the wizard. It will parse the user\n// supplied templates and add the default templates.\nfunc New(ctx context.Context, s backend.Storage) (*Wizard, error) {\n\tw := &Wizard{}\n\n\ttpls, err := w.parseTemplates(ctx, s)\n\tif err != nil {\n\t\treturn w, fmt.Errorf(\"could not parse templates: %w\", err)\n\t}\n\n\tif len(tpls) < 1 {\n\t\t// no templates found, write default templates\n\t\tif err := w.writeTemplates(ctx, s); err != nil {\n\t\t\treturn w, fmt.Errorf(\"could not write default templates: %w\", err)\n\t\t}\n\n\t\t// then re-parse them\n\t\ttpls, err = w.parseTemplates(ctx, s)\n\t\tif err != nil {\n\t\t\treturn w, fmt.Errorf(\"could not parse templates: %w\", err)\n\t\t}\n\t}\n\n\t// fallback to the default templates if no templates were found\n\t// even after writing them. this indicates a problem with writing\n\t// and should be investigated.\n\tif len(tpls) < 1 {\n\t\tout.Error(ctx, \"No templates found, falling back to default templates\")\n\n\t\ttpls, err = w.parseTemplatesFallback(ctx)\n\t\tif err != nil {\n\t\t\treturn w, fmt.Errorf(\"could not parse fallback templates: %w\", err)\n\t\t}\n\t}\n\n\tw.Templates = tpls\n\n\treturn w, nil\n}\n\nfunc (w *Wizard) parseTemplatesFallback(_ context.Context) ([]Template, error) {\n\tparsed := []Template{}\n\tfor _, tpl := range defaultTemplates {\n\t\tt := Template{}\n\t\tif err := yaml.Unmarshal([]byte(tpl), &t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tparsed = append(parsed, t)\n\t}\n\n\treturn parsed, nil\n}\n\nfunc (w *Wizard) parseTemplates(ctx context.Context, s backend.Storage) ([]Template, error) {\n\ttpls, err := s.List(ctx, tplPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparsedTpls := []Template{}\n\tfor _, f := range tpls {\n\t\tif !strings.HasSuffix(f, \".yml\") && !strings.HasSuffix(f, \".yaml\") {\n\t\t\tdebug.Log(\"ignoring unknown file extension: %s\", f)\n\n\t\t\tcontinue\n\t\t}\n\t\tbuf, err := s.Get(ctx, f)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to read template %s: %s\", f, err)\n\n\t\t\tcontinue\n\t\t}\n\t\ttpl := Template{}\n\t\tif err := yaml.Unmarshal(buf, &tpl); err != nil {\n\t\t\tdebug.Log(\"failed to parse template %s: %s\", f, err)\n\t\t\tout.Errorf(ctx, \"Bad template %s: %s\\n%s\", f, err, string(buf))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tparsedTpls = append(parsedTpls, tpl)\n\t}\n\n\tsort.Slice(parsedTpls, func(i, j int) bool {\n\t\treturn parsedTpls[i].Priority < parsedTpls[j].Priority\n\t})\n\n\treturn parsedTpls, nil\n}\n\n// ActionCallback is the callback for the creation calls to print and copy the credentials.\ntype ActionCallback func(context.Context, *cli.Context, string, string, bool) error\n\n// Actions returns a list of actions that can be performed on the wizard. The actions directly\n// interact with the underlying storage.\nfunc (w *Wizard) Actions(s *root.Store, cb ActionCallback) cui.Actions {\n\tsort.Slice(w.Templates, func(i, j int) bool {\n\t\treturn w.Templates[i].Priority < w.Templates[j].Priority\n\t})\n\n\tacts := make(cui.Actions, 0, len(w.Templates))\n\tfor _, tpl := range w.Templates {\n\t\tacts = append(acts, cui.Action{\n\t\t\tName: tpl.Name,\n\t\t\tFn:   mkActFunc(tpl, s, cb),\n\t\t})\n\t}\n\n\treturn acts\n}\n\nfunc mkActFunc(tpl Template, s *root.Store, cb ActionCallback) func(context.Context, *cli.Context) error { //nolint:cyclop\n\tdebug.Log(\"creating action func for %+v, cb: %p\", tpl, cb)\n\n\treturn func(ctx context.Context, c *cli.Context) error {\n\t\tname := c.Args().First()\n\t\tstore := c.String(\"store\")\n\n\t\t// select store.\n\t\tif store == \"\" {\n\t\t\tstore = cui.AskForStore(ctx, s)\n\t\t}\n\t\tctx = config.WithMount(ctx, store)\n\n\t\tforce := c.Bool(\"force\")\n\n\t\tif err := hook.Invoke(ctx, \"create.pre-hook\", name); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsec := secrets.NewAKV()\n\n\t\tout.Print(ctx, tpl.Welcome)\n\n\t\t// genPW is needed for the callback\n\t\tvar genPw bool\n\t\t// password is needed for the callback\n\t\tvar password string\n\t\t// hostname is needed in later iterations (e.g. password rule lookup)\n\t\tvar hostname string\n\t\t// wantForName is a list of attributes that will be used to build the name\n\t\twantForName := set.Map(tpl.NameFrom)\n\t\t// nameParts are the components the name will be built from\n\t\tvar nameParts []string\n\t\t// step is only used for printing the progress\n\t\tvar step int\n\t\tfor _, v := range tpl.Attributes {\n\t\t\tstep++\n\t\t\tk := v.Name\n\n\t\t\t// if no prompt is set default to the key\n\t\t\tif v.Prompt == \"\" {\n\t\t\t\tv.Prompt = strings.ToTitle(k)\n\t\t\t}\n\n\t\t\tswitch v.Type {\n\t\t\tcase \"string\":\n\t\t\t\tsv, err := termio.AskForString(ctx, fmtfn(2, strconv.Itoa(step), v.Prompt), \"\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif v.Min > 0 && len(sv) < v.Min {\n\t\t\t\t\treturn fmt.Errorf(\"%s is too short (needs %d)\", v.Name, v.Min)\n\t\t\t\t}\n\t\t\t\tif v.Max > 0 && len(sv) > v.Max {\n\t\t\t\t\treturn fmt.Errorf(\"%s is too long (at most %d)\", v.Name, v.Max)\n\t\t\t\t}\n\t\t\t\tif wantForName[k] {\n\t\t\t\t\tnameParts = append(nameParts, sv)\n\t\t\t\t}\n\t\t\t\t_ = sec.Set(k, sv)\n\t\t\tcase \"multiline\":\n\t\t\t\ted := editor.Path(c)\n\n\t\t\t\tcontent, err := renderTemplate(ctx, k, s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tdebug.Log(\"failed to render template %q: %s\", k, err)\n\t\t\t\t}\n\t\t\t\tcontent, err = editor.Invoke(ctx, ed, content)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn exit.Error(exit.Unknown, err, \"failed to invoke editor: %s\", err)\n\t\t\t\t}\n\t\t\t\tn, err := sec.Write(content)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to write %d bytes to %s: %w\", n, k, err)\n\t\t\t\t}\n\t\t\tcase \"hostname\":\n\t\t\t\tvar def string\n\t\t\t\tif k == \"username\" {\n\t\t\t\t\tdef = config.String(ctx, \"create.default-username\")\n\t\t\t\t}\n\t\t\t\tsv, err := termio.AskForString(ctx, fmtfn(2, strconv.Itoa(step), v.Prompt), def)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\thostname = extractHostname(sv)\n\t\t\t\tif hostname == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"can not parse URL %s\", sv)\n\t\t\t\t}\n\t\t\t\tif wantForName[k] {\n\t\t\t\t\tnameParts = append(nameParts, hostname)\n\t\t\t\t}\n\t\t\t\tif u := pwrules.LookupChangeURL(ctx, hostname); u != \"\" {\n\t\t\t\t\t_ = sec.Set(\"password-change-url\", u)\n\t\t\t\t}\n\t\t\t\t_ = sec.Set(k, sv)\n\t\t\tcase \"password\":\n\t\t\t\tvar err error\n\t\t\t\tif !v.AlwaysPrompt {\n\t\t\t\t\tgenPw, err = termio.AskForBool(ctx, fmtfn(2, strconv.Itoa(step), \"Generate Password?\"), true)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif genPw { //nolint:nestif\n\t\t\t\t\tpassword, err = generatePassword(ctx, hostname, v.Charset, v.Strict)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tpassword, err = termio.AskForPassword(ctx, v.Prompt, true)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tif v.Min > 0 && len(password) < v.Min {\n\t\t\t\t\t\treturn fmt.Errorf(\"%s is too short (needs %d)\", v.Name, v.Min)\n\t\t\t\t\t}\n\t\t\t\t\tif v.Max > 0 && len(password) > v.Min {\n\t\t\t\t\t\treturn fmt.Errorf(\"%s is too long (at most %d)\", v.Name, v.Max)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsec.SetPassword(password)\n\t\t\t}\n\t\t}\n\n\t\t// now we can generate a name. If it's already take we can the user for an alternative\n\t\t// name.\n\n\t\t// make sure the store is properly separated from the name.\n\t\tif store != \"\" {\n\t\t\tstore += \"/\"\n\t\t}\n\n\t\t// by default create will generate a name for the secret based on the user\n\t\t// input. Only when the force flag is given it will accept a secrets path\n\t\t// as the first argument.\n\t\tif name == \"\" || !force {\n\t\t\tfor i, s := range nameParts {\n\t\t\t\tnameParts[i] = fsutil.CleanFilename(s)\n\t\t\t}\n\t\t\tname = fmt.Sprintf(\"%s%s/%s\", store, tpl.Prefix, filepath.Join(nameParts...))\n\t\t}\n\t\tif force && !strings.HasPrefix(name, store) {\n\t\t\tout.Warningf(ctx, \"User supplied secret name %q does not match requested mount %q. Ignoring store flag.\", name, store)\n\t\t}\n\n\t\t// force will also override the check for existing entries.\n\t\tif s.Exists(ctx, name) && !force {\n\t\t\tstep++\n\t\t\tvar err error\n\t\t\tname, err = termio.AskForString(ctx, fmtfn(2, strconv.Itoa(step), \"Secret already exists. Choose another path or enter to overwrite\"), name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif err := s.Set(ctxutil.WithCommitMessage(ctx, \"Created new entry\"), name, sec); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set %q: %w\", name, err)\n\t\t}\n\t\tout.OKf(ctx, \"Credentials saved to %q\", name)\n\n\t\treturn cb(ctx, c, name, password, genPw)\n\t}\n}\n\nfunc renderTemplate(ctx context.Context, name string, s *root.Store) ([]byte, error) {\n\ttName, tmpl, found := s.LookupTemplate(ctx, name)\n\tif !found {\n\t\tdebug.Log(\"No template found for %s\", name)\n\n\t\treturn nil, nil\n\t}\n\n\ttmplStr := strings.TrimSpace(string(tmpl))\n\tif tmplStr == \"\" {\n\t\tdebug.Log(\"Skipping empty template %q, for %s\", tName, name)\n\n\t\treturn nil, nil\n\t}\n\n\tnc, err := tpl.Execute(ctx, string(tmpl), name, nil, s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute template %q: %w\", tName, err)\n\t}\n\n\treturn nc, nil\n}\n\n// generatePassword will walk through the password generation steps.\nfunc generatePassword(ctx context.Context, hostname, charset string, strict bool) (string, error) {\n\tdefaultLength, _ := config.DefaultPasswordLengthFromEnv(ctx)\n\n\tif charset != \"\" {\n\t\tlength, err := termio.AskForInt(ctx, fmtfn(4, \"a\", \"How long?\"), 4)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strict {\n\t\t\treturn pwgen.GeneratePasswordCharsetStrict(length, charset)\n\t\t}\n\n\t\treturn pwgen.GeneratePasswordCharset(length, charset), nil\n\t}\n\n\tif _, found := pwrules.LookupRule(ctx, hostname); found {\n\t\tout.Noticef(ctx, \"Using password rules for %s ...\", hostname)\n\t\tlength, err := termio.AskForInt(ctx, fmtfn(4, \"b\", \"How long?\"), defaultLength)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn pwgen.NewCrypticForDomain(ctx, length, hostname).Password(), nil\n\t}\n\n\txkcd, err := termio.AskForBool(ctx, fmtfn(4, \"a\", \"Human-pronounceable passphrase?\"), false)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif xkcd {\n\t\treturn generatePasswordXKCD(ctx)\n\t}\n\n\tlength, err := termio.AskForInt(ctx, fmtfn(4, \"b\", \"How long?\"), defaultLength)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsymbols, err := termio.AskForBool(ctx, fmtfn(4, \"c\", \"Include symbols?\"), config.Bool(ctx, \"generate.symbols\"))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcorp, err := termio.AskForBool(ctx, fmtfn(4, \"d\", \"Strict rules?\"), config.Bool(ctx, \"generate.strict\"))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif corp {\n\t\treturn pwgen.GeneratePasswordWithAllClasses(length, symbols)\n\t}\n\n\treturn pwgen.GeneratePassword(length, symbols), nil\n}\n\nfunc generatePasswordXKCD(ctx context.Context) (string, error) {\n\tlength, err := termio.AskForInt(ctx, fmtfn(4, \"b\", \"How many words?\"), config.Int(ctx, \"pwgen.xkcd-len\"))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif length < 1 {\n\t\tlength = config.DefaultXKCDLength\n\t}\n\n\tg := xkcdpwgen.NewGenerator()\n\tg.SetNumWords(length)\n\n\tif sv := config.String(ctx, \"pwgen.xkcd-sep\"); sv != \"\" {\n\t\tg.SetDelimiter(sv)\n\t}\n\n\tg.SetCapitalize(config.Bool(ctx, \"pwgen.xkcd-capitalize\"))\n\tg.SetRandomNumbers(config.Bool(ctx, \"pwgen.xkcd-numbers\"))\n\n\tif sv := config.String(ctx, \"pwgen.xkcd-lang\"); sv != \"\" {\n\t\tif err := g.UseLangWordlist(sv); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to use wordlist for lang %s: %w\", sv, err)\n\t\t}\n\t}\n\n\treturn g.GeneratePasswordString(), nil\n}\n"
  },
  {
    "path": "internal/create/wizard_test.go",
    "content": "package create\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/store/mockstore/inmem\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype fakeSetter struct{}\n\nfunc (f *fakeSetter) Set(ctx context.Context, name string, content []byte) error {\n\treturn nil\n}\n\nfunc (f *fakeSetter) Add(ctx context.Context, args ...string) error {\n\treturn nil\n}\n\nfunc (f *fakeSetter) TryAdd(ctx context.Context, args ...string) error {\n\treturn nil\n}\n\nfunc (f *fakeSetter) Commit(ctx context.Context, msg string) error {\n\treturn nil\n}\n\nfunc (f *fakeSetter) TryCommit(ctx context.Context, msg string) error {\n\treturn nil\n}\n\nfunc TestWrite(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tw := &Wizard{}\n\n\trequire.NoError(t, w.writeTemplates(ctx, &fakeSetter{}))\n}\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\ts := inmem.New()\n\t_ = s.Set(ctx, \".create/pin.yml\", []byte(`---\npriority: 1\nname: \"PIN Code (numerical)\"\nprefix: \"pin\"\nname_from:\n  - \"authority\"\n  - \"application\"\nwelcome: \"🧪 Creating numerical PIN\"\nattributes:\n  - name: \"authority\"\n    type: \"string\"\n    prompt: \"Authority\"\n    min: 1\n  - name: \"application\"\n    type: \"string\"\n    prompt: \"Entity\"\n    min: 1\n  - name: \"password\"\n    type: \"password\"\n    prompt: \"Pin\"\n    charset: \"0123456789\"\n    min: 1\n    max: 64\n    always_prompt: true\n  - name: \"comment\"\n    type: \"string\"\n`))\n\tw, err := New(ctx, s)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, w.Templates, \"no templates\")\n\trequire.Len(t, w.Templates, 1, \"wrong number of templates\")\n\n\tassert.Equal(t, \"pin\", w.Templates[0].Prefix, \"wrong prefix\")\n\tassert.Equal(t, \"🧪 Creating numerical PIN\", w.Templates[0].Welcome, \"wrong welcome\")\n\tassert.Len(t, w.Templates[0].Attributes, 4, \"wrong number of attributes\")\n\tassert.Equal(t, \"string\", w.Templates[0].Attributes[0].Type, \"wrong type\")\n\tassert.Equal(t, \"Authority\", w.Templates[0].Attributes[0].Prompt, \"wrong prompt\")\n\tassert.Equal(t, 1, w.Templates[0].Attributes[0].Min, \"wrong min\")\n\tassert.Equal(t, 0, w.Templates[0].Attributes[0].Max, \"wrong max\")\n\tassert.Equal(t, \"string\", w.Templates[0].Attributes[1].Type, \"wrong type\")\n\tassert.Equal(t, \"Entity\", w.Templates[0].Attributes[1].Prompt, \"wrong prompt\")\n\tassert.Equal(t, 1, w.Templates[0].Attributes[1].Min, \"wrong min\")\n\tassert.Equal(t, \"password\", w.Templates[0].Attributes[2].Type, \"wrong type\")\n\tassert.True(t, w.Templates[0].Attributes[2].AlwaysPrompt, \"wrong always_prompt\")\n}\n"
  },
  {
    "path": "internal/cui/actions.go",
    "content": "package cui\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Action is a action which can be selected.\ntype Action struct {\n\tName string\n\tFn   func(context.Context, *cli.Context) error\n}\n\n// Actions is a list of actions.\ntype Actions []Action\n\n// Selection return the list of actions.\nfunc (ca Actions) Selection() []string {\n\tkeys := make([]string, 0, len(ca))\n\tfor _, a := range ca {\n\t\tkeys = append(keys, a.Name)\n\t}\n\n\treturn keys\n}\n\n// Run executes the selected action.\nfunc (ca Actions) Run(ctx context.Context, c *cli.Context, i int) error {\n\tif len(ca) < i || i >= len(ca) {\n\t\treturn errors.New(\"action not found\")\n\t}\n\tif ca[i].Fn == nil {\n\t\treturn errors.New(\"action invalid\")\n\t}\n\n\treturn ca[i].Fn(ctx, c)\n}\n"
  },
  {
    "path": "internal/cui/actions_test.go",
    "content": "package cui\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestCreateActions(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tcas := Actions{\n\t\t{\n\t\t\tName: \"foo\",\n\t\t},\n\t\t{\n\t\t\tName: \"bar\",\n\t\t\tFn: func(context.Context, *cli.Context) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t}\n\tassert.Equal(t, []string{\"foo\", \"bar\"}, cas.Selection())\n\trequire.Error(t, cas.Run(ctx, nil, 0))\n\trequire.NoError(t, cas.Run(ctx, nil, 1))\n\trequire.Error(t, cas.Run(ctx, nil, 2))\n\trequire.Error(t, cas.Run(ctx, nil, 66))\n}\n"
  },
  {
    "path": "internal/cui/cui.go",
    "content": "// Package cui provides a simple command line user interface\n// for gopass. It is used to interact with the user.\npackage cui\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\n// GetSelection show a navigable multiple-choice list to the user\n// and returns the selected entry along with the action.\nfunc GetSelection(ctx context.Context, prompt string, choices []string) (string, int) {\n\tif ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) {\n\t\treturn \"impossible\", 0\n\t}\n\n\tfor i, c := range choices {\n\t\tfmt.Print(color.GreenString(\"[%  d]\", i))\n\t\tfmt.Printf(\" %s\\n\", c)\n\t}\n\tfmt.Println()\n\tvar i int\n\tfor {\n\t\tvar err error\n\t\ti, err = termio.AskForInt(ctx, prompt, 0)\n\t\tif err == nil && i < len(choices) {\n\t\t\tbreak\n\t\t}\n\t\tif errors.Is(err, termio.ErrAborted) {\n\t\t\treturn \"aborted\", 0\n\t\t}\n\t\tif err != nil {\n\t\t\tfmt.Println(err.Error())\n\t\t}\n\t}\n\tfmt.Println(i)\n\n\treturn \"default\", i\n}\n"
  },
  {
    "path": "internal/cui/cui_test.go",
    "content": "package cui\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetSelection(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\tact, sel := GetSelection(ctx, \"foo\", []string{\"foo\", \"bar\"})\n\tassert.Equal(t, \"impossible\", act)\n\tassert.Equal(t, 0, sel)\n}\n"
  },
  {
    "path": "internal/cui/recipients.go",
    "content": "package cui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\nvar (\n\t// Stdin is exported for tests.\n\tStdin io.Reader = os.Stdin\n\t// Stdout is exported for tests.\n\tStdout io.Writer = os.Stdout\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n)\n\nconst (\n\tmaxTries = 42\n)\n\n// AskForPrivateKey prompts the user to select from a list of private keys.\nfunc AskForPrivateKey(ctx context.Context, crypto backend.Crypto, prompt string) (string, error) {\n\tif crypto == nil {\n\t\treturn \"\", fmt.Errorf(\"can not select private key without valid crypto backend\")\n\t}\n\n\tkl, err := crypto.ListIdentities(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(kl) < 1 {\n\t\treturn \"\", fmt.Errorf(\"no useable private keys found for %s. make sure you have valid private keys with sufficient trust\", crypto.Name())\n\t}\n\n\t// shortcut: If there is only one key, use it\n\tif len(kl) == 1 {\n\t\treturn kl[0], nil\n\t}\n\n\tfmtStr := \"[%\" + strconv.Itoa((len(kl)/10)+1) + \"d] %s - %s\\n\"\n\tfor range maxTries {\n\t\tif !ctxutil.IsTerminal(ctx) || !ctxutil.IsInteractive(ctx) {\n\t\t\treturn kl[0], nil\n\t\t}\n\t\t// check for context cancelation\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", fmt.Errorf(\"user aborted\")\n\t\tdefault:\n\t\t}\n\n\t\tfmt.Fprintln(Stdout, prompt)\n\t\tfor i, k := range kl {\n\t\t\tfmt.Fprintf(Stdout, fmtStr, i, crypto.Name(), crypto.FormatKey(ctx, k, \"\"))\n\t\t}\n\n\t\tiv, err := termio.AskForInt(ctx, fmt.Sprintf(\"Please enter the number of a key (0-%d, [q]uit)\", len(kl)-1), 0)\n\t\tif err != nil {\n\t\t\tif err.Error() == \"user aborted\" {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif iv >= 0 && iv < len(kl) {\n\t\t\treturn kl[iv], nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no valid user input\")\n}\n\n// AskForGitConfigUser will iterate over GPG private key identities and prompt\n// the user for selecting one identity whose name and email address will be used as\n// git config user.name and git config user.email, respectively.\n// On error or no selection, name and email will be empty.\n//\n// If s.isTerm is false (i.e., the user cannot be prompted), however,\n// the first identity's name/email pair found is returned.\nfunc AskForGitConfigUser(ctx context.Context, crypto backend.Crypto) (string, string, error) {\n\tvar useCurrent bool\n\n\tif crypto == nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"crypto not available\")\n\t}\n\n\tif crypto.Name() == \"age\" {\n\t\tdebug.Log(\"skipping git config user prompt for non-gpg backend %s\", crypto.Name())\n\n\t\treturn \"\", \"\", nil\n\t}\n\n\tkeyList, err := crypto.ListIdentities(ctx)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tif len(keyList) < 1 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no usable private keys found\")\n\t}\n\n\tfor _, key := range keyList {\n\t\t// check for context cancelation\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", \"\", fmt.Errorf(\"user aborted\")\n\t\tdefault:\n\t\t}\n\n\t\tname := crypto.FormatKey(ctx, key, \"{{ .Identity.Name }}\")\n\t\temail := crypto.FormatKey(ctx, key, \"{{ .Identity.Email }}\")\n\n\t\tif name == \"\" && email == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tuseCurrent, err = termio.AskForBool(\n\t\t\tctx,\n\t\t\tfmt.Sprintf(\"Use %s (%s) for password store git config?\", name, email),\n\t\t\ttrue,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\tif useCurrent {\n\t\t\treturn name, email, nil\n\t\t}\n\t}\n\n\treturn \"\", \"\", nil\n}\n\ntype mountPointer interface {\n\tMountPoints() []string\n}\n\nfunc sorted(s []string) []string {\n\tsort.Strings(s)\n\n\treturn s\n}\n\n// AskForStore shows a store / mount point selection.\nfunc AskForStore(ctx context.Context, s mountPointer) string {\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn \"\"\n\t}\n\n\tstores := []string{\"<root>\"}\n\tstores = append(stores, sorted(s.MountPoints())...)\n\tif len(stores) < 2 {\n\t\treturn \"\"\n\t}\n\n\tact, sel := GetSelection(ctx, \"Please select the store you would like to use\", stores)\n\tswitch act {\n\tcase \"default\":\n\t\tfallthrough\n\tcase \"show\":\n\t\tstore := stores[sel]\n\t\tif store == \"<root>\" {\n\t\t\tstore = \"\"\n\t\t}\n\n\t\treturn store\n\tdefault:\n\t\treturn \"\" // root store\n\t}\n}\n"
  },
  {
    "path": "internal/cui/recipients_test.go",
    "content": "package cui\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAskForPrivateKey(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tStdout = buf\n\tdefer func() {\n\t\tStdout = os.Stdout\n\t}()\n\n\tctx := config.NewContextInMemory()\n\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tkey, err := AskForPrivateKey(ctx, plain.New(), \"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"0xDEADBEEF\", key)\n\tbuf.Reset()\n}\n\nfunc TestAskForGitConfigUser(t *testing.T) {\n\t// necessary for setting up the env\n\tu := gptest.NewGUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, true)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\t_, _, err := AskForGitConfigUser(ctx, plain.New())\n\trequire.NoError(t, err)\n}\n\ntype fakeMountPointer []string\n\nfunc (f fakeMountPointer) MountPoints() []string {\n\treturn f\n}\n\nfunc TestAskForStore(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\t// test non-interactive\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tassert.Empty(t, AskForStore(ctx, fakeMountPointer{\"foo\", \"bar\"}))\n\n\t// test interactive\n\tctx = ctxutil.WithInteractive(ctx, true)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tassert.Empty(t, AskForStore(ctx, fakeMountPointer{\"foo\", \"bar\"}))\n\n\t// test zero mps\n\tassert.Empty(t, AskForStore(ctx, fakeMountPointer{}))\n\n\t// test one mp\n\tassert.Empty(t, AskForStore(ctx, fakeMountPointer{\"foo\"}))\n\n\t// test two mps\n\tassert.Empty(t, AskForStore(ctx, fakeMountPointer{\"foo\", \"bar\"}))\n}\n\nfunc TestSorted(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, []string{\"a\", \"b\", \"c\"}, sorted([]string{\"c\", \"a\", \"b\"}))\n}\n"
  },
  {
    "path": "internal/diff/diff.go",
    "content": "// Package diff implements diffing of two lists.\npackage diff\n\n// Stat returnes the number of items added to and removed from the first to\n// the second list.\nfunc Stat[K comparable](l, r []K) (int, int) {\n\tadded, removed := List(l, r)\n\n\treturn len(added), len(removed)\n}\n\n// List returns two lists, the first one contains the items that were added from left\n// to right, the second one contains the items that were removed from left to right.\nfunc List[K comparable](l, r []K) ([]K, []K) {\n\tml := listToMap(l)\n\tmr := listToMap(r)\n\n\tvar added []K\n\n\tfor k := range mr {\n\t\tif _, found := ml[k]; !found {\n\t\t\tadded = append(added, k)\n\t\t}\n\t}\n\n\tvar removed []K\n\n\tfor k := range ml {\n\t\tif _, found := mr[k]; !found {\n\t\t\tremoved = append(removed, k)\n\t\t}\n\t}\n\n\treturn added, removed\n}\n\nfunc listToMap[K comparable](l []K) map[K]struct{} {\n\tm := make(map[K]struct{}, len(l))\n\tfor _, e := range l {\n\t\tm[e] = struct{}{}\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/diff/diff_test.go",
    "content": "package diff\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStat(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname    string\n\t\told     []string\n\t\tnew     []string\n\t\tadded   int\n\t\tremoved int\n\t}{\n\t\t{\n\t\t\tname:  \"added\",\n\t\t\told:   []string{\"foo\", \"bar\"},\n\t\t\tnew:   []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tadded: 1,\n\t\t},\n\t\t{\n\t\t\tname:    \"removed\",\n\t\t\told:     []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tnew:     []string{\"foo\", \"bar\"},\n\t\t\tremoved: 1,\n\t\t},\n\t\t{\n\t\t\tname:    \"added and removed\",\n\t\t\told:     []string{\"foo\", \"baz\"},\n\t\t\tnew:     []string{\"foo\", \"bar\"},\n\t\t\tadded:   1,\n\t\t\tremoved: 1,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ta, r := Stat(tc.old, tc.new)\n\t\t\tassert.Equal(t, tc.added, a)\n\t\t\tassert.Equal(t, tc.removed, r)\n\t\t})\n\t}\n}\n\nfunc TestList(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\told     []string\n\t\tnew     []string\n\t\tadded   []string\n\t\tremoved []string\n\t}{\n\t\t{\n\t\t\told:     []string{\"foo\", \"bar\"},\n\t\t\tnew:     []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tadded:   []string{\"baz\"},\n\t\t\tremoved: nil,\n\t\t},\n\t\t{\n\t\t\told:     []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tnew:     []string{\"foo\", \"bar\"},\n\t\t\tadded:   nil,\n\t\t\tremoved: []string{\"baz\"},\n\t\t},\n\t\t{\n\t\t\told:     []string{\"foo\", \"baz\"},\n\t\t\tnew:     []string{\"foo\", \"bar\"},\n\t\t\tadded:   []string{\"bar\"},\n\t\t\tremoved: []string{\"baz\"},\n\t\t},\n\t} {\n\t\ta, r := List(tc.old, tc.new)\n\t\tassert.Equal(t, tc.added, a)\n\t\tassert.Equal(t, tc.removed, r)\n\t}\n}\n\nfunc TestListToMap(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tl []string\n\t\tm map[string]struct{}\n\t}{\n\t\t{\n\t\t\tl: []string{\"foo\", \"bar\"},\n\t\t\tm: map[string]struct{}{\n\t\t\t\t\"foo\": {},\n\t\t\t\t\"bar\": {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tl: []string{\"foo\", \"bar\", \"baz\", \"baz\"},\n\t\t\tm: map[string]struct{}{\n\t\t\t\t\"foo\": {},\n\t\t\t\t\"bar\": {},\n\t\t\t\t\"baz\": {},\n\t\t\t},\n\t\t},\n\t} {\n\t\tm := listToMap(tc.l)\n\t\tassert.Equal(t, tc.m, m)\n\t}\n}\n"
  },
  {
    "path": "internal/editor/edit_linux.go",
    "content": "//go:build linux\n\npackage editor\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Path return the name/path of the preferred editor.\nfunc Path(c *cli.Context) string {\n\tif c != nil {\n\t\tif ed := c.String(\"editor\"); ed != \"\" {\n\t\t\tdebug.Log(\"Using editor from command line: %s\", ed)\n\n\t\t\treturn ed\n\t\t}\n\t}\n\tif ed := config.String(c.Context, \"edit.editor\"); ed != \"\" {\n\t\tdebug.Log(\"Using editor from config: %s\", ed)\n\n\t\treturn ed\n\t}\n\tif ed := os.Getenv(\"EDITOR\"); ed != \"\" {\n\t\tdebug.Log(\"Using editor from $EDITOR: %s\", ed)\n\n\t\treturn ed\n\t}\n\tif p, err := exec.LookPath(\"editor\"); err == nil {\n\t\tdebug.Log(\"Using editor from $PATH: %s\", p)\n\n\t\treturn p\n\t}\n\t// if neither EDITOR is set nor \"editor\" available we'll just assume that vi\n\t// is installed. If this fails the user will have to set `$EDITOR`.\n\tdebug.Log(\"Using default editor: %s\", \"vi\")\n\n\treturn \"vi\"\n}\n"
  },
  {
    "path": "internal/editor/edit_others.go",
    "content": "//go:build !linux && !windows\n\npackage editor\n\nimport (\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Path return the name/path of the preferred editor.\nfunc Path(c *cli.Context) string {\n\tif c != nil {\n\t\tif ed := c.String(\"editor\"); ed != \"\" {\n\t\t\treturn ed\n\t\t}\n\t}\n\n\tif ed := config.String(c.Context, \"edit.editor\"); ed != \"\" {\n\t\treturn ed\n\t}\n\n\tif ed := os.Getenv(\"EDITOR\"); ed != \"\" {\n\t\treturn ed\n\t}\n\n\t// given, this is a very opinionated default, but this should be available\n\t// on virtually any UNIX system and the user can still set EDITOR to get\n\t// his favorite one\n\treturn \"vi\"\n}\n"
  },
  {
    "path": "internal/editor/edit_others_test.go",
    "content": "//go:build !windows\n\npackage editor\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestEditor(t *testing.T) {\n\t// necessary for setting up the env\n\tu := gptest.NewGUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tctx := config.NewContextInMemory()\n\ttouch, err := exec.LookPath(\"touch\")\n\trequire.NoError(t, err, os.Getenv(\"PATH\"))\n\n\twant := \"foobar\"\n\tout, err := Invoke(ctx, touch, []byte(want))\n\trequire.NoError(t, err)\n\tif string(out) != want {\n\t\tt.Errorf(\"%q != %q\", string(out), want)\n\t}\n}\n\nfunc TestGetEditor(t *testing.T) {\n\tapp := cli.NewApp()\n\n\tt.Setenv(\"EDITOR\", \"\")\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tt.Run(\"--editor=fooed\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tsf := cli.StringFlag{\n\t\t\tName:  \"editor\",\n\t\t\tUsage: \"editor\",\n\t\t}\n\t\trequire.NoError(t, sf.Apply(fs))\n\t\trequire.NoError(t, fs.Parse([]string{\"--editor\", \"fooed\"}))\n\t\tc := cli.NewContext(app, fs, nil)\n\n\t\tassert.Equal(t, \"fooed\", Path(c))\n\t})\n\n\tt.Run(\"/usr/bin/editor\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tpathed, err := exec.LookPath(\"editor\")\n\t\tif err == nil {\n\t\t\tassert.Equal(t, pathed, Path(c))\n\t\t}\n\t})\n\n\tt.Run(\"EDITOR\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tt.Setenv(\"EDITOR\", \"fooenv\")\n\t\tassert.Equal(t, \"fooenv\", Path(c))\n\t})\n\n\tt.Run(\"vi\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tt.Setenv(\"PATH\", \"/tmp\")\n\t\tassert.Equal(t, \"vi\", Path(c))\n\t})\n}\n"
  },
  {
    "path": "internal/editor/edit_test.go",
    "content": "package editor\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEdit(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t_, err := Invoke(ctx, \"true\", []byte{})\n\trequire.Error(t, err)\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/editor/edit_windows.go",
    "content": "//go:build windows\n\npackage editor\n\nimport (\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Path return the name/path of the preferred editor\nfunc Path(c *cli.Context) string {\n\tif c != nil {\n\t\tif ed := c.String(\"editor\"); ed != \"\" {\n\t\t\treturn ed\n\t\t}\n\t}\n\tif ed := config.String(c.Context, \"edit.editor\"); ed != \"\" {\n\t\treturn ed\n\t}\n\tif ed := os.Getenv(\"EDITOR\"); ed != \"\" {\n\t\treturn ed\n\t}\n\treturn \"notepad.exe\"\n}\n"
  },
  {
    "path": "internal/editor/edit_windows_test.go",
    "content": "package editor\n\nimport (\n\t\"flag\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestEditor(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tassert.NotNil(t, u)\n\n\tctx := config.NewContextInMemory()\n\ttouch, err := exec.LookPath(\"rundll32\")\n\trequire.NoError(t, err)\n\n\twant := \"foobar\"\n\tout, err := Invoke(ctx, touch, []byte(want))\n\trequire.NoError(t, err)\n\tif string(out) != want {\n\t\tt.Errorf(\"%q != %q\", string(out), want)\n\t}\n}\n\nfunc TestGetEditor(t *testing.T) {\n\tapp := cli.NewApp()\n\n\tt.Run(\"--editor=fooed\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tsf := cli.StringFlag{\n\t\t\tName:  \"editor\",\n\t\t\tUsage: \"editor\",\n\t\t}\n\t\trequire.NoError(t, sf.Apply(fs))\n\t\trequire.NoError(t, fs.Parse([]string{\"--editor\", \"fooed\"}))\n\t\tc := cli.NewContext(app, fs, nil)\n\n\t\tassert.Equal(t, \"fooed\", Path(c))\n\t})\n\n\tt.Run(\"/usr/bin/editor\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tpathed, err := exec.LookPath(\"editor\")\n\t\tif err == nil {\n\t\t\tassert.Equal(t, pathed, Path(c))\n\t\t}\n\t})\n\n\tt.Run(\"EDITOR\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tt.Setenv(\"EDITOR\", \"fooenv\")\n\t\tassert.Equal(t, \"fooenv\", Path(c))\n\t})\n\n\tt.Run(\"vi\", func(t *testing.T) {\n\t\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\t\tc := cli.NewContext(app, fs, nil)\n\t\tt.Setenv(\"PATH\", \"/tmp\")\n\t\tassert.Equal(t, \"notepad.exe\", Path(c))\n\t})\n}\n"
  },
  {
    "path": "internal/editor/editor.go",
    "content": "// Package editor provides a simple wrapper around the EDITOR environment variable.\npackage editor\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/tempfile\"\n\tshellquote \"github.com/kballard/go-shellquote\"\n)\n\nvar (\n\t// Stdin is exported for tests.\n\tStdin io.Reader = os.Stdin\n\t// Stdout is exported for tests.\n\tStdout io.Writer = os.Stdout\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n)\n\n// Invoke will start the given editor and return the content.\nfunc Invoke(ctx context.Context, editor string, content []byte) ([]byte, error) {\n\tif !ctxutil.IsTerminal(ctx) {\n\t\treturn nil, fmt.Errorf(\"need terminal\")\n\t}\n\n\ttmpfile, err := tempfile.New(ctx, \"gopass-edit\")\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to create tmpfile %s: %w\", editor, err)\n\t}\n\n\tdefer func() {\n\t\tif err := tmpfile.Remove(ctx); err != nil {\n\t\t\tcolor.Red(\"Failed to remove tempfile at %s: %s\", tmpfile.Name(), err)\n\t\t}\n\t}()\n\n\tif _, err := tmpfile.Write(content); err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to write tmpfile to start with %s %v: %w\", editor, tmpfile.Name(), err)\n\t}\n\n\tif err := tmpfile.Close(); err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to close tmpfile to start with %s %v: %w\", editor, tmpfile.Name(), err)\n\t}\n\n\targs := make([]string, 0, 4)\n\tif runtime.GOOS != \"windows\" {\n\t\tcmdArgs, err := shellquote.Split(editor)\n\t\tif err != nil {\n\t\t\treturn []byte{}, fmt.Errorf(\"failed to parse EDITOR command `%s`\", editor)\n\t\t}\n\n\t\teditor = cmdArgs[0]\n\t\targs = append(args, cmdArgs[1:]...)\n\t\targs = append(args, vimOptions(resolveEditor(editor))...)\n\t}\n\n\targs = append(args, tmpfile.Name())\n\n\tcmd := exec.Command(editor, args...)\n\tcmd.Stdin = Stdin\n\tcmd.Stdout = Stdout\n\tcmd.Stderr = Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\tdebug.Log(\"cmd: %s %+v - error: %+v\", cmd.Path, cmd.Args, err)\n\n\t\treturn []byte{}, fmt.Errorf(\"failed to run %s with %s file: %w\", editor, tmpfile.Name(), err)\n\t}\n\n\tnContent, err := os.ReadFile(tmpfile.Name())\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to read from tmpfile: %w\", err)\n\t}\n\n\t// enforce unix line endings in the password store.\n\tnContent = bytes.ReplaceAll(nContent, []byte(\"\\r\\n\"), []byte(\"\\n\"))\n\tnContent = bytes.ReplaceAll(nContent, []byte(\"\\r\"), []byte(\"\\n\"))\n\n\treturn nContent, nil\n}\n\nfunc vimOptions(editor string) []string {\n\tif editor != \"vi\" && editor != \"vim\" && editor != \"neovim\" {\n\t\tdebug.Log(\"Editor %s is not known to be vim compatible\", editor)\n\n\t\treturn []string{}\n\t}\n\n\tif !isVim(editor) {\n\t\tdebug.Log(\"Editor %s is not known to be vim compatible\", editor)\n\n\t\treturn []string{}\n\t}\n\n\tpath := \"/dev/shm/gopass*\"\n\tif runtime.GOOS == \"darwin\" {\n\t\tpath = \"/private/**/gopass**\"\n\t}\n\tviminfo := `viminfo=\"\"`\n\tif editor == \"neovim\" {\n\t\tviminfo = `shada=\"\"`\n\t}\n\n\targs := []string{\n\t\t\"-c\",\n\t\tfmt.Sprintf(\"autocmd BufNewFile,BufRead %s setlocal noswapfile nobackup noundofile %s\", path, viminfo),\n\t}\n\targs = append(args, \"-i\", \"NONE\") // disable viminfo\n\targs = append(args, \"-n\")         // disable swap\n\n\treturn args\n}\n\n// isVim tries to identify the vi variant as vim compatible or not.\nfunc isVim(editor string) bool {\n\tif editor == \"neovim\" {\n\t\treturn true\n\t}\n\n\tcmd := exec.Command(editor, \"--version\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tdebug.Log(\"failed to check %s --version: %s\", cmd.Path, err)\n\n\t\treturn false\n\t}\n\n\tdebug.Log(\"%s --version: %s\", cmd.Path, string(out))\n\n\treturn strings.Contains(string(out), \"VIM - Vi IMproved\")\n}\n\n// resolveEditor tries to resolve the final link destination of the editor name given\n// and then extract the binary file name from the path. In practice the actual editor\n// is often hidden behing several layers of indirection and we want to get an idea\n// which options might work.\nfunc resolveEditor(editor string) string {\n\tpath, err := exec.LookPath(editor)\n\tif err != nil {\n\t\tdebug.Log(\"failed to look up editor binary: %s\", err)\n\n\t\treturn editor\n\t}\n\n\tfor {\n\t\tfi, err := os.Stat(path)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to resolve %s: %s\", path, err)\n\n\t\t\treturn editor\n\t\t}\n\n\t\tif fi.Mode()&fs.ModeSymlink != fs.ModeSymlink {\n\t\t\t// not a symlink\n\t\t\tbreak\n\t\t}\n\n\t\tpath, err = os.Readlink(path)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to read link %s: %s\", path, err)\n\t\t}\n\t}\n\n\t// return the binary name only\n\treturn filepath.Base(path)\n}\n"
  },
  {
    "path": "internal/env/doc.go",
    "content": "// Package env provides a way to validate the environment\n// and the configuration of gopass.\n\npackage env\n"
  },
  {
    "path": "internal/env/env_darwin.go",
    "content": "//go:build darwin\n\npackage env\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nvar (\n\t// Stdin is exported for tests.\n\tStdin io.Reader = os.Stdin\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n)\n\n// Check validates the runtime environment on MacOS.\n// It checks if the keychain is used.\nfunc Check(ctx context.Context) (string, error) {\n\tbuf := &bytes.Buffer{}\n\n\tcmd := exec.CommandContext(ctx, \"defaults\", \"read\", \"org.gpgtools.common\", \"UseKeychain\")\n\tcmd.Stdin = Stdin\n\tcmd.Stdout = buf\n\tcmd.Stderr = Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"`default read org.gpgtools.common UseKeychain` failed: %w\", err)\n\t}\n\n\t// if the keychain is not used, we can skip the rest\n\tif strings.ToUpper(strings.TrimSpace(buf.String())) == \"NO\" {\n\t\treturn \"\", nil\n\t}\n\n\t// gpg uses the keychain to store the passphrase, warn once in a while that users\n\t// might want to change that because it's not secure.\n\treturn \"pinentry-mac will use the MacOS Keychain to store your passphrase indefinitely. Consider running 'defaults write org.gpgtools.common UseKeychain NO' to disable that.\", nil\n}\n"
  },
  {
    "path": "internal/env/env_others.go",
    "content": "//go:build !darwin\n\npackage env\n\nimport \"context\"\n\n// Check does nothing on these OSes, yet.\nfunc Check(ctx context.Context) (string, error) {\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "internal/hashsum/hashsums.go",
    "content": "// Package hashsum provides hash functions for various algorithms.\npackage hashsum\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/hex\"\n\n\t\"github.com/zeebo/blake3\"\n)\n\n// MD5Hex returns the MD5 hash of the input string.\n// Note: MD5 is cryptographically broken and should not be used for security purposes.\nfunc MD5Hex(in string) string {\n\thash := md5.Sum([]byte(in))\n\n\treturn hex.EncodeToString(hash[:])\n}\n\n// SHA1Hex returns the SHA-1 hash of the input string.\n// Note: SHA-1 is cryptographically broken and should not be used for security purposes.\nfunc SHA1Hex(in string) string {\n\thash := sha1.Sum([]byte(in))\n\n\treturn hex.EncodeToString(hash[:])\n}\n\n// SHA256Hex returns the SHA-256 hash of the input string.\nfunc SHA256Hex(in string) string {\n\thash := sha256.Sum256([]byte(in))\n\n\treturn hex.EncodeToString(hash[:])\n}\n\n// SHA512Hex returns the SHA-512 hash of the input string.\nfunc SHA512Hex(in string) string {\n\thash := sha512.Sum512([]byte(in))\n\n\treturn hex.EncodeToString(hash[:])\n}\n\n// Blake3Hex returns the BLAKE3 hash of the input string.\nfunc Blake3Hex(in string) string {\n\thash := blake3.Sum256([]byte(in))\n\n\treturn hex.EncodeToString(hash[:])\n}\n"
  },
  {
    "path": "internal/hashsum/hashsums_test.go",
    "content": "package hashsum\n\nimport (\n\t\"testing\"\n)\n\nfunc TestMD5Hex(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"test\"\n\twant := \"098f6bcd4621d373cade4e832627b4f6\"\n\tgot := MD5Hex(in)\n\tif got != want {\n\t\tt.Errorf(\"MD5Hex(%q) = %q, want %q\", in, got, want)\n\t}\n}\n\nfunc TestSHA1Hex(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"test\"\n\twant := \"a94a8fe5ccb19ba61c4c0873d391e987982fbbd3\"\n\tgot := SHA1Hex(in)\n\tif got != want {\n\t\tt.Errorf(\"SHA1Hex(%q) = %q, want %q\", in, got, want)\n\t}\n}\n\nfunc TestSHA256Hex(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"test\"\n\twant := \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\"\n\tgot := SHA256Hex(in)\n\tif got != want {\n\t\tt.Errorf(\"SHA256Hex(%q) = %q, want %q\", in, got, want)\n\t}\n}\n\nfunc TestSHA512Hex(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"test\"\n\twant := \"ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff\"\n\tgot := SHA512Hex(in)\n\tif got != want {\n\t\tt.Errorf(\"SHA512Hex(%q) = %q, want %q\", in, got, want)\n\t}\n}\n\nfunc TestBlake3Hex(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"test\"\n\twant := \"4878ca0425c739fa427f7eda20fe845f6b2e46ba5fe2a14df5b1e32f50603215\"\n\tgot := Blake3Hex(in)\n\tif got != want {\n\t\tt.Errorf(\"Blake3Hex(%q) = %q, want %q\", in, got, want)\n\t}\n}\n"
  },
  {
    "path": "internal/hook/hook.go",
    "content": "// Package hook provides a flexible hook system for gopass.\npackage hook\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/kballard/go-shellquote\"\n)\n\n// Stderr is exported for tests.\nvar Stderr io.Writer = os.Stderr\n\ntype subStoreGetter interface {\n\tGetSubStore(string) (*leaf.Store, error)\n\tMountPoint(string) string\n}\n\nfunc InvokeRoot(ctx context.Context, hookName, secName string, s subStoreGetter, hookArgs ...string) error {\n\tsub, err := s.GetSubStore(s.MountPoint(secName))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn Invoke(ctx, hookName, sub.Storage().Path(), hookArgs...)\n}\n\nfunc Invoke(ctx context.Context, hook, dir string, hookArgs ...string) error {\n\tif true {\n\t\t// TODO(GH-2546) disabled until further discussion, cf. https://www.cvedetails.com/cve/CVE-2023-24055/\n\n\t\treturn nil\n\t}\n\n\thCmd := strings.TrimSpace(config.String(ctx, hook))\n\tif hCmd == \"\" {\n\t\treturn nil\n\t}\n\tif sv := os.Getenv(\"GOPASS_HOOK\"); sv == \"1\" {\n\t\tdebug.Log(\"GOPASS_HOOK=1, skipping reentrant hook execution\")\n\n\t\treturn nil\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, time.Minute)\n\tdefer cancel()\n\n\targs := make([]string, 0, 4)\n\tif runtime.GOOS != \"windows\" {\n\t\tcmdArgs, err := shellquote.Split(hCmd)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse hook command `%s`\", hCmd)\n\t\t}\n\n\t\thook = cmdArgs[0]\n\t\targs = append(args, cmdArgs[1:]...)\n\t}\n\n\tif len(hook) > 2 && hook[:2] == \"~/\" {\n\t\thook = appdir.UserHome() + hook[1:]\n\t}\n\n\targs = append(args, hookArgs...)\n\n\tcmd := exec.CommandContext(ctx, hook, args...)\n\tcmd.Stdin = nil\n\tcmd.Stdout = nil\n\tcmd.Stderr = Stderr\n\tcmd.Env = os.Environ()\n\tcmd.Env = append(cmd.Env, \"GOPASS_HOOK=1\")\n\tcmd.Dir = dir\n\n\tdebug.Log(\"running hook %s with: %s %+v\", hook, cmd.Path, cmd.Args)\n\n\tif err := cmd.Run(); err != nil {\n\t\tdebug.Log(\"cmd: %s %+v - error: %+v\", cmd.Path, cmd.Args, err)\n\n\t\treturn fmt.Errorf(\"failed to run %s %v: %w\", hook, args, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/notify/doc.go",
    "content": "// Package notify provides a notification system for gopass.\n\npackage notify\n"
  },
  {
    "path": "internal/notify/icon.go",
    "content": "package notify\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\nfunc iconURI(ctx context.Context) string {\n\tif config.Bool(ctx, \"notify.disable-icon\") {\n\t\treturn \"\"\n\t}\n\n\tuserCache := appdir.UserCache()\n\tif !fsutil.IsDir(userCache) {\n\t\tif err := os.MkdirAll(userCache, 0o755); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\n\ticonFN := filepath.Join(userCache, \"gopass-logo-small.png\")\n\tif !fsutil.IsFile(iconFN) {\n\t\tfh, err := os.OpenFile(iconFN, os.O_WRONLY|os.O_CREATE, 0o644)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = fh.Close()\n\t\t}()\n\n\t\tif err = bindataWrite(assetLogoSmallPng(), fh); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tif err = fh.Close(); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\n\tif fsutil.IsFile(iconFN) {\n\t\treturn \"file://\" + iconFN\n\t}\n\n\treturn \"\"\n}\n\nfunc bindataWrite(in []byte, out io.Writer) error {\n\tgz, err := gzip.NewReader(bytes.NewBuffer(in))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = gz.Close()\n\t}()\n\n\t_, err = io.Copy(out, gz)\n\n\treturn err\n}\n\nfunc assetLogoSmallPng() []byte {\n\treturn []byte{\n\t\t0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0x00, 0x56,\n\t\t0x1d, 0xa9, 0xe2, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00,\n\t\t0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x33, 0x00,\n\t\t0x00, 0x00, 0x50, 0x08, 0x06, 0x00, 0x00, 0x00, 0xb4, 0xc1, 0x4d, 0xde,\n\t\t0x00, 0x00, 0x00, 0x06, 0x62, 0x4b, 0x47, 0x44, 0x00, 0xff, 0x00, 0xff,\n\t\t0x00, 0xff, 0xa0, 0xbd, 0xa7, 0x93, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48,\n\t\t0x59, 0x73, 0x00, 0x00, 0x06, 0x31, 0x00, 0x00, 0x06, 0x31, 0x01, 0x84,\n\t\t0x43, 0x8c, 0x96, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07,\n\t\t0xe1, 0x0a, 0x1e, 0x0e, 0x2f, 0x27, 0x56, 0x32, 0xfa, 0x86, 0x00, 0x00,\n\t\t0x1c, 0xe3, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0xed, 0x7b, 0x79, 0x74,\n\t\t0x55, 0xd5, 0xd9, 0xfe, 0xb3, 0xf7, 0x3e, 0xd3, 0x9d, 0x72, 0x6f, 0xe6,\n\t\t0x81, 0x24, 0x40, 0x98, 0x02, 0x04, 0x02, 0x18, 0x10, 0x05, 0x04, 0x11,\n\t\t0x41, 0x45, 0x41, 0xc5, 0x01, 0x50, 0x2b, 0xd6, 0x01, 0xad, 0x75, 0xa8,\n\t\t0xd6, 0xa1, 0xda, 0xe1, 0xab, 0xb5, 0x76, 0xf8, 0xac, 0x5a, 0x11, 0xd1,\n\t\t0x6a, 0x5b, 0x6b, 0x5b, 0x05, 0x9c, 0x10, 0x44, 0x41, 0x10, 0x07, 0x88,\n\t\t0xcc, 0x33, 0x09, 0x33, 0x84, 0x04, 0x02, 0x49, 0x48, 0x72, 0x73, 0x73,\n\t\t0x87, 0x33, 0xed, 0xbd, 0xbf, 0x3f, 0x42, 0x94, 0x21, 0x84, 0x80, 0xb6,\n\t\t0xbf, 0x7f, 0x7e, 0xef, 0x5a, 0x67, 0x65, 0xad, 0x7b, 0x4f, 0xf6, 0xd9,\n\t\t0xcf, 0xd9, 0xef, 0xf8, 0xbc, 0xef, 0x25, 0xf8, 0xcf, 0x09, 0x03, 0x40,\n\t\t0x8e, 0xbb, 0x00, 0x40, 0x1e, 0xbb, 0xc4, 0xb1, 0xeb, 0x7b, 0x15, 0xf2,\n\t\t0x3d, 0xaf, 0x97, 0x04, 0xa0, 0x73, 0x72, 0xb7, 0x1e, 0xb9, 0x29, 0xdd,\n\t\t0x7a, 0x14, 0xe8, 0x81, 0x60, 0x26, 0x18, 0x0b, 0x50, 0x4a, 0x75, 0x48,\n\t\t0x29, 0x85, 0x10, 0xa6, 0x14, 0x3c, 0x62, 0x36, 0x34, 0x54, 0xd7, 0xed,\n\t\t0x28, 0xdb, 0x1b, 0x3d, 0x7c, 0xa8, 0x1a, 0x40, 0x05, 0x00, 0xf3, 0xf8,\n\t\t0x45, 0x0a, 0xef, 0xb8, 0x03, 0x3b, 0x5e, 0x7f, 0x1d, 0x00, 0xba, 0x03,\n\t\t0x70, 0x8f, 0xdd, 0xf3, 0xdd, 0xc0, 0x5c, 0xbb, 0xb0, 0x14, 0xef, 0x8f,\n\t\t0x1f, 0x76, 0xc2, 0x67, 0x54, 0x55, 0x21, 0x1c, 0xe7, 0xe4, 0x5b, 0xd3,\n\t\t0x01, 0x4c, 0x2f, 0x9a, 0x32, 0x6d, 0x48, 0xde, 0x05, 0xc3, 0xfa, 0x18,\n\t\t0xa1, 0x94, 0x34, 0x4f, 0x4a, 0x8a, 0x57, 0xf5, 0x06, 0x54, 0xc2, 0x28,\n\t\t0x08, 0x21, 0x00, 0x24, 0xa4, 0x04, 0x04, 0x77, 0x61, 0x37, 0x37, 0x5b,\n\t\t0x89, 0xfa, 0xa3, 0x71, 0x33, 0xdc, 0x50, 0xb3, 0x7b, 0xd1, 0x82, 0xad,\n\t\t0xfb, 0x3e, 0xfd, 0xf8, 0x2b, 0x00, 0x33, 0x8f, 0x9d, 0x5a, 0xab, 0x8c,\n\t\t0x19, 0xf6, 0xd8, 0xaf, 0xfe, 0xac, 0x78, 0x3c, 0xd6, 0xca, 0x67, 0x9f,\n\t\t0x5e, 0x62, 0x47, 0xa3, 0x3f, 0x07, 0xe0, 0x9c, 0xe6, 0xf9, 0x1d, 0x3e,\n\t\t0x99, 0x61, 0xaa, 0xcf, 0xf7, 0x14, 0x00, 0xe6, 0xc4, 0x62, 0x77, 0x02,\n\t\t0xd8, 0x7d, 0x3c, 0x36, 0xaa, 0x69, 0x8f, 0xf5, 0x9a, 0x70, 0xdd, 0x23,\n\t\t0x17, 0x3c, 0xf8, 0x58, 0x92, 0x1e, 0x0c, 0x31, 0xa2, 0x30, 0x90, 0x6f,\n\t\t0x96, 0x25, 0x6d, 0x3f, 0x41, 0xca, 0x63, 0x7f, 0x04, 0x20, 0x25, 0x9a,\n\t\t0x0e, 0x54, 0xd8, 0x5f, 0x3e, 0xf5, 0x44, 0xd3, 0xc1, 0x55, 0x2b, 0x9e,\n\t\t0x04, 0xf0, 0x1a, 0x00, 0xb5, 0xf7, 0xa4, 0x29, 0xab, 0x2f, 0x7e, 0xea,\n\t\t0x8f, 0x03, 0x09, 0xa5, 0x88, 0xd7, 0xd6, 0x8a, 0xc5, 0x3f, 0xbd, 0x27,\n\t\t0x5c, 0xbd, 0x76, 0xd5, 0x30, 0x00, 0x3b, 0xce, 0xe5, 0x64, 0xd4, 0xe4,\n\t\t0x82, 0xee, 0xcf, 0xf6, 0x9b, 0x3a, 0xed, 0xfe, 0x41, 0x77, 0xdc, 0x0b,\n\t\t0xc1, 0x39, 0x3e, 0x7d, 0xf8, 0x1e, 0xb1, 0x63, 0xde, 0x3b, 0x7d, 0x00,\n\t\t0xec, 0xd6, 0xfc, 0xfe, 0xf1, 0x9d, 0x47, 0x8e, 0x79, 0xa9, 0xff, 0x4d,\n\t\t0xd3, 0xf2, 0xbb, 0x8e, 0x1e, 0x07, 0x33, 0x1c, 0x6e, 0xd9, 0xdc, 0xd9,\n\t\t0x8a, 0x94, 0x20, 0x8a, 0x02, 0x23, 0x98, 0x8c, 0x1d, 0x1f, 0xcc, 0x41,\n\t\t0xd9, 0x9c, 0x7f, 0x95, 0x1e, 0x58, 0xbe, 0xec, 0xe3, 0x2b, 0x5f, 0x79,\n\t\t0xf3, 0xf1, 0x2e, 0xa3, 0x2e, 0x0d, 0x58, 0xd1, 0x88, 0x60, 0xaa, 0x0a,\n\t\t0x6f, 0x5a, 0x06, 0x5d, 0xfc, 0xd0, 0x3d, 0xd8, 0xfa, 0xef, 0xbf, 0x5f,\n\t\t0x00, 0x60, 0xd5, 0xe9, 0x8c, 0xb4, 0x4d, 0xf1, 0x67, 0x66, 0xfd, 0x66,\n\t\t0xdc, 0xf3, 0xaf, 0xfe, 0x34, 0xbd, 0x6f, 0x91, 0x2d, 0x1c, 0x97, 0x68,\n\t\t0x01, 0x3f, 0xe9, 0x36, 0xe6, 0x0a, 0xb2, 0x67, 0xd1, 0x82, 0x91, 0xa9,\n\t\t0x3d, 0x0a, 0xf3, 0xce, 0x7f, 0xe0, 0xd1, 0x19, 0x25, 0xf7, 0x3c, 0x10,\n\t\t0x4a, 0x2e, 0xe8, 0x01, 0x2b, 0xd2, 0xf4, 0x1d, 0xac, 0x96, 0x00, 0x52,\n\t\t0xc2, 0x89, 0xc7, 0x90, 0xde, 0xbb, 0x08, 0xf9, 0x23, 0x46, 0xe5, 0x9b,\n\t\t0xe1, 0xc6, 0x51, 0x7d, 0x6e, 0xbc, 0x59, 0x3b, 0xb4, 0xaa, 0xd4, 0xde,\n\t\t0xb9, 0xe0, 0x3d, 0x2b, 0x98, 0xd7, 0x99, 0x30, 0x4d, 0x63, 0x5d, 0x46,\n\t\t0x8d, 0x91, 0xcd, 0xd5, 0x07, 0x83, 0x47, 0xb7, 0x97, 0x2d, 0x38, 0x66,\n\t\t0x4b, 0x1d, 0x02, 0x93, 0x51, 0x34, 0x75, 0xda, 0x53, 0x45, 0x93, 0x6f,\n\t\t0xc9, 0x6a, 0xd8, 0xb9, 0xc3, 0xda, 0xf2, 0xd6, 0xdf, 0x13, 0x81, 0xec,\n\t\t0x4e, 0xcc, 0x9b, 0x9a, 0xce, 0x52, 0xba, 0x75, 0x4f, 0x21, 0x0a, 0xbb,\n\t\t0xb0, 0xf8, 0xd6, 0x3b, 0x15, 0xc9, 0x39, 0x84, 0xe3, 0x1c, 0xb3, 0x89,\n\t\t0xef, 0xe8, 0x89, 0x08, 0x81, 0x70, 0x1d, 0x28, 0xba, 0x21, 0xb3, 0x06,\n\t\t0x96, 0xe0, 0xc8, 0x86, 0x75, 0x6e, 0xd5, 0x9a, 0x15, 0xee, 0x80, 0x69,\n\t\t0x77, 0x51, 0x7f, 0x66, 0x8e, 0x41, 0x28, 0x05, 0x77, 0x6c, 0x52, 0xb5,\n\t\t0xe2, 0xab, 0xad, 0xb5, 0x65, 0x9b, 0x17, 0x02, 0xb0, 0x4f, 0x5e, 0x43,\n\t\t0x39, 0xcd, 0xda, 0x4d, 0x91, 0xaa, 0x03, 0x31, 0xc1, 0x5d, 0x64, 0x16,\n\t\t0x0f, 0xf2, 0x68, 0xc9, 0x21, 0xb2, 0xf5, 0xad, 0x37, 0xac, 0x92, 0xe9,\n\t\t0x0f, 0xd0, 0xec, 0x41, 0x43, 0x94, 0xf4, 0xbe, 0xfd, 0x85, 0x6b, 0x9a,\n\t\t0xdf, 0xe8, 0xfe, 0xf7, 0x29, 0x52, 0x08, 0xa2, 0x27, 0x05, 0x89, 0x70,\n\t\t0x1d, 0xbb, 0xef, 0xe4, 0x9b, 0x0d, 0x4f, 0x30, 0x45, 0x28, 0x1e, 0x2f,\n\t\t0x9a, 0x2a, 0xf7, 0x63, 0xfd, 0x2b, 0x33, 0xca, 0xb7, 0xcd, 0x79, 0x73,\n\t\t0x06, 0x80, 0xe8, 0xd9, 0xa8, 0x19, 0x6f, 0xd8, 0xb3, 0x8b, 0x67, 0x14,\n\t\t0x15, 0x5f, 0x96, 0xda, 0xb3, 0x50, 0xd5, 0xfc, 0x01, 0x22, 0x08, 0x47,\n\t\t0xed, 0xa6, 0x8d, 0x56, 0xd6, 0xc0, 0x12, 0x9d, 0x30, 0x46, 0xff, 0x13,\n\t\t0x40, 0x8e, 0x97, 0xd4, 0x9e, 0x85, 0x9a, 0xee, 0x0b, 0x48, 0xe1, 0x38,\n\t\t0xb4, 0x7c, 0xee, 0xbf, 0xe3, 0xcb, 0x9f, 0x7a, 0xf2, 0xcd, 0xfd, 0x9f,\n\t\t0x7f, 0x7a, 0x37, 0x80, 0xad, 0xed, 0x05, 0xb6, 0xd3, 0xc9, 0xe6, 0xda,\n\t\t0x6d, 0x5b, 0x72, 0x32, 0x8b, 0x8a, 0x07, 0x06, 0x72, 0x72, 0x95, 0x50,\n\t\t0x6e, 0x17, 0x91, 0xde, 0xbb, 0xc8, 0x80, 0x94, 0x04, 0xff, 0x05, 0x91,\n\t\t0x9c, 0x03, 0x84, 0xd0, 0x86, 0xbd, 0xbb, 0xdd, 0x43, 0x6b, 0x56, 0x7a,\n\t\t0xea, 0xf7, 0xec, 0x9c, 0x63, 0x86, 0x1b, 0x17, 0x52, 0xa6, 0x90, 0xd3,\n\t\t0x39, 0x9a, 0xf6, 0xc0, 0xc0, 0x0c, 0x37, 0x2c, 0x3e, 0xb8, 0xba, 0x74,\n\t\t0x6c, 0xee, 0x85, 0x23, 0x3a, 0xf9, 0xd2, 0x33, 0x14, 0x48, 0xfc, 0x57,\n\t\t0x80, 0x9c, 0xe0, 0x88, 0x32, 0xb2, 0x58, 0xb7, 0x71, 0xe3, 0x01, 0x29,\n\t\t0x2e, 0xad, 0xf8, 0x7c, 0xc9, 0x36, 0x29, 0x45, 0xf9, 0xe9, 0xee, 0xa5,\n\t\t0x67, 0x58, 0xab, 0x6f, 0xd1, 0x94, 0x1f, 0x14, 0xf9, 0x33, 0x32, 0x89,\n\t\t0xe4, 0xe2, 0xa4, 0x98, 0xf6, 0xdf, 0x11, 0xc1, 0x5d, 0x58, 0x4d, 0x61,\n\t\t0x14, 0xdf, 0x7a, 0x17, 0x06, 0x4c, 0xbb, 0x6b, 0x06, 0x80, 0xae, 0xe7,\n\t\t0x04, 0xa6, 0xd3, 0x90, 0x0b, 0x7e, 0xd9, 0x67, 0xd2, 0x94, 0x24, 0x2d,\n\t\t0x10, 0x24, 0xe7, 0x14, 0x43, 0xbe, 0x47, 0x71, 0xe2, 0x31, 0x0c, 0xb9,\n\t\t0xff, 0x91, 0x8c, 0xe4, 0x82, 0xee, 0xf7, 0x9d, 0x0b, 0x98, 0xa1, 0xbd,\n\t\t0x26, 0x5e, 0x7f, 0x8d, 0x3f, 0x2b, 0x1b, 0x52, 0xfc, 0xbf, 0x05, 0xf2,\n\t\t0x8d, 0xeb, 0xd5, 0x0d, 0x56, 0x32, 0xfd, 0x81, 0x2b, 0x00, 0x14, 0x9e,\n\t\t0x15, 0x98, 0xb4, 0xde, 0x45, 0xf7, 0xf7, 0xbc, 0xea, 0x1a, 0x6a, 0xc7,\n\t\t0x62, 0xff, 0xdd, 0x1d, 0x4b, 0xf9, 0xed, 0x75, 0x72, 0x2c, 0xa2, 0x14,\n\t\t0x29, 0x3d, 0x7a, 0xf6, 0xea, 0x3c, 0x7c, 0xd4, 0x88, 0xb3, 0x01, 0xd3,\n\t\t0xbf, 0x68, 0xea, 0xad, 0x43, 0x15, 0xdd, 0xf8, 0xef, 0x81, 0xa0, 0x14,\n\t\t0xaa, 0xd7, 0x0f, 0x3d, 0x94, 0x0c, 0x35, 0x94, 0x0c, 0x3d, 0x94, 0x02,\n\t\t0xd5, 0x17, 0x00, 0x61, 0x27, 0xfa, 0xa8, 0x40, 0x6e, 0x3e, 0x42, 0x05,\n\t\t0x3d, 0x26, 0x00, 0xf0, 0x75, 0x28, 0x68, 0xaa, 0x1e, 0x6f, 0x51, 0x7a,\n\t\t0xcf, 0x3e, 0x5d, 0xdb, 0x8b, 0x25, 0x84, 0x90, 0x96, 0xc2, 0x44, 0x08,\n\t\t0x40, 0x0a, 0x00, 0x14, 0x94, 0x51, 0x10, 0x00, 0xf2, 0x2c, 0x63, 0x90,\n\t\t0x93, 0x88, 0xcb, 0xc4, 0xc1, 0x2a, 0xeb, 0xd0, 0xfc, 0xb9, 0x4e, 0x6c,\n\t\t0xd3, 0xba, 0x44, 0x9a, 0xd7, 0xa3, 0x37, 0xc4, 0xe3, 0x8e, 0xda, 0xa3,\n\t\t0x8f, 0x9e, 0x77, 0xf5, 0x0d, 0xd4, 0xd7, 0xbd, 0x97, 0x4f, 0xf5, 0xfa,\n\t\t0x5a, 0xbd, 0x1b, 0x3c, 0x29, 0xa9, 0x57, 0xfa, 0x32, 0x33, 0x93, 0x62,\n\t\t0x35, 0x35, 0xb1, 0x33, 0x81, 0xd1, 0xbb, 0x5d, 0x76, 0xe5, 0xd0, 0x40,\n\t\t0x6e, 0xee, 0x69, 0xb1, 0x30, 0x4d, 0x43, 0x5d, 0x55, 0x25, 0x37, 0xab,\n\t\t0x0f, 0xd9, 0x86, 0x15, 0x27, 0x4c, 0x70, 0x85, 0x53, 0xe6, 0x9a, 0x86,\n\t\t0x97, 0x78, 0x73, 0xf3, 0x95, 0xb4, 0xdc, 0x7c, 0xe6, 0x9a, 0x89, 0x33,\n\t\t0xa7, 0x30, 0x94, 0xa2, 0x6e, 0x67, 0xb9, 0xed, 0xdf, 0xb2, 0xd6, 0x1e,\n\t\t0x9d, 0x9a, 0xa4, 0x4c, 0xfa, 0xfd, 0xd3, 0x9e, 0x40, 0x6a, 0x6a, 0xa0,\n\t\t0xf5, 0x7b, 0xd7, 0x4c, 0xb8, 0xf3, 0xde, 0x7d, 0x57, 0x2c, 0xfd, 0x68,\n\t\t0x6e, 0x73, 0x75, 0x8f, 0x7e, 0x7a, 0xe7, 0x61, 0x17, 0x69, 0xc2, 0x75,\n\t\t0x91, 0xd1, 0x7f, 0x00, 0xb4, 0xf9, 0x49, 0x17, 0xc6, 0x50, 0xf3, 0xde,\n\t\t0x19, 0xc1, 0xe8, 0x81, 0xa4, 0x41, 0x9e, 0x94, 0xf4, 0x36, 0x5d, 0x31,\n\t\t0x51, 0x14, 0xec, 0x58, 0xf2, 0x89, 0x59, 0x50, 0x77, 0xc8, 0xbe, 0xa1,\n\t\t0x64, 0xa0, 0x9e, 0x97, 0x99, 0x47, 0x0c, 0x8f, 0x41, 0x12, 0x09, 0x93,\n\t\t0x1e, 0xac, 0xa9, 0x91, 0x1f, 0x7d, 0xb6, 0x20, 0xb1, 0x39, 0x39, 0x9b,\n\t\t0xf5, 0xbf, 0xea, 0x1a, 0x0f, 0xb7, 0xcc, 0x76, 0x4e, 0x96, 0x22, 0x5a,\n\t\t0x57, 0x63, 0x07, 0xbf, 0x5c, 0x6c, 0x3f, 0xfd, 0xd3, 0x07, 0x8d, 0xf4,\n\t\t0xac, 0xec, 0x53, 0xf6, 0xa2, 0x18, 0x1e, 0xe5, 0xba, 0x9b, 0x6f, 0xc1,\n\t\t0xb8, 0xc6, 0x46, 0xe5, 0x77, 0x33, 0x5e, 0x8a, 0xaf, 0xae, 0xaf, 0xe7,\n\t\t0x83, 0xae, 0xbb, 0xd1, 0xd3, 0x69, 0xc8, 0x85, 0xe0, 0xb6, 0x75, 0x23,\n\t\t0x80, 0xf6, 0xc1, 0x78, 0x53, 0xd3, 0x54, 0x6f, 0x5a, 0x7a, 0x27, 0xc2,\n\t\t0x68, 0x4b, 0x14, 0x3e, 0xce, 0x30, 0x99, 0x61, 0x60, 0xff, 0x92, 0x4f,\n\t\t0xe2, 0x23, 0x6b, 0xf6, 0xf3, 0x47, 0x1e, 0x7f, 0xdc, 0xaf, 0xaa, 0x2a,\n\t\t0xc5, 0xb7, 0x49, 0x26, 0x2b, 0x01, 0x70, 0xf9, 0xc5, 0x23, 0xd5, 0x97,\n\t\t0x5e, 0x9a, 0x99, 0x58, 0xfc, 0xc1, 0xdc, 0x58, 0xe1, 0x84, 0x6b, 0x7d,\n\t\t0xc2, 0x75, 0xdb, 0x34, 0x72, 0x35, 0x29, 0x80, 0x8d, 0x77, 0x4f, 0x8d,\n\t\t0xaf, 0xfd, 0xe4, 0xe3, 0x80, 0x6a, 0x18, 0xed, 0x06, 0xef, 0x40, 0x72,\n\t\t0x32, 0x7d, 0xe6, 0x97, 0xbf, 0xf0, 0x4f, 0xbc, 0x71, 0x4a, 0xe4, 0x60,\n\t\t0x9f, 0x22, 0x96, 0x3f, 0xe0, 0x3c, 0x4d, 0x0b, 0x24, 0x0d, 0x39, 0xa3,\n\t\t0x03, 0xd0, 0x92, 0x82, 0x19, 0xde, 0x8c, 0xac, 0x53, 0x8d, 0xcb, 0xe3,\n\t\t0xc5, 0xae, 0xcf, 0x16, 0x9b, 0x45, 0xdb, 0xd7, 0xf1, 0x27, 0x7e, 0xf9,\n\t\t0xcb, 0x80, 0xaa, 0x69, 0xc7, 0x03, 0x39, 0xee, 0x58, 0x0d, 0xfa, 0xf0,\n\t\t0xc3, 0x0f, 0xfb, 0x2e, 0x8a, 0xd7, 0xa1, 0x7a, 0xc3, 0x5a, 0x8b, 0x28,\n\t\t0xa7, 0x1e, 0xbe, 0x96, 0x94, 0x84, 0x65, 0xcf, 0x3e, 0x13, 0xf9, 0xf3,\n\t\t0x13, 0x3f, 0x33, 0xda, 0x03, 0x22, 0xa5, 0xc4, 0xaa, 0x55, 0xab, 0xec,\n\t\t0xd5, 0xab, 0x57, 0x3b, 0x42, 0x08, 0xcc, 0x7e, 0xe3, 0xef, 0xbe, 0xbd,\n\t\t0x73, 0xff, 0xe5, 0x70, 0xce, 0x91, 0xda, 0xb3, 0xb0, 0xd3, 0x19, 0xc1,\n\t\t0x10, 0x42, 0x02, 0x9a, 0xdf, 0x7f, 0xc2, 0x03, 0x28, 0x63, 0xa8, 0xd9,\n\t\t0xb7, 0x97, 0x67, 0x6c, 0x5e, 0x63, 0x3d, 0xf3, 0xfc, 0x0b, 0xfe, 0x78,\n\t\t0x3c, 0x2e, 0xe7, 0xcd, 0x9b, 0x67, 0x6d, 0xdb, 0xb6, 0xcd, 0x3e, 0xdd,\n\t\t0x46, 0x7e, 0xf6, 0xc8, 0xa3, 0x1e, 0xb9, 0x74, 0x81, 0xed, 0x38, 0x8e,\n\t\t0x3c, 0x01, 0x34, 0x21, 0x88, 0x47, 0x9a, 0x79, 0xb0, 0x72, 0x0f, 0x46,\n\t\t0x5e, 0x7a, 0x69, 0xbb, 0xee, 0xd2, 0xb6, 0x6d, 0xdc, 0x76, 0xdb, 0x6d,\n\t\t0xce, 0x9b, 0x6f, 0xbe, 0x69, 0xdb, 0xb6, 0x2d, 0x15, 0x85, 0xb1, 0xf1,\n\t\t0x83, 0xfa, 0x93, 0xba, 0x8a, 0x7d, 0x32, 0xa9, 0x53, 0x5e, 0x2b, 0xe7,\n\t\t0xd0, 0x8e, 0x6b, 0x26, 0x44, 0x65, 0x8a, 0x4a, 0x8f, 0x37, 0x17, 0x49,\n\t\t0x08, 0xea, 0x36, 0xac, 0xb1, 0x1f, 0xf8, 0xe1, 0xad, 0x5e, 0x00, 0xa4,\n\t\t0xbc, 0xbc, 0xdc, 0x99, 0x39, 0x73, 0x26, 0x1f, 0x33, 0x66, 0x0c, 0x00,\n\t\t0xb4, 0x59, 0x90, 0x33, 0x5d, 0xa7, 0xd7, 0x5d, 0x78, 0xbe, 0x5a, 0xbd,\n\t\t0x76, 0x95, 0x45, 0x15, 0xf5, 0x38, 0x0f, 0xcc, 0x70, 0xa8, 0x7c, 0xab,\n\t\t0xbc, 0x76, 0xfc, 0x15, 0xca, 0x19, 0xe9, 0x1d, 0xc6, 0x70, 0xc9, 0x25,\n\t\t0x97, 0x30, 0xc3, 0x30, 0x54, 0x4a, 0x29, 0x61, 0x94, 0xa2, 0x4b, 0x5a,\n\t\t0x2a, 0x93, 0xf1, 0x98, 0xa3, 0xfa, 0xfc, 0x0a, 0x00, 0x4f, 0xfb, 0x0e,\n\t\t0x40, 0x4a, 0x2e, 0xf8, 0xf1, 0xc6, 0x02, 0xd8, 0xa6, 0x89, 0x0c, 0xe9,\n\t\t0x88, 0xfc, 0xbc, 0x3c, 0x7a, 0xec, 0x8d, 0xd1, 0x8d, 0x1b, 0x37, 0xaa,\n\t\t0xa9, 0xa9, 0xa9, 0x1c, 0x80, 0x76, 0xba, 0xcd, 0x5c, 0x3f, 0x65, 0xaa,\n\t\t0xf2, 0xbb, 0x1f, 0x4e, 0xb7, 0xbb, 0x8d, 0x1e, 0x0b, 0xe1, 0xd8, 0xdf,\n\t\t0x9e, 0x4c, 0xb8, 0x91, 0xe7, 0x75, 0xca, 0xb2, 0x01, 0x78, 0xdb, 0x8d,\n\t\t0xf8, 0x8a, 0x82, 0xe7, 0x9e, 0x7b, 0x4e, 0x17, 0x42, 0x40, 0xd3, 0x34,\n\t\t0x08, 0x21, 0xe0, 0x33, 0x74, 0x49, 0xb9, 0x4b, 0x5c, 0xcb, 0x92, 0x27,\n\t\t0x17, 0x68, 0x4a, 0x1b, 0x27, 0x13, 0x73, 0xcd, 0xc4, 0x09, 0x6e, 0x4c,\n\t\t0xb8, 0x2e, 0x0c, 0xe1, 0x72, 0x7f, 0x20, 0x20, 0x00, 0xb0, 0xa1, 0x43,\n\t\t0x87, 0x2a, 0x4b, 0x96, 0x2c, 0x71, 0x0a, 0x0a, 0x0a, 0x94, 0xf6, 0xb2,\n\t\t0x08, 0x45, 0xd7, 0x15, 0xf3, 0xc8, 0xe1, 0x26, 0x42, 0xe9, 0x71, 0x9b,\n\t\t0x96, 0xd0, 0x3c, 0x1e, 0x1a, 0x6e, 0x8e, 0x6a, 0x1d, 0x89, 0x41, 0x9a,\n\t\t0xa6, 0x9d, 0x60, 0x98, 0xb6, 0xe3, 0x12, 0x12, 0xd0, 0x68, 0xac, 0xf6,\n\t\t0x70, 0x1c, 0x40, 0x63, 0xbb, 0x6a, 0x46, 0x29, 0xb3, 0x84, 0xe3, 0x92,\n\t\t0x13, 0x92, 0x7d, 0x42, 0x20, 0x09, 0x23, 0xc2, 0x75, 0x49, 0xcb, 0x3d,\n\t\t0x14, 0x03, 0x07, 0x0e, 0x54, 0x83, 0xc1, 0xe0, 0x99, 0x54, 0x45, 0x10,\n\t\t0x55, 0x55, 0x4e, 0xaa, 0x24, 0x91, 0xd1, 0xbd, 0x17, 0x59, 0xf2, 0xc5,\n\t\t0x97, 0xfc, 0xec, 0x33, 0x1d, 0x89, 0xc3, 0x4d, 0x4d, 0x5c, 0xe8, 0x06,\n\t\t0xb3, 0xa3, 0xcd, 0x67, 0x4e, 0x67, 0x04, 0xe7, 0xae, 0x70, 0x6d, 0xe7,\n\t\t0x78, 0x9b, 0x51, 0x35, 0x1d, 0x61, 0x10, 0xd6, 0xd8, 0xd8, 0x40, 0xcf,\n\t\t0xe6, 0xe1, 0xfb, 0x76, 0xee, 0x72, 0x53, 0x8a, 0x06, 0x68, 0xc2, 0x75,\n\t\t0x4e, 0x00, 0x93, 0x96, 0x97, 0xaf, 0xac, 0xad, 0x6d, 0xc0, 0xae, 0xf2,\n\t\t0xf2, 0xe6, 0xb3, 0x59, 0x2f, 0x61, 0x9a, 0xd6, 0xa7, 0x9b, 0xb6, 0xb9,\n\t\t0xa1, 0x9c, 0x1c, 0x22, 0x2c, 0x5b, 0x3f, 0x33, 0x18, 0xd7, 0x76, 0xb9,\n\t\t0xe3, 0x38, 0xc7, 0x9f, 0x8c, 0xaa, 0x6b, 0x68, 0xf2, 0x25, 0x29, 0x1b,\n\t\t0xb6, 0x94, 0xc5, 0xcf, 0xe6, 0x45, 0xbe, 0xf2, 0xca, 0xcb, 0x66, 0xf7,\n\t\t0xab, 0xae, 0x55, 0xb9, 0x65, 0x9d, 0x98, 0xbe, 0x34, 0x47, 0x30, 0xfa,\n\t\t0xe7, 0xbf, 0xf1, 0xdd, 0x75, 0xdf, 0x8f, 0xb9, 0x6d, 0x59, 0x66, 0x07,\n\t\t0xa8, 0x5a, 0x09, 0xc0, 0x79, 0xf4, 0x91, 0x47, 0x4c, 0x65, 0xd0, 0x50,\n\t\t0xc3, 0xe3, 0x4f, 0x82, 0x6b, 0x59, 0xa2, 0x03, 0x27, 0xe3, 0xda, 0xdc,\n\t\t0xb1, 0x4f, 0x50, 0x01, 0xee, 0x38, 0x28, 0x1a, 0x3f, 0x51, 0x7d, 0xf0,\n\t\t0x7f, 0x9f, 0xe5, 0xd1, 0x48, 0x24, 0xd2, 0x91, 0x9a, 0x6a, 0xfd, 0xaa,\n\t\t0x55, 0xb1, 0x75, 0x52, 0xa7, 0xa1, 0x9c, 0x5c, 0x45, 0x9e, 0xe8, 0x4f,\n\t\t0x20, 0x84, 0x80, 0xe1, 0xf3, 0xd1, 0xcc, 0x7b, 0x1f, 0xf7, 0x3c, 0xf9,\n\t\t0xf8, 0xe3, 0xbc, 0xae, 0xb6, 0xd6, 0x76, 0x5d, 0xd7, 0x96, 0x2d, 0x49,\n\t\t0x9d, 0x3c, 0x51, 0xb3, 0xa4, 0x70, 0x1c, 0xc7, 0xfe, 0xc3, 0xd3, 0xbf,\n\t\t0xb1, 0xd6, 0xfb, 0x52, 0x95, 0xc2, 0xd1, 0x63, 0x34, 0x6e, 0x9b, 0xb0,\n\t\t0xa3, 0x91, 0x86, 0x33, 0x82, 0x91, 0xae, 0xe0, 0xd2, 0xe5, 0xe2, 0xa4,\n\t\t0x25, 0x41, 0x38, 0x27, 0xc3, 0x9e, 0x7a, 0xd6, 0x3b, 0xe9, 0xae, 0xbb,\n\t\t0x69, 0xd9, 0x96, 0xcd, 0x71, 0xd7, 0x75, 0x5d, 0x21, 0x04, 0x17, 0x42,\n\t\t0x48, 0xd1, 0x22, 0x52, 0x4a, 0xc9, 0x39, 0x77, 0xdd, 0x0d, 0x6b, 0xd6,\n\t\t0x58, 0xbf, 0xff, 0xd7, 0x5b, 0x24, 0x77, 0xd2, 0x14, 0x2f, 0x39, 0x4d,\n\t\t0x82, 0x27, 0x5c, 0x17, 0x19, 0xfd, 0x8a, 0xf5, 0x6d, 0x5d, 0x0a, 0xe9,\n\t\t0x8c, 0xbf, 0xbf, 0x81, 0xd5, 0x5f, 0x97, 0xca, 0x43, 0x95, 0x95, 0x76,\n\t\t0x22, 0x1e, 0xb7, 0x5c, 0xd7, 0x95, 0x8e, 0x6d, 0x3b, 0xb5, 0x47, 0x8e,\n\t\t0x58, 0xeb, 0x57, 0xaf, 0xb6, 0xff, 0xf0, 0xc2, 0x9f, 0xc5, 0xc2, 0x84,\n\t\t0xa4, 0x23, 0x1e, 0x7e, 0xc2, 0x67, 0x86, 0xc3, 0x00, 0x08, 0xec, 0x68,\n\t\t0xb4, 0xe9, 0x8c, 0x59, 0xb3, 0x70, 0x5d, 0x2e, 0x5c, 0xf7, 0x14, 0xae,\n\t\t0x93, 0xdb, 0x36, 0x52, 0x72, 0xf3, 0x55, 0x7e, 0xdb, 0xbd, 0xe4, 0x57,\n\t\t0xff, 0x9e, 0x6b, 0x0e, 0x4f, 0x4d, 0x72, 0x26, 0x4c, 0xbc, 0x9a, 0x16,\n\t\t0xf4, 0xea, 0xc5, 0x8e, 0xbd, 0x4d, 0xb6, 0xbb, 0xac, 0xcc, 0x9d, 0x3f,\n\t\t0x6f, 0x9e, 0x58, 0x15, 0x77, 0xa4, 0x32, 0xfe, 0x7a, 0x23, 0x39, 0xa7,\n\t\t0x13, 0xe5, 0xa7, 0xe1, 0x85, 0x5b, 0x49, 0x8b, 0x6e, 0x97, 0x4d, 0xf4,\n\t\t0x94, 0x6f, 0xd9, 0x68, 0xef, 0x59, 0xbd, 0x89, 0xe7, 0xac, 0x58, 0x29,\n\t\t0x93, 0x25, 0x27, 0x1a, 0x77, 0x23, 0x0e, 0xa5, 0x4a, 0x98, 0xaa, 0xb4,\n\t\t0x9a, 0xe9, 0x24, 0x91, 0xd7, 0x83, 0xf5, 0x9f, 0x78, 0xbe, 0x66, 0x36,\n\t\t0x36, 0x80, 0x50, 0x0a, 0x48, 0xc0, 0x35, 0x13, 0xb1, 0x33, 0x83, 0xe1,\n\t\t0x2e, 0xa4, 0xe0, 0xa4, 0x2d, 0xe6, 0xd1, 0xb5, 0x4c, 0x24, 0x77, 0x29,\n\t\t0x50, 0xdc, 0xa9, 0xb7, 0xfb, 0xd7, 0xee, 0xdb, 0x9d, 0x78, 0xf7, 0xa9,\n\t\t0x67, 0x1c, 0x73, 0xdb, 0xc6, 0x78, 0x8a, 0xcf, 0xab, 0xd7, 0x47, 0xe3,\n\t\t0x11, 0xdf, 0x80, 0x12, 0x23, 0x7f, 0xd2, 0x14, 0x35, 0xb5, 0x73, 0x37,\n\t\t0x55, 0xf1, 0x78, 0xc8, 0xa9, 0x40, 0xc8, 0xa9, 0xc9, 0xab, 0x14, 0xc8,\n\t\t0xea, 0x37, 0x40, 0xe3, 0x85, 0x7d, 0x64, 0x53, 0x2c, 0x26, 0x1b, 0xcc,\n\t\t0x38, 0x20, 0x84, 0x2a, 0x09, 0xa1, 0xcc, 0xf0, 0xc0, 0xef, 0xf7, 0x93,\n\t\t0x90, 0xaa, 0x11, 0x08, 0x81, 0x6f, 0x33, 0x09, 0x89, 0x0e, 0x91, 0x80,\n\t\t0xdc, 0x71, 0x20, 0x38, 0x6f, 0xb7, 0x12, 0x54, 0x74, 0x03, 0x29, 0x45,\n\t\t0x03, 0x3c, 0x19, 0x25, 0x17, 0xb4, 0x74, 0x05, 0xa4, 0x00, 0x25, 0xd4,\n\t\t0x2b, 0x5c, 0x07, 0x6e, 0x3c, 0xde, 0x52, 0x66, 0x9f, 0xac, 0x5e, 0x52,\n\t\t0x22, 0x5a, 0x53, 0x2d, 0x98, 0xaa, 0x11, 0x4f, 0x6a, 0x1a, 0x39, 0xd9,\n\t\t0xbe, 0x99, 0xaa, 0x12, 0x16, 0x0a, 0x11, 0x90, 0xe4, 0x6f, 0xd5, 0xff,\n\t\t0x74, 0x35, 0x48, 0xcb, 0xc7, 0xfc, 0x8c, 0x36, 0xe3, 0xc4, 0xa2, 0x52,\n\t\t0x38, 0x8e, 0xdd, 0x11, 0x5e, 0xcb, 0x89, 0x45, 0x61, 0x35, 0x36, 0xc0,\n\t\t0x09, 0x87, 0x5b, 0xfe, 0x46, 0xa3, 0xa7, 0xf0, 0x05, 0x84, 0x10, 0xe8,\n\t\t0xa1, 0x64, 0x54, 0x94, 0x7e, 0x65, 0xcf, 0xbe, 0xfa, 0x52, 0xfe, 0xe1,\n\t\t0x1d, 0x53, 0x9d, 0xc8, 0x91, 0xc3, 0xae, 0xe2, 0xf5, 0x9e, 0x75, 0xd9,\n\t\t0xfc, 0x4d, 0x9d, 0x63, 0x5b, 0x38, 0x9e, 0xd5, 0xec, 0x9e, 0x7b, 0xfa,\n\t\t0xb2, 0x59, 0x72, 0xc7, 0x31, 0xcf, 0x8a, 0xf8, 0x6e, 0xbd, 0xda, 0xd8,\n\t\t0x98, 0x90, 0x90, 0x6b, 0x66, 0x3c, 0x97, 0xd8, 0xf4, 0xfc, 0xd3, 0xdc,\n\t\t0x6a, 0x0a, 0xab, 0xf5, 0x3b, 0xca, 0xd4, 0x95, 0xbf, 0xfa, 0xa9, 0xbd,\n\t\t0xfd, 0xe3, 0x05, 0x09, 0xcd, 0xeb, 0x3b, 0xeb, 0xaa, 0x94, 0x10, 0x02,\n\t\t0xe1, 0x38, 0xd2, 0x75, 0xb8, 0x7e, 0x7e, 0x11, 0xfe, 0x37, 0x3d, 0x84,\n\t\t0xec, 0x3d, 0x07, 0x4f, 0x0f, 0xc6, 0x25, 0x80, 0xf9, 0x9d, 0x29, 0x32,\n\t\t0x29, 0xa1, 0x78, 0x3c, 0x28, 0x9f, 0xf5, 0xc7, 0xf8, 0x2f, 0xba, 0x45,\n\t\t0x94, 0x4d, 0x6f, 0x3c, 0xa4, 0x8d, 0xb9, 0xb0, 0x5f, 0xe2, 0x47, 0x53,\n\t\t0xc7, 0x5a, 0x65, 0x7f, 0x99, 0x6e, 0xf4, 0x5f, 0xf5, 0x2f, 0xb9, 0xf7,\n\t\t0xab, 0xcf, 0xe3, 0xaa, 0xe1, 0x39, 0xeb, 0xae, 0x81, 0x00, 0x31, 0x9d,\n\t\t0xa8, 0x79, 0xde, 0xcd, 0x97, 0xe3, 0x07, 0x75, 0x61, 0x04, 0xdb, 0x52,\n\t\t0x33, 0xb5, 0x30, 0x1f, 0x37, 0x65, 0x06, 0x71, 0x25, 0x75, 0xe3, 0x41,\n\t\t0x50, 0xfa, 0xdd, 0xd8, 0x7d, 0x4a, 0xe1, 0xba, 0xdc, 0xf1, 0x6c, 0xf8,\n\t\t0x8c, 0x8f, 0x1d, 0x79, 0x1e, 0xa9, 0xaf, 0x6f, 0xb2, 0x7f, 0x7e, 0xf7,\n\t\t0xd5, 0xf2, 0xca, 0x8b, 0x07, 0x91, 0x75, 0x1b, 0x77, 0x59, 0x77, 0xdf,\n\t\t0x70, 0x49, 0x74, 0xd3, 0xdc, 0xb7, 0xa1, 0xfa, 0x7c, 0x1d, 0x27, 0xe0,\n\t\t0x09, 0x81, 0x15, 0x8d, 0xb9, 0xd6, 0x27, 0xcf, 0x26, 0x0a, 0xd2, 0x9b,\n\t\t0xd4, 0xec, 0x54, 0x10, 0x00, 0x03, 0xda, 0xa2, 0x67, 0xbd, 0x23, 0x07,\n\t\t0xe1, 0x93, 0x41, 0x85, 0xb8, 0x39, 0x97, 0x1d, 0x30, 0x0e, 0x9b, 0x69,\n\t\t0x16, 0x49, 0xc9, 0x27, 0x46, 0xc0, 0xcf, 0xa4, 0xe0, 0xe7, 0x84, 0x87,\n\t\t0x10, 0x42, 0x8e, 0x1c, 0x3e, 0xea, 0xce, 0x9e, 0xbd, 0x28, 0x6e, 0xd3,\n\t\t0x4c, 0xb6, 0xab, 0x0e, 0x72, 0xef, 0x51, 0x8e, 0xfa, 0xb8, 0xc6, 0x9e,\n\t\t0x5f, 0xba, 0x59, 0xf6, 0xb8, 0xe9, 0x76, 0xc3, 0x9b, 0x92, 0xca, 0x3a,\n\t\t0xc2, 0xcb, 0x29, 0x1e, 0x1f, 0x9a, 0x6a, 0xc3, 0x89, 0xc4, 0xdc, 0x47,\n\t\t0x63, 0x9d, 0x2b, 0x67, 0xd3, 0xed, 0xfb, 0x6c, 0x32, 0xa2, 0x58, 0x1a,\n\t\t0xa6, 0x8d, 0xac, 0xdd, 0x55, 0xf8, 0x37, 0x00, 0x71, 0x3c, 0x18, 0x7b,\n\t\t0xff, 0x61, 0x14, 0x0d, 0x2f, 0x46, 0xef, 0xea, 0xc3, 0xa6, 0x93, 0x53,\n\t\t0xbb, 0x88, 0x47, 0xd6, 0x7f, 0x1e, 0x8f, 0x67, 0x0f, 0x96, 0xfe, 0xac,\n\t\t0x4e, 0x1a, 0x81, 0x3c, 0x6b, 0xfd, 0x86, 0x94, 0x24, 0xad, 0xe4, 0x42,\n\t\t0xcd, 0x74, 0x09, 0x9e, 0xbc, 0xe5, 0x66, 0xb5, 0x47, 0xaf, 0x9e, 0x22,\n\t\t0xdc, 0xd8, 0x48, 0x8b, 0x7b, 0x15, 0xb2, 0x8a, 0xfe, 0x17, 0xaa, 0xc9,\n\t\t0x85, 0x45, 0x5a, 0x9b, 0x65, 0xf5, 0x49, 0xa4, 0x07, 0x97, 0x8a, 0xd8,\n\t\t0x3d, 0xf7, 0xf5, 0x70, 0xff, 0x5d, 0x4f, 0x09, 0xad, 0x72, 0xb9, 0xb2,\n\t\t0x29, 0xff, 0x61, 0x65, 0xcd, 0xa7, 0x5b, 0x2b, 0x3a, 0x25, 0x27, 0x32,\n\t\t0x93, 0x7c, 0xd8, 0xb4, 0xb6, 0x1c, 0x73, 0x4e, 0x21, 0xce, 0x53, 0x92,\n\t\t0xe8, 0x25, 0x03, 0xba, 0xab, 0xe7, 0x73, 0x41, 0x5c, 0x8f, 0x21, 0xd0,\n\t\t0x4c, 0x93, 0xd5, 0x5e, 0x7b, 0x5e, 0x73, 0xf6, 0xb9, 0x5d, 0x6c, 0xc7,\n\t\t0xc8, 0xa0, 0x9e, 0x60, 0x12, 0x03, 0x77, 0xdb, 0x36, 0xf6, 0x76, 0xfc,\n\t\t0x43, 0xac, 0xb6, 0x46, 0xb8, 0xdb, 0x37, 0x5b, 0x47, 0x0f, 0x56, 0x35,\n\t\t0x7d, 0xf4, 0xe1, 0x87, 0x84, 0x4b, 0x38, 0x75, 0x79, 0xdd, 0x75, 0x2d,\n\t\t0x18, 0xa2, 0xed, 0xd9, 0x1c, 0xd3, 0x34, 0x98, 0x71, 0xdb, 0x35, 0xdf,\n\t\t0xba, 0xaf, 0x69, 0x82, 0x78, 0x51, 0x59, 0x5d, 0x5a, 0x4d, 0x09, 0x84,\n\t\t0xb0, 0xc3, 0x47, 0x71, 0x55, 0x71, 0x4d, 0x46, 0x4a, 0x12, 0xdf, 0xfb,\n\t\t0x87, 0x37, 0x31, 0x1d, 0x40, 0xdd, 0x29, 0x60, 0x74, 0x55, 0xee, 0xb4,\n\t\t0x5c, 0x4c, 0x2c, 0xec, 0x24, 0x82, 0xcb, 0x1b, 0x06, 0xf2, 0x4e, 0xbf,\n\t\t0x5a, 0x1c, 0xa8, 0xde, 0xbb, 0x4f, 0xf4, 0xda, 0xf6, 0x0c, 0xaf, 0xfb,\n\t\t0x7c, 0x76, 0xac, 0xa6, 0xea, 0xa8, 0x19, 0x1a, 0x78, 0xb1, 0x46, 0x09,\n\t\t0x68, 0xc7, 0x49, 0x74, 0x02, 0x23, 0x33, 0x8b, 0xad, 0xab, 0xaa, 0x96,\n\t\t0x9b, 0x1a, 0x22, 0xaa, 0x1c, 0x38, 0xd4, 0x7b, 0xd0, 0x13, 0x40, 0x4a,\n\t\t0xef, 0xbe, 0x3a, 0x55, 0x54, 0x72, 0x3a, 0x8f, 0x05, 0xaa, 0x88, 0xaa,\n\t\t0xe5, 0xcb, 0x22, 0xb1, 0x39, 0x8f, 0x98, 0x25, 0xf2, 0x53, 0xf6, 0x59,\n\t\t0xd1, 0xdb, 0xc6, 0x81, 0xb5, 0x6b, 0xec, 0x07, 0xaf, 0x8a, 0x50, 0xb3,\n\t\t0xbe, 0xd1, 0x28, 0xaf, 0xe0, 0x5f, 0xbe, 0xfc, 0x1e, 0xae, 0x02, 0x70,\n\t\t0xe0, 0x94, 0x06, 0x6d, 0x76, 0x2a, 0xc1, 0xe1, 0x7a, 0x09, 0x00, 0xa1,\n\t\t0x2b, 0x86, 0xe0, 0xa3, 0xec, 0x82, 0x9c, 0x62, 0xe7, 0xf6, 0x77, 0x8c,\n\t\t0xa3, 0x3b, 0x77, 0x98, 0x81, 0x39, 0xb7, 0x23, 0xab, 0x93, 0xc2, 0x43,\n\t\t0x7e, 0x97, 0xec, 0x77, 0x7a, 0x98, 0xe2, 0xc6, 0x57, 0x7d, 0xfe, 0xae,\n\t\t0x7d, 0xbc, 0x0a, 0x03, 0x91, 0x9c, 0x77, 0xe8, 0x78, 0xa8, 0xa2, 0x00,\n\t\t0x12, 0x20, 0x94, 0x40, 0x4a, 0x09, 0xe1, 0xba, 0x6d, 0x53, 0xb0, 0x4c,\n\t\t0x41, 0x53, 0x55, 0xa5, 0xe9, 0xfb, 0xe4, 0xe1, 0x66, 0x67, 0xdb, 0x57,\n\t\t0xac, 0xa8, 0x07, 0xb4, 0xd2, 0x6d, 0xcc, 0x09, 0x3c, 0xfe, 0x99, 0xef,\n\t\t0xf0, 0xe6, 0x0d, 0x36, 0x7f, 0xf3, 0x21, 0x51, 0xd7, 0x7b, 0x52, 0xd2,\n\t\t0xe6, 0xcf, 0xd6, 0x4e, 0x93, 0x0d, 0x95, 0xff, 0x68, 0xb3, 0x3f, 0x13,\n\t\t0xfd, 0x96, 0xb3, 0x23, 0xb1, 0x2e, 0xa3, 0x2f, 0xca, 0xc9, 0x0d, 0xf4,\n\t\t0x0e, 0xae, 0x79, 0xd9, 0xdc, 0xba, 0x70, 0x29, 0x1f, 0x3f, 0x38, 0x4e,\n\t\t0x37, 0xd3, 0x11, 0xa4, 0xe6, 0xd2, 0x57, 0x0c, 0xb6, 0x7f, 0xb9, 0x70,\n\t\t0x3f, 0x7f, 0xc5, 0x51, 0x9c, 0x70, 0xbc, 0x9e, 0x15, 0x90, 0xe4, 0x4e,\n\t\t0x59, 0xaa, 0x68, 0x27, 0xff, 0x3a, 0xbe, 0x8e, 0x91, 0x42, 0x40, 0x72,\n\t\t0x7e, 0x62, 0x60, 0x6d, 0x6d, 0xa3, 0x03, 0x50, 0x74, 0x0d, 0x4d, 0x3b,\n\t\t0x36, 0xc6, 0x3a, 0x7f, 0x32, 0x39, 0xe1, 0xd5, 0x5c, 0x7d, 0x59, 0xe4,\n\t\t0x22, 0xe3, 0xb2, 0x6e, 0x3b, 0xac, 0xf2, 0x0a, 0xc2, 0x58, 0x6d, 0x99,\n\t\t0xdb, 0xa7, 0xea, 0x6f, 0xd2, 0x33, 0xf6, 0x76, 0xca, 0x0b, 0x2f, 0xd3,\n\t\t0xea, 0x37, 0xad, 0x9c, 0x9d, 0x68, 0xa8, 0x2f, 0x3b, 0x13, 0xd7, 0x4c,\n\t\t0xd2, 0xba, 0x76, 0x15, 0xfa, 0x0f, 0x66, 0x79, 0x1a, 0x87, 0xdc, 0xab,\n\t\t0xa7, 0xf7, 0x1b, 0xc0, 0xf6, 0xd7, 0x50, 0x0e, 0x01, 0x92, 0xd2, 0x7b,\n\t\t0x20, 0x12, 0xe1, 0x46, 0x39, 0x20, 0xaf, 0x49, 0x8d, 0xce, 0x9f, 0xa9,\n\t\t0x8e, 0x5d, 0x7b, 0x69, 0x6c, 0xd3, 0x07, 0x0b, 0xc2, 0xcc, 0xf0, 0x9d,\n\t\t0x53, 0x1c, 0x62, 0x9a, 0x06, 0x23, 0x94, 0x0c, 0xcd, 0xef, 0x87, 0x27,\n\t\t0x25, 0x0d, 0x95, 0xbb, 0x8e, 0x44, 0x2b, 0xff, 0x78, 0x8b, 0x59, 0x61,\n\t\t0xe6, 0x2b, 0xf5, 0x37, 0x2f, 0xf6, 0xe5, 0x5c, 0x73, 0xb7, 0x58, 0xf4,\n\t\t0x39, 0xb4, 0xe2, 0x7c, 0x01, 0xba, 0x6d, 0x8d, 0x5b, 0xde, 0xfd, 0x41,\n\t\t0x22, 0xae, 0x7c, 0xda, 0xa7, 0x28, 0x14, 0x90, 0xe2, 0xcc, 0x89, 0x66,\n\t\t0xcb, 0x4b, 0xe4, 0x5c, 0xf7, 0xe8, 0xd4, 0x7f, 0xc5, 0x74, 0x4f, 0xf2,\n\t\t0x65, 0x77, 0x79, 0xaa, 0xd7, 0x2c, 0xb7, 0x8a, 0xbf, 0x9e, 0x6e, 0xcd,\n\t\t0x9b, 0xdc, 0xc3, 0x1a, 0xd8, 0xb9, 0x59, 0x59, 0xbd, 0x2f, 0x4d, 0x06,\n\t\t0x1f, 0x7c, 0x4d, 0x9f, 0x35, 0xf3, 0x37, 0xf4, 0x81, 0xe1, 0xb7, 0xbb,\n\t\t0x1f, 0x7e, 0xfc, 0x62, 0x53, 0xf6, 0xc8, 0xf1, 0x01, 0x30, 0x4a, 0x48,\n\t\t0xbb, 0xb3, 0x05, 0x2d, 0x89, 0xa6, 0x10, 0x42, 0x10, 0x48, 0x52, 0xb9,\n\t\t0xec, 0xe3, 0xda, 0x5d, 0xf3, 0x66, 0x93, 0xa4, 0x82, 0xde, 0xd2, 0x3d,\n\t\t0xb2, 0x5b, 0x5c, 0xe6, 0x5b, 0xe0, 0x1f, 0x79, 0x3d, 0xfc, 0x8b, 0xb6,\n\t\t0x38, 0xce, 0xe7, 0x2f, 0xbf, 0x18, 0x19, 0x1e, 0xfd, 0x8b, 0x3a, 0x64,\n\t\t0x5c, 0x90, 0x3f, 0xbf, 0x67, 0x0a, 0xfa, 0x3c, 0x75, 0x7b, 0x20, 0x3b,\n\t\t0x37, 0x4f, 0x55, 0x74, 0x0d, 0x4e, 0x3c, 0x06, 0x09, 0xd2, 0x31, 0x30,\n\t\t0x10, 0xd2, 0x15, 0x9c, 0x03, 0x9c, 0x83, 0x00, 0xe8, 0x34, 0xfc, 0x12,\n\t\t0x7d, 0x03, 0xf9, 0x27, 0xb9, 0xae, 0xc7, 0xcb, 0xa6, 0xb9, 0x6b, 0x9d,\n\t\t0xd0, 0xe3, 0x3a, 0x69, 0x82, 0x57, 0xb9, 0xf0, 0xb5, 0x65, 0xea, 0xdf,\n\t\t0x6e, 0x29, 0x68, 0x2e, 0xc9, 0xbb, 0x57, 0x54, 0xad, 0x7b, 0xfd, 0x68,\n\t\t0xa0, 0xdf, 0x18, 0xa6, 0xf6, 0x3a, 0xdf, 0xc3, 0xb3, 0xfa, 0x6b, 0xba,\n\t\t0xdf, 0xa7, 0x80, 0xbb, 0x20, 0x8c, 0x81, 0x2a, 0x2a, 0xa2, 0x4d, 0x71,\n\t\t0x61, 0x87, 0x6b, 0x6d, 0x79, 0x70, 0x2b, 0xf7, 0x1e, 0x5c, 0x61, 0x7b,\n\t\t0xb6, 0xfe, 0xcd, 0xbd, 0xb3, 0x48, 0xa6, 0xe6, 0x5f, 0x0f, 0xf3, 0x9f,\n\t\t0x0b, 0x3f, 0xe1, 0xe7, 0xf5, 0x81, 0x3a, 0xf3, 0xb3, 0x74, 0x7b, 0xc3,\n\t\t0xfe, 0x58, 0x34, 0xcb, 0x17, 0x56, 0x27, 0x77, 0x7d, 0x5e, 0x89, 0x14,\n\t\t0x8e, 0x14, 0xaf, 0xa5, 0x3e, 0xa6, 0x8d, 0x9a, 0x76, 0xb1, 0x61, 0x35,\n\t\t0x37, 0x41, 0xba, 0x2e, 0x20, 0x5b, 0x6a, 0xa1, 0x78, 0x43, 0x7d, 0x5e,\n\t\t0x47, 0x7a, 0x9a, 0x2c, 0xad, 0xb0, 0xef, 0xe8, 0xdc, 0x0b, 0x86, 0x0d,\n\t\t0x51, 0x8e, 0xa5, 0x1a, 0x92, 0xbb, 0x48, 0xed, 0xd2, 0x85, 0x55, 0xa7,\n\t\t0x8d, 0xd1, 0xf6, 0xd0, 0x62, 0xd5, 0xa9, 0xd8, 0xec, 0xf4, 0x3c, 0xfa,\n\t\t0x01, 0x2f, 0x5d, 0xbc, 0x95, 0x4f, 0xc8, 0x59, 0x4d, 0x6a, 0x79, 0x06,\n\t\t0xe1, 0xdd, 0x46, 0x19, 0xac, 0x7a, 0x2b, 0x21, 0x2b, 0x66, 0x24, 0x52,\n\t\t0x9a, 0xcb, 0x9d, 0x68, 0xce, 0x70, 0x12, 0xc8, 0xee, 0xa4, 0x98, 0x09,\n\t\t0xc7, 0xda, 0xf8, 0xfe, 0x82, 0x78, 0xca, 0xb6, 0xbf, 0x98, 0x25, 0x35,\n\t\t0x2f, 0x93, 0xcb, 0x95, 0x37, 0xc8, 0x35, 0xf9, 0xeb, 0x7d, 0x37, 0x5e,\n\t\t0x0e, 0x7f, 0x6e, 0x0e, 0x9c, 0x59, 0xf3, 0xc0, 0x73, 0xb2, 0xc0, 0x8a,\n\t\t0xba, 0x41, 0x5b, 0x55, 0xdb, 0xcb, 0x55, 0x75, 0x8d, 0x65, 0x07, 0x4d,\n\t\t0xb1, 0xf7, 0xa2, 0x59, 0x5a, 0xf3, 0xe0, 0x9f, 0x18, 0x05, 0xc5, 0xdd,\n\t\t0x34, 0x3b, 0xd2, 0xf4, 0x8d, 0x6d, 0x51, 0x55, 0xc5, 0x81, 0x8d, 0x1b,\n\t\t0xf9, 0x4d, 0xdd, 0x94, 0xf3, 0x1a, 0x1a, 0x22, 0xa9, 0xb5, 0x0d, 0x91,\n\t\t0xaf, 0x5b, 0x33, 0xe8, 0xb6, 0xd5, 0xcc, 0xb1, 0x2d, 0xc9, 0xc5, 0x37,\n\t\t0xd3, 0x13, 0x2d, 0xc5, 0x99, 0x85, 0x40, 0x50, 0x27, 0x81, 0x8b, 0x46,\n\t\t0x28, 0xee, 0xa0, 0xe2, 0xc0, 0x86, 0xd7, 0x7f, 0x11, 0xad, 0x78, 0xf7,\n\t\t0x9f, 0xbc, 0xf0, 0xe7, 0x90, 0x8b, 0xf6, 0x8f, 0x65, 0x85, 0xd7, 0xfe,\n\t\t0xdc, 0x43, 0xb8, 0x23, 0x23, 0x15, 0xbb, 0x8d, 0x2f, 0x1e, 0xba, 0x36,\n\t\t0x96, 0xb2, 0x74, 0x42, 0x22, 0xd2, 0xfd, 0xba, 0x88, 0x5a, 0xf6, 0x1e,\n\t\t0xfb, 0xf0, 0xc9, 0x43, 0xbe, 0x34, 0x9f, 0xa9, 0xab, 0x4c, 0x12, 0x55,\n\t\t0x05, 0x28, 0x21, 0x16, 0x54, 0x2d, 0xfe, 0xe4, 0x2c, 0x97, 0x77, 0xcd,\n\t\t0xe1, 0xca, 0xe8, 0x12, 0x18, 0x7d, 0x7b, 0x80, 0x4d, 0xac, 0xd8, 0x6a,\n\t\t0x6c, 0xdf, 0x29, 0x12, 0xbb, 0xbb, 0xfe, 0x44, 0xc9, 0x1b, 0x32, 0x5e,\n\t\t0xa7, 0x44, 0x12, 0x27, 0x91, 0x38, 0x21, 0xae, 0x11, 0x10, 0x44, 0x9a,\n\t\t0x63, 0x6c, 0xea, 0x15, 0x17, 0x84, 0x6e, 0xba, 0x62, 0xe8, 0x43, 0x37,\n\t\t0x3d, 0x34, 0xa3, 0xd3, 0x86, 0xf2, 0xfd, 0x77, 0x02, 0xe0, 0x6d, 0x9d,\n\t\t0x0c, 0x0d, 0x16, 0xf4, 0x18, 0x91, 0x35, 0xf0, 0xbc, 0x61, 0x9e, 0x50,\n\t\t0x72, 0x9b, 0xfa, 0x4f, 0x35, 0x9d, 0xe4, 0x5c, 0x71, 0x8b, 0x4e, 0xb2,\n\t\t0xba, 0x3a, 0x91, 0x95, 0x0b, 0x08, 0xb2, 0x8b, 0x24, 0xcd, 0xec, 0xa1,\n\t\t0x68, 0xc1, 0x64, 0x5a, 0x3d, 0xff, 0x2f, 0xf6, 0x43, 0x83, 0x57, 0x23,\n\t\t0xd3, 0xdb, 0xa8, 0xde, 0xd4, 0xad, 0x94, 0x04, 0xd0, 0xa0, 0xff, 0x63,\n\t\t0xa1, 0x6b, 0x65, 0x84, 0x80, 0xa0, 0x1f, 0xfc, 0x68, 0xa3, 0x27, 0xbe,\n\t\t0x74, 0x53, 0x32, 0xb9, 0xee, 0x37, 0xa9, 0x24, 0xdb, 0xdb, 0xc0, 0x7a,\n\t\t0x77, 0x61, 0x72, 0x68, 0x11, 0x14, 0x46, 0x40, 0x7b, 0x76, 0x12, 0xe2,\n\t\t0xe1, 0x77, 0x07, 0xa0, 0xff, 0x93, 0x2f, 0x1a, 0x2a, 0x23, 0xac, 0x2d,\n\t\t0xd7, 0xcd, 0x34, 0x15, 0x4d, 0xe5, 0x9b, 0xf1, 0xfc, 0x93, 0xcf, 0x94,\n\t\t0x05, 0x3d, 0xba, 0xd5, 0xaf, 0x30, 0xaf, 0x78, 0xd9, 0xaa, 0xb2, 0x9d,\n\t\t0x52, 0x62, 0x67, 0x5b, 0x60, 0x14, 0x0d, 0xee, 0xa4, 0x22, 0x2d, 0xd6,\n\t\t0x97, 0xf4, 0x1f, 0x06, 0x4a, 0x29, 0x3b, 0x5d, 0xc3, 0x34, 0xbb, 0xff,\n\t\t0x00, 0xed, 0x10, 0xcf, 0x17, 0xf9, 0x75, 0x9f, 0xc8, 0xf8, 0xd2, 0x59,\n\t\t0x96, 0xbb, 0x7d, 0xb9, 0xc3, 0x0e, 0xac, 0x14, 0xb5, 0x35, 0x31, 0x2c,\n\t\t0xa9, 0x39, 0x0f, 0xbb, 0xaa, 0x18, 0xae, 0x19, 0x1a, 0xa1, 0x83, 0x7a,\n\t\t0x81, 0x7d, 0xbd, 0x15, 0xce, 0xde, 0x2a, 0xea, 0xfe, 0xf6, 0xeb, 0x51,\n\t\t0xea, 0xfa, 0xc2, 0x5f, 0x2b, 0x7d, 0xef, 0x7b, 0x8a, 0x62, 0xcb, 0xc7,\n\t\t0xce, 0xe1, 0x83, 0x4d, 0x18, 0xda, 0x17, 0x24, 0x29, 0x55, 0x28, 0x7a,\n\t\t0x10, 0x4c, 0x3a, 0x1e, 0x7b, 0x7f, 0xe6, 0x35, 0x2c, 0x90, 0x9e, 0xca,\n\t\t0x64, 0x1b, 0xa9, 0x0e, 0x53, 0x35, 0x54, 0x7e, 0xb5, 0x0c, 0xdb, 0x1d,\n\t\t0x5f, 0xf7, 0xcf, 0x66, 0xbf, 0xbb, 0x70, 0xe9, 0xd7, 0xdb, 0x56, 0x11,\n\t\t0x60, 0x83, 0x04, 0x22, 0xdf, 0xa8, 0x99, 0x6c, 0xfa, 0x12, 0x24, 0x38,\n\t\t0x12, 0xb9, 0x59, 0x29, 0xcf, 0xbe, 0x70, 0xdf, 0x95, 0x53, 0x86, 0x97,\n\t\t0xf4, 0xf6, 0xfc, 0x49, 0x85, 0x15, 0x93, 0x68, 0x33, 0x4c, 0x13, 0x42,\n\t\t0x60, 0xc7, 0xa2, 0xc8, 0xb8, 0xf8, 0x5a, 0x4f, 0x78, 0xc0, 0x48, 0xae,\n\t\t0xd4, 0x6c, 0x13, 0xbc, 0x7a, 0xbf, 0xeb, 0x1e, 0x9a, 0xe1, 0x84, 0x65,\n\t\t0x32, 0x89, 0xf4, 0xbd, 0x45, 0xfe, 0x28, 0xf8, 0xb4, 0x76, 0xf9, 0x45,\n\t\t0x50, 0xe3, 0x71, 0xb0, 0x75, 0x3b, 0x10, 0xef, 0xd1, 0x59, 0x50, 0x6a,\n\t\t0x66, 0xb9, 0xe7, 0x5d, 0x3f, 0xd1, 0x2b, 0xa2, 0x4d, 0x88, 0xdf, 0xbf,\n\t\t0x90, 0x6d, 0xfc, 0xc7, 0x2c, 0xf3, 0xae, 0xdf, 0xff, 0xd9, 0xed, 0x9a,\n\t\t0xeb, 0x1e, 0xf5, 0x1a, 0x08, 0x55, 0x1e, 0xdc, 0x8f, 0x8f, 0x3e, 0xfb,\n\t\t0x6d, 0x7c, 0xe0, 0xd4, 0x29, 0x7a, 0xd1, 0xb8, 0x51, 0xaa, 0xa6, 0x11,\n\t\t0xe6, 0xc4, 0xa2, 0x27, 0x04, 0x58, 0xc9, 0x39, 0x02, 0xbb, 0xd7, 0x86,\n\t\t0x9a, 0x81, 0x5d, 0x00, 0x76, 0x09, 0x09, 0x50, 0x72, 0xcc, 0x66, 0x54,\n\t\t0x46, 0x41, 0x82, 0x23, 0xe1, 0xd1, 0xb5, 0xb7, 0xe6, 0xfe, 0xf9, 0x27,\n\t\t0x53, 0x86, 0x16, 0x77, 0x47, 0xed, 0xd1, 0x70, 0xcc, 0x1f, 0xa9, 0x45,\n\t\t0x34, 0x29, 0x17, 0xa4, 0x9d, 0xb6, 0x39, 0x11, 0x2e, 0x8c, 0x60, 0x90,\n\t\t0x21, 0x38, 0x8c, 0x91, 0xc2, 0x11, 0x6a, 0x03, 0xcd, 0x20, 0xbd, 0x56,\n\t\t0xde, 0x63, 0x06, 0x2b, 0x7e, 0xa6, 0x2c, 0x89, 0x38, 0xf2, 0x86, 0x11,\n\t\t0x06, 0xf7, 0x07, 0x4d, 0x65, 0xec, 0x10, 0xb0, 0x45, 0xab, 0xe0, 0x6a,\n\t\t0x3a, 0x75, 0x05, 0x97, 0x38, 0xb2, 0x6d, 0x5b, 0x7c, 0xe3, 0xb3, 0x8f,\n\t\t0xf1, 0x09, 0xb9, 0x5b, 0xdc, 0x3b, 0x1f, 0xb4, 0x8d, 0xf4, 0x54, 0x24,\n\t\t0xcb, 0x16, 0x87, 0xe4, 0x9b, 0x21, 0xe6, 0x3a, 0xa5, 0x9b, 0xe7, 0xdb,\n\t\t0x7f, 0x9a, 0xea, 0x8f, 0xdb, 0xc3, 0xee, 0xd4, 0xfa, 0xdf, 0x3a, 0xdd,\n\t\t0xa3, 0x79, 0x34, 0xd6, 0x9a, 0x6d, 0x08, 0xce, 0xe1, 0xc6, 0xe3, 0x27,\n\t\t0x6c, 0x4a, 0x1c, 0xfb, 0x67, 0x88, 0x16, 0xd4, 0xd3, 0xdf, 0x9f, 0xf9,\n\t\t0xd0, 0xa3, 0xc3, 0x87, 0xf4, 0x96, 0x5f, 0xaf, 0x2d, 0x37, 0x67, 0xcd,\n\t\t0x5f, 0x23, 0x36, 0x99, 0x5e, 0x92, 0xd5, 0xb7, 0x48, 0x13, 0x9c, 0x77,\n\t\t0x38, 0x10, 0x26, 0xf7, 0xec, 0xab, 0xec, 0x4e, 0xbd, 0x9a, 0x59, 0x82,\n\t\t0x3a, 0x5b, 0xca, 0x9a, 0x64, 0x49, 0x6e, 0xa3, 0x9a, 0x97, 0x21, 0x65,\n\t\t0x7e, 0x1f, 0xa9, 0xfd, 0x73, 0x2e, 0x31, 0x6b, 0xfa, 0xdf, 0xaf, 0x3b,\n\t\t0x2b, 0x5e, 0xb3, 0x4a, 0x36, 0xdc, 0x8b, 0xd9, 0x8f, 0x1d, 0xd4, 0x86,\n\t\t0x9d, 0xef, 0xf8, 0x93, 0x34, 0x30, 0x8f, 0x06, 0xa6, 0x33, 0x40, 0x67,\n\t\t0x20, 0x86, 0x22, 0x59, 0xef, 0x02, 0x47, 0xbb, 0xed, 0xfa, 0x98, 0xa7,\n\t\t0x24, 0xbe, 0x9c, 0x2f, 0x7f, 0x6b, 0x9e, 0xd9, 0xa8, 0x75, 0x13, 0xfe,\n\t\t0xfc, 0x02, 0x55, 0xd1, 0x54, 0x54, 0x2e, 0xff, 0x9c, 0x1f, 0xde, 0xb8,\n\t\t0xf6, 0x39, 0x6e, 0xdb, 0xb1, 0xb6, 0x5c, 0x33, 0xe9, 0xd7, 0x33, 0xef,\n\t\t0x9e, 0x92, 0xbe, 0x5d, 0xb3, 0x5e, 0xf9, 0xe7, 0xe2, 0xad, 0xcf, 0xbd,\n\t\t0x5b, 0x5a, 0x49, 0x27, 0x3f, 0xd0, 0xbd, 0xd7, 0xb8, 0xf1, 0x3a, 0xb7,\n\t\t0xcc, 0xb3, 0xca, 0x92, 0x25, 0x77, 0x91, 0x9c, 0x9d, 0xc6, 0x7c, 0x03,\n\t\t0xc7, 0xea, 0x69, 0x23, 0xae, 0x52, 0x16, 0xbf, 0x3c, 0xc7, 0xba, 0xe1,\n\t\t0xc2, 0xb8, 0xa2, 0x30, 0x49, 0x73, 0x52, 0xc1, 0x3f, 0x7f, 0x7b, 0xa9,\n\t\t0x3b, 0x63, 0xe2, 0x4a, 0x7d, 0xc2, 0x04, 0x78, 0x9e, 0x7b, 0x03, 0xe6,\n\t\t0x43, 0xcf, 0x02, 0x43, 0xfa, 0x02, 0x79, 0x99, 0xa0, 0x94, 0xc2, 0xa1,\n\t\t0x14, 0x14, 0x00, 0xe1, 0x2e, 0x20, 0x12, 0x40, 0x4e, 0x2e, 0xd4, 0x09,\n\t\t0xfd, 0xc3, 0x6a, 0x62, 0xdd, 0x7b, 0x7c, 0xd9, 0x1a, 0xc7, 0x0c, 0x16,\n\t\t0x8f, 0xd4, 0x6b, 0xd6, 0x95, 0xba, 0x87, 0xd7, 0xaf, 0x79, 0x4e, 0x38,\n\t\t0x4e, 0x9b, 0x60, 0x50, 0xd7, 0x18, 0x29, 0x7f, 0xff, 0xd3, 0xb5, 0x1f,\n\t\t0x6f, 0xd8, 0x5e, 0xf1, 0x12, 0xeb, 0xd4, 0xd5, 0xee, 0x37, 0xf1, 0xda,\n\t\t0xab, 0xf5, 0x50, 0xe8, 0xdc, 0x87, 0x78, 0x5c, 0x1b, 0xde, 0x24, 0x3f,\n\t\t0x6d, 0x66, 0xe9, 0xc2, 0x5f, 0xb6, 0x40, 0xd4, 0xc5, 0xa8, 0xbd, 0xb0,\n\t\t0x54, 0x5a, 0x33, 0x1f, 0x72, 0x02, 0x59, 0xe9, 0x50, 0x61, 0xb7, 0xe4,\n\t\t0x83, 0x6b, 0xcb, 0xe0, 0x8e, 0x1d, 0x02, 0xc7, 0x36, 0xa9, 0x59, 0xb6,\n\t\t0x0f, 0xac, 0xbe, 0x91, 0x58, 0x3a, 0x83, 0xa5, 0x6b, 0x44, 0xaa, 0x1a,\n\t\t0x98, 0xeb, 0x80, 0x10, 0x06, 0x32, 0xa8, 0x0f, 0xd4, 0x5e, 0xee, 0x6a,\n\t\t0xcc, 0x5b, 0xe6, 0x98, 0x0d, 0x8d, 0xb6, 0xd9, 0xb8, 0x6b, 0xdb, 0x0c,\n\t\t0xd7, 0x34, 0xe3, 0xa7, 0x80, 0x49, 0x4d, 0xf1, 0x23, 0x1e, 0xb7, 0x1b,\n\t\t0x00, 0x54, 0x01, 0x48, 0xf8, 0x92, 0x53, 0xfb, 0x77, 0xb9, 0xf8, 0xd2,\n\t\t0xab, 0xbd, 0xe9, 0x19, 0xdf, 0x8d, 0x06, 0xe0, 0x1c, 0x46, 0x7a, 0x36,\n\t\t0xfd, 0x7a, 0xc1, 0x6a, 0xab, 0xa1, 0xe2, 0x90, 0xfd, 0xcb, 0x3b, 0x10,\n\t\t0x32, 0x34, 0x50, 0x21, 0x00, 0x02, 0xc8, 0xac, 0x64, 0x66, 0x47, 0x63,\n\t\t0xba, 0xbb, 0x60, 0xd3, 0x20, 0xf9, 0x55, 0xe5, 0xa5, 0xfa, 0x11, 0x7e,\n\t\t0x39, 0x59, 0x77, 0x70, 0x30, 0x9d, 0xfd, 0x45, 0x8a, 0x5c, 0xb7, 0x35,\n\t\t0xc6, 0x89, 0xdd, 0x6c, 0xf7, 0xe8, 0xaa, 0x40, 0x70, 0xc1, 0x04, 0x07,\n\t\t0xc9, 0x49, 0x87, 0x12, 0x8a, 0x6d, 0x27, 0xb3, 0xe7, 0x1e, 0xa8, 0x6b,\n\t\t0x3a, 0x12, 0x79, 0x15, 0xd2, 0x4d, 0x9c, 0x92, 0xce, 0xd4, 0x37, 0x44,\n\t\t0x4f, 0x62, 0x2f, 0x2d, 0xe7, 0x5c, 0x4b, 0xe5, 0x53, 0x7a, 0x9c, 0x49,\n\t\t0x21, 0xda, 0x9c, 0x37, 0x9c, 0x5d, 0xd7, 0x6b, 0x9d, 0xa6, 0xa9, 0x20,\n\t\t0x52, 0x02, 0x8a, 0x02, 0xb9, 0xbb, 0x82, 0x46, 0x1f, 0x9a, 0x95, 0x4b,\n\t\t0x2e, 0x9e, 0xf8, 0xb8, 0x31, 0xac, 0xbf, 0xce, 0x87, 0x9e, 0x5f, 0xa2,\n\t\t0x51, 0x4a, 0x09, 0xa5, 0x04, 0x94, 0x32, 0x7d, 0xd7, 0xde, 0x6a, 0xb1,\n\t\t0x68, 0xc9, 0xbb, 0xe6, 0x57, 0x33, 0x5f, 0x75, 0x7e, 0x71, 0x07, 0x05,\n\t\t0x63, 0x42, 0x97, 0x12, 0xb8, 0xa4, 0x28, 0xac, 0x5e, 0xd2, 0x35, 0xec,\n\t\t0x7b, 0x6b, 0xcf, 0xa9, 0xd9, 0x0b, 0x6d, 0x9b, 0x07, 0x76, 0x38, 0xc4,\n\t\t0xf7, 0x33, 0x8f, 0x95, 0x68, 0x6c, 0x10, 0x43, 0xe4, 0x62, 0xd1, 0xaf,\n\t\t0x27, 0x54, 0x29, 0x01, 0x46, 0x81, 0xb2, 0xdd, 0x88, 0x3f, 0xf1, 0xaf,\n\t\t0x12, 0xfa, 0xc4, 0xef, 0xde, 0xf5, 0x8c, 0xbf, 0x7c, 0x94, 0xb6, 0x7f,\n\t\t0xdf, 0x6e, 0x5e, 0x5d, 0x7d, 0xc8, 0x31, 0x4d, 0x13, 0xb1, 0x58, 0x1c,\n\t\t0xcd, 0xcd, 0xcd, 0xc8, 0xcb, 0x09, 0xd1, 0x1f, 0x4c, 0xbb, 0xdb, 0x9b,\n\t\t0x3a, 0xf0, 0x39, 0x36, 0x6b, 0x9e, 0xc7, 0xe2, 0xbc, 0xa5, 0x53, 0xe0,\n\t\t0x0b, 0x00, 0x37, 0x8f, 0x47, 0x7a, 0x4e, 0x1a, 0x26, 0x74, 0x08, 0x0c,\n\t\t0x77, 0x5c, 0x26, 0xbe, 0x87, 0xe1, 0x1f, 0x42, 0x29, 0xa2, 0xbb, 0x36,\n\t\t0x3b, 0x83, 0x93, 0xb6, 0x2b, 0x86, 0xa7, 0xe5, 0xdd, 0x34, 0xc7, 0xc0,\n\t\t0xdf, 0x5f, 0xdd, 0x43, 0xde, 0xf9, 0xe3, 0xdf, 0x1a, 0x86, 0xce, 0x58,\n\t\t0x2c, 0x16, 0xc7, 0xe8, 0xd1, 0xa3, 0xbd, 0x29, 0x29, 0x29, 0xcc, 0xef,\n\t\t0xf7, 0xc1, 0xeb, 0xf5, 0x80, 0x52, 0x02, 0xce, 0x05, 0x5c, 0xd7, 0xc6,\n\t\t0xb0, 0x61, 0xc3, 0x3d, 0x35, 0xec, 0x76, 0xb2, 0xb7, 0x9a, 0x39, 0x04,\n\t\t0x80, 0x6b, 0x01, 0x97, 0x8f, 0x06, 0xba, 0xe4, 0xe0, 0xa9, 0x0e, 0x81,\n\t\t0x89, 0x1e, 0x3e, 0x68, 0x0a, 0xf7, 0x0c, 0x4c, 0x43, 0x47, 0x78, 0x33,\n\t\t0xc3, 0x40, 0xdd, 0x67, 0x6f, 0x89, 0x71, 0x17, 0x80, 0x88, 0x63, 0xab,\n\t\t0x55, 0xd4, 0xa8, 0x52, 0xcf, 0xba, 0x56, 0x09, 0x25, 0x27, 0x33, 0x21,\n\t\t0x04, 0x84, 0x10, 0xc8, 0xcd, 0xed, 0x44, 0x2b, 0xab, 0x9b, 0xf8, 0xb3,\n\t\t0x2f, 0xce, 0x8e, 0xcd, 0x79, 0xff, 0xcb, 0x04, 0xa5, 0x9a, 0xa0, 0xb4,\n\t\t0x45, 0x2f, 0x42, 0xc1, 0x00, 0xfa, 0x0f, 0xbe, 0x5c, 0x9b, 0x57, 0x9a,\n\t\t0xe9, 0xb0, 0xd6, 0x3e, 0xaf, 0x0b, 0xfc, 0x68, 0x12, 0x52, 0x00, 0x8c,\n\t\t0xe8, 0xc8, 0x20, 0x50, 0xb3, 0x6b, 0xc5, 0x9a, 0xbe, 0xab, 0xa2, 0x11,\n\t\t0xa6, 0x02, 0x7b, 0x96, 0xc5, 0x7d, 0x59, 0xd0, 0xc4, 0xb1, 0x00, 0x5e,\n\t\t0xdd, 0x18, 0x72, 0x8d, 0xe4, 0x01, 0x8a, 0xa6, 0xb2, 0xd6, 0x9e, 0x25,\n\t\t0x3e, 0x59, 0xfc, 0x45, 0x62, 0xfb, 0xe2, 0x09, 0xf6, 0x8b, 0x37, 0xfc,\n\t\t0xda, 0xb8, 0x32, 0xe7, 0x5e, 0xbc, 0x3e, 0xf3, 0xd1, 0x98, 0x65, 0x73,\n\t\t0x61, 0x59, 0x96, 0xfb, 0xcc, 0x33, 0xcf, 0x34, 0xef, 0xd8, 0xb6, 0xda,\n\t\t0x7d, 0xa7, 0xb4, 0xc0, 0x6a, 0xb5, 0x12, 0x61, 0x02, 0x13, 0x2f, 0x82,\n\t\t0x0e, 0x60, 0x58, 0x47, 0xc0, 0xd4, 0x36, 0x56, 0xec, 0xab, 0xc5, 0x77,\n\t\t0xf0, 0x02, 0x84, 0x52, 0x44, 0x1b, 0x23, 0x56, 0xcf, 0xcc, 0xb8, 0xaf,\n\t\t0x95, 0xfb, 0x60, 0x14, 0xd8, 0x71, 0x28, 0xd5, 0xcd, 0xcd, 0x49, 0x27,\n\t\t0xad, 0xb4, 0x55, 0x4d, 0x6d, 0x98, 0xd3, 0xda, 0x97, 0xc5, 0xcf, 0xa6,\n\t\t0x99, 0x7e, 0x4d, 0x03, 0x1b, 0x5c, 0x0c, 0xcf, 0xe4, 0xc1, 0x8b, 0xb4,\n\t\t0x2d, 0x9b, 0x56, 0x39, 0xa6, 0x99, 0xc0, 0xe4, 0xc9, 0x93, 0x8d, 0xbb,\n\t\t0xa7, 0xdf, 0xe9, 0x2b, 0x1a, 0x30, 0xc2, 0xb7, 0x6f, 0x3f, 0xe2, 0x8a,\n\t\t0xd2, 0x12, 0xed, 0xf5, 0x96, 0x53, 0xca, 0xeb, 0x08, 0x98, 0x1d, 0x47,\n\t\t0x36, 0xad, 0x6f, 0x70, 0xec, 0x04, 0xc7, 0x39, 0xce, 0x32, 0xb6, 0xe4,\n\t\t0x6e, 0xcd, 0xc2, 0xab, 0xc3, 0x6a, 0x5d, 0x81, 0x10, 0x20, 0x1c, 0xd5,\n\t\t0x5c, 0x8f, 0x47, 0x83, 0x94, 0x12, 0x94, 0x52, 0xc4, 0x13, 0x71, 0xd1,\n\t\t0x25, 0xed, 0x08, 0x65, 0xc7, 0xb8, 0x72, 0x6e, 0x03, 0xfd, 0xbb, 0x41,\n\t\t0x39, 0x54, 0xb5, 0x5b, 0x64, 0x66, 0x64, 0x2a, 0x19, 0x19, 0x19, 0x6a,\n\t\t0x63, 0xb8, 0x09, 0xd9, 0x99, 0xa9, 0xe4, 0x50, 0x2d, 0x74, 0xb0, 0x6f,\n\t\t0x4b, 0xd9, 0xdc, 0x0c, 0x84, 0x3a, 0x02, 0xc6, 0xaa, 0xfa, 0xea, 0xcb,\n\t\t0x2d, 0x66, 0x53, 0x83, 0x94, 0xe2, 0xdc, 0x66, 0x7e, 0x85, 0x94, 0x30,\n\t\t0x02, 0x49, 0x34, 0x62, 0xc2, 0x68, 0x7d, 0xba, 0x94, 0x40, 0x7a, 0xd0,\n\t\t0xd2, 0x13, 0x09, 0x87, 0x26, 0x25, 0x25, 0x21, 0x16, 0x8b, 0x59, 0x5e,\n\t\t0x8f, 0x8f, 0x1d, 0x68, 0xc8, 0xe7, 0xf6, 0xb1, 0x5e, 0x1d, 0x53, 0x80,\n\t\t0x8d, 0xbb, 0xe0, 0xe6, 0xe6, 0xf5, 0x22, 0x8e, 0xeb, 0x42, 0x08, 0x01,\n\t\t0x45, 0x61, 0xa8, 0xac, 0x3a, 0x2c, 0xba, 0xe4, 0x20, 0xd1, 0xda, 0xc8,\n\t\t0x90, 0x00, 0x0e, 0xd6, 0xa2, 0xbe, 0x23, 0x60, 0x60, 0x86, 0x1b, 0xde,\n\t\t0xae, 0xf8, 0x72, 0x99, 0x74, 0x5b, 0xf2, 0x99, 0x73, 0x40, 0x23, 0xe0,\n\t\t0x0b, 0x25, 0xe9, 0xbb, 0x6b, 0x92, 0xbe, 0x49, 0x39, 0xb8, 0x00, 0xfa,\n\t\t0xe4, 0x37, 0x62, 0xc3, 0x86, 0x4d, 0x89, 0x27, 0x9e, 0x78, 0xa2, 0x71,\n\t\t0xce, 0x9c, 0x39, 0xa2, 0x4b, 0xe7, 0x2c, 0x62, 0x86, 0xee, 0x52, 0xfe,\n\t\t0xba, 0x30, 0xab, 0xb9, 0x36, 0xac, 0x89, 0xf5, 0x3b, 0x0c, 0xfb, 0xfd,\n\t\t0x2d, 0x57, 0x5b, 0xc5, 0x83, 0x86, 0xea, 0xad, 0xb3, 0x15, 0x52, 0x52,\n\t\t0x54, 0xee, 0x5b, 0x1b, 0xcf, 0xeb, 0x02, 0xbf, 0xeb, 0xb4, 0x9c, 0x70,\n\t\t0xb4, 0x25, 0xf6, 0x57, 0x9c, 0xa9, 0x6c, 0x6e, 0x95, 0x4a, 0xaa, 0x28,\n\t\t0x13, 0x3a, 0x0d, 0xbd, 0xa0, 0x8b, 0xa2, 0xa8, 0x16, 0x55, 0x54, 0x86,\n\t\t0xb3, 0x44, 0x45, 0x18, 0xc5, 0xd1, 0x1d, 0xdb, 0x31, 0x3e, 0xbb, 0x1c,\n\t\t0x5e, 0x1f, 0x98, 0x94, 0x80, 0x69, 0x9a, 0x64, 0xfd, 0xde, 0xb4, 0xf8,\n\t\t0xdd, 0xf7, 0x3e, 0xe6, 0xbb, 0xe8, 0xa2, 0x11, 0x46, 0x38, 0x1c, 0x26,\n\t\t0xc5, 0xfd, 0x7a, 0x2a, 0xf5, 0x72, 0x14, 0x5d, 0xba, 0xad, 0x8f, 0x55,\n\t\t0x65, 0x5f, 0x8b, 0x49, 0x37, 0xdc, 0xea, 0x63, 0x44, 0x10, 0x29, 0x25,\n\t\t0x14, 0x45, 0xc1, 0xba, 0x8d, 0x7b, 0x62, 0xdd, 0xf4, 0x7f, 0x28, 0x43,\n\t\t0x7b, 0x46, 0x35, 0x21, 0x01, 0xe6, 0x03, 0xfe, 0xf1, 0x21, 0x9c, 0x85,\n\t\t0xa5, 0x78, 0x12, 0xc0, 0x91, 0x8e, 0x80, 0x41, 0xa4, 0xb2, 0x22, 0x91,\n\t\t0x3d, 0xe4, 0xfc, 0xab, 0x3d, 0x69, 0xe9, 0x8c, 0x31, 0xc5, 0x25, 0x94,\n\t\t0xb1, 0xb3, 0xe1, 0x65, 0xa5, 0x10, 0x90, 0x9e, 0x90, 0x24, 0xab, 0xe7,\n\t\t0x58, 0x03, 0xfa, 0x41, 0x13, 0x1c, 0x24, 0xc9, 0x2b, 0xc9, 0xa6, 0xf2,\n\t\t0x5a, 0x61, 0xa4, 0x8e, 0xa4, 0x8c, 0x51, 0x57, 0x55, 0x55, 0xc5, 0x34,\n\t\t0x2d, 0x64, 0x67, 0x06, 0x59, 0xdf, 0x3e, 0x85, 0x5a, 0x5e, 0x6e, 0x1a,\n\t\t0xdb, 0xbd, 0x6b, 0x87, 0x1b, 0x0a, 0x85, 0x18, 0x63, 0x0c, 0xd1, 0x98,\n\t\t0x15, 0xfb, 0xf2, 0xd3, 0xbf, 0x8a, 0x9b, 0x86, 0xad, 0x36, 0x92, 0x7c,\n\t\t0x2d, 0x9a, 0x44, 0x0d, 0x60, 0xfa, 0xd3, 0xa8, 0x3a, 0x7c, 0x14, 0x8f,\n\t\t0x77, 0x48, 0xcd, 0x8e, 0x35, 0x6b, 0xff, 0xbd, 0x7e, 0xc6, 0x0b, 0xeb,\n\t\t0x24, 0x24, 0x4d, 0xc4, 0xc2, 0x90, 0x42, 0x9c, 0x55, 0xec, 0x91, 0x42,\n\t\t0xc0, 0xdf, 0xb5, 0x50, 0x59, 0x15, 0x1d, 0x88, 0xa6, 0xfa, 0x96, 0x1e,\n\t\t0xa4, 0xa1, 0x83, 0xdc, 0x36, 0xf6, 0x88, 0xfe, 0xca, 0xf3, 0xf7, 0xd9,\n\t\t0xcd, 0xcd, 0x09, 0xc5, 0xb2, 0xcc, 0x38, 0x21, 0x44, 0xb8, 0xae, 0x2b,\n\t\t0x2d, 0xcb, 0x84, 0xa2, 0x28, 0x74, 0xcb, 0x96, 0x2d, 0xb6, 0x99, 0x48,\n\t\t0xd8, 0xb1, 0x58, 0x3c, 0xfa, 0xc5, 0xb2, 0x45, 0xac, 0x6f, 0xd2, 0xdb,\n\t\t0x4a, 0xe7, 0x6c, 0xc9, 0x5a, 0x46, 0x58, 0x80, 0xb9, 0x1f, 0x01, 0xf5,\n\t\t0x61, 0x3c, 0xd2, 0x11, 0x76, 0xe6, 0xa4, 0x6c, 0xc0, 0xde, 0x0c, 0x42,\n\t\t0x6f, 0x4a, 0x2f, 0xea, 0x67, 0x70, 0xcb, 0x72, 0x14, 0x4d, 0x07, 0x21,\n\t\t0xac, 0xc3, 0x93, 0x1a, 0xaa, 0xd7, 0x47, 0x2a, 0x6b, 0x04, 0xc9, 0xa8,\n\t\t0x2d, 0xe5, 0x9d, 0xb3, 0x24, 0x28, 0x15, 0x34, 0x18, 0x02, 0x1b, 0x33,\n\t\t0xa0, 0x89, 0x3d, 0xf3, 0xc2, 0x82, 0xb8, 0xab, 0x74, 0x56, 0xbd, 0xbe,\n\t\t0x80, 0x4b, 0x40, 0x5c, 0x29, 0x01, 0x2e, 0x04, 0xb7, 0x1d, 0x57, 0x1c,\n\t\t0x3c, 0x54, 0xcd, 0x57, 0xaf, 0xf8, 0x88, 0x06, 0x23, 0x2f, 0xe0, 0xc7,\n\t\t0x53, 0x60, 0x48, 0xde, 0x42, 0x41, 0x87, 0x23, 0xc0, 0x8c, 0x39, 0x28,\n\t\t0xfd, 0x6a, 0x13, 0xfe, 0xe7, 0xe4, 0x26, 0xed, 0x19, 0xc1, 0xb8, 0xa6,\n\t\t0x79, 0x38, 0x5e, 0x53, 0x63, 0x26, 0x77, 0xef, 0x39, 0xc6, 0x9b, 0x99,\n\t\t0xa1, 0x3a, 0xb6, 0x29, 0x18, 0x53, 0x5d, 0xca, 0x3a, 0xa6, 0x72, 0x04,\n\t\t0x00, 0xcb, 0x2c, 0x20, 0xa5, 0x1f, 0x6f, 0xe0, 0x17, 0x66, 0x1f, 0x26,\n\t\t0x5e, 0x83, 0x49, 0x29, 0x84, 0x0c, 0x26, 0x41, 0xb9, 0xea, 0x82, 0x84,\n\t\t0x5a, 0x5b, 0xb1, 0x44, 0xac, 0x59, 0xbb, 0x5e, 0xec, 0xdb, 0xbb, 0x9b,\n\t\t0xee, 0xdb, 0x5b, 0x16, 0xdf, 0x5d, 0xb6, 0x92, 0x37, 0x1c, 0x2a, 0x65,\n\t\t0x9e, 0xe6, 0x39, 0x98, 0x50, 0xfc, 0xb5, 0x3e, 0xe9, 0x72, 0x68, 0xc2,\n\t\t0x06, 0x11, 0x80, 0xcb, 0xb9, 0x22, 0x66, 0x2f, 0xa6, 0x35, 0xaf, 0x7c,\n\t\t0x20, 0xef, 0x8f, 0x5b, 0xd8, 0xd9, 0xd6, 0xb3, 0x3a, 0x22, 0x46, 0xe6,\n\t\t0x80, 0x41, 0xaf, 0x5c, 0xf4, 0xeb, 0x67, 0x6e, 0xf5, 0x65, 0x66, 0x41,\n\t\t0x72, 0xce, 0x35, 0x6f, 0xc0, 0xd1, 0x0c, 0xbf, 0xd1, 0x91, 0x30, 0x44,\n\t\t0x55, 0x15, 0xb5, 0x07, 0xaa, 0xdd, 0xba, 0xdf, 0x5e, 0x69, 0xff, 0xf5,\n\t\t0xae, 0x23, 0x5a, 0x6a, 0xaa, 0x0e, 0xaa, 0x58, 0x8e, 0x47, 0x77, 0x3d,\n\t\t0x94, 0x02, 0x2e, 0x87, 0xac, 0x6b, 0x84, 0x08, 0x47, 0x21, 0x54, 0x05,\n\t\t0xc8, 0x4c, 0x06, 0xf3, 0x79, 0x40, 0x5b, 0xbf, 0xe3, 0x9c, 0x59, 0x52,\n\t\t0x68, 0xea, 0x07, 0x5f, 0x48, 0xfb, 0x91, 0x99, 0xf6, 0xaf, 0x0f, 0xd5,\n\t\t0x89, 0x3f, 0xb6, 0x15, 0xff, 0x3a, 0x6a, 0xd0, 0x6e, 0xec, 0xc8, 0xe1,\n\t\t0xcf, 0x6b, 0xb7, 0x6c, 0x1a, 0x90, 0x5d, 0x32, 0xb8, 0xbb, 0x37, 0x2d,\n\t\t0x83, 0x5a, 0xb1, 0x66, 0x85, 0x3b, 0x56, 0x9c, 0xa9, 0x1a, 0x28, 0x63,\n\t\t0xb4, 0xbd, 0x17, 0x23, 0x85, 0x80, 0x3f, 0x94, 0x44, 0x03, 0xc3, 0x27,\n\t\t0x93, 0xbf, 0xbd, 0xb4, 0xdc, 0xea, 0xed, 0xab, 0x62, 0x19, 0x29, 0x5e,\n\t\t0x3d, 0x96, 0x50, 0x4c, 0x29, 0xa5, 0xa0, 0x54, 0x22, 0xe0, 0x03, 0xd2,\n\t\t0x93, 0xa1, 0x24, 0x07, 0xc0, 0x54, 0x05, 0x9c, 0x73, 0xc2, 0x13, 0xa6,\n\t\t0xe2, 0xd8, 0x96, 0xe1, 0x86, 0x9b, 0x35, 0xcf, 0x8c, 0x77, 0x78, 0xe2,\n\t\t0x89, 0x57, 0xed, 0x27, 0x8f, 0x36, 0xc9, 0x3f, 0x9d, 0x76, 0xd8, 0xee,\n\t\t0x2c, 0xec, 0xd9, 0x8a, 0xd7, 0xd5, 0xbe, 0x77, 0x64, 0xc3, 0xba, 0xee,\n\t\t0xc1, 0xae, 0x05, 0x45, 0x81, 0x9c, 0x5c, 0x08, 0xd7, 0x51, 0x1d, 0x33,\n\t\t0x4e, 0x84, 0xe0, 0x0e, 0x53, 0x34, 0x42, 0x5a, 0x9a, 0xa0, 0xe4, 0x74,\n\t\t0xe3, 0x55, 0x8a, 0xa1, 0xd3, 0xd0, 0x88, 0x49, 0xca, 0xdc, 0xc5, 0x51,\n\t\t0xfb, 0xc8, 0xb6, 0x32, 0x5e, 0xdc, 0x55, 0x68, 0xdc, 0xd5, 0x98, 0xe3,\n\t\t0xaa, 0xd2, 0xb1, 0x55, 0x58, 0x96, 0x22, 0x2c, 0x4b, 0x75, 0x2d, 0x4b,\n\t\t0xa5, 0x09, 0x53, 0x83, 0x69, 0x6a, 0xfa, 0xe6, 0x3d, 0x92, 0x3e, 0xf9,\n\t\t0xaa, 0xb5, 0xe6, 0xb5, 0xf9, 0xce, 0x2d, 0xb6, 0x83, 0x77, 0xce, 0xa4,\n\t\t0xd2, 0x67, 0x9d, 0xa9, 0xf8, 0xf3, 0xbb, 0xfc, 0x4f, 0xf1, 0x0f, 0xa7,\n\t\t0x3f, 0xd8, 0x65, 0xf4, 0x25, 0x01, 0x55, 0xd3, 0xc8, 0xb1, 0x56, 0x9e,\n\t\t0xab, 0x1a, 0x5e, 0x57, 0xd5, 0xbd, 0x0a, 0x65, 0x0a, 0x21, 0x84, 0xb0,\n\t\t0xb6, 0x06, 0x32, 0x08, 0xa1, 0xa0, 0xba, 0x07, 0x55, 0xeb, 0xd7, 0x3b,\n\t\t0x15, 0xef, 0xbc, 0x6a, 0x8d, 0x4a, 0xd9, 0x80, 0xe2, 0xf4, 0x23, 0x6a,\n\t\t0xb2, 0xd7, 0x51, 0x0c, 0x9d, 0x4a, 0xc7, 0x11, 0xc4, 0xb4, 0x89, 0xd3,\n\t\t0xd8, 0x8c, 0xc3, 0xf3, 0x4b, 0xdd, 0xed, 0x0b, 0x56, 0xb8, 0xf3, 0x01,\n\t\t0xfc, 0x15, 0x6d, 0xfc, 0xc6, 0xec, 0xfb, 0x00, 0x93, 0xdc, 0xb7, 0x6b,\n\t\t0xd6, 0xfd, 0x13, 0xc7, 0x5d, 0x70, 0x4f, 0x19, 0x49, 0xf6, 0xc5, 0x7a,\n\t\t0x9f, 0xef, 0xcf, 0x39, 0xaf, 0x04, 0x4c, 0xd7, 0xe1, 0xc6, 0xe3, 0x20,\n\t\t0x94, 0x72, 0xaa, 0xa8, 0x42, 0x51, 0x34, 0x49, 0x15, 0x95, 0x50, 0xaa,\n\t\t0x08, 0x42, 0x89, 0x7a, 0x0c, 0x16, 0x91, 0x42, 0x70, 0x29, 0xb8, 0x94,\n\t\t0x04, 0x44, 0x00, 0xa8, 0xd9, 0x53, 0x21, 0xcc, 0x48, 0x13, 0x15, 0x47,\n\t\t0xf6, 0x98, 0x8d, 0xa5, 0x9f, 0xda, 0xce, 0xae, 0x32, 0x75, 0xc7, 0xfe,\n\t\t0xc6, 0xb9, 0x8e, 0x63, 0xcd, 0x01, 0xb0, 0xb4, 0xf5, 0xa1, 0x6f, 0x3c,\n\t\t0xee, 0xc5, 0xb4, 0xdf, 0xb7, 0x3f, 0x21, 0xc6, 0xce, 0x12, 0x48, 0x97,\n\t\t0xe9, 0x93, 0x2f, 0x59, 0xf4, 0xc7, 0xc7, 0x6e, 0x99, 0x64, 0xd9, 0x76,\n\t\t0xd2, 0xd6, 0xd2, 0xb5, 0xf5, 0x4b, 0x67, 0xbf, 0xf7, 0x76, 0xf4, 0xc0,\n\t\t0x7e, 0x0f, 0xa5, 0x34, 0x3d, 0xa3, 0x5f, 0x31, 0x08, 0xa1, 0x94, 0x3b,\n\t\t0x16, 0xe3, 0xae, 0x43, 0x5d, 0xcb, 0xa4, 0x8e, 0x1d, 0x67, 0x8e, 0x19,\n\t\t0x87, 0x63, 0xc6, 0x85, 0x63, 0xc6, 0x89, 0x63, 0x25, 0x14, 0xd7, 0x4a,\n\t\t0x50, 0xee, 0x3a, 0x8c, 0x29, 0x8c, 0x19, 0x5e, 0x4d, 0x39, 0xb2, 0xf2,\n\t\t0x4b, 0xb6, 0xe3, 0xfd, 0xf7, 0xf4, 0xc1, 0x41, 0xea, 0x7d, 0xe1, 0x67,\n\t\t0xd3, 0xc4, 0x0f, 0xaf, 0x1b, 0xd9, 0xa7, 0xb8, 0x77, 0xe7, 0xa1, 0x9b,\n\t\t0xb6, 0x1f, 0xe8, 0xda, 0x1c, 0x4b, 0x2c, 0x01, 0x80, 0x79, 0x2b, 0x1c,\n\t\t0x7c, 0xdf, 0x27, 0x93, 0x17, 0x0a, 0x78, 0x3f, 0x55, 0x55, 0x56, 0x59,\n\t\t0xd7, 0xd0, 0xfc, 0x22, 0x80, 0x4f, 0xd1, 0x32, 0x3d, 0x4b, 0x01, 0x4c,\n\t\t0xf6, 0x67, 0x65, 0x3f, 0xd1, 0x67, 0xca, 0x2d, 0x79, 0x5d, 0x2f, 0x1d,\n\t\t0xa7, 0xa9, 0x1e, 0xaf, 0x0e, 0x42, 0x00, 0x4a, 0x48, 0xeb, 0x8c, 0xa4,\n\t\t0x84, 0x04, 0x84, 0x94, 0x84, 0x10, 0xc4, 0x8f, 0x1e, 0x8d, 0xef, 0x9a,\n\t\t0xff, 0xbe, 0xb5, 0xeb, 0xc3, 0xf7, 0x37, 0x5a, 0xe1, 0xc6, 0xc7, 0x01,\n\t\t0x6c, 0xf5, 0x7a, 0xb4, 0x31, 0x9c, 0xcb, 0x1b, 0x7b, 0x17, 0xe4, 0x0c,\n\t\t0x2f, 0xec, 0x9a, 0x1d, 0x58, 0xb1, 0x61, 0x97, 0x73, 0xb0, 0xa6, 0xa1,\n\t\t0x27, 0x4e, 0xf3, 0x83, 0xb9, 0xef, 0x43, 0xcd, 0xce, 0x24, 0x7d, 0x01,\n\t\t0x8c, 0x4b, 0xca, 0xef, 0xdc, 0x2d, 0xb3, 0x78, 0x60, 0x4a, 0x20, 0x37,\n\t\t0x2f, 0x59, 0xf5, 0x7a, 0x15, 0x48, 0x10, 0x2b, 0xda, 0x6c, 0x45, 0x0e,\n\t\t0x54, 0x34, 0x1c, 0x5a, 0xf5, 0x75, 0xbd, 0xd5, 0x14, 0x2e, 0x03, 0xb0,\n\t\t0x10, 0xc0, 0xa1, 0x76, 0x3b, 0x53, 0x67, 0x63, 0xcc, 0xf8, 0xcf, 0x8a,\n\t\t0x81, 0x96, 0x9f, 0x86, 0xb4, 0x16, 0xbc, 0xf6, 0xb1, 0xb7, 0x6c, 0xe3,\n\t\t0xff, 0x4b, 0xfb, 0xf2, 0x7f, 0x57, 0xd0, 0xf3, 0xb9, 0xb7, 0xbc, 0xbe,\n\t\t0x56, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60,\n\t\t0x82, 0x01, 0x00, 0x00, 0xff, 0xff, 0x45, 0x7f, 0x81, 0x36, 0x56, 0x1d,\n\t\t0x00, 0x00,\n\t}\n}\n"
  },
  {
    "path": "internal/notify/notify_darwin.go",
    "content": "//go:build darwin\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n)\n\nconst (\n\tterminalNotifier string = \"terminal-notifier\"\n\tosascript        string = \"osascript\"\n)\n\nvar (\n\texecCommand  = exec.Command\n\texecLookPath = exec.LookPath\n)\n\n// Notify displays a desktop notification using osascript.\nfunc Notify(ctx context.Context, subj, msg string) error {\n\tif os.Getenv(\"GOPASS_NO_NOTIFY\") != \"\" || !config.Bool(ctx, \"core.notifications\") {\n\t\treturn nil\n\t}\n\n\t// check if terminal-notifier was installed else use the applescript fallback\n\ttn, _ := executableExists(terminalNotifier)\n\tif tn {\n\t\treturn tnNotification(ctx, msg, subj)\n\t}\n\n\treturn osaNotification(msg, subj)\n}\n\n// display notification with osascript.\nfunc osaNotification(msg string, subj string) error {\n\t_, err := executableExists(osascript)\n\tif err != nil {\n\t\treturn err\n\t}\n\targs := []string{\"-e\", `display notification \"` + msg + `\" with title \"` + subj + `\"`}\n\n\treturn execNotification(osascript, args)\n}\n\n// exec notification program with passed arguments.\nfunc execNotification(executable string, args []string) error {\n\treturn execCommand(executable, args...).Start()\n}\n\n// display notification with terminal-notifier.\nfunc tnNotification(ctx context.Context, msg string, subj string) error {\n\targuments := []string{\"-title\", \"Gopass\", \"-message\", msg, \"-subtitle\", subj, \"-appIcon\", iconURI(ctx)}\n\n\treturn execNotification(terminalNotifier, arguments)\n}\n\n// check if executable exists.\nfunc executableExists(executable string) (bool, error) {\n\t_, err := execLookPath(executable)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/notify/notify_darwin_test.go",
    "content": "package notify\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// to test cmd.exec correctly we use the same functionality as go itself see exec_test.go.\nfunc TestDarwinNotify(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\trequire.NoError(t, Notify(ctx, \"foo\", \"bar\"))\n}\n\nfunc TestLegacyNotification(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\t// override execCommand with mock\n\texecCommand = mockExecCommand\n\tdefer func() {\n\t\texecCommand = exec.Command\n\t}()\n\n\terr := Notify(ctx, \"foo\", \"bar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLegacyTerminalNotifierNotification(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\t// override execCommand with mock\n\texecCommand = mockExecCommand\n\texecLookPath = mockExecLookPathTerminalNotifier\n\tdefer func() {\n\t\texecCommand = exec.Command\n\t}()\n\n\terr := Notify(ctx, \"foo\", \"bar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestNoExecutableFound(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\t// override execCommand with mock\n\texecCommand = mockExecCommand\n\texecLookPath = mockExecLookPath\n\tdefer func() {\n\t\texecCommand = exec.Command\n\t}()\n\n\terr := Notify(ctx, \"foo\", \"bar\")\n\trequire.Error(t, err)\n}\n\nfunc mockExecLookPath(_ string) (string, error) {\n\treturn \"\", fmt.Errorf(\"no binary found\")\n}\n\nfunc mockExecLookPathTerminalNotifier(command string) (string, error) {\n\tif command == terminalNotifier {\n\t\treturn \"\", fmt.Errorf(\"no binary found\")\n\t}\n\n\treturn \"\", nil\n}\n\nfunc mockExecCommand(command string, args ...string) *exec.Cmd {\n\tcs := []string{\"-test.run=TestHelperProcess\", \"--\", command}\n\tcs = append(cs, args...)\n\tcmd := exec.Command(os.Args[0], cs...)\n\tcmd.Env = []string{\"GO_WANT_HELPER_PROCESS=1\"}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/notify/notify_dbus.go",
    "content": "//go:build linux\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/godbus/dbus/v5\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Notify displays a desktop notification with dbus.\nfunc Notify(ctx context.Context, subj, msg string) error {\n\tif os.Getenv(\"GOPASS_NO_NOTIFY\") != \"\" || !config.Bool(ctx, \"core.notifications\") {\n\t\tdebug.Log(\"Notifications disabled\")\n\n\t\treturn nil\n\t}\n\tconn, err := dbus.SessionBus()\n\tif err != nil {\n\t\tdebug.Log(\"DBus failure: %s\", err)\n\n\t\treturn err\n\t}\n\n\tobj := conn.Object(\"org.freedesktop.Notifications\", \"/org/freedesktop/Notifications\")\n\tcall := obj.Call(\"org.freedesktop.Notifications.Notify\", 0, \"gopass\", uint32(0), iconURI(ctx), subj, msg, []string{}, map[string]dbus.Variant{\"transient\": dbus.MakeVariant(true)}, int32(3000))\n\tif call.Err != nil {\n\t\tdebug.Log(\"DBus notification failure: %s\", call.Err)\n\n\t\treturn call.Err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/notify/notify_others.go",
    "content": "//go:build !linux && !windows && !darwin\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime\"\n)\n\n// Notify is not yet implemented on this platform\nfunc Notify(ctx context.Context, subj, msg string) error {\n\treturn fmt.Errorf(\"GOOS %s not yet supported\", runtime.GOOS)\n}\n"
  },
  {
    "path": "internal/notify/notify_test.go",
    "content": "package notify\n\nimport (\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNotify(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\trequire.NoError(t, Notify(ctx, \"foo\", \"bar\"))\n}\n\nfunc TestIcon(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tfn := strings.TrimPrefix(iconURI(ctx), \"file://\")\n\trequire.NoError(t, os.Remove(fn))\n\t_ = iconURI(ctx)\n\tfh, err := os.Open(fn)\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\trequire.NoError(t, fh.Close())\n\t}()\n\n\trequire.NotNil(t, fh)\n\t_, err = png.Decode(fh)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/notify/notify_windows.go",
    "content": "//go:build windows\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n)\n\n// Notify displays a desktop notification through msg\nfunc Notify(ctx context.Context, subj, msg string) error {\n\tif os.Getenv(\"GOPASS_NO_NOTIFY\") != \"\" || !config.Bool(ctx, \"core.notifications\") {\n\t\treturn nil\n\t}\n\twinmsg, err := exec.LookPath(\"msg\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn exec.Command(winmsg,\n\t\t\"*\",\n\t\t\"/TIME:3\",\n\t\tsubj+\"\\n\\n\"+msg,\n\t).Start()\n}\n"
  },
  {
    "path": "internal/out/context.go",
    "content": "package out\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyPrefix contextKey = iota\n\tctxKeyNewline\n)\n\n// WithPrefix returns a context with the given prefix set.\nfunc WithPrefix(ctx context.Context, prefix string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPrefix, prefix)\n}\n\n// AddPrefix returns a context with the given prefix added to end of the\n// existing prefix.\nfunc AddPrefix(ctx context.Context, prefix string) context.Context {\n\tif prefix == \"\" {\n\t\treturn ctx\n\t}\n\n\tpfx := Prefix(ctx)\n\tif pfx == \"\" {\n\t\treturn WithPrefix(ctx, prefix)\n\t}\n\n\treturn WithPrefix(ctx, pfx+prefix)\n}\n\n// Prefix returns the prefix or an empty string.\nfunc Prefix(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyPrefix).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n\n// WithNewline returns a context with the flag value for newline set.\nfunc WithNewline(ctx context.Context, nl bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyNewline, nl)\n}\n\n// HasNewline returns the value of newline or the default (true).\nfunc HasNewline(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyNewline).(bool)\n\tif !ok {\n\t\treturn true\n\t}\n\n\treturn bv\n}\n"
  },
  {
    "path": "internal/out/context_test.go",
    "content": "package out\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, Prefix(ctx))\n\n\tctx = AddPrefix(ctx, \"[foo] \")\n\tassert.Equal(t, \"[foo] \", Prefix(ctx))\n\n\tctx = AddPrefix(ctx, \"[bar] \")\n\tassert.Equal(t, \"[foo] [bar] \", Prefix(ctx))\n\n\tctx = AddPrefix(ctx, \"\")\n\tassert.Equal(t, \"[foo] [bar] \", Prefix(ctx))\n}\n\nfunc TestNewline(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.True(t, HasNewline(ctx))\n\tassert.False(t, HasNewline(WithNewline(ctx, false)))\n}\n"
  },
  {
    "path": "internal/out/print.go",
    "content": "// Package out provides a simple output interface for gopass.\n// It provides functions to print messages to stdout and stderr.\n// These sinks can be replaced by a different implementation, e.g. for testing.\npackage out\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar (\n\t// Stdout is exported for tests.\n\tStdout io.Writer = os.Stdout\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n)\n\n// Secret is a string wrapper for strings containing secrets. These won't be\n// logged as long a GOPASS_DEBUG_LOG_SECRETS is not set.\ntype Secret string\n\n// SafeStr always return \"(elided)\".\nfunc (s Secret) SafeStr() string {\n\treturn \"(elided)\"\n}\n\nfunc newline(ctx context.Context) string {\n\tif HasNewline(ctx) {\n\t\treturn \"\\n\"\n\t}\n\n\treturn \"\"\n}\n\n// Print prints the given string.\nfunc Print(ctx context.Context, arg any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"%s\", arg)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+\"%s\"+newline(ctx), arg)\n}\n\n// Printf formats and prints the given string.\nfunc Printf(ctx context.Context, format string, args ...any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, format, args...)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+format+newline(ctx), args...)\n}\n\n// Notice prints the string with an exclamation mark.\nfunc Notice(ctx context.Context, arg any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"NOTICE: %s\", arg)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+\"⚠ %s\"+newline(ctx), arg)\n}\n\n// Noticef prints the string with an exclamation mark in front.\nfunc Noticef(ctx context.Context, format string, args ...any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"NOTICE: \"+format, args...)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+\"⚠ \"+format+newline(ctx), args...)\n}\n\n// Error prints the string with a red cross in front.\nfunc Error(ctx context.Context, arg any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"ERROR: %s\", arg)\n\tfmt.Fprint(Stderr, color.RedString(Prefix(ctx)+\"❌ %s\"+newline(ctx), arg))\n}\n\n// Errorf prints the string in red to stderr.\nfunc Errorf(ctx context.Context, format string, args ...any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"ERROR: \"+format, args...)\n\tfmt.Fprint(Stderr, color.RedString(Prefix(ctx)+\"❌ \"+format+newline(ctx), args...))\n}\n\n// OK prints the string with a green checkmark in front.\nfunc OK(ctx context.Context, arg any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"OK: %s\", arg)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+\"✅ %s\"+newline(ctx), arg)\n}\n\n// OKf prints the string in with an OK checkmark in front.\nfunc OKf(ctx context.Context, format string, args ...any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"OK: \"+format, args...)\n\tfmt.Fprintf(Stdout, Prefix(ctx)+\"✅ \"+format+newline(ctx), args...)\n}\n\n// Warning prints the string with a warning sign in front.\nfunc Warning(ctx context.Context, arg any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"WARNING: %s\", arg)\n\tfmt.Fprint(Stderr, color.YellowString(Prefix(ctx)+\"⚠ %s\"+newline(ctx), arg))\n}\n\n// Warningf prints the string in yellow to stderr and prepends a warning sign.\nfunc Warningf(ctx context.Context, format string, args ...any) {\n\tif ctxutil.IsHidden(ctx) {\n\t\treturn\n\t}\n\tdebug.LogN(1, \"WARNING: \"+format, args...)\n\tfmt.Fprint(Stderr, color.YellowString(Prefix(ctx)+\"⚠ \"+format+newline(ctx), args...))\n}\n"
  },
  {
    "path": "internal/out/print_test.go",
    "content": "package out\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPrint(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tbuf := &bytes.Buffer{}\n\tStdout = buf\n\tdefer func() {\n\t\tStdout = os.Stdout\n\t}()\n\n\tPrintf(ctx, \"%s = %d\", \"foo\", 42)\n\tassert.Equal(t, \"foo = 42\\n\", buf.String())\n\tbuf.Reset()\n\n\tPrintf(ctxutil.WithHidden(ctx, true), \"%s = %d\", \"foo\", 42)\n\tassert.Empty(t, buf.String())\n\tbuf.Reset()\n\n\tPrintf(WithNewline(ctx, false), \"%s = %d\", \"foo\", 42)\n\tassert.Equal(t, \"foo = 42\", buf.String())\n\tbuf.Reset()\n}\n"
  },
  {
    "path": "internal/pwschemes/argon2i/argon2i.go",
    "content": "// Package argon2i provides an Argon2i password hashing scheme.\npackage argon2i\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/subtle\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/argon2\"\n)\n\nvar (\n\t// ErrInvalidHash is returned if the required parameters can not be obtained\n\t// from the hash.\n\tErrInvalidHash = fmt.Errorf(\"argon2i: invalid hash format\")\n\n\t// ErrVersionIncompatible is returned if the argon2i version generating\n\t// the hash does not match the version validating it.\n\tErrVersionIncompatible = fmt.Errorf(\"argon2i: incompatible version\")\n\n\t// Prefix is set to be compatible with Dovecot. Can be set to an empty string.\n\tPrefix = \"{ARGON2I}\"\n)\n\n// DefaultParams provides sane default parameters for password hashing as of\n// 2021. Depending on your environment you will need to adjust these.\nvar DefaultParams = &Params{\n\tMemory:      256 * 1024,\n\tIterations:  4,\n\tParallelism: 4,\n\tSaltLen:     32,\n\tKeyLen:      32,\n}\n\n// Params contains the input parameters for the argon2i algorithm. Memory and\n// Iterations tweak the computational cost. If you have more cores available\n// you can change the parallelism to reduce runtime without reducing cost. But\n// note that this will change the hash.\n//\n// See https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4\ntype Params struct {\n\tMemory      uint32\n\tIterations  uint32\n\tParallelism uint8\n\tSaltLen     uint32\n\tKeyLen      uint32\n}\n\n// Generate generates a new argon2i hash with recommended values for it's\n// complexity parameters. By default the generated hash is compatible with\n// the Dovecot Password Scheme.\n//\n// See https://doc.dovecot.org/configuration_manual/authentication/password_schemes/\n//\n// It looks like this\n//\n//\t{ARGON2I}$argon2i$v=19$m=262144,t=4,p=4$KUsgM194XAqV2bsQt+OtThf/wFHwltwHJLEnNWFjW6c$Zpwq7e1tzcIlQBTbXQgnUfiryWo91IvWZbmEQc31y/s\nfunc Generate(password string, saltLen uint32) (string, error) {\n\tparams := DefaultParams\n\tif saltLen > 0 {\n\t\tparams.SaltLen = saltLen\n\t}\n\n\tsalt := make([]byte, params.SaltLen)\n\tif _, err := rand.Read(salt); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read rand: %w\", err)\n\t}\n\n\thash := argon2.Key([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen)\n\n\tb64hash := base64.RawStdEncoding.EncodeToString(hash)\n\tb64salt := base64.RawStdEncoding.EncodeToString(salt)\n\n\treturn fmt.Sprintf(Prefix+\"$argon2i$v=%d$m=%d,t=%d,p=%d$%s$%s\", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64salt, b64hash), nil\n}\n\n// Validate unpacks the parameters from the hash, computes the hash of the given\n// password with these parameters and performs a constant time comparison between\n// both hashes.\nfunc Validate(password string, hash string) (bool, error) {\n\tparams, salt, key, err := unpackHash(hash)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\totherKey := argon2.Key([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen)\n\n\tif subtle.ConstantTimeEq(int32(len(key)), int32(len(otherKey))) == 0 {\n\t\treturn false, nil\n\t}\n\n\tif subtle.ConstantTimeCompare(key, otherKey) == 1 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc unpackHash(hash string) (*Params, []byte, []byte, error) {\n\thash = strings.TrimPrefix(hash, Prefix)\n\n\tp := strings.Split(hash, \"$\")\n\tif len(p) != 6 {\n\t\treturn nil, nil, nil, ErrInvalidHash\n\t}\n\n\tif p[1] != \"argon2i\" {\n\t\treturn nil, nil, nil, ErrInvalidHash\n\t}\n\n\tvar version int\n\n\t_, err := fmt.Sscanf(p[2], \"v=%d\", &version)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode version %s: %w\", p[2], err)\n\t}\n\n\tif version != argon2.Version {\n\t\treturn nil, nil, nil, ErrVersionIncompatible\n\t}\n\n\tparams := &Params{}\n\n\t_, err = fmt.Sscanf(p[3], \"m=%d,t=%d,p=%d\", &params.Memory, &params.Iterations, &params.Parallelism)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode header %s: %w\", p[3], err)\n\t}\n\n\tsalt, err := base64.RawStdEncoding.DecodeString(p[4])\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode salt %s: %w\", p[4], err)\n\t}\n\n\tparams.SaltLen = uint32(len(salt))\n\n\tkey, err := base64.RawStdEncoding.DecodeString(p[5])\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode hash %s: %w\", p[5], err)\n\t}\n\n\tparams.KeyLen = uint32(len(key))\n\n\treturn params, salt, key, nil\n}\n"
  },
  {
    "path": "internal/pwschemes/argon2i/argon2i_test.go",
    "content": "package argon2i\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestArgon2I(t *testing.T) {\n\tt.Parallel()\n\n\tpw := \"foobar\"\n\thash, err := Generate(pw, 0)\n\trequire.NoError(t, err)\n\n\tt.Logf(\"PW: %s - Hash: %s\", pw, hash)\n\tok, err := Validate(pw, hash)\n\trequire.NoError(t, err)\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "internal/pwschemes/argon2id/argon2id.go",
    "content": "// Package argon2id provides an Argon2id password hashing scheme.\npackage argon2id\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/subtle\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/argon2\"\n)\n\nvar (\n\t// ErrInvalidHash is returned if the required parameters can not be obtained\n\t// from the hash.\n\tErrInvalidHash = fmt.Errorf(\"argon2id: invalid hash format\")\n\n\t// ErrVersionIncompatible is returned if the Argon2id version generating\n\t// the hash does not match the version validating it.\n\tErrVersionIncompatible = fmt.Errorf(\"argon2id: incompatible version\")\n\n\t// Prefix is set to be compatible with Dovecot. Can be set to an empty string.\n\tPrefix = \"{ARGON2ID}\"\n)\n\n// DefaultParams provides sane default parameters for password hashing as of\n// 2021. Depending on your environment you will need to adjust these.\nvar DefaultParams = &Params{\n\tMemory:      512 * 1024,\n\tIterations:  3,\n\tParallelism: 4,\n\tSaltLen:     32,\n\tKeyLen:      32,\n}\n\n// Params contains the input parameters for the Argon2id algorithm. Memory and\n// Iterations tweak the computational cost. If you have more cores available\n// you can change the parallelism to reduce runtime without reducing cost. But\n// note that this will change the hash.\n//\n// See https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4\ntype Params struct {\n\tMemory      uint32\n\tIterations  uint32\n\tParallelism uint8\n\tSaltLen     uint32\n\tKeyLen      uint32\n}\n\n// Generate generates a new Argon2ID hash with recommended values for it's\n// complexity parameters. By default the generated hash is compatible with\n// the Dovecot Password Scheme.\n//\n// See https://doc.dovecot.org/configuration_manual/authentication/password_schemes/\n//\n// It looks like this\n//\n//\t{ARGON2ID}$argon2id$v=19$m=524288,t=3,p=4$464unwkIcBGXjqWBZ0A5FWClURgYdWFqRlQaBJOE5fs$5ofdht4OkXsg/tftXGgxNchAdgHzpe+QJyizabiKZFk\nfunc Generate(password string, saltLen uint32) (string, error) {\n\tparams := DefaultParams\n\tif saltLen > 0 {\n\t\tparams.SaltLen = saltLen\n\t}\n\n\tsalt := make([]byte, params.SaltLen)\n\tif _, err := rand.Read(salt); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read rand: %w\", err)\n\t}\n\n\thash := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen)\n\n\tb64hash := base64.RawStdEncoding.EncodeToString(hash)\n\tb64salt := base64.RawStdEncoding.EncodeToString(salt)\n\n\treturn fmt.Sprintf(Prefix+\"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s\", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64salt, b64hash), nil\n}\n\n// Validate unpacks the parameters from the hash, computes the hash of the given\n// password with these parameters and performs a constant time comparison between\n// both hashes.\nfunc Validate(password string, hash string) (bool, error) {\n\tparams, salt, key, err := unpackHash(hash)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\totherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen)\n\n\tif subtle.ConstantTimeEq(int32(len(key)), int32(len(otherKey))) == 0 {\n\t\treturn false, nil\n\t}\n\n\tif subtle.ConstantTimeCompare(key, otherKey) == 1 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc unpackHash(hash string) (*Params, []byte, []byte, error) {\n\thash = strings.TrimPrefix(hash, Prefix)\n\n\tp := strings.Split(hash, \"$\")\n\tif len(p) != 6 {\n\t\treturn nil, nil, nil, ErrInvalidHash\n\t}\n\n\tif p[1] != \"argon2id\" {\n\t\treturn nil, nil, nil, ErrInvalidHash\n\t}\n\n\tvar version int\n\n\t_, err := fmt.Sscanf(p[2], \"v=%d\", &version)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to scan version %s: %w\", p[2], err)\n\t}\n\n\tif version != argon2.Version {\n\t\treturn nil, nil, nil, ErrVersionIncompatible\n\t}\n\n\tparams := &Params{}\n\n\t_, err = fmt.Sscanf(p[3], \"m=%d,t=%d,p=%d\", &params.Memory, &params.Iterations, &params.Parallelism)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to scan header %s: %w\", p[3], err)\n\t}\n\n\tsalt, err := base64.RawStdEncoding.DecodeString(p[4])\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode salt %s: %w\", p[4], err)\n\t}\n\n\tparams.SaltLen = uint32(len(salt))\n\n\tkey, err := base64.RawStdEncoding.DecodeString(p[5])\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decode hash %s: %w\", p[5], err)\n\t}\n\n\tparams.KeyLen = uint32(len(key))\n\n\treturn params, salt, key, nil\n}\n"
  },
  {
    "path": "internal/pwschemes/argon2id/argon2id_test.go",
    "content": "package argon2id\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestArgon2ID(t *testing.T) {\n\tt.Parallel()\n\n\tpw := \"foobar\"\n\thash, err := Generate(pw, 0)\n\trequire.NoError(t, err)\n\n\tt.Logf(\"PW: %s - Hash: %s\", pw, hash)\n\tok, err := Validate(pw, hash)\n\trequire.NoError(t, err)\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "internal/pwschemes/bcrypt/bcrypt.go",
    "content": "// Package bcrypt provides a bcrypt password hashing scheme.\n// It is compatible with Dovecot and other systems that use bcrypt.\npackage bcrypt\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nconst (\n\tcost = 12\n)\n\n// Prefix is set to be compatible with Dovecot. Can be set to an empty string.\nvar Prefix = \"{BLF-CRYPT}\"\n\n// Generate generates a new Bcrypt hash with recommended values for it's\n// cost parameter.\nfunc Generate(password string) (string, error) {\n\th, err := bcrypt.GenerateFromPassword([]byte(password), cost)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate password hash: %w\", err)\n\t}\n\n\treturn Prefix + string(h), nil\n}\n\n// Validate validates the password against the given hash.\nfunc Validate(password, hash string) error {\n\thash = strings.TrimPrefix(hash, Prefix)\n\n\tif err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {\n\t\treturn fmt.Errorf(\"failed to validate password hash %s: %w\", hash, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/pwschemes/bcrypt/bcrypt_test.go",
    "content": "package bcrypt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBcrypt(t *testing.T) {\n\tt.Parallel()\n\n\tpw := \"foobar\"\n\n\thash, err := Generate(pw)\n\trequire.NoError(t, err)\n\n\tt.Logf(\"PW: %s - Hash: %s\", pw, hash)\n\n\trequire.NoError(t, Validate(pw, hash))\n}\n"
  },
  {
    "path": "internal/queue/background.go",
    "content": "// Package queue implements an experimental background queue for cleanup jobs.\n// Beware: It's likely broken.\n// We can easily close a channel which might later be written to.\n// The current locking is but a poor workaround.\n// A better implementation would create a queue object in main, pass\n// it through and wait for the channel to be empty before leaving main.\n// Will do that later.\npackage queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\ntype contextKey int\n\nconst (\n\tctxKeyQueue contextKey = iota\n)\n\n// Queuer is a queue interface.\ntype Queuer interface {\n\tAdd(Task) Task\n\tClose(context.Context) error\n\tIdle(time.Duration) error\n}\n\n// WithQueue adds the given queue to the context. Add a nil\n// queue to disable queuing in this context.\nfunc WithQueue(ctx context.Context, q *Queue) context.Context {\n\treturn context.WithValue(ctx, ctxKeyQueue, q)\n}\n\n// GetQueue returns an existing queue from the context or\n// returns a noop one.\nfunc GetQueue(ctx context.Context) Queuer {\n\tif q, ok := ctx.Value(ctxKeyQueue).(*Queue); ok && q != nil {\n\t\treturn q\n\t}\n\n\treturn &noop{}\n}\n\ntype noop struct{}\n\n// Add always returns the task.\nfunc (n *noop) Add(t Task) Task {\n\treturn t\n}\n\n// Close always returns nil.\nfunc (n *noop) Close(_ context.Context) error {\n\treturn nil\n}\n\n// Idle always returns nil.\nfunc (n *noop) Idle(_ time.Duration) error {\n\treturn nil\n}\n\n// Task is a background task.\ntype Task func(ctx context.Context) (context.Context, error)\n\n// Queue is a serialized background processing unit.\ntype Queue struct {\n\twork chan Task\n\tdone chan struct{}\n}\n\n// New creates a new queue.\nfunc New(ctx context.Context) *Queue {\n\tq := &Queue{\n\t\twork: make(chan Task, 1024),\n\t\tdone: make(chan struct{}, 1),\n\t}\n\tgo q.run(ctx)\n\n\treturn q\n}\n\nfunc (q *Queue) run(ctx context.Context) {\n\tfor t := range q.work {\n\t\tctx2, err := t(ctx)\n\t\tif err != nil {\n\t\t\tout.Errorf(ctx, \"Task failed: %s\", err)\n\t\t}\n\t\tif ctx2 != nil {\n\t\t\t// if a task returns a context, it is to transmit information to the next tasks in line\n\t\t\t// so replace the in-queue context with the new one\n\t\t\t// (each task has access to two contexts: one from the queue, and one from the function creating the task)\n\t\t\tctx = ctx2\n\t\t}\n\t\tdebug.Log(\"Task done\")\n\t}\n\tdebug.Log(\"all tasks done\")\n\tq.done <- struct{}{}\n}\n\n// Add enqueues a new task.\nfunc (q *Queue) Add(t Task) Task {\n\tq.work <- t\n\tdebug.Log(\"enqueued task\")\n\n\treturn func(ctx2 context.Context) (context.Context, error) {\n\t\treturn ctx2, nil\n\t}\n}\n\n// Idle returns nil the next time the queue is empty.\nfunc (q *Queue) Idle(maxWait time.Duration) error {\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tfor {\n\t\t\tif len(q.work) < 1 {\n\t\t\t\tselect {\n\t\t\t\tcase done <- struct{}{}:\n\t\t\t\t\t// sent\n\t\t\t\tdefault:\n\t\t\t\t\t// no-op\n\t\t\t\t}\n\t\t\t}\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\treturn nil\n\tcase <-time.After(maxWait):\n\t\treturn fmt.Errorf(\"timed out waiting for empty queue\")\n\t}\n}\n\n// Close waits for all tasks to be processed. Must only be called once on\n// shutdown.\nfunc (q *Queue) Close(ctx context.Context) error {\n\tclose(q.work)\n\tselect {\n\tcase <-q.done:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\tdebug.Log(\"context canceled\")\n\n\t\treturn ctx.Err()\n\t}\n}\n"
  },
  {
    "path": "internal/queue/background_test.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestQueue_Add(t *testing.T) {\n\tctx := t.Context()\n\tq := New(ctx)\n\n\ttask := func(ctx context.Context) (context.Context, error) {\n\t\treturn ctx, nil\n\t}\n\n\tq.Add(task)\n\n\tif len(q.work) != 1 {\n\t\tt.Errorf(\"expected 1 task in queue, got %d\", len(q.work))\n\t}\n}\n\nfunc TestQueue_Close(t *testing.T) {\n\tctx := t.Context()\n\tq := New(ctx)\n\n\ttask := func(ctx context.Context) (context.Context, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\treturn ctx, nil\n\t}\n\n\tq.Add(task)\n\tq.Add(task)\n\n\terr := q.Close(ctx)\n\tif err != nil {\n\t\tt.Errorf(\"expected no error, got %v\", err)\n\t}\n}\n\nfunc TestQueue_Idle(t *testing.T) {\n\tctx := t.Context()\n\tq := New(ctx)\n\n\ttask := func(ctx context.Context) (context.Context, error) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\treturn ctx, nil\n\t}\n\n\tq.Add(task)\n\n\terr := q.Idle(200 * time.Millisecond)\n\tif err != nil {\n\t\tt.Errorf(\"expected no error, got %v\", err)\n\t}\n\n\tq.Add(task)\n\n\terr = q.Idle(50 * time.Millisecond)\n\tif err == nil {\n\t\tt.Errorf(\"expected timeout error, got nil\")\n\t}\n}\n\nfunc TestWithQueue(t *testing.T) {\n\tctx := t.Context()\n\tq := New(ctx)\n\n\tctxWithQueue := WithQueue(ctx, q)\n\tif GetQueue(ctxWithQueue) != q {\n\t\tt.Errorf(\"expected queue to be set in context\")\n\t}\n}\n\nfunc TestGetQueue(t *testing.T) {\n\tctx := t.Context()\n\n\tq := GetQueue(ctx)\n\tif _, ok := q.(*noop); !ok {\n\t\tt.Errorf(\"expected noop queue, got %T\", q)\n\t}\n}\n"
  },
  {
    "path": "internal/recipients/recipients.go",
    "content": "// Package recipients provides a datastruct for managing for managinig recipients.\n// It also provides methods for marshalling and unmarshalling the recipients.\npackage recipients\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// Recipients is a list of Key IDs. It will try to retain the file as much as possible while manipulating the recipients.\ntype Recipients struct {\n\tr   map[string]bool\n\traw strings.Builder\n}\n\n// New creates a new list of Key IDs.\nfunc New() *Recipients {\n\treturn &Recipients{\n\t\tr:   make(map[string]bool, 4),\n\t\traw: strings.Builder{},\n\t}\n}\n\n// Len returns the number of recipients.\nfunc (r *Recipients) Len() int {\n\tif r == nil {\n\t\treturn 0\n\t}\n\n\treturn len(r.r)\n}\n\n// IDs returns the key IDs.\nfunc (r *Recipients) IDs() []string {\n\treturn set.SortedKeys(r.r)\n}\n\n// Add adds a new recipients. It returns true if the recipient was added.\nfunc (r *Recipients) Add(key string) bool {\n\tkey = strings.TrimSpace(key)\n\tif _, found := r.r[key]; found {\n\t\treturn false\n\t}\n\n\tr.r[key] = true\n\n\treturn true\n}\n\n// Remove deletes an existing recipient. It returns true if the recipients\n// was present and got removed.\nfunc (r *Recipients) Remove(key string) bool {\n\tkey = strings.TrimSpace(key)\n\tif _, found := r.r[key]; !found {\n\t\treturn false\n\t}\n\n\tdelete(r.r, key)\n\n\treturn true\n}\n\n// Has returns true if the recipient is found.\nfunc (r *Recipients) Has(key string) bool {\n\tkey = strings.TrimSpace(key)\n\t_, found := r.r[key]\n\n\treturn found\n}\n\n// Marshal all in memory Recipients line by line to []byte.\nfunc (r *Recipients) Marshal() []byte {\n\tif len(r.r) == 0 {\n\t\treturn []byte(\"\\n\")\n\t}\n\n\tseen := make(map[string]bool, len(r.r))\n\n\tout := bytes.Buffer{}\n\ts := bufio.NewScanner(strings.NewReader(r.raw.String()))\n\tfor s.Scan() {\n\t\tline := strings.TrimSpace(s.Text())\n\n\t\t// pass through comments\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tout.WriteString(line)\n\t\t\tout.WriteString(\"\\n\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\tkey := line\n\t\t// trim any trailing comments\n\t\tif before, _, ok := strings.Cut(line, \"#\"); ok {\n\t\t\tkey = strings.TrimSpace(before)\n\t\t}\n\n\t\t// skip deleted IDs\n\t\tif _, found := r.r[key]; !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tout.WriteString(line)\n\t\tout.WriteString(\"\\n\")\n\n\t\tseen[key] = true\n\t}\n\n\t// add new keys\n\tfor _, k := range set.SortedKeys(r.r) {\n\t\t// added before\n\t\tif _, found := seen[k]; found {\n\t\t\tcontinue\n\t\t}\n\n\t\tout.WriteString(k)\n\t\tout.WriteString(\"\\n\")\n\n\t\tseen[k] = true\n\t}\n\n\treturn out.Bytes()\n}\n\n// Hash returns the hex encoded SHA256 sum of the recipients.\nfunc (r *Recipients) Hash() string {\n\th := sha256.New()\n\t_, _ = h.Write(r.Marshal())\n\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// Unmarshal Recipients line by line from a io.Reader. Handles Unix, Windows and Mac line endings.\nfunc Unmarshal(buf []byte) *Recipients {\n\tin := strings.ReplaceAll(string(buf), \"\\r\", \"\\n\")\n\n\tr := New()\n\ts := bufio.NewScanner(strings.NewReader(in))\n\tfor s.Scan() {\n\t\tline := s.Text()\n\n\t\tr.raw.WriteString(line)\n\t\tr.raw.WriteString(\"\\n\")\n\n\t\tline = strings.TrimSpace(line)\n\n\t\t// skip comments\n\t\tif strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\t// trim trailing comments\n\t\tkey := line\n\t\tif before, _, ok := strings.Cut(line, \"#\"); ok {\n\t\t\tkey = strings.TrimSpace(before)\n\t\t}\n\n\t\tif len(key) < 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.r[key] = true\n\t}\n\n\treturn r\n}\n"
  },
  {
    "path": "internal/recipients/recipients_test.go",
    "content": "package recipients\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMarshal(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tin   []string\n\t\twant string\n\t}{\n\t\t{\n\t\t\twant: \"foo@bar.com\\n\",\n\t\t\tin:   []string{\"foo@bar.com\\n\\r\"},\n\t\t},\n\t\t{\n\t\t\twant: \"baz@bar.com\\nfoo@bar.com\\n\",\n\t\t\tin:   []string{\"baz@bar.com\", \"foo@bar.com\"},\n\t\t},\n\t\t{\n\t\t\twant: \"baz@bar.com\\nzab@zab.com\\n\",\n\t\t\tin:   []string{\"baz@bar.com\", \"zab@zab.com\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.want, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := New()\n\t\t\tsort.Strings(tc.in)\n\t\t\tfor _, k := range tc.in {\n\t\t\t\tr.Add(k)\n\t\t\t}\n\t\t\tgot := string(r.Marshal())\n\t\t\tassert.Equal(t, tc.want, got, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tin   string\n\t\twant []string\n\t}{\n\t\t{\n\t\t\tin:   \"foo@bar.com\",\n\t\t\twant: []string{\"foo@bar.com\"},\n\t\t},\n\t\t{\n\t\t\tin:   \"foo@bar.com\\nbaz@bar.com\\n\",\n\t\t\twant: []string{\"baz@bar.com\", \"foo@bar.com\"},\n\t\t},\n\t\t{\n\t\t\tin:   \"foo@bar.com\\r\\nbaz@bar.com\\r\\n\",\n\t\t\twant: []string{\"baz@bar.com\", \"foo@bar.com\"},\n\t\t},\n\t\t{\n\t\t\tin:   \"foo@bar.com\\rbaz@bar.com\\r\",\n\t\t\twant: []string{\"baz@bar.com\", \"foo@bar.com\"},\n\t\t},\n\t\t{\n\t\t\tin:   \"# foo@bar.com\\nbaz@bar.com\\nzab@zab.com # comment\",\n\t\t\twant: []string{\"baz@bar.com\", \"zab@zab.com\"},\n\t\t},\n\t\t{\n\t\t\tin:   \"# foo@bar.com\\nbaz@bar.com\\n# comment\\nzab@zab.com\\n\",\n\t\t\twant: []string{\"baz@bar.com\", \"zab@zab.com\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.in, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := Unmarshal([]byte(tc.in))\n\t\t\tsort.Strings(tc.want)\n\n\t\t\tgot := r.IDs()\n\t\t\tsort.Strings(got)\n\n\t\t\tassert.Equal(t, tc.want, got, tc.in)\n\t\t})\n\t}\n}\n\nfunc TestEndToEnd(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tin   string\n\t\top   func(r *Recipients) error\n\t\tout  string\n\t}{\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"0xDEADBEEF\",\n\t\t\top: func(r *Recipients) error {\n\t\t\t\tr.Add(\"0xFEEDBEEF\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tout: \"0xDEADBEEF\\n0xFEEDBEEF\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"comments\",\n\t\t\tin: `0xDEADBEEF # john doe\n\n# some disabled ones\n# 0xFOOBAR\n\n0xFEEDBEEF\n`,\n\t\t\top: func(r *Recipients) error {\n\t\t\t\tr.Remove(\"0xFEEDBEEF\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tout: `0xDEADBEEF # john doe\n\n# some disabled ones\n# 0xFOOBAR\n\n`,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := Unmarshal([]byte(tc.in))\n\t\t\trequire.NoError(t, tc.op(r))\n\t\t\tbuf := r.Marshal()\n\t\t\tassert.Equal(t, tc.out, string(buf))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/reminder/reminder.go",
    "content": "// Package reminder provides a reminder store for gopass.\n// It stores timestamps on disk to remind the user of certain events.\npackage reminder\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/cache\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Store stores timestamps on disk.\ntype Store struct {\n\tcache *cache.OnDisk\n}\n\n// New creates a new persistent timestamp store.\nfunc New() (*Store, error) {\n\tod, err := cache.NewOnDisk(\"reminder\", 90*24*time.Hour)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to init reminder cache: %w\", err)\n\t}\n\n\treturn &Store{\n\t\tcache: od,\n\t}, nil\n}\n\n// LastSeen returns the time when the key was last reset.\nfunc (s *Store) LastSeen(key string) time.Time {\n\tt := time.Time{}\n\tif s == nil {\n\t\treturn t\n\t}\n\n\tres, err := s.cache.Get(key)\n\tif err != nil {\n\t\tdebug.V(2).Log(\"failed to read %q from cache: %s\", key, err)\n\n\t\treturn t\n\t}\n\n\tif len(res) < 1 {\n\t\tdebug.V(1).Log(\"cache result is empty\")\n\n\t\treturn t\n\t}\n\n\tts, err := time.Parse(time.RFC3339, res[0])\n\tif err != nil {\n\t\tdebug.V(1).Log(\"failed to parse stored time %q: %s\", err)\n\n\t\treturn t\n\t}\n\n\treturn ts\n}\n\n// Reset marks a key as just see.\nfunc (s *Store) Reset(key string) error {\n\tif s == nil {\n\t\treturn nil\n\t}\n\n\treturn s.cache.Set(key, []string{time.Now().Format(time.RFC3339)})\n}\n\n// Overdue returns true iff (a) overdue did not return true within 24h AND (b)\n// the key wasn't updated within the last 90 day.\nfunc (s *Store) Overdue(key string) bool {\n\tif s == nil {\n\t\treturn false\n\t}\n\n\tif time.Since(s.LastSeen(\"overdue\")) < 24*time.Hour {\n\t\treturn false\n\t}\n\n\t_ = s.Reset(\"overdue\")\n\n\treturn time.Since(s.LastSeen(key)) > 90*24*time.Hour\n}\n"
  },
  {
    "path": "internal/reminder/reminder_test.go",
    "content": "package reminder\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Setenv(\"GOPASS_HOMEDIR\", t.TempDir())\n\n\tstore, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, store)\n}\n\nfunc TestLastSeen(t *testing.T) {\n\tt.Setenv(\"GOPASS_HOMEDIR\", t.TempDir())\n\n\tstore, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, store)\n\n\tkey := \"test-key\"\n\tnow := time.Now().Format(time.RFC3339)\n\terr = store.cache.Set(key, []string{now})\n\trequire.NoError(t, err)\n\n\tlastSeen := store.LastSeen(key)\n\tassert.Equal(t, now, lastSeen.Format(time.RFC3339))\n}\n\nfunc TestReset(t *testing.T) {\n\tt.Setenv(\"GOPASS_HOMEDIR\", t.TempDir())\n\n\tstore, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, store)\n\n\tkey := \"test-key\"\n\terr = store.Reset(key)\n\trequire.NoError(t, err)\n\n\tlastSeen := store.LastSeen(key)\n\tassert.WithinDuration(t, time.Now(), lastSeen, time.Second)\n}\n\nfunc TestOverdue(t *testing.T) {\n\tt.Setenv(\"GOPASS_HOMEDIR\", t.TempDir())\n\n\tstore, err := New()\n\trequire.NoError(t, err)\n\tassert.NotNil(t, store)\n\n\tkey := \"test-key\"\n\terr = store.Reset(key)\n\trequire.NoError(t, err)\n\n\toverdue := store.Overdue(key)\n\tassert.False(t, overdue)\n\n\t// Simulate overdue by setting the last seen time to more than 90 days ago\n\tpast := time.Now().Add(-91 * 24 * time.Hour).Format(time.RFC3339)\n\trequire.NoError(t, store.cache.Set(key, []string{past}))\n\trequire.NoError(t, err)\n\trequire.NoError(t, store.cache.Set(\"overdue\", []string{time.Now().Add(-25 * time.Hour).Format(time.RFC3339)}))\n\n\tt.Logf(\"last seen: %s, %s ago\", store.LastSeen(key), time.Since(store.LastSeen(key)))\n\toverdue = store.Overdue(key)\n\tassert.True(t, overdue)\n}\n"
  },
  {
    "path": "internal/store/err.go",
    "content": "package store\n\nimport \"fmt\"\n\nvar (\n\t// ErrExistsFailed is returend if we can't check for existence.\n\tErrExistsFailed = fmt.Errorf(\"failed to check for existence\")\n\t// ErrNotFound is returned if an entry was not found.\n\tErrNotFound = fmt.Errorf(\"entry is not in the password store\")\n\t// ErrEncrypt is returned if we failed to encrypt an entry.\n\tErrEncrypt = fmt.Errorf(\"failed to encrypt\")\n\t// ErrDecrypt is returned if we failed to decrypt and entry.\n\tErrDecrypt = fmt.Errorf(\"failed to decrypt\")\n\t// ErrIO is any kind of I/O error.\n\tErrIO = fmt.Errorf(\"i/o error\")\n\t// ErrGitInit is returned if git is already initialized.\n\tErrGitInit = fmt.Errorf(\"git is already initialized\")\n\t// ErrGitNotInit is returned if git is not initialized.\n\tErrGitNotInit = fmt.Errorf(\"git is not initialized\")\n\t// ErrGitNoRemote is returned if git has no origin remote.\n\tErrGitNoRemote = fmt.Errorf(\"git has no remote origin\")\n\t// ErrGitNothingToCommit is returned if there are no staged changes.\n\tErrGitNothingToCommit = fmt.Errorf(\"git has nothing to commit\")\n\t// ErrEmptySecret is returned if a secret exists but has no content.\n\tErrEmptySecret = fmt.Errorf(\"empty secret. see https://go.gopass.pw/faq#empty-secret\")\n\t// ErrMeaninglessWrite is returned if a secret is overwritten with its current (ciphertext) content.\n\tErrMeaninglessWrite = fmt.Errorf(\"meaningless write\")\n\t// ErrNoBody is returned if a secret exists but has no content beyond a password.\n\tErrNoBody = fmt.Errorf(\"no safe content to display, you can force display with -f\")\n\t// ErrNoPassword is returned is a secret exists but has no password, only a body.\n\tErrNoPassword = fmt.Errorf(\"no password to display, check the body of the entry instead\")\n\t// ErrYAMLNoMark is returned if a secret contains no valid YAML document marker.\n\tErrYAMLNoMark = fmt.Errorf(\"no YAML document marker found\")\n\t// ErrNoKey is returned if a KV or YAML entry doesn't contain a key.\n\tErrNoKey = fmt.Errorf(\"key not found in entry\")\n\t// ErrYAMLValueUnsupported is returned is the user tries to unmarshal an nested struct.\n\tErrYAMLValueUnsupported = fmt.Errorf(\"can not unmarshal nested YAML value\")\n)\n"
  },
  {
    "path": "internal/store/leaf/context.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/internal/store\"\n)\n\ntype contextKey int\n\nconst (\n\tctxKeyFsckCheck contextKey = iota\n\tctxKeyFsckForce\n\tctxKeyFsckFunc\n\tctxKeyFsckDecrypt\n\tctxKeyNoGitOps\n\tctxKeyPubkeyUpdate\n)\n\n// WithFsckCheck returns a context with the flag for fscks check set.\nfunc WithFsckCheck(ctx context.Context, check bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyFsckCheck, check)\n}\n\n// HasFsckCheck returns true if a value for fsck check has been set in this\n// context.\nfunc HasFsckCheck(ctx context.Context) bool {\n\treturn hasBool(ctx, ctxKeyFsckCheck)\n}\n\n// IsFsckCheck returns the value of fsck check.\nfunc IsFsckCheck(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyFsckCheck).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithFsckForce returns a context with the flag for fsck force set.\nfunc WithFsckForce(ctx context.Context, force bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyFsckForce, force)\n}\n\n// HasFsckForce returns true if a value for fsck force has been set in this\n// context.\nfunc HasFsckForce(ctx context.Context) bool {\n\treturn hasBool(ctx, ctxKeyFsckForce)\n}\n\n// IsFsckForce returns the value of fsck force.\nfunc IsFsckForce(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyFsckForce).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithFsckFunc will return a context with the fsck confirmation callback set.\nfunc WithFsckFunc(ctx context.Context, imf store.FsckCallback) context.Context {\n\treturn context.WithValue(ctx, ctxKeyFsckFunc, imf)\n}\n\n// HasFsckFunc returns true if a fsck func has been set in this context.\nfunc HasFsckFunc(ctx context.Context) bool {\n\timf, ok := ctx.Value(ctxKeyFsckFunc).(store.FsckCallback)\n\n\treturn ok && imf != nil\n}\n\n// GetFsckFunc will return the fsck confirmation callback or a default one\n// returning true.\n// Note: will never return nil.\nfunc GetFsckFunc(ctx context.Context) store.FsckCallback {\n\timf, ok := ctx.Value(ctxKeyFsckFunc).(store.FsckCallback)\n\tif !ok || imf == nil {\n\t\treturn func(context.Context, string) bool {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn imf\n}\n\n// WithFsckDecrypt will return a context with the value for the decrypt\n// during fsck flag set.\nfunc WithFsckDecrypt(ctx context.Context, d bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyFsckDecrypt, d)\n}\n\n// IsFsckDecrypt will return the value for the decrypt during fsck, defaulting\n// to false.\nfunc IsFsckDecrypt(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyFsckDecrypt, false)\n}\n\n// WithNoGitOps returns a context with the value for NoGitOps set.\n// This will skip any git operations in concurrent goroutines.\nfunc WithNoGitOps(ctx context.Context, d bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyNoGitOps, d)\n}\n\n// IsNoGitOps returns the value for NoGitOps from the context\n// or the default (false).\nfunc IsNoGitOps(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyNoGitOps, false)\n}\n\n// IsPubkeyUpdate returns true if we should update all exported\n// recipients pub keys.\nfunc IsPubkeyUpdate(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyPubkeyUpdate, false)\n}\n\n// WithPubkeyUpdate returns a context with the selection to update\n// all exported recipients pub keys set.\nfunc WithPubkeyUpdate(ctx context.Context, d bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPubkeyUpdate, d)\n}\n\n// hasBool is a helper function for checking if a bool has been set in\n// the provided context.\nfunc hasBool(ctx context.Context, key contextKey) bool {\n\t_, ok := ctx.Value(key).(bool)\n\n\treturn ok\n}\n\n// is is a helper function for returning the value of a bool from the context\n// or the provided default.\nfunc is(ctx context.Context, key contextKey, def bool) bool {\n\tbv, ok := ctx.Value(key).(bool)\n\tif !ok {\n\t\treturn def\n\t}\n\n\treturn bv\n}\n"
  },
  {
    "path": "internal/store/leaf/context_test.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFsckCheck(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsFsckCheck(ctx))\n\tassert.True(t, IsFsckCheck(WithFsckCheck(ctx, true)))\n\tassert.False(t, IsFsckCheck(WithFsckCheck(ctx, false)))\n\tassert.True(t, HasFsckCheck(WithFsckCheck(ctx, true)))\n}\n\nfunc TestFsckForce(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsFsckForce(ctx))\n\tassert.True(t, IsFsckForce(WithFsckForce(ctx, true)))\n\tassert.False(t, IsFsckForce(WithFsckForce(ctx, false)))\n\tassert.True(t, HasFsckForce(WithFsckForce(ctx, true)))\n}\n\nfunc TestFsckFunc(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tffunc := func(context.Context, string) bool {\n\t\treturn true\n\t}\n\tassert.NotNil(t, GetFsckFunc(ctx))\n\tassert.True(t, GetFsckFunc(ctx)(ctx, \"\"))\n\tassert.True(t, GetFsckFunc(WithFsckFunc(ctx, ffunc))(ctx, \"\"))\n\tassert.True(t, HasFsckFunc(WithFsckFunc(ctx, ffunc)))\n}\n"
  },
  {
    "path": "internal/store/leaf/convert.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/cui\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\n// Convert will convert an existing store to a new store with possibly\n// different set of crypto and storage backends. Please note that it\n// will happily convert to the same set of backends if requested.\nfunc (s *Store) Convert(ctx context.Context, cryptoBe backend.CryptoBackend, storageBe backend.StorageBackend, move bool) error { //nolint:cyclop\n\t// use a temp queue so we can flush it before removing the old store\n\tq := queue.New(ctx)\n\tctx = queue.WithQueue(ctx, q)\n\n\t// remove any previous attempts\n\tif pDir := filepath.Join(filepath.Dir(s.path), filepath.Base(s.path)+\"-autoconvert\"); fsutil.IsDir(pDir) {\n\t\tif err := os.RemoveAll(pDir); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove previous attempt %q: %w\", pDir, err)\n\t\t}\n\t}\n\n\t// create temp path\n\ttmpPath := s.path + \"-autoconvert\"\n\tif err := os.MkdirAll(tmpPath, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create temporary conversion directory %s: %w\", tmpPath, err)\n\t}\n\n\tdebug.Log(\"create temporary store path for conversion: %s\", tmpPath)\n\n\t// init new store at temp path\n\tst, err := backend.InitStorage(ctx, storageBe, tmpPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize new stroage backend %s: %w\", storageBe.String(), err)\n\t}\n\n\tdebug.Log(\"initialized storage %s at %s\", st, tmpPath)\n\n\tcrypto, err := backend.NewCrypto(ctx, cryptoBe)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize new crypto backend %s: %w\", cryptoBe.String(), err)\n\t}\n\n\tdebug.Log(\"initialized Crypto %s\", crypto)\n\n\ttmpStore := &Store{\n\t\talias:   s.alias,\n\t\tpath:    tmpPath,\n\t\tcrypto:  crypto,\n\t\tstorage: st,\n\t}\n\n\t// init new store\n\tkey, err := cui.AskForPrivateKey(ctx, crypto, \"Please select a private key\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ask for the private key for %v: %w\", crypto, err)\n\t}\n\n\tif err := tmpStore.Init(ctx, tmpPath, key); err != nil {\n\t\treturn fmt.Errorf(\"failed to init new store at %s: %w\", tmpPath, err)\n\t}\n\n\t// copy everything from old to temp, including all revisions\n\tentries, err := s.List(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list entries of the old store: %w\", err)\n\t}\n\n\tout.Printf(ctx, \"Converting store ...\")\n\tbar := termio.NewProgressBar(int64(len(entries)))\n\tbar.Hidden = ctxutil.IsHidden(ctx)\n\tif !ctxutil.IsTerminal(ctx) || ctxutil.IsHidden(ctx) {\n\t\tbar = nil\n\t}\n\n\t// Avoid network operations slowing down the bulk conversion.\n\t// We will sync with the remote later.\n\tctx = ctxutil.WithNoNetwork(ctx, true)\n\tfor _, e := range entries {\n\t\te = strings.TrimPrefix(e, s.alias+Sep)\n\t\tdebug.Log(\"converting %s\", e)\n\t\trevs, err := s.ListRevisions(ctx, e)\n\t\tif err != nil {\n\t\t\t// the fs backend does not support revisions. but we can still convert the \"latest\" (only) revision.\n\t\t\tif !errors.Is(err, backend.ErrNotSupported) || len(revs) < 1 {\n\t\t\t\treturn fmt.Errorf(\"failed to list revision of %s: %w\", e, err)\n\t\t\t}\n\t\t}\n\t\tsort.Sort(sort.Reverse(backend.Revisions(revs)))\n\n\t\t// fail if the first revision fails, but if others fail only warn\n\t\tfirst := true\n\t\tfor _, r := range revs {\n\t\t\tdebug.Log(\"converting %s@%s\", e, r.Hash)\n\t\t\tsec, err := s.GetRevision(ctx, e, r.Hash)\n\t\t\tif err != nil {\n\t\t\t\tif first {\n\t\t\t\t\treturn fmt.Errorf(\"failed to convert revision %s of %s: %w\", r.Hash, e, err)\n\t\t\t\t}\n\t\t\t\tdebug.Log(\"failed to convert revision %s of %s: %w\", r.Hash, e, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmsg := fmt.Sprintf(\"%s\\n%s\\nCommitted as: %s\\nDate: %s\\nAuthor: %s <%s>\",\n\t\t\t\tr.Subject,\n\t\t\t\tr.Body,\n\t\t\t\tr.Hash,\n\t\t\t\tr.Date.Format(time.RFC3339),\n\t\t\t\tr.AuthorName,\n\t\t\t\tr.AuthorEmail,\n\t\t\t)\n\t\t\tctx := ctxutil.WithCommitMessage(ctx, msg)\n\t\t\tctx = ctxutil.WithCommitTimestamp(ctx, r.Date)\n\t\t\tif err := tmpStore.Set(ctx, e, sec); err != nil {\n\t\t\t\tif first {\n\t\t\t\t\treturn fmt.Errorf(\"failed to write converted revision %s of %s to the new store: %w\", r.Hash, e, err)\n\t\t\t\t}\n\t\t\t\tdebug.Log(\"failed to write converted revision %s of %s to the new store: %w\", r.Hash, e, err)\n\t\t\t}\n\n\t\t\tfirst = false\n\t\t}\n\t\tbar.Inc()\n\t}\n\tbar.Done()\n\n\t// flush queue\n\t_ = q.Close(ctx)\n\n\tif !move {\n\t\tdebug.Log(\"conversion done. no move requested. keeping both.\")\n\n\t\treturn nil\n\t}\n\n\t// remove any previous backups\n\tbDir := filepath.Join(filepath.Dir(s.path), filepath.Base(s.path)+\"-backup\")\n\tif fsutil.IsDir(bDir) {\n\t\tif err := os.RemoveAll(bDir); err != nil {\n\t\t\tdebug.Log(\"failed to remove previous backup %q: %s\", bDir, err)\n\t\t}\n\t}\n\n\t// rename old to backup\n\tif err := os.Rename(s.path, bDir); err != nil {\n\t\treturn fmt.Errorf(\"failed to rename old store from %s to backup at %s: %w\", s.path, bDir, err)\n\t}\n\n\t// rename temp to old\n\tif err := os.Rename(tmpPath, s.path); err != nil {\n\t\treturn fmt.Errorf(\"failed to rename temp store %s to old %s: %w\", tmpPath, s.path, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/crypto.go",
    "content": "package leaf\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nfunc (s *Store) initCryptoBackend(ctx context.Context) error {\n\tcb, err := backend.DetectCrypto(ctx, s.storage)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.crypto = cb\n\n\treturn nil\n}\n\n// Crypto returns the crypto backend.\nfunc (s *Store) Crypto() backend.Crypto {\n\treturn s.crypto\n}\n\n// recipientCheck checks if a recipient is already present in the keyring and up-to-date.\n// It returns true if the recipient is fine and the import can be skipped.\nfunc (s *Store) recipientCheck(ctx context.Context, r string) bool {\n\t// check if this recipient is missing\n\t// we could list all keys outside the loop and just do the lookup here\n\t// but this way we ensure to use the exact same lookup logic as\n\t// gpg does on encryption\n\tkl, err := s.crypto.FindRecipients(ctx, r)\n\tif err != nil {\n\t\t// this is expected if we don't have the key\n\t\tdebug.Log(\"Failed to get public key for %s: %s\", r, err)\n\t}\n\n\tif len(kl) > 0 { //nolint:nestif\n\t\tdebug.Log(\"Keyring contains %d public keys for %s\", len(kl), r)\n\t\tif !IsPubkeyUpdate(ctx) {\n\t\t\treturn true\n\t\t}\n\t\tex, ok := s.crypto.(keyExporter)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tpk, err := ex.ExportPublicKey(ctx, r)\n\t\tif err != nil {\n\t\t\treturn true\n\t\t}\n\t\tpk2, err2 := s.getPublicKey(ctx, r)\n\t\tif err2 != nil {\n\t\t\treturn true\n\t\t}\n\t\tif bytes.Equal(pk, pk2) {\n\t\t\treturn true\n\t\t}\n\t} else {\n\t\t// if key is not found, try to check by fingerprint\n\t\tpk, err := s.getPublicKey(ctx, r)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to get public key for %s: %s\", r, err)\n\n\t\t\treturn true\n\t\t}\n\t\tfp, err := s.crypto.GetFingerprint(ctx, pk)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to get fingerprint for %s: %s\", r, err)\n\n\t\t\treturn true\n\t\t}\n\t\tkl, err = s.crypto.FindRecipients(ctx, fp)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to find recipients for %s: %s\", fp, err)\n\t\t}\n\t\tif len(kl) > 0 {\n\t\t\tdebug.Log(\"key %s with fingerprint %s already in keyring\", r, fp)\n\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ImportMissingPublicKeys will try to import any missing public keys from the\n// .public-keys folder in the password store.\nfunc (s *Store) ImportMissingPublicKeys(ctx context.Context, newrs ...string) error {\n\t// do not import any keys for age, where public key == key id\n\t// TODO: do not hard code exceptions, ask the backend if it supports it\n\tif _, ok := s.crypto.(*age.Age); ok {\n\t\tdebug.Log(\"not importing public keys for age\")\n\n\t\treturn nil\n\t}\n\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get recipients: %w\", err)\n\t}\n\n\tids := append(rs.IDs(), newrs...)\n\tfor _, r := range ids {\n\t\tdebug.Log(\"Checking recipients %s ...\", r)\n\t\tif s.recipientCheck(ctx, r) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// get info about this public key\n\t\tnames, err := s.decodePublicKey(ctx, r)\n\t\tif err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to decode public key %s: %s\", r, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// we need to ask the user before importing\n\t\t// any key material into his keyring!\n\t\tif imf := ctxutil.GetImportFunc(ctx); imf != nil && !config.Bool(ctx, \"core.autoimport\") {\n\t\t\tif !imf(ctx, r, names) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tdebug.Log(\"Public Key %s not found in keyring, importing\", r)\n\n\t\t// try to load this recipient\n\t\tif err := s.importPublicKey(ctx, r); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to import public key for %s: %s\", r, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tout.Printf(ctx, \"Imported public key for %s into Keyring\", r)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) decodePublicKey(ctx context.Context, r string) ([]string, error) {\n\tfor _, kd := range []string{keyDir, oldKeyDir} {\n\t\tfilename := filepath.Join(kd, r)\n\t\tif !s.storage.Exists(ctx, filename) {\n\t\t\tdebug.Log(\"Public Key %s not found at %s\", r, filename)\n\n\t\t\tcontinue\n\t\t}\n\t\tbuf, err := s.storage.Get(ctx, filename)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to read Public Key %q %q: %w\", r, filename, err)\n\t\t}\n\n\t\treturn s.crypto.ReadNamesFromKey(ctx, buf)\n\t}\n\n\treturn nil, fmt.Errorf(\"public key %q not found\", r)\n}\n\n// export an ASCII armored public key.\nfunc (s *Store) exportPublicKey(ctx context.Context, exp keyExporter, r string) (string, error) {\n\tfilename := filepath.Join(keyDir, r)\n\n\t// do not overwrite existing keys, unless forced\n\tif !IsPubkeyUpdate(ctx) && s.storage.Exists(ctx, filename) {\n\t\tdebug.Log(\"leaving existing key for %s at %s alone\", filename)\n\n\t\treturn \"\", nil\n\t}\n\n\tpk, err := exp.ExportPublicKey(ctx, r)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to export public key: %w\", err)\n\t}\n\n\t// ECC keys are at least 700 byte, RSA should be a lot bigger\n\tif len(pk) < 32 {\n\t\treturn \"\", fmt.Errorf(\"exported key too small\")\n\t}\n\n\tif err := s.storage.Set(ctx, filename, pk); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to write exported public key to store: %w\", err)\n\t\t}\n\t\tdebug.Log(\"No need to write exported public key %s: already stored\", r)\n\t}\n\n\tdebug.Log(\"exported public keys for %s to %s\", r, filename)\n\n\treturn filename, nil\n}\n\ntype keyImporter interface {\n\tImportPublicKey(ctx context.Context, key []byte) error\n}\ntype keyExporter interface {\n\tExportPublicKey(ctx context.Context, id string) ([]byte, error)\n}\n\nfunc (s *Store) getPublicKey(ctx context.Context, r string) ([]byte, error) {\n\tfor _, kd := range []string{keyDir, oldKeyDir} {\n\t\tfilename := filepath.Join(kd, r)\n\t\tif !s.storage.Exists(ctx, filename) {\n\t\t\tdebug.Log(\"Public Key %s not found at %s\", r, filename)\n\n\t\t\tcontinue\n\t\t}\n\t\tpk, err := s.storage.Get(ctx, filename)\n\n\t\treturn pk, err\n\t}\n\n\treturn nil, fmt.Errorf(\"public key not found in store\")\n}\n\n// import an public key into the default keyring.\nfunc (s *Store) importPublicKey(ctx context.Context, r string) error {\n\tim, ok := s.crypto.(keyImporter)\n\tif !ok {\n\t\tdebug.Log(\"importing public keys not supported by %T\", s.crypto)\n\n\t\treturn nil\n\t}\n\n\tpk, err := s.getPublicKey(ctx, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn im.ImportPublicKey(ctx, pk)\n}\n\ntype locker interface {\n\tLock()\n}\n\n// Lock clears the credential caches of all supported backends.\nfunc (s *Store) Lock() error {\n\tf, ok := s.crypto.(locker)\n\tif !ok {\n\t\tdebug.Log(\"locking not supported by %T in %q\", s.crypto, s.alias)\n\t}\n\n\tif f == nil {\n\t\tdebug.Log(\"backend %q invalid\", s.alias)\n\n\t\treturn nil\n\t}\n\n\tf.Lock()\n\tdebug.Log(\"locked backend %T for %q\", s.crypto, s.alias)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/crypto_test.go",
    "content": "package leaf\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGPG(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.ImportMissingPublicKeys(ctx))\n\n\tnewRecp := \"A3683834\"\n\terr = s.AddRecipient(ctx, newRecp)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.ImportMissingPublicKeys(ctx))\n}\n"
  },
  {
    "path": "internal/store/leaf/fsck.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/age\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/diff\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\n// ErrorSeverity provides a way for a function to specify how severe of an error it experienced.\ntype ErrorSeverity int\n\nconst (\n\terrsNil      ErrorSeverity = iota\n\terrsNonFatal               // an error that was recovered from, but still should be acknowledged\n\terrsFatal                  // an error that terminated the function early\n)\n\nfunc (e ErrorSeverity) String() string {\n\tswitch e {\n\tcase errsNonFatal:\n\t\treturn \"non-fatal\"\n\tcase errsFatal:\n\t\treturn \"fatal\"\n\tcase errsNil:\n\t\treturn \"nil\"\n\tdefault:\n\t\treturn \"nil\"\n\t}\n}\n\ntype fsckMultiError struct {\n\tSeverity ErrorSeverity\n\tErrors   []error\n}\n\nfunc (f *fsckMultiError) IsError() bool {\n\treturn len(f.Errors) > 0\n}\n\nfunc (f *fsckMultiError) Error() string {\n\tif len(f.Errors) < 1 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"[%s] \", f.Severity.String())\n\tmsgs := make([]string, 0, len(f.Errors))\n\tfor _, e := range f.Errors {\n\t\tmsgs = append(msgs, e.Error())\n\t}\n\tsb.WriteString(strings.Join(msgs, \", \"))\n\n\treturn sb.String()\n}\n\nfunc (f *fsckMultiError) Append(s ErrorSeverity, e error) *fsckMultiError {\n\tif e == nil {\n\t\treturn f\n\t}\n\n\tif f.Errors == nil {\n\t\tf.Errors = make([]error, 0, 1)\n\t}\n\n\tf.Errors = append(f.Errors, e)\n\tif s > f.Severity {\n\t\tf.Severity = s\n\t}\n\n\treturn f\n}\n\nfunc (f *fsckMultiError) ErrorOrNil() error {\n\tif len(f.Errors) < 1 {\n\t\treturn nil\n\t}\n\n\treturn f\n}\n\n// Fsck checks all entries matching the given prefix.\nfunc (s *Store) Fsck(ctx context.Context, path string) error {\n\tctx = out.AddPrefix(ctx, \"[\"+s.alias+\"] \")\n\tctx = config.WithMount(ctx, s.alias)\n\tdebug.Log(\"Checking %s\", path)\n\n\t// first let the storage backend check itself\n\tdebug.Log(\"Checking storage backend\")\n\tif err := s.storage.Fsck(ctx); err != nil {\n\t\treturn fmt.Errorf(\"storage backend error: %w\", err)\n\t}\n\n\t// then try to compact storage / rcs\n\tdebug.Log(\"Compacting storage\")\n\tif err := s.storage.Compact(ctx); err != nil {\n\t\treturn fmt.Errorf(\"storage backend compaction failed: %w\", err)\n\t}\n\n\t// make sure all recipients are valid\n\tdebug.Log(\"Checking recipients\")\n\tif err := s.CheckRecipients(ctx); err != nil {\n\t\tout.Errorf(ctx, \"Invalid recipients found: %s\", err)\n\t}\n\n\t// then we'll make sure all the secrets are readable by us and every\n\t// valid recipient\n\tif path != \"\" {\n\t\tout.Printf(ctx, \"Checking all secrets matching %s\", path)\n\t}\n\n\tif err := s.fsckLoop(ctx, path); err != nil {\n\t\treturn err\n\t}\n\n\tif !config.Bool(ctx, \"core.autopush\") {\n\t\tdebug.Log(\"not pushing to git remote, core.autopush is false\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.Push(ctx, \"\", \"\"); err != nil {\n\t\tif !errors.Is(err, store.ErrGitNoRemote) {\n\t\t\tout.Printf(ctx, \"RCS Push failed: %s\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) fsckLoop(ctx context.Context, path string) error {\n\tpcb := ctxutil.GetProgressCallback(ctx)\n\n\t// disable network ops, we will push at the end. pushing on possibly\n\t// every single secret could be terribly slow.\n\tctx = ctxutil.WithNoNetwork(ctx, true)\n\n\t// disable the queue, for batch operations this is not necessary / wanted\n\t// since different git processes might step onto each other toes.\n\tctx = queue.WithQueue(ctx, nil)\n\n\tnames, err := s.List(ctx, path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list entries for %s: %w\", path, err)\n\t}\n\n\tif IsFsckDecrypt(ctx) {\n\t\tctx = ctxutil.WithCommitMessage(ctx, \"fsck --decrypt to fix recipients and format\")\n\t} else {\n\t\tctx = ctxutil.WithCommitMessage(ctx, \"fsck to fix (a limited part of) recipients and format\")\n\t}\n\n\tvar warnings strings.Builder\n\n\tif err := s.fsckUpdatePublicKeys(ctxutil.WithGitCommit(ctx, false)); err != nil {\n\t\tout.Errorf(ctx, \"Failed to update public keys: %s\", err)\n\t} else {\n\t\tctx = ctxutil.AddToCommitMessageBody(ctx, \"- updated public keys\")\n\t}\n\n\tsort.Strings(names)\n\n\tdebug.Log(\"names (%d): %q\", len(names), names)\n\tbuf := &strings.Builder{}\n\tfor _, name := range names {\n\t\tpcb()\n\t\tif after, ok := strings.CutPrefix(name, s.alias+\"/\"); ok {\n\t\t\tname = after\n\t\t}\n\n\t\tdebug.Log(\"[%s] Checking %s\", path, name)\n\n\t\tmsg, err := s.fsckCheckEntry(ctx, name)\n\t\tif err != nil {\n\t\t\twarnings.WriteString(fmt.Sprintf(\"failed to check %q:\\n    %s\\n\", name, err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tbuf.WriteString(msg)\n\t\tbuf.WriteString(\"\\n\")\n\t}\n\tif buf.Len() > 0 {\n\t\tctx = ctxutil.AddToCommitMessageBody(ctx, buf.String())\n\t}\n\n\t// print out any deferred warnings (if any)\n\tif warnings.Len() > 0 {\n\t\tout.Warning(ctx, warnings.String())\n\t}\n\n\tif ctxutil.GetCommitMessageBody(ctx) == \"\" {\n\t\tif warnings.Len() > 0 {\n\t\t\tout.Errorf(ctx, \"Nothing to commit: all secrets that were not up to date failed to be updated\")\n\n\t\t\treturn nil\n\t\t}\n\t\tout.Warningf(ctx, \"Nothing to commit: all secrets up to date\")\n\t}\n\n\tif err := s.storage.TryCommit(ctx, ctxutil.GetCommitMessageFull(ctx)); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) fsckUpdatePublicKeys(ctx context.Context) error {\n\tctx = WithPubkeyUpdate(ctx, true)\n\trs := s.Recipients(ctx)\n\n\t// first import possibly new/updated keys to merge any changes\n\t// that might come from others.\n\tif err := s.ImportMissingPublicKeys(ctx, rs...); err != nil {\n\t\treturn fmt.Errorf(\"failed to import new or updated pubkeys: %w\", err)\n\t}\n\n\t// then export our (possibly updated) keys for consumption\n\t// by others.\n\texported, err := s.UpdateExportedPublicKeys(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update exported pubkeys: %w\", err)\n\t}\n\tdebug.Log(\"Updated exported public keys: %t\", exported)\n\n\treturn nil\n}\n\ntype convertedSecret interface {\n\tgopass.Secret\n\tFromMime() bool\n}\n\nfunc (s *Store) fsckCheckEntry(ctx context.Context, name string) (string, error) {\n\terrs := &fsckMultiError{}\n\trecpNeedFix := false\n\n\tmerr := s.fsckCheckRecipients(ctx, name)\n\tif merr.ErrorOrNil() != nil {\n\t\tif merr.Severity == errsFatal {\n\t\t\treturn \"\", errs.Append(errsFatal, fmt.Errorf(\"checking recipients for %s failed:\\n    %w\", name, merr)).ErrorOrNil()\n\t\t}\n\t\t// the only errsNonFatal error from that function are missing/extra recipients, or unsupported recipient checks\n\t\t// all of which aren't much of an issue since we have yet to correct that by re-encrypting.\n\t\trecpNeedFix = true\n\t\t_ = errs.Append(merr.Severity, merr)\n\t}\n\n\t// make sure we are actually allowed to decode this secret\n\t// if this fails there is no way we could fix anything\n\tif !IsFsckDecrypt(ctx) {\n\t\tif !recpNeedFix {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\treturn \"\", errs.Append(errsFatal, fmt.Errorf(\"secret %s needs re-encryption\", name)).ErrorOrNil()\n\t}\n\n\t// we need to make sure Parsing is enabled in order to parse old Mime secrets\n\tctx = ctxutil.WithShowParsing(ctx, true)\n\tsec, err := s.Get(ctx, name)\n\tif err != nil {\n\t\treturn \"\", errs.Append(errsFatal, fmt.Errorf(\"failed to decode secret %s: %w\", name, err)).ErrorOrNil()\n\t}\n\n\t// check if this is still an old MIME secret.\n\t// Note: the secret was already converted when it was parsed during Get.\n\t// This is just checking if it was converted from MIME or not.\n\t// This branch is pretty much useless right now, but I'd like to add some\n\t// reporting on how many secrets were converted from MIME to new format.\n\t// TODO: report these stats\n\tif cs, ok := sec.(convertedSecret); ok && cs.FromMime() {\n\t\tdebug.Log(\"leftover Mime secret: %s\", name)\n\t}\n\n\tif recpNeedFix {\n\t\tout.Printf(ctx, \"Re-encrypting %s to fix recipients and storage format. [leaf store]\", name)\n\t} else {\n\t\tout.Printf(ctx, \"Re-encrypting %s to fix storage format. [leaf store]\", name)\n\t}\n\n\tif err := s.Set(ctxutil.WithGitCommit(ctx, false), name, sec); err != nil {\n\t\treturn \"\", errs.Append(errsFatal, fmt.Errorf(\"failed to write secret %s: %w\", name, err)).ErrorOrNil()\n\t}\n\n\tmerr = s.fsckCheckRecipients(ctx, name)\n\tif merr.ErrorOrNil() != nil {\n\t\tif merr.Severity == errsFatal {\n\t\t\t_ = errs.Append(merr.Severity, fmt.Errorf(\"checking recipients for %s failed:\\n    %w\", name, merr))\n\t\t} else {\n\t\t\t_ = errs.Append(merr.Severity, merr)\n\t\t}\n\t}\n\n\tif merr.IsError() {\n\t\treturn \"\", merr.ErrorOrNil()\n\t}\n\n\treturn fmt.Sprintf(\"- re-encrypt secret %s\", name), nil\n}\n\nfunc (s *Store) fsckCheckRecipients(ctx context.Context, name string) *fsckMultiError {\n\te := &fsckMultiError{}\n\n\t// now compare the recipients this secret was encoded for and fix it if\n\t// it doesn't match.\n\tciphertext, err := s.storage.Get(ctx, s.Passfile(name))\n\tif err != nil {\n\t\treturn e.Append(errsFatal, fmt.Errorf(\"failed to get raw secret: %w\", err))\n\t}\n\n\tif _, ok := s.crypto.(*age.Age); ok {\n\t\tdebug.Log(\"RecipientIDs not supported yet by age\")\n\t\t_ = e.Append(errsNonFatal, fmt.Errorf(\"recipients check not supported by age backend for now\"))\n\n\t\treturn e\n\t}\n\n\titemRecps, err := s.crypto.RecipientIDs(ctx, ciphertext)\n\tif err != nil {\n\t\treturn e.Append(errsFatal, fmt.Errorf(\"failed to read recipient IDs from raw secret: %w\", err))\n\t}\n\n\titemRecps = fingerprints(ctx, s.crypto, itemRecps)\n\n\trs, err := s.GetRecipients(ctx, name)\n\tif err != nil {\n\t\treturn e.Append(errsFatal, fmt.Errorf(\"failed to get recipients from store: %w\", err))\n\t}\n\n\tperItemStoreRecps := fingerprints(ctx, s.crypto, rs.IDs())\n\n\t// check itemRecps matches storeRecps\n\textra, missing := diff.List(perItemStoreRecps, itemRecps)\n\tif len(missing) > 0 {\n\t\t_ = e.Append(errsNonFatal, fmt.Errorf(\"missing recipients on %s: %+v\", name, missing))\n\t}\n\tif len(extra) > 0 {\n\t\t_ = e.Append(errsNonFatal, fmt.Errorf(\"extra recipients on %s: %+v\", name, extra))\n\t}\n\n\treturn e\n}\n\nfunc fingerprints(ctx context.Context, crypto backend.Crypto, in []string) []string {\n\tout := make([]string, 0, len(in))\n\tfor _, r := range in {\n\t\tout = append(out, crypto.Fingerprint(ctx, r))\n\t}\n\n\treturn out\n}\n\nfunc compareStringSlices(want, have []string) ([]string, []string) {\n\tmissing := []string{}\n\textra := []string{}\n\n\twantMap := make(map[string]struct{}, len(want))\n\thaveMap := make(map[string]struct{}, len(have))\n\n\tfor _, w := range want {\n\t\twantMap[w] = struct{}{}\n\t}\n\n\tfor _, h := range have {\n\t\thaveMap[h] = struct{}{}\n\t}\n\n\tfor k := range wantMap {\n\t\tif _, found := haveMap[k]; !found {\n\t\t\tmissing = append(missing, k)\n\t\t}\n\t}\n\n\tfor k := range haveMap {\n\t\tif _, found := wantMap[k]; !found {\n\t\t\textra = append(extra, k)\n\t\t}\n\t}\n\n\tsort.Strings(missing)\n\tsort.Strings(extra)\n\n\treturn missing, extra\n}\n"
  },
  {
    "path": "internal/store/leaf/fsck_test.go",
    "content": "package leaf\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFsck(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\t// common setup\n\ttempdir := t.TempDir()\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\trs := recipients.New()\n\trs.Add(\"john.doe\")\n\n\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\tfor _, e := range []string{\"foo/bar\", \"foo/baz\", \"foo/zab\"} {\n\t\tsec := secrets.NewAKV()\n\t\tsec.SetPassword(\"bar\")\n\t\trequire.NoError(t, s.Set(ctx, e, sec))\n\t}\n\n\trequire.NoError(t, s.Fsck(ctx, \"\"))\n\tobuf.Reset()\n}\n\nfunc TestCompareStringSlices(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname    string\n\t\tfrom    []string\n\t\tto      []string\n\t\tmissing []string\n\t\textra   []string\n\t}{\n\t\t{\n\t\t\tname:    \"Add foo, remove baz\",\n\t\t\tfrom:    []string{\"foo\", \"bar\"},\n\t\t\tto:      []string{\"baz\", \"bar\"},\n\t\t\tmissing: []string{\"foo\"},\n\t\t\textra:   []string{\"baz\"},\n\t\t},\n\t\t{\n\t\t\tname:    \"Add foo, bar, baz, zab\",\n\t\t\tfrom:    []string{\"foo\", \"bar\"},\n\t\t\tto:      []string{\"foo\", \"bar\", \"bar\", \"baz\", \"zab\"},\n\t\t\tmissing: []string{},\n\t\t\textra:   []string{\"baz\", \"zab\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tmissing, extra := compareStringSlices(tc.from, tc.to)\n\t\t\tassert.Equal(t, tc.missing, missing)\n\t\t\tassert.Equal(t, tc.extra, extra)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/leaf/init.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// IsInitialized returns true if the store is properly initialized.\nfunc (s *Store) IsInitialized(ctx context.Context) bool {\n\tif s == nil || s.storage == nil {\n\t\treturn false\n\t}\n\n\tok := s.storage.Exists(ctx, s.idFile(ctx, \"\"))\n\tdebug.Log(\"store %q is initialized: %t\", s.path, ok)\n\n\treturn ok\n}\n\n// Init tries to initialize a new password store location matching the object.\nfunc (s *Store) Init(ctx context.Context, path string, ids ...string) error {\n\tif s.IsInitialized(ctx) {\n\t\treturn fmt.Errorf(`found already initialized store at %q.\nYou can add secondary stores with 'gopass init --path <path to secondary store> --store <mount name>'`, path)\n\t}\n\n\t// initialize recipient list\n\trs := recipients.New()\n\n\tfor _, id := range ids {\n\t\tif id == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkl, err := s.crypto.FindRecipients(ctx, id)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"no usable key for %q: %s. Ignoring.\", id, err)\n\t\t\tout.Errorf(ctx, \"Failed to fetch public key for %q: %s\", id, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tif len(kl) < 1 {\n\t\t\tdebug.Log(\"no usable key for %q. Ignoring.\", id)\n\t\t\tout.Errorf(ctx, \"No usable keys for %q\", id)\n\n\t\t\tcontinue\n\t\t}\n\n\t\trs.Add(kl[0])\n\t}\n\n\tif len(rs.IDs()) < 1 {\n\t\treturn fmt.Errorf(\"failed to initialize store: no valid recipients given in %+v\", ids)\n\t}\n\n\tkl, err := s.crypto.FindIdentities(ctx, rs.IDs()...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get available private keys: %w\", err)\n\t}\n\n\tif len(kl) < 1 {\n\t\treturn fmt.Errorf(\"none of the recipients has a secret key. You will not be able to decrypt the secrets you add\")\n\t}\n\n\tif err := s.saveRecipients(ctx, rs, \"Initialized Store for \"+strings.Join(rs.IDs(), \", \")); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize store: %w\", err)\n\t}\n\tout.OKf(ctx, \"Wrote recipients to %s\", s.idFile(ctx, \"\"))\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/init_test.go",
    "content": "package leaf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInit(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\trequire.Error(t, s.Init(ctx, \"\", \"0xDEADBEEF\"))\n}\n"
  },
  {
    "path": "internal/store/leaf/link.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Link creates a symlink.\nfunc (s *Store) Link(ctx context.Context, from, to string) error {\n\tif !s.Exists(ctx, from) {\n\t\treturn fmt.Errorf(\"source %q does not exists\", from)\n\t}\n\n\tif s.Exists(ctx, to) {\n\t\treturn fmt.Errorf(\"destination %q already exists\", to)\n\t}\n\n\tif err := s.storage.Link(ctx, s.Passfile(from), s.Passfile(to)); err != nil {\n\t\treturn fmt.Errorf(\"failed to create symlink from %q to %q: %w\", from, to, err)\n\t}\n\n\tdebug.Log(\"created symlink from %q to %q\", from, to)\n\n\tif err := s.storage.Add(ctx, s.Passfile(to)); err != nil {\n\t\tif errors.Is(err, store.ErrGitNotInit) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", to, err)\n\t}\n\n\t// try to enqueue this task, if the queue is not available\n\t// it will return the task and we will execute it inline\n\tt := queue.GetQueue(ctx).Add(func(ctx context.Context) (context.Context, error) {\n\t\treturn nil, s.gitCommitAndPush(ctx, to)\n\t})\n\n\t_, err := t(ctx)\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/store/leaf/link_test.go",
    "content": "package leaf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLink(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"foo\")\n\t_, err = sec.Write([]byte(\"bar\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, s.Set(ctx, \"zab/zab\", sec))\n\n\trequire.NoError(t, s.Link(ctx, \"zab/zab\", \"foo/123\"))\n\n\tp, err := s.Get(ctx, \"foo/123\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foo\", p.Password())\n}\n"
  },
  {
    "path": "internal/store/leaf/list.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Sep is the separator used in lists to separate folders from entries.\nvar Sep = \"/\"\n\n// List will list all entries in this store.\nfunc (s *Store) List(ctx context.Context, prefix string) ([]string, error) {\n\tif s.storage == nil || s.crypto == nil {\n\t\treturn nil, nil\n\t}\n\n\tlst, err := s.storage.List(ctx, prefix)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebug.Log(\"Listing storage content of %s: %+v\", prefix, lst)\n\tout := make([]string, 0, len(lst))\n\tcExt := \".\" + s.crypto.Ext()\n\tfor _, path := range lst {\n\t\tif !strings.HasSuffix(path, cExt) {\n\t\t\tcontinue\n\t\t}\n\t\tpath = strings.TrimSuffix(path, cExt)\n\t\tif s.alias != \"\" {\n\t\t\tpath = s.alias + Sep + path\n\t\t}\n\t\tout = append(out, path)\n\t}\n\tdebug.Log(\"Leaf store entries: %+v\", out)\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "internal/store/leaf/list_test.go",
    "content": "package leaf\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestList(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tprep func(s *Store) error\n\t\tout  []string\n\t}{\n\t\t{\n\t\t\tname: \"Empty store\",\n\t\t\tprep: func(s *Store) error { return nil },\n\t\t\tout:  []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Single entry\",\n\t\t\tprep: func(s *Store) error {\n\t\t\t\tsec := secrets.New()\n\t\t\t\tsec.SetPassword(\"bar\")\n\n\t\t\t\treturn s.Set(ctx, \"foo\", sec)\n\t\t\t},\n\t\t\tout: []string{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Multi-entry-single-level\",\n\t\t\tprep: func(s *Store) error {\n\t\t\t\tfor _, e := range []string{\"foo\", \"bar\", \"baz\"} {\n\t\t\t\t\tsec := secrets.New()\n\t\t\t\t\tsec.SetPassword(\"bar\")\n\t\t\t\t\tif err := s.Set(ctx, e, sec); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tout: []string{\"bar\", \"baz\", \"foo\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Multi-entry-multi-level\",\n\t\t\tprep: func(s *Store) error {\n\t\t\t\tfor _, e := range []string{\"foo/bar\", \"foo/baz\", \"foo/zab\"} {\n\t\t\t\t\tsec := secrets.New()\n\t\t\t\t\tsec.SetPassword(\"bar\")\n\t\t\t\t\tif err := s.Set(ctx, e, sec); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tout: []string{\"foo/bar\", \"foo/baz\", \"foo/zab\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// common setup\n\t\t\ttempdir := t.TempDir()\n\n\t\t\tdefer func() {\n\t\t\t\tobuf.Reset()\n\t\t\t}()\n\n\t\t\ts := &Store{\n\t\t\t\talias:   \"\",\n\t\t\t\tpath:    tempdir,\n\t\t\t\tcrypto:  plain.New(),\n\t\t\t\tstorage: fs.New(tempdir),\n\t\t\t}\n\n\t\t\trs := recipients.New()\n\t\t\trs.Add(\"john.doe\")\n\n\t\t\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\t\t\t// prepare store\n\t\t\trequire.NoError(t, tc.prep(s))\n\t\t\tobuf.Reset()\n\n\t\t\t// run test case\n\t\t\tout, err := s.List(ctx, \"\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.out, out)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/leaf/move.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Copy will copy one entry to another location. Multi-store copies are\n// supported. Each entry has to be decoded and encoded for the destination\n// to make sure it's encrypted for the right set of recipients.\nfunc (s *Store) Copy(ctx context.Context, from, to string) error {\n\t// recursive copy?\n\tif s.IsDir(ctx, from) {\n\t\treturn fmt.Errorf(\"recursive operations are not supported\")\n\t}\n\n\t// try direct copy first\n\terr := s.directMove(ctx, from, to, false)\n\tif err == nil {\n\t\tdebug.Log(\"direct copy %s -> %s successful\", from, to)\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"direct copy failed: %v\", err)\n\n\tcontent, err := s.Get(ctx, from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get %q from store: %w\", from, err)\n\t}\n\n\tif err := s.Set(ctxutil.WithCommitMessage(ctx, fmt.Sprintf(\"Copied from %s to %s\", from, to)), to, content); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn fmt.Errorf(\"failed to save secret %q to store: %w\", to, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\treturn nil\n}\n\n// Move will move one entry from one location to another.\n// Moving an entry will decode it from the old location, encode it\n// for the destination store with the right set of recipients and remove it\n// from the old location afterwards.\nfunc (s *Store) Move(ctx context.Context, from, to string) error {\n\t// recursive move?\n\tif s.IsDir(ctx, from) {\n\t\treturn fmt.Errorf(\"recursive operations are not supported\")\n\t}\n\n\t// try direct move first\n\terr := s.directMove(ctx, from, to, true)\n\tif err == nil {\n\t\tdebug.Log(\"direct move %s -> %s successful\", from, to)\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"direct move failed: %v\", err)\n\n\t// fall back to copy and delete\n\tcontent, err := s.Get(ctx, from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to decrypt %q: %w\", from, err)\n\t}\n\n\tif err := s.Set(ctxutil.WithCommitMessage(ctx, fmt.Sprintf(\"Move from %s to %s\", from, to)), to, content); err != nil {\n\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn fmt.Errorf(\"failed to save secret %q to store: %w\", to, err)\n\t\t}\n\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t}\n\n\tif err := s.Delete(ctx, from); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete %q: %w\", from, err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) directMove(ctx context.Context, from, to string, del bool) error {\n\tpFrom := s.Passfile(from)\n\tpTo := s.Passfile(to)\n\n\t// if original destination has trailing slash,\n\t// it means we should create folder and move/copy source file in it\n\tif strings.HasSuffix(to, \"/\") {\n\t\t// Check if the destination already exists as a file\n\t\tif s.storage.Exists(ctx, to) && !s.storage.IsDir(ctx, to) {\n\t\t\treturn fmt.Errorf(\"destination %q already exists as a file\", to)\n\t\t}\n\t\tpTo = filepath.Join(to, filepath.Base(pFrom))\n\t}\n\n\tdebug.Log(\"directMove %s (%q) -> %s (%q)\", from, to, pFrom, pTo)\n\n\tif err := s.storage.Move(ctx, pFrom, pTo, del); err != nil {\n\t\treturn fmt.Errorf(\"failed to move %q to %q: %w\", from, to, err)\n\t}\n\n\t// It is not possible to perform concurrent git add and git commit commands\n\t// so we need to skip this step when using concurrency and perform them\n\t// at the end of the batch processing.\n\tif IsNoGitOps(ctx) {\n\t\tdebug.Log(\"sub.directMove(%q -> %q) - skipping git ops (disabled)\", from, to)\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.TryAdd(ctx, pFrom, pTo); err != nil {\n\t\treturn fmt.Errorf(\"failed to add %q and %q to git: %w\", pFrom, pTo, err)\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\t// try to enqueue this task, if the queue is not available\n\t// it will return the task and we will execute it inline\n\tt := queue.GetQueue(ctx).Add(func(_ context.Context) (context.Context, error) {\n\t\treturn nil, s.gitCommitAndPush(ctx, to)\n\t})\n\n\t_, err := t(ctx)\n\n\treturn err\n}\n\n// Delete will remove an single entry from the store.\nfunc (s *Store) Delete(ctx context.Context, name string) error {\n\treturn s.delete(ctx, name, false)\n}\n\n// Prune will remove a subtree from the Store.\nfunc (s *Store) Prune(ctx context.Context, tree string) error {\n\treturn s.delete(ctx, tree, true)\n}\n\n// delete will either delete one file or an directory tree depending on the\n// recurse flag.\nfunc (s *Store) delete(ctx context.Context, name string, recurse bool) error {\n\tpath := s.Passfile(name)\n\n\tif recurse {\n\t\tif err := s.deleteRecurse(ctx, name, path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := s.deleteSingle(ctx, path); err != nil {\n\t\t// might fail if we deleted the root of a tree which isn't a secret\n\t\t// itself\n\t\tif !recurse {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\tcommitMsg := ctxutil.GetCommitMessage(ctx)\n\tif commitMsg == \"\" {\n\t\tcommitMsg = fmt.Sprintf(\"Remove %s from store.\", name)\n\t}\n\n\tif err := s.storage.TryCommit(ctx, commitMsg); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\tif !config.Bool(ctx, \"core.autopush\") {\n\t\tdebug.Log(\"not pushing to git remote, core.autopush is false\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.TryPush(ctx, \"\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push change to git remote: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) deleteRecurse(ctx context.Context, name, path string) error {\n\tif !s.storage.IsDir(ctx, name) && !s.storage.Exists(ctx, path) {\n\t\treturn store.ErrNotFound\n\t}\n\n\tname = strings.TrimPrefix(name, string(filepath.Separator))\n\n\tdebug.Log(\"Pruning %s\", name)\n\tif err := s.storage.Prune(ctx, name); err != nil {\n\t\tdebug.Log(\"storage.Prune(%v) failed\", name)\n\n\t\treturn err\n\t}\n\n\tif err := s.storage.TryAdd(ctx, name); err != nil {\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", path, err)\n\t}\n\tdebug.Log(\"pruned\")\n\n\treturn nil\n}\n\nfunc (s *Store) deleteSingle(ctx context.Context, path string) error {\n\tif !s.storage.Exists(ctx, path) {\n\t\treturn store.ErrNotFound\n\t}\n\n\tdebug.Log(\"Deleting %s\", path)\n\tif err := s.storage.Delete(ctx, path); err != nil {\n\t\treturn err\n\t}\n\n\tif err := s.storage.TryAdd(ctx, path); err != nil {\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", path, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/move_test.go",
    "content": "package leaf\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCopy(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\ttf   func(s *Store) func(t *testing.T)\n\t}{\n\t\t{\n\t\t\tname: \"Empty store\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\trequire.Error(t, s.Copy(ctx, \"foo\", \"bar\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single entry\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tnsec := secrets.NewAKV()\n\t\t\t\t\tnsec.SetPassword(\"bar\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo\", nsec))\n\t\t\t\t\trequire.NoError(t, s.Copy(ctx, \"foo\", \"bar\"))\n\t\t\t\t\tsec, err := s.Get(ctx, \"foo\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, \"bar\", sec.Password())\n\t\t\t\t\tsec, err = s.Get(ctx, \"bar\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, \"bar\", sec.Password())\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Recursive\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tsec := secrets.NewAKV()\n\t\t\t\t\tsec.SetPassword(\"baz\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/baz\", sec))\n\t\t\t\t\tsec.SetPassword(\"zab\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/zab\", sec))\n\t\t\t\t\trequire.Error(t, s.Copy(ctx, \"foo\", \"bar\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// common setup\n\t\t\ttempdir := t.TempDir()\n\n\t\t\tdefer func() {\n\t\t\t\tobuf.Reset()\n\t\t\t}()\n\n\t\t\ts := &Store{\n\t\t\t\talias:   \"\",\n\t\t\t\tpath:    tempdir,\n\t\t\t\tcrypto:  plain.New(),\n\t\t\t\tstorage: fs.New(tempdir),\n\t\t\t}\n\n\t\t\trs := recipients.New()\n\t\t\trs.Add(\"john.doe\")\n\t\t\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\t\t\t// run test case\n\t\t\tt.Run(tc.name, tc.tf(s))\n\t\t})\n\t}\n}\n\nfunc TestMove(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\ttf   func(s *Store) func(t *testing.T)\n\t}{\n\t\t{\n\t\t\tname: \"Empty store\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\trequire.Error(t, s.Move(ctx, \"foo\", \"bar\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single entry\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tnsec := secrets.NewAKV()\n\t\t\t\t\tnsec.SetPassword(\"bar\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo\", nsec))\n\t\t\t\t\trequire.NoError(t, s.Move(ctx, \"foo\", \"bar\"))\n\t\t\t\t\t_, err := s.Get(ctx, \"foo\")\n\t\t\t\t\trequire.Error(t, err)\n\n\t\t\t\t\tsec, err := s.Get(ctx, \"bar\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, \"bar\", sec.Password())\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Recursive\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tsec := secrets.NewAKV()\n\t\t\t\t\tsec.SetPassword(\"baz\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/baz\", sec))\n\t\t\t\t\tsec.SetPassword(\"zab\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/zab\", sec))\n\t\t\t\t\trequire.Error(t, s.Move(ctx, \"foo\", \"bar\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// common setup\n\t\t\ttempdir := t.TempDir()\n\n\t\t\tdefer func() {\n\t\t\t\tobuf.Reset()\n\t\t\t}()\n\n\t\t\ts := &Store{\n\t\t\t\talias:   \"\",\n\t\t\t\tpath:    tempdir,\n\t\t\t\tcrypto:  plain.New(),\n\t\t\t\tstorage: fs.New(tempdir),\n\t\t\t}\n\n\t\t\trs := recipients.New()\n\t\t\trs.Add(\"john.doe\")\n\n\t\t\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\t\t\t// run test case\n\t\t\tt.Run(tc.name, tc.tf(s))\n\t\t})\n\t}\n}\n\nfunc TestDelete(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\ttf   func(s *Store) func(t *testing.T)\n\t}{\n\t\t{\n\t\t\tname: \"Empty store\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\trequire.Error(t, s.Delete(ctx, \"foo\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single entry\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tsec := secrets.NewAKV()\n\t\t\t\t\tsec.SetPassword(\"bar\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo\", sec))\n\t\t\t\t\trequire.NoError(t, s.Delete(ctx, \"foo\"))\n\t\t\t\t\t_, err := s.Get(ctx, \"foo\")\n\t\t\t\t\trequire.Error(t, err)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// common setup\n\t\t\ttempdir := t.TempDir()\n\n\t\t\tdefer func() {\n\t\t\t\tobuf.Reset()\n\t\t\t}()\n\n\t\t\ts := &Store{\n\t\t\t\talias:   \"\",\n\t\t\t\tpath:    tempdir,\n\t\t\t\tcrypto:  plain.New(),\n\t\t\t\tstorage: fs.New(tempdir),\n\t\t\t}\n\n\t\t\trs := recipients.New()\n\t\t\trs.Add(\"john.doe\")\n\n\t\t\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\t\t\t// run test case\n\t\t\tt.Run(tc.name, tc.tf(s))\n\t\t})\n\t}\n}\n\nfunc TestPrune(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = config.NewInMemory().WithConfig(ctx)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\ttf   func(s *Store) func(t *testing.T)\n\t}{\n\t\t{\n\t\t\tname: \"Empty store\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\trequire.Error(t, s.Prune(ctx, \"foo\"))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single entry\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tsec := secrets.NewAKV()\n\t\t\t\t\tsec.SetPassword(\"bar\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo\", sec))\n\t\t\t\t\trequire.NoError(t, s.Prune(ctx, \"foo\"))\n\n\t\t\t\t\t_, err := s.Get(ctx, \"foo\")\n\t\t\t\t\trequire.Error(t, err)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multi entry nested\",\n\t\t\ttf: func(s *Store) func(t *testing.T) {\n\t\t\t\treturn func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tsec := secrets.NewAKV()\n\t\t\t\t\tsec.SetPassword(\"bar\")\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/baz\", sec))\n\t\t\t\t\trequire.NoError(t, s.Set(ctx, \"foo/bar/zab\", sec))\n\t\t\t\t\trequire.NoError(t, s.Prune(ctx, \"foo/bar\"))\n\n\t\t\t\t\t_, err := s.Get(ctx, \"foo/bar/baz\")\n\t\t\t\t\trequire.Error(t, err)\n\n\t\t\t\t\t_, err = s.Get(ctx, \"foo/bar/zab\")\n\t\t\t\t\trequire.Error(t, err)\n\n\t\t\t\t\t// delete empty folder\n\t\t\t\t\trequire.Error(t, s.Prune(ctx, \"foo/\"), \"delete non-existing entry\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// common setup\n\t\t\ttempdir := t.TempDir()\n\n\t\t\tdefer func() {\n\t\t\t\tobuf.Reset()\n\t\t\t}()\n\n\t\t\ts := &Store{\n\t\t\t\talias:   \"\",\n\t\t\t\tpath:    tempdir,\n\t\t\t\tcrypto:  plain.New(),\n\t\t\t\tstorage: fs.New(tempdir),\n\t\t\t}\n\n\t\t\trs := recipients.New()\n\t\t\trs.Add(\"john.doe\")\n\n\t\t\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test\"))\n\n\t\t\t// run test case\n\t\t\tt.Run(tc.name, tc.tf(s))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/leaf/rcs.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets/secparse\"\n)\n\n// GitInit initializes the git storage.\nfunc (s *Store) GitInit(ctx context.Context) error {\n\tstorage, err := backend.InitStorage(ctx, backend.GetStorageBackend(ctx), s.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.storage = storage\n\n\treturn nil\n}\n\n// ListRevisions will list all revisions for a secret.\nfunc (s *Store) ListRevisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\tp := s.Passfile(name)\n\n\treturn s.storage.Revisions(ctx, p)\n}\n\n// GetRevision will retrieve a single revision from the backend.\nfunc (s *Store) GetRevision(ctx context.Context, name, revision string) (gopass.Secret, error) {\n\tp := s.Passfile(name)\n\tciphertext, err := s.storage.GetRevision(ctx, p, revision)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get ciphertext of %q@%q: %w\", name, revision, err)\n\t}\n\n\tcontent, err := s.crypto.Decrypt(ctx, ciphertext)\n\tif err != nil {\n\t\tdebug.Log(\"Decryption failed: %s\", err)\n\n\t\treturn nil, store.ErrDecrypt\n\t}\n\n\tsec, err := secparse.Parse(content)\n\tif err != nil {\n\t\tdebug.Log(\"Failed to parse YAML: %s\", err)\n\t}\n\n\treturn sec, nil\n}\n\n// GitStatus shows the git status output.\nfunc (s *Store) GitStatus(ctx context.Context, _ string) error {\n\tdebug.Log(\"RCS status for %s\", s.path)\n\tbuf, err := s.storage.Status(ctx)\n\tif err != nil {\n\t\tdebug.Log(\"RCS status failed for %s: %s\", s.path, err)\n\n\t\treturn fmt.Errorf(\"failed to get RCS status for %s: %w\", s.path, err)\n\t}\n\n\tout.Printf(ctx, string(buf))\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/rcs_test.go",
    "content": "package leaf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGit(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, s.Storage())\n\trequire.Equal(t, \"fs\", s.Storage().Name())\n\trequire.NoError(t, s.Storage().InitConfig(ctx, \"foo\", \"bar@baz.com\"))\n\t// RCS ops not supported by the fs backend\n\trequire.Error(t, s.Storage().AddRemote(ctx, \"foo\", \"bar\"))\n\trequire.Error(t, s.Storage().Pull(ctx, \"origin\", \"master\"))\n\trequire.Error(t, s.Storage().Push(ctx, \"origin\", \"master\"))\n\n\trequire.NoError(t, s.GitInit(ctx))\n\trequire.NoError(t, s.GitInit(backend.WithStorageBackend(ctx, backend.FS)))\n\trequire.Error(t, s.GitInit(backend.WithStorageBackend(ctx, -1)))\n\n\tctx = ctxutil.WithUsername(ctx, \"foo\")\n\tctx = ctxutil.WithEmail(ctx, \"foo@baz.com\")\n\trequire.NoError(t, s.GitInit(backend.WithStorageBackend(ctx, backend.GitFS)))\n}\n\nfunc TestGitRevisions(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, s.Storage())\n\trequire.Equal(t, \"fs\", s.Storage().Name())\n\trequire.NoError(t, s.Storage().InitConfig(ctx, \"foo\", \"bar@baz.com\"))\n\n\trevs, err := s.ListRevisions(ctx, \"foo\")\n\trequire.Error(t, err)  // not supported by the fs backend\n\tassert.Len(t, revs, 1) // but it will still give a fake \"latest\" rev\n\n\tsec, err := s.GetRevision(ctx, \"foo\", \"latest\")\n\trequire.Error(t, err)\n\tassert.Nil(t, sec)\n}\n"
  },
  {
    "path": "internal/store/leaf/read.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets/secparse\"\n)\n\n// Get returns the plaintext of a single key.\nfunc (s *Store) Get(ctx context.Context, name string) (gopass.Secret, error) {\n\tp := s.Passfile(name)\n\n\tciphertext, err := s.storage.Get(ctx, p)\n\tif err != nil {\n\t\tdebug.Log(\"File %s not found: %s\", p, err)\n\n\t\treturn nil, store.ErrNotFound\n\t}\n\n\tcontent, err := s.crypto.Decrypt(ctx, ciphertext)\n\tif err != nil {\n\t\tout.Errorf(ctx, \"Decryption failed: %s\\n%s\", err, string(content))\n\n\t\treturn nil, store.ErrDecrypt\n\t}\n\n\tif !ctxutil.IsShowParsing(ctx) {\n\t\tdebug.Log(\"secrets parsing is disabled. parsing as AKV\")\n\n\t\treturn secrets.ParseAKV(content), nil\n\t}\n\n\tdebug.Log(\"secrets parsing is enabled\")\n\n\treturn secparse.Parse(content)\n}\n"
  },
  {
    "path": "internal/store/leaf/recipients.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\nconst (\n\tkeyDir    = \".public-keys\"\n\toldKeyDir = \".gpg-keys\"\n)\n\n// ErrInvalidHash indicates an outdated value of `recipients.hash`.\nvar ErrInvalidHash = fmt.Errorf(\"recipients.hash invalid\")\n\n// InvalidRecipientsError is a custom error type that contains a\n// list of invalid recipients with their check failures.\ntype InvalidRecipientsError struct {\n\tInvalid map[string]error\n}\n\nfunc (e InvalidRecipientsError) Error() string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"Invalid Recipients: \")\n\tfor _, k := range set.SortedKeys(e.Invalid) {\n\t\tsb.WriteString(k)\n\t\tsb.WriteString(\": \")\n\t\tsb.WriteString(e.Invalid[k].Error())\n\t\tsb.WriteString(\", \")\n\t}\n\n\treturn sb.String()\n}\n\n// IsError returns true if this multi error contains any underlying errors.\nfunc (e InvalidRecipientsError) IsError() bool {\n\treturn len(e.Invalid) > 0\n}\n\n// Recipients returns the list of recipients of this store.\nfunc (s *Store) Recipients(ctx context.Context) []string {\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\tout.Errorf(ctx, \"failed to read recipient list: %s\", err)\n\t\tout.Notice(ctx, \"Please review the recipients list and confirm any changes with 'gopass recipients ack'\")\n\t}\n\n\treturn rs.IDs()\n}\n\n// RecipientsTree returns a mapping of secrets to recipients.\n// Note: Usually that is one set of recipients per store, but we\n// offer limited support of different recipients per sub-directory\n// so this is why we are here.\nfunc (s *Store) RecipientsTree(ctx context.Context) map[string][]string {\n\tidfs := s.idFiles(ctx)\n\tout := make(map[string][]string, len(idfs))\n\n\troot := s.Recipients(ctx)\n\tfor _, idf := range idfs {\n\t\tif strings.HasPrefix(idf, \".\") {\n\t\t\tcontinue\n\t\t}\n\t\tsrs, err := s.getRecipients(ctx, idf)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to list recipients: %s\", err)\n\n\t\t\tcontinue\n\t\t}\n\t\tif cmp.Equal(out[\"\"], srs) {\n\t\t\tdebug.Log(\"root recipients equal secret recipients from %s\", idf)\n\n\t\t\tcontinue\n\t\t}\n\t\tdir := filepath.Dir(idf)\n\t\tdebug.Log(\"adding recipients %+v for %s\", srs, dir)\n\t\tout[dir] = srs.IDs()\n\t}\n\n\tout[\"\"] = root\n\n\treturn out\n}\n\n// AllRecipients returns a list of all recipients of this store,\n// including all sub-stores.\nfunc (s *Store) AllRecipients(ctx context.Context) *recipients.Recipients {\n\trs := recipients.New()\n\tfor _, recs := range s.RecipientsTree(ctx) {\n\t\tfor _, r := range recs {\n\t\t\trs.Add(r)\n\t\t}\n\t}\n\n\treturn rs\n}\n\n// CheckRecipients makes sure all existing recipients are valid.\nfunc (s *Store) CheckRecipients(ctx context.Context) error {\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read recipient list: %w\", err)\n\t}\n\n\ter := InvalidRecipientsError{\n\t\tInvalid: make(map[string]error, len(rs.IDs())),\n\t}\n\tfor _, k := range rs.IDs() {\n\t\tvalidKeys, err := s.crypto.FindRecipients(ctx, k)\n\t\tif err != nil {\n\t\t\tdebug.Log(\"no GPG key info (unexpected) for %s: %s\", k, err)\n\t\t\ter.Invalid[k] = err\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(validKeys) < 1 {\n\t\t\tdebug.Log(\"no valid keys (expired?) for %s\", k)\n\t\t\ter.Invalid[k] = fmt.Errorf(\"no valid keys (expired?)\")\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"valid keys found for %s\", k)\n\t}\n\n\tif er.IsError() {\n\t\treturn er\n\t}\n\n\treturn nil\n}\n\n// AddRecipient adds a new recipient to the list.\nfunc (s *Store) AddRecipient(ctx context.Context, id string) error {\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read recipient list: %w\", err)\n\t}\n\n\tdebug.Log(\"new recipient: %q - existing: %+v\", id, rs)\n\n\tidAlreadyInStore := rs.Has(id)\n\tif idAlreadyInStore {\n\t\tif !termio.AskForConfirmation(ctx, fmt.Sprintf(\"key %q already in store. Do you want to re-encrypt with public key? This is useful if you changed your public key (e.g. added subkeys).\", id)) {\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\trs.Add(id)\n\n\t\tif err := s.saveRecipients(ctx, rs, \"Added Recipient \"+id); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to save recipients: %w\", err)\n\t\t}\n\t}\n\n\tout.Printf(ctx, \"Reencrypting existing secrets. This may take some time ...\")\n\n\tcommitMsg := \"Recipient \" + id\n\tif idAlreadyInStore {\n\t\tcommitMsg = \"Re-encrypted Store for \" + commitMsg\n\t} else {\n\t\tcommitMsg = \"Added \" + commitMsg\n\t}\n\n\treturn s.reencrypt(ctxutil.WithCommitMessage(ctx, commitMsg))\n}\n\n// SaveRecipients persists the current recipients on disk. Setting ack to true\n// will acknowledge an invalid hash and allow updating it.\nfunc (s *Store) SaveRecipients(ctx context.Context, ack bool) error {\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\tif !errors.Is(err, ErrInvalidHash) || !ack {\n\t\t\treturn fmt.Errorf(\"failed to get recipients: %w\", err)\n\t\t}\n\t}\n\n\treturn s.saveRecipients(ctx, rs, \"Save Recipients\")\n}\n\n// SetRecipients will update the stored recipients.\nfunc (s *Store) SetRecipients(ctx context.Context, rs *recipients.Recipients) error {\n\treturn s.saveRecipients(ctx, rs, \"Set Recipients\")\n}\n\n// RemoveRecipient will remove the given recipient from the store\n// but if this key is not available on this machine we\n// just try to remove it literally.\nfunc (s *Store) RemoveRecipient(ctx context.Context, key string) error {\n\trs, err := s.GetRecipients(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read recipient list: %w\", err)\n\t}\n\n\tvar removed int\nRECIPIENTS:\n\tfor _, k := range rs.IDs() { //nolint:whitespace\n\t\tdebug.V(1).Log(\"testing key: %q\", k)\n\t\t// First lets try a simple match of the stored ids\n\t\tif k == key {\n\t\t\tdebug.Log(\"removing recipient based on id match %s\", k)\n\t\t\tif rs.Remove(k) {\n\t\t\t\tremoved++\n\t\t\t}\n\n\t\t\tcontinue RECIPIENTS\n\t\t}\n\n\t\t// If we don't match immediately, we may need to loop through the recipient keys to try and match.\n\t\t// To do this though, we need to ensure that we also do a FindRecipients on the id name from the stored ids.\n\t\trecipientIds, err := s.crypto.FindRecipients(ctx, k)\n\t\tif err != nil {\n\t\t\tout.Warningf(ctx, \"Warning: Failed to get GPG Key Info for %s: %s\", k, err)\n\t\t}\n\t\tdebug.Log(\"returned the following ids for recipient %s: %s\", k, recipientIds)\n\n\t\t// if the key is available locally we can also match the id against\n\t\t// the fingerprint or failing that we can try against the recipientIds\n\t\tif strings.HasSuffix(key, k) {\n\t\t\tdebug.Log(\"removing recipient based on id suffix match: %s %s\", key, k)\n\t\t\tif rs.Remove(k) {\n\t\t\t\tremoved++\n\t\t\t}\n\n\t\t\tcontinue RECIPIENTS\n\t\t}\n\n\t\tfor _, recipientID := range recipientIds {\n\t\t\tif recipientID == key {\n\t\t\t\tdebug.Log(\"removing recipient based on recipient id match %s\", recipientID)\n\t\t\t\tif rs.Remove(k) {\n\t\t\t\t\tremoved++\n\t\t\t\t}\n\n\t\t\t\tcontinue RECIPIENTS\n\t\t\t}\n\t\t}\n\t}\n\n\tif removed < 1 {\n\t\treturn fmt.Errorf(\"recipient not in store\")\n\t}\n\n\tif err := s.saveRecipients(ctx, rs, \"Removed Recipient \"+key); err != nil {\n\t\treturn fmt.Errorf(\"failed to save recipients: %w\", err)\n\t}\n\n\treturn s.reencrypt(ctxutil.WithCommitMessage(ctx, \"Removed Recipient \"+key))\n}\n\nfunc (s *Store) ensureOurKeyID(ctx context.Context, recp []string) []string {\n\tkl, _ := s.crypto.FindIdentities(ctx, recp...)\n\tif len(kl) > 0 {\n\t\tdebug.Log(\"one of our key is already in the recipient list, not changing it\")\n\n\t\treturn recp\n\t}\n\n\tourID := s.OurKeyID(ctx)\n\tif ourID == \"\" {\n\t\tdebug.Log(\"no owner key found, couldn't add it to the recipients list\")\n\n\t\treturn recp\n\t}\n\tdebug.Log(\"adding our key to the recipient list\")\n\trecp = append(recp, ourID)\n\n\treturn recp\n}\n\n// OurKeyID returns the key fingprint this user can use to access the store\n// (if any).\nfunc (s *Store) OurKeyID(ctx context.Context) string {\n\trecp := s.Recipients(ctx)\n\n\tdebug.Log(\"getting our key ID from store for recipients %v\", recp)\n\n\tkl, err := s.crypto.FindIdentities(ctx, recp...)\n\tif err != nil || len(kl) < 1 {\n\t\tdebug.Log(\"WARNING: no owner key found in %v\", recp)\n\t\tout.Warning(ctx, \"No owner key found. Make sure your key is fully trusted.\")\n\n\t\treturn \"\"\n\t}\n\n\treturn kl[0]\n}\n\n// GetRecipients will load all Recipients from the .gpg-id file for the given\n// secret path.\nfunc (s *Store) GetRecipients(ctx context.Context, name string) (*recipients.Recipients, error) {\n\treturn s.getRecipients(ctx, s.idFile(ctx, name))\n}\n\nfunc (s *Store) getRecipients(ctx context.Context, idf string) (*recipients.Recipients, error) {\n\tbuf, err := s.storage.Get(ctx, idf)\n\tif err != nil {\n\t\treturn recipients.New(), fmt.Errorf(\"failed to get recipients from IDFile %q: %w\", idf, err)\n\t}\n\n\trs := recipients.Unmarshal(buf)\n\n\tcfg, _ := config.FromContext(ctx)\n\t// check recipients hash, global config takes precedence here for security reasons\n\tif cfg.GetGlobal(\"recipients.check\") != \"true\" && !config.AsBool(cfg.GetM(s.alias, \"recipients.check\")) {\n\t\treturn rs, nil\n\t}\n\n\t// we do NOT support local recipients.hash keys since they could be remotely changed\n\tcfgHash := cfg.GetGlobal(s.rhKey())\n\trsHash := rs.Hash()\n\tif rsHash != cfgHash {\n\t\treturn rs, fmt.Errorf(\"config hash %q= %q - Recipients file %q = %q: %w\", s.rhKey(), cfgHash, idf, rsHash, ErrInvalidHash)\n\t}\n\n\treturn rs, nil\n}\n\n// UpdateExportedPublicKeys will export any possibly missing public keys to the\n// stores .public-keys directory.\nfunc (s *Store) UpdateExportedPublicKeys(ctx context.Context) (bool, error) {\n\texp, ok := s.crypto.(keyExporter)\n\tif !ok {\n\t\tdebug.Log(\"not exporting public keys for %T\", s.crypto)\n\n\t\treturn false, nil\n\t}\n\n\trecipients := make(map[string]bool, s.AllRecipients(ctx).Len())\n\tfor _, r := range s.AllRecipients(ctx).IDs() {\n\t\trecipients[r] = true\n\t}\n\n\t// add any missing keys\n\tfailed, exported := s.addMissingKeys(ctx, exp, recipients)\n\n\t// remove any extra key files, we do not support this at the local config level\n\t// TODO(GH-2620): Temporarily disabled by default until we fix the\n\t// key cleanup.\n\tif cfg, _ := config.FromContext(ctx); cfg.GetGlobal(\"recipients.remove-extra-keys\") == \"true\" {\n\t\tf, e := s.removeExtraKeys(ctx, recipients)\n\t\tfailed = failed || f\n\t\texported = exported || e\n\t}\n\n\tif exported && ctxutil.IsGitCommit(ctx) {\n\t\tif err := s.storage.TryCommit(ctx, \"Updated exported Public Keys\"); err != nil {\n\t\t\tfailed = true\n\n\t\t\tout.Errorf(ctx, \"Failed to git commit: %s\", err)\n\t\t}\n\t}\n\n\tif failed {\n\t\treturn exported, fmt.Errorf(\"some keys failed\")\n\t}\n\n\treturn exported, nil\n}\n\nfunc (s *Store) addMissingKeys(ctx context.Context, exp keyExporter, recipients map[string]bool) (bool, bool) {\n\tvar failed, exported bool\n\n\tfor r := range recipients {\n\t\tif r == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tpath, err := s.exportPublicKey(ctx, exp, r)\n\t\tif err != nil {\n\t\t\tfailed = true\n\n\t\t\tout.Errorf(ctx, \"failed to export public key for %q: %s\", r, err)\n\n\t\t\tcontinue\n\t\t}\n\t\tif path == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// at least one key has been exported\n\t\texported = true\n\t\tif err := s.storage.TryAdd(ctx, path); err != nil {\n\t\t\tfailed = true\n\n\t\t\tout.Errorf(ctx, \"failed to add public key for %q to git: %s\", r, err)\n\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn failed, exported\n}\n\nfunc extraKeys(recipients map[string]bool, keys []string) []string {\n\textras := make([]string, 0, len(keys))\n\tfor _, key := range keys {\n\t\t// do not use filepath, that would break on Windows. storage.List normalizes all paths\n\t\t// returned to normal (forward) slashes. Even on Windows.\n\t\tkey := path.Base(key)\n\n\t\tif recipients[key] {\n\t\t\tdebug.Log(\"Key %s found. Not removing\", key)\n\n\t\t\tcontinue\n\t\t}\n\t\textras = append(extras, key)\n\t}\n\n\treturn extras\n}\n\nfunc (s *Store) removeExtraKeys(ctx context.Context, recipients map[string]bool) (bool, bool) {\n\tvar failed, exported bool\n\n\tkeys, err := s.storage.List(ctx, keyDir)\n\tif err != nil {\n\t\tfailed = true\n\n\t\tout.Errorf(ctx, \"Failed to list keys: %s\", err)\n\t}\n\n\tdebug.Log(\"Checking %q for extra keys that need to be removed\", keys)\n\tfor _, key := range extraKeys(recipients, keys) {\n\t\tdebug.Log(\"Removing extra key %s\", key)\n\n\t\tif err := s.storage.Delete(ctx, filepath.Join(keyDir, key)); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to remove extra key %q: %s\", key, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := s.storage.Add(ctx, filepath.Join(keyDir, key)); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to mark extra key for removal %q: %s\", key, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// to ensure the commit\n\t\texported = true\n\t\tdebug.Log(\"Removed extra key %s\", key)\n\t}\n\n\treturn failed, exported\n}\n\ntype recipientMarshaler interface {\n\tIDs() []string\n\tMarshal() []byte\n\tHash() string\n}\n\n// Save all Recipients in memory to the recipients file on disk.\nfunc (s *Store) saveRecipients(ctx context.Context, rs recipientMarshaler, msg string) error {\n\tif rs == nil {\n\t\treturn fmt.Errorf(\"need valid recipients\")\n\t}\n\tif len(rs.IDs()) < 1 {\n\t\treturn fmt.Errorf(\"can not remove all recipients\")\n\t}\n\n\tidf := s.idFile(ctx, \"\")\n\n\tbuf := rs.Marshal()\n\terrSet := s.storage.Set(ctx, idf, buf)\n\tif errSet != nil && !errors.Is(errSet, store.ErrMeaninglessWrite) {\n\t\treturn fmt.Errorf(\"failed to write recipients file: %w\", errSet)\n\t}\n\n\t// always save recipients hash to global config\n\tcfg, _ := config.FromContext(ctx)\n\tif err := cfg.Set(\"\", s.rhKey(), rs.Hash()); err != nil {\n\t\tout.Errorf(ctx, \"Failed to update %s: %s\", s.rhKey(), err)\n\t}\n\n\t// save all recipients public keys to the repo if wanted\n\tif config.AsBool(cfg.GetM(s.alias, \"core.exportkeys\")) {\n\t\tdebug.Log(\"updating exported keys\")\n\t\tif _, err := s.UpdateExportedPublicKeys(ctx); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to export missing public keys: %s\", err)\n\t\t}\n\t} else {\n\t\tdebug.Log(\"updating exported keys not requested\")\n\t}\n\n\tif errors.Is(errSet, store.ErrMeaninglessWrite) {\n\t\tdebug.Log(\"no need to overwrite recipient file: ErrMeaninglessWrite\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.TryAdd(ctx, idf); err != nil {\n\t\treturn fmt.Errorf(\"failed to add file %q to git: %w\", idf, err)\n\t}\n\n\tif err := s.storage.TryCommit(ctx, msg); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\tif !config.AsBool(cfg.GetM(s.alias, \"core.autopush\")) {\n\t\tdebug.Log(\"not pushing to git remote, core.autopush is false\")\n\n\t\treturn nil\n\t}\n\n\t// push to remote repo\n\tdebug.Log(\"pushing changes to git remote\")\n\tif err := s.storage.Push(ctx, \"\", \"\"); err != nil {\n\t\tif errors.Is(err, store.ErrGitNotInit) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif errors.Is(err, store.ErrGitNoRemote) {\n\t\t\tmsg := \"Warning: git has no remote. Ignoring auto-push option\\n\" +\n\t\t\t\t\"Run: gopass git remote add origin ...\"\n\t\t\tdebug.Log(msg)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to push changes to git: %w\", err)\n\t}\n\n\tdebug.Log(\"recipients saved\")\n\n\treturn nil\n}\n\nfunc (s *Store) rhKey() string {\n\tif s.alias == \"\" {\n\t\treturn \"recipients.hash\"\n\t}\n\n\treturn fmt.Sprintf(\"recipients.%s.hash\", s.alias)\n}\n"
  },
  {
    "path": "internal/store/leaf/recipients_test.go",
    "content": "package leaf\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/backend/storage/fs\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/recipients\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetRecipientsDefault(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\ttempdir := t.TempDir()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tgenRecs, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\tassert.Equal(t, genRecs, s.Recipients(ctx))\n\trecs, err := s.GetRecipients(ctx, \"\")\n\trequire.NoError(t, err)\n\n\tids := recs.IDs()\n\tsort.Strings(ids)\n\tassert.Equal(t, genRecs, ids)\n}\n\nfunc TestGetRecipientsSubID(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\ttempdir := t.TempDir()\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tgenRecs, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\trecs, err := s.GetRecipients(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, genRecs, recs.IDs())\n\n\terr = os.WriteFile(filepath.Join(tempdir, \"foo\", \"bar\", s.crypto.IDFile()), []byte(\"john.doe\\n\"), 0o600)\n\trequire.NoError(t, err)\n\n\trecs, err = s.GetRecipients(ctx, \"foo/bar/baz\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"john.doe\"}, recs.IDs())\n}\n\nfunc TestSaveRecipients(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\ttempdir := t.TempDir()\n\n\t_, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\t// remove recipients\n\t_ = os.Remove(filepath.Join(tempdir, s.crypto.IDFile()))\n\n\trs := recipients.New()\n\trs.Add(\"john.doe\")\n\n\trequire.NoError(t, s.saveRecipients(ctx, rs, \"test-save-recipients\"))\n\trequire.Error(t, s.saveRecipients(ctx, nil, \"test-save-recipients\"))\n\n\tbuf, err := s.storage.Get(ctx, s.idFile(ctx, \"\"))\n\trequire.NoError(t, err)\n\n\tfoundRecs := []string{}\n\tscanner := bufio.NewScanner(bytes.NewReader(buf))\n\n\tfor scanner.Scan() {\n\t\tfoundRecs = append(foundRecs, strings.TrimSpace(scanner.Text()))\n\t}\n\n\tsort.Strings(foundRecs)\n\n\tids := rs.IDs()\n\tfor i := range ids {\n\t\tif i >= len(foundRecs) {\n\t\t\tt.Errorf(\"Read too few recipients\")\n\n\t\t\tbreak\n\t\t}\n\n\t\tif ids[i] != foundRecs[i] {\n\t\t\tt.Errorf(\"Mismatch at %d: %s vs %s\", i, ids[i], foundRecs[i])\n\t\t}\n\t}\n}\n\nfunc TestAddRecipient(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithHidden(ctx, true)\n\tctx = config.NewInMemory().WithConfig(ctx)\n\n\ttempdir := t.TempDir()\n\n\tgenRecs, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\tnewRecp := \"A3683834\"\n\n\terr = s.AddRecipient(ctx, newRecp)\n\trequire.NoError(t, err)\n\n\trs, err := s.GetRecipients(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, append(genRecs, newRecp), rs.IDs())\n\n\terr = s.SaveRecipients(ctx, false)\n\trequire.NoError(t, err)\n}\n\nfunc TestRemoveRecipient(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithHidden(ctx, true)\n\tctx = config.NewInMemory().WithConfig(ctx)\n\n\ttempdir := t.TempDir()\n\n\t_, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\ts := &Store{\n\t\talias:   \"\",\n\t\tpath:    tempdir,\n\t\tcrypto:  plain.New(),\n\t\tstorage: fs.New(tempdir),\n\t}\n\n\terr = s.RemoveRecipient(ctx, \"0xDEADBEEF\")\n\trequire.NoError(t, err)\n\n\trs, err := s.GetRecipients(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"0xFEEDBEEF\"}, rs.IDs())\n}\n\nfunc TestListRecipients(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\ttempdir := t.TempDir()\n\n\tgenRecs, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tctx, err = backend.WithCryptoBackendString(ctx, \"plain\")\n\trequire.NoError(t, err)\n\ts, err := New(\n\t\tctx,\n\t\t\"\",\n\t\ttempdir,\n\t)\n\trequire.NoError(t, err)\n\n\trs, err := s.GetRecipients(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, genRecs, rs.IDs())\n\n\tassert.Equal(t, \"0xDEADBEEF\", s.OurKeyID(ctx))\n}\n\nfunc TestCheckRecipients(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"test setup not supported on Windows\")\n\t}\n\n\tu := gptest.NewGUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\n\tobuf := &bytes.Buffer{}\n\tout.Stdout = obuf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\ts, err := New(ctx, \"\", u.StoreDir(\"\"))\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.CheckRecipients(ctx))\n\n\tu.AddExpiredRecipient()\n\trequire.Error(t, s.CheckRecipients(ctx))\n}\n\nfunc TestExtraKeys(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tName       string\n\t\tRecipients map[string]bool\n\t\tKeys       []string\n\t\tExtras     []string\n\t}{\n\t\t{\n\t\t\tName:   \"empty\",\n\t\t\tExtras: []string{},\n\t\t},\n\t\t{\n\t\t\tName: \"one recipient, one key, match\",\n\t\t\tRecipients: map[string]bool{\n\t\t\t\t\"foo\": true,\n\t\t\t},\n\t\t\tKeys:   []string{\"foo\"},\n\t\t\tExtras: []string{},\n\t\t},\n\t\t{\n\t\t\tName: \"one recipient, one key, no match\",\n\t\t\tRecipients: map[string]bool{\n\t\t\t\t\"foo\": true,\n\t\t\t},\n\t\t\tKeys:   []string{\"bar\"},\n\t\t\tExtras: []string{\"bar\"},\n\t\t},\n\t\t{\n\t\t\tName: \"two recipients, one key, no match\",\n\t\t\tRecipients: map[string]bool{\n\t\t\t\t\"foo\": true,\n\t\t\t\t\"bar\": true,\n\t\t\t},\n\t\t\tKeys:   []string{\"baz\"},\n\t\t\tExtras: []string{\"baz\"},\n\t\t},\n\t\t{\n\t\t\tName: \"two recipients, two keys, one match\",\n\t\t\tRecipients: map[string]bool{\n\t\t\t\t\"foo\": true,\n\t\t\t\t\"bar\": true,\n\t\t\t},\n\t\t\tKeys:   []string{\"foo\", \"baz\"},\n\t\t\tExtras: []string{\"baz\"},\n\t\t},\n\t} {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.Extras, extraKeys(tc.Recipients, tc.Keys))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/leaf/reencrypt.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\n// nolint:ifshort\n// reencrypt will re-encrypt all entries for the current recipients.\nfunc (s *Store) reencrypt(ctx context.Context) error {\n\tentries, err := s.List(ctx, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list store: %w\", err)\n\t}\n\n\t// Most gnupg setups don't work well with concurrency > 1, but\n\t// for other backends - e.g. age - this could very well be > 1.\n\tconc := s.crypto.Concurrency()\n\n\t// save original value of auto push\n\t{\n\t\t// shadow ctx in this block only\n\t\tctx := ctxutil.WithGitCommit(ctx, false)\n\n\t\t// progress bar\n\t\tbar := termio.NewProgressBar(int64(len(entries)))\n\t\tbar.Hidden = !ctxutil.IsTerminal(ctx) || ctxutil.IsHidden(ctx)\n\n\t\tvar wg sync.WaitGroup\n\t\tjobs := make(chan string)\n\t\t// We use a logger to write without race condition on stdout\n\t\tlogger := log.New(os.Stdout, \"\", 0)\n\t\tout.Print(ctx, \"Starting reencrypt\")\n\n\t\tfor i := range conc {\n\t\t\twg.Add(1) // we start a new job\n\t\t\tgo func(workerId int) {\n\t\t\t\t// the workers are fed through an unbuffered channel\n\t\t\t\tfor e := range jobs {\n\t\t\t\t\tcontent, err := s.Get(ctx, e)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Printf(\"Worker %d: Failed to get current value for %s: %s\\n\", workerId, e, err)\n\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif err := s.Set(WithNoGitOps(ctx, conc > 1), e, content); err != nil {\n\t\t\t\t\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\t\t\t\t\tlogger.Printf(\"Worker %d: Failed to write %s: %s\\n\", workerId, e, err)\n\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlogger.Printf(\"Worker %d: Writing secret %s is not needed\\n\", workerId, e)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twg.Done() // report the job as finished\n\t\t\t}(i)\n\t\t}\n\n\t\tfor _, e := range entries {\n\t\t\t// check for context cancellation\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// We close the channel, so the worker will terminate\n\t\t\t\tclose(jobs)\n\t\t\t\t// we wait for all workers to have finished\n\t\t\t\twg.Wait()\n\n\t\t\t\treturn fmt.Errorf(\"context canceled\")\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tif bar != nil {\n\t\t\t\tbar.Inc()\n\t\t\t}\n\n\t\t\te = strings.TrimPrefix(e, s.alias)\n\t\t\tjobs <- e\n\t\t}\n\t\t// We close the channel, so the workers will terminate\n\t\tclose(jobs)\n\t\t// we wait for all workers to have finished\n\t\twg.Wait()\n\t\tbar.Done()\n\t}\n\n\t// if we are working concurrently, we cannot git add during the process\n\t// to avoid a race condition on git .index.lock file, so we do it now.\n\tif conc > 1 {\n\t\tfor _, name := range entries {\n\t\t\tp := s.Passfile(name)\n\t\t\tif err := s.storage.TryAdd(ctx, p); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", p, err)\n\t\t\t}\n\n\t\t\tdebug.Log(\"added %s to git\", p)\n\t\t}\n\t}\n\n\tif err := s.storage.TryCommit(ctx, ctxutil.GetCommitMessage(ctx)); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\treturn s.reencryptGitPush(ctx)\n}\n\nfunc (s *Store) reencryptGitPush(ctx context.Context) error {\n\tctx = config.WithMount(ctx, s.alias)\n\tif !config.Bool(ctx, \"core.autopush\") {\n\t\tdebug.Log(\"not pushing to git remote, core.autopush is false\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.TryPush(ctx, \"\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push change to git remote: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/storage.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n)\n\nfunc (s *Store) initStorageBackend(ctx context.Context) error {\n\tctx = ctxutil.WithAlias(ctx, s.alias)\n\n\tstore, err := backend.DetectStorage(ctx, s.path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unknown storage backend: %w\", err)\n\t}\n\n\ts.storage = store\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/store.go",
    "content": "// Package leaf provides the leaf store implementation for gopass.\n// It implements the gopass.Store interface and provides methods to\n// interact with the password store.\npackage leaf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// Store is a password store.\ntype Store struct {\n\talias   string\n\tpath    string\n\tcrypto  backend.Crypto\n\tstorage backend.Storage\n}\n\n// Init initializes this sub store.\nfunc Init(ctx context.Context, alias, path string) (*Store, error) {\n\tdebug.Log(\"Initializing %s at %s\", alias, path)\n\n\ts := &Store{\n\t\talias: alias,\n\t\tpath:  path,\n\t}\n\n\tst, err := backend.InitStorage(ctx, backend.GetStorageBackend(ctx), path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize storage for %s at %s: %w\", alias, path, err)\n\t}\n\n\ts.storage = st\n\tdebug.Log(\"Storage for %s => %s initialized as %s\", alias, path, st.Name())\n\n\tcrypto, err := backend.NewCrypto(ctx, backend.GetCryptoBackend(ctx))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize crypto for %s at %s: %w\", alias, path, err)\n\t}\n\n\ts.crypto = crypto\n\tdebug.Log(\"Crypto for %q => %q initialized as %s\", alias, path, crypto.Name())\n\n\treturn s, nil\n}\n\n// New creates a new store.\nfunc New(ctx context.Context, alias, path string) (*Store, error) {\n\tdebug.Log(\"Instantiating %q at %q\", alias, path)\n\n\ts := &Store{\n\t\talias: alias,\n\t\tpath:  path,\n\t}\n\n\t// init storage and rcs backend\n\tif err := s.initStorageBackend(ctx); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to init storage backend: %w\", err)\n\t}\n\n\tdebug.Log(\"Storage for %q (%q) initialized as %s\", alias, path, s.storage)\n\n\t// init crypto backend\n\tif err := s.initCryptoBackend(ctx); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to init crypto backend: %w\", err)\n\t}\n\n\tdebug.Log(\"Crypto for %q (%q) initialized as %s\", alias, path, s.crypto)\n\n\treturn s, nil\n}\n\n// idFile returns the path to the recipient list for this store\n// it walks up from the given filename until it finds a directory containing\n// a gpg id file or it leaves the scope of storage.\nfunc (s *Store) idFile(ctx context.Context, name string) string {\n\tif s.crypto == nil {\n\t\treturn \"\"\n\t}\n\n\tfn := name\n\n\tvar cnt uint8\n\n\tfor {\n\t\tcnt++\n\t\tif cnt > 100 {\n\t\t\tbreak\n\t\t}\n\n\t\tif fn == \"\" || fn == Sep {\n\t\t\tbreak\n\t\t}\n\n\t\tgfn := filepath.Join(fn, s.crypto.IDFile())\n\t\tif s.storage.Exists(ctx, gfn) {\n\t\t\treturn gfn\n\t\t}\n\n\t\tfn = filepath.Dir(fn)\n\t}\n\n\treturn s.crypto.IDFile()\n}\n\n// idFiles returns the path to all id files in this store.\nfunc (s *Store) idFiles(ctx context.Context) []string {\n\tif s == nil || s.crypto == nil {\n\t\treturn nil\n\t}\n\n\tfiles, err := s.storage.List(ctx, \"\")\n\tif err != nil {\n\t\tdebug.Log(\"failed to list files: %s\", err)\n\n\t\treturn nil\n\t}\n\n\tidfs := make([]string, 0, len(files))\n\tfor _, file := range files {\n\t\tif !strings.HasSuffix(file, s.crypto.IDFile()) {\n\t\t\tcontinue\n\t\t}\n\t\tif filepath.Base(file) != s.crypto.IDFile() {\n\t\t\tcontinue\n\t\t}\n\t\tidfs = append(idfs, file)\n\t}\n\n\tdebug.Log(\"idFiles: %q\", idfs)\n\n\treturn set.Sorted(idfs)\n}\n\n// Equals returns true if this storage has the same on-disk path as the other.\nfunc (s *Store) Equals(other *Store) bool {\n\tif other == nil {\n\t\treturn false\n\t}\n\n\treturn s.Path() == other.Path()\n}\n\n// IsDir returns true if the entry is folder inside the store.\nfunc (s *Store) IsDir(ctx context.Context, name string) bool {\n\treturn s.storage.IsDir(ctx, name)\n}\n\n// Exists checks the existence of a single entry.\nfunc (s *Store) Exists(ctx context.Context, name string) bool {\n\treturn s.storage.Exists(ctx, s.Passfile(name))\n}\n\nfunc (s *Store) useableKeys(ctx context.Context, name string) ([]string, error) {\n\trs, err := s.GetRecipients(ctx, name)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get recipients: %w\", err)\n\t}\n\n\tkl, err := s.crypto.FindRecipients(ctx, rs.IDs()...)\n\tif err != nil {\n\t\tdebug.Log(\"failed to find useableKeys: %s\", err)\n\n\t\treturn rs.IDs(), err\n\t}\n\n\t// not ideal, but since this used to be a no-op, let us warn about it when it's triggered for now\n\tif len(kl) == 0 {\n\t\tout.Warningf(ctx, \"crypto backend had no useable keys for recipients %v. Trying to default to these\", rs.IDs())\n\n\t\treturn rs.IDs(), nil\n\t}\n\n\tdebug.Log(\"useableKeys: %v\", kl)\n\n\treturn kl, nil\n}\n\n// Passfile returns the name of gpg file on disk, for the given key/name.\nfunc (s *Store) Passfile(name string) string {\n\treturn strings.TrimPrefix(name+\".\"+s.crypto.Ext(), \"/\")\n}\n\n// String implement fmt.Stringer.\nfunc (s *Store) String() string {\n\treturn fmt.Sprintf(\"Store(Alias: %s, Path: %s)\", s.alias, s.path)\n}\n\n// Path returns the value of path.\nfunc (s *Store) Path() string {\n\treturn s.path\n}\n\n// Alias returns the value of alias.\nfunc (s *Store) Alias() string {\n\treturn s.alias\n}\n\n// Storage returns the storage backend used by this store.\nfunc (s *Store) Storage() backend.Storage {\n\treturn s.storage\n}\n\n// Valid returns true if this store is not nil.\nfunc (s *Store) Valid() bool {\n\treturn s != nil\n}\n\n// Concurrency returns the number of concurrent operations allowed\n// by this stores crypto implementation (e.g. usually 1 for GPG).\nfunc (s *Store) Concurrency() int {\n\treturn s.Crypto().Concurrency()\n}\n"
  },
  {
    "path": "internal/store/leaf/store_test.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createSubStore(t *testing.T) (*Store, error) {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\tsd := filepath.Join(dir, \"sub\")\n\n\t_, _, err := createStore(sd, nil, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tt.Setenv(\"GOPASS_HOMEDIR\", dir)\n\tt.Setenv(\"CHECKPOINT_DISABLE\", \"true\")\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\tt.Setenv(\"GOPASS_DISABLE_ENCRYPTION\", \"true\")\n\tt.Setenv(\"GNUPGHOME\", filepath.Join(dir, \".gnupg\"))\n\n\tif err := os.Unsetenv(\"PAGER\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\tctx := config.NewContextInMemory()\n\tctx, err = backend.WithCryptoBackendString(ctx, \"plain\")\n\trequire.NoError(t, err)\n\tctx, err = backend.WithStorageBackendString(ctx, \"fs\")\n\trequire.NoError(t, err)\n\n\treturn New(\n\t\tctx,\n\t\t\"\",\n\t\tsd,\n\t)\n}\n\nfunc createStore(dir string, recipients, entries []string) ([]string, []string, error) {\n\tif recipients == nil {\n\t\trecipients = []string{\n\t\t\t\"0xDEADBEEF\",\n\t\t\t\"0xFEEDBEEF\",\n\t\t}\n\t}\n\n\tif entries == nil {\n\t\tentries = []string{\n\t\t\t\"foo/bar/baz\",\n\t\t\t\"baz/ing/a\",\n\t\t}\n\t}\n\n\tsort.Strings(entries)\n\n\tfor _, file := range entries {\n\t\tfilename := filepath.Join(dir, file+\".\"+plain.Ext)\n\t\tif err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {\n\t\t\treturn recipients, entries, err\n\t\t}\n\n\t\tif err := os.WriteFile(filename, []byte{}, 0o644); err != nil {\n\t\t\treturn recipients, entries, err\n\t\t}\n\t}\n\n\terr := os.WriteFile(filepath.Join(dir, plain.IDFile), []byte(strings.Join(recipients, \"\\n\")), 0o600)\n\n\treturn recipients, entries, err\n}\n\nfunc TestStore(t *testing.T) {\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\tif !s.Equals(s) {\n\t\tt.Errorf(\"Should be equal to myself\")\n\t}\n}\n\nfunc TestIdFile(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\t// test sub-id\n\tsecName := \"a\"\n\tfor range 99 {\n\t\tsecName += \"/a\"\n\t}\n\n\tsec := secrets.NewAKV()\n\n\t_ = sec.Set(\"foo\", \"bar\")\n\t_, err = sec.Write([]byte(\"bar\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, s.Set(ctx, secName, sec))\n\trequire.NoError(t, os.WriteFile(filepath.Join(s.path, \"a\", plain.IDFile), []byte(\"foobar\"), 0o600))\n\tassert.Equal(t, filepath.Join(\"a\", plain.IDFile), s.idFile(ctx, secName))\n\tassert.True(t, s.Exists(ctx, secName))\n\n\t// test abort condition\n\tsecName = \"a\"\n\tfor range 100 {\n\t\tsecName += \"/a\"\n\t}\n\trequire.NoError(t, s.Set(ctx, secName, sec))\n\trequire.NoError(t, os.WriteFile(filepath.Join(s.path, \"a\", \".gpg-id\"), []byte(\"foobar\"), 0o600))\n\tassert.Equal(t, plain.IDFile, s.idFile(ctx, secName))\n}\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tdsc   string\n\t\tnoDir bool\n\t\tctx   context.Context //nolint:containedctx\n\t\tok    bool\n\t}{\n\t\t{\n\t\t\tdsc:   \"Invalid Storage\",\n\t\t\tctx:   backend.WithStorageBackend(config.NewContextInMemory(), -1),\n\t\t\tnoDir: true,\n\t\t\tok:    false,\n\t\t},\n\t\t{\n\t\t\tdsc: \"GitFS Storage\",\n\t\t\tctx: backend.WithCryptoBackend(backend.WithStorageBackend(config.NewContextInMemory(), backend.GitFS), backend.Plain),\n\t\t\tok:  true,\n\t\t},\n\t\t{\n\t\t\tdsc: \"FS Storage\",\n\t\t\tctx: backend.WithCryptoBackend(backend.WithStorageBackend(config.NewContextInMemory(), backend.FS), backend.Plain),\n\t\t\tok:  true,\n\t\t},\n\t\t{\n\t\t\tdsc: \"GPG Crypto\",\n\t\t\tctx: backend.WithCryptoBackend(config.NewContextInMemory(), backend.GPGCLI),\n\t\t\tok:  true,\n\t\t},\n\t\t{\n\t\t\tdsc: \"Plain Crypto\",\n\t\t\tctx: backend.WithCryptoBackend(config.NewContextInMemory(), backend.Plain),\n\t\t\tok:  true,\n\t\t},\n\t\t{\n\t\t\tdsc: \"Invalid Crypto\",\n\t\t\tctx: backend.WithCryptoBackend(config.NewContextInMemory(), -1),\n\t\t\t// ok:  false, // TODO once backend.DetectCrypto returns an error this should be false\n\t\t\tok: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.dsc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar tempdir string\n\t\t\tif !tc.noDir {\n\t\t\t\ttempdir = t.TempDir()\n\t\t\t}\n\n\t\t\ts, err := New(tc.ctx, \"\", tempdir)\n\t\t\tif !tc.ok {\n\t\t\t\trequire.Error(t, err, tc.dsc)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, tc.dsc)\n\t\t\tassert.NotNil(t, s, tc.dsc)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/leaf/templates.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\t// TemplateFile is the name of a pass template.\n\tTemplateFile = \".pass-template\"\n)\n\n// LookupTemplate will lookup and return a template.\nfunc (s *Store) LookupTemplate(ctx context.Context, name string) (string, []byte, bool) {\n\toName := name\n\t// go upwards in the directory tree until we find a template\n\t// by chopping off one path element by one.\n\tfor {\n\t\tl1 := len(name)\n\t\tname = filepath.Dir(name)\n\n\t\tif len(name) == l1 {\n\t\t\tbreak\n\t\t}\n\n\t\ttpl := filepath.Join(name, TemplateFile)\n\n\t\tif s.storage.Exists(ctx, tpl) {\n\t\t\tif content, err := s.storage.Get(ctx, tpl); err == nil {\n\t\t\t\tdebug.Log(\"Found template %q for %q\", tpl, oName)\n\n\t\t\t\treturn tpl, content, true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", []byte{}, false\n}\n\n// ListTemplates will list all templates in this store.\nfunc (s *Store) ListTemplates(ctx context.Context, prefix string) []string {\n\tlst, err := s.storage.List(ctx, \"\")\n\tif err != nil {\n\t\tdebug.Log(\"failed to list templates: %s\", err)\n\n\t\treturn nil\n\t}\n\n\ttpls := make(map[string]struct{}, len(lst))\n\n\tfor _, path := range lst {\n\t\tif !strings.HasSuffix(path, TemplateFile) {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath = strings.TrimSuffix(path, Sep+TemplateFile)\n\n\t\tif prefix != \"\" {\n\t\t\tpath = prefix + Sep + path\n\t\t}\n\n\t\ttpls[path] = struct{}{}\n\t}\n\n\tout := make([]string, 0, len(tpls))\n\n\tfor k := range tpls {\n\t\tout = append(out, k)\n\t}\n\n\tsort.Strings(out)\n\n\treturn out\n}\n\n// TemplateTree returns a tree of all templates.\nfunc (s *Store) TemplateTree(ctx context.Context) *tree.Root {\n\troot := tree.New(\"gopass\")\n\n\tfor _, t := range s.ListTemplates(ctx, \"\") {\n\t\tif err := root.AddFile(t, \"gopass/template\"); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to add template: %s\", err)\n\t\t}\n\t}\n\n\treturn root\n}\n\n// templatefile returns the name of the given template on disk.\nfunc (s *Store) templatefile(name string) string {\n\treturn strings.TrimPrefix(filepath.Join(name, TemplateFile), string(filepath.Separator))\n}\n\n// HasTemplate returns true if the template exists.\nfunc (s *Store) HasTemplate(ctx context.Context, name string) bool {\n\treturn s.storage.Exists(ctx, s.templatefile(name))\n}\n\n// GetTemplate will return the content of the named template.\nfunc (s *Store) GetTemplate(ctx context.Context, name string) ([]byte, error) {\n\treturn s.storage.Get(ctx, s.templatefile(name))\n}\n\n// SetTemplate will (over)write the content to the template file.\nfunc (s *Store) SetTemplate(ctx context.Context, name string, content []byte) error {\n\tp := s.templatefile(name)\n\n\tif err := s.storage.Set(ctx, p, content); err != nil {\n\t\tif errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to write template: %w\", err)\n\t}\n\n\tif err := s.storage.TryAdd(ctx, p); err != nil {\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", p, err)\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\treturn s.gitCommitAndPush(ctx, name)\n}\n\n// RemoveTemplate will delete the named template if it exists.\nfunc (s *Store) RemoveTemplate(ctx context.Context, name string) error {\n\tp := s.templatefile(name)\n\n\tif err := s.storage.Delete(ctx, p); err != nil {\n\t\treturn fmt.Errorf(\"failed to remote template: %w\", err)\n\t}\n\n\tif err := s.storage.Add(ctx, p); err != nil {\n\t\tif errors.Is(err, store.ErrGitNotInit) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", p, err)\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\treturn s.gitCommitAndPush(ctx, name)\n}\n"
  },
  {
    "path": "internal/store/leaf/templates_test.go",
    "content": "package leaf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTemplates(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\ttempdir := t.TempDir()\n\n\tcolor.NoColor = true\n\n\t_, _, err := createStore(tempdir, nil, nil)\n\trequire.NoError(t, err)\n\n\tctx, err = backend.WithCryptoBackendString(ctx, \"plain\")\n\trequire.NoError(t, err)\n\tctx, err = backend.WithStorageBackendString(ctx, \"fs\")\n\trequire.NoError(t, err)\n\ts, err := New(\n\t\tctx,\n\t\t\"\",\n\t\ttempdir,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, s.ListTemplates(ctx, \"\"))\n\trequire.NoError(t, s.SetTemplate(ctx, \"foo\", []byte(\"foobar\")))\n\tassert.Len(t, s.ListTemplates(ctx, \"\"), 1)\n\n\ttt := s.TemplateTree(ctx)\n\tassert.Equal(t, \"gopass\\n└── foo\\n\", tt.Format(0))\n\n\tassert.True(t, s.HasTemplate(ctx, \"foo\"))\n\n\tb, err := s.GetTemplate(ctx, \"foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", string(b))\n\n\t_, b, found := s.LookupTemplate(ctx, \"foo/bar\")\n\tassert.True(t, found)\n\tassert.Equal(t, \"foobar\", string(b))\n\n\trequire.NoError(t, s.RemoveTemplate(ctx, \"foo\"))\n\tassert.Empty(t, s.ListTemplates(ctx, \"\"))\n\n\trequire.Error(t, s.RemoveTemplate(ctx, \"foo\"))\n}\n"
  },
  {
    "path": "internal/store/leaf/write.go",
    "content": "package leaf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\n// Set encodes and writes the ciphertext of one entry to disk.\nfunc (s *Store) Set(ctx context.Context, name string, sec gopass.Byter) error {\n\tif strings.Contains(name, \"//\") {\n\t\treturn fmt.Errorf(\"invalid secret name: %s\", name)\n\t}\n\n\tif cfg, _ := config.FromContext(ctx); cfg.GetM(s.alias, \"core.readonly\") == \"true\" {\n\t\treturn fmt.Errorf(\"writing to %s is disabled by `core.readonly`\", s.alias)\n\t}\n\n\tp := s.Passfile(name)\n\n\trecipients, err := s.useableKeys(ctx, name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list useable keys for %q: %w\", p, err)\n\t}\n\n\t// make sure the encryptor can decrypt later\n\trecipients = s.ensureOurKeyID(ctx, recipients)\n\n\t// we can not encrypt without recipients\n\tif len(recipients) < 1 {\n\t\treturn fmt.Errorf(\"no useable recipients for %q. can not encrypt without recipients\", name)\n\t}\n\n\tciphertext, err := s.crypto.Encrypt(ctx, sec.Bytes(), recipients)\n\tif err != nil {\n\t\tdebug.Log(\"Failed encrypt secret: %s\", err)\n\n\t\treturn store.ErrEncrypt\n\t}\n\n\tif err := s.storage.Set(ctx, p, ciphertext); err != nil {\n\t\treturn fmt.Errorf(\"failed to write secret: %w\", err)\n\t}\n\n\t// It is not possible to perform concurrent git add and git commit commands\n\t// so we need to skip this step when using concurrency and perform them\n\t// at the end of the batch processing.\n\tif IsNoGitOps(ctx) {\n\t\tdebug.Log(\"sub.Set(%s) - skipping git ops (disabled)\")\n\n\t\treturn nil\n\t}\n\n\tif err := s.storage.TryAdd(ctx, p); err != nil {\n\t\treturn fmt.Errorf(\"failed to add %q to git: %w\", p, err)\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\t// try to enqueue this task, if the queue is not available\n\t// it will return the task and we will execute it inline\n\tt := queue.GetQueue(ctx).Add(func(_ context.Context) (context.Context, error) {\n\t\treturn nil, s.gitCommitAndPush(ctx, name)\n\t})\n\n\tctx, err = t(ctx)\n\n\treturn err\n}\n\nfunc (s *Store) gitCommitAndPush(ctx context.Context, name string) error {\n\tcommitMessage := ctxutil.GetCommitMessage(ctx)\n\tmessage := fmt.Sprintf(\"Save secret %s: %s\", name, commitMessage)\n\tif commitMessage == \"\" {\n\t\tmessage = fmt.Sprintf(\"Save secret: %s\", name)\n\t}\n\tif err := s.storage.TryCommit(ctx, message); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git: %w\", err)\n\t}\n\n\tctx = config.WithMount(ctx, s.alias)\n\tif !config.Bool(ctx, \"core.autopush\") {\n\t\tdebug.Log(\"not pushing to git remote, core.autopush is false\")\n\n\t\treturn nil\n\t}\n\n\tdebug.Log(\"pushing to remote ...\")\n\n\tif err := s.storage.TryPush(ctx, \"\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push to git remote: %w\", err)\n\t}\n\n\tdebug.Log(\"pushed to remote\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/leaf/write_test.go",
    "content": "package leaf\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSet(t *testing.T) {\n\tctx := gpg.WithAlwaysTrust(config.NewContextInMemory(), true)\n\n\ts, err := createSubStore(t)\n\trequire.NoError(t, err)\n\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"foo\")\n\t_, err = sec.Write([]byte(\"bar\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, s.Set(ctx, \"zab/zab\", sec))\n\n\tif runtime.GOOS != \"windows\" {\n\t\trequire.Error(t, s.Set(ctx, \"../../../../../etc/passwd\", sec))\n\t} else {\n\t\trequire.NoError(t, s.Set(ctx, \"../../../../../etc/passwd\", sec))\n\t}\n\n\trequire.NoError(t, s.Set(ctx, \"zab\", sec))\n}\n"
  },
  {
    "path": "internal/store/mockstore/inmem/store.go",
    "content": "// Package inmem implements an in memory storage backend for tests.\npackage inmem\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// InMem is a thread-safe in-memory store.\ntype InMem struct {\n\tsync.Mutex\n\tdata map[string][]byte\n}\n\n// New creates a new mock.\nfunc New() *InMem {\n\treturn &InMem{\n\t\tdata: make(map[string][]byte, 128),\n\t}\n}\n\n// Get retrieves a value.\nfunc (m *InMem) Get(ctx context.Context, name string) ([]byte, error) {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tif m.data == nil {\n\t\treturn nil, fmt.Errorf(\"entry not found\")\n\t}\n\n\tsec, found := m.data[name]\n\tif !found {\n\t\t// not found\n\t\treturn nil, fmt.Errorf(\"entry not found\")\n\t}\n\n\t// found\n\treturn sec, nil\n}\n\n// Set writes a value.\nfunc (m *InMem) Set(ctx context.Context, name string, value []byte) error {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tm.data[name] = value\n\n\treturn nil\n}\n\n// Delete removes a value.\nfunc (m *InMem) Delete(ctx context.Context, name string) error {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tdelete(m.data, name)\n\n\treturn nil\n}\n\n// Exists checks is a value exists.\nfunc (m *InMem) Exists(ctx context.Context, name string) bool {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\t_, found := m.data[name]\n\n\treturn found\n}\n\n// List shows all values.\nfunc (m *InMem) List(ctx context.Context, prefix string) ([]string, error) {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\treturn set.SortedKeys(m.data), nil\n}\n\n// IsDir returns true if the entry is a directory.\nfunc (m *InMem) IsDir(ctx context.Context, name string) bool {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tfor k := range m.data {\n\t\tif strings.HasPrefix(k, name+\"/\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Prune removes a directory.\nfunc (m *InMem) Prune(ctx context.Context, prefix string) error {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tdeleted := 0\n\n\tfor k := range m.data {\n\t\tif strings.HasPrefix(k, prefix+\"/\") {\n\t\t\tdelete(m.data, k)\n\t\t\tdeleted++\n\t\t}\n\t}\n\n\tif deleted < 1 {\n\t\treturn fmt.Errorf(\"not found\")\n\t}\n\n\treturn nil\n}\n\n// Name returns the name of this backend.\nfunc (m *InMem) Name() string {\n\treturn \"inmem\"\n}\n\n// Version returns the version of this backend.\nfunc (m *InMem) Version(context.Context) semver.Version {\n\treturn semver.Version{Major: 1}\n}\n\n// String implement fmt.Stringer.\nfunc (m *InMem) String() string {\n\treturn \"inmem()\"\n}\n\n// Path returns inmem.\nfunc (m *InMem) Path() string {\n\treturn \"inmem\"\n}\n\n// Fsck always returns nil.\nfunc (m *InMem) Fsck(ctx context.Context) error {\n\treturn nil\n}\n\n// Add does nothing.\nfunc (m *InMem) Add(ctx context.Context, args ...string) error {\n\treturn nil\n}\n\n// TryAdd does nothing.\nfunc (m *InMem) TryAdd(ctx context.Context, args ...string) error {\n\treturn nil\n}\n\n// Commit does nothing.\nfunc (m *InMem) Commit(ctx context.Context, msg string) error {\n\treturn nil\n}\n\n// TryCommit does nothing.\nfunc (m *InMem) TryCommit(ctx context.Context, msg string) error {\n\treturn nil\n}\n\n// Push does nothing.\nfunc (m *InMem) Push(ctx context.Context, origin, branch string) error {\n\treturn nil\n}\n\n// TryPush does nothing.\nfunc (m *InMem) TryPush(ctx context.Context, origin, branch string) error {\n\treturn nil\n}\n\n// Pull does nothing.\nfunc (m *InMem) Pull(ctx context.Context, origin, branch string) error {\n\treturn nil\n}\n\n// Cmd does nothing.\nfunc (m *InMem) Cmd(ctx context.Context, name string, args ...string) error {\n\treturn nil\n}\n\n// Init does nothing.\nfunc (m *InMem) Init(context.Context, string, string) error {\n\treturn nil\n}\n\n// InitConfig does nothing.\nfunc (m *InMem) InitConfig(context.Context, string, string) error {\n\treturn nil\n}\n\n// AddRemote does nothing.\nfunc (m *InMem) AddRemote(ctx context.Context, remote, url string) error {\n\treturn nil\n}\n\n// RemoveRemote does nothing.\nfunc (m *InMem) RemoveRemote(ctx context.Context, remote string) error {\n\treturn nil\n}\n\n// Revisions is not implemented.\nfunc (m *InMem) Revisions(context.Context, string) ([]backend.Revision, error) {\n\treturn []backend.Revision{\n\t\t{\n\t\t\tHash: \"latest\",\n\t\t\tDate: time.Now(),\n\t\t},\n\t}, nil\n}\n\n// GetRevision is not implemented.\nfunc (m *InMem) GetRevision(context.Context, string, string) ([]byte, error) {\n\treturn []byte(\"foo\\nbar\"), nil\n}\n\n// Status is not implemented.\nfunc (m *InMem) Status(context.Context) ([]byte, error) {\n\treturn []byte(\"\"), nil\n}\n\n// Compact is not implemented.\nfunc (m *InMem) Compact(context.Context) error {\n\treturn nil\n}\n\n// Link is not implemented.\nfunc (m *InMem) Link(context.Context, string, string) error {\n\treturn nil\n}\n\n// Move is not implemented.\nfunc (m *InMem) Move(context.Context, string, string, bool) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/mockstore/store.go",
    "content": "// Package mockstore provides a mock store for testing purposes.\n// It implements the gopass.Store interface and uses an in-memory storage backend.\n\npackage mockstore\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/plain\"\n\t\"github.com/gopasspw/gopass/internal/store/mockstore/inmem\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets/secparse\"\n)\n\n// MockStore is an mocked store.\ntype MockStore struct {\n\talias   string\n\tstorage backend.Storage\n}\n\n// New creates a new mock store.\nfunc New(alias string) *MockStore {\n\treturn &MockStore{\n\t\talias:   alias,\n\t\tstorage: inmem.New(),\n\t}\n}\n\n// String implements fmt.Stringer.\nfunc (m *MockStore) String() string {\n\treturn \"mockstore\"\n}\n\n// GetTemplate returns nothing.\nfunc (m *MockStore) GetTemplate(context.Context, string) ([]byte, error) {\n\treturn []byte{}, nil\n}\n\n// HasTemplate returns false.\nfunc (m *MockStore) HasTemplate(context.Context, string) bool {\n\treturn false\n}\n\n// ListTemplates returns nothing.\nfunc (m *MockStore) ListTemplates(context.Context, string) []string {\n\treturn nil\n}\n\n// LookupTemplate returns nothing.\nfunc (m *MockStore) LookupTemplate(context.Context, string) ([]byte, bool) {\n\treturn []byte{}, false\n}\n\n// RemoveTemplate does nothing.\nfunc (m *MockStore) RemoveTemplate(context.Context, string) error {\n\treturn nil\n}\n\n// SetTemplate does nothing.\nfunc (m *MockStore) SetTemplate(context.Context, string, []byte) error {\n\treturn nil\n}\n\n// TemplateTree does nothing.\nfunc (m *MockStore) TemplateTree(context.Context) (*tree.Root, error) {\n\treturn nil, fmt.Errorf(\"not supported\")\n}\n\n// AddRecipient does nothing.\nfunc (m *MockStore) AddRecipient(context.Context, string) error {\n\treturn nil\n}\n\n// GetRecipients does nothing.\nfunc (m *MockStore) GetRecipients(context.Context, string) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not supported\")\n}\n\n// RemoveRecipient does nothing.\nfunc (m *MockStore) RemoveRecipient(context.Context, string) error {\n\treturn nil\n}\n\n// SaveRecipients does nothing.\nfunc (m *MockStore) SaveRecipients(context.Context) error {\n\treturn nil\n}\n\n// Recipients does nothing.\nfunc (m *MockStore) Recipients(context.Context) []string {\n\treturn nil\n}\n\n// ImportMissingPublicKeys does nothing.\nfunc (m *MockStore) ImportMissingPublicKeys(context.Context) error {\n\treturn nil\n}\n\n// ExportMissingPublicKeys does nothing.\nfunc (m *MockStore) ExportMissingPublicKeys(context.Context, []string) (bool, error) {\n\treturn false, nil\n}\n\n// Fsck does nothing.\nfunc (m *MockStore) Fsck(context.Context, string) error {\n\treturn nil\n}\n\n// Path does nothing.\nfunc (m *MockStore) Path() string {\n\treturn \"\"\n}\n\n// URL does nothing.\nfunc (m *MockStore) URL() string {\n\treturn \"mockstore://\"\n}\n\n// Crypto does nothing.\nfunc (m *MockStore) Crypto() backend.Crypto {\n\treturn plain.New()\n}\n\n// Storage does nothing.\nfunc (m *MockStore) Storage() backend.Storage {\n\treturn m.storage\n}\n\n// GitInit does nothing.\nfunc (m *MockStore) GitInit(context.Context, string, string) error {\n\treturn nil\n}\n\n// Alias does nothing.\nfunc (m *MockStore) Alias() string {\n\treturn m.alias\n}\n\n// Copy does nothing.\nfunc (m *MockStore) Copy(ctx context.Context, from string, to string) error {\n\tcontent, err := m.storage.Get(ctx, from)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn m.storage.Set(ctx, to, content)\n}\n\n// Delete does nothing.\nfunc (m *MockStore) Delete(ctx context.Context, name string) error {\n\treturn m.storage.Delete(ctx, name)\n}\n\n// Equals does nothing.\nfunc (m *MockStore) Equals(other *MockStore) bool {\n\treturn false\n}\n\n// Exists does nothing.\nfunc (m *MockStore) Exists(ctx context.Context, name string) bool {\n\treturn m.storage.Exists(ctx, name)\n}\n\n// Get does nothing.\nfunc (m *MockStore) Get(ctx context.Context, name string) (gopass.Secret, error) {\n\tcontent, err := m.storage.Get(ctx, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn secparse.Parse(content)\n}\n\n// GetRevision does nothing.\nfunc (m *MockStore) GetRevision(context.Context, string, string) (gopass.Secret, error) {\n\treturn nil, fmt.Errorf(\"not supported\")\n}\n\n// Init does nothing.\nfunc (m *MockStore) Init(context.Context, string, ...string) error {\n\treturn nil\n}\n\n// Initialized does nothing.\nfunc (m *MockStore) Initialized(context.Context) bool {\n\treturn true\n}\n\n// IsDir does nothing.\nfunc (m *MockStore) IsDir(ctx context.Context, name string) bool {\n\treturn m.storage.IsDir(ctx, name)\n}\n\n// List does nothing.\nfunc (m *MockStore) List(ctx context.Context, name string) ([]string, error) {\n\treturn m.storage.List(ctx, name)\n}\n\n// ListRevisions does nothing.\nfunc (m *MockStore) ListRevisions(context.Context, string) ([]backend.Revision, error) {\n\treturn nil, nil\n}\n\n// Move does nothing.\nfunc (m *MockStore) Move(ctx context.Context, from string, to string) error {\n\tcontent, _ := m.storage.Get(ctx, from)\n\t_ = m.storage.Set(ctx, to, content)\n\n\treturn m.storage.Delete(ctx, from)\n}\n\n// Set does nothing.\nfunc (m *MockStore) Set(ctx context.Context, name string, sec gopass.Byter) error {\n\treturn m.storage.Set(ctx, name, sec.Bytes())\n}\n\n// Prune does nothing.\nfunc (m *MockStore) Prune(context.Context, string) error {\n\treturn fmt.Errorf(\"not supported\")\n}\n\n// Valid does nothing.\nfunc (m *MockStore) Valid() bool {\n\treturn true\n}\n\n// MountPoints does nothing.\nfunc (m *MockStore) MountPoints() []string {\n\treturn nil\n}\n\n// Link does nothing.\nfunc (m *MockStore) Link(context.Context, string, string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/mockstore/store_test.go",
    "content": "package mockstore\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMockStore(t *testing.T) {\n\tctx := t.Context()\n\tstore := New(\"test\")\n\n\tt.Run(\"String\", func(t *testing.T) {\n\t\tassert.Equal(t, \"mockstore\", store.String())\n\t})\n\n\tt.Run(\"GetTemplate\", func(t *testing.T) {\n\t\tdata, err := store.GetTemplate(ctx, \"test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, data)\n\t})\n\n\tt.Run(\"HasTemplate\", func(t *testing.T) {\n\t\tassert.False(t, store.HasTemplate(ctx, \"test\"))\n\t})\n\n\tt.Run(\"ListTemplates\", func(t *testing.T) {\n\t\tassert.Nil(t, store.ListTemplates(ctx, \"test\"))\n\t})\n\n\tt.Run(\"LookupTemplate\", func(t *testing.T) {\n\t\tdata, found := store.LookupTemplate(ctx, \"test\")\n\t\tassert.False(t, found)\n\t\tassert.Empty(t, data)\n\t})\n\n\tt.Run(\"RemoveTemplate\", func(t *testing.T) {\n\t\trequire.NoError(t, store.RemoveTemplate(ctx, \"test\"))\n\t})\n\n\tt.Run(\"SetTemplate\", func(t *testing.T) {\n\t\trequire.NoError(t, store.SetTemplate(ctx, \"test\", []byte(\"data\")))\n\t})\n\n\tt.Run(\"TemplateTree\", func(t *testing.T) {\n\t\ttree, err := store.TemplateTree(ctx)\n\t\trequire.Error(t, err)\n\t\tassert.Nil(t, tree)\n\t})\n\n\tt.Run(\"AddRecipient\", func(t *testing.T) {\n\t\trequire.NoError(t, store.AddRecipient(ctx, \"test\"))\n\t})\n\n\tt.Run(\"GetRecipients\", func(t *testing.T) {\n\t\trecipients, err := store.GetRecipients(ctx, \"test\")\n\t\trequire.Error(t, err)\n\t\tassert.Nil(t, recipients)\n\t})\n\n\tt.Run(\"RemoveRecipient\", func(t *testing.T) {\n\t\trequire.NoError(t, store.RemoveRecipient(ctx, \"test\"))\n\t})\n\n\tt.Run(\"SaveRecipients\", func(t *testing.T) {\n\t\trequire.NoError(t, store.SaveRecipients(ctx))\n\t})\n\n\tt.Run(\"Recipients\", func(t *testing.T) {\n\t\tassert.Nil(t, store.Recipients(ctx))\n\t})\n\n\tt.Run(\"ImportMissingPublicKeys\", func(t *testing.T) {\n\t\trequire.NoError(t, store.ImportMissingPublicKeys(ctx))\n\t})\n\n\tt.Run(\"ExportMissingPublicKeys\", func(t *testing.T) {\n\t\tok, err := store.ExportMissingPublicKeys(ctx, []string{\"test\"})\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, ok)\n\t})\n\n\tt.Run(\"Fsck\", func(t *testing.T) {\n\t\trequire.NoError(t, store.Fsck(ctx, \"test\"))\n\t})\n\n\tt.Run(\"Path\", func(t *testing.T) {\n\t\tassert.Empty(t, store.Path())\n\t})\n\n\tt.Run(\"URL\", func(t *testing.T) {\n\t\tassert.Equal(t, \"mockstore://\", store.URL())\n\t})\n\n\tt.Run(\"Crypto\", func(t *testing.T) {\n\t\tassert.NotNil(t, store.Crypto())\n\t})\n\n\tt.Run(\"Storage\", func(t *testing.T) {\n\t\tassert.NotNil(t, store.Storage())\n\t})\n\n\tt.Run(\"GitInit\", func(t *testing.T) {\n\t\trequire.NoError(t, store.GitInit(ctx, \"test\", \"test\"))\n\t})\n\n\tt.Run(\"Alias\", func(t *testing.T) {\n\t\tassert.Equal(t, \"test\", store.Alias())\n\t})\n\n\tt.Run(\"Copy\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\terr := store.Set(ctx, \"from\", sec)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, store.Copy(ctx, \"from\", \"to\"))\n\t\tsec, err = store.Get(ctx, \"to\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"password\", sec.Password())\n\t})\n\n\tt.Run(\"Delete\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test\", sec))\n\t\trequire.NoError(t, store.Delete(ctx, \"test\"))\n\t\t_, err := store.Get(ctx, \"test\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"Equals\", func(t *testing.T) {\n\t\tother := New(\"other\")\n\t\tassert.False(t, store.Equals(other))\n\t})\n\n\tt.Run(\"Exists\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test\", sec))\n\t\tassert.True(t, store.Exists(ctx, \"test\"))\n\t})\n\n\tt.Run(\"Get\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test\", sec))\n\t\tsec, err := store.Get(ctx, \"test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"password\", sec.Password())\n\t})\n\n\tt.Run(\"GetRevision\", func(t *testing.T) {\n\t\t_, err := store.GetRevision(ctx, \"test\", \"revision\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"Init\", func(t *testing.T) {\n\t\trequire.NoError(t, store.Init(ctx, \"test\"))\n\t})\n\n\tt.Run(\"Initialized\", func(t *testing.T) {\n\t\tassert.True(t, store.Initialized(ctx))\n\t})\n\n\tt.Run(\"IsDir\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test/dir\", sec))\n\t\tassert.True(t, store.IsDir(ctx, \"test\"))\n\t})\n\n\tt.Run(\"List\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test\", sec))\n\t\tlist, err := store.List(ctx, \"test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, list, \"test\")\n\t})\n\n\tt.Run(\"ListRevisions\", func(t *testing.T) {\n\t\trevisions, err := store.ListRevisions(ctx, \"test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Nil(t, revisions)\n\t})\n\n\tt.Run(\"Move\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"from\", sec))\n\t\trequire.NoError(t, store.Move(ctx, \"from\", \"to\"))\n\t\t_, err := store.Get(ctx, \"from\")\n\t\trequire.Error(t, err)\n\t\tsec, err = store.Get(ctx, \"to\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"password\", sec.Password())\n\t})\n\n\tt.Run(\"Set\", func(t *testing.T) {\n\t\tsec := secrets.New()\n\t\tsec.SetPassword(\"password\")\n\t\trequire.NoError(t, store.Set(ctx, \"test\", sec))\n\t\tsec, err := store.Get(ctx, \"test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"password\", sec.Password())\n\t})\n\n\tt.Run(\"Prune\", func(t *testing.T) {\n\t\trequire.Error(t, store.Prune(ctx, \"test\"))\n\t})\n\n\tt.Run(\"Valid\", func(t *testing.T) {\n\t\tassert.True(t, store.Valid())\n\t})\n\n\tt.Run(\"MountPoints\", func(t *testing.T) {\n\t\tassert.Nil(t, store.MountPoints())\n\t})\n\n\tt.Run(\"Link\", func(t *testing.T) {\n\t\trequire.NoError(t, store.Link(ctx, \"from\", \"to\"))\n\t})\n}\n"
  },
  {
    "path": "internal/store/root/convert.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Convert will try to convert a given mount to a different set of\n// backends.\nfunc (r *Store) Convert(ctx context.Context, name string, cryptoBe backend.CryptoBackend, storageBe backend.StorageBackend, move bool) error {\n\tsub, err := r.GetSubStore(name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mount %q not found: %w\", name, err)\n\t}\n\n\tdebug.Log(\"converting %s to crypto: %s, storage: %s\", name, cryptoBe, storageBe)\n\n\tif err := sub.Convert(ctx, cryptoBe, storageBe, move); err != nil {\n\t\treturn fmt.Errorf(\"conversion failed: %w\", err)\n\t}\n\n\tif name == \"\" {\n\t\tdebug.Log(\"success. updating root path to %s\", sub.Path())\n\n\t\treturn r.cfg.Set(\"\", \"mounts.path\", sub.Path())\n\t}\n\n\tdebug.Log(\"success. updating path for %s to %s\", name, sub.Path())\n\n\treturn r.cfg.Set(\"\", \"mounts.\"+name+\".path\", sub.Path())\n}\n"
  },
  {
    "path": "internal/store/root/crypto.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Crypto returns the crypto backend.\nfunc (r *Store) Crypto(ctx context.Context, name string) backend.Crypto {\n\tsub, _ := r.getStore(name)\n\tif !sub.Valid() {\n\t\tdebug.Log(\"Sub-Store not found for %s. Returning nil crypto backend\", name)\n\n\t\treturn nil\n\t}\n\n\treturn sub.Crypto()\n}\n"
  },
  {
    "path": "internal/store/root/crypto_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCrypto(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tcolor.NoColor = true\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, rs.Crypto(ctx, \"\"))\n}\n"
  },
  {
    "path": "internal/store/root/errors.go",
    "content": "package root\n\nimport \"fmt\"\n\n// AlreadyMountedError is an error that is returned when\n// a store is already mounted on a given mount point.\ntype AlreadyMountedError string\n\nfunc (a AlreadyMountedError) Error() string {\n\t// important: must pass a as string(a)!\n\treturn fmt.Sprintf(\"%s is already mounted\", string(a))\n}\n\n// NotInitializedError is an error that is returned when\n// a not initialized store should be mounted.\ntype NotInitializedError struct {\n\talias string\n\tpath  string\n}\n\n// Alias returns the store alias this error was generated for.\nfunc (n NotInitializedError) Alias() string { return n.alias }\n\n// Path returns the store path this error was generated for.\nfunc (n NotInitializedError) Path() string { return n.path }\n\nfunc (n NotInitializedError) Error() string {\n\treturn fmt.Sprintf(\"password store %s is not initialized. Try gopass init --store %s --path %s\", n.alias, n.alias, n.path)\n}\n"
  },
  {
    "path": "internal/store/root/fsck.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Fsck checks all stores/entries matching the given prefix.\nfunc (r *Store) Fsck(ctx context.Context, store, path string) error {\n\tvar result []error\n\n\tfor alias, sub := range r.mounts {\n\t\tif sub == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif store != \"\" && alias != store {\n\t\t\tcontinue\n\t\t}\n\n\t\tif path != \"\" && !strings.HasPrefix(path, alias+\"/\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath = strings.TrimPrefix(path, alias+\"/\")\n\n\t\t// check sub store\n\t\tdebug.Log(\"Checking mount point %s\", alias)\n\n\t\tif err := sub.Fsck(ctx, path); err != nil {\n\t\t\tout.Errorf(ctx, \"fsck failed on sub store %s: %s\", alias, err)\n\t\t\tresult = append(result, err)\n\t\t}\n\n\t\tdebug.Log(\"Checked mount point %s\", alias)\n\t}\n\n\t// check root store\n\tdebug.Log(\"Checking root store\")\n\tif err := r.store.Fsck(ctx, path); err != nil {\n\t\tout.Errorf(ctx, \"fsck failed on root store: %s\", err)\n\t\tresult = append(result, err)\n\t}\n\n\tdebug.Log(\"Checked root store\")\n\n\treturn errors.Join(result...)\n}\n"
  },
  {
    "path": "internal/store/root/fsck_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFsck(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, rs)\n\n\trequire.NoError(t, rs.Fsck(ctx, \"\", \"\"))\n}\n"
  },
  {
    "path": "internal/store/root/init.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// IsInitialized checks on disk if .gpg-id was generated and thus returns true.\nfunc (r *Store) IsInitialized(ctx context.Context) (bool, error) {\n\tif r.store == nil {\n\t\tdebug.Log(\"initializing store and possible sub-stores\")\n\n\t\tif err := r.initialize(ctx); err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to initialize stores: %w\", err)\n\t\t}\n\t}\n\n\tdebug.Log(\"root store is initialized\")\n\n\treturn r.store.IsInitialized(ctx), nil\n}\n\n// Init tries to initialize a new password store location matching the object.\nfunc (r *Store) Init(ctx context.Context, alias, path string, ids ...string) error {\n\talias = CleanMountAlias(alias)\n\tdebug.Log(\"Instantiating new sub store %s at %s for %+v\", alias, path, ids)\n\n\tif !backend.HasCryptoBackend(ctx) {\n\t\tctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)\n\t}\n\n\tif !backend.HasStorageBackend(ctx) {\n\t\tctx = backend.WithStorageBackend(ctx, backend.GitFS)\n\t}\n\n\tsub, err := leaf.New(ctx, alias, path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to instantiate new sub store: %w\", err)\n\t}\n\n\tif !r.store.IsInitialized(ctx) && alias == \"\" {\n\t\tr.store = sub\n\t}\n\n\tdebug.Log(\"Initializing sub store at %s for %+v\", path, ids)\n\n\tif err := sub.Init(ctx, path, ids...); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize new sub store: %w\", err)\n\t}\n\n\tif alias != \"\" {\n\t\tdebug.Log(\"mounted %s at %s\", alias, path)\n\n\t\treturn r.cfg.SetMountPath(alias, path)\n\t}\n\n\tdebug.Log(\"initialized root at %s\", path)\n\n\treturn r.cfg.SetPath(path)\n}\n\nfunc (r *Store) initialize(ctx context.Context) error {\n\t// already initialized?\n\tif r.store != nil {\n\t\treturn nil\n\t}\n\n\t// create the base store\n\tpath := fsutil.CleanPath(r.cfg.Get(\"mounts.path\"))\n\tif sv := os.Getenv(\"PASSWORD_STORE_DIR\"); sv != \"\" {\n\t\tpath = fsutil.CleanPath(sv)\n\t}\n\tdebug.Log(\"initialize - %s\", path)\n\n\ts, err := leaf.New(ctx, \"\", path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize the root store at %q: %w\", r.cfg.Path(), err)\n\t}\n\n\tdebug.Log(\"Root Store initialized at %s\", path)\n\n\tr.store = s\n\n\t// initialize all mounts\n\tfor _, alias := range r.cfg.Mounts() {\n\t\tpath := fsutil.CleanPath(r.cfg.MountPath(alias))\n\t\tif err := r.addMount(ctx, alias, path); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to initialize mount %s (%s). Ignoring: %s\", alias, path, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"Sub-Store mounted at %s from %s\", alias, path)\n\t}\n\n\t// check for duplicate mounts\n\tif err := r.checkMounts(); err != nil {\n\t\treturn fmt.Errorf(\"checking mounts failed: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/store/root/init_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInit(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\n\tcfg := config.NewInMemory()\n\trequire.NoError(t, cfg.SetPath(u.StoreDir(\"rs\")))\n\trs := New(cfg)\n\n\tinited, err := rs.IsInitialized(ctx)\n\trequire.NoError(t, err)\n\tassert.False(t, inited)\n\trequire.NoError(t, rs.Init(ctx, \"\", u.StoreDir(\"rs\"), \"0xDEADBEEF\"))\n\n\tinited, err = rs.IsInitialized(ctx)\n\trequire.NoError(t, err)\n\tassert.True(t, inited)\n\trequire.NoError(t, rs.Init(ctx, \"rs2\", u.StoreDir(\"rs2\"), \"0xDEADBEEF\"))\n}\n"
  },
  {
    "path": "internal/store/root/link.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// Link creates a symlink.\nfunc (r *Store) Link(ctx context.Context, from, to string) error {\n\tsubFrom, fName := r.getStore(from)\n\tsubTo, tName := r.getStore(to)\n\n\tif !subFrom.Equals(subTo) {\n\t\treturn fmt.Errorf(\"sylinks across stores are not supported\")\n\t}\n\n\treturn subFrom.Link(ctx, fName, tName)\n}\n"
  },
  {
    "path": "internal/store/root/list.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// List will return a flattened list of all tree entries.\nfunc (r *Store) List(ctx context.Context, maxDepth int) ([]string, error) {\n\tt, err := r.Tree(ctx)\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\n\treturn t.List(maxDepth), nil\n}\n\n// Tree returns the tree representation of the entries.\nfunc (r *Store) Tree(ctx context.Context) (*tree.Root, error) {\n\troot := tree.New(\"gopass\")\n\taddFileFunc := func(in ...string) {\n\t\tfor _, f := range in {\n\t\t\tvar ct string\n\n\t\t\tswitch {\n\t\t\tcase strings.HasSuffix(f, \".b64\"):\n\t\t\t\tct = \"application/octet-stream\"\n\t\t\tcase strings.HasSuffix(f, \".yml\"):\n\t\t\t\tct = \"text/yaml\"\n\t\t\tcase strings.HasSuffix(f, \".yaml\"):\n\t\t\t\tct = \"text/yaml\"\n\t\t\tdefault:\n\t\t\t\tct = \"text/plain\"\n\t\t\t}\n\n\t\t\tif err := root.AddFile(f, ct); err != nil {\n\t\t\t\tout.Errorf(ctx, \"Failed to add file %s to tree: %s\", f, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\taddTplFunc := func(in ...string) {\n\t\tfor _, f := range in {\n\t\t\tif err := root.AddTemplate(f); err != nil {\n\t\t\t\tout.Errorf(ctx, \"Failed to add template %s to tree: %s\", f, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\tsf, err := r.store.List(ctx, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdebug.V(1).Log(\"[root] adding files: %q\", sf)\n\taddFileFunc(sf...)\n\tdebug.V(1).Log(\"[root] Tree: %s\", root.Format(-1))\n\taddTplFunc(r.store.ListTemplates(ctx, \"\")...)\n\n\tmps := r.MountPoints()\n\tsort.Sort(store.ByPathLen(mps))\n\n\tfor _, alias := range mps {\n\t\tsubstore := r.mounts[alias]\n\t\tif substore == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := root.AddMount(alias, substore.Path()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to add mount: %w\", err)\n\t\t}\n\n\t\tsf, err := substore.List(ctx, \"\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to add file: %w\", err)\n\t\t}\n\n\t\tdebug.V(1).Log(\"[%s] adding files: %q\", alias, sf)\n\t\taddFileFunc(sf...)\n\t\taddTplFunc(substore.ListTemplates(ctx, alias)...)\n\t}\n\n\treturn root, nil\n}\n\n// HasSubDirs returns true if the named entity has subdirectories.\nfunc (r *Store) HasSubDirs(ctx context.Context, name string) (bool, error) {\n\tsub, prefix := r.getStore(name)\n\n\tentries, err := sub.List(ctx, prefix)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, e := range entries {\n\t\tif sub.IsDir(ctx, e) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// Format will pretty print all entries in this store and all substores.\nfunc (r *Store) Format(ctx context.Context, maxDepth int) (string, error) {\n\tt, err := r.Tree(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn t.Format(maxDepth), nil\n}\n"
  },
  {
    "path": "internal/store/root/list_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestList(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tcolor.NoColor = true\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tes, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"foo\"}, es)\n\n\tsd, err := rs.HasSubDirs(ctx, \"foo\")\n\trequire.NoError(t, err)\n\tassert.False(t, sd)\n\n\tstr, err := rs.Format(ctx, -1)\n\trequire.NoError(t, err)\n\tassert.Equal(t, `gopass\n└── foo\n`, str)\n}\n"
  },
  {
    "path": "internal/store/root/mount.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// AddMount adds a new mount.\nfunc (r *Store) AddMount(ctx context.Context, alias, path string, keys ...string) error {\n\tif err := r.addMount(ctx, alias, path, keys...); err != nil {\n\t\treturn fmt.Errorf(\"failed to add mount: %w\", err)\n\t}\n\n\t// check for duplicate mounts\n\treturn r.checkMounts()\n}\n\nfunc (r *Store) addMount(ctx context.Context, alias, path string, keys ...string) error {\n\t// disallow filepath separators in alias and always disallow regular slashes\n\t// even on Windows, since these are used internally to separate folders.\n\tif strings.HasSuffix(alias, \"/\") {\n\t\treturn fmt.Errorf(\"alias must not end with '/'\")\n\t}\n\tif strings.HasSuffix(alias, string(filepath.Separator)) {\n\t\treturn fmt.Errorf(\"alias must not end with '%s'\", string(filepath.Separator))\n\t}\n\n\talias = CleanMountAlias(alias)\n\tif alias == \"\" {\n\t\treturn fmt.Errorf(\"alias must not be empty\")\n\t}\n\n\tif r.mounts == nil {\n\t\tr.mounts = make(map[string]*leaf.Store, 1)\n\t}\n\n\tif _, found := r.mounts[alias]; found {\n\t\treturn AlreadyMountedError(alias)\n\t}\n\n\tfullPath := fsutil.CleanPath(path)\n\tdebug.Log(\"addMount - Path: %s - Full: %s\", path, fullPath)\n\n\t// initialize sub store\n\ts, err := r.initSub(ctx, alias, fullPath, keys)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to init sub store %q at %q: %w\", alias, fullPath, err)\n\t}\n\n\tr.mounts[alias] = s\n\tif err := r.cfg.SetMountPath(alias, path); err != nil {\n\t\treturn fmt.Errorf(\"failed to set mount path: %w\", err)\n\t}\n\n\tdebug.Log(\"Added mount %s -> %s (%s)\", alias, path, fullPath)\n\n\treturn nil\n}\n\nfunc (r *Store) initSub(ctx context.Context, alias, path string, keys []string) (*leaf.Store, error) {\n\talias = CleanMountAlias(alias)\n\t// init regular sub store\n\ts, err := leaf.New(ctx, alias, path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize store %q at %q: %w\", alias, path, err)\n\t}\n\n\tif s.IsInitialized(ctx) {\n\t\treturn s, nil\n\t}\n\n\tdebug.Log(\"[%s] Mount %s is not initialized\", alias, path)\n\n\tif len(keys) < 1 {\n\t\tdebug.Log(\"[%s] No keys available\", alias)\n\n\t\treturn s, NotInitializedError{alias, path}\n\t}\n\n\tdebug.Log(\"[%s] Trying to initialize at %s for %+v\", alias, path, keys)\n\n\tif err := s.Init(ctx, path, keys...); err != nil {\n\t\treturn s, fmt.Errorf(\"failed to initialize store %q at %q: %w\", alias, path, err)\n\t}\n\n\tout.Printf(ctx, \"Password store %s initialized for:\", path)\n\n\tfor _, r := range s.Recipients(ctx) {\n\t\tout.Noticef(ctx, \"  %s\", r)\n\t}\n\n\treturn s, nil\n}\n\n// RemoveMount removes and existing mount.\nfunc (r *Store) RemoveMount(ctx context.Context, alias string) error {\n\tif _, found := r.mounts[alias]; !found {\n\t\tout.Warningf(ctx, \"%s is not mounted\", alias)\n\t}\n\n\tif _, found := r.mounts[alias]; !found {\n\t\tout.Warningf(ctx, \"%s is not initialized\", alias)\n\t}\n\n\tdelete(r.mounts, alias)\n\tif err := r.cfg.Unset(\"\", \"mounts.\"+alias+\".path\"); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Mounts returns a map of mounts with their paths.\nfunc (r *Store) Mounts() map[string]string {\n\tm := make(map[string]string, len(r.mounts))\n\tfor alias, sub := range r.mounts {\n\t\tm[alias] = sub.Path()\n\t}\n\n\treturn m\n}\n\n// MountPoints returns a sorted list of mount points. It encodes the logic that\n// the longer a mount point the more specific it is. This allows to \"shadow\" a\n// shorter mount point by a longer one.\nfunc (r *Store) MountPoints() []string {\n\tmps := make([]string, 0, len(r.mounts))\n\tfor k := range r.mounts {\n\t\tmps = append(mps, k)\n\t}\n\n\tsort.Sort(sort.Reverse(store.ByPathLen(mps)))\n\n\treturn mps\n}\n\n// MountPoint returns the most-specific mount point for the given key.\nfunc (r *Store) MountPoint(name string) string {\n\tfor _, mp := range r.MountPoints() {\n\t\tif strings.HasPrefix(name+\"/\", mp+\"/\") {\n\t\t\treturn mp\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// Lock drops all cached credentials, if any. Mostly only useful\n// for the gopass REPL.\nfunc (r *Store) Lock() error {\n\tfor _, sub := range r.mounts {\n\t\tif err := sub.Lock(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn r.store.Lock()\n}\n\n// getStore returns the Store object at the most-specific mount point for the\n// given key. returns sub store reference, truncated path to secret.\nfunc (r *Store) getStore(name string) (*leaf.Store, string) {\n\tname = strings.TrimSuffix(name, \"/\")\n\tmp := r.MountPoint(name)\n\n\tif sub, found := r.mounts[mp]; found {\n\t\treturn sub, strings.TrimPrefix(name, sub.Alias())\n\t}\n\n\treturn r.store, name\n}\n\n// GetSubStore returns an exact match for a mount point or an error if this\n// mount point does not exist.\nfunc (r *Store) GetSubStore(name string) (*leaf.Store, error) {\n\tif name == \"\" {\n\t\treturn r.store, nil\n\t}\n\n\tif sub, found := r.mounts[name]; found {\n\t\treturn sub, nil\n\t}\n\n\tdebug.Log(\"mounts available: %+v\", r.mounts)\n\n\treturn nil, fmt.Errorf(\"no such mount point %q\", name)\n}\n\n// checkMounts performs some sanity checks on our mounts. At the moment it\n// only checks if some path is mounted twice.\nfunc (r *Store) checkMounts() error {\n\tpaths := make(map[string]string, len(r.mounts))\n\tfor k, v := range r.mounts {\n\t\tif _, found := paths[v.Path()]; found {\n\t\t\treturn fmt.Errorf(\"doubly mounted path at %s: %s\", v.Path(), k)\n\t\t}\n\n\t\tpaths[v.Path()] = k\n\t}\n\n\treturn nil\n}\n\n// CleanMountAlias removes all leading and trailing slashes from a mount alias.\n// Note: Slashes inside the alias are valid and will be kept.\nfunc CleanMountAlias(alias string) string {\n\tfor strings.HasPrefix(alias, \"/\") || strings.HasPrefix(alias, \"\\\\\") {\n\t\talias = strings.TrimPrefix(strings.TrimSuffix(alias, \"/\"), \"/\")\n\t\talias = strings.TrimPrefix(strings.TrimSuffix(alias, \"\\\\\"), \"\\\\\")\n\t}\n\tfor strings.HasSuffix(alias, \"/\") || strings.HasSuffix(alias, \"\\\\\") {\n\t\talias = strings.TrimSuffix(strings.TrimPrefix(alias, \"/\"), \"/\")\n\t\talias = strings.TrimSuffix(strings.TrimPrefix(alias, \"\\\\\"), \"\\\\\")\n\t}\n\n\treturn alias\n}\n"
  },
  {
    "path": "internal/store/root/mount_test.go",
    "content": "package root\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMount(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, map[string]string{}, rs.Mounts())\n\tassert.Equal(t, []string{}, rs.MountPoints())\n\n\tsub, err := rs.GetSubStore(\"\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sub)\n\n\tsub, err = rs.GetSubStore(\"foo\")\n\trequire.Error(t, err)\n\tassert.Nil(t, sub)\n\n\t// removing mounts should never fail\n\trequire.NoError(t, rs.RemoveMount(ctx, \"foo\"))\n}\n\nfunc TestMountPoint(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, u.InitStore(\"sub1\"))\n\trequire.NoError(t, u.InitStore(\"sub2\"))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub1\", u.StoreDir(\"sub1\")))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub2\", u.StoreDir(\"sub2\")))\n\n\tassert.Equal(t, \"sub1\", rs.MountPoint(\"sub1\"))\n}\n\nfunc TestMountPointIllegal(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, u.InitStore(\"sub1\"))\n\trequire.NoError(t, u.InitStore(\"sub2\"))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub1/foo\", u.StoreDir(\"sub1\")))\n\tif runtime.GOOS == \"windows\" {\n\t\trequire.NoError(t, rs.AddMount(ctx, \"sub2\\\\foo\", u.StoreDir(\"sub2\")))\n\t\trequire.Error(t, rs.AddMount(ctx, \"sub2\\\\\", u.StoreDir(\"sub2\")))\n\t}\n\trequire.Error(t, rs.AddMount(ctx, \"sub2/\", u.StoreDir(\"sub2\")))\n}\n\nfunc TestCleanMountAlias(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname     string\n\t\tin, want string\n\t}{\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo\",\n\t\t\twant: \"foo\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar/\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar//\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar/////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar//////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar///////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar/////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar//////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar///////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar////////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar/////////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar//////////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar///////////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"foo/bar////////////////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tin:   \"foo/bar/\\\\///\\\\////\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"/foo/bar\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"//foo/bar\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   \"////foo/bar\",\n\t\t\twant: \"foo/bar\",\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.want, CleanMountAlias(tc.in))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/store/root/move.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/fsutil\"\n)\n\n// Copy will copy one entry to another location. Multi-store copies are\n// supported. Each entry has to be decoded and encoded for the destination\n// to make sure it's encrypted for the right set of recipients.\nfunc (r *Store) Copy(ctx context.Context, from, to string) error {\n\tdebug.Log(\"Copy %s to %s\", from, to)\n\n\treturn r.move(ctx, from, to, false)\n}\n\n// Move will move one entry from one location to another. Cross-store moves are\n// supported. Moving an entry will decode it from the old location, encode it\n// for the destination store with the right set of recipients and remove it\n// from the old location afterwards.\nfunc (r *Store) Move(ctx context.Context, from, to string) error {\n\tdebug.Log(\"Move %s to %s\", from, to)\n\n\treturn r.move(ctx, from, to, true)\n}\n\n// move handles both copy and move operations. Since the only difference is\n// deleting the source entry after the copy, we can reuse the same code.\nfunc (r *Store) move(ctx context.Context, from, to string, del bool) error {\n\tsubFrom, fromPrefix := r.getStore(from)\n\tsubTo, _ := r.getStore(to)\n\n\tif err := r.moveFromTo(ctx, subFrom, from, to, fromPrefix, del); err != nil {\n\t\treturn err\n\t}\n\n\tif !ctxutil.IsGitCommit(ctx) {\n\t\treturn nil\n\t}\n\n\tcommitMsg := ctxutil.GetCommitMessage(ctx)\n\tif err := subFrom.Storage().TryCommit(ctx, commitMsg); del && err != nil {\n\t\treturn fmt.Errorf(\"failed to commit changes to git (%s): %w\", subFrom.Alias(), err)\n\t}\n\n\tif !subFrom.Equals(subTo) {\n\t\tif err := subTo.Storage().TryCommit(ctx, commitMsg); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to commit changes to git (%s): %w\", subTo.Alias(), err)\n\t\t}\n\t}\n\n\tif err := subFrom.Storage().TryPush(ctx, \"\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push change to git remote: %w\", err)\n\t}\n\n\tif subFrom.Equals(subTo) {\n\t\treturn nil\n\t}\n\n\tif err := subTo.Storage().TryPush(ctx, \"\", \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to push change to git remote: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (r *Store) moveFromTo(ctx context.Context, subFrom *leaf.Store, from, to, fromPrefix string, del bool) error {\n\tctx = ctxutil.WithGitCommit(ctx, false)\n\n\t// source is a directory and not a \"shadowed\" leaf\n\tsrcIsDir := r.IsDir(ctx, from) && !r.Exists(ctx, from)\n\tdstIsDir := r.IsDir(ctx, to)\n\n\tif srcIsDir && r.Exists(ctx, to) && !dstIsDir {\n\t\treturn fmt.Errorf(\"destination is a file\")\n\t}\n\n\tentries := []string{from}\n\t// if the source is a directory we enumerate all it's children\n\t// and move them one by one.\n\tif srcIsDir {\n\t\tvar err error\n\n\t\tentries, err = subFrom.List(ctx, fromPrefix+\"/\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(entries) < 1 {\n\t\tdebug.Log(\"Subtree %q has no entries\", from)\n\n\t\treturn fmt.Errorf(\"no entries\")\n\t}\n\n\tdebug.Log(\"Moving (sub) tree %q to %q (entries: %+v)\", from, to, entries)\n\n\tvar moved uint\n\tfor _, src := range entries {\n\t\tdst := computeMoveDestination(src, from, to, srcIsDir, dstIsDir)\n\t\tif src == dst {\n\t\t\tdebug.Log(\"skipping %q. src eq dst\", src)\n\n\t\t\tcontinue\n\t\t}\n\t\tdebug.Log(\"Moving entry %q (%q) => %q (%q) (srcIsDir:%t, dstIsDir:%t, delete:%t)\\n\", src, from, dst, to, srcIsDir, dstIsDir, del)\n\n\t\terr := r.directMove(ctx, src, dst, del)\n\t\tif err == nil {\n\t\t\tmoved++\n\t\t\tdebug.Log(\"directly moved from %q to %q\", src, dst)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"direct move failed to move entry %q to %q: %s. Falling back to get and set\", src, dst, err)\n\n\t\tcontent, err := r.Get(ctx, src)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"source %s does not exist in source store %s: %w\", from, subFrom.Alias(), err)\n\t\t}\n\n\t\tif err := r.Set(ctx, dst, content); err != nil {\n\t\t\tif !errors.Is(err, store.ErrMeaninglessWrite) {\n\t\t\t\treturn fmt.Errorf(\"failed to save secret %q to store: %w\", to, err)\n\t\t\t}\n\t\t\tout.Warningf(ctx, \"No need to write: the secret is already there and with the right value\")\n\t\t}\n\n\t\tif del {\n\t\t\tdebug.Log(\"Deleting moved entry %q from source %q\", from, src)\n\n\t\t\tif err := r.Delete(ctx, src); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to delete secret %q: %w\", src, err)\n\t\t\t}\n\t\t}\n\n\t\tmoved++\n\t}\n\n\tif moved < 1 {\n\t\treturn fmt.Errorf(\"no entries moved\")\n\t}\n\n\tdebug.Log(\"Moved (sub) tree %q to %q\", from, to)\n\n\treturn nil\n}\n\nfunc (r *Store) directMove(ctx context.Context, from, to string, del bool) error {\n\tdebug.Log(\"directMove from %q to %q\", from, to)\n\n\t// will also remove the store prefix, if applicable\n\tsubFrom, from := r.getStore(from)\n\n\t// we don't remove store prefix for destination, as it can be a new folder\n\tsubTo, to := r.getStore(to)\n\n\tif subFrom.Equals(subTo) {\n\t\tdebug.Log(\"directMove from %q to %q: same store\", from, to)\n\n\t\tif del {\n\t\t\treturn subFrom.Move(ctx, from, to)\n\t\t}\n\n\t\treturn subFrom.Copy(ctx, from, to)\n\t}\n\n\tdebug.Log(\"cross mount direct move from %s%s to %s%s\", subFrom.Alias(), from, subTo.Alias(), to)\n\n\t// assemble source and destination paths, call fsutil.CopyFile(from, to), remove source\n\t// if del is true and then git add and commit both stores.\n\tsfn := filepath.Join(subFrom.Path(), subFrom.Passfile(from))\n\tdfn := filepath.Join(subTo.Path(), subTo.Passfile(to))\n\n\tif err := fsutil.CopyFile(sfn, dfn); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy %q to %q: %w\", from, to, err)\n\t}\n\n\tif del {\n\t\tif err := os.Remove(sfn); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete %q from %s: %w\", sfn, subFrom.Alias(), err)\n\t\t}\n\t}\n\n\tif err := subFrom.Storage().Add(ctx, sfn); err != nil {\n\t\tdebug.Log(\"failed to add %q to %s: %w\", sfn, subFrom.Alias(), err)\n\t}\n\n\tif err := subTo.Storage().Add(ctx, dfn); err != nil {\n\t\tdebug.Log(\"failed to add %q to %s: %w\", dfn, subTo.Alias(), err)\n\t}\n\n\treturn nil\n}\n\nfunc computeMoveDestination(src, from, to string, srcIsDir, dstIsDir bool) string {\n\t// special case: moving up to the root\n\tif to == \".\" || to == \"/\" {\n\t\tdstIsDir = false\n\t\tto = \"\"\n\t}\n\n\t// are we moving into an existing directory? Then we just need to prepend\n\t// it's name to the source.\n\t// a -> b\n\t// - a/f1 -> b/a/f1\n\t// a -> b\n\t// - a -> b/a\n\tif dstIsDir {\n\t\tif !srcIsDir {\n\t\t\treturn path.Join(to, path.Base(src))\n\t\t}\n\n\t\treturn path.Join(to, src)\n\t}\n\n\t// are we moving a simple file? that's easy\n\tif !srcIsDir {\n\t\t// otherwise we just rename a file to another name\n\t\treturn to\n\t}\n\n\t// move a/ b, where a is a directory with a trailing slash and b\n\t// does not exist, i.e. move a to b\n\tif strings.HasSuffix(from, \"/\") {\n\t\treturn path.Join(to, strings.TrimPrefix(src, from))\n\t}\n\n\t// move a b, where a is a directory but not b, i.e. rename a to b.\n\t// this is applied to every child of a, so we need to remove the\n\t// old prefix (a) and add the new one (b).\n\treturn path.Join(to, strings.TrimPrefix(src, from))\n}\n\n// Delete will remove an single entry from the store.\nfunc (r *Store) Delete(ctx context.Context, name string) error {\n\tstore, sn := r.getStore(name)\n\tif sn == \"\" {\n\t\treturn fmt.Errorf(\"can not delete a mount point. Use `gopass mounts remove %s`\", store.Alias())\n\t}\n\n\treturn store.Delete(ctx, sn)\n}\n\n// Prune will remove a subtree from the Store.\nfunc (r *Store) Prune(ctx context.Context, tree string) error {\n\tfor mp := range r.mounts {\n\t\tif strings.HasPrefix(mp, tree) {\n\t\t\treturn fmt.Errorf(\"can not prune subtree with mounts. Unmount first: `gopass mounts remove %s`\", mp)\n\t\t}\n\t}\n\n\tstore, tree := r.getStore(tree)\n\n\treturn store.Prune(ctx, tree)\n}\n"
  },
  {
    "path": "internal/store/root/move_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMoveShadow(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"old/www/foo\",\n\t\t\"old/www/bar\",\n\t}\n\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// -> move old/www/foo www/dir/foo => OK\n\trequire.NoError(t, rs.Move(ctx, \"old/www/foo\", \"www/dir/foo\"))\n\tentries, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"old/www/bar\",\n\t\t\"www/dir/foo\",\n\t}, entries)\n\n\t// -> move old/www/bar www/ => OK\n\trequire.NoError(t, rs.Move(ctx, \"old/www/bar\", \"www/\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"www/bar\",\n\t\t\"www/dir/foo\",\n\t}, entries)\n}\n\nfunc TestMove(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"foo/bar\",\n\t\t\"foo/baz\",\n\t\t\"misc/zab\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// Initial state:\n\tentries, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"foo/bar\",\n\t\t\"foo/baz\",\n\t\t\"misc/zab\",\n\t}, entries)\n\n\t// -> move foo/ misc/zab => ERROR: misc/zab is a file\n\trequire.Error(t, rs.Move(ctx, \"foo/\", \"misc/zab\"))\n\n\t// -> move foo misc/zab => ERROR: misc/zab is a file\n\trequire.Error(t, rs.Move(ctx, \"foo\", \"misc/zab\"))\n\n\t// -> move foo misc => OK\n\trequire.NoError(t, rs.Move(ctx, \"foo\", \"misc\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"misc/foo/bar\",\n\t\t\"misc/foo/baz\",\n\t\t\"misc/zab\",\n\t}, entries)\n\n\t// -> move misc/foo bar/ => OK\n\trequire.NoError(t, rs.Move(ctx, \"misc/foo\", \"bar/\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\n\t\t\"bar/bar\",\n\t\t\"bar/baz\",\n\t\t\"misc/zab\",\n\t}, entries)\n\n\t// -> move misc/zab bar/foo/zab => OK\n\trequire.NoError(t, rs.Move(ctx, \"misc/zab\", \"bar/foo/zab\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\n\t\t\"bar/bar\",\n\t\t\"bar/baz\",\n\t\t\"bar/foo/zab\",\n\t}, entries)\n\n\t// -> move bar/foo/ baz => OK\n\trequire.NoError(t, rs.Move(ctx, \"bar/foo/\", \"baz\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\n\t\t\"bar/bar\",\n\t\t\"bar/baz\",\n\t\t\"baz/zab\",\n\t}, entries)\n\n\t// -> move baz/ boz/ => OK\n\trequire.NoError(t, rs.Move(ctx, \"baz/\", \"boz/\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\n\t\t\"bar/bar\",\n\t\t\"bar/baz\",\n\t\t\"boz/zab\",\n\t}, entries)\n\n\t// this fails if empty directories are not removed, because 'bar' and 'baz'\n\t// were directories in the root folder.\n\t// -> move boz/ / => OK\n\trequire.NoError(t, rs.Move(ctx, \"boz/\", \".\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\n\t\t\"bar/bar\",\n\t\t\"bar/baz\",\n\t\t\"zab\",\n\t}, entries)\n}\n\nfunc TestUnixMvSemantics(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"a/f1\",\n\t\t\"a/f2\",\n\t\t\"b/f3\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// Initial state:\n\tentries, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"a/f1\",\n\t\t\"a/f2\",\n\t\t\"b/f3\",\n\t}, entries)\n\n\t// -> move a b => Move a below b\n\trequire.NoError(t, rs.Move(ctx, \"a\", \"b\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"b/a/f1\",\n\t\t\"b/a/f2\",\n\t\t\"b/f3\",\n\t}, entries)\n}\n\nfunc TestRegression2079(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"comm/test\",\n\t\t\"comm/test2\",\n\t\t\"communication/t1\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// Initial state:\n\tentries, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"comm/test\",\n\t\t\"comm/test2\",\n\t\t\"communication/t1\",\n\t}, entries)\n\n\t// -> move comm email => Rename comm to email\n\trequire.NoError(t, rs.Move(ctx, \"comm\", \"email\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"communication/t1\",\n\t\t\"email/test\",\n\t\t\"email/test2\",\n\t}, entries)\n}\n\nfunc TestCopy(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"foo/bar\",\n\t\t\"foo/baz\",\n\t\t\"misc/zab\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// Initial state:\n\tt.Run(\"initial state\", func(t *testing.T) {\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"foo/bar\",\n\t\t\t\"foo/baz\",\n\t\t\t\"misc/zab\",\n\t\t}, entries)\n\t})\n\n\t// -> copy foo/ misc/zab => ERROR: misc/zab is a file\n\trequire.Error(t, rs.Copy(ctx, \"foo/\", \"misc/zab\"))\n\t// -> copy foo misc/zab => ERROR: misc/zab is a file\n\trequire.Error(t, rs.Copy(ctx, \"foo\", \"misc/zab\"))\n\n\t// -> copy foo/ misc => OK\n\tt.Run(\"copy foo misc\", func(t *testing.T) {\n\t\trequire.NoError(t, rs.Copy(ctx, \"foo\", \"misc\"))\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"foo/bar\",\n\t\t\t\"foo/baz\",\n\t\t\t\"misc/foo/bar\",\n\t\t\t\"misc/foo/baz\",\n\t\t\t\"misc/zab\",\n\t\t}, entries)\n\t})\n\n\t// -> copy misc/foo/ bar/ => OK\n\tt.Run(\"copy misc/foo/ bar/\", func(t *testing.T) {\n\t\trequire.NoError(t, rs.Copy(ctx, \"misc/foo/\", \"bar/\"))\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"bar/bar\",\n\t\t\t\"bar/baz\",\n\t\t\t\"foo/bar\",\n\t\t\t\"foo/baz\",\n\t\t\t\"misc/foo/bar\",\n\t\t\t\"misc/foo/baz\",\n\t\t\t\"misc/zab\",\n\t\t}, entries)\n\t})\n\n\t// -> copy misc/zab bar/foo/zab => OK\n\tt.Run(\"copy misc/zab bar/foo/zab\", func(t *testing.T) {\n\t\trequire.NoError(t, rs.Copy(ctx, \"misc/zab\", \"bar/foo/zab\"))\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"bar/bar\",\n\t\t\t\"bar/baz\",\n\t\t\t\"bar/foo/zab\",\n\t\t\t\"foo/bar\",\n\t\t\t\"foo/baz\",\n\t\t\t\"misc/foo/bar\",\n\t\t\t\"misc/foo/baz\",\n\t\t\t\"misc/zab\",\n\t\t}, entries)\n\t})\n}\n\nfunc TestMoveSelf(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"foo/bar/example\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\t// Initial state:\n\tt.Run(\"initial state\", func(t *testing.T) {\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"foo\",\n\t\t\t\"foo/bar/example\",\n\t\t}, entries)\n\t})\n\n\t// -> move foo/bar/example foo/bar -> no op\n\tt.Run(\"move self\", func(t *testing.T) {\n\t\trequire.Error(t, rs.Move(ctx, \"foo/bar/example\", \"foo/bar\"))\n\t\tentries, err := rs.List(ctx, tree.INF)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []string{\n\t\t\t\"foo\",\n\t\t\t\"foo/bar/example\",\n\t\t}, entries)\n\t})\n}\n\nfunc TestComputeMoveDestination(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tname     string\n\t\tsrc      string\n\t\tfrom     string\n\t\tto       string\n\t\tsrcIsDir bool\n\t\tdstIsDir bool\n\t\tdst      string\n\t}{\n\t\t{\n\t\t\tname: \"rename file a to file b\", // mv a b\n\t\t\tsrc:  \"a\",\n\t\t\tfrom: \"a\",\n\t\t\tto:   \"b\",\n\t\t\tdst:  \"b\",\n\t\t},\n\t\t{\n\t\t\tname:     \"rename dir a to dir b (#2079)\", // mv comm email\n\t\t\tsrc:      \"comm/test\",\n\t\t\tfrom:     \"comm\",\n\t\t\tto:       \"email\",\n\t\t\tdst:      \"email/test\",\n\t\t\tsrcIsDir: true,\n\t\t\tdstIsDir: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rename dir a to dir b (existing dir)\", // mv a b\n\t\t\tsrc:      \"a/f1\",\n\t\t\tfrom:     \"a\",\n\t\t\tto:       \"b\",\n\t\t\tdst:      \"b/a/f1\",\n\t\t\tsrcIsDir: true,\n\t\t\tdstIsDir: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"move up\", // mv a/b/c c\n\t\t\tsrc:      \"a/b/c/f1\",\n\t\t\tfrom:     \"a/b/c\",\n\t\t\tto:       \"c\",\n\t\t\tdst:      \"c/f1\",\n\t\t\tsrcIsDir: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"move fully up\", // mv a/ .\n\t\t\tsrc:      \"a/f1\",\n\t\t\tfrom:     \"a/\",\n\t\t\tto:       \".\",\n\t\t\tdst:      \"f1\",\n\t\t\tsrcIsDir: true,\n\t\t\tdstIsDir: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"old www\", // mv old/www/bar www/\n\t\t\tsrc:      \"old/www/bar\",\n\t\t\tfrom:     \"old/www/bar\",\n\t\t\tto:       \"www\",\n\t\t\tdst:      \"www/bar\",\n\t\t\tsrcIsDir: false,\n\t\t\tdstIsDir: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"one level up\", // mv foo/bar/example foo/bar\n\t\t\tsrc:      \"foo/bar/example\",\n\t\t\tfrom:     \"foo/bar\",\n\t\t\tto:       \"foo/bar\",\n\t\t\tdst:      \"foo/bar/example\",\n\t\t\tsrcIsDir: false,\n\t\t\tdstIsDir: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdst := computeMoveDestination(tc.src, tc.from, tc.to, tc.srcIsDir, tc.dstIsDir)\n\t\t\tassert.Equal(t, tc.dst, dst, tc.name)\n\t\t})\n\t}\n}\n\nfunc TestRegression892(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\tu.Entries = []string{\n\t\t\"some/example\",\n\t\t\"some/example/test2\",\n\t\t\"communication/t1\",\n\t}\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Delete(ctx, \"foo\"))\n\n\t// Initial state:\n\tentries, err := rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"communication/t1\",\n\t\t\"some/example\",\n\t\t\"some/example/test2\",\n\t}, entries)\n\n\t// -> move comm email => Rename comm to email\n\trequire.NoError(t, rs.Move(ctx, \"some/example\", \"some/example/test1\"))\n\tentries, err = rs.List(ctx, tree.INF)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\n\t\t\"communication/t1\",\n\t\t\"some/example/test1\",\n\t\t\"some/example/test2\",\n\t}, entries)\n}\n\nfunc TestMoveInMountedStore(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := t.Context()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\t// create a mount\n\trequire.NoError(t, u.InitStore(\"m7an\"))\n\tmountDir := u.StoreDir(\"m7an\")\n\trequire.NoError(t, rs.AddMount(ctx, \"m7an\", mountDir))\n\tsec := secrets.New()\n\tsec.SetPassword(\"foo\")\n\trequire.NoError(t, rs.Set(ctx, \"m7an/www/hostprvdr.de/hostprvdr@m7an.de\", sec))\n\n\t// move the secret\n\trequire.NoError(t, rs.Move(ctx, \"m7an/www/hostprvdr.de/hostprvdr@m7an.de\", \"m7an/www/hostprvdr.de/meinhostprvdr@m7an.de\"))\n\n\t// check if the secret was moved correctly\n\t_, err = rs.Get(ctx, \"m7an/www/hostprvdr.de/meinhostprvdr@m7an.de\")\n\trequire.NoError(t, err)\n\n\t// check that the old secret is gone\n\t_, err = rs.Get(ctx, \"m7an/www/hostprvdr.de/hostprvdr@m7an.de\")\n\trequire.Error(t, err)\n\n\t// check that no extra directory was created\n\t_, err = rs.Get(ctx, \"m7an/m7an/www/hostprvdr.de/meinhostprvdr@m7an.de\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "internal/store/root/rcs.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\n// RCSInit initializes the version control repo.\nfunc (r *Store) RCSInit(ctx context.Context, name, userName, userEmail string) error {\n\tstore, _ := r.getStore(name)\n\tctx = ctxutil.WithUsername(ctx, userName)\n\tctx = ctxutil.WithEmail(ctx, userEmail)\n\n\treturn store.GitInit(ctx)\n}\n\n// RCSInitConfig initializes the git repos local config.\nfunc (r *Store) RCSInitConfig(ctx context.Context, name, userName, userEmail string) error {\n\tstore, _ := r.getStore(name)\n\n\treturn store.Storage().InitConfig(ctx, userName, userEmail)\n}\n\n// RCSAddRemote adds a git remote.\nfunc (r *Store) RCSAddRemote(ctx context.Context, name, remote, url string) error {\n\tstore, _ := r.getStore(name)\n\n\treturn store.Storage().AddRemote(ctx, remote, url)\n}\n\n// RCSRemoveRemote removes a git remote.\nfunc (r *Store) RCSRemoveRemote(ctx context.Context, name, remote string) error {\n\tstore, _ := r.getStore(name)\n\n\treturn store.Storage().RemoveRemote(ctx, remote)\n}\n\n// RCSPull performs a git pull.\nfunc (r *Store) RCSPull(ctx context.Context, name, origin, remote string) error {\n\tstore, _ := r.getStore(name)\n\n\treturn store.Storage().Pull(ctx, origin, remote)\n}\n\n// RCSPush performs a git push.\nfunc (r *Store) RCSPush(ctx context.Context, name, origin, remote string) error {\n\tstore, _ := r.getStore(name)\n\n\treturn store.Storage().Push(ctx, origin, remote)\n}\n\n// ListRevisions will list all revisions for the named entity.\nfunc (r *Store) ListRevisions(ctx context.Context, name string) ([]backend.Revision, error) {\n\tstore, name := r.getStore(name)\n\n\treturn store.ListRevisions(ctx, name)\n}\n\n// GetRevision will try to retrieve the given revision from the sync backend.\nfunc (r *Store) GetRevision(ctx context.Context, name, revision string) (context.Context, gopass.Secret, error) {\n\tstore, name := r.getStore(name)\n\tsec, err := store.GetRevision(ctx, name, revision)\n\n\tif ref, ok := sec.Ref(); ctxutil.IsFollowRef(ctx) && ok {\n\t\trefSec, err := store.GetRevision(ctx, ref, revision)\n\t\tif err != nil {\n\t\t\treturn ctx, sec, fmt.Errorf(\"failed to read reference %s by %s: %w\", ref, name, err)\n\t\t}\n\n\t\tsec.SetPassword(refSec.Password())\n\t}\n\n\treturn ctx, sec, err\n}\n\n// RCSStatus show the git status.\n// TODO this should likely iterate over all stores.\nfunc (r *Store) RCSStatus(ctx context.Context, name string) error {\n\tstore, name := r.getStore(name)\n\tout.Printf(ctx, \"Store: %s\", store.Path())\n\n\treturn store.GitStatus(ctx, name)\n}\n"
  },
  {
    "path": "internal/store/root/rcs_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRCS(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tcolor.NoColor = true\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\trequire.Error(t, rs.RCSStatus(ctx, \"\"))\n\n\trevs, err := rs.ListRevisions(ctx, \"foo\")\n\trequire.Error(t, err)\n\tassert.Len(t, revs, 1)\n}\n"
  },
  {
    "path": "internal/store/root/read.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\n// Get returns the plaintext of a single key.\nfunc (r *Store) Get(ctx context.Context, name string) (gopass.Secret, error) {\n\tstore, name := r.getStore(name)\n\n\tsec, err := store.Get(ctx, name)\n\tif err != nil {\n\t\treturn sec, err\n\t}\n\n\tif ref, ok := sec.Ref(); ctxutil.IsFollowRef(ctx) && ok {\n\t\trefSec, err := store.Get(ctx, ref)\n\t\tif err != nil {\n\t\t\treturn sec, fmt.Errorf(\"failed to read reference %s by %s: %w\", ref, name, err)\n\t\t}\n\n\t\tsec.SetPassword(refSec.Password())\n\t}\n\n\treturn sec, nil\n}\n"
  },
  {
    "path": "internal/store/root/read_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGet(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\t_, err = rs.Get(ctx, \"foo\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/store/root/recipients.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// ListRecipients lists all recipients for the given store.\nfunc (r *Store) ListRecipients(ctx context.Context, store string) []string {\n\tsub, _ := r.getStore(store)\n\n\treturn sub.Recipients(ctx)\n}\n\n// CheckRecipients checks all current recipients to make sure that they are\n// valid, e.g. not expired.\nfunc (r *Store) CheckRecipients(ctx context.Context, store string) error {\n\tsub, _ := r.getStore(store)\n\n\treturn sub.CheckRecipients(ctx)\n}\n\n// AddRecipient adds a single recipient to the given store.\nfunc (r *Store) AddRecipient(ctx context.Context, store, rec string) error {\n\tsub, _ := r.getStore(store)\n\n\treturn sub.AddRecipient(ctx, rec)\n}\n\n// RemoveRecipient removes a single recipient from the given store.\nfunc (r *Store) RemoveRecipient(ctx context.Context, store, rec string) error {\n\tsub, _ := r.getStore(store)\n\n\treturn sub.RemoveRecipient(ctx, rec)\n}\n\nfunc (r *Store) addRecipient(ctx context.Context, prefix string, root *tree.Root, recp string, pretty bool) error {\n\tsub, _ := r.getStore(prefix)\n\tkey := recp\n\n\tif pretty {\n\t\tkey = fmt.Sprintf(\"%s (missing public key)\", recp)\n\n\t\tif v := sub.Crypto().FormatKey(ctx, recp, \"\"); v != \"\" {\n\t\t\tkey = v\n\t\t\tif !strings.HasPrefix(v, recp) {\n\t\t\t\tkey = recp + \" => \" + v\n\t\t\t}\n\t\t\tdebug.Log(\"formated (FormatKey) %s as %s\", recp, key)\n\t\t}\n\t}\n\n\t// workaround to keep key names from breaking the folder structure.\n\t// A proper fix should change tree.AddFile to take a path and file name\n\t// (which could then contain slashes).\n\tkey = strings.ReplaceAll(key, \"/\", \"\")\n\n\tdebug.Log(\"adding %q to the tree\", key)\n\n\treturn root.AddFile(prefix+key, \"gopass/recipient\")\n}\n\n// ImportMissingPublicKeys import missing public keys in any substore.\nfunc (r *Store) ImportMissingPublicKeys(ctx context.Context) error {\n\tfor alias, sub := range r.mounts {\n\t\tif err := sub.ImportMissingPublicKeys(ctx); err != nil {\n\t\t\tout.Errorf(ctx, \"[%s] Failed to import missing public keys: %s\", alias, err)\n\t\t}\n\t}\n\n\treturn r.store.ImportMissingPublicKeys(ctx)\n}\n\n// SaveRecipients persists the recipients to disk. Only useful if persist keys is\n// enabled.\nfunc (r *Store) SaveRecipients(ctx context.Context, ack bool) error {\n\tfor alias, sub := range r.mounts {\n\t\tif err := sub.SaveRecipients(ctx, ack); err != nil {\n\t\t\tout.Errorf(ctx, \"[%s] Failed to save recipients: %s\", alias, err)\n\t\t}\n\t}\n\n\treturn r.store.SaveRecipients(ctx, ack)\n}\n\n// RecipientsTree returns a tree view of all stores' recipients.\nfunc (r *Store) RecipientsTree(ctx context.Context, pretty bool) (*tree.Root, error) {\n\troot := tree.New(\"gopass\")\n\n\tfor name, recps := range r.store.RecipientsTree(ctx) {\n\t\tif name != \"\" {\n\t\t\tname += \"/\"\n\t\t}\n\n\t\tdebug.Log(\"Store/Secret: %q -> Recipients: %v\", name, recps)\n\n\t\tfor _, recp := range recps {\n\t\t\tif err := r.addRecipient(ctx, name, root, recp, pretty); err != nil {\n\t\t\t\tcolor.Yellow(\"Failed to add recipient to tree %s: %s\", recp, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tmps := r.MountPoints()\n\tsort.Sort(store.ByPathLen(mps))\n\n\tfor _, alias := range mps {\n\t\tsubstore := r.mounts[alias]\n\n\t\t// ignore invalid entries\n\t\tif substore == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := root.AddMount(alias, substore.Path()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to add mount: %w\", err)\n\t\t}\n\n\t\tfor name, recps := range substore.RecipientsTree(ctx) {\n\t\t\tif name != \"\" {\n\t\t\t\tname += \"/\"\n\t\t\t}\n\n\t\t\tfor _, recp := range recps {\n\t\t\t\tif err := r.addRecipient(ctx, alias+\"/\"+name, root, recp, pretty); err != nil {\n\t\t\t\t\tdebug.Log(\"Failed to add recipient to tree %s: %s\", recp, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn root, nil\n}\n"
  },
  {
    "path": "internal/store/root/recipients_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecipients(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tcolor.NoColor = true\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, []string{\"0xDEADBEEF\"}, rs.ListRecipients(ctx, \"\"))\n\trt, err := rs.RecipientsTree(ctx, false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"gopass\\n└── 0xDEADBEEF\\n\", rt.Format(0))\n}\n"
  },
  {
    "path": "internal/store/root/store.go",
    "content": "// Package root provides the root store implementation for gopass.\n// It implements the gopass.Store interface and provides methods to\n// interact with the password store. It is responsible for managing\n// the underlying storage backends and mounting them as needed.\npackage root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Store is the public facing password store. It contains one or more\n// leaf stores.\ntype Store struct {\n\tcfg    *config.Config\n\tmounts map[string]*leaf.Store\n\tstore  *leaf.Store\n}\n\n// New creates a new store.\nfunc New(cfg *config.Config) *Store {\n\tif cfg == nil {\n\t\tcfg = &config.Config{}\n\t}\n\n\tr := &Store{\n\t\tcfg:    cfg,\n\t\tmounts: make(map[string]*leaf.Store, len(cfg.Mounts())),\n\t}\n\n\tdebug.Log(\"created store %s\", r)\n\n\treturn r\n}\n\n// WithStoreConfig populates the context with the store config.\nfunc (r *Store) WithStoreConfig(ctx context.Context) context.Context {\n\treturn r.cfg.WithConfig(ctx)\n}\n\n// Exists checks the existence of a single entry.\nfunc (r *Store) Exists(ctx context.Context, name string) bool {\n\tstore, name := r.getStore(name)\n\n\treturn store.Exists(ctx, name)\n}\n\n// IsDir checks if a given key is actually a folder.\nfunc (r *Store) IsDir(ctx context.Context, name string) bool {\n\tstore, name := r.getStore(name)\n\n\treturn store.IsDir(ctx, name)\n}\n\nfunc (r *Store) String() string {\n\tms := make([]string, 0, len(r.mounts))\n\tfor alias, sub := range r.mounts {\n\t\tms = append(ms, alias+\"=\"+sub.String())\n\t}\n\n\tpath := \"\"\n\tif r.store != nil {\n\t\tpath = r.store.Path()\n\t}\n\n\treturn fmt.Sprintf(\"Store(Path: %s, Mounts: %+v)\", path, strings.Join(ms, \",\"))\n}\n\n// Path returns the store path.\nfunc (r *Store) Path() string {\n\tif r.store == nil {\n\t\treturn \"\"\n\t}\n\n\treturn r.store.Path()\n}\n\n// Alias always returns an empty string.\nfunc (r *Store) Alias() string {\n\treturn \"\"\n}\n\n// Storage returns the storage backend for the given mount point.\nfunc (r *Store) Storage(ctx context.Context, name string) backend.Storage {\n\tsub, _ := r.getStore(name)\n\tif sub == nil || !sub.Valid() {\n\t\tdebug.Log(\"failed to get mount point for %q\", name)\n\n\t\treturn nil\n\t}\n\n\treturn sub.Storage()\n}\n\n// Concurrency returns the concurrency level supported by this store,\n// which is the minimum of all mount points, limited by the number of cores.\nfunc (r *Store) Concurrency() int {\n\tc := math.MaxInt\n\tfor _, sub := range r.mounts {\n\t\tc = min(c, sub.Concurrency())\n\t}\n\n\treturn min(c, runtime.NumCPU())\n}\n"
  },
  {
    "path": "internal/store/root/store_test.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"path\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSimpleList(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\n\tu := gptest.NewUnitTester(t)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tst, err := rs.Tree(ctx)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"foo\"}, st.List(tree.INF))\n}\n\nfunc TestListMulti(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\n\tu := gptest.NewUnitTester(t)\n\n\t// root store\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tents := make([]string, 0, 3*len(u.Entries))\n\tents = append(ents, u.Entries...)\n\n\t// sub1 store\n\trequire.NoError(t, u.InitStore(\"sub1\"))\n\n\tfor _, k := range u.Entries {\n\t\tents = append(ents, path.Join(\"sub1\", k))\n\t}\n\n\t// sub2 store\n\trequire.NoError(t, u.InitStore(\"sub2\"))\n\n\tfor _, k := range u.Entries {\n\t\tents = append(ents, path.Join(\"sub2\", k))\n\t}\n\n\trequire.NoError(t, rs.AddMount(ctx, \"sub1\", u.StoreDir(\"sub1\")))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub2\", u.StoreDir(\"sub2\")))\n\n\tst, err := rs.Tree(ctx)\n\trequire.NoError(t, err)\n\n\tsort.Strings(ents)\n\n\tlst := st.List(tree.INF)\n\n\tsort.Strings(lst)\n\tassert.Equal(t, ents, lst)\n\n\tassert.Contains(t, rs.String(), \"Store(Path:\")\n}\n\nfunc TestListNested(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tctx = backend.WithCryptoBackend(ctx, backend.Plain)\n\n\tu := gptest.NewUnitTester(t)\n\n\t// root store\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tents := make([]string, 0, 3*len(u.Entries))\n\tents = append(ents, u.Entries...)\n\n\t// sub1 store\n\trequire.NoError(t, u.InitStore(\"sub1\"))\n\n\tfor _, k := range u.Entries {\n\t\tents = append(ents, path.Join(\"sub1\", k))\n\t}\n\n\t// sub2 store\n\trequire.NoError(t, u.InitStore(\"sub2\"))\n\n\tfor _, k := range u.Entries {\n\t\tents = append(ents, path.Join(\"sub2\", k))\n\t}\n\n\t// sub3 store\n\trequire.NoError(t, u.InitStore(\"sub3\"))\n\n\tfor _, k := range u.Entries {\n\t\tents = append(ents, path.Join(\"sub2\", \"sub3\", k))\n\t}\n\n\trequire.NoError(t, rs.AddMount(ctx, \"sub1\", u.StoreDir(\"sub1\")))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub2\", u.StoreDir(\"sub2\")))\n\trequire.NoError(t, rs.AddMount(ctx, \"sub2/sub3\", u.StoreDir(\"sub3\")))\n\n\tst, err := rs.Tree(ctx)\n\trequire.NoError(t, err)\n\n\tsort.Strings(ents)\n\n\tlst := st.List(tree.INF)\n\n\tsort.Strings(lst)\n\tassert.Equal(t, ents, lst)\n\n\tassert.False(t, rs.Exists(ctx, \"sub1\"))\n\tassert.True(t, rs.IsDir(ctx, \"sub1\"))\n\tassert.Empty(t, rs.Alias())\n\tassert.NotNil(t, rs.Storage(ctx, \"sub1\"))\n}\n\nfunc createRootStore(ctx context.Context, u *gptest.Unit) (*Store, error) {\n\tctx, err := backend.WithCryptoBackendString(ctx, \"plain\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcfg := config.NewInMemory()\n\tif err := cfg.SetPath(u.StoreDir(\"\")); err != nil {\n\t\treturn nil, err\n\t}\n\ts := New(cfg)\n\n\tif _, err := s.IsInitialized(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/store/root/templates.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/gopasspw/gopass/internal/tree\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// LookupTemplate will lookup and return a template.\nfunc (r *Store) LookupTemplate(ctx context.Context, name string) (string, []byte, bool) {\n\toName := name\n\tstore, name := r.getStore(name)\n\ttName, content, found := store.LookupTemplate(ctx, name)\n\ttName = filepath.Join(r.MountPoint(oName), tName)\n\n\treturn tName, content, found\n}\n\n// TemplateTree returns a tree of all templates.\nfunc (r *Store) TemplateTree(ctx context.Context) (*tree.Root, error) {\n\troot := tree.New(\"gopass\")\n\n\tfor _, t := range r.store.ListTemplates(ctx, \"\") {\n\t\tdebug.Log(\"[<root>] Adding template %s\", t)\n\n\t\tif err := root.AddFile(t, \"gopass/template\"); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to add file to tree: %s\", err)\n\t\t}\n\t}\n\n\tmps := r.MountPoints()\n\tsort.Sort(store.ByPathLen(mps))\n\n\tfor _, alias := range mps {\n\t\tsubstore := r.mounts[alias]\n\t\tif substore == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := root.AddMount(alias, substore.Path()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to add mount: %w\", err)\n\t\t}\n\n\t\tfor _, t := range substore.ListTemplates(ctx, alias) {\n\t\t\tdebug.Log(\"[%s] Adding template %s\", alias, t)\n\n\t\t\tif err := root.AddFile(t, \"gopass/template\"); err != nil {\n\t\t\t\tout.Errorf(ctx, \"Failed to add file to tree: %s\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn root, nil\n}\n\n// HasTemplate returns true if the template exists.\nfunc (r *Store) HasTemplate(ctx context.Context, name string) bool {\n\tstore, name := r.getStore(name)\n\n\treturn store.HasTemplate(ctx, name)\n}\n\n// GetTemplate will return the content of the named template.\nfunc (r *Store) GetTemplate(ctx context.Context, name string) ([]byte, error) {\n\tstore, name := r.getStore(name)\n\n\treturn store.GetTemplate(ctx, name)\n}\n\n// SetTemplate will (over)write the content to the template file.\nfunc (r *Store) SetTemplate(ctx context.Context, name string, content []byte) error {\n\tstore, name := r.getStore(name)\n\n\treturn store.SetTemplate(ctx, name, content)\n}\n\n// RemoveTemplate will delete the named template if it exists.\nfunc (r *Store) RemoveTemplate(ctx context.Context, name string) error {\n\tstore, name := r.getStore(name)\n\n\treturn store.RemoveTemplate(ctx, name)\n}\n"
  },
  {
    "path": "internal/store/root/templates_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTemplate(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tcolor.NoColor = true\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\ttt, err := rs.TemplateTree(ctx)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"gopass\\n\", tt.Format(0))\n\n\tassert.False(t, rs.HasTemplate(ctx, \"foo\"))\n\t_, err = rs.GetTemplate(ctx, \"foo\")\n\trequire.Error(t, err)\n\trequire.Error(t, rs.RemoveTemplate(ctx, \"foo\"))\n\n\trequire.NoError(t, rs.SetTemplate(ctx, \"foo\", []byte(\"foobar\")))\n\tassert.True(t, rs.HasTemplate(ctx, \"foo\"))\n\n\tb, err := rs.GetTemplate(ctx, \"foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", string(b))\n\n\t_, b, found := rs.LookupTemplate(ctx, \"foo/bar\")\n\tassert.True(t, found)\n\tassert.Equal(t, \"foobar\", string(b))\n\trequire.NoError(t, rs.RemoveTemplate(ctx, \"foo\"))\n}\n"
  },
  {
    "path": "internal/store/root/write.go",
    "content": "package root\n\nimport (\n\t\"context\"\n\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\n// Set encodes and write the ciphertext of one entry to disk.\nfunc (r *Store) Set(ctx context.Context, name string, sec gopass.Byter) error {\n\tstore, name := r.getStore(name)\n\n\treturn store.Set(ctx, name, sec)\n}\n"
  },
  {
    "path": "internal/store/root/write_test.go",
    "content": "package root\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSet(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithHidden(ctx, true)\n\n\trs, err := createRootStore(ctx, u)\n\trequire.NoError(t, err)\n\n\tsec := secrets.NewAKV()\n\tsec.SetPassword(\"foo\")\n\t_, err = sec.Write([]byte(\"bar\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, rs.Set(ctx, \"zab\", sec))\n\n\terr = rs.Set(ctx, \"zab2\", sec)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/store/sort.go",
    "content": "package store\n\nimport \"strings\"\n\n// ByPathLen sorts mount points by the number of level / path separators.\ntype ByPathLen []string\n\nfunc (s ByPathLen) Len() int { return len(s) }\n\nfunc (s ByPathLen) Less(i, j int) bool {\n\treturn strings.Count(s[i], \"/\") < strings.Count(s[j], \"/\")\n}\n\nfunc (s ByPathLen) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\n// ByLen is a list of mount points (string) that can be sorted by length.\ntype ByLen []string\n\n// Len return the number of mount points in the list.\nfunc (s ByLen) Len() int { return len(s) }\n\n// Less returns if a Mount point is shorter than another.\nfunc (s ByLen) Less(i, j int) bool { return len(s[i]) > len(s[j]) }\n\n// Swap Mount Point in the list of Mount Points.\nfunc (s ByLen) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\n"
  },
  {
    "path": "internal/store/sort_test.go",
    "content": "package store\n\nimport (\n\t\"sort\"\n\t\"testing\"\n)\n\nfunc TestMountPointSort(t *testing.T) {\n\tt.Parallel()\n\n\tmps := []string{\n\t\t\"sub2\",\n\t\t\"sub1isveryverylong\",\n\t\t\"sub2/sub3\",\n\t}\n\n\tsort.Sort(ByPathLen(mps))\n\n\tfor i, v := range []string{\n\t\t\"sub2\",\n\t\t\"sub1isveryverylong\",\n\t\t\"sub2/sub3\",\n\t} {\n\t\tt.Logf(\"[%d] %s - Want: %s\", i, mps[i], v)\n\n\t\tif mps[i] != v {\n\t\t\tt.Errorf(\"Mismatch at %d: %s vs. %s\", i, v, mps[i])\n\t\t}\n\t}\n}\n\nfunc TestMountPointReverseSort(t *testing.T) {\n\tt.Parallel()\n\n\tmps := []string{\n\t\t\"sub2\",\n\t\t\"sub1isveryverylong\",\n\t\t\"sub2/sub3\",\n\t}\n\n\tsort.Sort(sort.Reverse(ByPathLen(mps)))\n\n\tfor i, v := range []string{\n\t\t\"sub2/sub3\",\n\t\t\"sub2\",\n\t\t\"sub1isveryverylong\",\n\t} {\n\t\tt.Logf(\"[%d] %s - Want: %s\", i, mps[i], v)\n\n\t\tif mps[i] != v {\n\t\t\tt.Errorf(\"Mismatch at %d: %s vs. %s\", i, v, mps[i])\n\t\t}\n\t}\n}\n\nfunc TestSortByLen(t *testing.T) {\n\tt.Parallel()\n\n\tin := []string{\n\t\t\"a\",\n\t\t\"bb\",\n\t\t\"ccc\",\n\t\t\"dddd\",\n\t}\n\tout := []string{\n\t\t\"dddd\",\n\t\t\"ccc\",\n\t\t\"bb\",\n\t\t\"a\",\n\t}\n\n\tsort.Sort(ByLen(in))\n\n\tfor i, s := range in {\n\t\tif out[i] != s {\n\t\t\tt.Errorf(\"Mismatch at pos %d (%s - %s)\", i, out[i], s)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/store/store.go",
    "content": "// Package store provides the interface for the gopass password store.\n// It defines the methods and types used to interact with the password store.\npackage store\n\nimport (\n\t\"context\"\n)\n\n// RecipientCallback is a callback to verify the list of recipients.\ntype RecipientCallback func(context.Context, string, []string) ([]string, error)\n\n// ImportCallback is a callback to ask the user if they want to import\n// a certain recipients public key into their keystore.\ntype ImportCallback func(context.Context, string, []string) bool\n\n// FsckCallback is a callback to ask the user to confirm certain fsck\n// corrective actions.\ntype FsckCallback func(context.Context, string) bool\n"
  },
  {
    "path": "internal/tpl/funcs.go",
    "content": "package tpl\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/hashsum\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2i\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2id\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/bcrypt\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/jsimonetti/pwscheme/md5crypt\"\n\t\"github.com/jsimonetti/pwscheme/ssha\"\n\t\"github.com/jsimonetti/pwscheme/ssha256\"\n\t\"github.com/jsimonetti/pwscheme/ssha512\"\n)\n\n// These constants defined the template function names used.\nconst (\n\tFuncMd5sum        = \"md5sum\"\n\tFuncSha1sum       = \"sha1sum\"\n\tFuncSha256sum     = \"sha256sum\"\n\tFuncSha512sum     = \"sha512sum\"\n\tFuncBlake3        = \"blake3\"\n\tFuncMd5Crypt      = \"md5crypt\"\n\tFuncSSHA          = \"ssha\"\n\tFuncSSHA256       = \"ssha256\"\n\tFuncSSHA512       = \"ssha512\"\n\tFuncGet           = \"get\"\n\tFuncGetPassword   = \"getpw\"\n\tFuncGetValue      = \"getval\"\n\tFuncGetValues     = \"getvals\"\n\tFuncArgon2i       = \"argon2i\"\n\tFuncArgon2id      = \"argon2id\"\n\tFuncBcrypt        = \"bcrypt\"\n\tFuncJoin          = \"join\"\n\tFuncRoundDuration = \"roundDuration\"\n\tFuncDate          = \"date\"\n\tFuncTruncate      = \"truncate\"\n)\n\nfunc md5sum() func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\treturn hashsum.MD5Hex(s[0]), nil\n\t}\n}\n\nfunc sha1sum() func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\treturn hashsum.SHA1Hex(s[0]), nil\n\t}\n}\n\nfunc sha256sum() func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\treturn hashsum.SHA256Hex(s[0]), nil\n\t}\n}\n\nfunc sha512sum() func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\treturn hashsum.SHA512Hex(s[0]), nil\n\t}\n}\n\nfunc blake3sum() func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\treturn hashsum.Blake3Hex(s[0]), nil\n\t}\n}\n\n// saltLen tries to parse the given string into a numeric salt length.\nfunc saltLen(s []string) uint8 {\n\tif len(s) < 2 {\n\t\tdebug.Log(\"no salt length given, using default %d\", 32)\n\n\t\treturn 32\n\t}\n\n\ti, err := strconv.ParseUint(s[0], 10, 8)\n\tif err != nil {\n\t\tdebug.Log(\"failed to parse saltLen %+v: %q. using default: %d\", s, err, 32)\n\n\t\treturn 32\n\t}\n\n\tsl := uint8(i)\n\n\tdebug.Log(\"using saltLen %d\", sl)\n\n\treturn sl\n}\n\nfunc md5cryptFunc() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password>\", FuncMd5Crypt)\n\t\t}\n\n\t\tsl := saltLen(s)\n\t\tif sl > 8 || sl < 1 {\n\t\t\tsl = 4\n\t\t}\n\n\t\treturn md5crypt.Generate(s[len(s)-1], sl) //nolint:wrapcheck\n\t}\n}\n\nfunc sshaFunc() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password>\", FuncSSHA)\n\t\t}\n\n\t\treturn ssha.Generate(s[len(s)-1], saltLen(s)) //nolint:wrapcheck\n\t}\n}\n\nfunc ssha256Func() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password>\", FuncSSHA256)\n\t\t}\n\n\t\treturn ssha256.Generate(s[len(s)-1], saltLen(s)) //nolint:wrapcheck\n\t}\n}\n\nfunc ssha512Func() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password>\", FuncSSHA512)\n\t\t}\n\n\t\treturn ssha512.Generate(s[len(s)-1], saltLen(s)) //nolint:wrapcheck\n\t}\n}\n\nfunc argon2iFunc() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password>\", FuncArgon2i)\n\t\t}\n\n\t\treturn argon2i.Generate(s[len(s)-1], uint32(saltLen(s))) //nolint:wrapcheck\n\t}\n}\n\nfunc argon2idFunc() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <salt> <password> or <password>\", FuncArgon2id)\n\t\t}\n\n\t\treturn argon2id.Generate(s[len(s)-1], uint32(saltLen(s))) //nolint:wrapcheck\n\t}\n}\n\nfunc bcryptFunc() func(...string) (string, error) {\n\t// parameters: s[0] = salt, s[-1] = password\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", fmt.Errorf(\"usage: %s <password>\", FuncBcrypt)\n\t\t}\n\n\t\treturn bcrypt.Generate(s[len(s)-1]) //nolint:wrapcheck\n\t}\n}\n\nfunc get(ctx context.Context, kv kvstore) func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tif kv == nil {\n\t\t\treturn \"\", fmt.Errorf(\"KV is nil\")\n\t\t}\n\n\t\tsec, err := kv.Get(ctx, s[0])\n\t\tif err != nil {\n\t\t\treturn err.Error(), nil\n\t\t}\n\n\t\treturn string(sec.Bytes()), nil\n\t}\n}\n\nfunc getPassword(ctx context.Context, kv kvstore) func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 1 {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tif kv == nil {\n\t\t\treturn \"\", fmt.Errorf(\"KV is nil\")\n\t\t}\n\n\t\tsec, err := kv.Get(ctx, s[0])\n\t\tif err != nil {\n\t\t\treturn err.Error(), nil\n\t\t}\n\n\t\treturn sec.Password(), nil\n\t}\n}\n\nfunc getValue(ctx context.Context, kv kvstore) func(...string) (string, error) {\n\treturn func(s ...string) (string, error) {\n\t\tif len(s) < 2 {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tif kv == nil {\n\t\t\treturn \"\", fmt.Errorf(\"KV is nil\")\n\t\t}\n\n\t\tsec, err := kv.Get(ctx, s[0])\n\t\tif err != nil {\n\t\t\treturn err.Error(), nil\n\t\t}\n\n\t\tsv, found := sec.Get(s[1])\n\t\tif !found {\n\t\t\treturn \"\", fmt.Errorf(\"key %q not found\", s[1])\n\t\t}\n\n\t\treturn sv, nil\n\t}\n}\n\nfunc getValues(ctx context.Context, kv kvstore) func(...string) ([]string, error) {\n\treturn func(s ...string) ([]string, error) {\n\t\tif len(s) < 2 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif kv == nil {\n\t\t\treturn nil, fmt.Errorf(\"KV is nil\")\n\t\t}\n\n\t\tsec, err := kv.Get(ctx, s[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get %q: %w\", s[0], err)\n\t\t}\n\n\t\tvalues, found := sec.Values(s[1])\n\t\tif !found {\n\t\t\treturn nil, fmt.Errorf(\"key %q not found\", s[1])\n\t\t}\n\n\t\treturn values, nil\n\t}\n}\n\nfunc roundDuration(duration any) string {\n\tvar d time.Duration\n\tswitch duration := duration.(type) {\n\tcase string:\n\t\td, _ = time.ParseDuration(duration)\n\tcase int64:\n\t\td = time.Duration(duration)\n\tcase time.Time:\n\t\td = time.Since(duration)\n\tcase time.Duration:\n\t\td = duration\n\tdefault:\n\t\td = 0\n\t}\n\n\tu := uint64(d)\n\tyear := uint64(time.Hour) * 24 * 365\n\tmonth := uint64(time.Hour) * 24 * 30\n\tday := uint64(time.Hour) * 24\n\thour := uint64(time.Hour)\n\tminute := uint64(time.Minute)\n\tsecond := uint64(time.Second)\n\n\tswitch {\n\tcase u >= year:\n\t\treturn strconv.FormatUint(u/year, 10) + \"y\"\n\tcase u >= month:\n\t\treturn strconv.FormatUint(u/month, 10) + \"mo\"\n\tcase u >= day:\n\t\treturn strconv.FormatUint(u/day, 10) + \"d\"\n\tcase u >= hour:\n\t\treturn strconv.FormatUint(u/hour, 10) + \"h\"\n\tcase u >= minute:\n\t\treturn strconv.FormatUint(u/minute, 10) + \"m\"\n\tcase u >= second:\n\t\treturn strconv.FormatUint(u/second, 10) + \"s\"\n\tdefault:\n\t\treturn \"0s\"\n\t}\n}\n\nfunc date(ts time.Time) string {\n\treturn ts.Format(\"2006-01-02\")\n}\n\nfunc truncate(length int, v any) string {\n\tsv := strval(v)\n\t// we can't properly truncate to zero, so we return the full string\n\tif len(sv) < length {\n\t\treturn sv\n\t}\n\n\treturn sv[:length] + \"...\"\n}\n\nfunc join(sep string, v any) string {\n\treturn strings.Join(stringslice(v), sep)\n}\n\nfunc stringslice(v any) []string {\n\tswitch v := v.(type) {\n\tcase []string:\n\t\treturn v\n\tcase []any:\n\t\tres := make([]string, 0, len(v))\n\t\tfor _, s := range v {\n\t\t\tif s == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres = append(res, strval(s))\n\t\t}\n\n\t\treturn res\n\tdefault:\n\t\tval := reflect.ValueOf(v)\n\t\tswitch val.Kind() { //nolint:exhaustive\n\t\tcase reflect.Array, reflect.Slice:\n\t\t\tl := val.Len()\n\t\t\tres := make([]string, 0, l)\n\t\t\tfor i := range l {\n\t\t\t\tvalue := val.Index(i).Interface()\n\t\t\t\tif value == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tres = append(res, strval(value))\n\t\t\t}\n\n\t\t\treturn res\n\t\tdefault:\n\t\t\tif v == nil {\n\t\t\t\treturn []string{}\n\t\t\t}\n\n\t\t\treturn []string{strval(v)}\n\t\t}\n\t}\n}\n\nfunc strval(v any) string {\n\tswitch v := v.(type) {\n\tcase string:\n\t\treturn v\n\tcase []byte:\n\t\treturn string(v)\n\tcase error:\n\t\treturn v.Error()\n\tcase fmt.Stringer:\n\t\treturn v.String()\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", v)\n\t}\n}\n\nfunc funcMap(ctx context.Context, kv kvstore) template.FuncMap {\n\treturn template.FuncMap{\n\t\tFuncGet:           get(ctx, kv),\n\t\tFuncGetPassword:   getPassword(ctx, kv),\n\t\tFuncGetValue:      getValue(ctx, kv),\n\t\tFuncGetValues:     getValues(ctx, kv),\n\t\tFuncMd5sum:        md5sum(),\n\t\tFuncSha1sum:       sha1sum(),\n\t\tFuncSha256sum:     sha256sum(),\n\t\tFuncSha512sum:     sha512sum(),\n\t\tFuncBlake3:        blake3sum(),\n\t\tFuncMd5Crypt:      md5cryptFunc(),\n\t\tFuncSSHA:          sshaFunc(),\n\t\tFuncSSHA256:       ssha256Func(),\n\t\tFuncSSHA512:       ssha512Func(),\n\t\tFuncArgon2i:       argon2iFunc(),\n\t\tFuncArgon2id:      argon2idFunc(),\n\t\tFuncBcrypt:        bcryptFunc(),\n\t\tFuncJoin:          join,\n\t\tFuncRoundDuration: roundDuration,\n\t\tFuncDate:          date,\n\t\tFuncTruncate:      truncate,\n\t}\n}\n\n// PublicFuncMap returns a template.FuncMap with useful template functions.\nfunc PublicFuncMap() template.FuncMap {\n\treturn template.FuncMap{\n\t\tFuncMd5sum:        md5sum(),\n\t\tFuncSha1sum:       sha1sum(),\n\t\tFuncSha256sum:     sha256sum(),\n\t\tFuncSha512sum:     sha512sum(),\n\t\tFuncBlake3:        blake3sum(),\n\t\tFuncMd5Crypt:      md5cryptFunc(),\n\t\tFuncSSHA:          sshaFunc(),\n\t\tFuncSSHA256:       ssha256Func(),\n\t\tFuncSSHA512:       ssha512Func(),\n\t\tFuncArgon2i:       argon2iFunc(),\n\t\tFuncArgon2id:      argon2idFunc(),\n\t\tFuncBcrypt:        bcryptFunc(),\n\t\tFuncJoin:          join,\n\t\tFuncRoundDuration: roundDuration,\n\t\tFuncDate:          date,\n\t\tFuncTruncate:      truncate,\n\t}\n}\n"
  },
  {
    "path": "internal/tpl/funcs_test.go",
    "content": "package tpl\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/hashsum\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMd5sum(t *testing.T) {\n\tresult, err := md5sum()(\"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, hashsum.MD5Hex(\"test\"), result)\n}\n\nfunc TestSha1sum(t *testing.T) {\n\tresult, err := sha1sum()(\"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, hashsum.SHA1Hex(\"test\"), result)\n}\n\nfunc TestSha256sum(t *testing.T) {\n\tresult, err := sha256sum()(\"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, hashsum.SHA256Hex(\"test\"), result)\n}\n\nfunc TestSha512sum(t *testing.T) {\n\tresult, err := sha512sum()(\"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, hashsum.SHA512Hex(\"test\"), result)\n}\n\nfunc TestBlake3sum(t *testing.T) {\n\tresult, err := blake3sum()(\"test\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, hashsum.Blake3Hex(\"test\"), result)\n}\n\nfunc TestMd5cryptFunc(t *testing.T) {\n\tresult, err := md5cryptFunc()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{MD5-CRYPT}\"), result)\n}\n\nfunc TestSshaFunc(t *testing.T) {\n\tresult, err := sshaFunc()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{SSHA}\"), result)\n}\n\nfunc TestSsha256Func(t *testing.T) {\n\tresult, err := ssha256Func()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{SSHA256}\"), result)\n}\n\nfunc TestSsha512Func(t *testing.T) {\n\tresult, err := ssha512Func()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{SSHA512}\"), result)\n}\n\nfunc TestArgon2iFunc(t *testing.T) {\n\tresult, err := argon2iFunc()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{ARGON2I}$argon2i$\"), result)\n}\n\nfunc TestArgon2idFunc(t *testing.T) {\n\tresult, err := argon2idFunc()(\"salt\", \"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{ARGON2ID}$argon2id$\"), result)\n}\n\nfunc TestBcryptFunc(t *testing.T) {\n\tresult, err := bcryptFunc()(\"password\")\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(result, \"{BLF-CRYPT}$2a$\"))\n}\n\nfunc TestRoundDuration(t *testing.T) {\n\tassert.Equal(t, \"1h\", roundDuration(time.Hour))\n\tassert.Equal(t, \"1m\", roundDuration(time.Minute))\n\tassert.Equal(t, \"1s\", roundDuration(time.Second))\n\tassert.Equal(t, \"1d\", roundDuration(time.Hour*24))\n\tassert.Equal(t, \"1mo\", roundDuration(time.Hour*24*30))\n\tassert.Equal(t, \"1y\", roundDuration(time.Hour*24*365))\n}\n\nfunc TestDate(t *testing.T) {\n\tts := time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC)\n\tassert.Equal(t, \"2023-10-01\", date(ts))\n}\n\nfunc TestTruncate(t *testing.T) {\n\tassert.Equal(t, \"hello wo...\", truncate(8, \"hello world\"))\n\tassert.Equal(t, \"hello\", truncate(8, \"hello\"))\n\tassert.Equal(t, \"...\", truncate(0, \"hello\"))\n\tassert.Equal(t, \"h...\", truncate(1, \"hello\"))\n\tassert.Equal(t, \"he...\", truncate(2, \"hello\"))\n\tassert.Equal(t, \"hel...\", truncate(3, \"hello\"))\n\tassert.Equal(t, \"hell...\", truncate(4, \"hello\"))\n\tassert.Equal(t, \"hel...\", truncate(3, \"hel\"))\n\tassert.Equal(t, \"he...\", truncate(2, \"hel\"))\n}\n\nfunc TestJoin(t *testing.T) {\n\tassert.Equal(t, \"a,b,c\", join(\",\", []string{\"a\", \"b\", \"c\"}))\n\tassert.Equal(t, \"1,2,3\", join(\",\", []int{1, 2, 3}))\n}\n"
  },
  {
    "path": "internal/tpl/template.go",
    "content": "// Package tpl provides functions to handle templates.\n// It can parse templates from various formats and generate output for them.\npackage tpl\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"text/template\"\n\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n)\n\ntype kvstore interface {\n\tGet(context.Context, string) (gopass.Secret, error)\n}\n\ntype payload struct {\n\tDir     string\n\tDirName string\n\tPath    string\n\tName    string\n\tContent string\n}\n\n// Execute executes the given template.\nfunc Execute(ctx context.Context, tpl, name string, content []byte, s kvstore) ([]byte, error) {\n\tfuncs := funcMap(ctx, s)\n\n\tdir := filepath.Dir(name)\n\n\tpl := payload{\n\t\tDir:     dir,\n\t\tDirName: filepath.Base(dir),\n\t\tPath:    name,\n\t\tName:    filepath.Base(name),\n\t\tContent: string(content),\n\t}\n\n\ttmpl, err := template.New(tpl).Funcs(funcs).Parse(tpl)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to parse template: %w\", err)\n\t}\n\n\tbuff := &bytes.Buffer{}\n\tif err := tmpl.Execute(buff, pl); err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to execute template: %w\", err)\n\t}\n\n\treturn buff.Bytes(), nil\n}\n"
  },
  {
    "path": "internal/tpl/template_test.go",
    "content": "package tpl\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2i\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2id\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets/secparse\"\n\t\"github.com/jsimonetti/pwscheme/md5crypt\"\n\t\"github.com/jsimonetti/pwscheme/ssha\"\n\t\"github.com/jsimonetti/pwscheme/ssha256\"\n\t\"github.com/jsimonetti/pwscheme/ssha512\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Example() { //nolint:testableexamples\n\tctx := config.NewContextInMemory()\n\ttpl := `Password-value of existing entry: {{ getpw \"foo\" }}\nPassword-value of the new entry: {{ .Content }}\nMd5sum of the new password: {{ .Content | md5sum }}\nSha1sum of the new password: {{ .Content | sha1sum }}\nBlake3 of the new password: {{ .Content | blake3 }}\nMd5crypt of the new password: {{ .Content | md5crypt }}\nSSHA of the new password: {{ .Content | ssha }}\nSSHA256 of the new password: {{ .Content | ssha256 }}\nSSHA512 of the new password: {{ .Content | ssha512 }}\nArgon2i of the new password: {{ .Content | argon2i }}\nArgon2id of the new password: {{ .Content | argon2id }}\nBcrypt of the new password: {{ .Content | bcrypt }}\n`\n\tkv := kvMock{}\n\n\t// Arguments: context, template string, name of the secret, generated password, kv store\n\tbuf, err := Execute(ctx, tpl, \"example\", []byte(\"bar\"), kv)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(buf))\n}\n\ntype kvMock struct{}\n\nfunc (k kvMock) Get(ctx context.Context, key string) (gopass.Secret, error) {\n\treturn secparse.Parse([]byte(\"barfoo\\n---\\nbarkey: barvalue\\n\")) //nolint:wrapcheck\n}\n\n//nolint:gocognit\nfunc TestVars(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tkv := kvMock{}\n\n\tfor _, tc := range []struct {\n\t\tTemplate   string\n\t\tName       string\n\t\tContent    []byte\n\t\tOutput     string\n\t\tOutputFunc func(string) error\n\t\tShouldFail bool\n\t}{\n\t\t{\n\t\t\tTemplate: \"{{.Dir}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \".\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Path}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"testdir\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.DirName}}\",\n\t\t\tName:     \"foo/bar/baz\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"bar\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Name}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"testdir\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"foobar\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | md5sum}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"3858f62230ac3c915f300c664312c63f\",\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | sha1sum}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"8843d7f92416211de9ebb963ff4ce28125932878\",\n\t\t},\n\t\t{\n\t\t\tTemplate: `{{getpw \"testdir\"}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"barfoo\",\n\t\t},\n\t\t{\n\t\t\tTemplate: `{{get \"testdir\"}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"barfoo\\n---\\nbarkey: barvalue\\n\",\n\t\t},\n\t\t{\n\t\t\tTemplate: `{{getpw \"testdir\"}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"barfoo\",\n\t\t},\n\t\t{\n\t\t\tTemplate: `{{getval \"testdir\" \"barkey\"}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"barvalue\",\n\t\t},\n\t\t{\n\t\t\tTemplate:   `{{getval \"testdir\" \"barkeyINVALID\"}}`,\n\t\t\tName:       \"testdir\",\n\t\t\tContent:    []byte(\"foobar\"),\n\t\t\tOutput:     \"\",\n\t\t\tShouldFail: true,\n\t\t},\n\t\t{\n\t\t\tTemplate: `{{getvals \"testdir\" \"barkey\"}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"[barvalue]\",\n\t\t},\n\t\t{\n\t\t\tTemplate: `md5{{(print .Content .Name) | md5sum}}`,\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutput:   \"md55d952fb5e2b5c6258b044a663518349f\",\n\t\t},\n\t\t{\n\t\t\tTemplate:   `{{|}}`,\n\t\t\tName:       \"testdir\",\n\t\t\tContent:    []byte(\"foobar\"),\n\t\t\tOutput:     \"\",\n\t\t\tShouldFail: true,\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | ssha \\\"12\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{SSHA}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix\")\n\t\t\t\t}\n\n\t\t\t\tok, err := ssha.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | ssha256 \\\"12\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{SSHA256}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := ssha256.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | ssha512 \\\"12\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{SSHA512}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := ssha512.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | ssha512 \\\"-12\\\" \\\"invalid\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{SSHA512}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := ssha512.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | md5crypt \\\"7\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{MD5-CRYPT}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := md5crypt.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | md5crypt \\\"0\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{MD5-CRYPT}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := md5crypt.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | argon2i \\\"64\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{ARGON2I}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := argon2i.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate: \"{{.Content | argon2id \\\"256\\\"}}\",\n\t\t\tName:     \"testdir\",\n\t\t\tContent:  []byte(\"foobar\"),\n\t\t\tOutputFunc: func(s string) error {\n\t\t\t\tif !strings.HasPrefix(s, \"{ARGON2ID}\") {\n\t\t\t\t\treturn fmt.Errorf(\"wrong prefix: %s\", s)\n\t\t\t\t}\n\n\t\t\t\tok, err := argon2id.Validate(\"foobar\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"can't validate: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"hash mismatch\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tTemplate:   \"{{ argon2id }}\",\n\t\t\tName:       \"testdir\",\n\t\t\tContent:    []byte(\"foobar\"),\n\t\t\tShouldFail: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.Template, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tbuf, err := Execute(ctx, tc.Template, tc.Name, tc.Content, kv)\n\t\t\tif tc.ShouldFail {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tif tc.OutputFunc != nil && tc.Output != \"\" {\n\t\t\t\tt.Error(\"must not set output and output func\")\n\t\t\t}\n\t\t\tif tc.OutputFunc != nil {\n\t\t\t\trequire.NoError(t, tc.OutputFunc(string(buf)), tc.Template)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.Output, string(buf), tc.Template)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tree/node.go",
    "content": "package tree\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Node is a tree node.\ntype Node struct {\n\tName     string\n\tLeaf     bool\n\tTemplate bool\n\tMount    bool\n\tPath     string\n\tSubtree  *Tree\n}\n\nconst (\n\t// INF allows to have a full recursion until the leaves of a tree.\n\tINF = -1\n)\n\n// Nodes is a slice of nodes which can be sorted.\ntype Nodes []*Node\n\nfunc (n Nodes) Len() int {\n\treturn len(n)\n}\n\nfunc (n Nodes) Less(i, j int) bool {\n\treturn n[i].Name < n[j].Name\n}\n\nfunc (n Nodes) Swap(i, j int) {\n\tn[i], n[j] = n[j], n[i]\n}\n\n// Equals compares to another node.\nfunc (n Node) Equals(other Node) bool {\n\tif n.Name != other.Name {\n\t\treturn false\n\t}\n\n\tif n.Leaf != other.Leaf {\n\t\treturn false\n\t}\n\n\tif n.Subtree != nil {\n\t\tif other.Subtree == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tif !n.Subtree.Equals(other.Subtree) {\n\t\t\treturn false\n\t\t}\n\t} else if other.Subtree != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Merge will merge two nodes into a new node. Does not change either of the two\n// input nodes. The merged node will be in the returned node. Implements semantics\n// specific to gopass' tree model, i.e. mounts shadow (erase) everything below\n// a mount point, nodes within a tree can be leafs (i.e. contain a secret as well\n// as subdirectories) and any node can also contain a template.\nfunc (n Node) Merge(other Node) *Node {\n\tr := Node{\n\t\tName:     n.Name,\n\t\tLeaf:     n.Leaf,\n\t\tTemplate: n.Template,\n\t\tMount:    n.Mount,\n\t\tPath:     n.Path,\n\t\tSubtree:  n.Subtree,\n\t}\n\n\t// During a merge we can't change the name.\n\n\t// If either of the nodes is a leaf (i.e. contains a secret) the\n\t// merged node will be a leaf.\n\tif other.Leaf {\n\t\tr.Leaf = true\n\t}\n\n\t// If either node has a template the merged has a template, too.\n\tif other.Template {\n\t\tr.Template = true\n\t}\n\n\t// Handling of mounts is a bit more tricky. See the comment above.\n\t// If we're adding a mount to the tree this shadows (erases) everything\n\t// that was on this branch before a replaces it with the mount.\n\t// Think of Unix mount semantics here.\n\tif other.Mount {\n\t\tr.Mount = true\n\t\t// anything at the mount point, including a secret at the root\n\t\t// of the mount point will become inaccessible.\n\t\tr.Leaf = false\n\t\tr.Path = other.Path\n\t\t// existing templates will become invisible\n\t\tr.Template = false\n\t\t// the subtree from the mount overlays (shadows) the original tree\n\t\tr.Subtree = other.Subtree\n\t}\n\t// Merging can't change the path (except a mount, see above)\n\t// If the other node has a subtree we use that, otherwise\n\t// this method shouldn't have been called in the first place.\n\tif r.Subtree == nil && other.Subtree != nil {\n\t\tr.Subtree = other.Subtree\n\t}\n\n\tdebug.V(4).Log(\"merged %+v and %+v into %+v\", n, other, r)\n\n\treturn &r\n}\n\n// format returns a pretty printed string of all nodes in and below\n// this node, e.g. `├── baz`.\nfunc (n *Node) format(prefix string, last bool, maxDepth, curDepth int) string {\n\tif maxDepth > INF && (curDepth > maxDepth+1) {\n\t\treturn \"\"\n\t}\n\n\tout := bytes.NewBufferString(prefix)\n\t// adding either an L or a T, depending if this is the last node\n\t// or not\n\tif last {\n\t\t_, _ = out.WriteString(symLeaf)\n\t} else {\n\t\t_, _ = out.WriteString(symBranch)\n\t}\n\t// the next levels prefix needs to be extended depending if\n\t// this is the last node in a group or not\n\tif last {\n\t\tprefix += symEmpty\n\t} else {\n\t\tprefix += symVert\n\t}\n\n\t// any mount will be colored and include the on-disk path\n\tswitch {\n\tcase n.Mount:\n\t\t_, _ = out.WriteString(colMount(n.Name + \" (\" + n.Path + \")\"))\n\tcase n.Subtree != nil:\n\t\t_, _ = out.WriteString(colDir(n.Name + sep))\n\tdefault:\n\t\t_, _ = out.WriteString(n.Name)\n\t}\n\t// mark templates\n\tif n.Template {\n\t\t_, _ = out.WriteString(\" \" + colTpl(\"(template)\"))\n\t}\n\t// mark shadowed entries\n\tif n.Leaf && n.Subtree != nil && !n.Mount {\n\t\t_, _ = out.WriteString(\" \" + colShadow(\"(shadowed)\"))\n\t}\n\t// finish this output\n\t_, _ = out.WriteString(\"\\n\")\n\n\tif n.Subtree == nil {\n\t\treturn out.String()\n\t}\n\n\t// let our children format themselves\n\tfor i, node := range n.Subtree.Nodes {\n\t\tlast := i == len(n.Subtree.Nodes)-1\n\t\t_, _ = out.WriteString(node.format(prefix, last, maxDepth, curDepth+1))\n\t}\n\n\treturn out.String()\n}\n\n// Len returns the length of this subtree.\nfunc (n *Node) Len() int {\n\tif n.Subtree == nil {\n\t\treturn 1\n\t}\n\n\tvar l int\n\n\t// this node might point to a secret itself so we must account for that\n\tif n.Leaf {\n\t\tl++\n\t}\n\n\t// and for any secret it's subtree might contain\n\tfor _, t := range n.Subtree.Nodes {\n\t\tl += t.Len()\n\t}\n\n\treturn l\n}\n\nfunc (n *Node) list(prefix string, maxDepth, curDepth int, files bool) []string {\n\tif maxDepth >= 0 && curDepth > maxDepth {\n\t\treturn nil\n\t}\n\n\tif prefix != \"\" {\n\t\tprefix += sep\n\t}\n\n\tprefix += n.Name\n\n\tout := make([]string, 0, n.Len())\n\t// if it's a file and we are looking for files\n\tif n.Leaf && !n.Mount && files {\n\t\t// we return the file\n\t\tout = append(out, prefix)\n\t} else if curDepth == maxDepth && n.Subtree != nil {\n\t\t// otherwise if we are \"at the bottom\" and it's not a file\n\t\t// we return the directory name with a separator at the end\n\t\treturn []string{prefix + sep}\n\t}\n\n\t// if we don't have subitems, then it's a leaf and we return\n\t// (notice that this is what ends the recursion when maxDepth is set to -1)\n\tif n.Subtree == nil {\n\t\treturn out\n\t}\n\n\t// this is the part that will list the subdirectories on their own line when using the -d option\n\tif !files {\n\t\tout = append(out, prefix+sep)\n\t}\n\n\t// we keep listing the subtree nodes if we haven't exited yet.\n\tfor _, t := range n.Subtree.Nodes {\n\t\tout = append(out, t.list(prefix, maxDepth, curDepth+1, files)...)\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "internal/tree/node_test.go",
    "content": "package tree\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNodeEquals(t *testing.T) {\n\tn1 := Node{Name: \"node1\", Leaf: true}\n\tn2 := Node{Name: \"node1\", Leaf: true}\n\tn3 := Node{Name: \"node2\", Leaf: false}\n\n\tassert.True(t, n1.Equals(n2))\n\tassert.False(t, n1.Equals(n3))\n}\n\nfunc TestNodeMerge(t *testing.T) {\n\tn1 := Node{Name: \"node1\", Leaf: true, Template: true}\n\tn2 := Node{Name: \"node1\", Leaf: false, Template: false, Mount: true, Path: \"/mnt\"}\n\n\tmerged := n1.Merge(n2)\n\n\tassert.Equal(t, \"node1\", merged.Name)\n\tassert.False(t, merged.Leaf)\n\tassert.False(t, merged.Template)\n\tassert.True(t, merged.Mount)\n\tassert.Equal(t, \"/mnt\", merged.Path)\n}\n\nfunc TestNodeFormat(t *testing.T) {\n\tn := Node{Name: \"node1\", Leaf: true}\n\texpected := \"└── node1\\n\"\n\tresult := n.format(\"\", true, INF, 0)\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestNodeLen(t *testing.T) {\n\tn := Node{Name: \"node1\", Leaf: true}\n\tassert.Equal(t, 1, n.Len())\n\n\tsubtree := &Tree{Nodes: []*Node{{Name: \"child1\", Leaf: true}, {Name: \"child2\", Leaf: true}}}\n\tn.Subtree = subtree\n\tassert.Equal(t, 3, n.Len())\n}\n\nfunc TestNodeList(t *testing.T) {\n\tn := Node{Name: \"node1\", Leaf: true}\n\texpected := []string{\"node1\"}\n\tresult := n.list(\"\", INF, 0, true)\n\n\tassert.Equal(t, expected, result)\n\n\tsubtree := &Tree{Nodes: []*Node{{Name: \"child1\", Leaf: true}, {Name: \"child2\", Leaf: true}}}\n\tn.Subtree = subtree\n\texpected = []string{\"node1\", \"node1/child1\", \"node1/child2\"}\n\tresult = n.list(\"\", INF, 0, true)\n\n\tassert.Equal(t, expected, result)\n}\n"
  },
  {
    "path": "internal/tree/root.go",
    "content": "package tree\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nconst (\n\tsymEmpty  = \"    \"\n\tsymBranch = \"├── \"\n\tsymLeaf   = \"└── \"\n\tsymVert   = \"│   \"\n)\n\nvar (\n\t// ErrNotFound is returned when a node is not found.\n\tErrNotFound = fmt.Errorf(\"not found\")\n\tcolMount    = color.New(color.FgCyan, color.Bold).SprintfFunc()\n\tcolDir      = color.New(color.FgBlue, color.Bold).SprintfFunc()\n\tcolTpl      = color.New(color.FgGreen, color.Bold).SprintfFunc()\n\tcolShadow   = color.New(color.FgRed, color.Bold).SprintfFunc()\n\t// sep is intentionally NOT platform-agnostic. This is used for the CLI output\n\t// and should always be a regular slash.\n\tsep = \"/\"\n)\n\n// Root is the root of a tree. It contains a name and a subtree.\ntype Root struct {\n\tName    string\n\tSubtree *Tree\n\tPrefix  string\n}\n\n// New creates a new tree.\nfunc New(name string) *Root {\n\treturn &Root{\n\t\tName:    name,\n\t\tSubtree: NewTree(),\n\t}\n}\n\n// AddFile adds a new file to the tree.\nfunc (r *Root) AddFile(path string, _ string) error {\n\treturn r.insert(path, false, \"\")\n}\n\n// AddMount adds a new mount point to the tree.\nfunc (r *Root) AddMount(path, dest string) error {\n\treturn r.insert(path, false, dest)\n}\n\n// AddTemplate adds a template to the tree.\nfunc (r *Root) AddTemplate(path string) error {\n\treturn r.insert(path, true, \"\")\n}\n\nfunc (r *Root) insert(path string, template bool, mountPath string) error {\n\tt := r.Subtree\n\n\tdebug.V(4).Log(\"adding: %s [tpl: %t, mp: %q]\", path, template, mountPath)\n\n\t// split the path into its components, iterate over them and create\n\t// the tree structure. Everything but the last element is a folder.\n\tp := strings.Split(path, \"/\")\n\tfor i, e := range p {\n\t\tn := &Node{\n\t\t\tName:    e,\n\t\t\tSubtree: NewTree(),\n\t\t}\n\t\t// this is the final element (a leaf)\n\t\tif i == len(p)-1 {\n\t\t\tn.Leaf = true\n\t\t\tn.Subtree = nil\n\t\t\tn.Template = template\n\n\t\t\tif mountPath != \"\" {\n\t\t\t\tn.Mount = true\n\t\t\t\tn.Path = mountPath\n\t\t\t}\n\t\t}\n\n\t\tdebug.V(4).Log(\"[%d] %s -> Node: %+v\", i, e, n)\n\n\t\tnode := t.Insert(n)\n\t\tdebug.V(4).Log(\"node after insert: %+v\", node)\n\n\t\t// do we need to extend an existing subtree?\n\t\tif i < len(p)-1 && node.Subtree == nil {\n\t\t\tnode.Subtree = NewTree()\n\t\t}\n\n\t\t// re-root t to the new subtree\n\t\tt = node.Subtree\n\t}\n\n\treturn nil\n}\n\n// Format returns a pretty printed string of all nodes in and below\n// this node, e.g. `├── baz`.\nfunc (r *Root) Format(maxDepth int) string {\n\tvar sb strings.Builder\n\n\t// any mount will be colored and include the on-disk path\n\t_, _ = sb.WriteString(colDir(r.Name))\n\n\t// finish this folders output\n\t_, _ = sb.WriteString(\"\\n\")\n\n\t// let our children format themselves\n\tfor i, node := range r.Subtree.Nodes {\n\t\tlast := i == len(r.Subtree.Nodes)-1\n\t\t_, _ = sb.WriteString(node.format(\"\", last, maxDepth, 1))\n\t}\n\n\treturn sb.String()\n}\n\n// List returns a flat list of all files in this tree.\nfunc (r *Root) List(maxDepth int) []string {\n\tout := make([]string, 0, r.Len())\n\tfor _, t := range r.Subtree.Nodes {\n\t\tout = append(out, t.list(r.Prefix, maxDepth, 0, true)...)\n\t}\n\n\treturn out\n}\n\n// ListFolders returns a flat list of all folders in this tree.\nfunc (r *Root) ListFolders(maxDepth int) []string {\n\tout := make([]string, 0, r.Len())\n\tfor _, t := range r.Subtree.Nodes {\n\t\tout = append(out, t.list(r.Prefix, maxDepth, 0, false)...)\n\t}\n\n\treturn out\n}\n\n// String returns the name of this tree.\nfunc (r *Root) String() string {\n\treturn r.Name\n}\n\n// FindFolder returns the subtree rooted at path.\nfunc (r *Root) FindFolder(path string) (*Root, error) {\n\tpath = strings.TrimSuffix(path, \"/\")\n\tt := r.Subtree\n\tp := strings.Split(path, \"/\")\n\tprefix := \"\"\n\n\tfor _, e := range p {\n\t\t_, node := t.findPositionFor(e)\n\t\tif node == nil || node.Subtree == nil {\n\t\t\treturn nil, ErrNotFound\n\t\t}\n\n\t\tt = node.Subtree\n\t\tprefix = filepath.Join(prefix, e)\n\t}\n\n\treturn &Root{Name: r.Name, Subtree: t, Prefix: prefix}, nil\n}\n\n// SetName changes the name of this tree.\nfunc (r *Root) SetName(n string) {\n\tr.Name = n\n}\n\n// Len returns the number of entries in this folder and all subfolder including\n// this folder itself.\nfunc (r *Root) Len() int {\n\tvar l int\n\tfor _, t := range r.Subtree.Nodes {\n\t\tl += t.Len()\n\t}\n\n\treturn l\n}\n"
  },
  {
    "path": "internal/tree/root_test.go",
    "content": "package tree\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRoot(t *testing.T) {\n\tt.Parallel()\n\n\tcolor.NoColor = true\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddTemplate(\"foo\"))\n\trequire.NoError(t, r.AddFile(\"foo/bar/baz\", \"\"))\n\trequire.NoError(t, r.AddFile(\"foo/bar/zab\", \"\"))\n\trequire.NoError(t, r.AddMount(\"mnt/m1\", \"/tmp/m1\"))\n\trequire.NoError(t, r.AddFile(\"mnt/m1/foo\", \"\"))\n\trequire.NoError(t, r.AddFile(\"mnt/m1/foo/bar\", \"\"))\n\tt.Logf(\"%+#v\", r)\n\tassert.Equal(t, `gopass\n├── foo/ (template) (shadowed)\n│   └── bar/\n│       ├── baz\n│       └── zab\n└── mnt/\n    └── m1 (/tmp/m1)\n        └── foo/ (shadowed)\n            └── bar\n`, r.Format(INF))\n\n\tassert.Equal(t, []string{\n\t\t\"foo\",\n\t\t\"foo/bar/baz\",\n\t\t\"foo/bar/zab\",\n\t\t\"mnt/m1/foo\",\n\t\t\"mnt/m1/foo/bar\",\n\t}, r.List(INF))\n\tassert.Equal(t, []string{\n\t\t\"foo/\",\n\t\t\"foo/bar/\",\n\t\t\"mnt/\",\n\t\t\"mnt/m1/\",\n\t\t\"mnt/m1/foo/\",\n\t}, r.ListFolders(INF))\n\n\tf, err := r.FindFolder(\"mnt/m1\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, `gopass\n└── foo/ (shadowed)\n    └── bar\n`, f.Format(INF))\n}\n\nfunc TestMountShadow(t *testing.T) {\n\tt.Parallel()\n\n\tcolor.NoColor = true\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddTemplate(\"foo\"))\n\trequire.NoError(t, r.AddFile(\"foo/bar/baz\", \"\"))\n\trequire.NoError(t, r.AddFile(\"foo/bar/zab\", \"\"))\n\trequire.NoError(t, r.AddMount(\"foo\", \"/tmp/m1\"))\n\trequire.NoError(t, r.AddFile(\"foo/zab\", \"\"))\n\trequire.NoError(t, r.AddFile(\"foo/baz\", \"\"))\n\tt.Logf(\"%+#v\", r)\n\tassert.Equal(t, `gopass\n└── foo (/tmp/m1)\n    ├── baz\n    └── zab\n`, r.Format(INF))\n\n\tassert.Equal(t, []string{\n\t\t\"foo/baz\",\n\t\t\"foo/zab\",\n\t}, r.List(INF))\n\tassert.Equal(t, []string{\n\t\t\"foo/\",\n\t}, r.ListFolders(INF))\n\n\t_, err := r.FindFolder(\"mnt/m1\")\n\trequire.Error(t, err)\n}\n\nfunc TestAddFile(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddFile(\"foo/bar/baz\", \"\"))\n\tassert.Equal(t, []string{\"foo/bar/baz\"}, r.List(INF))\n}\n\nfunc TestAddTemplate(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddTemplate(\"foo\"))\n\tassert.Equal(t, []string{\"foo\"}, r.List(INF))\n}\n\nfunc TestAddMount(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddMount(\"mnt/m1\", \"/tmp/m1\"))\n\t// empty mounts don't show up in the list, so we need to add a file\n\trequire.NoError(t, r.AddFile(\"mnt/m1/baz\", \"\"))\n\tassert.Equal(t, []string{\"mnt/m1/baz\"}, r.List(INF))\n}\n\nfunc TestFindFolderNotFound(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\t_, err := r.FindFolder(\"nonexistent\")\n\trequire.Error(t, err)\n\tassert.Equal(t, ErrNotFound, err)\n}\n\nfunc TestSetName(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\tr.SetName(\"newname\")\n\tassert.Equal(t, \"newname\", r.Name)\n}\n\nfunc TestLen(t *testing.T) {\n\tt.Parallel()\n\n\tr := New(\"gopass\")\n\trequire.NoError(t, r.AddFile(\"foo/bar/baz\", \"\"))\n\tassert.Equal(t, 1, r.Len())\n}\n"
  },
  {
    "path": "internal/tree/tree.go",
    "content": "// Package tree implements a tree for displaying hierarchical\n// password store entries. It is loosely based on\n// https://github.com/restic/restic/blob/master/internal/restic/tree.go\npackage tree\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n)\n\n// ErrNodePresent is returned when a node with the same name is already present.\nvar ErrNodePresent = fmt.Errorf(\"node already present\")\n\n// Tree is a tree.\ntype Tree struct {\n\tNodes []*Node\n}\n\n// NewTree creates a new tree.\nfunc NewTree() *Tree {\n\treturn &Tree{\n\t\tNodes: []*Node{},\n\t}\n}\n\n// String returns the name of this tree.\nfunc (t *Tree) String() string {\n\treturn fmt.Sprintf(\"Tree<%d nodes>\", len(t.Nodes))\n}\n\n// Equals compares to another tree.\nfunc (t *Tree) Equals(other *Tree) bool {\n\tif len(t.Nodes) != len(other.Nodes) {\n\t\treturn false\n\t}\n\n\tfor i, node := range t.Nodes {\n\t\tif !node.Equals(*other.Nodes[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Insert adds a new node at the right position.\nfunc (t *Tree) Insert(other *Node) *Node {\n\tpos, node := t.findPositionFor(other.Name)\n\tif node != nil {\n\t\tm := node.Merge(*other)\n\t\tt.Nodes[pos] = m\n\n\t\treturn m\n\t}\n\n\t// insert at the right position, see\n\t// https://code.google.com/p/go-wiki/wiki/SliceTricks\n\tt.Nodes = append(t.Nodes, &Node{})\n\tcopy(t.Nodes[pos+1:], t.Nodes[pos:])\n\tt.Nodes[pos] = other\n\n\treturn other\n}\n\nfunc (t *Tree) findPositionFor(name string) (int, *Node) {\n\tpos := sort.Search(len(t.Nodes), func(i int) bool {\n\t\treturn t.Nodes[i].Name >= name\n\t})\n\n\tif pos < len(t.Nodes) && t.Nodes[pos].Name == name {\n\t\treturn pos, t.Nodes[pos]\n\t}\n\n\treturn pos, nil\n}\n\n// Sort ensures this tree is sorted.\nfunc (t *Tree) Sort() {\n\tlist := Nodes(t.Nodes)\n\tsort.Sort(list)\n\tt.Nodes = list\n}\n"
  },
  {
    "path": "internal/tree/tree_test.go",
    "content": "package tree\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTree(t *testing.T) {\n\tt.Parallel()\n\n\tt1 := NewTree()\n\tt2 := NewTree()\n\n\tassert.True(t, t1.Equals(t2))\n\n\t_ = t1.Insert(&Node{Name: \"foo\"})\n\tassert.False(t, t1.Equals(t2))\n\n\t_ = t2.Insert(&Node{Name: \"foo\"})\n\tassert.True(t, t1.Equals(t2))\n}\n\nfunc TestTreeInsert(t *testing.T) {\n\tt.Parallel()\n\n\ttree := NewTree()\n\tnode := &Node{Name: \"foo\"}\n\n\tinsertedNode := tree.Insert(node)\n\tassert.Equal(t, node, insertedNode)\n\tassert.Len(t, tree.Nodes, 1)\n\tassert.Equal(t, \"foo\", tree.Nodes[0].Name)\n}\n\nfunc TestTreeString(t *testing.T) {\n\tt.Parallel()\n\n\ttree := NewTree()\n\tassert.Equal(t, \"Tree<0 nodes>\", tree.String())\n\n\ttree.Insert(&Node{Name: \"foo\"})\n\tassert.Equal(t, \"Tree<1 nodes>\", tree.String())\n}\n\nfunc TestTreeSort(t *testing.T) {\n\tt.Parallel()\n\n\ttree := NewTree()\n\ttree.Insert(&Node{Name: \"foo\"})\n\ttree.Insert(&Node{Name: \"bar\"})\n\ttree.Insert(&Node{Name: \"baz\"})\n\n\ttree.Sort()\n\n\tassert.Equal(t, \"bar\", tree.Nodes[0].Name)\n\tassert.Equal(t, \"baz\", tree.Nodes[1].Name)\n\tassert.Equal(t, \"foo\", tree.Nodes[2].Name)\n}\n\nfunc TestTreeFindPositionFor(t *testing.T) {\n\tt.Parallel()\n\n\ttree := NewTree()\n\ttree.Insert(&Node{Name: \"foo\"})\n\ttree.Insert(&Node{Name: \"bar\"})\n\n\tpos, node := tree.findPositionFor(\"foo\")\n\tassert.Equal(t, 1, pos)\n\tassert.NotNil(t, node)\n\tassert.Equal(t, \"foo\", node.Name)\n\n\tpos, node = tree.findPositionFor(\"bar\")\n\tassert.Equal(t, 0, pos)\n\tassert.NotNil(t, node)\n\n\t// does not exist\n\tpos, node = tree.findPositionFor(\"baz\")\n\tassert.Equal(t, 1, pos)\n\tassert.Nil(t, node)\n}\n"
  },
  {
    "path": "internal/updater/README.md",
    "content": "# Release signing and key rollover documentation\n\nAudience: core maintainers\n\nThis document captures the necessary steps to perform regular (usually every 2nd year) release signing key rollover.\n\n## Updating the gopass binary\n\nThe gopass self-updater is invoked when calling `gopass update`. It works only if the binary is writable by the user running the command. It is specifically not designed to update any gopass\npackages installed by a package manager.\n\nThe updater first tries to ensure that it is supposed to update the binary (usually because it can write to the binary location) and then fetches the latest release from GitHub. If this ever causes trouble we could cache this info and proxy requests through gopass.pw.\n\nIf there is a new release it will fetch both `SHA256SUMS` and `SHA256SUMS.sig` assets from the latest release and verify the signature matches one of the built-in updater keys.\n\nIf the checksum file is verified we continue to fetch the actual binary archive and compare that against\nthe (verified) checksum file and replace the binary.\n\nAll of this is implemented by the files in this directory.\n\n## Publishing assets for the updater during releases\n\nWhen a new release is cut we rely on GoReleaser and GitHub Action workflows to update all necessary assets.\nThe configuration for those is spread across the repository and the GitHub Action configuration.\n\nA new release is published by pushing a version tag (`v*`) to the repository. Once that happens the GHA workflow `autorelease.yml` is kicked off. It is configured in `.github/workflows/autorelease.yml` and through a number of injected environment variables from the GHA settings. Most importantly `GPG_PRIVATE_KEY` which contains the armored\nGPG private part of the current release signing key and the respective passphrase in `PASSPHRASE`.\n\nGoReleaser is controlled by `.goreleaser.yml` in the root of this repository. The relevant sections there are `checksum` to ensure a checksum file is generated and the `signs` section to sign the checksum file using the provided `GPG_FINGERPRINT` in the workflow.\n\n## Managing keys and related assets\n\nThe relase signing key is set to expire every other year, so we need to follow a certain key rotation protocol to allow for a seamless key rotation.\n\n* At T-6 Month we should notice that `TestGPGVerifyIn6Months` starts to fail.\n    * There is likely a loss obstrusive way to achieve that, but I'll leave it at that for now.\n* We should then create an issue to track the key rollover (this should never happen in secret). The entire security posture isn't perfect but that's the best I can do with my resources. Help always appreciated.\n* For the actual rollout we first need to generate a new key. That needs to be done by exactly one core maintainer with write access to the repo and the GHA secrets since only they can inject the new key, fingerprint and passphrase.\n* To generate the key run: `gpg --expert --full-generate-key` and select `RSA and RSA`, `3072` (bits) and a validity of `2y`. Use `Gopass Release Signing Key YYYY` as the name, `GitHub Actions only` as the comment and `release@gopass.pw` as the email.\n  * Note: If you're correctly following the 6 Months advance notice process, use the next year for the name.\n  * RSA isn't perfect but we used to have some compatability issues with non-RSA keys. Feel free to revisit this in the future. Keep the keep size to a reasonable value. Last time I checked 4096 did seem a bit excessive and with different algorithms these numbers will need to change as well. In doubt the `BSI TR-02102-1` should have a reasonable recommendation.\n  * Use a strong, random passphrase. Since you should never have to type it anywhere make it long, cryptic and stop worrying about it, e.g. `gopass pwgen 32`.\n* If you are me, you should probably also save a copy of both parts of new key into your gopass maintainer password store. For convenience add the Key ID / Fingerprint into the secret so you don't have to import the key just to get the Key ID.\n  * Hint: `gpg --output 0xKEYID.pub --armor --export 0xKEYID` and `gpg --output 0xKEYID.private --armor --export-secret-key 0xKEYID`\n* You should also sign the new key with the old key and possibly your personal key and push it to some keyservers.\n  * Use `gpg --yes -u 0xOLDKEYID --sign-key 0xKEYID` to sign.\n* Now export the public key and inject it into the pubkeys slice in `verify.go`. Add a comment with the year and the key id.\n* As usual send a PR and get this merged. Consider kicking off a new release, if that makes sense.\n* STOP HERE. For a seamless key rollover we need to wait until most users had a chance to update to a version that has both the old and the new keys. So if possible wait a few months at least. Keep the GH issue open and assigned to track that process.\n* After ~5 Months continue here.\n* Regenerate the test signature for verify_test.go\n  * Create a file that contains `gopass-sign-test\\n` and run `gpg -u 0xKEYID --armor --output /tmp/testdata.sig --detatch-sign testdata`. Use the correct KEYID (the one of the NEW key).\n  * Hint: Make sure the input only contains one line break, not two.\n  * Paste the content of /tmp/testdata.sig into the `testSignature` in `verify_test.go`. Make sure all tests pass.\n* Navigate to https://github.com/gopasspw/gopass/settings/secrets/actions\n    * Paste the armored private part of the new key into the existing `GPG_PRIVATE_KEY` secret.\n    * Paste the corresponding passphrase into `PASSPHRASE`.\n* At this point you should be able to safely delete the old public key from verify.go and kick off a new release.\n* At the very end upload the new key to some keyservers:  `gpg --send-keys 0xKEYID` and possibly `gpg --keyserver pgp.mit.edu --send-keys 0xKEYID`.\n  * In case you mess up during key generation you might need to start over and you don't want to have conflicting keys on a keyserver where you can't delete them.\n"
  },
  {
    "path": "internal/updater/access_others.go",
    "content": "//go:build !windows\n\npackage updater\n\nimport \"golang.org/x/sys/unix\"\n\nfunc canWrite(path string) error {\n\treturn unix.Access(path, unix.W_OK) //nolint:wrapcheck\n}\n\nfunc removeOldBinary(dir, dest string) error {\n\t// no need, os.Rename will replace the destination\n\treturn nil\n}\n"
  },
  {
    "path": "internal/updater/access_windows.go",
    "content": "//go:build windows\n\npackage updater\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc canWrite(path string) error {\n\treturn nil\n}\n\n// Windows won't allow us to remove the binary that's currently being executed.\n// So rename the binary and then the updater should be able to write it's\n// update to the correct location.\n//\n// See https://stackoverflow.com/a/459860\nfunc removeOldBinary(dir, dest string) error {\n\tbakFile := filepath.Join(dir, filepath.Base(dest)+\".bak\")\n\t// check if the bakup file already exists\n\tif _, err := os.Stat(bakFile); err == nil {\n\t\t// ... then remove it\n\t\t_ = os.Remove(bakFile)\n\t}\n\t// we can't remove the currently running binary, but should be able to\n\t// rename it.\n\tif err := os.Rename(dest, bakFile); err != nil {\n\t\treturn fmt.Errorf(\"unable to rename %s to %s: %w\", dest, bakFile, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/updater/download.go",
    "content": "package updater\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"golang.org/x/net/context/ctxhttp\"\n)\n\nvar (\n\t// DownloadTimeout is the overall timeout for the download, including all retries.\n\tDownloadTimeout = time.Minute * 5\n\thttpClient      = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\t// enforce TLS 1.3\n\t\t\t\tMinVersion: tls.VersionTLS13,\n\t\t\t},\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t},\n\t}\n)\n\nfunc tryDownload(ctx context.Context, url string) ([]byte, error) {\n\tctx, cancel := context.WithTimeout(ctx, DownloadTimeout)\n\tdefer cancel()\n\n\tbo := backoff.NewExponentialBackOff()\n\tbo.MaxElapsedTime = DownloadTimeout\n\n\tvar buf []byte\n\n\t//nolint:wrapcheck\n\treturn buf, backoff.Retry(func() error {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn backoff.Permanent(fmt.Errorf(\"user aborted\"))\n\t\tdefault:\n\t\t}\n\t\td, err := download(ctx, url)\n\t\tif err == nil {\n\t\t\tbuf = d\n\t\t}\n\n\t\treturn err\n\t}, bo)\n}\n\nfunc download(ctx context.Context, url string) ([]byte, error) {\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\t// we want binary data, please\n\treq.Header.Set(\"Accept\", \"application/octet-stream\")\n\n\tt0 := time.Now()\n\n\tresp, err := ctxhttp.Do(ctx, httpClient, req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to download %s: %w\", url, err)\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tvar body io.ReadCloser\n\t// do not show progress bar for small assets, like SHA256SUMS\n\tbar := termio.NewProgressBar(resp.ContentLength)\n\tbar.Hidden = ctxutil.IsHidden(ctx) || resp.ContentLength < 10000\n\n\tbody = &passThru{\n\t\tReadCloser: resp.Body,\n\t\tBar:        bar,\n\t}\n\n\tbuf := &bytes.Buffer{}\n\n\tcount, err := io.Copy(buf, body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response body: %w\", err)\n\t}\n\n\tbar.Set(resp.ContentLength)\n\tbar.Done()\n\n\telapsed := time.Since(t0)\n\tdebug.Log(\"Transferred %d bytes from %q in %s\", count, url, elapsed)\n\n\treturn buf.Bytes(), nil\n}\n\ntype setter interface {\n\tSet(int64)\n}\n\ntype passThru struct {\n\tio.ReadCloser\n\tBar setter\n}\n\nfunc (pt *passThru) Read(p []byte) (int, error) {\n\tn, err := pt.ReadCloser.Read(p)\n\tif pt.Bar != nil && n > 0 {\n\t\tpt.Bar.Set(int64(n))\n\t}\n\n\treturn n, err //nolint:wrapcheck\n}\n"
  },
  {
    "path": "internal/updater/extract.go",
    "content": "package updater\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"compress/bzip2\"\n\t\"compress/gzip\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nfunc extractFile(buf []byte, filename, dest string) error {\n\tmode := os.FileMode(0o755)\n\tdir := filepath.Dir(dest)\n\n\t// if overwriting an existing binary retain it's mode flags\n\tfi, err := os.Lstat(dest)\n\tif err == nil {\n\t\tmode = fi.Mode()\n\t}\n\n\ttfn, err := extractToTempFile(buf, filename, dest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extract update to %s: %w\", dest, err)\n\t}\n\n\tif err := removeOldBinary(dir, dest); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove old binary %s: %w\", dest, err)\n\t}\n\n\tif err := os.Rename(tfn, dest); err != nil {\n\t\treturn fmt.Errorf(\"failed to rename tempfile %s to %s: %w\", tfn, dest, err)\n\t}\n\n\treturn os.Chmod(dest, mode)\n}\n\nfunc extractToTempFile(buf []byte, filename, dest string) (string, error) {\n\t// open a temp file for writing\n\tdir := filepath.Dir(dest)\n\tdfh, err := os.CreateTemp(dir, \"gopass\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file in %s: %w\", dir, err)\n\t}\n\n\tdefer func() {\n\t\t_ = dfh.Sync()\n\t\t_ = dfh.Close()\n\t}()\n\n\tvar rd io.Reader = bytes.NewReader(buf)\n\n\tswitch filepath.Ext(filename) {\n\tcase \".gz\":\n\t\tgzr, err := gzip.NewReader(rd)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to open gzip file: %w\", err)\n\t\t}\n\n\t\treturn extractTar(gzr, dfh, dfh.Name())\n\tcase \".bz2\":\n\t\treturn extractTar(bzip2.NewReader(rd), dfh, dfh.Name())\n\tcase \".zip\":\n\t\treturn extractZip(buf, dfh, dfh.Name())\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unsupported file extension: %q\", filepath.Ext(filename))\n\t}\n}\n\nfunc extractZip(buf []byte, dfh io.WriteCloser, dest string) (string, error) {\n\tzrd, err := zip.NewReader(bytes.NewReader(buf), int64(len(buf)))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open zip file: %w\", err)\n\t}\n\n\tfor i := range len(zrd.File) {\n\t\tif zrd.File[i].Name != \"gopass.exe\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfile, err := zrd.File[i].Open()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read from zip file: %w\", err)\n\t\t}\n\n\t\tn, err := io.Copy(dfh, file)\n\t\tif err != nil {\n\t\t\t_ = dfh.Close()\n\t\t\t_ = os.Remove(dest)\n\n\t\t\treturn \"\", fmt.Errorf(\"failed to read gopass.exe from zip file: %w\", err)\n\t\t}\n\t\t// success\n\t\tdebug.Log(\"wrote %d bytes to %v\", n, dest)\n\n\t\treturn dest, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"file not found in archive\")\n}\n\nfunc extractTar(rd io.Reader, dfh io.WriteCloser, dest string) (string, error) {\n\ttarReader := tar.NewReader(rd)\n\n\tfor {\n\t\theader, err := tarReader.Next()\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read from tar file: %w\", err)\n\t\t}\n\n\t\tname := filepath.Base(header.Name)\n\n\t\tif header.Typeflag != tar.TypeReg {\n\t\t\tcontinue\n\t\t}\n\n\t\tif name != \"gopass\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tn, err := io.Copy(dfh, tarReader)\n\t\tif err != nil {\n\t\t\t_ = dfh.Close()\n\t\t\t_ = os.Remove(dest)\n\n\t\t\treturn \"\", fmt.Errorf(\"failed to read gopass from tar file: %w\", err)\n\t\t}\n\t\t// success\n\t\tdebug.Log(\"wrote %d bytes to %v\", n, dest)\n\n\t\treturn dest, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"file not found in archive\")\n}\n"
  },
  {
    "path": "internal/updater/extract_test.go",
    "content": "//go:build !windows\n\npackage updater\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExtractFile(t *testing.T) {\n\t// Create a temporary directory for testing\n\ttempDir := t.TempDir()\n\tdest := filepath.Join(tempDir, \"gopass\")\n\n\t// Create a sample gzip file\n\tvar buf bytes.Buffer\n\tgz := gzip.NewWriter(&buf)\n\ttw := tar.NewWriter(gz)\n\terr := tw.WriteHeader(&tar.Header{\n\t\tName: \"gopass\",\n\t\tMode: 0o600,\n\t\tSize: int64(len(\"test content\")),\n\t})\n\trequire.NoError(t, err)\n\t_, err = tw.Write([]byte(\"test content\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, tw.Close())\n\trequire.NoError(t, gz.Close())\n\n\terr = extractFile(buf.Bytes(), \"gopass.gz\", dest)\n\trequire.NoError(t, err)\n\n\tcontent, err := os.ReadFile(dest)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test content\", string(content))\n}\n\nfunc TestExtractToTempFile(t *testing.T) {\n\t// Create a temporary directory for testing\n\ttempDir := t.TempDir()\n\tdest := filepath.Join(tempDir, \"gopass\")\n\n\t// Create a sample gzip file\n\tvar buf bytes.Buffer\n\tgz := gzip.NewWriter(&buf)\n\ttw := tar.NewWriter(gz)\n\terr := tw.WriteHeader(&tar.Header{\n\t\tName: \"gopass\",\n\t\tMode: 0o600,\n\t\tSize: int64(len(\"test content\")),\n\t})\n\trequire.NoError(t, err)\n\t_, err = tw.Write([]byte(\"test content\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, tw.Close())\n\trequire.NoError(t, gz.Close())\n\n\ttempFile, err := extractToTempFile(buf.Bytes(), \"gopass.gz\", dest)\n\trequire.NoError(t, err)\n\n\tcontent, err := os.ReadFile(tempFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test content\", string(content))\n}\n"
  },
  {
    "path": "internal/updater/github.go",
    "content": "package updater\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"golang.org/x/net/context/ctxhttp\"\n)\n\nvar (\n\t// APITimeout is how long we wait for the GitHub API.\n\tAPITimeout = 30 * time.Second\n\n\t// BaseURL is exported for tests.\n\tBaseURL    = \"https://api.github.com/repos/%s/%s/releases/latest\"\n\tgitHubOrg  = \"gopasspw\"\n\tgitHubRepo = \"gopass\"\n)\n\n// Asset is a GitHub release asset.\ntype Asset struct {\n\tID   int    `json:\"id\"`\n\tName string `json:\"name\"`\n\tURL  string `json:\"browser_download_url\"`\n}\n\n// Release is a GitHub release.\ntype Release struct {\n\tID          int            `json:\"id\"`\n\tName        string         `json:\"name\"`\n\tTagName     string         `json:\"tag_name\"`\n\tDraft       bool           `json:\"draft\"`\n\tPrerelease  bool           `json:\"prerelease\"`\n\tPublishedAt time.Time      `json:\"published_at\"`\n\tAssets      []Asset        `json:\"assets\"`\n\tVersion     semver.Version `json:\"-\"`\n}\n\nfunc downloadAsset(ctx context.Context, assets []Asset, suffix string) (string, []byte, error) {\n\tvar url string\n\n\tvar filename string\n\n\tfor _, a := range assets {\n\t\tif !strings.HasSuffix(a.Name, suffix) {\n\t\t\tcontinue\n\t\t}\n\n\t\turl = a.URL\n\t\tfilename = a.Name\n\n\t\tbreak\n\t}\n\n\tif url == \"\" {\n\t\treturn \"\", nil, fmt.Errorf(\"asset with suffix %q not found\", suffix)\n\t}\n\n\tbuf, err := tryDownload(ctx, url)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\treturn filename, buf, nil\n}\n\n// FetchLatestRelease fetches meta-data about the latest Gopass release\n// from GitHub.\nfunc FetchLatestRelease(ctx context.Context) (Release, error) {\n\towner := gitHubOrg\n\trepo := gitHubRepo\n\n\tctx, cancel := context.WithTimeout(ctx, APITimeout)\n\tdefer cancel()\n\n\turl := fmt.Sprintf(BaseURL, owner, repo)\n\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn Release{}, nil\n\t}\n\n\t// pin to API version 3 to avoid breaking our structs\n\treq.Header.Set(\"Accept\", \"application/vnd.github.v3+json\")\n\n\tresp, err := ctxhttp.Do(ctx, httpClient, req)\n\tif err != nil {\n\t\treturn Release{}, fmt.Errorf(\"HTTP request failed: %w\", err)\n\t}\n\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn Release{}, fmt.Errorf(\"request faild with %v (%v)\", resp.StatusCode, resp.Status)\n\t}\n\n\tvar rs Release\n\tif err := json.NewDecoder(resp.Body).Decode(&rs); err != nil {\n\t\treturn rs, fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\n\tif !strings.HasPrefix(rs.TagName, \"v\") {\n\t\treturn rs, fmt.Errorf(\"tag name %q is invalid, must start with 'v'\", rs.TagName)\n\t}\n\n\tv, err := semver.Parse(rs.TagName[1:])\n\tif err != nil {\n\t\treturn rs, fmt.Errorf(\"failed to parse version %q: %w\", rs.TagName[1:], err)\n\t}\n\n\trs.Version = v\n\n\treturn rs, nil\n}\n"
  },
  {
    "path": "internal/updater/github_test.go",
    "content": "package updater\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFetchLatestRelease(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tresponseBody   string\n\t\tresponseStatus int\n\t\texpectedError  bool\n\t\texpectedTag    string\n\t}{\n\t\t{\n\t\t\tname: \"successful fetch\",\n\t\t\tresponseBody: `{\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"name\": \"v1.0.0\",\n\t\t\t\t\"tag_name\": \"v1.0.0\",\n\t\t\t\t\"draft\": false,\n\t\t\t\t\"prerelease\": false,\n\t\t\t\t\"published_at\": \"2021-01-01T00:00:00Z\",\n\t\t\t\t\"assets\": []\n\t\t\t}`,\n\t\t\tresponseStatus: http.StatusOK,\n\t\t\texpectedError:  false,\n\t\t\texpectedTag:    \"v1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:           \"invalid status code\",\n\t\t\tresponseBody:   \"\",\n\t\t\tresponseStatus: http.StatusInternalServerError,\n\t\t\texpectedError:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid tag name\",\n\t\t\tresponseBody: `{\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"name\": \"1.0.0\",\n\t\t\t\t\"tag_name\": \"1.0.0\",\n\t\t\t\t\"draft\": false,\n\t\t\t\t\"prerelease\": false,\n\t\t\t\t\"published_at\": \"2021-01-01T00:00:00Z\",\n\t\t\t\t\"assets\": []\n\t\t\t}`,\n\t\t\tresponseStatus: http.StatusOK,\n\t\t\texpectedError:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid JSON\",\n\t\t\tresponseBody: `{\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"name\": \"v1.0.0\",\n\t\t\t\t\"tag_name\": \"v1.0.0\",\n\t\t\t\t\"draft\": false,\n\t\t\t\t\"prerelease\": false,\n\t\t\t\t\"published_at\": \"2021-01-01T00:00:00Z\",\n\t\t\t\t\"assets\": [`,\n\t\t\tresponseStatus: http.StatusOK,\n\t\t\texpectedError:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(tt.responseStatus)\n\t\t\t\t_, _ = w.Write([]byte(tt.responseBody))\n\t\t\t}))\n\t\t\tdefer server.Close()\n\n\t\t\tBaseURL = server.URL + \"/repos/%s/%s/releases/latest\"\n\n\t\t\tctx := t.Context()\n\t\t\trelease, err := FetchLatestRelease(ctx)\n\n\t\t\tif tt.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedTag, release.TagName)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDownloadAsset(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tassets        []Asset\n\t\tsuffix        string\n\t\texpectedError bool\n\t\texpectedName  string\n\t}{\n\t\t{\n\t\t\tname: \"successful download\",\n\t\t\tassets: []Asset{\n\t\t\t\t{Name: \"asset1.txt\", URL: \"/asset1.txt\"},\n\t\t\t\t{Name: \"asset2.txt\", URL: \"/asset2.txt\"},\n\t\t\t},\n\t\t\tsuffix:        \".txt\",\n\t\t\texpectedError: false,\n\t\t\texpectedName:  \"asset1.txt\",\n\t\t},\n\t\t{\n\t\t\tname: \"asset not found\",\n\t\t\tassets: []Asset{\n\t\t\t\t{Name: \"asset1.bin\", URL: \"/asset1.bin\"},\n\t\t\t},\n\t\t\tsuffix:        \".txt\",\n\t\t\texpectedError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t_, _ = w.Write([]byte(\"test content\"))\n\t\t\t}))\n\t\t\tdefer server.Close()\n\n\t\t\tfor i, a := range tt.assets {\n\t\t\t\ttt.assets[i].URL = server.URL + a.URL\n\t\t\t}\n\n\t\t\tctx := t.Context()\n\n\t\t\tname, _, err := downloadAsset(ctx, tt.assets, tt.suffix)\n\n\t\t\tif tt.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedName, name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/updater/update.go",
    "content": "// Package updater provides a simple update mechanism for gopass.\n// It will check for updates, download the latest release and\n// verify the GPG signature of the release.\npackage updater\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// UpdateMoveAfterQuit is exported for testing.\nvar UpdateMoveAfterQuit = true\n\n// Update will start the interactive update assistant.\n//\n//nolint:goerr113\nfunc Update(ctx context.Context, currentVersion semver.Version) error {\n\tif err := IsUpdateable(ctx); err != nil {\n\t\tout.Errorf(ctx, \"Your gopass binary is externally managed. Can not update: %q\", err)\n\n\t\treturn err\n\t}\n\n\tdest, err := executable(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trel, err := FetchLatestRelease(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"Current: %s - Latest: %s\", currentVersion.String(), rel.Version.String())\n\t// binary is newer or equal to the latest release -> nothing to do\n\tif currentVersion.GTE(rel.Version) {\n\t\tout.Printf(ctx, \"gopass is up to date (%s)\", currentVersion.String())\n\n\t\tif gfu := os.Getenv(\"GOPASS_FORCE_UPDATE\"); gfu == \"\" {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdebug.Log(\"downloading SHA256SUMS ...\")\n\n\t_, sha256sums, err := downloadAsset(ctx, rel.Assets, \"SHA256SUMS\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"downloading SHA256SUMS.sig ...\")\n\n\t_, sig, err := downloadAsset(ctx, rel.Assets, \"SHA256SUMS.sig\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"verifying GPG signature ...\")\n\n\tok, err := gpgVerify(sha256sums, sig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"signature verification failed: %w\", err)\n\t}\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"GPG signature verification for SHA256SUMS failed\")\n\t}\n\n\tdebug.Log(\"GPG signature OK!\")\n\n\text := \"tar.gz\"\n\tif runtime.GOOS == \"windows\" {\n\t\text = \"zip\"\n\t}\n\n\tsuffix := fmt.Sprintf(\"%s-%s.%s\", runtime.GOOS, runtime.GOARCH, ext)\n\tdebug.Log(\"downloading tarball %q ...\", suffix)\n\n\tdlFilename, buf, err := downloadAsset(ctx, rel.Assets, suffix)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"finding hashsum entry for %q\", dlFilename)\n\n\twantHash, err := findHashForFile(sha256sums, dlFilename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"calculating hashsum of downloaded archive ...\")\n\n\tgotHash := sha256.Sum256(buf)\n\tif !bytes.Equal(wantHash, gotHash[:]) {\n\t\treturn fmt.Errorf(\"SHA256 hash mismatch, want %02x, got %02x\", wantHash, gotHash)\n\t}\n\n\tdebug.Log(\"hashsums match!\")\n\tdebug.Log(\"extracting binary from tarball ...\")\n\n\tif err := extractFile(buf, dlFilename, dest); err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"extracted %q to %q\", dlFilename, dest)\n\tdebug.Log(\"success!\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/updater/update_test.go",
    "content": "//go:build !windows\n\npackage updater\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n//nolint:wrapcheck\nfunc TestIsUpdateable(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\toldExec := executable\n\n\tdefer func() {\n\t\texecutable = oldExec\n\t}()\n\n\ttd := t.TempDir()\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tpre  func() error\n\t\texec func(context.Context) (string, error)\n\t\tpost func() error\n\t\tok   bool\n\t}{\n\t\t{\n\t\t\tname: \"executable error\",\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed\") //nolint:goerr113\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"test binary\",\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn \"action.test\", nil\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t{\n\t\t\tname: \"force update\",\n\t\t\tpre: func() error {\n\t\t\t\tt.Setenv(\"GOPASS_FORCE_UPDATE\", \"true\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tpost: func() error {\n\t\t\t\treturn os.Unsetenv(\"GOPASS_FORCE_UPDATE\")\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t{\n\t\t\tname: \"update in gopath\",\n\t\t\tpre: func() error {\n\t\t\t\tt.Setenv(\"GOPATH\", \"/tmp/foo\")\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn \"/tmp/foo/gopass\", nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stat error\",\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn \"/tmp/foo/gopass\", nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no regular file\",\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn td, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no write access to file\",\n\t\t\tpre: func() error {\n\t\t\t\treturn os.WriteFile(filepath.Join(td, \"gopass\"), []byte(\"foobar\"), 0o555)\n\t\t\t},\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn filepath.Join(td, \"gopass\"), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no write access to dir\",\n\t\t\tpre: func() error {\n\t\t\t\tdir := filepath.Join(td, \"bin\")\n\n\t\t\t\treturn os.Mkdir(dir, 0o555)\n\t\t\t},\n\t\t\texec: func(context.Context) (string, error) {\n\t\t\t\treturn filepath.Join(td, \"bin\"), nil\n\t\t\t},\n\t\t},\n\t} {\n\t\tif tc.pre != nil {\n\t\t\trequire.NoError(t, tc.pre(), tc.name)\n\t\t}\n\n\t\texecutable = tc.exec\n\n\t\terr := IsUpdateable(ctx)\n\t\tif tc.ok {\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t} else {\n\t\t\trequire.Error(t, err, tc.name)\n\t\t}\n\n\t\tif tc.post != nil {\n\t\t\trequire.NoError(t, tc.post(), tc.name)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/updater/updateable.go",
    "content": "package updater\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// IsUpdateable returns an error if this binary is not updateable.\n//\n//nolint:goerr113\nfunc IsUpdateable(ctx context.Context) error {\n\tfn, err := executable(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug.Log(\"File: %s\", fn)\n\t// check if this is a test binary\n\tif strings.HasSuffix(filepath.Base(fn), \".test\") {\n\t\treturn nil\n\t}\n\n\t// check if we want to force updateability\n\tif uf := os.Getenv(\"GOPASS_FORCE_UPDATE\"); uf != \"\" {\n\t\tdebug.Log(\"updateable due to force flag\")\n\n\t\treturn nil\n\t}\n\n\t// check if file is in GOPATH\n\tif gp := os.Getenv(\"GOPATH\"); strings.HasPrefix(fn, gp) {\n\t\treturn fmt.Errorf(\"use go get -u to update binary in GOPATH\")\n\t}\n\n\t// check file\n\tfi, err := os.Stat(fn)\n\tif err != nil {\n\t\treturn err //nolint:wrapcheck\n\t}\n\n\tif !fi.Mode().IsRegular() {\n\t\treturn fmt.Errorf(\"not a regular file\")\n\t}\n\n\tif err := canWrite(fn); err != nil {\n\t\treturn fmt.Errorf(\"can not write %q: %w\", fn, err)\n\t}\n\n\t// no need to check the directory since we'll be writing to the destination file directly\n\treturn nil\n}\n\n//nolint:wrapcheck\nvar executable = func(ctx context.Context) (string, error) {\n\tpath, err := os.Executable()\n\tif err != nil {\n\t\treturn path, err\n\t}\n\tpath, err = filepath.EvalSymlinks(path)\n\n\treturn path, err\n}\n"
  },
  {
    "path": "internal/updater/verify.go",
    "content": "package updater\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/packet\"\n\t\"github.com/gopasspw/gopass/internal/hashsum\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// To update see README.md.\nvar pubkeys = [][]byte{\n\t// 2025 key - 0x67E6E8D2\n\t[]byte(`-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGNBGenVz8BDADZxBInXWFlF8Jp1pM0/qBYnViYlcAXiXFWZ2gkWQwXg42cFDl0\nMEi7V3szFOf9rRX08t8etHAFtWwY8PAMCulKUy2m1sL38ulLeFIuYB5k/VdtKpbz\n67y8CP65VaIqL02fHo4r4BSAJtauoFI8BV93PjKPxRNNY3lJ9gdJUvO+mgv9PvBq\n0fPT9ZXkMnN+J09/CSK9DOdPH22sQs3TIWwC7FxmNskTzNCiFDBTWJXGxDTU29L1\ncUagsz8OOh7G8QFq1GLpDnbb3DrBEMH9UsaeKFQOJws+u7jBhz/VfvNAiuWeXKAF\nw+qpNcTm0UeaPQIMylyzPRmASkFFj7vClOwLA1AL69bIGDJdrfzjOFiGwzsT0qcN\nCI66VumLktRLCrS0gUskJRGXdc9ptsLTzpjCis8CCATrn1LGTBlLOioIEsg4ABXA\nt5Bvce6M6HVx2l+1vFuMDOBz/KoMqgtwcjfaQIam0zcTj+dzg3BchobayGHl9rTi\nqQcRqygzGcWpXbcAEQEAAbRJR29wYXNzIFJlbGVhc2UgU2lnbmluZyBLZXkgMjAy\nNSAoR2l0SHViIEFjdGlvbnMgT25seSkgPHJlbGVhc2VAZ29wYXNzLnB3PokB1wQT\nAQgAQRYhBKH6wP1QrKjeHoxEcX6nCjVn5ujSBQJnp1c/AhsDBQkDwmcABQsJCAcC\nAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEH6nCjVn5ujSgh0MAKzTaGVlRFEltOm6\n7oF2CcDPoQxomsH/cTyn6aygtoWChUozWtMcF+10u0lxvPaVKA6VylNkEaUm2NQd\n5tBpulotx6GwhGDorha/IsgxEh3Sskbms7hVV5HLjieRQbD0Efa9JIoyp8D705k6\nuWKxGNAvQhO3sMdkOf0REjIOfKW+qoV0S375x272fFBnQX9x9h9vjCOSWsGIo6iK\n+RyLMYbUZbKiezuWGhEb19EEFCxiMWAMCp7cbrMGbV1jlqN2AlHPBO45wI5ZS9Rd\nzU+8IPwJqkUhwVc9NwKcIoYCW3YxDT4Io/aGU99SSITgdxtW3RcmJaylpTApdb7P\neTiQwFLS8YWfi2J/Rsm8aLopBWPC7WmfAtg+DvIk1KOURwj7US40C8kNUaKVPRPL\nFWi3idbRLSDKwf9MjX9Cqgq66iowbjj/I0v6mgbTnViV5jatNwuMJFmA9UC97C5H\n14dj0/FyMY7+R4k6FfFuIRrjjGqISths1LzV7N6f+xdxpDi4fohdBBARAgAdFiEE\ne85h9ADzzZEe+G7x0x+gVMha76wFAmenV+AACgkQ0x+gVMha76w5SwCfYRFvwgB7\n5Qcmhtmy886wVJ0IEk4AnRFMgCM1Znzz4zx0ZQatafi1bP97uQGNBGenVz8BDACZ\nNrUH5ilkbV5RkC8NTQwGDOWpQW1BP+giaum1isaEj8dU4529aAjsXCmWwwcwzn4t\nQIbd7Gp4KKcnPQ4rGJDU3BZuSmma/2UyRQScxf+OOVuOs3clF/FWK0AZywMvDHrU\nqd//HVnlWZFDftH7BYMWM4bGYEpIULggOTF5VeYQI0/rO+5Z1QWHUA/LMwA5L48I\n/0+2ju6heTd6l8QaGFOHgqUMXyC7UIpCoj5RAeWgctt/GVwy6+Xx3AWrOQw2MFKM\n8UMpqMlpVmT09mODd7Fd5+cLqyB0LyFkLRbUJHhX1pHrEO2ihDcpHqf8i0Oxd6ao\nWU2YMsQDZYfFLOtdxd0bgDuOzyRBzeW4k2K+wbxYEIvLHDGh6XsxJcwA5TmIq36+\nJFrj6FUalN27XQpvpP7NLaYOfEd4i1wl3S8yjtf8puY+uiW9sX3KvzDPo+rYZF4x\ntOvznVHnYDXjjH1O1tYhHCqVN5cnzg89Tn5O2Bobeaz05GEolbgZ2cmV6PSKkccA\nEQEAAYkBvAQYAQgAJhYhBKH6wP1QrKjeHoxEcX6nCjVn5ujSBQJnp1c/AhsMBQkD\nwmcAAAoJEH6nCjVn5ujSfsoMAKQgs3+0Hsf3nQcZ8e4Ct1k153dMLeTUutFStUXM\nMqRYG6gVnmXz51cPucEzHlFTpf00l9/guSUehrcqxKbz6dodBJf2VYiMlkDJ+Zj/\nAXnBQtudL4HBKVwLAB5hvDnixf5wD0S7lSYojidz4osVjT/uj2D3SZU2bj5MoKA+\n3GoLrUPPMgEvjpgOSiKDYvfqa92x+IlWz5rmug2zT5H+/UmizgexyCfRbVlTfi/8\nLgAC95fFvk6mo/s0IwZ4m87whlywFkGYEwmbXGhs29f/qZ7ZJPFOW7BZc8ipvrUe\nrTASZuFDwYIMDaFD/aT9wgn27P/UHsqFW0PbVxm44gS90Q4xTx2XTBmJg4S/3Dwn\n1JZ70RVzsU4kL0tVQ5GDzKvN2SBhHsr5POBTxbrVW1+HATXeRGv0orqccHwmFaPh\nOO4szdamDmhzgr9mdVv0gHg9cyTizvNiH026FYRwJmATPj1sjAnnjscZPKBeKiNO\nfT1TaQbilUs+PL7VNI6d2uAPwQ==\n=bs0I\n-----END PGP PUBLIC KEY BLOCK-----\n`),\n}\n\ntype krLogger struct {\n\tr openpgp.EntityList\n}\n\nfunc (k *krLogger) Str() string {\n\tvar out strings.Builder\n\n\tfor _, e := range k.r {\n\t\tfor k := range e.Identities {\n\t\t\tout.WriteString(k)\n\t\t\tout.WriteString(\", \")\n\t\t}\n\t}\n\n\treturn out.String()[:out.Len()-2]\n}\n\nfunc gpgVerify(data, sig []byte) (bool, error) {\n\treturn gpgVerifyAt(data, sig, nil)\n}\n\nfunc gpgVerifyAt(data, sig []byte, nowFn func() time.Time) (bool, error) {\n\tif nowFn == nil {\n\t\tnowFn = time.Now\n\t}\n\n\tvar keyring openpgp.EntityList\n\tfor _, pubkey := range pubkeys {\n\t\tk, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pubkey))\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to read public key: %q\", err)\n\n\t\t\treturn false, fmt.Errorf(\"failed to read public key: %w\", err)\n\t\t}\n\t\tkeyring = append(keyring, k...)\n\t}\n\n\tdebug.Log(\"Keyring: %q\", &krLogger{keyring})\n\n\t_, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(sig), &packet.Config{\n\t\tTime: nowFn,\n\t})\n\tif err != nil {\n\t\tdebug.Log(\"failed to validate detached GPG signature: %q\", err)\n\t\tdebug.Log(\"data: %q, %s\", string(data), hashsum.MD5Hex(string(data)))\n\t\tdebug.Log(\"sig: %q\", string(sig))\n\n\t\treturn false, fmt.Errorf(\"failed to validated detached GPG signature: %w\", err)\n\t}\n\n\treturn true, nil\n}\n\n// retrieve the hash for the given filename from a checksum file.\n//\n//nolint:goerr113\nfunc findHashForFile(buf []byte, filename string) ([]byte, error) {\n\ts := bufio.NewScanner(bytes.NewReader(buf))\n\tfor s.Scan() {\n\t\tp := strings.Split(s.Text(), \"  \")\n\t\tif len(p) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif p[1] != filename {\n\t\t\tcontinue\n\t\t}\n\n\t\th, err := hex.DecodeString(p[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to decode hash: %w\", err)\n\t\t}\n\n\t\treturn h, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"hash for file %q not found\", filename)\n}\n"
  },
  {
    "path": "internal/updater/verify_test.go",
    "content": "package updater\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// To update see README.md.\nvar testSignature = []byte(`\n-----BEGIN PGP SIGNATURE-----\n\niQGzBAABCAAdFiEEofrA/VCsqN4ejERxfqcKNWfm6NIFAmena44ACgkQfqcKNWfm\n6NLPcgv+PeNs5OLB9y+kJhcWJXyGMyCCq4fj8ACA/mMkRxi+T9iP+51Di+GWyXvd\niMAHCBNbra2qn6nfiy7YJbgFDWZZVVUOXayqbgoGuxojO3n5AF9sK8Ieou7iYXpd\nTXx0Zr8XFrhMMvzHVEDNqMtrRpmuwtixHA1PtGx/8Adv35gHRFZzW8xZ1ar5FVXk\nJk/bjo7h1bVf/jaakN9SDx8xc0D72LniPFNrEeOf8QTxSHZFaAOXuU9GsED8Cx1U\nwQKBwveBSFKy17dGx03xcknqF/V3djApIgOIZ3MbaD50gpu3x9ltt9yOtkP9op0B\nANkUpIyrgcv39Trf44Z/rgj/bZz0UaagjMwA/RWtjnA6Kuw93BctVcfxuA2jC00g\nGSny65MYtI6ynXnJ3xJVrIlNDawK/PjkS/HFWHFLKF7/K4ycL0KBVm/SETdIoGDK\ngGTBIqBqDvHISE686mpH6rBRvyu7VOdbh6WTvztynHbdX/1cwyTKghnHNlw6gtIP\nrp7LGb+c\n=NeAM\n-----END PGP SIGNATURE-----\n\n`)\n\nvar testData = []byte(`gopass-sign-test\n`)\n\nfunc TestGPGVerify(t *testing.T) {\n\tt.Parallel()\n\n\tok, err := gpgVerify(testData, testSignature)\n\trequire.NoError(t, err)\n\tassert.True(t, ok)\n}\n\n// TestGPGVerifyIn6Months tests that the signature is still valid in 6 months.\n// This is supposed to act as a canary so we don't forget to roll the key\n// before it expires. See README.md for details.\nfunc TestGPGVerifyIn6Months(t *testing.T) {\n\tt.Parallel()\n\n\tok, err := gpgVerifyAt(testData, testSignature, func() time.Time { return time.Now().AddDate(0, 6, 0) })\n\trequire.NoError(t, err, \"If TestGPGVerify succeeds but this test fails the self-updater key is about to expire. Please open an issue to update the key. Thank you.\")\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "main.go",
    "content": "// Copyright 2021 The gopass Authors. All rights reserved.\n// Use of this source code is governed by the MIT license,\n// that can be found in the LICENSE file.\n\n// Gopass implements the gopass command line tool.\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\trdebug \"runtime/debug\"\n\t\"runtime/pprof\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/fatih/color\"\n\tap \"github.com/gopasspw/gopass/internal/action\"\n\t\"github.com/gopasspw/gopass/internal/action/exit\"\n\t\"github.com/gopasspw/gopass/internal/action/pwgen\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/crypto\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t_ \"github.com/gopasspw/gopass/internal/backend/storage\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/hook\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/internal/queue\"\n\t\"github.com/gopasspw/gopass/internal/store/leaf\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/protect\"\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\tcolorable \"github.com/mattn/go-colorable\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst (\n\tname = \"gopass\"\n)\n\n// Version is the released version of gopass.\nvar version string\n\nfunc main() {\n\t// important: execute the func now but the returned func only on defer!\n\t// Example: https://go.dev/play/p/8214zCX6hVq.\n\tdefer writeCPUProfile()()\n\n\tif err := protect.Pledge(\"stdio rpath wpath cpath tty proc exec fattr\"); err != nil {\n\t\tpanic(err)\n\t}\n\n\tctx := context.Background()\n\n\t// trap Ctrl+C and call cancel on the context\n\tctx, cancel := context.WithCancel(ctx)\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt)\n\n\tdefer func() {\n\t\tsignal.Stop(sigChan)\n\t\tcancel()\n\t}()\n\n\tgo func(ctx context.Context) {\n\t\tselect {\n\t\tcase <-sigChan:\n\t\t\tcancel()\n\t\tcase <-ctx.Done():\n\t\t}\n\t}(ctx)\n\n\tcli.ErrWriter = errorWriter{ //nolint:reassign\n\t\tout: colorable.NewColorableStderr(),\n\t}\n\tsv := getVersion()\n\tcli.VersionPrinter = makeVersionPrinter(os.Stdout, sv)\n\n\tdebug.Log(\"gopass %s starting. Args: %v\", sv.String(), os.Args)\n\n\t// run the app\n\tq := queue.New(ctx)\n\tctx = queue.WithQueue(ctx, q)\n\tctx, app := setupApp(ctx, sv)\n\n\tif err := app.RunContext(ctx, os.Args); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// process all pending queue items\n\t_ = q.Close(ctx)\n\n\twriteMemProfile()\n\n\tdebug.Log(\"gopass %s shutting down ...\\n\\n\", sv.String())\n}\n\n//nolint:wrapcheck\nfunc setupApp(ctx context.Context, sv semver.Version) (context.Context, *cli.App) {\n\t// try to read config (if it exists)\n\tcfg := config.New()\n\n\t// set config values\n\tctx = initContext(ctx, cfg)\n\n\t// initialize action handlers\n\taction, err := ap.New(cfg, sv)\n\tif err != nil {\n\t\tout.Errorf(ctx, \"failed to initialize gopass: %s\", err)\n\t\tos.Exit(exit.Unknown)\n\t}\n\n\t// set some action callbacks\n\tif !config.AsBool(cfg.Get(\"core.autoimport\")) {\n\t\tctx = ctxutil.WithImportFunc(ctx, termio.AskForKeyImport)\n\t}\n\n\tctx = leaf.WithFsckFunc(ctx, termio.AskForConfirmation)\n\n\tapp := cli.NewApp()\n\n\tapp.Name = name\n\tapp.Version = sv.String()\n\tapp.Usage = \"The standard unix password manager - rewritten in Go\"\n\tapp.UseShortOptionHandling = true\n\tapp.EnableBashCompletion = true\n\tapp.BashComplete = func(c *cli.Context) {\n\t\tcli.DefaultAppComplete(c)\n\t\taction.Complete(c)\n\t}\n\n\tapp.Flags = ap.ShowFlags()\n\tapp.Action = func(c *cli.Context) error {\n\t\tif err := action.IsInitialized(c); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif c.Args().Present() {\n\t\t\treturn action.Show(c)\n\t\t}\n\n\t\treturn action.REPL(c)\n\t}\n\n\tapp.Commands = getCommands(action, app)\n\n\treturn ctx, app\n}\n\nfunc getCommands(action *ap.Action, app *cli.App) []*cli.Command {\n\tcmds := []*cli.Command{\n\t\t{\n\t\t\tName:  \"completion\",\n\t\t\tUsage: \"Bash and ZSH completion\",\n\t\t\tDescription: \"\" +\n\t\t\t\t\"Source the output of this command with bash or zsh to get auto completion\",\n\t\t\tSubcommands: []*cli.Command{{\n\t\t\t\tName:   \"bash\",\n\t\t\t\tUsage:  \"Source for auto completion in bash\",\n\t\t\t\tAction: action.CompletionBash,\n\t\t\t}, {\n\t\t\t\tName:  \"zsh\",\n\t\t\t\tUsage: \"Source for auto completion in zsh\",\n\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\treturn action.CompletionZSH(app) //nolint:wrapcheck\n\t\t\t\t},\n\t\t\t}, {\n\t\t\t\tName:  \"fish\",\n\t\t\t\tUsage: \"Source for auto completion in fish\",\n\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\treturn action.CompletionFish(app) //nolint:wrapcheck\n\t\t\t\t},\n\t\t\t}, {\n\t\t\t\tName:  \"openbsdksh\",\n\t\t\t\tUsage: \"Source for auto completion in OpenBSD's ksh\",\n\t\t\t\tAction: func(c *cli.Context) error {\n\t\t\t\t\treturn action.CompletionOpenBSDKsh(app) //nolint:wrapcheck\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t}\n\n\tcmds = append(cmds, action.GetCommands()...)\n\tcmds = append(cmds, pwgen.GetCommands()...)\n\tsort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })\n\n\tfor i, cmd := range cmds {\n\t\t// fmt.Printf(\"[%6d - %10s] Before: %p - After %p\\n\", i, cmds[i].Name, cmds[i].Before, cmds[i].After)\n\t\tcmds[i].Before = mkHookFn(\"core.pre-hook\", cmd.Name, action.Store, cmd.Before)\n\t\tcmds[i].After = mkHookFn(\"core.post-hook\", cmd.Name, action.Store, cmd.After)\n\t\t// fmt.Printf(\"[%6d - %10s] Before: %p - After %p\\n\", i, cmds[i].Name, cmds[i].Before, cmds[i].After)\n\t\t// fmt.Println()\n\t}\n\n\treturn cmds\n}\n\ntype pathGetter interface {\n\tPath() string\n}\n\nfunc mkHookFn(hookName, cmdName string, s pathGetter, fn func(c *cli.Context) error) func(c *cli.Context) error {\n\tif fn == nil {\n\t\treturn func(c *cli.Context) error {\n\t\t\tdir := config.String(c.Context, \"mounts.path\")\n\n\t\t\treturn hook.Invoke(c.Context, hookName, dir, cmdName)\n\t\t}\n\t}\n\n\treturn func(c *cli.Context) error {\n\t\tif err := fn(c); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn hook.Invoke(c.Context, hookName, s.Path(), cmdName, c.Args().First())\n\t}\n}\n\nfunc parseBuildInfo() (string, string, string) {\n\tbi, ok := rdebug.ReadBuildInfo()\n\tif !ok {\n\t\treturn \"HEAD\", \"\", \"\"\n\t}\n\n\tvar (\n\t\tcommit string\n\t\tdate   string\n\t\tdirty  string\n\t)\n\n\tfor _, v := range bi.Settings {\n\t\tswitch v.Key {\n\t\tcase \"vcs.revision\":\n\t\t\tcommit = v.Value[len(v.Value)-8:]\n\t\tcase \"vcs.time\":\n\t\t\tif bt, err := time.Parse(\"2006-01-02T15:04:05Z\", date); err == nil {\n\t\t\t\tdate = bt.Format(\"2006-01-02 15:04:05\")\n\t\t\t}\n\t\tcase \"vcs.modified\":\n\t\t\tif v.Value == \"true\" {\n\t\t\t\tdirty = \" (dirty)\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn commit, date, dirty\n}\n\nfunc makeVersionPrinter(out io.Writer, sv semver.Version) func(c *cli.Context) {\n\treturn func(c *cli.Context) {\n\t\tcommit, buildtime, dirty := parseBuildInfo()\n\t\tbuildInfo := \"\"\n\n\t\tif commit != \"\" {\n\t\t\tbuildInfo = commit + dirty\n\t\t}\n\n\t\tif buildtime != \"\" {\n\t\t\tif buildInfo != \"\" {\n\t\t\t\tbuildInfo += \" \"\n\t\t\t}\n\n\t\t\tbuildInfo += buildtime\n\t\t}\n\n\t\tif buildInfo != \"\" {\n\t\t\tbuildInfo = \"(\" + buildInfo + \") \"\n\t\t}\n\n\t\tfmt.Fprintf(\n\t\t\tout,\n\t\t\t\"%s %s %s%s %s %s\\n\",\n\t\t\tname,\n\t\t\tsv.String(),\n\t\t\tbuildInfo,\n\t\t\truntime.Version(),\n\t\t\truntime.GOOS,\n\t\t\truntime.GOARCH,\n\t\t)\n\t}\n}\n\ntype errorWriter struct {\n\tout io.Writer\n}\n\nfunc (e errorWriter) Write(p []byte) (int, error) {\n\treturn e.out.Write([]byte(\"\\n\" + color.RedString(\"Error: %s\", p))) //nolint:wrapcheck\n}\n\nfunc initContext(ctx context.Context, cfg *config.Config) context.Context {\n\t// initialize from config, may be overridden by env vars\n\tctx = cfg.WithConfig(ctx)\n\n\t// always trust\n\tctx = gpg.WithAlwaysTrust(ctx, true)\n\n\t// only emit color codes when stdout is a terminal\n\tif !isatty.IsTerminal(os.Stdout.Fd()) {\n\t\tcolor.NoColor = true\n\t\tctx = ctxutil.WithTerminal(ctx, false)\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t}\n\n\t// reading from stdin?\n\tif info, err := os.Stdin.Stat(); err == nil && info.Mode()&os.ModeCharDevice == 0 {\n\t\tctx = ctxutil.WithInteractive(ctx, false)\n\t\tctx = ctxutil.WithStdin(ctx, true)\n\t}\n\n\t// enable following references based on the configuration\n\tctx = ctxutil.WithFollowRef(ctx, config.AsBool(cfg.Get(\"core.follow-references\")))\n\n\t// disable colored output on windows since cmd.exe doesn't support ANSI color\n\t// codes. Other terminal may do, but until we can figure that out better\n\t// disable this for all terms on this platform\n\tif sv := os.Getenv(\"NO_COLOR\"); runtime.GOOS == \"windows\" || sv == \"true\" {\n\t\tcolor.NoColor = true\n\t} else {\n\t\t// on all other platforms we should be able to use color. Only set\n\t\t// this if it's in the config.\n\t\tif cfg.IsSet(\"core.nocolor\") {\n\t\t\tcolor.NoColor = config.AsBool(cfg.Get(\"core.nocolor\"))\n\t\t}\n\t}\n\n\t// using a password callback for age identity file or not?\n\tif pw, isSet := os.LookupEnv(\"GOPASS_AGE_PASSWORD\"); isSet {\n\t\tctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {\n\t\t\tdebug.Log(\"using age password callback from env variable GOPASS_AGE_PASSWORD\")\n\n\t\t\treturn []byte(pw), nil\n\t\t})\n\t}\n\n\treturn ctx\n}\n\nfunc writeCPUProfile() func() {\n\tcp := os.Getenv(\"GOPASS_CPU_PROFILE\")\n\tif cp == \"\" {\n\t\treturn func() {}\n\t}\n\n\tf, err := os.Create(cp)\n\tif err != nil {\n\t\tlog.Fatalf(\"could not create CPU profile at %s: %s\", cp, err)\n\t}\n\n\tif err := pprof.StartCPUProfile(f); err != nil {\n\t\tlog.Fatalf(\"could not start CPU profile: %s\", err)\n\t}\n\n\treturn func() {\n\t\tpprof.StopCPUProfile()\n\n\t\t_ = f.Close()\n\n\t\tdebug.Log(\"wrote CPU profile to %s\", cp)\n\t}\n}\n\nfunc writeMemProfile() {\n\tmp := os.Getenv(\"GOPASS_MEM_PROFILE\")\n\tif mp == \"\" {\n\t\treturn\n\t}\n\n\tf, err := os.Create(mp)\n\tif err != nil {\n\t\tlog.Fatalf(\"could not write mem profile to %s: %s\", mp, err)\n\t}\n\n\tdefer func() {\n\t\t_ = f.Close()\n\t}()\n\n\truntime.GC() // get up-to-date statistics\n\n\tif err := pprof.WriteHeapProfile(f); err != nil {\n\t\tlog.Fatalf(\"could not write heap profile: %s\", err)\n\t}\n\n\tdebug.Log(\"wrote heap profile to %s\", mp)\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/action\"\n\t\"github.com/gopasspw/gopass/internal/backend\"\n\t\"github.com/gopasspw/gopass/internal/backend/crypto/gpg\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestVersionPrinter(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := &bytes.Buffer{}\n\tvp := makeVersionPrinter(buf, semver.Version{Major: 1})\n\tvp(nil)\n\n\tcommit, _, _ := parseBuildInfo()\n\n\tassert.Contains(t, buf.String(), \"gopass 1.0.0\")\n\tassert.Contains(t, buf.String(), commit)\n}\n\nfunc TestGetVersion(t *testing.T) {\n\tt.Parallel()\n\n\tversion = \"1.9.0\"\n\n\tif getVersion().LT(semver.Version{Major: 1, Minor: 9}) {\n\t\tt.Errorf(\"invalid version\")\n\t}\n}\n\nfunc TestSetupApp(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\t_, app := setupApp(ctx, semver.Version{})\n\tassert.NotNil(t, app)\n}\n\n// commandsWithError is a list of commands that return an error when\n// invoked without arguments.\nvar commandsWithError = set.Map([]string{\n\t\".age.identities.add\",\n\t\".age.identities.remove\",\n\t\".age.lock\",\n\t\".alias.add\",\n\t\".alias.remove\",\n\t\".alias.delete\",\n\t\".cat\",\n\t\".clone\",\n\t\".copy\",\n\t\".create\",\n\t\".delete\",\n\t\".edit\",\n\t\".env\",\n\t\".find\",\n\t\".fscopy\",\n\t\".fsmove\",\n\t\".generate\",\n\t\".git\",\n\t\".git.push\",\n\t\".git.pull\",\n\t\".git.status\",\n\t\".git.remote.add\",\n\t\".git.remote.remove\",\n\t\".grep\",\n\t\".history\",\n\t\".init\",\n\t\".insert\",\n\t\".link\",\n\t\".merge\",\n\t\".mounts.add\",\n\t\".mounts.remove\",\n\t\".move\",\n\t\".otp\",\n\t\".process\",\n\t\".recipients.add\",\n\t\".recipients.remove\",\n\t\".show\",\n\t\".sum\",\n\t\".templates.edit\",\n\t\".templates.remove\",\n\t\".templates.show\",\n\t\".unclip\",\n\t\".reorg\",\n})\n\nfunc TestGetCommands(t *testing.T) {\n\tu := gptest.NewUnitTester(t)\n\n\tbuf := &bytes.Buffer{}\n\tcolor.NoColor = true\n\n\tout.Stdout = buf\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\tcfg := config.NewInMemory()\n\trequire.NoError(t, cfg.SetPath(u.StoreDir(\"\")))\n\n\tclipboard.ForceUnsupported = true\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tctx = ctxutil.WithInteractive(ctx, false)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithHidden(ctx, true)\n\tctx, err := backend.WithCryptoBackendString(ctx, \"plain\")\n\trequire.NoError(t, err)\n\tctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {\n\t\treturn []byte(\"foobar\"), nil\n\t})\n\n\tact, err := action.New(cfg, semver.Version{})\n\trequire.NoError(t, err)\n\n\tapp := cli.NewApp()\n\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\tc := cli.NewContext(app, fs, nil)\n\tc.Context = ctx\n\n\tcommands := getCommands(act, app)\n\tassert.Len(t, commands, 42)\n\n\tprefix := \"\"\n\ttestCommands(t, c, commands, prefix)\n}\n\nfunc testCommands(t *testing.T, c *cli.Context, commands []*cli.Command, prefix string) {\n\tt.Helper()\n\n\tfor _, cmd := range commands {\n\t\tif cmd.Name == \"update\" || cmd.Name == \"agent\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(cmd.Subcommands) > 0 {\n\t\t\ttestCommands(t, c, cmd.Subcommands, prefix+\".\"+cmd.Name)\n\t\t}\n\n\t\tif cmd.Before != nil {\n\t\t\tif err := cmd.Before(c); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif cmd.BashComplete != nil {\n\t\t\tcmd.BashComplete(c)\n\t\t}\n\n\t\tif cmd.Action != nil {\n\t\t\tfullName := prefix + \".\" + cmd.Name\n\t\t\tif _, found := commandsWithError[fullName]; found {\n\t\t\t\trequire.Error(t, cmd.Action(c), fullName)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trequire.NoError(t, cmd.Action(c), fullName)\n\t\t}\n\t}\n}\n\nfunc TestInitContext(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\tcfg := config.NewInMemory()\n\n\tctx = initContext(ctx, cfg)\n\tassert.True(t, gpg.IsAlwaysTrust(ctx))\n}\n"
  },
  {
    "path": "main_unix.go",
    "content": "//go:build !windows\n\npackage main\n\nimport (\n\t\"os/signal\"\n\t\"syscall\"\n)\n\nfunc init() {\n\t// workaround for https://github.com/golang/go/issues/37942\n\tsignal.Ignore(syscall.SIGURG)\n}\n"
  },
  {
    "path": "pkg/appdir/appdir.go",
    "content": "// Package appdir implements a customized lookup pattern for application paths\n// like config, cache and data dirs. On Linux this uses the XDG specification,\n// on MacOS and Windows the platform defaults.\npackage appdir\n\nimport (\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// DefaultAppdir is the default appdir for gopass.\nvar DefaultAppdir = New(\"gopass\")\n\n// Appdir is a helper struct to generate paths for config, cache and data dirs.\ntype Appdir struct {\n\t// Name is used in the final path of the generated path.\n\tname string\n}\n\n// New returns a new Appdir for the given application name.\n// The name is used to construct the paths to the application's\n// directories.\nfunc New(name string) *Appdir {\n\treturn &Appdir{\n\t\tname: name,\n\t}\n}\n\n// Name returns the name of the appdir.\nfunc (a *Appdir) Name() string {\n\treturn a.name\n}\n\n// UserConfig returns the user's config dir for gopass.\n// See a.UserConfig() for more details.\nfunc UserConfig() string {\n\treturn DefaultAppdir.UserConfig()\n}\n\n// UserCache returns the user's cache dir for gopass.\n// See a.UserCache() for more details.\nfunc UserCache() string {\n\treturn DefaultAppdir.UserCache()\n}\n\n// UserData returns the user's data dir for gopass.\n// See a.UserData() for more details.\nfunc UserData() string {\n\treturn DefaultAppdir.UserData()\n}\n\n// UserHome returns the user's home dir.\n// It respects the GOPASS_HOMEDIR environment variable.\nfunc UserHome() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn hd\n\t}\n\n\tuhd, err := os.UserHomeDir()\n\tif err != nil {\n\t\tdebug.Log(\"failed to detect user home dir: %s\", err)\n\n\t\treturn \"\"\n\t}\n\n\treturn uhd\n}\n"
  },
  {
    "path": "pkg/appdir/appdir_test.go",
    "content": "package appdir\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUserHome(t *testing.T) {\n\ttd := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tassert.Equal(t, td, UserHome())\n}\n"
  },
  {
    "path": "pkg/appdir/appdir_windows.go",
    "content": "package appdir\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// UserConfig returns the user's config directory.\n// It uses the APPDATA environment variable on Windows.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\nfunc (a *Appdir) UserConfig() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".config\", a.name)\n\t}\n\n\treturn filepath.Join(os.Getenv(\"APPDATA\"), a.name)\n}\n\n// UserCache returns the user's cache directory.\n// It uses the LOCALAPPDATA environment variable on Windows.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\nfunc (a *Appdir) UserCache() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".cache\", a.name)\n\t}\n\n\treturn filepath.Join(os.Getenv(\"LOCALAPPDATA\"), a.name)\n}\n\n// UserData returns the user's data directory.\n// It uses the LOCALAPPDATA environment variable on Windows.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\nfunc (a *Appdir) UserData() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".local\", \"share\", a.name)\n\t}\n\treturn filepath.Join(os.Getenv(\"LOCALAPPDATA\"), a.name)\n}\n"
  },
  {
    "path": "pkg/appdir/appdir_xdg.go",
    "content": "//go:build !windows\n\npackage appdir\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// UserConfig returns the user's config directory.\n// It follows the XDG Base Directory Specification.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\n// See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html\nfunc (a *Appdir) UserConfig() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\tdebug.V(3).Log(\"GOPASS_HOMEDIR is set to %s\", hd)\n\n\t\treturn filepath.Join(hd, \".config\", a.name)\n\t}\n\n\tbase := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif base == \"\" {\n\t\tbase = filepath.Join(os.Getenv(\"HOME\"), \".config\")\n\t}\n\n\treturn filepath.Join(base, a.name)\n}\n\n// UserCache returns the user's cache directory.\n// It follows the XDG Base Directory Specification.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\n// See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html\nfunc (a *Appdir) UserCache() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".cache\", a.name)\n\t}\n\n\tbase := os.Getenv(\"XDG_CACHE_HOME\")\n\tif base == \"\" {\n\t\tbase = filepath.Join(os.Getenv(\"HOME\"), \".cache\")\n\t}\n\n\treturn filepath.Join(base, a.name)\n}\n\n// UserData returns the user's data directory.\n// It follows the XDG Base Directory Specification.\n// The GOPASS_HOMEDIR environment variable can be used to override the base path.\n// See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html\nfunc (a *Appdir) UserData() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".local\", \"share\", a.name)\n\t}\n\n\tbase := os.Getenv(\"XDG_DATA_HOME\")\n\tif base == \"\" {\n\t\tbase = filepath.Join(os.Getenv(\"HOME\"), \".local\", \"share\")\n\t}\n\n\treturn filepath.Join(base, a.name)\n}\n"
  },
  {
    "path": "pkg/appdir/appdir_xdg_test.go",
    "content": "//go:build !windows\n\npackage appdir\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUserConfig(t *testing.T) {\n\tov := gptest.UnsetVars(\"GOPASS_HOMEDIR\", \"XDG_CONFIG_HOME\", \"HOME\")\n\tdefer ov()\n\n\tt.Run(\"gopass homedir\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", \"/foo/bar\")\n\t\tassert.Equal(t, \"/foo/bar/.config/gopass\", UserConfig())\n\t})\n\n\tt.Run(\"xdg_config_home\", func(t *testing.T) {\n\t\tt.Setenv(\"XDG_CONFIG_HOME\", \"/foo/baz/myconfig\")\n\t\tassert.Equal(t, \"/foo/baz/myconfig/gopass\", UserConfig())\n\t})\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(\"HOME\", \"/home/gopass\")\n\t\tassert.Equal(t, \"/home/gopass/.config/gopass\", UserConfig())\n\t})\n}\n\nfunc TestUserCache(t *testing.T) {\n\tov := gptest.UnsetVars(\"GOPASS_HOMEDIR\", \"XDG_CACHE_HOME\", \"HOME\")\n\tdefer ov()\n\n\tt.Run(\"gopass homedir\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", \"/foo/bar\")\n\t\tassert.Equal(t, \"/foo/bar/.cache/gopass\", UserCache())\n\t})\n\n\tt.Run(\"xdg_cache_home\", func(t *testing.T) {\n\t\tt.Setenv(\"XDG_CACHE_HOME\", \"/foo/baz/mycache\")\n\t\tassert.Equal(t, \"/foo/baz/mycache/gopass\", UserCache())\n\t})\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(\"HOME\", \"/home/gopass\")\n\t\tassert.Equal(t, \"/home/gopass/.cache/gopass\", UserCache())\n\t})\n}\n\nfunc TestUserData(t *testing.T) {\n\tov := gptest.UnsetVars(\"GOPASS_HOMEDIR\", \"XDG_DATA_HOME\", \"HOME\")\n\tdefer ov()\n\n\tt.Run(\"gopass homedir\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_HOMEDIR\", \"/foo/bar\")\n\t\tassert.Equal(t, \"/foo/bar/.local/share/gopass\", UserData())\n\t})\n\n\tt.Run(\"xdg_data_home\", func(t *testing.T) {\n\t\tt.Setenv(\"XDG_DATA_HOME\", \"/foo/baz/mydata\")\n\t\tassert.Equal(t, \"/foo/baz/mydata/gopass\", UserData())\n\t})\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Setenv(\"HOME\", \"/home/gopass\")\n\t\tassert.Equal(t, \"/home/gopass/.local/share/gopass\", UserData())\n\t})\n}\n"
  },
  {
    "path": "pkg/appdir/runtime_windows.go",
    "content": "package appdir\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// UserRuntime returns the users runtime dir\nfunc (a *Appdir) UserRuntime() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".run\")\n\t}\n\n\treturn filepath.Join(os.Getenv(\"LOCALAPPDATA\"), a.name)\n}\n\n// UserRuntime returns the users runtime dir.\nfunc UserRuntime() string {\n\treturn DefaultAppdir.UserRuntime()\n}\n"
  },
  {
    "path": "pkg/appdir/runtime_xdg.go",
    "content": "//go:build !windows\n\npackage appdir\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// UserRuntime returns the users runtime dir.\nfunc (a *Appdir) UserRuntime() string {\n\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\treturn filepath.Join(hd, \".run\")\n\t}\n\n\tbase := os.Getenv(\"XDG_RUNTIME_DIR\")\n\tif base == \"\" {\n\t\tbase = filepath.Join(os.Getenv(\"HOME\"), \".run\")\n\t}\n\n\treturn filepath.Join(base, a.name)\n}\n\n// UserRuntime returns the users runtime dir.\nfunc UserRuntime() string {\n\treturn DefaultAppdir.UserRuntime()\n}\n"
  },
  {
    "path": "pkg/clipboard/clipboard.go",
    "content": "// Package clipboard provides functions to copy and clear the clipboard.\npackage clipboard\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/notify\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar (\n\t// Helpers can be overridden at compile time, e.g. go build \\\n\t// -ldflags=='-X github.com/gopasspw/gopass/pkg/clipboard.Helpers=termux-api'.\n\tHelpers = \"xsel, xclip or wl-clipboard\"\n\t// ErrNotSupported is returned when the clipboard is not accessible.\n\tErrNotSupported = fmt.Errorf(\"WARNING: No clipboard available. \"+\n\t\t\"Install %s, provide $GOPASS_CLIPBOARD_COPY_CMD and $GOPASS_CLIPBOARD_CLEAR_CMD or use -f to print to console\", Helpers)\n)\n\n// CopyTo copies the given data to the clipboard and enqueues automatic\n// clearing of the clipboard. The name of the secret is passed for logging\n// and notifications. The timeout is in seconds.\nfunc CopyTo(ctx context.Context, name string, content []byte, timeout int) error {\n\tdebug.Log(\"Copying to clipboard: %s for %ds\", name, timeout)\n\n\tclipboardCopyCMD := os.Getenv(\"GOPASS_CLIPBOARD_COPY_CMD\")\n\tif clipboardCopyCMD != \"\" {\n\t\tif err := callCommand(ctx, clipboardCopyCMD, name, content); err != nil {\n\t\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"failed to call clipboard copy command\")\n\n\t\t\treturn fmt.Errorf(\"failed to call clipboard copy command: %w\", err)\n\t\t}\n\t} else if clipboard.IsUnsupported() {\n\t\tout.Errorf(ctx, \"%s\", ErrNotSupported)\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", ErrNotSupported.Error())\n\n\t\treturn nil\n\t} else if err := copyToClipboard(ctx, content); err != nil {\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"failed to write to clipboard\")\n\n\t\treturn fmt.Errorf(\"failed to write to clipboard: %w\", err)\n\t}\n\n\tif timeout < 1 {\n\t\tdebug.Log(\"Auto-clear of clipboard disabled.\")\n\n\t\tout.Printf(ctx, \"✔ Copied %s to clipboard.\", color.YellowString(name))\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", fmt.Sprintf(\"✔ Copied %s to clipboard.\", name))\n\n\t\treturn nil\n\t}\n\n\tif err := clearClip(ctx, name, content, timeout); err != nil {\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"failed to clear clipboard\")\n\n\t\treturn fmt.Errorf(\"failed to clear clipboard: %w\", err)\n\t}\n\n\tout.Printf(ctx, \"✔ Copied %s to clipboard. Will clear in %d seconds.\", color.YellowString(name), timeout)\n\t_ = notify.Notify(ctx, \"gopass - clipboard\", fmt.Sprintf(\"✔ Copied %s to clipboard. Will clear in %d seconds.\", name, timeout))\n\n\treturn nil\n}\n\nfunc callCommand(_ context.Context, cmd string, parameter string, stdinValue []byte) error {\n\tclipboardProcess := exec.Command(cmd, parameter)\n\tstdin, err := clipboardProcess.StdinPipe()\n\n\tdefer func() {\n\t\t_ = stdin.Close()\n\t}()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create stdin pipe: %w\", err)\n\t}\n\n\tif err = clipboardProcess.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start clipboard process: %w\", err)\n\t}\n\n\tif _, err = stdin.Write(stdinValue); err != nil {\n\t\treturn fmt.Errorf(\"failed to write to STDIN: %w\", err)\n\t}\n\n\t// Force STDIN close before we wait for the process to finish, so we avoid deadlocks\n\tif err = stdin.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to close STDIN: %w\", err)\n\t}\n\n\tif err := clipboardProcess.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"failed to call clipboard command: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc killProc(pid int) {\n\t// err should be always nil, but just to be sure\n\tproc, err := os.FindProcess(pid)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif err := proc.Kill(); err != nil {\n\t\tdebug.Log(\"failed to kill %d: %s\", pid, err)\n\n\t\treturn\n\t}\n\n\t// wait for the process to actually exit to avoid zombie processes\n\tps, err := proc.Wait()\n\tif err != nil {\n\t\tdebug.Log(\"failed to wait for %d: %s\", pid, err)\n\n\t\treturn\n\t}\n\n\tdebug.Log(\"killed process exited with %d\", ps.ExitCode())\n}\n"
  },
  {
    "path": "pkg/clipboard/clipboard_others.go",
    "content": "//go:build !windows\n\npackage clipboard\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2id\"\n)\n\n// clearClip will spawn a copy of gopass that waits in a detached background\n// process group until the timeout is expired. It will then compare the contents\n// of the clipboard and erase it if it still contains the data gopass copied\n// to it.\nfunc clearClip(ctx context.Context, name string, content []byte, timeout int) error {\n\thash, err := argon2id.Generate(string(content), 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate checksum: %w\", err)\n\t}\n\n\t// kill any pending unclip processes\n\t_ = killPrecedessors()\n\n\tcmd := exec.Command(os.Args[0], \"unclip\", \"--timeout\", strconv.Itoa(timeout))\n\t// https://groups.google.com/d/msg/golang-nuts/shST-SDqIp4/za4oxEiVtI0J\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetpgid: true,\n\t}\n\n\tcmd.Env = append(os.Environ(), \"GOPASS_UNCLIP_NAME=\"+name)\n\tcmd.Env = append(cmd.Env, \"GOPASS_UNCLIP_CHECKSUM=\"+hash)\n\n\tif !config.Bool(ctx, \"core.notifications\") {\n\t\tcmd.Env = append(cmd.Env, \"GOPASS_NO_NOTIFY=true\")\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to invoke unclip: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc walkFn(pid int, killFn func(int)) {\n\t// read the commandline for this process\n\tcmdline, err := os.ReadFile(fmt.Sprintf(\"/proc/%d/cmdline\", pid))\n\tif err != nil {\n\t\treturn\n\t}\n\t// compare the name of the binary and the first argument to avoid killing\n\t// any unrelated (gopass) processes\n\targs := bytes.Split(cmdline, []byte{0})\n\tif len(args) < 2 {\n\t\treturn\n\t}\n\t// the commandline should start with \"gopass\"\n\tif string(args[0]) != os.Args[0] {\n\t\treturn\n\t}\n\t// and have \"unclip\" as the first argument\n\tif string(args[1]) != \"unclip\" {\n\t\treturn\n\t}\n\n\tkillFn(pid)\n}\n"
  },
  {
    "path": "pkg/clipboard/clipboard_test.go",
    "content": "package clipboard\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/mitchellh/go-ps\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNotExistingClipboardCopyCommand(t *testing.T) {\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\tt.Setenv(\"GOPASS_CLIPBOARD_COPY_CMD\", \"not_existing_command\")\n\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\tdefer cancel()\n\n\tmaybeErr := CopyTo(ctx, \"foo\", []byte(\"bar\"), 1)\n\trequire.Error(t, maybeErr)\n\tassert.Contains(t, maybeErr.Error(), \"\\\"not_existing_command\\\": executable file not found\")\n}\n\nfunc TestUnsupportedCopyToClipboard(t *testing.T) {\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\tdefer cancel()\n\n\tclipboard.ForceUnsupported = true\n\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\n\trequire.NoError(t, CopyTo(ctx, \"foo\", []byte(\"bar\"), 1))\n\tassert.Contains(t, buf.String(), \"WARNING\")\n}\n\nfunc TestClearClipboard(t *testing.T) {\n\tctx, cancel := context.WithCancel(config.NewContextInMemory())\n\trequire.NoError(t, clearClip(ctx, \"foo\", []byte(\"bar\"), 0))\n\tcancel()\n\ttime.Sleep(50 * time.Millisecond)\n}\n\nfunc BenchmarkWalkProc(b *testing.B) {\n\tfor b.Loop() {\n\t\t_ = filepath.Walk(\"/proc\", func(path string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif strings.Count(path, \"/\") != 3 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif !strings.HasSuffix(path, \"/status\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tpid, err := strconv.Atoi(path[6:strings.LastIndex(path, \"/\")])\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\twalkFn(pid, func(int) {})\n\n\t\t\treturn nil\n\t\t})\n\t}\n}\n\nfunc BenchmarkListProc(b *testing.B) {\n\tfor b.Loop() {\n\t\tprocs, err := ps.Processes()\n\t\tif err != nil {\n\t\t\tb.Fatalf(\"err: %s\", err)\n\t\t}\n\n\t\tfor _, proc := range procs {\n\t\t\twalkFn(proc.Pid(), func(int) {})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/clipboard/clipboard_windows.go",
    "content": "//go:build windows\n\npackage clipboard\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2id\"\n)\n\n// clearClip will spawn a copy of gopass that waits in a detached background\n// process group until the timeout is expired. It will then compare the contents\n// of the clipboard and erase it if it still contains the data gopass copied\n// to it.\nfunc clearClip(ctx context.Context, name string, content []byte, timeout int) error {\n\thash, err := argon2id.Generate(string(content), 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd := exec.CommandContext(ctx, os.Args[0], \"unclip\", \"--timeout\", strconv.Itoa(timeout))\n\tcmd.Env = append(os.Environ(), \"GOPASS_UNCLIP_NAME=\"+name)\n\tcmd.Env = append(cmd.Env, \"GOPASS_UNCLIP_CHECKSUM=\"+hash)\n\tif !config.Bool(ctx, \"core.notifications\") {\n\t\tcmd.Env = append(cmd.Env, \"GOPASS_NO_NOTIFY=true\")\n\t}\n\treturn cmd.Start()\n}\n\nfunc walkFn(int, func(int)) {}\n"
  },
  {
    "path": "pkg/clipboard/copy_darwin.go",
    "content": "//go:build darwin\n\npackage clipboard\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/clipboard\"\n)\n\nfunc copyToClipboard(ctx context.Context, content []byte) error {\n\tif err := clipboard.WritePassword(ctx, content); err != nil {\n\t\treturn fmt.Errorf(\"failed to write to clipboard: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/copy_others.go",
    "content": "//go:build !darwin\n\npackage clipboard\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/clipboard\"\n)\n\nfunc copyToClipboard(ctx context.Context, content []byte) error {\n\t// We should be using clipboard.WritePassword here, but many\n\t// Linux distros currently do not ship with the required dependencies.\n\t// See https://github.com/gopasspw/gopass/pull/3234\n\tif err := clipboard.WriteAll(ctx, content); err != nil {\n\t\treturn fmt.Errorf(\"failed to write to clipboard: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/kill_others.go",
    "content": "//go:build !darwin && !linux && !solaris && !windows && !freebsd\n\npackage clipboard\n\nfunc killPrecedessors() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/kill_ps.go",
    "content": "//go:build darwin || (freebsd && amd64) || linux || solaris || windows || (freebsd && arm) || (freebsd && arm64)\n\npackage clipboard\n\nimport (\n\t\"fmt\"\n\n\tps \"github.com/mitchellh/go-ps\"\n)\n\n// killPrecedessors will kill any previous \"gopass unclip\" invocations to avoid\n// erasing the clipboard prematurely in case the the same content is copied to\n// the clipboard repeatedly.\nfunc killPrecedessors() error {\n\tprocs, err := ps.Processes()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list processes: %w\", err)\n\t}\n\n\tfor _, proc := range procs {\n\t\twalkFn(proc.Pid(), killProc)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/unclip.go",
    "content": "package clipboard\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/internal/notify\"\n\t\"github.com/gopasspw/gopass/internal/pwschemes/argon2id\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n// Clear will attempt to erase the clipboard.\nfunc Clear(ctx context.Context, name string, checksum string, force bool) error {\n\tclipboardClearCMD := os.Getenv(\"GOPASS_CLIPBOARD_CLEAR_CMD\")\n\tif clipboardClearCMD != \"\" {\n\t\tif err := callCommand(ctx, clipboardClearCMD, name, []byte(checksum)); err != nil {\n\t\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"failed to call clipboard clear command\")\n\n\t\t\treturn fmt.Errorf(\"failed to call clipboard clear command: %w\", err)\n\t\t}\n\n\t\tdebug.Log(\"clipboard cleared (%s)\", checksum)\n\n\t\treturn nil\n\t}\n\n\tif clipboard.IsUnsupported() {\n\t\treturn ErrNotSupported\n\t}\n\n\tcur, err := clipboard.ReadAllString(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read clipboard: %w\", err)\n\t}\n\n\tmatch, err := argon2id.Validate(cur, checksum)\n\tif err != nil {\n\t\tdebug.Log(\"failed to validate checksum %s: %s\", checksum, err)\n\n\t\treturn nil\n\t}\n\n\tif !match && !force {\n\t\treturn nil\n\t}\n\n\tif err := clipboard.WriteAllString(ctx, \"\"); err != nil {\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"Failed to clear clipboard\")\n\n\t\treturn fmt.Errorf(\"failed to write clipboard: %w\", err)\n\t}\n\n\tif err := clearClipboardHistory(ctx); err != nil {\n\t\t_ = notify.Notify(ctx, \"gopass - clipboard\", \"Failed to clear clipboard history\")\n\n\t\treturn fmt.Errorf(\"failed to clear clipboard history: %w\", err)\n\t}\n\n\tif err := notify.Notify(ctx, \"gopass - clipboard\", \"Clipboard has been cleared\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to send unclip notification: %w\", err)\n\t}\n\n\tdebug.Log(\"clipboard cleared (%s)\", checksum)\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/unclip_linux.go",
    "content": "//go:build linux\n\npackage clipboard\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/godbus/dbus/v5\"\n)\n\nfunc clearClipboardHistory(ctx context.Context) error {\n\tconn, err := dbus.SessionBus()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to connect to session bus: %w\", err)\n\t}\n\n\tobj := conn.Object(\"org.kde.klipper\", \"/klipper\")\n\tcall := obj.Call(\"org.kde.klipper.klipper.clearClipboardHistory\", 0)\n\n\tif call.Err != nil {\n\t\tif strings.HasPrefix(call.Err.Error(), \"The name org.kde.klipper was not provided\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.HasPrefix(call.Err.Error(), \"The name is not activatable\") {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn call.Err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/unclip_others.go",
    "content": "//go:build !linux\n\npackage clipboard\n\nimport \"context\"\n\nfunc clearClipboardHistory(ctx context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clipboard/unclip_test.go",
    "content": "package clipboard\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNotExistingClipboardClearCommand(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tt.Setenv(\"GOPASS_CLIPBOARD_CLEAR_CMD\", \"not_existing_command\")\n\n\tmaybeErr := Clear(ctx, \"\", \"\", false)\n\trequire.Error(t, maybeErr)\n\tassert.Contains(t, maybeErr.Error(), \"\\\"not_existing_command\\\": executable file not found in\")\n}\n\nfunc TestUnclip(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tbuf := &bytes.Buffer{}\n\tout.Stdout = buf\n\n\tdefer func() {\n\t\tout.Stdout = os.Stdout\n\t}()\n\n\trequire.EqualError(t, Clear(ctx, \"\", \"\", false), ErrNotSupported.Error())\n}\n"
  },
  {
    "path": "pkg/ctxutil/ctxutil.go",
    "content": "// Package ctxutil provides a set of functions to manage context values\n// in a gopass application. It allows to set and get values in the context.\npackage ctxutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/internal/store\"\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype contextKey int\n\nconst (\n\tctxKeyTerminal contextKey = iota\n\tctxKeyInteractive\n\tctxKeyStdin\n\tctxKeyGitCommit\n\tctxKeyAlwaysYes\n\tctxKeyProgressCallback\n\tctxKeyAlias\n\tctxKeyGitInit\n\tctxKeyForce\n\tctxKeyCommitMessage\n\tctxKeyNoNetwork\n\tctxKeyUsername\n\tctxKeyEmail\n\tctxKeyImportFunc\n\tctxKeyPasswordCallback\n\tctxKeyPasswordPurgeCallback\n\tctxKeyCommitTimestamp\n\tctxKeyShowParsing\n\tctxKeyHidden\n\tctxFollowRef\n)\n\n// ErrNoCallback is returned when no callback is set in the context.\nvar ErrNoCallback = fmt.Errorf(\"no callback\")\n\n// WithGlobalFlags parses any global flags from the cli context and returns\n// a regular context. It handles the --yes flag and sets the appropriate\n// context value.\nfunc WithGlobalFlags(c *cli.Context) context.Context {\n\tif c.Bool(\"yes\") {\n\t\treturn WithAlwaysYes(c.Context, true)\n\t}\n\n\treturn c.Context\n}\n\n// ProgressCallback is a callback for updating progress.\ntype ProgressCallback func()\n\n// WithTerminal returns a context with an explicit value for whether or not we are\n// in a terminal.\nfunc WithTerminal(ctx context.Context, isTerm bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyTerminal, isTerm)\n}\n\n// HasTerminal returns true if a value for Terminal has been set in this context.\nfunc HasTerminal(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyTerminal).(bool)\n\n\treturn ok\n}\n\n// IsTerminal returns the value of terminal or the default (true).\nfunc IsTerminal(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyTerminal).(bool)\n\tif !ok {\n\t\treturn true\n\t}\n\n\treturn bv\n}\n\n// WithInteractive returns a context with an explicit value for whether or not we are\n// in an interactive session.\nfunc WithInteractive(ctx context.Context, isInteractive bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyInteractive, isInteractive)\n}\n\n// HasInteractive returns true if a value for Interactive has been set in this context.\nfunc HasInteractive(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyInteractive).(bool)\n\n\treturn ok\n}\n\n// IsInteractive returns the value of interactive or the default (true).\nfunc IsInteractive(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyInteractive).(bool)\n\tif !ok {\n\t\treturn true\n\t}\n\n\treturn bv\n}\n\n// WithStdin returns a context with the value for Stdin set. If true some input\n// is available on Stdin (e.g. something is being piped into it).\nfunc WithStdin(ctx context.Context, isStdin bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyStdin, isStdin)\n}\n\n// HasStdin returns true if a value for Stdin has been set in this context.\nfunc HasStdin(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyStdin).(bool)\n\n\treturn ok\n}\n\n// IsStdin returns the value of stdin, i.e. if it's true some data is being\n// piped to stdin. If not set it returns the default value (false).\nfunc IsStdin(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyStdin).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithShowParsing returns a context with the value for ShowParsing set.\n// This is used to control whether to show parsing errors.\nfunc WithShowParsing(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyShowParsing, bv)\n}\n\n// HasShowParsing returns true if a value for ShowParsing has been set in this context.\nfunc HasShowParsing(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyShowParsing).(bool)\n\n\treturn ok\n}\n\n// IsShowParsing returns the value of ShowParsing or the default (true).\nfunc IsShowParsing(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyShowParsing).(bool)\n\tif !ok {\n\t\treturn true\n\t}\n\n\treturn bv\n}\n\n// WithGitCommit returns a context with the value of git commit set.\n// If true, changes will be committed to git.\nfunc WithGitCommit(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyGitCommit, bv)\n}\n\n// HasGitCommit returns true if a value for GitCommit has been set in this context.\nfunc HasGitCommit(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyGitCommit).(bool)\n\n\treturn ok\n}\n\n// IsGitCommit returns the value of git commit or the default (true).\nfunc IsGitCommit(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyGitCommit, true)\n}\n\n// IsFollowRef returns the value of follow-ref or the default (false).\n// If true, symlinks will be followed.\nfunc IsFollowRef(ctx context.Context) bool {\n\treturn is(ctx, ctxFollowRef, false)\n}\n\n// HasFollowRef returns true if a value for follow-ref has been set in this context.\nfunc HasFollowRef(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxFollowRef).(bool)\n\n\treturn ok\n}\n\n// WithFollowRef returns a context with the value of follow-ref set.\nfunc WithFollowRef(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxFollowRef, bv)\n}\n\n// WithAlwaysYes returns a context with the value of always yes set.\n// If true, any prompts will be answered with yes.\nfunc WithAlwaysYes(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyAlwaysYes, bv)\n}\n\n// HasAlwaysYes returns true if a value for AlwaysYes has been set in this context.\nfunc HasAlwaysYes(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyAlwaysYes).(bool)\n\n\treturn ok\n}\n\n// IsAlwaysYes returns the value of always yes or the default (false).\nfunc IsAlwaysYes(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyAlwaysYes).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n\n// WithProgressCallback returns a context with the value of ProgressCallback set.\nfunc WithProgressCallback(ctx context.Context, cb ProgressCallback) context.Context {\n\treturn context.WithValue(ctx, ctxKeyProgressCallback, cb)\n}\n\n// HasProgressCallback returns true if a ProgressCallback has been set.\nfunc HasProgressCallback(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyProgressCallback).(ProgressCallback)\n\n\treturn ok\n}\n\n// GetProgressCallback return the set progress callback or a default one.\n// It never returns nil.\nfunc GetProgressCallback(ctx context.Context) ProgressCallback {\n\tcb, ok := ctx.Value(ctxKeyProgressCallback).(ProgressCallback)\n\tif !ok || cb == nil {\n\t\treturn func() {}\n\t}\n\n\treturn cb\n}\n\n// WithAlias returns a context with the alias set.\nfunc WithAlias(ctx context.Context, alias string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyAlias, alias)\n}\n\n// HasAlias returns true if a value for alias has been set.\nfunc HasAlias(ctx context.Context) bool {\n\treturn hasString(ctx, ctxKeyAlias)\n}\n\n// GetAlias returns an alias if it has been set or an empty string otherwise.\nfunc GetAlias(ctx context.Context) string {\n\ta, ok := ctx.Value(ctxKeyAlias).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn a\n}\n\n// WithGitInit returns a context with the value for the git init flag set.\n// If true, a git repository will be initialized.\nfunc WithGitInit(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyGitInit, bv)\n}\n\n// HasGitInit returns true if the git init flag was set.\nfunc HasGitInit(ctx context.Context) bool {\n\treturn hasBool(ctx, ctxKeyGitInit)\n}\n\n// IsGitInit returns the value of the git init flag or true if none was set.\nfunc IsGitInit(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyGitInit, true)\n}\n\n// WithForce returns a context with the force flag set.\n// If true, operations that would otherwise fail will be forced to succeed.\nfunc WithForce(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyForce, bv)\n}\n\n// HasForce returns true if the context has the force flag set.\nfunc HasForce(ctx context.Context) bool {\n\treturn hasBool(ctx, ctxKeyForce)\n}\n\n// IsForce returns the force flag value of the default (false).\nfunc IsForce(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyForce, false)\n}\n\n// AddToCommitMessageBody returns a context with something added to the commit's body.\nfunc AddToCommitMessageBody(ctx context.Context, sv string) context.Context {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\tvar headedText HeadedText\n\t\tht = &headedText\n\t\tctx = context.WithValue(ctx, ctxKeyCommitMessage, ht)\n\t}\n\tht.AddToBody(sv)\n\n\treturn ctx\n}\n\n// HasCommitMessageBody returns true if the commit message body is nonempty.\nfunc HasCommitMessageBody(ctx context.Context) bool {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn ht.HasBody()\n}\n\n// GetCommitMessageBody returns the set commit message body or an empty string.\nfunc GetCommitMessageBody(ctx context.Context) string {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn ht.GetBody()\n}\n\n// WithCommitMessage returns a context with a commit message (head) set.\n// (full commit message is the commit message's body is not defined, commit message head otherwise).\nfunc WithCommitMessage(ctx context.Context, sv string) context.Context {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\tvar headedText HeadedText\n\t\tht = &headedText\n\t\tctx = context.WithValue(ctx, ctxKeyCommitMessage, ht)\n\t}\n\tht.SetHead(sv)\n\n\treturn ctx\n}\n\n// HasCommitMessage returns true if the commit message (head) was set.\nfunc HasCommitMessage(ctx context.Context) bool {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\n\treturn ok && ht.head != \"\" // not the most intuitive answer, but a backwards-compatible one. for now.\n}\n\n// GetCommitMessage returns the set commit message (head) or an empty string.\nfunc GetCommitMessage(ctx context.Context) string {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn ht.head\n}\n\n// GetCommitMessageFull returns the set commit message (head+body, if either are defined) or an empty string.\nfunc GetCommitMessageFull(ctx context.Context) string {\n\tht, ok := ctx.Value(ctxKeyCommitMessage).(*HeadedText)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn ht.GetText()\n}\n\n// WithNoNetwork returns a context with the value of no network set.\n// If true, no network operations will be performed.\nfunc WithNoNetwork(ctx context.Context, bv bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyNoNetwork, bv)\n}\n\n// HasNoNetwork returns true if no network was set.\nfunc HasNoNetwork(ctx context.Context) bool {\n\treturn hasBool(ctx, ctxKeyNoNetwork)\n}\n\n// IsNoNetwork returns the value of no network or false.\nfunc IsNoNetwork(ctx context.Context) bool {\n\treturn is(ctx, ctxKeyNoNetwork, false)\n}\n\n// WithUsername returns a context with the username set in the context.\nfunc WithUsername(ctx context.Context, sv string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyUsername, sv)\n}\n\n// GetUsername returns the username from the context.\nfunc GetUsername(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyUsername).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n\n// WithEmail returns a context with the email set in the context.\nfunc WithEmail(ctx context.Context, sv string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyEmail, sv)\n}\n\n// GetEmail returns the email from the context.\nfunc GetEmail(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyEmail).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n\n// WithImportFunc will return a context with the import callback set.\n// The callback is used to ask the user for confirmation before importing a key.\nfunc WithImportFunc(ctx context.Context, imf store.ImportCallback) context.Context {\n\treturn context.WithValue(ctx, ctxKeyImportFunc, imf)\n}\n\n// HasImportFunc returns true if a value for import func has been set in this\n// context.\nfunc HasImportFunc(ctx context.Context) bool {\n\timf, ok := ctx.Value(ctxKeyImportFunc).(store.ImportCallback)\n\n\treturn ok && imf != nil\n}\n\n// GetImportFunc will return the import callback or a default one returning true\n// Note: will never return nil.\nfunc GetImportFunc(ctx context.Context) store.ImportCallback {\n\timf, ok := ctx.Value(ctxKeyImportFunc).(store.ImportCallback)\n\tif !ok || imf == nil {\n\t\treturn func(context.Context, string, []string) bool {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn imf\n}\n\n// PasswordCallback is a password prompt callback used in our age crypto backend.\n// The arguments are typically the filename and whether or not to ask for a confirmation\n// of the provided password, we use it notably together with askPass.\n// It is linked to the GOPASS_AGE_PASSWORD env variable.\ntype PasswordCallback func(string, bool) ([]byte, error)\n\n// WithPasswordCallback returns a context with the password callback set.\nfunc WithPasswordCallback(ctx context.Context, cb PasswordCallback) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPasswordCallback, cb)\n}\n\n// HasPasswordCallback returns true if a password callback was set in the context.\nfunc HasPasswordCallback(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyPasswordCallback).(PasswordCallback)\n\n\treturn ok\n}\n\n// GetPasswordCallback returns the password callback or a default (which always fails).\n// The default callback returns ErrNoCallback.\nfunc GetPasswordCallback(ctx context.Context) PasswordCallback {\n\tpwcb, ok := ctx.Value(ctxKeyPasswordCallback).(PasswordCallback)\n\tif !ok || pwcb == nil {\n\t\treturn func(string, bool) ([]byte, error) {\n\t\t\treturn nil, ErrNoCallback\n\t\t}\n\t}\n\n\treturn pwcb\n}\n\n// PasswordPurgeCallback is a callback to purge a password cached by PasswordCallback.\ntype PasswordPurgeCallback func(string)\n\n// WithPasswordPurgeCallback returns a context with the password purge callback set.\nfunc WithPasswordPurgeCallback(ctx context.Context, cb PasswordPurgeCallback) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPasswordPurgeCallback, cb)\n}\n\n// HasPasswordPurgeCallback returns true if a password purge callback was set in the context.\nfunc HasPasswordPurgeCallback(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyPasswordPurgeCallback).(PasswordPurgeCallback)\n\n\treturn ok\n}\n\n// GetPasswordPurgeCallback returns the password purge callback or a default (which is a no-op).\n// The default callback does nothing.\nfunc GetPasswordPurgeCallback(ctx context.Context) PasswordPurgeCallback {\n\tppcb, ok := ctx.Value(ctxKeyPasswordPurgeCallback).(PasswordPurgeCallback)\n\tif !ok || ppcb == nil {\n\t\treturn func(string) {}\n\t}\n\n\treturn ppcb\n}\n\n// WithCommitTimestamp returns a context with the value for the commit\n// timestamp set.\n// This is used to allow for reproducible builds.\nfunc WithCommitTimestamp(ctx context.Context, ts time.Time) context.Context {\n\treturn context.WithValue(ctx, ctxKeyCommitTimestamp, ts)\n}\n\n// HasCommitTimestamp returns true if the value for the commit timestamp\n// was set in the context.\nfunc HasCommitTimestamp(ctx context.Context) bool {\n\t_, ok := ctx.Value(ctxKeyCommitTimestamp).(time.Time)\n\n\treturn ok\n}\n\n// GetCommitTimestamp returns the commit timestamp from the context if\n// set or the default (now) otherwise.\nfunc GetCommitTimestamp(ctx context.Context) time.Time {\n\tif ts, ok := ctx.Value(ctxKeyCommitTimestamp).(time.Time); ok {\n\t\treturn ts\n\t}\n\n\treturn time.Now()\n}\n\n// WithHidden returns a context with the flag value for hidden set.\n// This is used to hide secrets from the output.\nfunc WithHidden(ctx context.Context, hidden bool) context.Context {\n\treturn context.WithValue(ctx, ctxKeyHidden, hidden)\n}\n\n// IsHidden returns true if any output should be hidden in this context.\nfunc IsHidden(ctx context.Context) bool {\n\tbv, ok := ctx.Value(ctxKeyHidden).(bool)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn bv\n}\n"
  },
  {
    "path": "pkg/ctxutil/ctxutil_test.go",
    "content": "package ctxutil\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc TestTerminal(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.True(t, IsTerminal(ctx))\n\tassert.True(t, IsTerminal(WithTerminal(ctx, true)))\n\tassert.False(t, IsTerminal(WithTerminal(ctx, false)))\n}\n\nfunc TestInteractive(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.True(t, IsInteractive(ctx))\n\tassert.True(t, IsInteractive(WithInteractive(ctx, true)))\n\tassert.False(t, IsInteractive(WithInteractive(ctx, false)))\n}\n\nfunc TestStdin(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsStdin(ctx))\n\tassert.True(t, IsStdin(WithStdin(ctx, true)))\n\tassert.False(t, IsStdin(WithStdin(ctx, false)))\n}\n\nfunc TestGitCommit(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.True(t, IsGitCommit(ctx))\n\tassert.True(t, IsGitCommit(WithGitCommit(ctx, true)))\n\tassert.False(t, IsGitCommit(WithGitCommit(ctx, false)))\n}\n\nfunc TestAlwaysYes(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsAlwaysYes(ctx))\n\tassert.True(t, IsAlwaysYes(WithAlwaysYes(ctx, true)))\n\tassert.False(t, IsAlwaysYes(WithAlwaysYes(ctx, false)))\n}\n\nfunc TestProgressCallback(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tvar foo bool\n\n\tpc := func() { foo = true }\n\n\tGetProgressCallback(WithProgressCallback(ctx, pc))()\n\tassert.True(t, foo)\n}\n\nfunc TestAlias(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, GetAlias(ctx))\n\tassert.Empty(t, GetAlias(WithAlias(ctx, \"\")))\n}\n\nfunc TestGitInit(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.True(t, IsGitInit(ctx))\n\tassert.True(t, IsGitInit(WithGitInit(ctx, true)))\n\tassert.False(t, IsGitInit(WithGitInit(ctx, false)))\n}\n\nfunc TestForce(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsForce(ctx))\n\tassert.True(t, IsForce(WithForce(ctx, true)))\n\tassert.False(t, IsForce(WithForce(ctx, false)))\n}\n\nfunc TestCommitMessage(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, GetCommitMessage(ctx))\n\tassert.Equal(t, \"foo\", GetCommitMessage(WithCommitMessage(ctx, \"foo\")))\n\tassert.Empty(t, GetCommitMessage(WithCommitMessage(ctx, \"\")))\n}\n\nfunc TestCommitMessageBody(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tctx2 := AddToCommitMessageBody(AddToCommitMessageBody(WithCommitMessage(ctx, \"foo\"), \"bar\"), \"baz\")\n\tassert.Equal(t, \"foo\\n\\nbar\\nbaz\", GetCommitMessageFull(ctx2))\n\tassert.Equal(t, \"foo\", GetCommitMessage(ctx2))\n\tassert.Equal(t, \"bar\\nbaz\", GetCommitMessageBody(ctx2))\n\tctx2 = AddToCommitMessageBody(AddToCommitMessageBody(ctx, \"bar\"), \"baz\")\n\tassert.Empty(t, GetCommitMessage(ctx2))\n\tassert.Equal(t, \"bar\\nbaz\", GetCommitMessageFull(ctx2))\n\tassert.Equal(t, \"bar\\nbaz\", GetCommitMessageBody(ctx2))\n\tctx2 = WithCommitMessage(ctx, \"foo\")\n\tassert.Equal(t, \"foo\", GetCommitMessage(ctx2))\n\tassert.Equal(t, \"foo\", GetCommitMessageFull(ctx2))\n\tassert.Empty(t, GetCommitMessageBody(ctx2))\n}\n\nfunc TestComposite(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = WithTerminal(ctx, false)\n\tctx = WithInteractive(ctx, false)\n\tctx = WithStdin(ctx, true)\n\tctx = WithGitCommit(ctx, false)\n\tctx = WithAlwaysYes(ctx, true)\n\tctx = WithEmail(ctx, \"foo@bar.com\")\n\tctx = WithUsername(ctx, \"foo\")\n\tctx = WithNoNetwork(ctx, true)\n\tctx = WithCommitMessage(ctx, \"foobar\")\n\tctx = WithForce(ctx, true)\n\tctx = WithGitInit(ctx, false)\n\n\tassert.False(t, IsTerminal(ctx))\n\tassert.True(t, HasTerminal(ctx))\n\n\tassert.False(t, IsInteractive(ctx))\n\tassert.True(t, HasInteractive(ctx))\n\n\tassert.True(t, IsStdin(ctx))\n\tassert.True(t, HasStdin(ctx))\n\n\tassert.False(t, IsGitCommit(ctx))\n\tassert.True(t, HasGitCommit(ctx))\n\n\tassert.True(t, IsAlwaysYes(ctx))\n\tassert.True(t, HasAlwaysYes(ctx))\n\n\tassert.Equal(t, \"foo@bar.com\", GetEmail(ctx))\n\tassert.Equal(t, \"foo\", GetUsername(ctx))\n\n\tassert.True(t, IsNoNetwork(ctx))\n\tassert.True(t, HasNoNetwork(ctx))\n\n\tassert.Equal(t, \"foobar\", GetCommitMessage(ctx))\n\tassert.True(t, HasCommitMessage(ctx))\n\n\tassert.True(t, IsForce(ctx))\n\tassert.True(t, HasForce(ctx))\n\n\tassert.False(t, IsGitInit(ctx))\n\tassert.True(t, HasGitInit(ctx))\n}\n\nfunc TestGlobalFlags(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tapp := cli.NewApp()\n\n\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\tsf := cli.BoolFlag{\n\t\tName:  \"yes\",\n\t\tUsage: \"yes\",\n\t}\n\trequire.NoError(t, sf.Apply(fs))\n\trequire.NoError(t, fs.Parse([]string{\"--yes\"}))\n\tc := cli.NewContext(app, fs, nil)\n\tc.Context = ctx\n\n\tassert.True(t, IsAlwaysYes(WithGlobalFlags(c)))\n}\n\nfunc TestImportFunc(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tifunc := func(context.Context, string, []string) bool {\n\t\treturn true\n\t}\n\n\tassert.NotNil(t, GetImportFunc(ctx))\n\tassert.True(t, GetImportFunc(WithImportFunc(ctx, ifunc))(ctx, \"\", nil))\n\tassert.True(t, HasImportFunc(WithImportFunc(ctx, ifunc)))\n\tassert.True(t, GetImportFunc(WithImportFunc(ctx, nil))(ctx, \"\", nil))\n}\n\nfunc TestHidden(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, IsHidden(ctx))\n\tassert.True(t, IsHidden(WithHidden(ctx, true)))\n}\n"
  },
  {
    "path": "pkg/ctxutil/helper.go",
    "content": "package ctxutil\n\nimport (\n\t\"context\"\n\t\"strings\"\n)\n\n// hasString is a helper function for checking if a string has been set in\n// the provided context.\nfunc hasString(ctx context.Context, key contextKey) bool {\n\t_, ok := ctx.Value(key).(string)\n\n\treturn ok\n}\n\n// hasBool is a helper function for checking if a bool has been set in\n// the provided context.\nfunc hasBool(ctx context.Context, key contextKey) bool {\n\t_, ok := ctx.Value(key).(bool)\n\n\treturn ok\n}\n\n// is is a helper function for returning the value of a bool from the context\n// or the provided default.\nfunc is(ctx context.Context, key contextKey, def bool) bool {\n\tbv, ok := ctx.Value(key).(bool)\n\tif !ok {\n\t\treturn def\n\t}\n\n\treturn bv\n}\n\n// HeadedText is a helper struct for storing a commit message with a subject and body.\ntype HeadedText struct {\n\thead string\n\tbody *strings.Builder\n}\n\n// SetHead sets the head of the text.\nfunc (h *HeadedText) SetHead(s string) {\n\th.head = s\n}\n\n// GetHead returns the head of the text.\nfunc (h *HeadedText) GetHead() string {\n\treturn h.head\n}\n\n// AddToBody adds a string to the body of the text.\nfunc (h *HeadedText) AddToBody(s string) {\n\tif h.body == nil {\n\t\tvar realBody strings.Builder\n\t\th.body = &realBody\n\t\trealBody.WriteString(s)\n\n\t\treturn\n\t}\n\t(*h.body).WriteString(\"\\n\" + s)\n}\n\n// ClearBody clears the body of the text.\nfunc (h *HeadedText) ClearBody() {\n\th.body = nil\n}\n\n// GetBody returns the body of the text.\nfunc (h *HeadedText) GetBody() string {\n\tif h.body == nil {\n\t\treturn \"\"\n\t}\n\n\treturn (*h.body).String()\n}\n\n// HasBody returns true if the body of the text is not empty.\nfunc (h *HeadedText) HasBody() bool {\n\tok := h.body != nil\n\n\treturn ok && (*h.body).Len() > 0\n}\n\n// GetText returns the full text, including the head and body.\nfunc (h *HeadedText) GetText() string {\n\tbody := h.GetBody()\n\tif body == \"\" && h.head == \"\" {\n\t\treturn \"\"\n\t}\n\tif h.head == \"\" {\n\t\treturn body\n\t}\n\tif body == \"\" {\n\t\treturn h.head\n\t}\n\n\treturn h.head + \"\\n\\n\" + body\n}\n"
  },
  {
    "path": "pkg/debug/debug.go",
    "content": "package debug\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\t// Stdout is exported for tests.\n\tStdout io.Writer = os.Stdout\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n\t// LogWriter is exposed for consuming extra command output, if needed.\n\tLogWriter io.Writer = io.Discard\n)\n\nvar opts struct {\n\tlogger     *log.Logger\n\tfuncs      map[string]bool\n\tfiles      map[string]bool\n\tlogFile    *os.File\n\tlogSecrets bool\n\tverbosity  int\n\tpid        int\n}\n\n// v is a verbosity level.\ntype v int\n\n// V returns a logger at the given verbosity level.\n// The higher the number, the more verbose the logging.\nfunc V(n int) v {\n\treturn v(n)\n}\n\nvar logFn = doNotLog\n\n// make sure all initializations happens before the init func.\nvar enabled = initDebug()\n\nfunc initDebug() bool {\n\tif opts.logFile != nil {\n\t\t_ = opts.logFile.Close()\n\t}\n\n\tif l := os.Getenv(\"GOPASS_DEBUG_VERBOSE\"); l != \"\" {\n\t\tif iv, err := strconv.Atoi(l); err == nil {\n\t\t\topts.verbosity = iv\n\t\t}\n\t}\n\n\tif os.Getenv(\"GOPASS_DEBUG\") == \"\" && os.Getenv(\"GOPASS_DEBUG_LOG\") == \"\" {\n\t\tlogFn = doNotLog\n\n\t\treturn false\n\t}\n\n\t// we need to explicitly set logSecrets to false in case tests run under an environment\n\t// where GOPASS_DEBUG_LOG_SECRETS is true. Otherwise setting it to false in the test\n\t// context won't have any effect.\n\topts.logSecrets = false\n\tif sv := os.Getenv(\"GOPASS_DEBUG_LOG_SECRETS\"); sv != \"\" && sv != \"false\" {\n\t\topts.logSecrets = true\n\t}\n\n\topts.pid = os.Getpid()\n\n\tinitDebugLogger()\n\tinitDebugTags()\n\n\tlogFn = doLog\n\n\treturn true\n}\n\nfunc initDebugLogger() {\n\tdebugfile := os.Getenv(\"GOPASS_DEBUG_LOG\")\n\tif debugfile == \"\" {\n\t\topts.logger = log.New(os.Stderr, \"\", log.Ldate|log.Lmicroseconds)\n\t\tLogWriter = os.Stderr\n\n\t\treturn\n\t}\n\n\tf, err := os.OpenFile(debugfile, os.O_WRONLY|os.O_APPEND, 0o600)\n\tif err == nil {\n\t\t// seek to the end of the file (offset, whence [2 = end])\n\t\t_, err := f.Seek(0, 2)\n\t\tif err != nil {\n\t\t\t_ = f.Close()\n\t\t\tfmt.Fprintf(Stderr, \"unable to seek to end of %v: %v\\n\", debugfile, err)\n\t\t\tos.Exit(3)\n\t\t}\n\t}\n\n\tif err != nil && os.IsNotExist(err) {\n\t\tf, err = os.OpenFile(debugfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)\n\t}\n\n\tif err != nil {\n\t\tfmt.Fprintf(Stderr, \"unable to open debug log file %v: %v\\n\", debugfile, err)\n\t\tos.Exit(2)\n\t}\n\n\topts.logFile = f\n\topts.logger = log.New(f, \"\", log.Ldate|log.Lmicroseconds)\n\tLogWriter = f\n}\n\nfunc parseFilter(envname string, pad func(string) string) map[string]bool {\n\tfilter := make(map[string]bool)\n\n\tenv := os.Getenv(envname)\n\tif env == \"\" {\n\t\treturn filter\n\t}\n\n\tfor fn := range strings.SplitSeq(env, \",\") {\n\t\tt := pad(strings.TrimSpace(fn))\n\t\tval := true\n\n\t\tswitch t[0] {\n\t\tcase '-':\n\t\t\tval = false\n\t\t\tt = t[1:]\n\t\tcase '+':\n\t\t\tval = true\n\t\t\tt = t[1:]\n\t\t}\n\n\t\t// test pattern\n\t\t_, err := path.Match(t, \"\")\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(Stderr, \"error: invalid pattern %q: %v\\n\", t, err)\n\t\t\tos.Exit(5)\n\t\t}\n\n\t\tfilter[t] = val\n\t}\n\n\treturn filter\n}\n\nfunc padFunc(s string) string {\n\tif s == \"all\" {\n\t\treturn s\n\t}\n\n\treturn s\n}\n\nfunc padFile(s string) string {\n\tif s == \"all\" {\n\t\treturn s\n\t}\n\n\tif !strings.Contains(s, \"/\") {\n\t\ts = \"*/\" + s\n\t}\n\n\tif !strings.Contains(s, \":\") {\n\t\ts += \":*\"\n\t}\n\n\treturn s\n}\n\nfunc initDebugTags() {\n\topts.funcs = parseFilter(\"GOPASS_DEBUG_FUNCS\", padFunc)\n\topts.files = parseFilter(\"GOPASS_DEBUG_FILES\", padFile)\n}\n\nfunc getPosition(offset int) (fn, dir, file string, line int) { //nolint:nonamedreturns\n\tpc, file, line, ok := runtime.Caller(3 + offset)\n\tif !ok {\n\t\treturn \"\", \"\", \"\", 0\n\t}\n\n\tdirname := filepath.Base(filepath.Dir(file))\n\tfilename := filepath.Base(file)\n\n\tf := runtime.FuncForPC(pc)\n\n\treturn path.Base(f.Name()), dirname, filename, line\n}\n\nfunc checkFilter(filter map[string]bool, key string) bool {\n\t// check if exact match\n\tif v, ok := filter[key]; ok {\n\t\treturn v\n\t}\n\n\t// check globbing\n\tfor k, v := range filter {\n\t\tif m, _ := path.Match(k, key); m {\n\t\t\treturn v\n\t\t}\n\t}\n\n\t// check if tag \"all\" is enabled\n\tif v, ok := filter[\"all\"]; ok && v {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// Log logs a statement to Stderr (unless filtered) and the\n// debug log file (if enabled), but only if the verbosity\n// level is greater or equal to the given level.\n//\n// This is a no-op if the verbosity level is not high enough.\nfunc (n v) Log(f string, args ...any) {\n\tlogFn(int(n), 0, f, args...)\n}\n\n// Log logs a statement to Stderr (unless filtered) and the\n// debug log file (if enabled).\n//\n// This is a no-op if debugging is not enabled.\nfunc Log(f string, args ...any) {\n\tlogFn(0, 0, f, args...)\n}\n\n// LogN logs a statement to Stderr (unless filtered) and the\n// debug log file (if enabled). The offset will be applied to\n// the runtime position.\n//\n// This is a no-op if debugging is not enabled.\nfunc LogN(offset int, f string, args ...any) {\n\tlogFn(0, offset, f, args...)\n}\n\nfunc doNotLog(verbosity, offset int, f string, args ...any) {}\n\nfunc doLog(verbosity, offset int, f string, args ...any) {\n\t// if the log message is too verbose for the requested verbosity level, skip it\n\tif verbosity > opts.verbosity {\n\t\treturn\n\t}\n\n\tfn, dir, file, line := getPosition(offset)\n\n\tif len(f) == 0 || f[len(f)-1] != '\\n' {\n\t\tf += \"\\n\"\n\t}\n\n\ttype Shortener interface {\n\t\tStr() string\n\t}\n\n\ttype Safer interface {\n\t\tSafeStr() string\n\t}\n\n\targsi := make([]any, len(args))\n\tfor i, item := range args {\n\t\targsi[i] = item\n\t\tif secreter, ok := item.(Safer); ok && !opts.logSecrets {\n\t\t\targsi[i] = secreter.SafeStr()\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif shortener, ok := item.(Shortener); ok {\n\t\t\targsi[i] = shortener.Str()\n\t\t}\n\t}\n\n\tpos := fmt.Sprintf(\"%s/%s:%d\", dir, file, line)\n\n\tformatString := fmt.Sprintf(\"%d\\t%-20s\\t%-20s\\t%s\", opts.pid, pos, fn, f)\n\n\tdbgprint := func() {\n\t\tfmt.Fprintf(Stderr, formatString, argsi...)\n\t}\n\n\tif opts.logger != nil {\n\t\topts.logger.Printf(formatString, argsi...)\n\t}\n\n\tfilename := fmt.Sprintf(\"%s/%s:%d\", dir, file, line)\n\tif checkFilter(opts.files, filename) {\n\t\tdbgprint()\n\n\t\treturn\n\t}\n\n\tif checkFilter(opts.funcs, fn) {\n\t\tdbgprint()\n\t}\n}\n\n// IsEnabled returns true if debug logging was enabled.\n// This is useful to avoid expensive computations if debugging is not enabled.\nfunc IsEnabled() bool {\n\treturn enabled\n}\n"
  },
  {
    "path": "pkg/debug/debug_test.go",
    "content": "package debug\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc BenchmarkLogging(b *testing.B) {\n\tb.Setenv(\"GOPASS_DEBUG\", \"true\")\n\n\tinitDebug()\n\n\tfor i := 0; i < b.N; i++ { //nolint:intrange // b.N is evaluated at each iteration.\n\t\tLog(\"string\")\n\t}\n}\n\nfunc BenchmarkNoLogging(b *testing.B) {\n\t_ = os.Unsetenv(\"GOPASS_DEBUG\")\n\n\tinitDebug()\n\n\tfor i := 0; i < b.N; i++ { //nolint:intrange // b.N is evaluated at each iteration.\n\t\tLog(\"string\")\n\t}\n}\n\n// can not import out.Secret.\ntype testSecret string\n\nfunc (t testSecret) SafeStr() string {\n\treturn \"(elided)\"\n}\n\ntype testShort string\n\nfunc (t testShort) Str() string {\n\treturn \"shorter\"\n}\n\nfunc TestDebug(t *testing.T) {\n\ttd := t.TempDir()\n\tt.Cleanup(func() {\n\t\tinitDebug()\n\t})\n\n\tfn := filepath.Join(td, \"gopass.log\")\n\tt.Setenv(\"GOPASS_DEBUG_LOG\", fn)\n\tt.Setenv(\"GOPASS_DEBUG_LOG_SECRETS\", \"false\")\n\n\t// it's been already initialized, need to re-init\n\tassert.True(t, initDebug())\n\n\tLog(\"foo\")\n\tLog(\"%s\", testSecret(\"secret\"))\n\tLog(\"%s\", testShort(\"toolong\"))\n\n\tbuf, err := os.ReadFile(fn)\n\trequire.NoError(t, err)\n\n\tlogStr := string(buf)\n\tassert.Contains(t, logStr, \"foo\")\n\tassert.NotEqual(t, \"true\", os.Getenv(\"GOPASS_DEBUG_LOG_SECRETS\"))\n\tassert.NotContains(t, logStr, \"secret\")\n\tassert.NotContains(t, logStr, \"toolong\")\n\tassert.Contains(t, logStr, \"shorter\")\n}\n\nfunc TestDebugSecret(t *testing.T) {\n\ttd := t.TempDir()\n\tt.Cleanup(func() {\n\t\tinitDebug()\n\t})\n\n\tfn := filepath.Join(td, \"gopass.log\")\n\tt.Setenv(\"GOPASS_DEBUG_LOG\", fn)\n\tt.Setenv(\"GOPASS_DEBUG_LOG_SECRETS\", \"true\")\n\n\t// it's been already initialized, need to re-init\n\tassert.True(t, initDebug())\n\n\tassert.True(t, opts.logSecrets)\n\n\tLog(\"foo\")\n\tLog(\"%s\", testSecret(\"secret\"))\n\n\tbuf, err := os.ReadFile(fn)\n\trequire.NoError(t, err)\n\n\tlogStr := string(buf)\n\tassert.Contains(t, logStr, \"foo\")\n\tassert.Contains(t, logStr, \"secret\")\n}\n\nfunc TestDebugFilter(t *testing.T) {\n\ttd := t.TempDir()\n\tt.Cleanup(func() {\n\t\tinitDebug()\n\t})\n\n\tfn := filepath.Join(td, \"gopass.log\")\n\tt.Setenv(\"GOPASS_DEBUG_LOG\", fn)\n\tt.Setenv(\"GOPASS_DEBUG_FUNCS\", \"TestDebugFilter\")\n\tt.Setenv(\"GOPASS_DEBUG_FILES\", \"debug_test.go\")\n\n\tbuf := &bytes.Buffer{}\n\tStderr = buf\n\tdefer func() {\n\t\tStderr = os.Stderr\n\t}()\n\n\t// it's been already initialized, need to re-init\n\tassert.True(t, initDebug())\n\n\tLog(\"foo\")\n\tLog(\"%s\", testSecret(\"secret\"))\n\n\tfbuf, err := os.ReadFile(fn)\n\trequire.NoError(t, err)\n\n\tlogStr := string(fbuf)\n\tassert.Contains(t, logStr, \"foo\")\n\n\tstderrStr := buf.String()\n\tassert.Contains(t, stderrStr, \"TestDebugFilter\")\n}\n"
  },
  {
    "path": "pkg/debug/doc.go",
    "content": "// Package debug provides logging of debug information.\n//\n// This package is heavily based on github.com/restic/restic/internal/debug\npackage debug\n"
  },
  {
    "path": "pkg/debug/version.go",
    "content": "package debug\n\nimport (\n\trdebug \"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\nvar biFunc func() (*rdebug.BuildInfo, bool) = rdebug.ReadBuildInfo\n\n// ModuleVersion returns the version of the named module.\n// It reads the build info and parses the version of the requested module.\nfunc ModuleVersion(m string) semver.Version {\n\tbi, ok := biFunc()\n\tif !ok || bi == nil {\n\t\tLog(\"Failed to read build info\")\n\n\t\treturn semver.Version{}\n\t}\n\n\t// special case for gopass\n\tif m == \"github.com/gopasspw/gopass\" || strings.HasPrefix(m, \"github.com/gopasspw/gopass/\") {\n\t\tsv, err := semver.Parse(strings.TrimPrefix(bi.Main.Version, \"v\"))\n\t\tif err == nil {\n\t\t\treturn sv\n\t\t}\n\t\tLog(\"Failed to parse version %q for %q (gopass): %s\", bi.Main.Version, m, err)\n\t}\n\n\tfor _, dep := range bi.Deps {\n\t\t// We might be asking for a package that is part of a module\n\t\t// but not the module itself.\n\t\tif !strings.HasPrefix(m, dep.Path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tsv, err := semver.Parse(strings.TrimPrefix(dep.Version, \"v\"))\n\t\tif err != nil {\n\t\t\tLog(\"Failed to parse version %q for %q: %s\", dep.Version, dep.Path, err)\n\n\t\t\tif dep.Version == \"\" {\n\t\t\t\treturn semver.Version{}\n\t\t\t}\n\n\t\t\t// remove invalid characters\n\t\t\tdv := strings.Trim(strings.TrimPrefix(dep.Version, \"v\"), \"()\")\n\n\t\t\treturn semver.Version{\n\t\t\t\tBuild: []string{dv},\n\t\t\t}\n\t\t}\n\n\t\treturn sv\n\t}\n\n\tLog(\"no module %s found. Modules: %v\", m, paths(bi.Deps))\n\n\treturn semver.Version{}\n}\n\nfunc paths(mods []*rdebug.Module) []string {\n\tout := make([]string, 0, len(mods))\n\tfor _, m := range mods {\n\t\tout = append(out, m.Path)\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "pkg/debug/version_test.go",
    "content": "package debug\n\nimport (\n\t\"testing\"\n\n\trdebug \"runtime/debug\"\n\n\t\"github.com/blang/semver/v4\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestModuleVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tpkg    string\n\t\tmodule string\n\t\tmodver string\n\t\twant   semver.Version\n\t\tnoBI   bool\n\t}{\n\t\t{\n\t\t\tname:   \"valid module version semver\",\n\t\t\tpkg:    \"github.com/blang/semver/v4\",\n\t\t\tmodule: \"github.com/blang/semver/\",\n\t\t\tmodver: \"v4.0.0\",\n\t\t\twant:   semver.MustParse(\"4.0.0\"),\n\t\t},\n\t\t{\n\t\t\tname:   \"valid module version gopass\",\n\t\t\tmodule: \"github.com/gopasspw/gopass\",\n\t\t\tpkg:    \"github.com/gopasspw/gopass/internal/backend/storage/fs\",\n\t\t\tmodver: \"v4.0.0\",\n\t\t\twant:   semver.MustParse(\"4.0.0\"),\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid module version\",\n\t\t\tmodule: \"invalid/module\",\n\t\t\tmodver: \"\",\n\t\t\twant:   semver.Version{},\n\t\t},\n\t\t{\n\t\t\tname:   \"non-existent module\",\n\t\t\tmodule: \"non/existent/module\",\n\t\t\tmodver: \"\",\n\t\t\twant:   semver.Version{},\n\t\t},\n\t\t{\n\t\t\tname:   \"build-info failure\",\n\t\t\tmodule: \"non/existent/module\",\n\t\t\tmodver: \"\",\n\t\t\twant:   semver.Version{},\n\t\t\tnoBI:   true,\n\t\t},\n\n\t\t{\n\t\t\tname:   \"invalid version\",\n\t\t\tmodule: \"some/module/with/invalid/version\",\n\t\t\tmodver: \"devel\",\n\t\t\twant:   semver.Version{Build: []string{\"devel\"}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbiFunc = func() (*rdebug.BuildInfo, bool) {\n\t\t\t\treturn &rdebug.BuildInfo{\n\t\t\t\t\tMain: rdebug.Module{\n\t\t\t\t\t\tVersion: \"v4.0.0\",\n\t\t\t\t\t},\n\t\t\t\t\tDeps: []*rdebug.Module{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath:    tt.module,\n\t\t\t\t\t\t\tVersion: tt.modver,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, true\n\t\t\t}\n\t\t\tif tt.noBI {\n\t\t\t\tbiFunc = func() (*rdebug.BuildInfo, bool) {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\t\t\t}\n\t\t\task := tt.pkg\n\t\t\tif ask == \"\" {\n\t\t\t\task = tt.module\n\t\t\t}\n\t\t\tgot := ModuleVersion(ask)\n\t\t\tassert.True(t, got.Equals(tt.want), \"ModuleVersion(%s) = %v, want %v\", ask, got, tt.want)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/fsutil/fsutil.go",
    "content": "// Package fsutil provides some common file system utilities\n// for gopass. It is used to handle file paths, directories, and files.\npackage fsutil\n\nimport (\n\t\"bufio\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/appdir\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar reCleanFilename = regexp.MustCompile(`[^\\w\\d@.-]`)\n\n// CleanFilename strips all possibly suspicious characters from a filename.\n// WARNING: NOT suitable for pathnames as slashes will be stripped as well!\nfunc CleanFilename(in string) string {\n\treturn strings.Trim(reCleanFilename.ReplaceAllString(in, \"_\"), \"_ \")\n}\n\n// ExpandHomedir expands the tilde to the users home dir (if present).\nfunc ExpandHomedir(path string) string {\n\tif len(path) > 1 && path[:2] == \"~/\" {\n\t\tdir := filepath.Clean(appdir.UserHome() + path[1:])\n\t\tdebug.V(1).Log(\"Expanding %s to %s\", path, dir)\n\n\t\treturn dir\n\t}\n\n\tdebug.V(2).Log(\"No tilde found in %s\", path)\n\n\treturn path\n}\n\n// CleanPath resolves common aliases in a path and cleans it as much as possible.\n// It expands the tilde to the user's home directory and resolves relative paths.\nfunc CleanPath(path string) string {\n\t// Replace ~ with GOPASS_HOMEDIR if set (mainly for testing and experiments),\n\t// otherwise replace ~ with user's homedir if set. We expect any reference\n\t// to the user's homedir to be replaced with one of these two values.\n\tif len(path) > 1 && path[:2] == \"~/\" {\n\t\tif hd := os.Getenv(\"GOPASS_HOMEDIR\"); hd != \"\" {\n\t\t\treturn filepath.Clean(hd + path[2:])\n\t\t}\n\n\t\tif home, err := os.UserHomeDir(); err == nil {\n\t\t\treturn filepath.Clean(home + path[1:])\n\t\t}\n\t}\n\n\tif p, err := filepath.Abs(path); err == nil && !strings.HasPrefix(path, \"~\") {\n\t\treturn p\n\t}\n\n\treturn filepath.Clean(path)\n}\n\n// IsDir checks if a certain path exists and is a directory.\nfunc IsDir(path string) bool {\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\t// not found\n\t\t\treturn false\n\t\t}\n\n\t\tdebug.Log(\"failed to check dir %s: %s\\n\", path, err)\n\n\t\treturn false\n\t}\n\n\treturn fi.IsDir()\n}\n\n// IsFile checks if a certain path is actually a file.\nfunc IsFile(path string) bool {\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\t// not found\n\t\t\treturn false\n\t\t}\n\n\t\tdebug.Log(\"failed to check file %s: %s\\n\", path, err)\n\n\t\treturn false\n\t}\n\n\treturn fi.Mode().IsRegular()\n}\n\n// IsNonEmptyFile checks if a certain path is a regular file and\n// non-zero in size.\nfunc IsNonEmptyFile(path string) bool {\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\t// not found\n\t\t\treturn false\n\t\t}\n\n\t\tdebug.Log(\"failed to check file %s: %s\\n\", path, err)\n\n\t\treturn false\n\t}\n\n\tif !fi.Mode().IsRegular() {\n\t\treturn false\n\t}\n\n\treturn fi.Size() > 0\n}\n\n// IsEmptyDir checks if a certain path is an empty directory.\nfunc IsEmptyDir(path string) (bool, error) {\n\tempty := true\n\n\tif err := filepath.Walk(path, func(fp string, fi os.FileInfo, ferr error) error {\n\t\tif ferr != nil {\n\t\t\treturn ferr\n\t\t}\n\t\tif fi.IsDir() && (fi.Name() == \".\" || fi.Name() == \"..\") {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tif !fi.IsDir() {\n\t\t\tempty = false\n\t\t}\n\n\t\treturn nil\n\t}); err != nil {\n\t\treturn false, fmt.Errorf(\"failed to walk %s: %w\", path, err)\n\t}\n\n\treturn empty, nil\n}\n\n// Shred overwrites the given file with random data and deletes it.\n// The file is overwritten `runs` times. The last run is with zeros.\nfunc Shred(path string, runs int) error {\n\tfh, err := os.OpenFile(path, os.O_WRONLY, 0o600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open file %q: %w\", path, err)\n\t}\n\n\t// ignore the error. this is only taking effect if we error out.\n\tdefer func() {\n\t\t_ = fh.Close()\n\t}()\n\n\tfi, err := fh.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat file %q: %w\", path, err)\n\t}\n\n\tflen := fi.Size()\n\n\t// overwrite using pseudo-random data n-1 times and\n\t// use zeros in the last iteration\n\tbufFn := func() []byte {\n\t\tbuf := make([]byte, 1024)\n\t\t_, _ = rand.Read(buf)\n\n\t\treturn buf\n\t}\n\n\tfor i := range runs {\n\t\tif i >= runs-1 {\n\t\t\tbufFn = func() []byte {\n\t\t\t\treturn make([]byte, 1024)\n\t\t\t}\n\t\t}\n\n\t\tif _, err := fh.Seek(0, 0); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to seek to 0,0: %w\", err)\n\t\t}\n\n\t\tvar written int64\n\n\t\tfor written < flen {\n\t\t\tbuf := bufFn()\n\n\t\t\tn, err := fh.Write(buf[0:min(flen-written, int64(len(buf)))])\n\t\t\tif err != nil {\n\t\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\t\treturn fmt.Errorf(\"failed to write to file: %w\", err)\n\t\t\t\t}\n\t\t\t\t// end of file, should not happen\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\twritten += int64(n)\n\t\t}\n\t\t// if we fail to sync the written blocks to disk it'd be pointless\n\t\t// do any further loops\n\t\tif err := fh.Sync(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to sync to disk: %w\", err)\n\t\t}\n\t}\n\n\tif err := fh.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to close file after writing: %w\", err)\n\t}\n\n\tif err := os.Remove(path); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove %s: %w\", path, err)\n\t}\n\n\treturn nil\n}\n\n// FileContains searches the given file for the search string and returns true\n// iff it's an exact (substring) match.\nfunc FileContains(path, needle string) bool {\n\tfh, err := os.Open(path)\n\tif err != nil {\n\t\tdebug.Log(\"failed to open %q for reading: %s\", path, err)\n\n\t\treturn false\n\t}\n\n\tdefer func() {\n\t\t_ = fh.Close()\n\t}()\n\n\ts := bufio.NewScanner(fh)\n\tfor s.Scan() {\n\t\tif strings.Contains(s.Text(), needle) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// CopyFile copies a file from src to dst. Permissions will be preserved. It is expected to\n// fail if the destination does exist but is not writeable.\nfunc CopyFile(from, to string) error {\n\trdr, err := os.Open(from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open file %q for reading: %w\", from, err)\n\t}\n\tdefer func() {\n\t\t_ = rdr.Close()\n\t}()\n\n\trdrStat, err := rdr.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat open file %q: %w\", from, err)\n\t}\n\n\twrt, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, rdrStat.Mode())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open file %q for writing: %w\", to, err)\n\t}\n\tdefer func() {\n\t\t_ = wrt.Close()\n\t}()\n\n\tn, err := io.Copy(wrt, rdr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to copy content of %q to %q: %w\", from, to, err)\n\t}\n\n\tdebug.Log(\"copied %d bytes from %q to %q\", n, from, to)\n\n\t// sync permission, applies in case the destination did exist but had different perms\n\tif err := os.Chmod(to, rdrStat.Mode()); err != nil {\n\t\treturn fmt.Errorf(\"failed to sync permissions to %q: %w\", to, err)\n\t}\n\n\treturn nil\n}\n\n// CopyFileForce copies a file from src to dst. Permissions will be preserved. The destination\n// is removed before copying to avoid permission issues.\nfunc CopyFileForce(from, to string) error {\n\tif IsFile(to) {\n\t\tif err := os.Remove(to); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove %q: %w\", to, err)\n\t\t}\n\t}\n\n\treturn CopyFile(from, to)\n}\n"
  },
  {
    "path": "pkg/fsutil/fsutil_test.go",
    "content": "package fsutil\n\nimport (\n\t\"crypto/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCleanFilename(t *testing.T) {\n\tt.Parallel()\n\n\tm := map[string]string{\n\t\t`\"§$%&aÜÄ*&b%§\"'Ä\"c%$\"'\"`: \"a____b______c\",\n\t}\n\tfor k, v := range m {\n\t\tout := CleanFilename(k)\n\t\tt.Logf(\"%s -> %s / %s\", k, v, out)\n\n\t\tassert.Equal(t, v, out)\n\t}\n}\n\nfunc TestCleanPath(t *testing.T) {\n\ttempdir := t.TempDir()\n\tt.Setenv(\"GOPASS_HOMEDIR\", \"\")\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\thome = \"~\"\n\t}\n\n\tm := map[string]string{\n\t\t\".\":                                 \"\",\n\t\t\"/home/user/../bob/.password-store\": \"/home/bob/.password-store\",\n\t\t\"/home/user//.password-store\":       \"/home/user/.password-store\",\n\t\ttempdir + \"/foo.gpg\":                tempdir + \"/foo.gpg\",\n\t\t\"~/.password-store\":                 home + \"/.password-store\",\n\t}\n\n\tfor in, out := range m {\n\t\tgot := CleanPath(in)\n\n\t\tif strings.HasPrefix(out, \"~\") {\n\t\t\tassert.Equal(t, out, got)\n\n\t\t\tcontinue\n\t\t}\n\t\t// filepath.Abs turns /home/bob into C:\\home\\bob on Windows\n\t\tabsOut, err := filepath.Abs(out)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, absOut, got)\n\t}\n}\n\nfunc TestIsDir(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\n\tfn := filepath.Join(tempdir, \"foo\")\n\trequire.NoError(t, os.WriteFile(fn, []byte(\"bar\"), 0o644))\n\tassert.True(t, IsDir(tempdir))\n\tassert.False(t, IsDir(fn))\n\tassert.False(t, IsDir(filepath.Join(tempdir, \"non-existing\")))\n}\n\nfunc TestIsFile(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\n\tfn := filepath.Join(tempdir, \"foo\")\n\trequire.NoError(t, os.WriteFile(fn, []byte(\"bar\"), 0o644))\n\tassert.False(t, IsFile(tempdir))\n\tassert.True(t, IsFile(fn))\n}\n\nfunc TestShred(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\n\tfn := filepath.Join(tempdir, \"file\")\n\t// test successful shread\n\tfh, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0o644)\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 1024)\n\tfor range 10 * 1024 {\n\t\t_, _ = rand.Read(buf)\n\t\t_, _ = fh.Write(buf)\n\t}\n\n\trequire.NoError(t, fh.Close())\n\trequire.NoError(t, Shred(fn, 8))\n\tassert.False(t, IsFile(fn))\n\n\t// test failed\n\tfh, err = os.OpenFile(fn, os.O_WRONLY|os.O_CREATE, 0o400)\n\trequire.NoError(t, err)\n\n\tbuf = make([]byte, 1024)\n\tfor range 10 * 1024 {\n\t\t_, _ = rand.Read(buf)\n\t\t_, _ = fh.Write(buf)\n\t}\n\n\trequire.NoError(t, fh.Close())\n\trequire.Error(t, Shred(fn, 8))\n\tassert.True(t, IsFile(fn))\n}\n\nfunc TestIsEmptyDir(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\n\tfn := filepath.Join(tempdir, \"foo\", \"bar\", \"baz\", \"zab\")\n\trequire.NoError(t, os.MkdirAll(fn, 0o755))\n\n\tisEmpty, err := IsEmptyDir(tempdir)\n\trequire.NoError(t, err)\n\tassert.True(t, isEmpty)\n\n\tfn = filepath.Join(fn, \".config.yml\")\n\trequire.NoError(t, os.WriteFile(fn, []byte(\"foo\"), 0o644))\n\n\tisEmpty, err = IsEmptyDir(tempdir)\n\trequire.NoError(t, err)\n\tassert.False(t, isEmpty)\n}\n\nfunc TestCopyFile(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\n\tsfn := filepath.Join(tempdir, \"foo\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(sfn), 0o755))\n\trequire.NoError(t, os.WriteFile(sfn, []byte(\"foo\"), 0o644))\n\n\tdfn := filepath.Join(tempdir, \"bar\")\n\n\trequire.NoError(t, CopyFile(sfn, dfn))\n\n\t// try to overwrite existing file w/o write bit\n\tdfn = filepath.Join(tempdir, \"bar2\")\n\trequire.NoError(t, os.WriteFile(dfn, []byte(\"foo\"), 0o400))\n\trequire.Error(t, CopyFile(sfn, dfn))\n\trequire.NoError(t, CopyFileForce(sfn, dfn))\n}\n"
  },
  {
    "path": "pkg/fsutil/umask.go",
    "content": "package fsutil\n\nimport (\n\t\"os\"\n\t\"strconv\"\n)\n\n// Umask extracts the umask from the environment variables.\n// It checks for GOPASS_UMASK and PASSWORD_STORE_UMASK.\n// If neither is set, it returns the default umask of 0o77.\nfunc Umask() int {\n\tfor _, en := range []string{\"GOPASS_UMASK\", \"PASSWORD_STORE_UMASK\"} {\n\t\tum := os.Getenv(en)\n\t\tif um == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tiv, err := strconv.ParseInt(um, 8, 32)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif iv >= 0 && iv <= 0o777 {\n\t\t\treturn int(iv)\n\t\t}\n\t}\n\n\treturn 0o77\n}\n"
  },
  {
    "path": "pkg/fsutil/umask_test.go",
    "content": "package fsutil\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUmask(t *testing.T) {\n\tfor _, vn := range []string{\"GOPASS_UMASK\", \"PASSWORD_STORE_UMASK\"} {\n\t\tfor in, out := range map[string]int{\n\t\t\t\"002\":      0o2,\n\t\t\t\"0777\":     0o777,\n\t\t\t\"000\":      0,\n\t\t\t\"07557575\": 0o77,\n\t\t} {\n\t\t\tt.Run(vn, func(t *testing.T) {\n\t\t\t\tt.Setenv(vn, in)\n\t\t\t\tassert.Equal(t, out, Umask())\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/otp/otp.go",
    "content": "// Package otp provides functions to handle OTP secrets.\n// It can parse OTP secrets from various formats and generate QR codes for them.\npackage otp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/pquerna/otp\"\n)\n\n// Calculate will compute an OTP code from a given secret.\n// It will look for a field named \"otpauth\" or \"totp\" or \"hotp\".\n// If none is found it will fall back to the password.\n//\n//nolint:ireturn\nfunc Calculate(name string, sec gopass.Secret) (*otp.Key, error) {\n\totpURL := getOTPURL(sec)\n\n\tif otpURL != \"\" {\n\t\tdebug.Log(\"found otpauth url: %s\", out.Secret(otpURL))\n\n\t\treturn otp.NewKeyFromURL(otpURL) //nolint:wrapcheck\n\t}\n\n\t// check KV entry and fall back to password if we don't have one\n\n\t// TOTP\n\tif secKey, found := sec.Get(\"totp\"); found {\n\t\treturn parseOTP(\"totp\", secKey)\n\t}\n\n\t// HOTP\n\tif secKey, found := sec.Get(\"hotp\"); found {\n\t\treturn parseOTP(\"hotp\", secKey)\n\t}\n\n\tdebug.Log(\"no totp secret found, falling back to password\")\n\n\treturn parseOTP(\"totp\", sec.Password())\n}\n\nfunc getOTPURL(sec gopass.Secret) string {\n\t// check if we have a key-value entry\n\tif url, found := sec.Get(\"otpauth\"); found {\n\t\tif strings.HasPrefix(url, \"//\") {\n\t\t\turl = \"otpauth:\" + url\n\t\t}\n\n\t\treturn url\n\t}\n\n\t// if there is no KV entry check the body\n\tfor line := range strings.SplitSeq(sec.Body(), \"\\n\") {\n\t\tif strings.HasPrefix(line, \"otpauth://\") {\n\t\t\treturn line\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc parseOTP(typ string, secKey string) (*otp.Key, error) {\n\tif strings.HasPrefix(secKey, \"otpauth://\") {\n\t\tdebug.Log(\"parsing otpauth:// URL %q\", out.Secret(secKey))\n\n\t\tk, err := otp.NewKeyFromURL(secKey)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse otpauth URL: %w\", err)\n\t\t}\n\n\t\treturn k, nil\n\t}\n\n\tdebug.Log(\"assembling otpauth URL from secret only (%q), using defaults\", out.Secret(secKey))\n\n\t// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example\n\tkey, err := otp.NewKeyFromURL(fmt.Sprintf(\"otpauth://%s/new?secret=%s&issuer=gopass\", typ, secKey))\n\tif err != nil {\n\t\tdebug.Log(\"failed to parse OTP: %s\", out.Secret(secKey))\n\n\t\treturn nil, fmt.Errorf(\"invalid OTP secret: %w\", err)\n\t}\n\n\treturn key, nil\n}\n\n// WriteQRFile writes the given OTP key as a QR image to disk.\nfunc WriteQRFile(key *otp.Key, file string) error {\n\t// Convert TOTP key into a QR code encoded as a PNG image.\n\tvar buf bytes.Buffer\n\timg, err := key.Image(200, 200)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode qr code: %w\", err)\n\t}\n\n\tif err := png.Encode(&buf, img); err != nil {\n\t\treturn fmt.Errorf(\"failed to encode as png: %w\", err)\n\t}\n\n\tif err := os.WriteFile(file, buf.Bytes(), 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write QR code: %w\", err)\n\t}\n\n\treturn nil\n}\n\nvar (\n\t// ErrOathOTP is returned when the secret is not a valid OATH secret.\n\tErrOathOTP = fmt.Errorf(\"QR codes can only be generated for OATH OTPs\")\n\t// ErrType is returned when the secret is not a valid OTP type.\n\tErrType = fmt.Errorf(\"type assertion failed\")\n)\n"
  },
  {
    "path": "pkg/otp/otp_test.go",
    "content": "package otp\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/gopass\"\n\t\"github.com/gopasspw/gopass/pkg/gopass/secrets/secparse\"\n\t\"github.com/pquerna/otp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tpw         string = \"password\"\n\ttotpSecret string = \"GJWTGMTNN5YWW2TNPJXWG2DHMIFA\"\n\ttotpURL    string = \"otpauth://totp/example-otp.com?secret=2m32moqkjmzochgb&issuer=authenticator&digits=6\"\n)\n\nfunc TestCalculate(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := [][]byte{\n\t\t[]byte(totpSecret),\n\t\tfmt.Appendf(nil, \"%s\\ntotp: %s\", pw, totpSecret),\n\t\tfmt.Appendf(nil, \"%s\\n---\\ntotp: %s\", pw, totpSecret),\n\t\tfmt.Appendf(nil, \"%s\\n%s\", pw, totpURL),\n\t\tfmt.Appendf(nil, \"%s\\n---\\n%s\", pw, totpURL),\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(string(tc), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ts, err := secparse.Parse(tc)\n\t\t\trequire.NoError(t, err)\n\t\t\totp, err := Calculate(\"test\", s)\n\t\t\trequire.NoError(t, err, string(tc))\n\t\t\tassert.NotNil(t, otp, string(tc))\n\t\t})\n\t}\n}\n\nfunc TestWrite(t *testing.T) {\n\tt.Parallel()\n\n\ttd := t.TempDir()\n\n\ttf := filepath.Join(td, \"qr.png\")\n\n\tkey, err := otp.NewKeyFromURL(totpURL)\n\trequire.NoError(t, err)\n\trequire.NoError(t, WriteQRFile(key, tf))\n}\n\nfunc TestGetOTPURL(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tsec  gopass.Secret\n\t\turl  string\n\t}{\n\t\t{\n\t\t\tname: \"url-only-in-body\",\n\t\t\tsec:  secparse.MustParse(fmt.Sprintf(\"%s\\n%s\", pw, totpURL)),\n\t\t\turl:  totpURL,\n\t\t},\n\t\t{\n\t\t\tname: \"url-and-other-text-in-body\",\n\t\t\tsec:  secparse.MustParse(fmt.Sprintf(\"%s\\n%s\\nfoo bar\\nbaz\\n\", pw, totpURL)),\n\t\t\turl:  totpURL,\n\t\t},\n\t\t{\n\t\t\tname: \"url-in-kvp\",\n\t\t\tsec:  secparse.MustParse(fmt.Sprintf(\"%s\\notpauth: %s\\nfoo bar\\nbaz\\n\", pw, totpURL)),\n\t\t\turl:  totpURL,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tc.url, getOTPURL(tc.sec))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/otp/screenshot_others.go",
    "content": "//go:build !((arm || arm64 || amd64 || 386) && (linux || windows || (cgo && darwin) || freebsd || netbsd))\n\npackage otp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// ParseScreen will attempt to parse all available screen and will look for otpauth QR codes. It returns the first one\n// it has found.\nfunc ParseScreen(ctx context.Context) (string, error) {\n\treturn \"\", fmt.Errorf(\"not supported on your platform\")\n}\n"
  },
  {
    "path": "pkg/otp/screenshot_supported.go",
    "content": "//go:build (arm || arm64 || amd64 || 386) && (linux || windows || (cgo && darwin) || freebsd || netbsd)\n\npackage otp\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/kbinani/screenshot\"\n\t\"github.com/makiuchi-d/gozxing\"\n\t\"github.com/makiuchi-d/gozxing/qrcode\"\n)\n\n// ParseScreen will attempt to parse all available screen and will look for otpauth QR codes. It returns the first one\n// it has found.\nfunc ParseScreen(ctx context.Context) (string, error) {\n\tfor i := range screenshot.NumActiveDisplays() {\n\t\tout.Noticef(ctx, \"Scanning screen n°%d\", i)\n\n\t\timg, err := screenshot.CaptureDisplay(i)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tout.OKf(ctx, \"Area scanned on screen n°%d: %v\", i, img.Bounds())\n\t\tbmp, err := gozxing.NewBinaryBitmapFromImage(img)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tqrReader := qrcode.NewQRCodeReader()\n\t\tresult, err := qrReader.Decode(bmp, nil)\n\t\tif err != nil {\n\t\t\tout.Warningf(ctx, \"No QR code found while parsing screen n°%d.\", i)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tout.Noticef(ctx, \"Found a qrcode, checking.\")\n\t\tif qr := result.GetText(); strings.HasPrefix(qr, \"otpauth://\") {\n\t\t\tout.OKf(ctx, \"Found an otpauth:// QR code on screen n°%d (%v) for %s\", i, img.Bounds(),\n\t\t\t\t// otpauth:// is 10 char, we display label information, but not the parameters containing the secret\n\t\t\t\tqr[10:10+strings.Index(qr[10:], \"?\")])\n\n\t\t\treturn qr, nil\n\t\t}\n\t\tout.Warningf(ctx, \"Not an otpauth:// QR code, please make sure to only have your OTP qrcode displayed.\")\n\t}\n\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "pkg/passkey/passkey.go",
    "content": "// Package passkey implements the support of Webauthn credentials for authentication.\n// It is based on the W3C's \"Web Authentication: An API for accessing Public Key Credentials\"\n// https://www.w3.org/TR/webauthn-2/\npackage passkey\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// CredentialFlags for the credential parameters.\n// See: https://www.w3.org/TR/webauthn-2/#flags\ntype CredentialFlags struct {\n\tUserPresent     bool\n\tUserVerified    bool\n\tAttestationData bool\n\tExtensionData   bool\n}\n\n// Credential is a structure to store information and key of public key credential.\ntype Credential struct {\n\tID        string\n\tRp        string\n\tUserName  string\n\tAlgorithm string\n\tSecretKey *ecdsa.PrivateKey\n\tCounter   uint32\n\tFlags     CredentialFlags\n}\n\n// ClientData for signature.\n// See: https://www.w3.org/TR/webauthn-1/#sec-client-data\ntype ClientData struct {\n\tChallenge string `json:\"challenge\"`\n\tOrigin    string `json:\"origin\"`\n\tCredType  string `json:\"type\"`\n}\n\n// Response from a GetAssertion.\n// See: https://www.w3.org/TR/webauthn-1/#authenticatorGetAssertion-return-values\ntype Response struct {\n\tAuthenticatorData []byte `json:\"authdata\"`\n\tClientDataJSON    []byte `json:\"client_data_json\"`\n\tSignature         []byte `json:\"signature\"`\n\tLogin             string `json:\"login\"`\n}\n\nfunc authDataFlags(options CredentialFlags) uint8 {\n\tflags := uint8(0)\n\n\tif options.ExtensionData {\n\t\tflags |= 0b10000000\n\t}\n\n\tif options.AttestationData {\n\t\tflags |= 0b01000000\n\t}\n\n\tif options.UserVerified {\n\t\tflags |= 0b00000100\n\t}\n\n\tif options.UserPresent {\n\t\tflags |= 0b00000001\n\t}\n\n\treturn flags\n}\n\n// CreateCredential is an implementation of the authenticatorMakeCredential Operation.\n// See: https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred\nfunc CreateCredential(rp string, user string, flags CredentialFlags) (*Credential, error) {\n\trawID := make([]byte, 32)\n\t_, err := rand.Read(rawID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error while generating random ID: %w\", err)\n\t}\n\tprivateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\n\treturn &Credential{\n\t\tID:        base64.RawURLEncoding.EncodeToString(rawID),\n\t\tRp:        rp,\n\t\tUserName:  user,\n\t\tAlgorithm: \"ECDSA\",\n\t\tSecretKey: privateKey,\n\t\tCounter:   0,\n\t\tFlags:     flags,\n\t}, nil\n}\n\n// GetAssertion is an implementation of the authenticatorGetAssertion Operation.\n// See: https://www.w3.org/TR/webauthn-2/#authenticatorgetassertion\nfunc (cred *Credential) GetAssertion(challenge string, origin string) (*Response, error) {\n\tcredType := \"webauthn.get\"\n\tclientData := ClientData{\n\t\tCredType:  credType,\n\t\tChallenge: challenge,\n\t\tOrigin:    origin,\n\t}\n\tclientDataJSON, err := json.Marshal(clientData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error while reading client data: %w\", err)\n\t}\n\n\tclientDataHash := sha256.Sum256(clientDataJSON)\n\trpIDHash := sha256.Sum256([]byte(cred.Rp))\n\tflags := []byte{authDataFlags(cred.Flags)}\n\n\t// Signature counter is incremented according to https://www.w3.org/TR/webauthn-2/#signature-counter\n\tcred.Counter += 1\n\tsignCount := make([]byte, 4)\n\tbinary.BigEndian.PutUint32(signCount, cred.Counter)\n\tauthData := append(rpIDHash[:], flags...)\n\tauthData = append(authData[:], signCount[:]...)\n\tmessage := sha256.Sum256(append(authData[:], clientDataHash[:]...))\n\tsignature, serr := ecdsa.SignASN1(rand.Reader, cred.SecretKey, message[:])\n\tif err != nil {\n\t\treturn nil, serr\n\t}\n\n\treturn &Response{\n\t\tAuthenticatorData: authData,\n\t\tClientDataJSON:    clientDataJSON,\n\t\tSignature:         signature,\n\t\tLogin:             cred.UserName,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/passkey/passkey_test.go",
    "content": "package passkey_test\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/passkey\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar flags passkey.CredentialFlags = passkey.CredentialFlags{\n\tUserPresent:     true,\n\tUserVerified:    true,\n\tAttestationData: false,\n\tExtensionData:   false,\n}\n\nfunc TestCreate(t *testing.T) {\n\tcred, err := passkey.CreateCredential(\"test.com\", \"user\", flags)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test.com\", cred.Rp)\n\tassert.Equal(t, uint32(0), cred.Counter)\n}\n\nfunc TestGetAssertion(t *testing.T) {\n\tcred, err := passkey.CreateCredential(\"test.com\", \"user\", flags)\n\trequire.NoError(t, err)\n\n\trsp, err := cred.GetAssertion(base64.RawURLEncoding.EncodeToString([]byte(\"test_challenge\")), \"test\")\n\trequire.NoError(t, err)\n\n\t// Verify signature\n\tclientDataHash := sha256.Sum256(rsp.ClientDataJSON)\n\n\tauthData := rsp.AuthenticatorData\n\trequire.NoError(t, err)\n\n\tmessage := sha256.Sum256(append(authData[:], clientDataHash[:]...))\n\tassert.True(t, ecdsa.VerifyASN1(&cred.SecretKey.PublicKey, message[:], rsp.Signature))\n}\n"
  },
  {
    "path": "pkg/pinentry/cli/fallback.go",
    "content": "// Package cli provides a pinentry client that uses the terminal\n// for input and output. It is a drop-in replacement for the\n// pinentry program. It is used to ask for a passphrase or PIN\n// in the terminal.\npackage cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n)\n\n// Client is a pinentry CLI drop-in.\ntype Client struct {\n\trepeat bool\n}\n\n// New creates a new client.\nfunc New() *Client {\n\treturn &Client{repeat: false}\n}\n\n// Set is a no-op unless you're requesting a repeat.\nfunc (c *Client) Set(key string) error {\n\tif key == \"REPEAT\" {\n\t\tc.repeat = true\n\t}\n\n\treturn nil\n}\n\n// Option is a no-op.\nfunc (c *Client) Option(string) error {\n\treturn nil\n}\n\n// GetPINContext prompts for the pin in the terminal and returns the output.\n// The context is only used for tests.\nfunc (c *Client) GetPINContext(ctx context.Context) (string, error) {\n\tpw, err := termio.AskForPassword(ctx, \"your PIN\", c.repeat)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to ask for PIN: %w\", err)\n\t}\n\n\treturn pw, nil\n}\n\n// GetPIN prompts for the pin in the terminal and returns the output.\nfunc (c *Client) GetPIN() (string, error) {\n\treturn c.GetPINContext(context.TODO())\n}\n"
  },
  {
    "path": "pkg/pinentry/cli/fallback_test.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/pkg/termio\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tclient := New()\n\tassert.NotNil(t, client)\n\tassert.False(t, client.repeat)\n}\n\nfunc TestSet(t *testing.T) {\n\tclient := New()\n\n\terr := client.Set(\"REPEAT\")\n\trequire.NoError(t, err)\n\tassert.True(t, client.repeat)\n\n\terr = client.Set(\"OTHER\")\n\trequire.NoError(t, err)\n\tassert.True(t, client.repeat)\n}\n\nfunc TestOption(t *testing.T) {\n\tclient := New()\n\n\terr := client.Option(\"ANY\")\n\trequire.NoError(t, err)\n}\n\nfunc TestGetPIN(t *testing.T) {\n\tclient := New()\n\n\tctx := termio.WithPassPromptFunc(t.Context(), func(ctx context.Context, s string) (string, error) {\n\t\treturn \"1234\", nil\n\t})\n\n\tpin, err := client.GetPINContext(ctx)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"1234\", pin)\n}\n"
  },
  {
    "path": "pkg/protect/protect.go",
    "content": "//go:build !openbsd\n\n// Package protect provides an interface to the pledge syscall.\n// It is used to limit the system calls a process can make.\n// This is used to limit the attack surface of the process.\n// The pledge syscall is only available on OpenBSD.\n// It is not available on other systems.\n// This package is a no-op on other systems.\npackage protect\n\n// ProtectEnabled lets us know if we have protection or not.\n// It is false on all systems except OpenBSD.\nvar ProtectEnabled = false\n\n// Pledge on any other system than OpenBSD doesn't do anything.\nfunc Pledge(s string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/protect/protect_openbsd.go",
    "content": "//go:build openbsd\n\npackage protect\n\nimport \"golang.org/x/sys/unix\"\n\n// ProtectEnabled lets us know if we have protection or not.\n// It is true on OpenBSD.\nvar ProtectEnabled = true\n\n// Pledge on OpenBSD lets us \"promise\" to only run a subset of\n// system calls: http://man.openbsd.org/pledge\nfunc Pledge(s string) error {\n\treturn unix.PledgePromises(s)\n}\n"
  },
  {
    "path": "pkg/protect/protect_test.go",
    "content": "package protect\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProtect(t *testing.T) {\n\tt.Parallel()\n\n\trequire.NoError(t, Pledge(\"\"))\n}\n"
  },
  {
    "path": "pkg/pwgen/cryptic.go",
    "content": "package pwgen\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/muesli/crunchy\"\n)\n\n// ErrCrypticInvalid is returned when a password is invalid.\nvar ErrCrypticInvalid = fmt.Errorf(\"password does not satisfy all validators\")\n\n// Cryptic is a generator for hard-to-remember passwords as required by (too)\n// many sites. Prefer memorable or xkcd-style passwords, if possible.\n//\n// The generator can be configured with a character set, a length, a maximum\n// number of tries, and a list of validators.\ntype Cryptic struct {\n\tChars      string\n\tLength     int\n\tMaxTries   int\n\tValidators []func(string) error\n}\n\n// NewCryptic creates a new generator with sane defaults.\n// The default length is 16, and the default character set is digits, upper and lower case letters.\n// If symbols is true, the symbol character set is added.\nfunc NewCryptic(length int, symbols bool) *Cryptic {\n\tif length < 1 {\n\t\tlength = 16\n\t}\n\n\tchars := Digits + Upper + Lower\n\n\tif symbols {\n\t\tchars += Syms\n\t}\n\n\treturn &Cryptic{\n\t\tChars:    chars,\n\t\tLength:   length,\n\t\tMaxTries: 64,\n\t}\n}\n\n// NewCrypticForDomain tries to look up password rules for the given domain\n// or uses the default generator.\n// It will adjust the length and character set of the generator based on the rules.\nfunc NewCrypticForDomain(ctx context.Context, length int, domain string) *Cryptic {\n\tc := NewCryptic(length, true)\n\tr, found := pwrules.LookupRule(ctx, domain)\n\n\tdebug.Log(\"found rules for %s: %t\", domain, found)\n\n\tif !found {\n\t\treturn c\n\t}\n\n\tif r.Maxlen > 0 && c.Length > r.Maxlen {\n\t\tc.Length = r.Maxlen\n\t}\n\n\tif r.Minlen > 0 && c.Length < r.Minlen {\n\t\tc.Length = r.Minlen\n\t}\n\n\tif chars := charsFromRule(append(r.Required, r.Allowed...)...); chars != \"\" {\n\t\tc.Chars = chars\n\t}\n\n\tfor _, req := range r.Required {\n\t\tchars := charsFromRule(req)\n\t\tif req == \"\" || strings.TrimSpace(chars) == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tdebug.Log(\"Adding validator for %s: Requires %q -> %q\", domain, req, chars)\n\n\t\tc.Validators = append(c.Validators, func(pw string) error {\n\t\t\twantChars := charsFromRule(req)\n\t\t\tif wantChars == \"\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif containsAllClasses(pw, wantChars) {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"password %s does not contain any of %s: %w\", pw, chars, ErrCrypticInvalid)\n\t\t})\n\t}\n\t// if we have a required rule, we need to make sure the password is at least that long.\n\tif c.Length < len(r.Required) {\n\t\tc.Length = len(r.Required) + 1\n\t}\n\n\tif r.Maxconsec > 0 {\n\t\tc.Validators = append(c.Validators, func(pw string) error {\n\t\t\tif containsMaxConsecutive(pw, r.Maxconsec) {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"password %s contains more than %d consecutive characters: %w\", pw, r.Maxconsec, ErrCrypticInvalid)\n\t\t})\n\t}\n\n\tdebug.Log(\"initialized generator: %+v\", c)\n\n\treturn c\n}\n\nfunc charsFromRule(rules ...string) string {\n\tvar chars strings.Builder\n\n\tfor _, req := range rules {\n\t\tswitch req {\n\t\tcase \"lower\":\n\t\t\tchars.WriteString(Lower)\n\t\tcase \"upper\":\n\t\t\tchars.WriteString(Upper)\n\t\tcase \"digit\":\n\t\t\tchars.WriteString(Digits)\n\t\tcase \"special\":\n\t\t\tchars.WriteString(Syms)\n\t\tdefault:\n\t\t\tif strings.HasPrefix(req, \"[\") && strings.HasSuffix(req, \"]\") {\n\t\t\t\tchars.WriteString(strings.Trim(req, \"[]\"))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn uniqueChars(chars.String())\n}\n\nfunc uniqueChars(in string) string {\n\t// a set of chars, not a charset\n\tcharSet := make(map[rune]struct{}, len(in))\n\tfor _, c := range in {\n\t\tcharSet[c] = struct{}{}\n\t}\n\n\tcharSlice := make([]string, 0, len(charSet))\n\tfor k := range charSet {\n\t\tcharSlice = append(charSlice, string(k))\n\t}\n\n\tsort.Strings(charSlice)\n\n\treturn strings.Join(charSlice, \"\")\n}\n\n// NewCrypticWithAllClasses returns a password generator that generates passwords\n// containing all available character classes.\n// This is useful for password policies that require a mix of character types.\nfunc NewCrypticWithAllClasses(length int, symbols bool) *Cryptic {\n\tc := NewCryptic(length, symbols)\n\tc.Validators = append(c.Validators, func(pw string) error {\n\t\tif containsAllClasses(pw, c.Chars) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"password does not contain all classes: %w\", ErrCrypticInvalid)\n\t})\n\n\treturn c\n}\n\n// NewCrypticWithCrunchy returns a password generator that only returns a\n// password if it's successfully validated with crunchy.\nfunc NewCrypticWithCrunchy(length int, symbols bool) *Cryptic {\n\tc := NewCryptic(length, symbols)\n\tc.MaxTries = 3\n\tvalidator := crunchy.NewValidator()\n\tc.Validators = append(c.Validators, validator.Check)\n\n\treturn c\n}\n\n// Password returns a single password from the generator.\n// It will try to generate a password that satisfies all validators.\n// If it fails after MaxTries, it will return an empty string.\nfunc (c *Cryptic) Password() string {\n\tround := 0\n\tmaxFn := func() bool {\n\t\tround++\n\n\t\tif c.MaxTries < 1 {\n\t\t\treturn false\n\t\t}\n\n\t\tif c.MaxTries == 0 && round >= 64 {\n\t\t\treturn true\n\t\t}\n\n\t\tif round > c.MaxTries {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t}\n\n\tfor {\n\t\tif maxFn() {\n\t\t\tdebug.Log(\"failed to generate password after %d rounds\", round)\n\n\t\t\treturn \"\"\n\t\t}\n\n\t\tpw := c.randomString()\n\t\tif c.isValid(pw) {\n\t\t\treturn pw\n\t\t}\n\t\tdebug.Log(\"generated invalid password %q, trying again (%d/%d)\", pw, round, c.MaxTries)\n\t}\n}\n\nfunc (c *Cryptic) isValid(pw string) bool {\n\tfor _, v := range c.Validators {\n\t\tif err := v(pw); err != nil {\n\t\t\tdebug.Log(\"failed to validate: %s\", err)\n\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (c *Cryptic) randomString() string {\n\tpw := &bytes.Buffer{}\n\tfor pw.Len() < c.Length {\n\t\t_ = pw.WriteByte(c.Chars[randomInteger(len(c.Chars))])\n\t}\n\n\treturn pw.String()\n}\n"
  },
  {
    "path": "pkg/pwgen/cryptic_test.go",
    "content": "package pwgen\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCrypticForDomain(t *testing.T) {\n\tt.Parallel()\n\n\trules := pwrules.AllRules()\n\tkeys := make([]string, 0, len(rules))\n\n\tfor k := range rules {\n\t\tkeys = append(keys, k)\n\t}\n\n\tsort.Strings(keys)\n\n\tfor _, domain := range keys {\n\t\tt.Run(domain, func(t *testing.T) {\n\t\t\tfor _, length := range []int{1, 4, 8, 100} {\n\t\t\t\ttcName := fmt.Sprintf(\"%s: generated password with %d chars\", domain, length)\n\t\t\t\tc := NewCrypticForDomain(config.NewContextInMemory(), length, domain)\n\t\t\t\tc.MaxTries = 1024\n\n\t\t\t\trequire.NotNil(t, c, tcName)\n\n\t\t\t\tpw := c.Password()\n\n\t\t\t\tassert.NotEmpty(t, pw, tcName)\n\t\t\t\tt.Logf(\"%s -> %s (%d)\", tcName, pw, len(pw))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUniqueChars(t *testing.T) {\n\tt.Parallel()\n\n\tfor in, out := range map[string]string{\n\t\t\"foobar\": \"abfor\",\n\t\t\"abced\":  \"abcde\",\n\t} {\n\t\tassert.Equal(t, out, uniqueChars(in))\n\t}\n}\n"
  },
  {
    "path": "pkg/pwgen/external.go",
    "content": "package pwgen\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshellquote \"github.com/kballard/go-shellquote\"\n)\n\nvar (\n\t// ErrNoExternal is returned when no external generator is set.\n\tErrNoExternal = fmt.Errorf(\"no external generator\")\n\t// ErrNoCommand is returned when no command is set.\n\tErrNoCommand = fmt.Errorf(\"no command\")\n)\n\n// GenerateExternal will invoke an external password generator,\n// if set, and return its output.\n// The external generator is configured via the GOPASS_EXTERNAL_PWGEN environment variable.\nfunc GenerateExternal(pwlen int) (string, error) {\n\tc := os.Getenv(\"GOPASS_EXTERNAL_PWGEN\")\n\tif c == \"\" {\n\t\treturn \"\", ErrNoExternal\n\t}\n\n\tcmdArgs, err := shellquote.Split(c)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to split %s: %w\", c, err)\n\t}\n\n\tif len(cmdArgs) < 1 {\n\t\treturn \"\", ErrNoCommand\n\t}\n\n\texe := cmdArgs[0]\n\targs := []string{}\n\n\tif len(cmdArgs) > 1 {\n\t\targs = cmdArgs[1:]\n\t}\n\n\targs = append(args, strconv.Itoa(pwlen))\n\n\tout, err := exec.Command(exe, args...).Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to execute %s %v: %w\", exe, args, err)\n\t}\n\n\treturn strings.TrimSpace(string(out)), nil\n}\n"
  },
  {
    "path": "pkg/pwgen/memorable.go",
    "content": "package pwgen\n\nimport \"strings\"\n\n// GenerateMemorablePassword will generate a memorable password\n// with a minimum length.\n// It will use a wordlist to generate the password.\n// If symbols is true, it will add symbols to the password.\n// If capitals is true, it will capitalize some words.\nfunc GenerateMemorablePassword(minLength int, symbols bool, capitals bool) string {\n\tvar sb strings.Builder\n\n\tupper := false\n\n\tfor sb.Len() < minLength {\n\t\t// when requesting uppercase, we randomly uppercase words\n\t\tif capitals && randomInteger(2) == 0 {\n\t\t\t// We control the input so we can safely ignore the linter.\n\t\t\tsb.WriteString(strings.Title(randomWord())) //nolint:staticcheck\n\n\t\t\tupper = true\n\t\t} else {\n\t\t\tsb.WriteString(randomWord())\n\t\t}\n\n\t\tsb.WriteByte(Digits[randomInteger(len(Digits))])\n\n\t\tif !symbols {\n\t\t\tcontinue\n\t\t}\n\n\t\tsb.WriteByte(Syms[randomInteger(len(Syms))])\n\t}\n\t// If there isn't already a capitalized word, capitalize the first letter\n\tif capitals && !upper {\n\t\tstr := sb.String()\n\n\t\treturn strings.Title(string(str[0])) + str[1:] //nolint:staticcheck\n\t}\n\n\treturn sb.String()\n}\n\nfunc randomWord() string {\n\treturn wordlist[randomInteger(len(wordlist))]\n}\n"
  },
  {
    "path": "pkg/pwgen/pwgen.go",
    "content": "// Package pwgen implements multiple popular password generate algorithms.\n// It supports creating classic cryptic passwords with different character\n// classes as well as more recent memorable approaches.\n//\n// Some methods try to ensure certain requirements are met and can be very slow.\npackage pwgen\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\n// ErrMaxTries is returned when the maximum number of tries is reached.\nvar ErrMaxTries = fmt.Errorf(\"maximum tries exceeded\")\n\n// Character classes.\nconst (\n\t// Digits is the class of digits.\n\tDigits = \"0123456789\"\n\t// Upper is the class of upper case letters.\n\tUpper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\t// Lower is the class of lower case letters.\n\tLower = \"abcdefghijklmnopqrstuvwxyz\"\n\t// Syms is the class of symbols.\n\tSyms = \"!\\\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~\"\n\t// Ambiq is the class of ambiguous characters.\n\tAmbiq = \"0ODQ1IlB8G6S5Z2\"\n\t// CharAlpha is the class of letters.\n\tCharAlpha = Upper + Lower\n\t// CharAlphaNum is the class of alpha-numeric characters.\n\tCharAlphaNum = Digits + Upper + Lower\n\t// CharAll is the class of all characters.\n\tCharAll = Digits + Upper + Lower + Syms\n)\n\n// GeneratePassword generates a random, hard-to-remember password.\nfunc GeneratePassword(length int, symbols bool) string {\n\tchars := Digits + Upper + Lower\n\tif symbols {\n\t\tchars += Syms\n\t}\n\n\tif c := os.Getenv(\"GOPASS_CHARACTER_SET\"); c != \"\" {\n\t\tchars = c\n\t}\n\n\treturn GeneratePasswordCharset(length, chars)\n}\n\n// GeneratePasswordCharset generates a random password from a given\n// set of characters.\n// It does not perform any checks on the generated password.\nfunc GeneratePasswordCharset(length int, chars string) string {\n\tc := NewCryptic(length, false)\n\tc.Chars = chars\n\n\treturn c.Password()\n}\n\n// GeneratePasswordWithAllClasses tries to enforce a password which\n// contains all character classes instead of only enabling them.\n// This is especially useful for broken (corporate) password policies\n// that mandate the use of certain character classes for no good reason.\n// It will try to generate a password that contains at least one character from each of the\n// enabled character classes (digits, upper, lower, symbols).\nfunc GeneratePasswordWithAllClasses(length int, symbols bool) (string, error) {\n\tc := NewCrypticWithAllClasses(length, symbols)\n\tif pw := c.Password(); pw != \"\" {\n\t\treturn pw, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"failed to generate matching password after %d rounds: %w\", c.MaxTries, ErrMaxTries)\n}\n\n// GeneratePasswordCharsetCheck generates a random password from a given\n// set of characters and validates the generated password with crunchy.\n// It will try to generate a password that passes the crunchy check.\nfunc GeneratePasswordCharsetCheck(length int, chars string) string {\n\tc := NewCrypticWithCrunchy(length, false)\n\tc.Chars = chars\n\n\treturn c.Password()\n}\n\n// GeneratePasswordCharsetStrict generates a random password from a given\n// set of characters and ensures that all detected character classes are\n// represented in the generated password.\n// It detects which character classes (digits, upper, lower, symbols) are present\n// in the charset and enforces that at least one character from each class appears\n// in the generated password.\nfunc GeneratePasswordCharsetStrict(length int, chars string) (string, error) {\n\tc := NewCryptic(length, false)\n\tc.Chars = chars\n\n\t// Detect which character classes are present in the charset\n\tvar classes []string\n\tif strings.ContainsAny(chars, Digits) {\n\t\tclasses = append(classes, Digits)\n\t}\n\tif strings.ContainsAny(chars, Upper) {\n\t\tclasses = append(classes, Upper)\n\t}\n\tif strings.ContainsAny(chars, Lower) {\n\t\tclasses = append(classes, Lower)\n\t}\n\tif strings.ContainsAny(chars, Syms) {\n\t\tclasses = append(classes, Syms)\n\t}\n\n\t// Add validator to ensure all detected classes are present\n\tc.Validators = append(c.Validators, func(pw string) error {\n\t\tif containsAllClasses(pw, classes...) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"password does not contain all character classes: %w\", ErrCrypticInvalid)\n\t})\n\n\tif pw := c.Password(); pw != \"\" {\n\t\treturn pw, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"failed to generate matching password after %d rounds: %w\", c.MaxTries, ErrMaxTries)\n}\n\n// Prune removes all characters in cutset from the input string.\nfunc Prune(in string, cutset string) string {\n\tout := make([]rune, 0, len(in))\n\n\tfor _, r := range in {\n\t\tif strings.Contains(cutset, string(r)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, r)\n\t}\n\n\treturn string(out)\n}\n"
  },
  {
    "path": "pkg/pwgen/pwgen_others_test.go",
    "content": "//go:build !windows\n\npackage pwgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPwgenExternal(t *testing.T) {\n\tt.Setenv(\"GOPASS_EXTERNAL_PWGEN\", \"echo foobar\")\n\n\tpw, err := GenerateExternal(4)\n\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar 4\", pw)\n}\n"
  },
  {
    "path": "pkg/pwgen/pwgen_test.go",
    "content": "package pwgen\n\nimport (\n\t\"bytes\"\n\tcrand \"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ExampleGenerateMemorablePassword() { //nolint:testableexamples\n\tfmt.Println(GenerateMemorablePassword(12, false, false))\n}\n\nfunc TestPwgen(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, sym := range []bool{true, false} {\n\t\tfor i := 1; i < 50; i++ {\n\t\t\tSyms := CharAlphaNum\n\t\t\tif sym {\n\t\t\t\tSyms = CharAll\n\t\t\t}\n\n\t\t\tassert.Len(t, GeneratePasswordCharset(i, Syms), i)\n\t\t}\n\t}\n}\n\nfunc TestPwgenCharset(t *testing.T) {\n\tt.Setenv(\"GOPASS_CHARACTER_SET\", \"a\")\n\n\tassert.Equal(t, \"aaaa\", GeneratePassword(4, true))\n\tassert.Empty(t, GeneratePasswordCharsetCheck(4, \"a\"))\n}\n\nfunc TestPwgenNoCrandFallback(t *testing.T) {\n\toldFallback := randFallback\n\toldReader := crand.Reader\n\tcrand.Reader = strings.NewReader(\"\")\n\n\tdefer func() {\n\t\tcrand.Reader = oldReader\n\t\trandFallback = oldFallback\n\t}()\n\n\toldOut := os.Stdout\n\tr, w, _ := os.Pipe()\n\tos.Stdout = w\n\tos.Stderr = w\n\tdone := make(chan string)\n\n\tgo func() {\n\t\tbuf := &bytes.Buffer{}\n\t\t_, _ = io.Copy(buf, r)\n\t\tdone <- buf.String()\n\t}()\n\n\t// if we seed math/rand with 1789, the first \"random number\" will be 42\n\trandFallback = rand.New(rand.NewSource(1789))\n\n\tn := randomInteger(1024)\n\n\trequire.NoError(t, w.Close())\n\n\tos.Stdout = oldOut\n\n\tassert.Equal(t, 42, n)\n\tassert.Equal(t, \"WARNING: No crypto/rand available. Falling back to PRNG\\n\", <-done)\n}\n\nfunc TestContainsAllClasses(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tpw      string\n\t\tclasses []string\n\t\tok      bool\n\t}{\n\t\t{\n\t\t\tpw:      \"foobar\",\n\t\t\tclasses: []string{Lower},\n\t\t\tok:      true,\n\t\t},\n\t\t{\n\t\t\tpw:      \"aB1$\",\n\t\t\tclasses: []string{Lower, Upper, Syms, Digits},\n\t\t\tok:      true,\n\t\t},\n\t\t{\n\t\t\tpw:      \"ab1$\",\n\t\t\tclasses: []string{Lower, Upper, Syms, Digits},\n\t\t\tok:      false,\n\t\t},\n\t} {\n\t\tt.Run(tc.pw, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tc.ok, containsAllClasses(tc.pw, tc.classes...))\n\t\t})\n\t}\n}\n\nfunc TestGeneratePasswordWithAllClasses(t *testing.T) {\n\tt.Parallel()\n\n\tpw, err := GeneratePasswordWithAllClasses(50, true)\n\trequire.NoError(t, err)\n\tassert.Len(t, pw, 50)\n}\n\nfunc TestGeneratePasswordCharsetStrict(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname    string\n\t\tlength  int\n\t\tcharset string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"all character classes\",\n\t\t\tlength:  20,\n\t\t\tcharset: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only digits and lowercase\",\n\t\t\tlength:  10,\n\t\t\tcharset: \"abcdefghijklmnopqrstuvwxyz0123456789\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only uppercase\",\n\t\t\tlength:  10,\n\t\t\tcharset: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"digits only\",\n\t\t\tlength:  6,\n\t\t\tcharset: \"0123456789\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"symbols and digits\",\n\t\t\tlength:  15,\n\t\t\tcharset: \"0123456789!@#$%^&*()\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tpw, err := GeneratePasswordCharsetStrict(tc.length, tc.charset)\n\t\t\tif tc.wantErr {\n\t\t\t\trequire.Error(t, err)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, pw, tc.length)\n\n\t\t\t// Verify all detected character classes are present\n\t\t\tif strings.ContainsAny(tc.charset, Digits) {\n\t\t\t\tassert.True(t, containsAllClasses(pw, Digits), \"password should contain at least one digit\")\n\t\t\t}\n\t\t\tif strings.ContainsAny(tc.charset, Upper) {\n\t\t\t\tassert.True(t, containsAllClasses(pw, Upper), \"password should contain at least one uppercase letter\")\n\t\t\t}\n\t\t\tif strings.ContainsAny(tc.charset, Lower) {\n\t\t\t\tassert.True(t, containsAllClasses(pw, Lower), \"password should contain at least one lowercase letter\")\n\t\t\t}\n\t\t\tif strings.ContainsAny(tc.charset, Syms) {\n\t\t\t\tassert.True(t, containsAllClasses(pw, Syms), \"password should contain at least one symbol\")\n\t\t\t}\n\n\t\t\t// Verify all characters are from the charset\n\t\t\tfor _, c := range pw {\n\t\t\t\tassert.Contains(t, tc.charset, string(c), \"password should only contain characters from charset\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGenerateMemorablePassword(t *testing.T) {\n\tt.Parallel()\n\n\tpw := GenerateMemorablePassword(20, false, false)\n\tassert.GreaterOrEqual(t, len(pw), 20)\n\tassert.Equal(t, pw, strings.ToLower(pw))\n}\n\nfunc TestGenerateMemorablePasswordCapital(t *testing.T) {\n\tt.Parallel()\n\n\tpw := GenerateMemorablePassword(20, false, true)\n\tassert.GreaterOrEqual(t, len(pw), 20)\n\tassert.NotEqual(t, pw, strings.ToLower(pw))\n}\n\nfunc TestPrune(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tIn     string\n\t\tCutset string\n\t\tOut    string\n\t}{\n\t\t{\n\t\t\t\"abc\",\n\t\t\t\"b\",\n\t\t\t\"ac\",\n\t\t},\n\t\t{\n\t\t\t\"01lZO\",\n\t\t\t\"01lO\",\n\t\t\t\"Z\",\n\t\t},\n\t} {\n\t\tassert.Equal(t, tc.Out, Prune(tc.In, tc.Cutset))\n\t}\n}\n\nfunc BenchmarkPwgen(b *testing.B) {\n\tfor n := 0; n < b.N; n++ { //nolint:intrange // b.N is evaluated at each iteration.\n\t\tGeneratePasswordCharset(24, CharAll)\n\t}\n}\n\nfunc BenchmarkPwgenCheck(b *testing.B) {\n\tfor n := 0; n < b.N; n++ { //nolint:intrange // b.N is evaluated at each iteration.\n\t\tGeneratePasswordCharsetCheck(24, CharAll)\n\t}\n}\n"
  },
  {
    "path": "pkg/pwgen/pwgen_windows_test.go",
    "content": "package pwgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPwgenExternal(t *testing.T) {\n\tt.Setenv(\"GOPASS_EXTERNAL_PWGEN\", \"powershell.exe -Command write-output 1234 #\")\n\tans, err := GenerateExternal(4)\n\tif err != nil {\n\t\tpanic(\"Unable to generate using external generator\")\n\t}\n\tassert.Equal(t, \"1234\", ans)\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/aliases.go",
    "content": "package pwrules\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/set\"\n)\n\n// LookupAliases looks up known aliases for the given domain.\n// It will check for both built-in and custom aliases.\nfunc LookupAliases(ctx context.Context, domain string) []string {\n\tcustomAliases := loadCustomAliases(ctx)\n\taliases := make([]string, 0, len(genAliases[domain])+len(customAliases[domain]))\n\taliases = append(aliases, genAliases[domain]...)\n\taliases = append(aliases, customAliases[domain]...)\n\tsort.Strings(aliases)\n\n\treturn aliases\n}\n\n// AllAliases returns all aliases, including built-in and custom aliases.\nfunc AllAliases(ctx context.Context) map[string][]string {\n\tcustomAliases := loadCustomAliases(ctx)\n\tall := make(map[string][]string, len(genAliases)+len(customAliases))\n\tfor k, v := range genAliases {\n\t\tall[k] = append(all[k], v...)\n\t}\n\n\tfor k, v := range customAliases {\n\t\tall[k] = append(all[k], v...)\n\t}\n\n\treturn all\n}\n\nfunc loadCustomAliases(ctx context.Context) map[string][]string {\n\tcfg, _ := config.FromContext(ctx)\n\tcustomAliases := make(map[string][]string, 128)\n\tfor _, k := range set.SortedFiltered(cfg.Keys(\"\"), func(k string) bool {\n\t\treturn strings.HasPrefix(k, \"domain-alias.\") && strings.HasSuffix(k, \".insteadof\")\n\t}) {\n\t\t// NB: we currently only support system, env, global or local <root> store level aliases\n\t\tfor _, from := range cfg.GetAll(k) {\n\t\t\tto := strings.TrimSuffix(strings.TrimPrefix(k, \"domain-alias.\"), \".insteadof\")\n\t\t\tdebug.Log(\"Loading alias: %q -> %q\", from, to)\n\t\t\tif e, found := customAliases[from]; found {\n\t\t\t\te = append(e, to)\n\t\t\t\tsort.Strings(e)\n\t\t\t\tcustomAliases[from] = e\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcustomAliases[from] = []string{to}\n\t\t}\n\t}\n\n\treturn customAliases\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/aliases_test.go",
    "content": "package pwrules\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLoadCustomRules(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := config.NewInMemory()\n\taliases := map[string]string{\n\t\t\"real.com\": \"alias.com\",\n\t\t\"real.de\":  \"copy.de\",\n\t}\n\n\tfor k, v := range aliases {\n\t\trequire.NoError(t, cfg.Set(\"\", \"domain-alias.\"+k+\".insteadof\", v))\n\t}\n\n\tctx := t.Context()\n\tctx = cfg.WithConfig(ctx)\n\n\ta := LookupAliases(ctx, \"alias.com\")\n\tassert.Equal(t, []string{\"real.com\"}, a)\n\n\ta = LookupAliases(ctx, \"copy.de\")\n\tassert.Equal(t, []string{\"real.de\"}, a)\n\n\tassert.Greater(t, len(AllAliases(ctx)), 256)\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/change.go",
    "content": "package pwrules\n\nimport \"context\"\n\nvar changeURLs = map[string]string{}\n\nfunc init() {\n\tfor k, v := range genChange {\n\t\t// filter out invalid entries\n\t\tif v == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tchangeURLs[k] = v\n\t}\n}\n\n// LookupChangeURL looks up a change URL, either directly or through\n// one of its known aliases.\nfunc LookupChangeURL(ctx context.Context, domain string) string {\n\tif u, found := changeURLs[domain]; found {\n\t\treturn u\n\t}\n\n\tfor _, alias := range LookupAliases(ctx, domain) {\n\t\tif u, found := changeURLs[alias]; found {\n\t\t\treturn u\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/change_test.go",
    "content": "package pwrules\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLookupChangeURL(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tassert.Equal(t, \"https://account.gmx.net/ciss/security/edit/passwordChange\", LookupChangeURL(ctx, \"gmx.net\"))\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/gen.go",
    "content": "//go:build ignore\n\n// This program generates pwrules_gen.go. It can be invoked by running\n// go generate.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n\t\"github.com/gopasspw/gopass/pkg/pwgen/pwrules\"\n)\n\nconst (\n\taliasURL  = \"https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/shared-credentials.json\"\n\tchangeURL = \"https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/change-password-URLs.json\"\n\trulesURL  = \"https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/password-rules.json\"\n)\n\nfunc main() {\n\taliases, err := fetchAliases()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tchanges, err := fetchChangeURLs()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tjsonRules, err := fetchRules()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tf, err := os.Create(\"pwrules_gen.go\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer f.Close()\n\n\trules := parseRules(jsonRules)\n\n\tpkgTpl.Execute(f, struct {\n\t\tTimestamp time.Time\n\t\tURLs      []string\n\t\tAliases   map[string][]string\n\t\tChanges   map[string]string\n\t\tRules     map[string]pwrules.Rule\n\t}{\n\t\tTimestamp: time.Now().UTC(),\n\t\tURLs: []string{\n\t\t\taliasURL,\n\t\t\tchangeURL,\n\t\t\trulesURL,\n\t\t},\n\t\tAliases: aliases,\n\t\tChanges: changes,\n\t\tRules:   rules,\n\t})\n}\n\ntype aliasRule struct {\n\tShared                 []string `json:\"shared\"`\n\tFrom                   []string `json:\"from\"`\n\tTo                     []string `json:\"to\"`\n\tFromDomainsAreObsolete bool     `json:\"fromDomainsAreObsolete\"`\n}\n\nfunc fetchAliases() (map[string][]string, error) {\n\tresp, err := http.Get(aliasURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar ja []aliasRule\n\tif err := json.NewDecoder(resp.Body).Decode(&ja); err != nil {\n\t\treturn nil, err\n\t}\n\taliases := make(map[string][]string, len(ja))\n\tfor _, as := range ja {\n\t\tfor _, a := range as.Shared {\n\t\t\taliases[a] = as.Shared\n\t\t}\n\t\tif len(as.Shared) > 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, f := range as.From {\n\t\t\taliases[f] = as.To\n\t\t}\n\t}\n\treturn aliases, nil\n}\n\nfunc fetchChangeURLs() (map[string]string, error) {\n\tresp, err := http.Get(changeURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar change map[string]string\n\tif err := json.NewDecoder(resp.Body).Decode(&change); err != nil {\n\t\treturn nil, err\n\t}\n\treturn change, nil\n}\n\ntype jsonRule struct {\n\tExact bool   `json:\"exact-domain-match-only\"`\n\tRules string `json:\"password-rules\"`\n}\n\nfunc fetchRules() (map[string]jsonRule, error) {\n\tvar src io.Reader\n\tif fn := os.Getenv(\"PWGEN_RULES_FILE\"); fn != \"\" {\n\t\tf, err := os.Open(fn)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrc = f\n\t} else {\n\t\tresp, err := http.Get(rulesURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tsrc = resp.Body\n\t}\n\n\tvar jr map[string]jsonRule\n\tif err := json.NewDecoder(&cleaningReader{src: src, ign: map[string]int{}}).Decode(&jr); err != nil {\n\t\treturn nil, err\n\t}\n\treturn jr, nil\n}\n\ntype cleaningReader struct {\n\tsrc io.Reader\n\trdr io.Reader\n\tign map[string]int // map of domains to ignore, value is number of lines to skip\n}\n\nfunc (c *cleaningReader) init() error {\n\tif c.rdr != nil {\n\t\treturn nil\n\t}\n\t// no need to do anything if the ignore list is empty\n\tif len(c.ign) < 1 {\n\t\tfmt.Println(\"ignore list is empty\")\n\t\tc.rdr = c.src\n\t\treturn nil\n\t}\n\n\tvar buf bytes.Buffer\n\tscanner := bufio.NewScanner(c.src)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tskip := 0\n\t\t// skip two broken entries. this is a terrible hack because\n\t\t// the JSON is not valid.\n\t\tfor needle, numSkip := range c.ign {\n\t\t\twant := fmt.Sprintf(\"\\\"%s\\\":\", needle)\n\t\t\tif strings.Contains(line, want) {\n\t\t\t\tdebug.Log(\"skipping %d lines after %s\\n\", numSkip, needle)\n\t\t\t\tskip = numSkip\n\t\t\t}\n\t\t}\n\t\t// the broken entries are three lines each. the first was already consumed\n\t\t// above, so we need to skip the next two lines to consume all of it.\n\t\tfor i := 0; i < skip; i++ {\n\t\t\tscanner.Scan()\n\t\t\tdebug.Log(\"Skipped line: %s\\n\", scanner.Text())\n\t\t}\n\t\tif skip > 0 {\n\t\t\tcontinue\n\t\t}\n\t\tbuf.WriteString(line)\n\t}\n\tc.rdr = bytes.NewReader(buf.Bytes())\n\treturn nil\n}\n\nfunc (c *cleaningReader) Read(p []byte) (n int, err error) {\n\tif err := c.init(); err != nil {\n\t\treturn 0, err\n\t}\n\treturn c.rdr.Read(p)\n}\n\nfunc parseRules(jr map[string]jsonRule) map[string]pwrules.Rule {\n\trules := make(map[string]pwrules.Rule, len(jr))\n\tfor domain, jr := range jr {\n\t\tr := pwrules.ParseRule(jr.Rules)\n\t\tr.Exact = jr.Exact\n\t\trules[domain] = r\n\t}\n\treturn rules\n}\n\n// cf. https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/\nvar pkgTpl = template.Must(template.New(\"\").Funcs(template.FuncMap{\n\t\"trimPrefix\": func(prefix, s string) string { return strings.TrimPrefix(s, prefix) },\n}).Parse(`// Code generated by go generate gen.go. DO NOT EDIT.\n// This package was generated by go generate gen.go at\n// {{ .Timestamp }}\n// using data from\n// {{- range .URLs }}\n// {{ . }}\n// {{- end }}\npackage pwrules\n\nvar genAliases = map[string][]string{\n{{- range $key, $value := .Aliases }}\n  \"{{ $key }}\": []string{\n  {{- range $value }}\n    \"{{ . }}\",\n  {{- end }}\n  },\n{{- end }}\n}\n\nvar genChange = map[string]string{\n{{- range $key, $value := .Changes }}\n\t\"{{ $key }}\": \"{{ $value }}\",\n{{- end }}\n}\n\nvar genRules = map[string]Rule{\n{{- range $key, $value := .Rules }}\n\t\"{{ $key }}\": {{ printf \"%#v\" $value | trimPrefix \"pwrules.\" }},\n{{- end }}\n}\n\n`))\n"
  },
  {
    "path": "pkg/pwgen/pwrules/pwrules.go",
    "content": "package pwrules\n\nimport (\n\t\"context\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\n//go:generate go run gen.go\n\nvar reChars = regexp.MustCompile(`(allowed|required):\\s*\\[(.*)\\](?:;|,)`)\n\n// AllRules returns all password rules.\nfunc AllRules() map[string]Rule {\n\treturn genRules\n}\n\n// LookupRule looks up a rule either directly or through one of its known\n// aliases.\nfunc LookupRule(ctx context.Context, domain string) (Rule, bool) {\n\tr, found := genRules[domain]\n\tif found {\n\t\treturn r, true\n\t}\n\n\tfor _, alias := range LookupAliases(ctx, domain) {\n\t\tif r, found := genRules[alias]; found {\n\t\t\treturn r, true\n\t\t}\n\t}\n\n\treturn Rule{}, false\n}\n\n// Rule is a password rule as defined by Apple.\n// See: https://developer.apple.com/password-rules/\ntype Rule struct {\n\tMinlen    int\n\tMaxlen    int\n\tRequired  []string\n\tAllowed   []string\n\tMaxconsec int\n\tExact     bool\n}\n\n// ParseRule parses a password rule.\n// NOTE: This is not a complete parser.\nfunc ParseRule(in string) Rule {\n\tr := Rule{}\n\n\tif reChars.MatchString(in) {\n\t\tm := reChars.FindStringSubmatch(in)\n\t\tif len(m) > 2 {\n\t\t\tre := \"[\" + m[2] + \"]\"\n\n\t\t\tswitch m[1] {\n\t\t\tcase \"required\":\n\t\t\t\tr.Required = append(r.Required, re)\n\t\t\tcase \"allowed\":\n\t\t\t\tr.Allowed = append(r.Allowed, re)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor part := range strings.SplitSeq(strings.TrimSuffix(in, \";\"), \";\") {\n\t\tp := strings.Split(part, \": \")\n\t\tif len(p) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar err error\n\n\t\tkey := strings.TrimSpace(p[0])\n\t\tstrVal := strings.TrimSpace(p[1])\n\t\tmaxVal := len(strVal)\n\n\t\tif i := strings.Index(strVal, \"[\"); i > 0 {\n\t\t\tmaxVal = i\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"minlength\":\n\t\t\tr.Minlen, err = strconv.Atoi(strVal)\n\t\tcase \"maxlength\":\n\t\t\tr.Maxlen, err = strconv.Atoi(strVal)\n\t\tcase \"max-consecutive\":\n\t\t\tr.Maxconsec, err = strconv.Atoi(strVal)\n\t\tcase \"required\":\n\t\t\tr.Required = append(r.Required, strings.Split(strVal[0:maxVal], \",\")...)\n\t\tcase \"allowed\":\n\t\t\tr.Allowed = append(r.Allowed, strings.Split(strVal[0:maxVal], \",\")...)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tdebug.Log(\"failed to parse %s for %s: %s\", strVal, key, err)\n\t\t}\n\t}\n\n\tr.Required = sanitize(r.Required)\n\tr.Allowed = sanitize(r.Allowed)\n\n\treturn r\n}\n\nfunc sanitize(in []string) []string {\n\tout := make([]string, 0, len(in))\n\n\tfor _, v := range in {\n\t\tv := strings.TrimSpace(v)\n\t\tif strings.HasPrefix(v, \"[\") && !strings.HasSuffix(v, \"]\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, v)\n\t}\n\n\tsort.Strings(out)\n\n\treturn out\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/pwrules_gen.go",
    "content": "// Code generated by go generate gen.go. DO NOT EDIT.\n// This package was generated by go generate gen.go at\n// 2025-11-12 21:40:07.249828517 +0000 UTC\n// using data from\n//\n// https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/shared-credentials.json\n//\n// https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/change-password-URLs.json\n//\n// https://raw.githubusercontent.com/apple/password-manager-resources/main/quirks/password-rules.json\npackage pwrules\n\nvar genAliases = map[string][]string{\n\t\"3docean.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"acmemarkets.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"airbnb.at\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.be\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.ca\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.ch\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.cl\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.cr\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.id\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.in\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.kr\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.nz\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.uk\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.co.ve\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.ar\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.au\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.bo\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.br\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.bz\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.co\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.ec\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.gt\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.hk\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.hn\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.mt\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.my\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.ni\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.pa\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.pe\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.py\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.sg\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.sv\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.tr\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.com.tw\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.cz\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.de\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.dk\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.es\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.fi\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.fr\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.gr\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.gy\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.hu\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.ie\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.is\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.it\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.jp\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.mx\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.nl\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.no\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.pl\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.pt\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.ru\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airbnb.se\": {\n\t\t\"airbnb.com.ar\",\n\t\t\"airbnb.com.au\",\n\t\t\"airbnb.at\",\n\t\t\"airbnb.be\",\n\t\t\"airbnb.com.bz\",\n\t\t\"airbnb.com.bo\",\n\t\t\"airbnb.com.br\",\n\t\t\"airbnb.ca\",\n\t\t\"airbnb.cl\",\n\t\t\"airbnb.com.co\",\n\t\t\"airbnb.co.cr\",\n\t\t\"airbnb.cz\",\n\t\t\"airbnb.dk\",\n\t\t\"airbnb.com.ec\",\n\t\t\"airbnb.com.sv\",\n\t\t\"airbnb.fi\",\n\t\t\"airbnb.fr\",\n\t\t\"airbnb.de\",\n\t\t\"airbnb.gr\",\n\t\t\"airbnb.com.gt\",\n\t\t\"airbnb.gy\",\n\t\t\"airbnb.com.hn\",\n\t\t\"airbnb.com.hk\",\n\t\t\"airbnb.hu\",\n\t\t\"airbnb.is\",\n\t\t\"airbnb.co.in\",\n\t\t\"airbnb.co.id\",\n\t\t\"airbnb.ie\",\n\t\t\"airbnb.it\",\n\t\t\"airbnb.jp\",\n\t\t\"airbnb.com.my\",\n\t\t\"airbnb.com.mt\",\n\t\t\"airbnb.mx\",\n\t\t\"airbnb.nl\",\n\t\t\"airbnb.co.nz\",\n\t\t\"airbnb.com.ni\",\n\t\t\"airbnb.no\",\n\t\t\"airbnb.com.pa\",\n\t\t\"airbnb.com.py\",\n\t\t\"airbnb.com.pe\",\n\t\t\"airbnb.pl\",\n\t\t\"airbnb.pt\",\n\t\t\"airbnb.ru\",\n\t\t\"airbnb.com.sg\",\n\t\t\"airbnb.co.kr\",\n\t\t\"airbnb.es\",\n\t\t\"airbnb.se\",\n\t\t\"airbnb.ch\",\n\t\t\"airbnb.com.tw\",\n\t\t\"airbnb.com.tr\",\n\t\t\"airbnb.co.uk\",\n\t\t\"airbnb.com\",\n\t\t\"airbnb.co.ve\",\n\t},\n\t\"airnewzealand.co.nz\": {\n\t\t\"airnewzealand.co.nz\",\n\t\t\"airnewzealand.com\",\n\t\t\"airnewzealand.com.au\",\n\t},\n\t\"airnewzealand.com\": {\n\t\t\"airnewzealand.co.nz\",\n\t\t\"airnewzealand.com\",\n\t\t\"airnewzealand.com.au\",\n\t},\n\t\"airnewzealand.com.au\": {\n\t\t\"airnewzealand.co.nz\",\n\t\t\"airnewzealand.com\",\n\t\t\"airnewzealand.com.au\",\n\t},\n\t\"albertsons.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"albertsonsmarket.com\": {\n\t\t\"albertsonsmarket.com\",\n\t\t\"amigosunited.com\",\n\t\t\"marketstreetunited.com\",\n\t\t\"unitedsupermarkets.com\",\n\t},\n\t\"alelo.com.br\": {\n\t\t\"alelo.com.br\",\n\t\t\"meualelo.com.br\",\n\t},\n\t\"amigosunited.com\": {\n\t\t\"albertsonsmarket.com\",\n\t\t\"amigosunited.com\",\n\t\t\"marketstreetunited.com\",\n\t\t\"unitedsupermarkets.com\",\n\t},\n\t\"ana.co.jp\": {\n\t\t\"ana.co.jp\",\n\t\t\"astyle.jp\",\n\t},\n\t\"andronicos.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"angel.co\": {\n\t\t\"wellfound.com\",\n\t},\n\t\"anthem.com\": {\n\t\t\"anthem.com\",\n\t\t\"sydneyhealth.com\",\n\t},\n\t\"appannie.com\": {\n\t\t\"data.ai\",\n\t},\n\t\"astyle.jp\": {\n\t\t\"ana.co.jp\",\n\t\t\"astyle.jp\",\n\t},\n\t\"audiojungle.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"auth.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"balduccis.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"bgg.cc\": {\n\t\t\"bgg.cc\",\n\t\t\"boardgamegeek.com\",\n\t\t\"rpggeek.com\",\n\t\t\"videogamegeek.com\",\n\t},\n\t\"boardgamegeek.com\": {\n\t\t\"bgg.cc\",\n\t\t\"boardgamegeek.com\",\n\t\t\"rpggeek.com\",\n\t\t\"videogamegeek.com\",\n\t},\n\t\"candyrect.com\": {\n\t\t\"candyrect.com\",\n\t\t\"nekochat.cn\",\n\t},\n\t\"carrsqc.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"centralfcu.com\": {\n\t\t\"centralfcu.org\",\n\t\t\"centralfcu.com\",\n\t},\n\t\"centralfcu.org\": {\n\t\t\"centralfcu.org\",\n\t\t\"centralfcu.com\",\n\t},\n\t\"check24.at\": {\n\t\t\"check24.at\",\n\t\t\"check24.com\",\n\t\t\"check24.de\",\n\t\t\"check24.es\",\n\t},\n\t\"check24.com\": {\n\t\t\"check24.at\",\n\t\t\"check24.com\",\n\t\t\"check24.de\",\n\t\t\"check24.es\",\n\t},\n\t\"check24.de\": {\n\t\t\"check24.at\",\n\t\t\"check24.com\",\n\t\t\"check24.de\",\n\t\t\"check24.es\",\n\t},\n\t\"check24.es\": {\n\t\t\"check24.at\",\n\t\t\"check24.com\",\n\t\t\"check24.de\",\n\t\t\"check24.es\",\n\t},\n\t\"codecanyon.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"commons.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"community.pioneerdj.com\": {\n\t\t\"rekordbox.com\",\n\t\t\"pioneerdj.com\",\n\t\t\"community.pioneerdj.com\",\n\t},\n\t\"coolblue.be\": {\n\t\t\"coolblue.nl\",\n\t\t\"coolblue.be\",\n\t\t\"coolblue.de\",\n\t},\n\t\"coolblue.de\": {\n\t\t\"coolblue.nl\",\n\t\t\"coolblue.be\",\n\t\t\"coolblue.de\",\n\t},\n\t\"coolblue.nl\": {\n\t\t\"coolblue.nl\",\n\t\t\"coolblue.be\",\n\t\t\"coolblue.de\",\n\t},\n\t\"dan.org\": {\n\t\t\"dan.org\",\n\t\t\"diversalertnetwork.org\",\n\t},\n\t\"digiromania.ro\": {\n\t\t\"digi.ro\",\n\t},\n\t\"discord.store\": {\n\t\t\"discordmerch.com\",\n\t\t\"discord.store\",\n\t},\n\t\"discordapp.com\": {\n\t\t\"discord.com\",\n\t},\n\t\"discordmerch.com\": {\n\t\t\"discordmerch.com\",\n\t\t\"discord.store\",\n\t},\n\t\"discovercard.com\": {\n\t\t\"discover.com\",\n\t},\n\t\"disney.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"disneyplus.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"disneystore.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"diversalertnetwork.org\": {\n\t\t\"dan.org\",\n\t\t\"diversalertnetwork.org\",\n\t},\n\t\"dmsguild.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"dnt.abine.com\": {\n\t\t\"dnt.abine.com\",\n\t\t\"ironvest.com\",\n\t},\n\t\"dq.com\": {\n\t\t\"dairyqueen.com\",\n\t},\n\t\"drivethrucards.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"drivethrucomics.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"drivethrufiction.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"drivethrurpg.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"ebay.at\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.be\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.ca\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.ch\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.cn\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.co.th\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.co.uk\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com.au\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com.hk\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com.my\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com.sg\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.com.tw\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.de\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.es\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.fr\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.ie\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.it\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.nl\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.ph\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.pl\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"ebay.vn\": {\n\t\t\"ebay.at\",\n\t\t\"ebay.be\",\n\t\t\"ebay.ca\",\n\t\t\"ebay.ch\",\n\t\t\"ebay.cn\",\n\t\t\"ebay.co.th\",\n\t\t\"ebay.co.uk\",\n\t\t\"ebay.com\",\n\t\t\"ebay.com.au\",\n\t\t\"ebay.com.hk\",\n\t\t\"ebay.com.my\",\n\t\t\"ebay.com.sg\",\n\t\t\"ebay.com.tw\",\n\t\t\"ebay.de\",\n\t\t\"ebay.es\",\n\t\t\"ebay.fr\",\n\t\t\"ebay.ie\",\n\t\t\"ebay.it\",\n\t\t\"ebay.nl\",\n\t\t\"ebay.ph\",\n\t\t\"ebay.pl\",\n\t\t\"ebay.vn\",\n\t},\n\t\"envato.com\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"epicgames.com\": {\n\t\t\"epicgames.com\",\n\t\t\"fortnite.com\",\n\t\t\"twinmotion.com\",\n\t\t\"unrealengine.com\",\n\t},\n\t\"espn.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"eventbrite.at\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.be\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.ca\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.ch\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.cl\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.co\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.com\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.de\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.dk\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.es\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.fi\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.fr\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.hk\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.ie\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.in\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.it\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.my\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.nl\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.ph\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.pt\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.se\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"eventbrite.sg\": {\n\t\t\"eventbrite.at\",\n\t\t\"eventbrite.be\",\n\t\t\"eventbrite.ca\",\n\t\t\"eventbrite.ch\",\n\t\t\"eventbrite.cl\",\n\t\t\"eventbrite.co\",\n\t\t\"eventbrite.com\",\n\t\t\"eventbrite.de\",\n\t\t\"eventbrite.dk\",\n\t\t\"eventbrite.es\",\n\t\t\"eventbrite.fi\",\n\t\t\"eventbrite.fr\",\n\t\t\"eventbrite.hk\",\n\t\t\"eventbrite.ie\",\n\t\t\"eventbrite.in\",\n\t\t\"eventbrite.it\",\n\t\t\"eventbrite.my\",\n\t\t\"eventbrite.nl\",\n\t\t\"eventbrite.ph\",\n\t\t\"eventbrite.pt\",\n\t\t\"eventbrite.se\",\n\t\t\"eventbrite.sg\",\n\t},\n\t\"express1040.com\": {\n\t\t\"taxhawk.com\",\n\t\t\"freetaxusa.com\",\n\t\t\"express1040.com\",\n\t},\n\t\"fancourier.ro\": {\n\t\t\"selfawb.ro\",\n\t},\n\t\"flyblade.com\": {\n\t\t\"blade.com\",\n\t},\n\t\"fortnite.com\": {\n\t\t\"epicgames.com\",\n\t\t\"fortnite.com\",\n\t\t\"twinmotion.com\",\n\t\t\"unrealengine.com\",\n\t},\n\t\"freetaxusa.com\": {\n\t\t\"taxhawk.com\",\n\t\t\"freetaxusa.com\",\n\t\t\"express1040.com\",\n\t},\n\t\"gamefaqs.com\": {\n\t\t\"gamefaqs.gamespot.com\",\n\t},\n\t\"gamepedia.com\": {\n\t\t\"fandom.com\",\n\t},\n\t\"gazduire.com.ro\": {\n\t\t\"admin.ro\",\n\t},\n\t\"gazduire.net\": {\n\t\t\"admin.ro\",\n\t},\n\t\"go.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"graphicriver.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"haggen.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"hawaiianairlines.com\": {\n\t\t\"alaskaair.com\",\n\t},\n\t\"hbo.com\": {\n\t\t\"max.com\",\n\t},\n\t\"hbomax.com\": {\n\t\t\"max.com\",\n\t},\n\t\"hbonow.com\": {\n\t\t\"max.com\",\n\t},\n\t\"heroku.com\": {\n\t\t\"verify.salesforce.com\",\n\t},\n\t\"hk.jobsdb.com\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"hulu.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"incubator.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"ing.de\": {\n\t\t\"ing.com\",\n\t},\n\t\"instagram.com\": {\n\t\t\"instagram.com\",\n\t\t\"threads.net\",\n\t\t\"threads.com\",\n\t},\n\t\"ironvest.com\": {\n\t\t\"dnt.abine.com\",\n\t\t\"ironvest.com\",\n\t},\n\t\"jewelosco.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"jobsdb.com\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"jobstreet.com\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"keypointcu.com\": {\n\t\t\"kpcu.com\",\n\t},\n\t\"kingsfoodmarkets.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"koboldpress.com\": {\n\t\t\"koboldpress.com\",\n\t\t\"labyrinth.talesofthevaliant.com\",\n\t},\n\t\"labyrinth.talesofthevaliant.com\": {\n\t\t\"koboldpress.com\",\n\t\t\"labyrinth.talesofthevaliant.com\",\n\t},\n\t\"letsdeel.com\": {\n\t\t\"deel.com\",\n\t},\n\t\"login.airfrance.com\": {\n\t\t\"login.airfrance.com\",\n\t\t\"login.flyingblue.com\",\n\t\t\"login.klm.com\",\n\t},\n\t\"login.flyingblue.com\": {\n\t\t\"login.airfrance.com\",\n\t\t\"login.flyingblue.com\",\n\t\t\"login.klm.com\",\n\t},\n\t\"login.klm.com\": {\n\t\t\"login.airfrance.com\",\n\t\t\"login.flyingblue.com\",\n\t\t\"login.klm.com\",\n\t},\n\t\"lrz.de\": {\n\t\t\"lrz.de\",\n\t\t\"mwn.de\",\n\t\t\"mytum.de\",\n\t\t\"tum.de\",\n\t\t\"tum.edu\",\n\t},\n\t\"marketstreetunited.com\": {\n\t\t\"albertsonsmarket.com\",\n\t\t\"amigosunited.com\",\n\t\t\"marketstreetunited.com\",\n\t\t\"unitedsupermarkets.com\",\n\t},\n\t\"mediawiki.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"mercadolibre.cl\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.co.cr\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.ar\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.bo\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.co\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.do\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.ec\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.gt\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.hn\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.mx\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.ni\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.pa\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.pe\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.py\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.sv\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.uy\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolibre.com.ve\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadolivre.com.br\": {\n\t\t\"mercadolivre.com\",\n\t},\n\t\"mercadopago.cl\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.ar\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.br\": {\n\t\t\"mercadolivre.com\",\n\t},\n\t\"mercadopago.com.co\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.ec\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.mx\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.pe\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.uy\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"mercadopago.com.ve\": {\n\t\t\"mercadolibre.com\",\n\t},\n\t\"meta.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"meualelo.com.br\": {\n\t\t\"alelo.com.br\",\n\t\t\"meualelo.com.br\",\n\t},\n\t\"monarch.com\": {\n\t\t\"monarch.com\",\n\t\t\"monarchmoney.com\",\n\t},\n\t\"monarchmoney.com\": {\n\t\t\"monarch.com\",\n\t\t\"monarchmoney.com\",\n\t},\n\t\"moneybird.de\": {\n\t\t\"moneybird.com\",\n\t},\n\t\"moneybird.nl\": {\n\t\t\"moneybird.com\",\n\t},\n\t\"mwn.de\": {\n\t\t\"lrz.de\",\n\t\t\"mwn.de\",\n\t\t\"mytum.de\",\n\t\t\"tum.de\",\n\t\t\"tum.edu\",\n\t},\n\t\"myjobstreet.jobstreet.co.id\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"myjobstreet.jobstreet.com.my\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"myjobstreet.jobstreet.com.ph\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"myjobstreet.jobstreet.com.sg\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"mytum.de\": {\n\t\t\"lrz.de\",\n\t\t\"mwn.de\",\n\t\t\"mytum.de\",\n\t\t\"tum.de\",\n\t\t\"tum.edu\",\n\t},\n\t\"nebula.app\": {\n\t\t\"nebula.tv\",\n\t},\n\t\"nekochat.cn\": {\n\t\t\"candyrect.com\",\n\t\t\"nekochat.cn\",\n\t},\n\t\"nextinpact.com\": {\n\t\t\"next.ink\",\n\t},\n\t\"nordpass.com\": {\n\t\t\"nordaccount.com\",\n\t},\n\t\"nordvpn.com\": {\n\t\t\"nordaccount.com\",\n\t},\n\t\"outreach.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"overstock.com\": {\n\t\t\"bedbathandbeyond.com\",\n\t},\n\t\"padmapper.com\": {\n\t\t\"padmapper.com\",\n\t\t\"zumper.com\",\n\t\t\"zumperrentals.com\",\n\t},\n\t\"parkmobile.us\": {\n\t\t\"parkmobile.io\",\n\t},\n\t\"pathfinderinfinite.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"pavilions.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"photodune.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"pinterest.at\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.ca\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.ch\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.cl\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.co.kr\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.co.uk\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.com\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.com.au\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.com.mx\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.de\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.dk\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.es\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.fr\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.ie\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.it\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.jp\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.nz\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.ph\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.pt\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.ru\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pinterest.se\": {\n\t\t\"pinterest.com\",\n\t\t\"pinterest.ca\",\n\t\t\"pinterest.co.uk\",\n\t\t\"pinterest.fr\",\n\t\t\"pinterest.de\",\n\t\t\"pinterest.es\",\n\t\t\"pinterest.com.au\",\n\t\t\"pinterest.se\",\n\t\t\"pinterest.ph\",\n\t\t\"pinterest.ch\",\n\t\t\"pinterest.com.mx\",\n\t\t\"pinterest.dk\",\n\t\t\"pinterest.pt\",\n\t\t\"pinterest.ru\",\n\t\t\"pinterest.it\",\n\t\t\"pinterest.at\",\n\t\t\"pinterest.jp\",\n\t\t\"pinterest.cl\",\n\t\t\"pinterest.ie\",\n\t\t\"pinterest.co.kr\",\n\t\t\"pinterest.nz\",\n\t},\n\t\"pioneerdj.com\": {\n\t\t\"rekordbox.com\",\n\t\t\"pioneerdj.com\",\n\t\t\"community.pioneerdj.com\",\n\t},\n\t\"placeit.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"postnl.be\": {\n\t\t\"postnl.nl\",\n\t\t\"postnl.be\",\n\t},\n\t\"postnl.nl\": {\n\t\t\"postnl.nl\",\n\t\t\"postnl.be\",\n\t},\n\t\"pretendo.cc\": {\n\t\t\"pretendo.network\",\n\t\t\"pretendo.cc\",\n\t},\n\t\"pretendo.network\": {\n\t\t\"pretendo.network\",\n\t\t\"pretendo.cc\",\n\t},\n\t\"profile.callofduty.com\": {\n\t\t\"s.activision.com\",\n\t\t\"profile.callofduty.com\",\n\t},\n\t\"proton.me\": {\n\t\t\"proton.me\",\n\t\t\"protonvpn.com\",\n\t\t\"protonmail.ch\",\n\t\t\"protonmail.com\",\n\t},\n\t\"protonmail.ch\": {\n\t\t\"proton.me\",\n\t\t\"protonvpn.com\",\n\t\t\"protonmail.ch\",\n\t\t\"protonmail.com\",\n\t},\n\t\"protonmail.com\": {\n\t\t\"proton.me\",\n\t\t\"protonvpn.com\",\n\t\t\"protonmail.ch\",\n\t\t\"protonmail.com\",\n\t},\n\t\"protonvpn.com\": {\n\t\t\"proton.me\",\n\t\t\"protonvpn.com\",\n\t\t\"protonmail.ch\",\n\t\t\"protonmail.com\",\n\t},\n\t\"quicken.com\": {\n\t\t\"quicken.com\",\n\t\t\"simplifimoney.com\",\n\t},\n\t\"randalls.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"raywenderlich.com\": {\n\t\t\"kodeco.com\",\n\t},\n\t\"rcs-rds.ro\": {\n\t\t\"digi.ro\",\n\t},\n\t\"redis.com\": {\n\t\t\"redis.com\",\n\t\t\"redislabs.com\",\n\t},\n\t\"redislabs.com\": {\n\t\t\"redis.com\",\n\t\t\"redislabs.com\",\n\t},\n\t\"rekordbox.com\": {\n\t\t\"rekordbox.com\",\n\t\t\"pioneerdj.com\",\n\t\t\"community.pioneerdj.com\",\n\t},\n\t\"rpggeek.com\": {\n\t\t\"bgg.cc\",\n\t\t\"boardgamegeek.com\",\n\t\t\"rpggeek.com\",\n\t\t\"videogamegeek.com\",\n\t},\n\t\"s.activision.com\": {\n\t\t\"s.activision.com\",\n\t\t\"profile.callofduty.com\",\n\t},\n\t\"safeway.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"scottscheapflights.com\": {\n\t\t\"going.com\",\n\t},\n\t\"sg.jobsdb.com\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"shaws.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"shopdisney.com\": {\n\t\t\"disney.com\",\n\t\t\"disneyplus.com\",\n\t\t\"disneystore.com\",\n\t\t\"espn.com\",\n\t\t\"go.com\",\n\t\t\"hulu.com\",\n\t\t\"shopdisney.com\",\n\t},\n\t\"simplifimoney.com\": {\n\t\t\"quicken.com\",\n\t\t\"simplifimoney.com\",\n\t},\n\t\"species.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"starmarket.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"steamcommunity.com\": {\n\t\t\"steampowered.com\",\n\t\t\"steamcommunity.com\",\n\t\t\"steamgames.com\",\n\t},\n\t\"steamgames.com\": {\n\t\t\"steampowered.com\",\n\t\t\"steamcommunity.com\",\n\t\t\"steamgames.com\",\n\t},\n\t\"steampowered.com\": {\n\t\t\"steampowered.com\",\n\t\t\"steamcommunity.com\",\n\t\t\"steamgames.com\",\n\t},\n\t\"storytellersvault.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"sydneyhealth.com\": {\n\t\t\"anthem.com\",\n\t\t\"sydneyhealth.com\",\n\t},\n\t\"taxhawk.com\": {\n\t\t\"taxhawk.com\",\n\t\t\"freetaxusa.com\",\n\t\t\"express1040.com\",\n\t},\n\t\"telegram.me\": {\n\t\t\"telegram.org\",\n\t},\n\t\"th.jobsdb.com\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"themeforest.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"threads.com\": {\n\t\t\"instagram.com\",\n\t\t\"threads.net\",\n\t\t\"threads.com\",\n\t},\n\t\"threads.net\": {\n\t\t\"instagram.com\",\n\t\t\"threads.net\",\n\t\t\"threads.com\",\n\t},\n\t\"ting.com\": {\n\t\t\"ting.com\",\n\t\t\"tingmobile.com\",\n\t},\n\t\"tingmobile.com\": {\n\t\t\"ting.com\",\n\t\t\"tingmobile.com\",\n\t},\n\t\"tomthumb.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"transferwise.com\": {\n\t\t\"wise.com\",\n\t},\n\t\"tum.de\": {\n\t\t\"lrz.de\",\n\t\t\"mwn.de\",\n\t\t\"mytum.de\",\n\t\t\"tum.de\",\n\t\t\"tum.edu\",\n\t},\n\t\"tum.edu\": {\n\t\t\"lrz.de\",\n\t\t\"mwn.de\",\n\t\t\"mytum.de\",\n\t\t\"tum.de\",\n\t\t\"tum.edu\",\n\t},\n\t\"tutsplus.com\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"tvnow.at\": {\n\t\t\"auth.rtl.de\",\n\t\t\"rtlplus.de\",\n\t\t\"rtlplus.com\",\n\t},\n\t\"tvnow.ch\": {\n\t\t\"auth.rtl.de\",\n\t\t\"rtlplus.de\",\n\t\t\"rtlplus.com\",\n\t},\n\t\"tvnow.de\": {\n\t\t\"auth.rtl.de\",\n\t\t\"rtlplus.de\",\n\t\t\"rtlplus.com\",\n\t},\n\t\"twinmotion.com\": {\n\t\t\"epicgames.com\",\n\t\t\"fortnite.com\",\n\t\t\"twinmotion.com\",\n\t\t\"unrealengine.com\",\n\t},\n\t\"twitter.com\": {\n\t\t\"x.com\",\n\t},\n\t\"unitedsupermarkets.com\": {\n\t\t\"albertsonsmarket.com\",\n\t\t\"amigosunited.com\",\n\t\t\"marketstreetunited.com\",\n\t\t\"unitedsupermarkets.com\",\n\t},\n\t\"unrealengine.com\": {\n\t\t\"epicgames.com\",\n\t\t\"fortnite.com\",\n\t\t\"twinmotion.com\",\n\t\t\"unrealengine.com\",\n\t},\n\t\"uspowerboating.com\": {\n\t\t\"uspowerboating.com\",\n\t\t\"ussailing.org\",\n\t},\n\t\"ussailing.org\": {\n\t\t\"uspowerboating.com\",\n\t\t\"ussailing.org\",\n\t},\n\t\"videogamegeek.com\": {\n\t\t\"bgg.cc\",\n\t\t\"boardgamegeek.com\",\n\t\t\"rpggeek.com\",\n\t\t\"videogamegeek.com\",\n\t},\n\t\"videohive.net\": {\n\t\t\"3docean.net\",\n\t\t\"audiojungle.net\",\n\t\t\"codecanyon.net\",\n\t\t\"envato.com\",\n\t\t\"graphicriver.net\",\n\t\t\"photodune.net\",\n\t\t\"placeit.net\",\n\t\t\"themeforest.net\",\n\t\t\"tutsplus.com\",\n\t\t\"videohive.net\",\n\t},\n\t\"vons.com\": {\n\t\t\"acmemarkets.com\",\n\t\t\"albertsons.com\",\n\t\t\"andronicos.com\",\n\t\t\"balduccis.com\",\n\t\t\"carrsqc.com\",\n\t\t\"haggen.com\",\n\t\t\"jewelosco.com\",\n\t\t\"kingsfoodmarkets.com\",\n\t\t\"pavilions.com\",\n\t\t\"randalls.com\",\n\t\t\"safeway.com\",\n\t\t\"shaws.com\",\n\t\t\"starmarket.com\",\n\t\t\"tomthumb.com\",\n\t\t\"vons.com\",\n\t},\n\t\"wacom.eu\": {\n\t\t\"wacom.com\",\n\t},\n\t\"wargamevault.com\": {\n\t\t\"drivethrucards.com\",\n\t\t\"drivethrucomics.com\",\n\t\t\"drivethrufiction.com\",\n\t\t\"drivethrurpg.com\",\n\t\t\"dmsguild.com\",\n\t\t\"pathfinderinfinite.com\",\n\t\t\"storytellersvault.com\",\n\t\t\"wargamevault.com\",\n\t},\n\t\"watchnebula.com\": {\n\t\t\"nebula.tv\",\n\t},\n\t\"wikia.com\": {\n\t\t\"fandom.com\",\n\t},\n\t\"wikia.org\": {\n\t\t\"fandom.com\",\n\t},\n\t\"wikibooks.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikicities.com\": {\n\t\t\"fandom.com\",\n\t},\n\t\"wikidata.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikimania.wikimedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikinews.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikipedia.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikiquote.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikisource.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikiversity.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wikivoyage.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"wiktionary.org\": {\n\t\t\"wikipedia.org\",\n\t\t\"mediawiki.org\",\n\t\t\"wikibooks.org\",\n\t\t\"wikidata.org\",\n\t\t\"wikinews.org\",\n\t\t\"wikiquote.org\",\n\t\t\"wikisource.org\",\n\t\t\"wikiversity.org\",\n\t\t\"wikivoyage.org\",\n\t\t\"wiktionary.org\",\n\t\t\"commons.wikimedia.org\",\n\t\t\"meta.wikimedia.org\",\n\t\t\"incubator.wikimedia.org\",\n\t\t\"outreach.wikimedia.org\",\n\t\t\"species.wikimedia.org\",\n\t\t\"wikimania.wikimedia.org\",\n\t\t\"auth.wikimedia.org\",\n\t},\n\t\"www.seek.co.nz\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"www.seek.com.au\": {\n\t\t\"login.seek.com\",\n\t},\n\t\"www.vistaprint.ca\": {\n\t\t\"account.vistaprint.com\",\n\t},\n\t\"youneedabudget.com\": {\n\t\t\"ynab.com\",\n\t},\n\t\"zumper.com\": {\n\t\t\"padmapper.com\",\n\t\t\"zumper.com\",\n\t\t\"zumperrentals.com\",\n\t},\n\t\"zumperrentals.com\": {\n\t\t\"padmapper.com\",\n\t\t\"zumper.com\",\n\t\t\"zumperrentals.com\",\n\t},\n}\n\nvar genChange = map[string]string{\n\t\"11st.co.kr\":                          \"https://www.11st.co.kr/register/popupModifyPWD.tmall\",\n\t\"1800contacts.com\":                    \"https://www.1800contacts.com/account/settings\",\n\t\"247sports.com\":                       \"https://247sports.com/my/settings/password/\",\n\t\"500px.com\":                           \"https://web.500px.com/settings/account/security\",\n\t\"aa.com\":                              \"https://www.aa.com/loyalty/profile/information\",\n\t\"aarp.org\":                            \"https://secure.aarp.org/account/editaccount?request_locale=en&nu=t\",\n\t\"account.magento.com\":                 \"https://account.magento.com/customer/account/changepassword\",\n\t\"account.publishing.service.gov.uk\":   \"https://www.account.publishing.service.gov.uk/account/edit/password\",\n\t\"accounts.panic.com\":                  \"https://accounts.panic.com/password_set\",\n\t\"accounts.pcid.ca\":                    \"https://accounts.pcid.ca/forgot-password\",\n\t\"acehardware.com\":                     \"https://www.acehardware.com/myaccount#settings\",\n\t\"acorns.com\":                          \"https://app.acorns.com/settings/change-password\",\n\t\"adafruit.com\":                        \"https://accounts.adafruit.com/settings/password\",\n\t\"adobe.com\":                           \"https://account.adobe.com/security\",\n\t\"adultfriendfinder.com\":               \"https://adultfriendfinder.com/p/update.cgi?p=my_account_update_account_password\",\n\t\"ae.com\":                              \"https://www.ae.com/myaccount\",\n\t\"aeon.co.jp\":                          \"https://www.aeon.co.jp/app/settings/profile/password/\",\n\t\"aerlingus.com\":                       \"https://www.aerlingus.com/html/user-profile.html\",\n\t\"aesop.com\":                           \"https://www.aesop.com/my-account\",\n\t\"airnewzealand.com\":                   \"https://www.airnewzealand.com/membership/profile/security/password\",\n\t\"alaskaair.com\":                       \"https://www.alaskaair.com/www2/ssl/myalaskaair/myalaskaair.aspx?view=myinformation&tab=email\",\n\t\"aliexpress.com\":                      \"https://login.aliexpress.com/\",\n\t\"allegro.pl\":                          \"https://allegro.pl/moje-allegro/moje-konto/logowanie-i-haslo\",\n\t\"alliantcreditunion.com\":              \"https://www.alliantcreditunion.com/OnlineBanking/Settings/AccessAndSecurity/ChangePassword.aspx\",\n\t\"allianz.com.br\":                      \"https://www.allianz.com.br/alteracao-de-password-ecliente\",\n\t\"allrecipes.com\":                      \"https://www.allrecipes.com/account/profile#/change-password\",\n\t\"alternate.de\":                        \"https://www.alternate.de/html/myAccount/account/basicData.html\",\n\t\"amazon.ae\":                           \"https://www.amazon.ae/ax/account/manage\",\n\t\"amazon.ca\":                           \"https://www.amazon.ca/ax/account/manage\",\n\t\"amazon.co.jp\":                        \"https://www.amazon.co.jp/ax/account/manage\",\n\t\"amazon.co.uk\":                        \"https://www.amazon.co.uk/ax/account/manage\",\n\t\"amazon.com\":                          \"https://www.amazon.com/ax/account/manage\",\n\t\"amazon.com.au\":                       \"https://www.amazon.com.au/ax/account/manage\",\n\t\"amazon.com.br\":                       \"https://www.amazon.com.br/ax/account/manage\",\n\t\"amazon.com.mx\":                       \"https://www.amazon.com.mx/ax/account/manage\",\n\t\"amazon.com.tr\":                       \"https://www.amazon.com.tr/ax/account/manage\",\n\t\"amazon.de\":                           \"https://www.amazon.de/ax/account/manage\",\n\t\"amazon.es\":                           \"https://www.amazon.es/ax/account/manage\",\n\t\"amazon.fr\":                           \"https://www.amazon.fr/ax/account/manage\",\n\t\"amazon.in\":                           \"https://www.amazon.in/ax/account/manage\",\n\t\"amazon.it\":                           \"https://www.amazon.it/ax/account/manage\",\n\t\"amazon.nl\":                           \"https://www.amazon.nl/ax/account/manage\",\n\t\"amazon.pl\":                           \"https://www.amazon.pl/ax/account/manage\",\n\t\"amazon.sa\":                           \"https://www.amazon.sa/ax/account/manage\",\n\t\"amazon.se\":                           \"https://www.amazon.se/ax/account/manage\",\n\t\"amazon.sg\":                           \"https://www.amazon.sg/ax/account/manage\",\n\t\"amctheatres.com\":                     \"https://www.amctheatres.com/amcstubs/account\",\n\t\"americanexpress.com\":                 \"https://www.americanexpress.com/en-us/account/password/reset\",\n\t\"ana.co.jp\":                           \"https://cam.ana.co.jp/psz/us/amc_us.jsp?index=105\",\n\t\"anatel.gov.br\":                       \"https://apps.anatel.gov.br/AnatelConsumidor/ConsumidorEditar.aspx\",\n\t\"ancestry.com\":                        \"https://www.ancestry.com/account/security/password\",\n\t\"aol.com\":                             \"https://login.aol.com/account/change-password\",\n\t\"apartments.com\":                      \"https://www.apartments.com/my-account/#\",\n\t\"api.id.me\":                           \"https://account.id.me/signin/password\",\n\t\"app.link\":                            \"https://dashboard.branch.io/account-settings/user\",\n\t\"app.parkmobile.io\":                   \"https://app.parkmobile.io/account/settings\",\n\t\"apple.com\":                           \"https://appleid.apple.com/account/manage\",\n\t\"archive.org\":                         \"https://archive.org/account/index.php?settings=1\",\n\t\"arlt.com\":                            \"https://www.arlt.com/mein-passwort/\",\n\t\"arxiv.org\":                           \"https://arxiv.org/user/change_own_password\",\n\t\"astonmartinf1.com\":                   \"https://auth.astonmartinf1.com/Dashboard/ChangePassword\",\n\t\"atlassian.com\":                       \"https://id.atlassian.com/manage-profile/security\",\n\t\"atlassian.net\":                       \"https://id.atlassian.com/manage-profile/security\",\n\t\"att.com\":                             \"https://www.att.com/acctmgmt/profile/overview\",\n\t\"att.net\":                             \"https://www.att.com/acctmgmt/profile/overview\",\n\t\"attn.tv\":                             \"https://ui.attentivemobile.com/forgot-password\",\n\t\"auction.co.kr\":                       \"https://memberssl.auction.co.kr/membership/MyInfo/MyInfo.aspx\",\n\t\"auctionzip.com\":                      \"https://www.auctionzip.com/cgi-bin/userpanel.cgi?mode=3\",\n\t\"autodesk.com\":                        \"https://accounts.autodesk.com/Profile/Security\",\n\t\"b2c.voegol.com.br\":                   \"https://b2c.voegol.com.br/minhas-viagens/meu-perfil\",\n\t\"bamboohr.com\":                        \"https://employeewe.bamboohr.com/dashboard/password.php\",\n\t\"bancochile.cl\":                       \"https://portalpersonas.bancochile.cl/mibancochile-web/front/persona/index.html#/mi-perfil/datos-seguridad\",\n\t\"bandcamp.com\":                        \"https://bandcamp.com/settings#password\",\n\t\"bankofamerica.com\":                   \"https://secure.bankofamerica.com/auth/security-center/main/?activity=changePasscode\",\n\t\"bathandbodyworks.com\":                \"https://www.bathandbodyworks.com/my-account/edit-profile\",\n\t\"bbq-grill-world.de\":                  \"https://www.bbq-grill-world.de/customer/account/edit/changepass/1/\",\n\t\"bedbathandbeyond.com\":                \"https://www.bedbathandbeyond.com/store/account/personalinfo\",\n\t\"belk.com\":                            \"https://www.belk.com/account-edit-profile/\",\n\t\"benefitslogin.discoverybenefits.com\": \"https://benefitslogin.discoverybenefits.com/Profile/UpdatePassword.aspx\",\n\t\"berlet.de\":                           \"https://www.berlet.de/mein-konto.htm#my-account--edit-pass\",\n\t\"bestbuy.com\":                         \"https://www.bestbuy.com/identity/accountSettings/page/password\",\n\t\"biblegateway.com\":                    \"https://www.biblegateway.com/user/account/\",\n\t\"birkenstock.com\":                     \"https://www.birkenstock.com/profile\",\n\t\"blackwells.co.uk\":                    \"https://blackwells.co.uk/bookshop/account/personal-details\",\n\t\"blend.io\":                            \"https://blend.io/settings\",\n\t\"blockchain.com\":                      \"https://login.blockchain.com/en/#/security-center/advanced\",\n\t\"bloomberg.com\":                       \"https://www.bloomberg.com/portal/account\",\n\t\"blutdruck-shop.de\":                   \"https://www.blutdruck-shop.de/mein-passwort/\",\n\t\"booking.com\":                         \"https://account.booking.com/account-recovery\",\n\t\"boredpanda.com\":                      \"https://www.boredpanda.com/settings/\",\n\t\"browserstack.com\":                    \"https://www.browserstack.com/accounts/profile\",\n\t\"bugzilla.kernel.org\":                 \"https://bugzilla.kernel.org/userprefs.cgi?tab=account\",\n\t\"businessinsider.com\":                 \"https://www.businessinsider.com/#\",\n\t\"buzzfeed.com\":                        \"https://www.buzzfeed.com/settings/password/change\",\n\t\"cakeresume.com\":                      \"https://www.cakeresume.com/settings/account?ref=navs_settings\",\n\t\"callofduty.com\":                      \"https://profile.callofduty.com/cod/info\",\n\t\"candyrect.com\":                       \"https://user.candyrect.com/helpcenter/\",\n\t\"canva.com\":                           \"https://www.canva.com/login?redirect=%2Fsettings%2Flogin-and-security\",\n\t\"capitalone.com\":                      \"https://myaccounts.capitalone.com/Security/changePassword\",\n\t\"cargurus.com\":                        \"https://www.cargurus.com/Cars/myAccount#/accountSettings\",\n\t\"carnival.com\":                        \"https://www.carnival.com/profilemanagement/profiles/changepassword\",\n\t\"cars.com\":                            \"https://www.cars.com/reset_password\",\n\t\"carta.com\":                           \"https://app.carta.com/profiles/update/\",\n\t\"cbsnews.com\":                         \"https://www.cbsnews.com/user/change-password/\",\n\t\"cbssports.com\":                       \"https://www.cbssports.com/settings/account\",\n\t\"cecredentialtrust.com\":               \"https://secure.cecredentialtrust.com/account/editpassword/\",\n\t\"censys.io\":                           \"https://censys.io/account\",\n\t\"change.org\":                          \"https://www.change.org/account_settings/change_password\",\n\t\"chapmanganato.com\":                   \"https://user.manganelo.com/user_changes_pass\",\n\t\"chase.com\":                           \"https://secure07ea.chase.com/web/auth/dashboard#/dashboard/myProfileSignInSecurity/resetPassword/resetPassword\",\n\t\"chaturbate.com\":                      \"https://chaturbate.com/auth/password_change/\",\n\t\"chegg.com\":                           \"https://www.chegg.com/my/account-next\",\n\t\"chess.com\":                           \"https://www.chess.com/settings/password\",\n\t\"chewy.com\":                           \"https://www.chewy.com/app/resetpassword\",\n\t\"churchofjesuschrist.org\":             \"https://account.churchofjesuschrist.org/changePassword\",\n\t\"cinemark.com.br\":                     \"https://www.cinemark.com.br/minha-conta\",\n\t\"citi.com\":                            \"https://online.citi.com/US/ag/profile-update/change-password\",\n\t\"claro.com.br\":                        \"https://minhanet.net.com.br/webcenter/portal/MinhaNet/pages_alterarsenha\",\n\t\"clevelandclinic.org\":                 \"https://mychart.clevelandclinic.org/inside.asp?mode=passwd\",\n\t\"clien.net\":                           \"https://www.clien.net/service/mypage/myInfoComfrim\",\n\t\"cloudflare.com\":                      \"https://dash.cloudflare.com/profile/authentication\",\n\t\"cnbc.com\":                            \"https://www.cnbc.com/account/#profile\",\n\t\"cnn.com\":                             \"https://www.cnn.com/account/settings\",\n\t\"codepen.io\":                          \"https://codepen.io/settings/account\",\n\t\"columbia.com\":                        \"https://www.columbia.com/profile\",\n\t\"constantcontact.com\":                 \"https://app.constantcontact.com/pages/myaccount/settings/account\",\n\t\"consumidor.gov.br\":                   \"https://www.consumidor.gov.br/pages/usuario/editar\",\n\t\"costco.com\":                          \"https://www.costco.com/AccountInformationView?identifier=manage-membership\",\n\t\"coupang.com\":                         \"https://login.coupang.com/login/userModify.pang\",\n\t\"coursehero.com\":                      \"https://www.coursehero.com/my-account/#/settings\",\n\t\"cpanel.com\":                          \"https://store.cpanel.net/my/password\",\n\t\"cpanel.net\":                          \"https://store.cpanel.net/my/password\",\n\t\"crackle.com\":                         \"https://www.crackle.com/profile\",\n\t\"craigslist.org\":                      \"https://accounts.craigslist.org/pass\",\n\t\"creditkarma.com\":                     \"https://www.creditkarma.com/myprofile/security\",\n\t\"credly.com\":                          \"https://www.credly.com/earner/settings/privacy\",\n\t\"crowdin.com\":                         \"https://accounts.crowdin.com/password/change\",\n\t\"crunchyroll.com\":                     \"https://www.crunchyroll.com/resetpw\",\n\t\"cvs.com\":                             \"https://www.cvs.com/my-account/profile/sign-in-and-security/edit-password\",\n\t\"dailymail.co.uk\":                     \"https://www.dailymail.co.uk/registration/profile/change-password.html\",\n\t\"dan.com\":                             \"https://dan.com/users/settings/account\",\n\t\"danawa.com\":                          \"https://auth.danawa.com/modifyMember\",\n\t\"darty.com\":                           \"https://www.darty.com/espace_client/donnees-personnelles/mot-de-passe/edition\",\n\t\"daum.net\":                            \"https://member.daum.net/change/password.daum\",\n\t\"deere.com\":                           \"https://account.deere.com/actmgmt/change-password\",\n\t\"dell.com\":                            \"https://www.dell.com/identity/global/editaccount?\",\n\t\"delta.com\":                           \"https://www.delta.com/myprofile/security-settings\",\n\t\"deviantart.com\":                      \"https://www.deviantart.com/settings/general\",\n\t\"dickssportinggoods.com\":              \"https://www.dickssportinggoods.com/MyAccount/AccountSettings\",\n\t\"digitalocean.com\":                    \"https://cloud.digitalocean.com/settings/security\",\n\t\"discogs.com\":                         \"https://www.discogs.com/settings/user\",\n\t\"discord.com\":                         \"https://discord.com/settings/account\",\n\t\"discover.com\":                        \"https://card.discover.com/cardmembersvcs/personalprofile/pp/UpdateDetails?ICMPGN=MYPROFILE_USERID_PASSWORD_TXT\",\n\t\"disneyplus.com\":                      \"https://www.disneyplus.com/account\",\n\t\"dittomusic.com\":                      \"https://dashboard.dittomusic.com/account/password\",\n\t\"dmm.co.jp\":                           \"https://accounts.dmm.co.jp/settings/change/password\",\n\t\"doctoralia.com.br\":                   \"https://l.doctoralia.com.br/change-password\",\n\t\"docusign.net\":                        \"https://account.docusign.com/me/changepassword\",\n\t\"dominos.com\":                         \"https://www.dominos.com/en/pages/customer/#!/customer/settings/\",\n\t\"doordash.com\":                        \"https://www.doordash.com/accounts/password/reset/\",\n\t\"dotloop.com\":                         \"https://www.dotloop.com/my/account/#/settings\",\n\t\"dropbox.com\":                         \"https://www.dropbox.com/account/security\",\n\t\"dsw.com\":                             \"https://www.dsw.com/en/us/profile\",\n\t\"duolingo.com\":                        \"https://duolingo.com/settings/profile\",\n\t\"dwr.com\":                             \"https://www.dwr.com/profile\",\n\t\"ea.com\":                              \"https://myaccount.ea.com/cp-ui/security/index\",\n\t\"ebay.com\":                            \"https://accounts.ebay.com/acctsec/security-center/chngpwd\",\n\t\"elpais.com\":                          \"https://elpais.com/subscriptions/#/profile\",\n\t\"epicgames.com\":                       \"https://www.epicgames.com/account/password?lang=en&productName=epicgames\",\n\t\"eporner.com\":                         \"https://www.eporner.com/profile/mturk_eporn/my/edit-pass/\",\n\t\"espn.com\":                            \"https://www.espn.com/\",\n\t\"eventbrite.com\":                      \"https://www.eventbrite.com/account-settings/password\",\n\t\"evite.com\":                           \"https://www.evite.com/reset_password/\",\n\t\"expedia.com\":                         \"https://www.expedia.com/user/forgotpassword\",\n\t\"experian.com\":                        \"https://usa.experian.com/member/ngx-profile/account-info\",\n\t\"familysearch.org\":                    \"https://www.familysearch.org/identity/settings/account\",\n\t\"fandom.com\":                          \"https://auth.fandom.com/auth/settings\",\n\t\"fanfiction.net\":                      \"https://www.fanfiction.net/account/password.php\",\n\t\"fedex.com\":                           \"https://www.fedex.com/en-us/create-account/how-to-reset-forgot-password.html\",\n\t\"fetlife.com\":                         \"https://fetlife.com/settings/account/password\",\n\t\"fidelity.com\":                        \"https://fps.fidelity.com/ftgw/Fps/Fidelity/RtlCust/ChangePIN/Init\",\n\t\"finance.yahoo.com\":                   \"https://login.yahoo.com/myaccount/security/change-password/?src=finance\",\n\t\"findagrave.com\":                      \"https://www.findagrave.com/user/account/password\",\n\t\"fitbit.com\":                          \"https://www.fitbit.com/settings/profile\",\n\t\"flightaware.com\":                     \"https://flightaware.com/account/manage\",\n\t\"fnac.com\":                            \"https://secure.fnac.com/account/update-password\",\n\t\"foodnetwork.com\":                     \"https://www.foodnetwork.com/user-profile-page\",\n\t\"forbes.com\":                          \"https://account.forbes.com/profile\",\n\t\"force.com\":                           \"https://na224.lightning.force.com/lightning/settings/personal/ChangePassword/home\",\n\t\"foursquare.com\":                      \"https://foursquare.com/change_password\",\n\t\"foxbusiness.com\":                     \"https://my.foxbusiness.com/?p=account\",\n\t\"foxnews.com\":                         \"https://my.foxnews.com/?pieces=reset\",\n\t\"foxsports.com\":                       \"https://www.foxsports.com/#\",\n\t\"frutifica.com.br\":                    \"https://www.frutifica.com.br/conta/alterar_senha\",\n\t\"gamefaqs.gamespot.com\":               \"https://gamefaqs.gamespot.com/user/mailpass\",\n\t\"gamespot.com\":                        \"https://www.gamespot.com/change-details/\",\n\t\"gap.com\":                             \"https://secure-www.gap.com/my-account/change-password\",\n\t\"genius.com\":                          \"https://genius.com/password_resets/new\",\n\t\"geocaching.com\":                      \"https://www.geocaching.com/account/settings/changepassword\",\n\t\"getflywheel.com\":                     \"https://app.getflywheel.com/profile/security/change_password\",\n\t\"github.com\":                          \"https://github.com/settings/security\",\n\t\"glassdoor.com\":                       \"https://www.glassdoor.com/member/profile/settings.htm\",\n\t\"gm.com\":                              \"https://experience.gm.com/myaccount/security/passwordChange\",\n\t\"gmail.com\":                           \"https://myaccount.google.com/signinoptions/password?continue=https://myaccount.google.com/security\",\n\t\"gmarket.co.kr\":                       \"https://sslmember2.gmarket.co.kr/MYInfo/MemberInfo\",\n\t\"gmx.net\":                             \"https://account.gmx.net/ciss/security/edit/passwordChange\",\n\t\"go.com\":                              \"https://go.com/profile/account-settings/edit\",\n\t\"gocomics.com\":                        \"https://www.gocomics.com/profiles/create-password\",\n\t\"gog.com\":                             \"https://www.gog.com/account/settings/security\",\n\t\"goodhousekeeping.com\":                \"https://www.mylo.id/account\",\n\t\"goodreads.com\":                       \"https://www.goodreads.com/ap/cnep\",\n\t\"google.com\":                          \"https://myaccount.google.com/signinoptions/password\",\n\t\"gov.br\":                              \"https://acesso.gov.br/area-cidadao/#/alterarSenha\",\n\t\"grainger.com\":                        \"https://www.grainger.com/myaccount/loginoptions\",\n\t\"grubhub.com\":                         \"https://www.grubhub.com/account/profile\",\n\t\"happycow.net\":                        \"https://www.happycow.net/members/profile/update/password\",\n\t\"hbomax.com\":                          \"https://play.hbomax.com/setting/account/edit/password\",\n\t\"heroku.com\":                          \"https://dashboard.heroku.com/account\",\n\t\"hibrain.net\":                         \"https://hibrain.net/mybrain/users/password/edit\",\n\t\"hilton.com\":                          \"https://www.hilton.com/en/hilton-honors/guest/profile/password/\",\n\t\"homedepot.com\":                       \"https://www.homedepot.com/myaccount/security\",\n\t\"honeywell.com\":                       \"https://honeywell.csod.com/resetPasswrd.aspx?\",\n\t\"hotels.com\":                          \"https://hotels.com/profile/settings.html\",\n\t\"housecallpro.com\":                    \"https://pro.housecallpro.com/service_pro/account/reset_password\",\n\t\"hp.com\":                              \"https://account.id.hp.com/security\",\n\t\"hsn.com\":                             \"https://www.hsn.com/myaccount/update\",\n\t\"huffpost.com\":                        \"https://www.huffpost.com/member/edit-profile\",\n\t\"hulu.com\":                            \"https://secure.hulu.com/account\",\n\t\"icloud.com\":                          \"https://appleid.apple.com/account/manage\",\n\t\"ign.com\":                             \"https://www.ign.com/account/security\",\n\t\"ihg.com\":                             \"https://www.ihg.com/rewardsclub/gb/en/account-mgmt/personalInformation\",\n\t\"ikea.com\":                            \"https://www.ikea.com/in/en/profile/dashboard/\",\n\t\"imgur.com\":                           \"https://imgur.com/account/settings/password\",\n\t\"impots.gouv.fr\":                      \"https://cfspart.impots.gouv.fr/monprofil-webapp/GererMonProfil\",\n\t\"indeed.com\":                          \"https://secure.indeed.com/account/changepassword\",\n\t\"independent.co.uk\":                   \"https://www.independent.co.uk/profile\",\n\t\"insider.com\":                         \"https://www.insider.com/\",\n\t\"instacart.com\":                       \"https://www.instacart.com/store/account\",\n\t\"instagram.com\":                       \"https://www.instagram.com/accounts/password/change/\",\n\t\"intuit.com\":                          \"https://accounts.intuit.com/app/account-manager/security/password\",\n\t\"istockphoto.com\":                     \"https://www.istockphoto.com/change-password\",\n\t\"jcpenney.com\":                        \"https://www.jcpenney.com/account/dashboard/personal/info\",\n\t\"jimdofree.com\":                       \"https://dash.e.jimdo.com/profile\",\n\t\"jw.org\":                              \"https://apps.jw.org/E_PASSCHG1\",\n\t\"key.harvard.edu\":                     \"https://key.harvard.edu/manage-account/change-password\",\n\t\"kohls.com\":                           \"https://www.kohls.com/myaccount/accountsettings.jsp\",\n\t\"kroger.com\":                          \"https://www.kroger.com/account/update\",\n\t\"kundenportal.edeka-smart.de\":         \"https://kundenportal.edeka-smart.de/edeka-csc/forgot-password\",\n\t\"latimes.com\":                         \"https://membership.latimes.com/settings\",\n\t\"leetcode.com\":                        \"https://leetcode.com/accounts/password/set/\",\n\t\"legacy.com\":                          \"https://legacy.memoriams.com/Network/Account/ChangePassword\",\n\t\"lemonde.fr\":                          \"https://moncompte.lemonde.fr/gcustomer/account/password\",\n\t\"letterboxd.com\":                      \"https://letterboxd.com/settings/auth/\",\n\t\"linkedin.com\":                        \"https://www.linkedin.com/psettings/change-password\",\n\t\"linktr.ee\":                           \"https://linktr.ee/admin/account\",\n\t\"linode.com\":                          \"https://cloud.linode.com/profile/auth\",\n\t\"live.com\":                            \"https://account.live.com/password/change?refd=account.microsoft.com&fref=home.banner.changepwd\",\n\t\"livejasmin.com\":                      \"https://www.livejasmin.com/en/girls/#!settings/account\",\n\t\"login.gov\":                           \"https://secure.login.gov/manage/password\",\n\t\"lowes.com\":                           \"https://www.lowes.com/mylowes/profile\",\n\t\"macys.com\":                           \"https://www.macys.com/account/profile?cm_sp=macys_account-_-my_account-_-my_profile&linklocation=leftrail\",\n\t\"marketwatch.com\":                     \"https://customercenter.marketwatch.com/account#password?mod=ql\",\n\t\"marktplaats.nl\":                      \"https://www.marktplaats.nl/account/password-reset/confirm.html\",\n\t\"marriott.com\":                        \"https://www.marriott.com/loyalty/myAccount/changePassword.mi\",\n\t\"mathworks.com\":                       \"https://mathworks.com/mwaccount/profiles/password/change\",\n\t\"maxpreps.com\":                        \"https://secure.maxpreps.com/utility/member/forgotpassword.aspx\",\n\t\"mediafire.com\":                       \"https://www.mediafire.com/myaccount/accountbilling.php#change-pwd-block\",\n\t\"meliuz.com.br\":                       \"https://www.meliuz.com.br/minha-conta/meus-dados/senha\",\n\t\"menards.com\":                         \"https://www.menards.com/main/accountoverview.html\",\n\t\"mercari.com\":                         \"https://www.mercari.com/mypage/email_password/\",\n\t\"messagebird.com\":                     \"https://dashboard.messagebird.com/account/security\",\n\t\"messenger.com\":                       \"https://www.facebook.com/settings?tab=security\",\n\t\"michaels.com\":                        \"https://www.michaels.com/on/demandware.store/Sites-MichaelsUS-Site/default/Account-EditProfile\",\n\t\"microsoft.com\":                       \"https://account.live.com/password/Change\",\n\t\"mlb.com\":                             \"https://www.mlb.com/account/general\",\n\t\"mountainwarehouse.com\":               \"https://www.mountainwarehouse.com/account/details-link/\",\n\t\"msn.com\":                             \"https://account.live.com/password/change?refd=account.microsoft.com&fref=home.banner.changepwd\",\n\t\"music.youtube.com\":                   \"https://myaccount.google.com/signinoptions/password\",\n\t\"my.goabode.com\":                      \"https://my.goabode.com/#/app/account\",\n\t\"myaccount.ea.com\":                    \"https://myaccount.ea.com/cp-ui/security/index\",\n\t\"myaccount.google.com\":                \"https://myaccount.google.com/signinoptions/password\",\n\t\"myfreecams.com\":                      \"https://www.myfreecams.com/php/account.php?request=status&vcc=1674246522#change_password\",\n\t\"mypay.dfas.mil\":                      \"https://mypay.dfas.mil/#/settings/password\",\n\t\"myshopify.com\":                       \"https://accounts.shopify.com/accounts/186490458/security\",\n\t\"myspace.com\":                         \"https://myspace.com/settings/profile/email\",\n\t\"naver.com\":                           \"https://nid.naver.com/user2/help/myInfo.nhn?m=viewChangePasswd\",\n\t\"nba.com\":                             \"https://www.nba.com/account/nbaprofile\",\n\t\"nbcnews.com\":                         \"https://nbcuniversal.nbc.com/request-password\",\n\t\"nekochat.cn\":                         \"https://user.candyrect.com/helpcenter/\",\n\t\"netflix.com\":                         \"https://www.netflix.com/password\",\n\t\"netvibes.com\":                        \"https://www.netvibes.com/account/password\",\n\t\"news.google.com\":                     \"https://myaccount.google.com/signinoptions/password\",\n\t\"news.yahoo.com\":                      \"https://login.yahoo.com/myaccount/security/change-password/\",\n\t\"news.ycombinator.com\":                \"https://news.ycombinator.com/changepw\",\n\t\"newsweek.com\":                        \"https://www.newsweek.com/contact\",\n\t\"nextdns.io\":                          \"https://my.nextdns.io/account\",\n\t\"nfl.com\":                             \"https://id.nfl.com/account/change-password\",\n\t\"nhentai.net\":                         \"https://nhentai.net/reset/\",\n\t\"nike.com\":                            \"https://www.nike.com/member/settings\",\n\t\"nintendo.com\":                        \"https://accounts.nintendo.com/password/edit\",\n\t\"njal.la\":                             \"https://njal.la/settings\",\n\t\"nordstrom.com\":                       \"https://www.nordstrom.com/my-account/sign-in-info\",\n\t\"nordstromrack.com\":                   \"https://www.nordstromrack.com/my-account/sign-in-info\",\n\t\"norton.com\":                          \"https://my.norton.com/extspa/account/personalinfo\",\n\t\"npr.org\":                             \"https://secure.npr.org/oauth2/login\",\n\t\"nvidia.com\":                          \"https://profile.nvgs.nvidia.com/security/change-password\",\n\t\"nypost.com\":                          \"https://nypost.com/account/settings\",\n\t\"nytimes.com\":                         \"https://www.nytimes.com/account/change-password\",\n\t\"office.com\":                          \"https://account.live.com/password/change?refd=account.microsoft.com&fref=home.banner.changepwd\",\n\t\"office365.com\":                       \"https://account.live.com/password/change?refd=account.microsoft.com&fref=home.banner.changepwd\",\n\t\"officedepot.com\":                     \"https://www.officedepot.com/account/editLoginDisplay.do\",\n\t\"okta.com\":                            \"https://my.okta.com/signin/password-reset\",\n\t\"onelink.me\":                          \"https://hq1.appsflyer.com/account/change-password\",\n\t\"onlyfans.com\":                        \"https://onlyfans.com/my/settings/account/password\",\n\t\"opentable.com\":                       \"https://support.opentable.com/s/login/ForgotPassword?language=en_US\",\n\t\"opera.com\":                           \"https://auth.opera.com/account/edit-profile\",\n\t\"orcid.org\":                           \"https://orcid.org/account\",\n\t\"overleaf.com\":                        \"https://www.overleaf.com/user/settings\",\n\t\"overstock.com\":                       \"https://www.overstock.com/myaccount/account/email-password\",\n\t\"paramountplus.com\":                   \"https://www.paramountplus.com/account/\",\n\t\"patreon.com\":                         \"https://www.patreon.com/settings/account\",\n\t\"paypal.com\":                          \"https://www.paypal.com/myaccount/security/password/change\",\n\t\"pch.com\":                             \"https://accounts.pch.com/forgotpass\",\n\t\"peacocktv.com\":                       \"https://www.peacocktv.com/forgot\",\n\t\"pilotflyingj.com\":                    \"https://portal.pilotflyingj.com/myrewards/forgot-password\",\n\t\"pinterest.com\":                       \"https://www.pinterest.com/settings/account-settings\",\n\t\"pl.canalplus.com\":                    \"https://logowanie.pl.canalplus.com/zmien-haslo\",\n\t\"playstation.com\":                     \"https://id.sonyentertainmentnetwork.com/id/management/#/p/security\",\n\t\"plex.tv\":                             \"https://app.plex.tv/desktop#!/account\",\n\t\"pogo.com\":                            \"https://myaccount.ea.com/cp-ui/security/index\",\n\t\"politico.com\":                        \"https://www.politico.com/settings\",\n\t\"pornhub.com\":                         \"https://www.pornhub.com/user/security\",\n\t\"portal.edd.ca.gov\":                   \"https://portal.edd.ca.gov/WebApp/Profile/UpdatePassword\",\n\t\"portlandgeneral.com\":                 \"https://portlandgeneral.com/secure/profile/change-password\",\n\t\"poshmark.com\":                        \"https://poshmark.com/user/account-info\",\n\t\"ppomppu.co.kr\":                       \"https://www.ppomppu.co.kr/myinfo/profile.php\",\n\t\"prolific.co\":                         \"https://app.prolific.co/account/general\",\n\t\"proton.me\":                           \"https://account.proton.me/u/0/vpn/account-password\",\n\t\"prowlapp.com\":                        \"https://www.prowlapp.com/settings.php\",\n\t\"quizlet.com\":                         \"https://quizlet.com/settings\",\n\t\"quora.com\":                           \"https://www.quora.com/settings\",\n\t\"rakuten.com\":                         \"https://www.rakuten.com/account-settings.htm\",\n\t\"readymag.com\":                        \"https://auth.readymag.com/password/forgot\",\n\t\"realtor.com\":                         \"https://www.realtor.com/myaccount/profile/settings\",\n\t\"redd.it\":                             \"https://www.reddit.com/prefs/update/\",\n\t\"reddit.com\":                          \"https://www.reddit.com/prefs/update/\",\n\t\"redfin.com\":                          \"https://www.redfin.com/change-password\",\n\t\"redgifs.com\":                         \"https://auth.redgifs.com/lo/reset?ticket=\",\n\t\"redirect.pizza\":                      \"https://redirect.pizza/profile\",\n\t\"redtube.com\":                         \"https://www.redtube.com/settings\",\n\t\"reelgood.com\":                        \"https://reelgood.com/account\",\n\t\"rei.com\":                             \"https://www.rei.com/YourAccountCredentials\",\n\t\"rejsekort.dk\":                        \"https://selvbetjening.rejsekort.dk/CWS/CustomerManagement/ChangePassword\",\n\t\"reuters.com\":                         \"https://www.reuters.com/account/forgot-password/\",\n\t\"roblox.com\":                          \"https://www.roblox.com/my/account#!/info\",\n\t\"rottentomatoes.com\":                  \"https://www.rottentomatoes.com/user/account\",\n\t\"ruc.dk\":                              \"https://pwrecovery.ruc.dk\",\n\t\"rule34.xxx\":                          \"https://rule34.xxx/index.php?page=account&s=change_password\",\n\t\"rumble.com\":                          \"https://rumble.com/account/profile\",\n\t\"safeco.com\":                          \"https://customer.safeco.com/accountmanager/profile/changepassword\",\n\t\"safeway.com\":                         \"https://www.safeway.com/customer-account/account-settings\",\n\t\"samsclub.com\":                        \"https://www.samsclub.com/account/personal-info?xid=hdr_account_change-password\",\n\t\"samsung.com\":                         \"https://account.samsung.com/membership/contents/security/password/change-password\",\n\t\"santahelenasaude.com.br\":             \"https://www.santahelenasaude.com.br/beneficiario/#/alterar-senha\",\n\t\"saturn.de\":                           \"https://www.saturn.de/webapp/wcs/stores/servlet/MultiChannelMAChangePassword\",\n\t\"scribd.com\":                          \"https://www.scribd.com/account-settings#change-password\",\n\t\"secondlife.com\":                      \"https://accounts.secondlife.com/change_password\",\n\t\"secure.orclinic.com\":                 \"https://secure.orclinic.com/portal/editprofile.aspx\",\n\t\"sephora.com\":                         \"https://www.sephora.com/profile/MyAccount\",\n\t\"serasa.com.br\":                       \"https://www.serasa.com.br/meus-dados/alterar-senha\",\n\t\"shein.com\":                           \"https://shein.com/user/security\",\n\t\"shodan.io\":                           \"https://account.shodan.io/change_password\",\n\t\"shoop.de\":                            \"https://www.shoop.de/einstellungen/benutzerdaten\",\n\t\"shopback.co.kr\":                      \"https://www.shopback.co.kr/account/change-password\",\n\t\"shutterfly.com\":                      \"https://www.shutterfly.com/account-settings/\",\n\t\"sipgatebasic.de\":                     \"https://app.sipgatebasic.de/settings\",\n\t\"slickdeals.net\":                      \"https://slickdeals.net/forums/login.php?do=lostpw\",\n\t\"soap2day.to\":                         \"https://soap2day.to/home/user/changepassword\",\n\t\"solitaired.com\":                      \"https://solitaired.com/user/reset-password?\",\n\t\"sonos.com\":                           \"https://www.sonos.com/myaccount/user/profile/\",\n\t\"soundcloud.com\":                      \"https://soundcloud.com/settings\",\n\t\"southwest.com\":                       \"https://www.southwest.com/loyalty/myaccount/profile-security.html\",\n\t\"spankbang.com\":                       \"https://spankbang.com/users/account\",\n\t\"spectrum.net\":                        \"https://www.spectrum.net/user-preferences/your-info/manage/security\",\n\t\"speedway.com\":                        \"https://www.speedway.com/my-account/security/passcode\",\n\t\"splunk.com\":                          \"https://www.splunk.com/my-account/#/profile-details\",\n\t\"sports.yahoo.com\":                    \"https://login.yahoo.com/account/change-password\",\n\t\"spotify.com\":                         \"https://www.spotify.com/in-en/account/change-password/\",\n\t\"ssa.gov\":                             \"https://secure.ssa.gov/RIM/UpwdView.action\",\n\t\"stackoverflow.com\":                   \"https://stackoverflow.com/users/account-recovery\",\n\t\"stacksocial.com\":                     \"https://stacksocial.com/user?show=account-tab\",\n\t\"state.nj.us\":                         \"https://my.state.nj.us/edituser/EditUserProfile\",\n\t\"steamcommunity.com\":                  \"https://help.steampowered.com/en/wizard/HelpWithLoginInfoReset/\",\n\t\"steampowered.com\":                    \"https://help.steampowered.com/en/wizard/HelpChangePassword?redir=store/account/\",\n\t\"stonly.com\":                          \"https://app.stonly.com/app/general/userSettings/Account\",\n\t\"stripchat.com\":                       \"https://stripchat.com/settings\",\n\t\"sulamericaseguros.com.br\":            \"https://saude.sulamericaseguros.com.br/segurado/gerenciar-cadastro/\",\n\t\"surveymonkey.com\":                    \"https://identity.surveymonkey.com/us/manage?locale=en\",\n\t\"swagbucks.com\":                       \"https://www.swagbucks.com/account/settings\",\n\t\"swarmapp.com\":                        \"https://foursquare.com/change_password\",\n\t\"swinglifestyle.com\":                  \"https://www.swinglifestyle.com/profile/\",\n\t\"syf.com\":                             \"https://mastercard.syf.com/login/reset\",\n\t\"synchrony.com\":                       \"https://consumercenter.mysynchrony.com/consumercenter/\",\n\t\"tagged.com\":                          \"https://secure.tagged.com/account_info.html?dataSource=Settings&ll=nav\",\n\t\"target.com\":                          \"https://logonservices.iam.target.com/change-password/?target=#!\",\n\t\"tasteofhome.com\":                     \"https://www.tasteofhome.com/login/updatepassword\",\n\t\"teacherspayteachers.com\":             \"https://www.teacherspayteachers.com/My-Account/Basics/edit\",\n\t\"teamviewer.com\":                      \"https://login.teamviewer.com/nav/profile/change-password\",\n\t\"telekom.de\":                          \"https://account.idm.telekom.com/account-manager/password/index.xhtml\",\n\t\"temu.com\":                            \"https://www.temu.com/bgp_account_security.html\",\n\t\"the-sun.com\":                         \"https://home.thesun.co.uk/edit/password\",\n\t\"theguardian.com\":                     \"https://profile.theguardian.com/reset\",\n\t\"thejigsawpuzzles.com\":                \"https://thejigsawpuzzles.com/profile/?changepassword\",\n\t\"thenounproject.com\":                  \"https://thenounproject.com/accounts/password/change/\",\n\t\"thesimsresource.com\":                 \"https://www.thesimsresource.com/account#/account\",\n\t\"thesun.co.uk\":                        \"https://login.thesun.co.uk/user/changePassword\",\n\t\"thetrainline.com\":                    \"https://www.thetrainline.com/my-account/change-password\",\n\t\"thetvdb.com\":                         \"https://www.thetvdb.com/dashboard/account/changepass\",\n\t\"ti.com\":                              \"https://login.ti.com/ext/pwdchange/Identify\",\n\t\"tiktok.com\":                          \"https://www.tiktok.com/login/email/forget-password\",\n\t\"time.com\":                            \"https://time.com/manage-account/\",\n\t\"tinyurl.com\":                         \"https://tinyurl.com/app/settings/security\",\n\t\"tmon.co.kr\":                          \"https://login.tmon.co.kr/user/info\",\n\t\"tmz.com\":                             \"https://shop.tmz.com/user?show=account-tab\",\n\t\"todoist.com\":                         \"https://todoist.com/prefs/account\",\n\t\"trakt.tv\":                            \"https://trakt.tv/settings#password\",\n\t\"tripadvisor.com\":                     \"https://www.tripadvisor.com/Settings-cp\",\n\t\"tripit.com\":                          \"https://tripit.com/account/edit/section/change_password\",\n\t\"trulia.com\":                          \"https://www.trulia.com/account/user_profile\",\n\t\"tum.de\":                              \"https://campus.tum.de\",\n\t\"tumblr.com\":                          \"https://www.tumblr.com/settings/account\",\n\t\"turkishairlines.com\":                 \"https://www.turkishairlines.com/tr-int/miles-and-smiles/forgot-password/\",\n\t\"twilio.com\":                          \"https://www.twilio.com/console/user/settings\",\n\t\"twitch.tv\":                           \"https://www.twitch.tv/settings/security\",\n\t\"twitter.com\":                         \"https://twitter.com/settings/password\",\n\t\"udacity.com\":                         \"https://classroom.udacity.com/settings/password\",\n\t\"udel.edu\":                            \"https://udapps.nss.udel.edu/myUDsettings/password\",\n\t\"uline.com\":                           \"https://www.uline.com/MyAccount/ContactPref\",\n\t\"ulta.com\":                            \"https://www.ulta.com/myaccount/index.jsp\",\n\t\"uml.edu\":                             \"https://mypassword.uml.edu/#Change\",\n\t\"umsystem.edu\":                        \"https://password.umsystem.edu/reset/\",\n\t\"united.com\":                          \"https://www.united.com/ual/en/US/account/security/setpassword\",\n\t\"ups.com\":                             \"https://www.ups.com/lasso/updatePass?loc=en_US\",\n\t\"usaa.com\":                            \"https://www.usaa.com/inet/ent_auth_password/pages/ChangePasswordPage\",\n\t\"usatoday.com\":                        \"https://login.usatoday.com/USAT-GUP/password-forgot/\",\n\t\"uscis.gov\":                           \"https://myaccount.uscis.gov/users/registration/password\",\n\t\"usnews.com\":                          \"https://auth.usnews.com/changePassword\",\n\t\"usps.com\":                            \"https://reg.usps.com/entreg/secure/ChangePasswordAction_input?returnActionName\",\n\t\"vccs.edu\":                            \"https://www.apply.vccs.edu/Profile/_default.aspx\",\n\t\"ventrachicago.com\":                   \"https://www.ventrachicago.com/account/manage-account/\",\n\t\"verizon.com\":                         \"https://myvpostpay.verizon.com/ui/bill/secure/\",\n\t\"victoriassecret.com\":                 \"https://www.victoriassecret.com/us/account/profile#changePassword\",\n\t\"virginmobile.ca\":                     \"https://myaccount.virginmobile.ca/MyProfile/Details/EditProfile?editField=PASSWORD\",\n\t\"vivo.com.br\":                         \"https://meuvivo.vivo.com.br/meuvivo/appmanager/portal/fixo\",\n\t\"vrbo.com\":                            \"https://www.vrbo.com/traveler/profile/edit\",\n\t\"walgreens.com\":                       \"https://www.walgreens.com/account/user_and_password\",\n\t\"walmart.com\":                         \"https://www.walmart.com/account/profile\",\n\t\"washingtonpost.com\":                  \"https://subscribe.washingtonpost.com/profile/#!/profile/access?destination=https:%2F%2Fwww.washingtonpost.com%2F%3Frefresh%3Dtrue&tid=nav_acctmgnt_menu\",\n\t\"wayfair.com\":                         \"https://www.wayfair.com/v/account/personal_info/edit\",\n\t\"weather.com\":                         \"https://weather.com/member/settings\",\n\t\"webmd.com\":                           \"https://member.webmd.com/password-reset\",\n\t\"wii-homebrew.com\":                    \"https://forum.wii-homebrew.com/index.php/AccountManagement/\",\n\t\"wikihow.com\":                         \"https://www.wikihow.com/Special:ChangeCredentials/MediaWiki%5CAuth%5CPasswordAuthenticationRequest\",\n\t\"wikipedia.org\":                       \"https://en.wikipedia.org/w/index.php?title=Special:ChangeCredentials/MediaWiki%5CAuth%5CPasswordAuthenticationRequest&returnto=Special%3APreferences\",\n\t\"wired.com\":                           \"https://www.wired.com/account/reset-password\",\n\t\"wordpress.com\":                       \"https://wordpress.com/me/security/password\",\n\t\"worldstar.com\":                       \"https://worldstarhiphop.com/videos/reset.php\",\n\t\"worldwinner.com\":                     \"https://www.worldwinner.com/cgi/finance/account.pl\",\n\t\"wsj.com\":                             \"https://customercenter.wsj.com/account#password\",\n\t\"wunderground.com\":                    \"https://www.wunderground.com/member/settings\",\n\t\"xero.com\":                            \"https://identity.xero.com/account/?AccountUrl=/!xkcD/Settings/MyAccount\",\n\t\"xfinity.com\":                         \"https://customer.xfinity.com/users/me/update-password\",\n\t\"xhamster.com\":                        \"https://xhamster.com/password-recovery\",\n\t\"xvideos.com\":                         \"https://www.xvideos.com/account/security\",\n\t\"yahoo.com\":                           \"https://login.yahoo.com/account/change-password\",\n\t\"yellowpages.com\":                     \"https://www.yellowpages.com/settings/password\",\n\t\"yelp.com\":                            \"https://yelp.com/profile_password\",\n\t\"youporn.com\":                         \"https://www.youporn.com/settings/change/password/\",\n\t\"youtube.com\":                         \"https://myaccount.google.com/signinoptions/password\",\n\t\"zeplin.io\":                           \"https://app.zeplin.io/profile/password\",\n\t\"zhihu.com\":                           \"https://www.zhihu.com/settings/account\",\n\t\"zillow.com\":                          \"https://www.zillow.com/myzillow/profile/\",\n\t\"ziprecruiter.com\":                    \"https://www.ziprecruiter.com/login/forgot-password?realm=candidates\",\n\t\"zocdoc.com\":                          \"https://www.zocdoc.com/patient/editprofile?section=Password\",\n\t\"zulily.com\":                          \"https://www.zulily.com/account/edit?rel=top_flyout\",\n}\n\nvar genRules = map[string]Rule{\n\t\"163.com\":                             {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"1800flowers.com\":                     {Minlen: 6, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"access.service.gov.uk\":               {Minlen: 10, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"account.samsung.com\":                 {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"acmemarkets.com\":                     {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"act.org\":                             {Minlen: 8, Maxlen: 64, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"admiral.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{\".:\", \"[- !\\\"#$&'()*+,.:;<=>?@[^_`{|}~]]\", \"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"ae.com\":                              {Minlen: 8, Maxlen: 25, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"aeon.co.jp\":                          {Minlen: 8, Maxlen: 8, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"aeromexico.com\":                      {Minlen: 8, Maxlen: 25, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"aetna.com\":                           {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"\", \"lower\"}, Maxconsec: 2, Exact: false},\n\t\"airasia.com\":                         {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"airfrance.com\":                       {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!#$&+/?@_]\", \"[-!#$&+/?@_]\"}, Maxconsec: 0, Exact: false},\n\t\"airfrance.us\":                        {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!#$&+/?@_]\", \"[-!#$&+/?@_]\"}, Maxconsec: 0, Exact: false},\n\t\"ajisushionline.com\":                  {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[ !#$%&*?@]\", \"[ !#$%&*?@]\"}, Maxconsec: 0, Exact: false},\n\t\"albertsons.com\":                      {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"alelo.com.br\":                        {Minlen: 6, Maxlen: 10, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"aliexpress.com\":                      {Minlen: 6, Maxlen: 20, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"alliantcreditunion.com\":              {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$*]\", \"[!#$*]\"}, Maxconsec: 3, Exact: false},\n\t\"allianz.com.br\":                      {Minlen: 4, Maxlen: 4, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"americanexpress.com\":                 {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[%&_?#=]\", \"[%&_?#=]\"}, Maxconsec: 4, Exact: false},\n\t\"amnh.org\":                            {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"ana.co.jp\":                           {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"anatel.gov.br\":                       {Minlen: 6, Maxlen: 15, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"ancestry.com\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"andronicos.com\":                      {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"angieslist.com\":                      {Minlen: 6, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"anthem.com\":                          {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!$*?@|]\", \"[!$*?@|]\"}, Maxconsec: 3, Exact: false},\n\t\"app.digiboxx.com\":                    {Minlen: 8, Maxlen: 14, Required: []string{\"[@$!%*?&]\", \"[@$!%*?&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"app.digio.in\":                        {Minlen: 8, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"app.parkmobile.io\":                   {Minlen: 8, Maxlen: 25, Required: []string{\"[!@#$%^&]\", \"[!@#$%^&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"app8menu.com\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"[@$!%*?&]\", \"[@$!%*?&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"apple.com\":                           {Minlen: 8, Maxlen: 63, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"appleloan.citizensbank.com\":          {Minlen: 10, Maxlen: 20, Required: []string{\"[!#$%@^_]\", \"[!#$%@^_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"areariservata.bancaetica.it\":         {Minlen: 8, Maxlen: 10, Required: []string{\"[!#&*+/=@_]\", \"[!#&*+/=@_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"artscyclery.com\":                     {Minlen: 6, Maxlen: 19, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"astonmartinf1.com\":                   {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"auth.readymag.com\":                   {Minlen: 8, Maxlen: 128, Required: []string{\"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"auth.zennioptical.com\":               {Minlen: 8, Maxlen: 14, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"autify.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{\"./:\", \"[!\\\"#$%&'()*+,./:;<=>?@[^_`{|}~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"axa.de\":                              {Minlen: 8, Maxlen: 65, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!\\\"§$%&/()=?;:_+*'#]\"}, Maxconsec: 0, Exact: false},\n\t\"baidu.com\":                           {Minlen: 6, Maxlen: 14, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"balduccis.com\":                       {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"bancochile.cl\":                       {Minlen: 8, Maxlen: 8, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bankofamerica.com\":                   {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-@#*()+={}/?~;,._]\"}, Maxconsec: 3, Exact: false},\n\t\"battle.net\":                          {Minlen: 8, Maxlen: 16, Required: []string{\"lower\", \"upper\"}, Allowed: []string{\"digit\", \"special\"}, Maxconsec: 0, Exact: false},\n\t\"bcassessment.ca\":                     {Minlen: 8, Maxlen: 14, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"belkin.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{\"%&]\", \"[$!@~_,%&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"benefitslogin.discoverybenefits.com\": {Minlen: 10, Maxlen: 0, Required: []string{\"[!#$%&*?@]\", \"[!#$%&*?@]\", \"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"benjerry.com\":                        {Minlen: 0, Maxlen: 0, Required: []string{\"digit\", \"digit\", \"special\", \"special\", \"upper\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"bestbuy.com\":                         {Minlen: 20, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bhphotovideo.com\":                    {Minlen: 0, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bilibili.com\":                        {Minlen: 0, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"billerweb.com\":                       {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"biovea.com\":                          {Minlen: 0, Maxlen: 19, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bitly.com\":                           {Minlen: 6, Maxlen: 0, Required: []string{\"[`!@#$%^&*()+~{}'\\\";:<>?]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"blackwells.co.uk\":                    {Minlen: 8, Maxlen: 30, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"bloomingdales.com\":                   {Minlen: 7, Maxlen: 16, Required: []string{\"[`!@#$%^&*()+~{}'\\\";:<>?]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bluesguitarunleashed.com\":            {Minlen: 0, Maxlen: 0, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"bochk.com\":                           {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\".:\", \"[#$%&()*+,.:;<=>?@_]\"}, Maxconsec: 3, Exact: false},\n\t\"box.com\":                             {Minlen: 6, Maxlen: 20, Required: []string{\"digit\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"bpl.bibliocommons.com\":               {Minlen: 4, Maxlen: 4, Required: []string{\"digit\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"brighthorizons.com\":                  {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"callofduty.com\":                      {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"candyrect.com\":                       {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"capitalone.com\":                      {Minlen: 8, Maxlen: 32, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-_./\\\\@$*&!#]\", \"[-_./\\\\@$*&!#]\"}, Maxconsec: 0, Exact: false},\n\t\"cardbenefitservices.com\":             {Minlen: 7, Maxlen: 100, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"cardcash.com\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"[!$%&*?@]\", \"[!$%&*?@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"carrefour.it\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&*?@_]\", \"[!#$%&*?@_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"carrsqc.com\":                         {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"carte-mobilite-inclusion.fr\":         {Minlen: 12, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"cathaypacific.com\":                   {Minlen: 8, Maxlen: 20, Required: []string{\"[!#$*^]\", \"[!#$*^]\", \"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"cb2.com\":                             {Minlen: 9, Maxlen: 0, Required: []string{\"[!#*_%.$]\", \"[!#*_%.$]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ccs-grp.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!#$%&'+./=?\\\\^_`{|}~]\", \"[-!#$%&'+./=?\\\\^_`{|}~]\"}, Maxconsec: 0, Exact: false},\n\t\"cecredentialtrust.com\":               {Minlen: 12, Maxlen: 0, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"charlie.mbta.com\":                    {Minlen: 10, Maxlen: 0, Required: []string{\"[!#$%@^]\", \"[!#$%@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"chase.com\":                           {Minlen: 8, Maxlen: 32, Required: []string{\"[!#$%+/=@~]\", \"[!#$%+/=@~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"cigna.co.uk\":                         {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"citi.com\":                            {Minlen: 8, Maxlen: 64, Required: []string{\"[-~`!@#$%^&*()_\\\\/|]\", \"[-~`!@#$%^&*()_\\\\/|]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"claimlookup.com\":                     {Minlen: 8, Maxlen: 16, Required: []string{\"[@#$%^&+=!]\", \"[@#$%^&+=!]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"clarksoneyecare.com\":                 {Minlen: 9, Maxlen: 0, Required: []string{\"[~!@#$%^&*()_+{}|;,.<>?[]]\", \"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"claro.com.br\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"lower\"}, Allowed: []string{\"\", \"digit\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"classmates.com\":                      {Minlen: 6, Maxlen: 20, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"clegc-gckey.gc.ca\":                   {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"clien.net\":                           {Minlen: 5, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"cogmembers.org\":                      {Minlen: 8, Maxlen: 14, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"collectivehealth.com\":                {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"comcastpaymentcenter.com\":            {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"comed.com\":                           {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?/\\\\]]\"}, Maxconsec: 0, Exact: false},\n\t\"commerzbank.de\":                      {Minlen: 5, Maxlen: 8, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"consorsbank.de\":                      {Minlen: 5, Maxlen: 5, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"consorsfinanz.de\":                    {Minlen: 6, Maxlen: 15, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"costco.com\":                          {Minlen: 8, Maxlen: 16, Required: []string{\"lower\", \"upper\"}, Allowed: []string{\"\", \"digit\"}, Maxconsec: 0, Exact: false},\n\t\"coursera.com\":                        {Minlen: 8, Maxlen: 72, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"cox.com\":                             {Minlen: 8, Maxlen: 24, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%()*@^]\", \"[!#$%()*@^]\"}, Maxconsec: 0, Exact: false},\n\t\"crateandbarrel.com\":                  {Minlen: 9, Maxlen: 64, Required: []string{\".:<>?@^_{|}]\", \"[!\\\"#$%&()*,.:<>?@^_{|}]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"crowdgen.com\":                        {Minlen: 8, Maxlen: 16, Required: []string{\"[!#$%&()*+=@^_]\", \"[!#$%&()*+=@^_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"cvs.com\":                             {Minlen: 8, Maxlen: 25, Required: []string{\"[!@#$%^&*()]\", \"[!@#$%^&*()]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dailymail.co.uk\":                     {Minlen: 5, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dan.org\":                             {Minlen: 8, Maxlen: 25, Required: []string{\"[!@$%^&*]\", \"[!@$%^&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"danawa.com\":                          {Minlen: 8, Maxlen: 21, Required: []string{\"[!@$%^&*]\", \"[!@$%^&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"darty.com\":                           {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dbs.com.hk\":                          {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"decluttr.com\":                        {Minlen: 8, Maxlen: 45, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"delta.com\":                           {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"deutsche-bank.de\":                    {Minlen: 5, Maxlen: 5, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"devstore.cn\":                         {Minlen: 6, Maxlen: 12, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dickssportinggoods.com\":              {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&*?@^]\", \"[!#$%&*?@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dkb.de\":                              {Minlen: 8, Maxlen: 38, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\".:]\", \"[-äüöÄÜÖß!$%&/()=?+#,.:]\"}, Maxconsec: 0, Exact: false},\n\t\"dmm.com\":                             {Minlen: 4, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dodgeridge.com\":                      {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"dowjones.com\":                        {Minlen: 0, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ea.com\":                              {Minlen: 8, Maxlen: 64, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"easycoop.com\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"special\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"easyjet.com\":                         {Minlen: 6, Maxlen: 20, Required: []string{\"[-]\", \"[-]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ebrap.org\":                           {Minlen: 15, Maxlen: 0, Required: []string{\"[-!@#$%^&*()_+|~=`{}[:\\\";'?,./.]]; required: [-!@#$%^&*()_+|~=`{}[:\\\";'?,./.]]\", \"digit\", \"digit\", \"lower\", \"lower\", \"upper\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ecompanystore.com\":                   {Minlen: 8, Maxlen: 16, Required: []string{\"[#$%*+.=@^_]\", \"[#$%*+.=@^_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"eddservices.edd.ca.gov\":              {Minlen: 8, Maxlen: 12, Required: []string{\"[!@#$%^&*()]\", \"[!@#$%^&*()]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"edistrict.kerala.gov.in\":             {Minlen: 5, Maxlen: 15, Required: []string{\"[!@#$]\", \"[!@#$]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"empower-retirement.com\":              {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"epicgames.com\":                       {Minlen: 7, Maxlen: 0, Required: []string{\"./:\", \"[-!\\\"#$%&'()*+,./:;<=>?@[^_`{|}~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"epicmix.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"equifax.com\":                         {Minlen: 8, Maxlen: 20, Required: []string{\"[!$*+@]\", \"[!$*+@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"essportal.excelityglobal.com\":        {Minlen: 6, Maxlen: 8, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"ettoday.net\":                         {Minlen: 6, Maxlen: 12, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"examservice.com.tw\":                  {Minlen: 6, Maxlen: 8, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"expertflyer.com\":                     {Minlen: 5, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"extraspace.com\":                      {Minlen: 8, Maxlen: 20, Required: []string{\"\", \"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"ezpassva.com\":                        {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"fc2.com\":                             {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"fccaccessonline.com\":                 {Minlen: 8, Maxlen: 14, Required: []string{\"[!#$%*^_]\", \"[!#$%*^_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"fedex.com\":                           {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!@#$%^&*_+=`|(){}[:;,.?]]\"}, Maxconsec: 3, Exact: false},\n\t\"fidelity.com\":                        {Minlen: 6, Maxlen: 20, Required: []string{\"./:\", \"[-!$%+,./:;=?@^_|]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"flysas.com\":                          {Minlen: 8, Maxlen: 14, Required: []string{\".?]]\", \"[-~!@#$%^&_+=`|(){}[:\\\"'<>,.?]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"fnac.com\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"fuelrewards.com\":                     {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"gamestop.com\":                        {Minlen: 8, Maxlen: 225, Required: []string{\"[!@#$%]\", \"[!@#$%]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"gap.com\":                             {Minlen: 8, Maxlen: 24, Required: []string{\"[-!@#$%^&*()_+]\", \"[-!@#$%^&*()_+]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"garmin.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"getflywheel.com\":                     {Minlen: 7, Maxlen: 72, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"girlscouts.org\":                      {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[$#!]\", \"[$#!]\"}, Maxconsec: 0, Exact: false},\n\t\"gmx.net\":                             {Minlen: 8, Maxlen: 40, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"gocurb.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{\"[$%&#*?!@^]\", \"[$%&#*?!@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"google.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"guardiananytime.com\":                 {Minlen: 8, Maxlen: 50, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"gwl.greatwestlife.com\":               {Minlen: 8, Maxlen: 0, Required: []string{\"[-!#$%_=+<>]\", \"[-!#$%_=+<>]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"haggen.com\":                          {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"hangseng.com\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hawaiianairlines.com\":                {Minlen: 0, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hertz-japan.com\":                     {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz-kuwait.com\":                    {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz-saudi.com\":                     {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.at\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.be\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.bh\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.ca\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.ch\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.cn\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.ao\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.id\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.kr\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.nz\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.th\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.co.uk\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com\":                           {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.au\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.bh\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.hk\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.kw\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.mt\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.pl\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.pt\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.sg\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.com.tw\":                        {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.cv\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.cz\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.de\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.ee\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.es\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.fi\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.fr\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.hu\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.ie\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.it\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.jo\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.lt\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.nl\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.no\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.nu\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.pl\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.pt\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.qa\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.ru\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.se\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertz.si\":                            {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hertzcaribbean.com\":                  {Minlen: 8, Maxlen: 30, Required: []string{\"[#$%^&!@]\", \"[#$%^&!@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"hetzner.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{\"\", \"[-^!$%/()=?+#.,;:~*@{}_&[]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hilton.com\":                          {Minlen: 8, Maxlen: 32, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hkbea.com\":                           {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hkexpress.com\":                       {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hotels.com\":                          {Minlen: 6, Maxlen: 20, Required: []string{\"[-~#@$%&!*_?^]\", \"[-~#@$%&!*_?^]\", \"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"hotwire.com\":                         {Minlen: 6, Maxlen: 30, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"hrblock.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{\"[$#%!]\", \"[$#%!]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"hsbc.com.hk\":                         {Minlen: 6, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"['.@_]\", \"['.@_]\"}, Maxconsec: 0, Exact: false},\n\t\"hsbc.com.my\":                         {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!$*.=?@_']\", \"[-!$*.=?@_']\"}, Maxconsec: 0, Exact: false},\n\t\"hypovereinsbank.de\":                  {Minlen: 6, Maxlen: 10, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!\\\"#$%&()*+:;<=>?@[{}~]]\"}, Maxconsec: 0, Exact: false},\n\t\"hyresbostader.se\":                    {Minlen: 6, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ichunqiu.com\":                        {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"id.nfpa.org\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"[-\\\"^#$%&'()*+:=@[_|{}~]]\", \"[-\\\"^#$%&'()*+:=@[_|{}~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"id.sonyentertainmentnetwork.com\":     {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!@#^&*=+;:]\"}, Maxconsec: 0, Exact: false},\n\t\"id.westfield.com\":                    {Minlen: 9, Maxlen: 20, Required: []string{\"./:\", \"[!\\\"#&'()*,./:;?@[\\\\^_`{|}~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"identity.codesignal.com\":             {Minlen: 14, Maxlen: 0, Required: []string{\"[!#$%&*@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"identitytheft.gov\":                   {Minlen: 0, Maxlen: 0, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"idestination.info\":                   {Minlen: 0, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"impots.gouv.fr\":                      {Minlen: 12, Maxlen: 128, Required: []string{\"digit\", \"lower\"}, Allowed: []string{\"[-!#$%&*+/=?^_'.{|}]\", \"[-!#$%&*+/=?^_'.{|}]\"}, Maxconsec: 0, Exact: false},\n\t\"indochino.com\":                       {Minlen: 6, Maxlen: 15, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"lower\", \"special\"}, Maxconsec: 0, Exact: false},\n\t\"inntopia.travel\":                     {Minlen: 7, Maxlen: 19, Required: []string{\"digit\"}, Allowed: []string{\"\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"internationalsos.com\":                {Minlen: 0, Maxlen: 0, Required: []string{\"[@#$%^&+=_]\", \"[@#$%^&+=_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"irctc.co.in\":                         {Minlen: 8, Maxlen: 15, Required: []string{\"[!@#$%^&*()+]\", \"[!@#$%^&*()+]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"irs.gov\":                             {Minlen: 8, Maxlen: 32, Required: []string{\"[!#$%&*@]\", \"[!#$%&*@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"jal.co.jp\":                           {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"japanpost.jp\":                        {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"jewelosco.com\":                       {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"jordancu-onlinebanking.org\":          {Minlen: 6, Maxlen: 32, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"keldoc.com\":                          {Minlen: 12, Maxlen: 0, Required: []string{\"[!@#$%^&*]\", \"[!@#$%^&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"kennedy-center.org\":                  {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&*?@]\", \"[!#$%&*?@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"key.harvard.edu\":                     {Minlen: 10, Maxlen: 100, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"\", \"[-@_#!&$`%*+()./,;~:{}|?>=<^[']]\"}, Maxconsec: 0, Exact: false},\n\t\"kfc.ca\":                              {Minlen: 6, Maxlen: 15, Required: []string{\"[!@#$%&?*]\", \"[!@#$%&?*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"kiehls.com\":                          {Minlen: 8, Maxlen: 25, Required: []string{\"[!#$%&?@]\", \"[!#$%&?@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"kingsfoodmarkets.com\":                {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"klm.com\":                             {Minlen: 8, Maxlen: 12, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"kundenportal.edeka-smart.de\":         {Minlen: 8, Maxlen: 16, Required: []string{\"[!\\\"§$%&#]\", \"[!\\\"§$%&#]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"la-z-boy.com\":                        {Minlen: 6, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"labcorp.com\":                         {Minlen: 8, Maxlen: 20, Required: []string{\"[!@#$%^&*]\", \"[!@#$%^&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ladwp.com\":                           {Minlen: 8, Maxlen: 20, Required: []string{\"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"launtel.net.au\":                      {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"leetchi.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{\"./:\", \"[!#$%&()*+,./:;<>?@\\\"_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"lepida.it\":                           {Minlen: 8, Maxlen: 16, Required: []string{\".:\", \"[-!\\\"#$%&'()*+,.:;<=>?@[^_`{|}~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"lg.com\":                              {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\".:\", \"[-!#$%&'()*+,.:;=?@[^_{|}~]]\"}, Maxconsec: 0, Exact: false},\n\t\"linearity.io\":                        {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"live.com\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"\", \"[-@_#!&$`%*+()./,;~:{}|?>=<^'[]]\"}, Maxconsec: 0, Exact: false},\n\t\"lloydsbank.co.uk\":                    {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\"}, Allowed: []string{\"upper\"}, Maxconsec: 0, Exact: false},\n\t\"lowes.com\":                           {Minlen: 8, Maxlen: 128, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"loyalty.accor.com\":                   {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&=@]\", \"[!#$%&=@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"lsacsso.b2clogin.com\":                {Minlen: 8, Maxlen: 16, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"lufthansa.com\":                       {Minlen: 8, Maxlen: 32, Required: []string{\"./:\", \"[!#$%&()*+,./:;<>?@\\\"_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"lufthansagroup.careers\":              {Minlen: 12, Maxlen: 0, Required: []string{\"[!#$%&*?@]\", \"[!#$%&*?@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"macys.com\":                           {Minlen: 7, Maxlen: 16, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"mailbox.org\":                         {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"\", \"[-!$\\\"%&/()=*+#.,;:@?{}[]]\"}, Maxconsec: 0, Exact: false},\n\t\"makemytrip.com\":                      {Minlen: 8, Maxlen: 0, Required: []string{\"[@$!%*#?&]\", \"[@$!%*#?&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"marriott.com\":                        {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[$!#&@?%=]\", \"[$!#&@?%=]\"}, Maxconsec: 0, Exact: false},\n\t\"maybank2u.com.my\":                    {Minlen: 8, Maxlen: 12, Required: []string{\"[-~!@#$%^&*_+=`|(){}[:;\\\"'<>,.?]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"medicare.gov\":                        {Minlen: 8, Maxlen: 16, Required: []string{\"[@!$%^*()]\", \"[@!$%^*()]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"meineschufa.de\":                      {Minlen: 10, Maxlen: 0, Required: []string{\"[!?#%$]\", \"[!?#%$]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"member.everbridge.net\":               {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!@#$%^&*()]\", \"[!@#$%^&*()]\"}, Maxconsec: 0, Exact: false},\n\t\"metlife.com\":                         {Minlen: 6, Maxlen: 20, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"microsoft.com\":                       {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"milogin.michigan.gov\":                {Minlen: 8, Maxlen: 0, Required: []string{\"[@#$!~&]\", \"[@#$!~&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"mintmobile.com\":                      {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{\"[!#$%&()*+:;=@[^_`{}~]]\"}, Maxconsec: 0, Exact: false},\n\t\"mlb.com\":                             {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"mountainwarehouse.com\":               {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"?/`~\\\"()\", \"[-@#$%^&*_+={}|\\\\:',?/`~\\\"();.]\"}, Maxconsec: 0, Exact: false},\n\t\"mpv.tickets.com\":                     {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"museumofflight.org\":                  {Minlen: 8, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"my.konami.net\":                       {Minlen: 8, Maxlen: 32, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"myaccess.dmdc.osd.mil\":               {Minlen: 9, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"\", \"[-@_#!&$`%*+()./,;~:{}|?>=<^'[]]\"}, Maxconsec: 0, Exact: false},\n\t\"mybam.bcbsnm.com\":                    {Minlen: 8, Maxlen: 64, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%&()*@[^{}~]\", \"[!#$%&()*@[^{}~]\"}, Maxconsec: 2, Exact: false},\n\t\"mygoodtogo.com\":                      {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"myhealthrecord.com\":                  {Minlen: 8, Maxlen: 20, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"mypatientvisit.com\":                  {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&*+.;?@^_~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"mypay.dfas.mil\":                      {Minlen: 9, Maxlen: 30, Required: []string{\"[#@$%^!*+=_]\", \"[#@$%^!*+=_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"mysavings.breadfinancial.com\":        {Minlen: 8, Maxlen: 25, Required: []string{\"[+_%@!$*~]\", \"[+_%@!$*~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"mysedgwick.com\":                      {Minlen: 8, Maxlen: 16, Required: []string{\"[@#%^&+=!]\", \"[@#%^&+=!]\", \"digit\", \"upper\"}, Allowed: []string{\"\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"mysubaru.com\":                        {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"./:\", \"[!#$%()*+,./:;=?@\\\\^`~]\"}, Maxconsec: 0, Exact: false},\n\t\"naver.com\":                           {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"nekochat.cn\":                         {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"nelnet.net\":                          {Minlen: 8, Maxlen: 15, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"netflix.com\":                         {Minlen: 4, Maxlen: 60, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"netgear.com\":                         {Minlen: 6, Maxlen: 128, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"nowinstock.net\":                      {Minlen: 6, Maxlen: 20, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"order.wendys.com\":                    {Minlen: 6, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%&()*+/=?^_{}]\", \"[!#$%&()*+/=?^_{}]\"}, Maxconsec: 0, Exact: false},\n\t\"ototoy.jp\":                           {Minlen: 8, Maxlen: 0, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"packageconciergeadmin.com\":           {Minlen: 4, Maxlen: 4, Required: []string{}, Allowed: []string{\"digit\"}, Maxconsec: 0, Exact: false},\n\t\"pavilions.com\":                       {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"paypal.com\":                          {Minlen: 8, Maxlen: 20, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"payvgm.youraccountadvantage.com\":     {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"pilotflyingj.com\":                    {Minlen: 7, Maxlen: 0, Required: []string{\"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"pixnet.cc\":                           {Minlen: 4, Maxlen: 16, Required: []string{}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"planetary.org\":                       {Minlen: 5, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"plazapremiumlounge.com\":              {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"@^]\", \"[!#$%&*,@^]\"}, Maxconsec: 0, Exact: false},\n\t\"portal.edd.ca.gov\":                   {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&()*@^]\", \"[!#$%&()*@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"portals.emblemhealth.com\":            {Minlen: 8, Maxlen: 0, Required: []string{\"./:\", \"[!#$%&'()*+,./:;<>?@\\\\^_`{|}~[]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"portlandgeneral.com\":                 {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%&*?@]\", \"[!#$%&*?@]\"}, Maxconsec: 0, Exact: false},\n\t\"poste.it\":                            {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"posteo.de\":                           {Minlen: 8, Maxlen: 0, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"powells.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"[\\\"!@#$%^&*(){}[]]\", \"[\\\"!@#$%^&*(){}[]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"preferredhotels.com\":                 {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&()*+@^_]\", \"[!#$%&()*+@^_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"premier.ticketek.com.au\":             {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"premierinn.com\":                      {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"lower\"}, Maxconsec: 0, Exact: false},\n\t\"prepaid.bankofamerica.com\":           {Minlen: 8, Maxlen: 16, Required: []string{\"[!@#$%^&*()+~{}'\\\";:<>?]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"prestocard.ca\":                       {Minlen: 8, Maxlen: 0, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"pret.com\":                            {Minlen: 12, Maxlen: 0, Required: []string{\"[@$!%*#?&]\", \"[@$!%*#?&]\", \"digit\", \"lower\"}, Allowed: []string{\"upper\"}, Maxconsec: 0, Exact: false},\n\t\"promozoneapp.nmlottery.com\":          {Minlen: 6, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"propelfuels.com\":                     {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"publix.com\":                          {Minlen: 8, Maxlen: 28, Required: []string{\"lower\", \"upper\"}, Allowed: []string{\"\", \"digit\"}, Maxconsec: 0, Exact: false},\n\t\"qdosstatusreview.com\":                {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&@^]\", \"[!#$%&@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"questdiagnostics.com\":                {Minlen: 8, Maxlen: 30, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"randalls.com\":                        {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"rejsekort.dk\":                        {Minlen: 7, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"renaud-bray.com\":                     {Minlen: 8, Maxlen: 38, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"ring.com\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"[!@#$%^&*<>?]\", \"[!@#$%^&*<>?]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"riteaid.com\":                         {Minlen: 8, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"robinhood.com\":                       {Minlen: 10, Maxlen: 0, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"rogers.com\":                          {Minlen: 8, Maxlen: 0, Required: []string{\"[!@#$]\", \"[!@#$]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ruc.dk\":                              {Minlen: 6, Maxlen: 8, Required: []string{\"[-!#%&(){}*+;%/<=>?_]\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"runescape.com\":                       {Minlen: 5, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ruten.com.tw\":                        {Minlen: 6, Maxlen: 15, Required: []string{\"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"safeway.com\":                         {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"salslimo.com\":                        {Minlen: 8, Maxlen: 50, Required: []string{\"[!@#$&*]\", \"[!@#$&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"santahelenasaude.com.br\":             {Minlen: 8, Maxlen: 15, Required: []string{\"[-!@#$%&*_+=<>]\", \"[-!@#$%&*_+=<>]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"santander.de\":                        {Minlen: 8, Maxlen: 12, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\".:\", \"[-!#$%&'()*,.:;=?^{}]\"}, Maxconsec: 0, Exact: false},\n\t\"savemart.com\":                        {Minlen: 8, Maxlen: 12, Required: []string{\"[!#$%&@]\", \"[!#$%&@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"sbisec.co.jp\":                        {Minlen: 10, Maxlen: 20, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"secure-arborfcu.org\":                 {Minlen: 8, Maxlen: 15, Required: []string{\".:?@[_`~]]\", \"[!#$%&'()+,.:?@[_`~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"secure.orclinic.com\":                 {Minlen: 6, Maxlen: 15, Required: []string{\"digit\", \"lower\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"secure.snnow.ca\":                     {Minlen: 7, Maxlen: 16, Required: []string{\"digit\"}, Allowed: []string{\"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"sephora.com\":                         {Minlen: 6, Maxlen: 12, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"serviziconsolari.esteri.it\":          {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"servizioelettriconazionale.it\":       {Minlen: 8, Maxlen: 20, Required: []string{\"[!#$%&*?@^_~]\", \"[!#$%&*?@^_~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"sfwater.org\":                         {Minlen: 10, Maxlen: 30, Required: []string{\"digit\"}, Allowed: []string{\"\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"shaws.com\":                           {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"signin.ea.com\":                       {Minlen: 8, Maxlen: 64, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[-!@#^&*=+;:]\"}, Maxconsec: 0, Exact: false},\n\t\"sjwaterhub.com\":                      {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#%&*.]\", \"[!#%&*.]\"}, Maxconsec: 0, Exact: false},\n\t\"southwest.com\":                       {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"speedway.com\":                        {Minlen: 4, Maxlen: 8, Required: []string{\"digit\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"spirit.com\":                          {Minlen: 8, Maxlen: 16, Required: []string{\"[!@#$%^&*()]\", \"[!@#$%^&*()]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"splunk.com\":                          {Minlen: 8, Maxlen: 64, Required: []string{\"[-!@#$%&*_+=<>]\", \"[-!@#$%&*_+=<>]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ssa.gov\":                             {Minlen: 0, Maxlen: 0, Required: []string{\"[~!@#$%^&*]\", \"[~!@#$%^&*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"starmarket.com\":                      {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"store.nintendo.co.uk\":                {Minlen: 8, Maxlen: 20, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"store.nvidia.com\":                    {Minlen: 8, Maxlen: 32, Required: []string{\"[-!@#$%^*~:;&><[{}|_+=?]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"store.steampowered.com\":              {Minlen: 6, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[~!@#$%^&*]\", \"[~!@#$%^&*]\"}, Maxconsec: 0, Exact: false},\n\t\"subscribe.free.fr\":                   {Minlen: 8, Maxlen: 16, Required: []string{\"[!#&()*+/@[_]]\", \"[!#&()*+/@[_]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"successfactors.eu\":                   {Minlen: 8, Maxlen: 18, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"sulamericaseguros.com.br\":            {Minlen: 6, Maxlen: 6, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"sunlife.com\":                         {Minlen: 8, Maxlen: 10, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"t-mobile.net\":                        {Minlen: 8, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"target.com\":                          {Minlen: 8, Maxlen: 20, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"tdscpc.gov.in\":                       {Minlen: 8, Maxlen: 15, Required: []string{\"\", \"[ &',;\\\"]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"telekom-dienste.de\":                  {Minlen: 8, Maxlen: 16, Required: []string{\"./<=>?@_{|}~]\", \"[#$%&()*+,./<=>?@_{|}~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"thameswater.co.uk\":                   {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"themovingportal.co.uk\":               {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"?/'~\\\" ()\", \"[-@#$%^&*_+={}|\\\\:',?/'~\\\" ();.[]]\"}, Maxconsec: 0, Exact: false},\n\t\"ticketweb.com\":                       {Minlen: 12, Maxlen: 15, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"tix.soundrink.com\":                   {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"tomthumb.com\":                        {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"training.confluent.io\":               {Minlen: 6, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%*@^_~]\", \"[!#$%*@^_~]\"}, Maxconsec: 0, Exact: false},\n\t\"treasurer.mo.gov\":                    {Minlen: 8, Maxlen: 26, Required: []string{\"[!#$&]\", \"[!#$&]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"truist.com\":                          {Minlen: 8, Maxlen: 28, Required: []string{\":\", \"[!#$%()*,:;=@_]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 2, Exact: false},\n\t\"turkishairlines.com\":                 {Minlen: 6, Maxlen: 6, Required: []string{\"digit\"}, Allowed: []string{}, Maxconsec: 3, Exact: false},\n\t\"twitch.tv\":                           {Minlen: 8, Maxlen: 71, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"twitter.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ubisoft.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"[!@#$%^&*()+]\", \"[-]\", \"[-]; required: [!@#$%^&*()+]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"udel.edu\":                            {Minlen: 12, Maxlen: 30, Required: []string{\"[!@#$%^&*()+]\", \"[!@#$%^&*()+]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"umterps.evenue.net\":                  {Minlen: 14, Maxlen: 0, Required: []string{\"[-~!@#$%^&*_+=`|(){}:;]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"unito.it\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"[-!?+*/:;'\\\"{}()@£$%&=^#[]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"user.ornl.gov\":                       {Minlen: 8, Maxlen: 30, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[!#$%./_]\", \"[!#$%./_]\"}, Maxconsec: 3, Exact: false},\n\t\"usps.com\":                            {Minlen: 8, Maxlen: 50, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"./?@]\", \"[-!\\\"#&'()+,./?@]\"}, Maxconsec: 2, Exact: false},\n\t\"vanguard.com\":                        {Minlen: 6, Maxlen: 20, Required: []string{\"digit\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"vanguardinvestor.co.uk\":              {Minlen: 8, Maxlen: 50, Required: []string{\"digit\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"ventrachicago.com\":                   {Minlen: 8, Maxlen: 0, Required: []string{\"\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"verizonwireless.com\":                 {Minlen: 8, Maxlen: 20, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"unicode\"}, Maxconsec: 0, Exact: false},\n\t\"vetsfirstchoice.com\":                 {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"[?!@$%^+=&]\", \"[?!@$%^+=&]\"}, Maxconsec: 0, Exact: false},\n\t\"vince.com\":                           {Minlen: 8, Maxlen: 0, Required: []string{\"[$%/(){}=?!.,_*|+~#[]]\", \"_*|+~#[]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"virginmobile.ca\":                     {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$@]\", \"[!#$@]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"visa.com\":                            {Minlen: 6, Maxlen: 32, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"visabenefits-auth.axa-assistance.us\": {Minlen: 8, Maxlen: 0, Required: []string{\".:<>?@^{|}]\", \"[!\\\"#$%&()*,.:<>?@^{|}]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"vivo.com.br\":                         {Minlen: 0, Maxlen: 6, Required: []string{}, Allowed: []string{\"digit\"}, Maxconsec: 3, Exact: false},\n\t\"volaris.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"special\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"vons.com\":                            {Minlen: 8, Maxlen: 40, Required: []string{\"[!#$%&*@^]\", \"[!#$%&*@^]\", \"upper\"}, Allowed: []string{\"digit\", \"lower\"}, Maxconsec: 0, Exact: false},\n\t\"wa.aaa.com\":                          {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"ascii-printable\"}, Maxconsec: 0, Exact: false},\n\t\"walkhighlands.co.uk\":                 {Minlen: 9, Maxlen: 15, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{\"special\"}, Maxconsec: 0, Exact: false},\n\t\"walmart.com\":                         {Minlen: 0, Maxlen: 0, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"waze.com\":                            {Minlen: 8, Maxlen: 64, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"wccls.org\":                           {Minlen: 4, Maxlen: 16, Required: []string{}, Allowed: []string{\"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"web.de\":                              {Minlen: 8, Maxlen: 40, Required: []string{}, Allowed: []string{\"\", \"digit\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"wegmans.com\":                         {Minlen: 8, Maxlen: 0, Required: []string{\"[!#$%&*+=?@^]\", \"[!#$%&*+=?@^]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"weibo.com\":                           {Minlen: 6, Maxlen: 16, Required: []string{}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"wellsfargo.com\":                      {Minlen: 8, Maxlen: 32, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"wmata.com\":                           {Minlen: 8, Maxlen: 0, Required: []string{\".?[]]\", \"[-!@#$%^&*~/\\\"()_=+\\\\|,.?[]]\", \"digit\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"worldstrides.com\":                    {Minlen: 8, Maxlen: 0, Required: []string{\"[-!#$%&*+=?@^_~]\", \"[-!#$%&*+=?@^_~]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"wsj.com\":                             {Minlen: 5, Maxlen: 15, Required: []string{\"digit\"}, Allowed: []string{\"\", \"lower\", \"upper\"}, Maxconsec: 0, Exact: false},\n\t\"xfinity.com\":                         {Minlen: 8, Maxlen: 16, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"xvoucher.com\":                        {Minlen: 11, Maxlen: 0, Required: []string{\"[!@#$%&_]\", \"[!@#$%&_]\", \"digit\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"yatra.com\":                           {Minlen: 8, Maxlen: 0, Required: []string{\".:?@[_`~]]\", \"[!#$%&'()+,.:?@[_`~]]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"yeti.com\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"[#$%*]\", \"[#$%*]\", \"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"zara.com\":                            {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 0, Exact: false},\n\t\"zdf.de\":                              {Minlen: 8, Maxlen: 0, Required: []string{\"digit\", \"upper\"}, Allowed: []string{\"lower\", \"special\"}, Maxconsec: 0, Exact: false},\n\t\"zoom.us\":                             {Minlen: 8, Maxlen: 32, Required: []string{\"digit\", \"lower\", \"upper\"}, Allowed: []string{}, Maxconsec: 6, Exact: false},\n}\n"
  },
  {
    "path": "pkg/pwgen/pwrules/pwrules_test.go",
    "content": "package pwrules\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseRule(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range []struct {\n\t\tin  string\n\t\tout Rule\n\t}{\n\t\t{\n\t\t\tin: \"minlength: 8; maxlength: 20; required: upper; required: lower; required: digit; max-consecutive: 3; allowed: [@#*()+={}/?~;,.-_];\",\n\t\t\tout: Rule{\n\t\t\t\tMinlen: 8,\n\t\t\t\tMaxlen: 20,\n\t\t\t\tRequired: []string{\n\t\t\t\t\t\"digit\",\n\t\t\t\t\t\"lower\",\n\t\t\t\t\t\"upper\",\n\t\t\t\t},\n\t\t\t\tAllowed: []string{\n\t\t\t\t\t\"[@#*()+={}/?~;,.-_]\",\n\t\t\t\t},\n\t\t\t\tMaxconsec: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tin: \"minlength: 7; maxlength: 16; required: lower, upper; required: digit; required: [`!@#$%^&*()+~{}'\\\";:<>?]];\",\n\t\t\tout: Rule{\n\t\t\t\tMinlen: 7,\n\t\t\t\tMaxlen: 16,\n\t\t\t\tRequired: []string{\n\t\t\t\t\t\"[`!@#$%^&*()+~{}'\\\";:<>?]]\",\n\t\t\t\t\t\"digit\",\n\t\t\t\t\t\"lower\",\n\t\t\t\t\t\"upper\",\n\t\t\t\t},\n\t\t\t\tAllowed: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tin: \"minlength: 8; maxlength: 16;\",\n\t\t\tout: Rule{\n\t\t\t\tMinlen:   8,\n\t\t\t\tMaxlen:   16,\n\t\t\t\tRequired: []string{},\n\t\t\t\tAllowed:  []string{},\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.in, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tc.out, ParseRule(tc.in))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/pwgen/rand.go",
    "content": "package pwgen\n\nimport (\n\tcrand \"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"math/rand\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc init() {\n\t// seed math/rand in case we have to fall back to using it\n\trandFallback = rand.New(rand.NewSource(time.Now().Unix() + int64(os.Getpid()+os.Getppid())))\n}\n\nvar randFallback *rand.Rand\n\nfunc randomInteger(maxVal int) int {\n\ti, err := crand.Int(crand.Reader, big.NewInt(int64(maxVal)))\n\tif err == nil {\n\t\treturn int(i.Int64())\n\t}\n\n\tfmt.Fprintln(os.Stderr, \"WARNING: No crypto/rand available. Falling back to PRNG\")\n\n\treturn randFallback.Intn(maxVal)\n}\n"
  },
  {
    "path": "pkg/pwgen/validate.go",
    "content": "package pwgen\n\nimport (\n\t\"strings\"\n)\n\n// containsAllClasses validates that the password contains at least one\n// character from each given character class. Can also contain other classes.\nfunc containsAllClasses(pw string, classes ...string) bool {\nCLASSES:\n\tfor _, class := range classes {\n\t\tfor _, ch := range class {\n\t\t\tif strings.Contains(pw, string(ch)) {\n\t\t\t\tcontinue CLASSES\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// containsOnlyClasses validates that the password only contains characters\n// from the given classes. Must not satisfy all classes.\nfunc containsOnlyClasses(pw string, classes ...string) bool {\n\tfor _, c := range pw {\n\t\tfor _, class := range classes {\n\t\t\tif !strings.Contains(class, string(c)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc containsMaxConsecutive(pw string, n int) bool {\n\tlast := \"\"\n\trepCnt := 1\n\n\tfor _, r := range pw {\n\t\tif last == string(r) {\n\t\t\trepCnt++\n\t\t\tif repCnt >= n {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\trepCnt = 1\n\t\t}\n\n\t\tlast = string(r)\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "pkg/pwgen/validate_test.go",
    "content": "package pwgen\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMaxConsec(t *testing.T) {\n\tt.Parallel()\n\n\t// good\n\tfor _, tc := range []string{\n\t\t\"abcd\",\n\t\t\"foobar\",\n\t\t\"nope\",\n\t\t\"AaAa\",\n\t\t\"aaabbbaaa\",\n\t} {\n\t\tassert.True(t, containsMaxConsecutive(tc, 4))\n\t}\n\t// bad\n\tfor _, tc := range []string{\n\t\t\"aaaa\",\n\t\t\"bbb\",\n\t\t\"fooobar\",\n\t\t\"AaaaA\",\n\t} {\n\t\tassert.False(t, containsMaxConsecutive(tc, 3))\n\t}\n}\n\nfunc TestContainsOnly(t *testing.T) {\n\tt.Parallel()\n\n\t// good\n\tfor _, tc := range []string{\n\t\t\"aBcDeF\",\n\t} {\n\t\tassert.True(t, containsOnlyClasses(tc, Upper+Lower))\n\t}\n\n\t// bad\n\tfor _, tc := range []string{\n\t\t\"aBcDeF3\",\n\t} {\n\t\tassert.False(t, containsOnlyClasses(tc, Upper+Lower))\n\t}\n}\n"
  },
  {
    "path": "pkg/pwgen/wordlist.go",
    "content": "package pwgen\n\nimport \"strings\"\n\n// wordlist is a slice of 2048 english mnemonic words taken from the bip39 specification\n// https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt\nvar wordlist = strings.Split(`abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap garage garbage garden garlic garment gas gasp gate gather gauge gaze general genius genre gentle genuine gesture ghost giant gift giggle ginger giraffe girl give glad glance glare glass glide glimpse globe gloom glory glove glow glue goat goddess gold good goose gorilla gospel gossip govern gown grab grace grain grant grape grass gravity great green grid grief grit grocery group grow grunt guard guess guide guilt guitar gun gym habit hair half hammer hamster hand happy harbor hard harsh harvest hat have hawk hazard head health heart heavy hedgehog height hello helmet help hen hero hidden high hill hint hip hire history hobby hockey hold hole holiday hollow home honey hood hope horn horror horse hospital host hotel hour hover hub huge human humble humor hundred hungry hunt hurdle hurry hurt husband hybrid ice icon idea identify idle ignore ill illegal illness image imitate immense immune impact impose improve impulse inch include income increase index indicate indoor industry infant inflict inform inhale inherit initial inject injury inmate inner innocent input inquiry insane insect inside inspire install intact interest into invest invite involve iron island isolate issue item ivory jacket jaguar jar jazz jealous jeans jelly jewel job join joke journey joy judge juice jump jungle junior junk just kangaroo keen keep ketchup key kick kid kidney kind kingdom kiss kit kitchen kite kitten kiwi knee knife knock know lab label labor ladder lady lake lamp language laptop large later latin laugh laundry lava law lawn lawsuit layer lazy leader leaf learn leave lecture left leg legal legend leisure lemon lend length lens leopard lesson letter level liar liberty library license life lift light like limb limit link lion liquid list little live lizard load loan lobster local lock logic lonely long loop lottery loud lounge love loyal lucky luggage lumber lunar lunch luxury lyrics machine mad magic magnet maid mail main major make mammal man manage mandate mango mansion manual maple marble march margin marine market marriage mask mass master match material math matrix matter maximum maze meadow mean measure meat mechanic medal media melody melt member memory mention menu mercy merge merit merry mesh message metal method middle midnight milk million mimic mind minimum minor minute miracle mirror misery miss mistake mix mixed mixture mobile model modify mom moment monitor monkey monster month moon moral more morning mosquito mother motion motor mountain mouse move movie much muffin mule multiply muscle museum mushroom music must mutual myself mystery myth naive name napkin narrow nasty nation nature near neck need negative neglect neither nephew nerve nest net network neutral never news next nice night noble noise nominee noodle normal north nose notable note nothing notice novel now nuclear number nurse nut oak obey object oblige obscure observe obtain obvious occur ocean october odor off offer office often oil okay old olive olympic omit once one onion online only open opera opinion oppose option orange orbit orchard order ordinary organ orient original orphan ostrich other outdoor outer output outside oval oven over own owner oxygen oyster ozone pact paddle page pair palace palm panda panel panic panther paper parade parent park parrot party pass patch path patient patrol pattern pause pave payment peace peanut pear peasant pelican pen penalty pencil people pepper perfect permit person pet phone photo phrase physical piano picnic picture piece pig pigeon pill pilot pink pioneer pipe pistol pitch pizza place planet plastic plate play please pledge pluck plug plunge poem poet point polar pole police pond pony pool popular portion position possible post potato pottery poverty powder power practice praise predict prefer prepare present pretty prevent price pride primary print priority prison private prize problem process produce profit program project promote proof property prosper protect proud provide public pudding pull pulp pulse pumpkin punch pupil puppy purchase purity purpose purse push put puzzle pyramid quality quantum quarter question quick quit quiz quote rabbit raccoon race rack radar radio rail rain raise rally ramp ranch random range rapid rare rate rather raven raw razor ready real reason rebel rebuild recall receive recipe record recycle reduce reflect reform refuse region regret regular reject relax release relief rely remain remember remind remove render renew rent reopen repair repeat replace report require rescue resemble resist resource response result retire retreat return reunion reveal review reward rhythm rib ribbon rice rich ride ridge rifle right rigid ring riot ripple risk ritual rival river road roast robot robust rocket romance roof rookie room rose rotate rough round route royal rubber rude rug rule run runway rural sad saddle sadness safe sail salad salmon salon salt salute same sample sand satisfy satoshi sauce sausage save say scale scan scare scatter scene scheme school science scissors scorpion scout scrap screen script scrub sea search season seat second secret section security seed seek segment select sell seminar senior sense sentence series service session settle setup seven shadow shaft shallow share shed shell sheriff shield shift shine ship shiver shock shoe shoot shop short shoulder shove shrimp shrug shuffle shy sibling sick side siege sight sign silent silk silly silver similar simple since sing siren sister situate six size skate sketch ski skill skin skirt skull slab slam sleep slender slice slide slight slim slogan slot slow slush small smart smile smoke smooth snack snake snap sniff snow soap soccer social sock soda soft solar soldier solid solution solve someone song soon sorry sort soul sound soup source south space spare spatial spawn speak special speed spell spend sphere spice spider spike spin spirit split spoil sponsor spoon sport spot spray spread spring spy square squeeze squirrel stable stadium staff stage stairs stamp stand start state stay steak steel stem step stereo stick still sting stock stomach stone stool story stove strategy street strike strong struggle student stuff stumble style subject submit subway success such sudden suffer sugar suggest suit summer sun sunny sunset super supply supreme sure surface surge surprise surround survey suspect sustain swallow swamp swap swarm swear sweet swift swim swing switch sword symbol symptom syrup system table tackle tag tail talent talk tank tape target task taste tattoo taxi teach team tell ten tenant tennis tent term test text thank that theme then theory there they thing this thought three thrive throw thumb thunder ticket tide tiger tilt timber time tiny tip tired tissue title toast tobacco today toddler toe together toilet token tomato tomorrow tone tongue tonight tool tooth top topic topple torch tornado tortoise toss total tourist toward tower town toy track trade traffic tragic train transfer trap trash travel tray treat tree trend trial tribe trick trigger trim trip trophy trouble truck true truly trumpet trust truth try tube tuition tumble tuna tunnel turkey turn turtle twelve twenty twice twin twist two type typical ugly umbrella unable unaware uncle uncover under undo unfair unfold unhappy uniform unique unit universe unknown unlock until unusual unveil update upgrade uphold upon upper upset urban urge usage use used useful useless usual utility vacant vacuum vague valid valley valve van vanish vapor various vast vault vehicle velvet vendor venture venue verb verify version very vessel veteran viable vibrant vicious victory video view village vintage violin virtual virus visa visit visual vital vivid vocal voice void volcano volume vote voyage wage wagon wait walk wall walnut want warfare warm warrior wash wasp waste water wave way wealth weapon wear weasel weather web wedding weekend weird welcome west wet whale what wheat wheel when where whip whisper wide width wife wild will win window wine wing wink winner winter wire wisdom wise wish witness wolf woman wonder wood wool word work world worry worth wrap wreck wrestle wrist write wrong yard year yellow you young youth zebra zero zone zoo`, \" \")\n"
  },
  {
    "path": "pkg/pwgen/xkcdgen/pwgen.go",
    "content": "// Package xkcdgen provides a simple wrapper around the xkcdpwgen\n// package to generate random passphrases.\npackage xkcdgen\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/martinhoefling/goxkcdpwgen/xkcdpwgen\"\n)\n\n// Random returns a random passphrase combined from four words.\nfunc Random() string {\n\tpassword, _ := RandomLength(4, \"en\")\n\n\treturn password\n}\n\n// RandomLength returns a random passphrase combined from the desired number\n// of words. Words are drawn from lang.\nfunc RandomLength(length int, lang string) (string, error) {\n\treturn RandomLengthDelim(length, \" \", lang, false, false)\n}\n\n// RandomLengthDelim returns a random passphrase combined from the desired number\n// of words and the given delimiter. Words are drawn from lang.\nfunc RandomLengthDelim(length int, delim, lang string, capitalize, numbers bool) (string, error) {\n\tg := xkcdpwgen.NewGenerator()\n\tg.SetNumWords(length)\n\tg.SetDelimiter(delim)\n\tg.SetCapitalize(delim == \"\" || capitalize)\n\tg.SetRandomNumbers(numbers)\n\n\tif err := g.UseLangWordlist(lang); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to use wordlist for lang %s: %w\", lang, err)\n\t}\n\n\treturn g.GeneratePasswordString(), nil\n}\n"
  },
  {
    "path": "pkg/pwgen/xkcdgen/pwgen_test.go",
    "content": "package xkcdgen\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRandom(t *testing.T) {\n\tt.Parallel()\n\n\tpw := Random()\n\tif len(pw) < 4 {\n\t\tt.Errorf(\"too short\")\n\t}\n\n\tif len(strings.Fields(pw)) < 4 {\n\t\tt.Errorf(\"too few words\")\n\t}\n}\n\nfunc TestRandomLengthDelim(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := RandomLengthDelim(10, \" \", \"cn_ZH\", false, false)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "pkg/qrcon/qrcon.go",
    "content": "// Package qrcon implements a QR Code ANSI printer for displaying QR codes on\n// the console.\npackage qrcon\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"strings\"\n\n\t\"github.com/skip2/go-qrcode\"\n)\n\nconst (\n\tblack = \"\\033[40m  \\033[0m\"\n\twhite = \"\\033[47m  \\033[0m\"\n)\n\n// ErrUnknowColor is returned when the color is unknown.\nvar ErrUnknowColor = fmt.Errorf(\"unknown color\")\n\n// QRCode returns a string containing an ANSI encoded QR Code.\n// This can be printed to a terminal that supports ANSI escape codes.\nfunc QRCode(content string) (string, error) {\n\tq, err := qrcode.New(content, qrcode.Medium)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create qr code: %w\", err)\n\t}\n\n\tvar sb strings.Builder\n\n\ti := q.Image(0)\n\tb := i.Bounds()\n\n\tfor x := b.Min.X; x < b.Max.X; x++ {\n\t\tfor y := b.Min.Y; y < b.Max.Y; y++ {\n\t\t\tcol := i.At(x, y)\n\n\t\t\tswitch {\n\t\t\tcase sameColor(col, q.ForegroundColor):\n\t\t\t\t_, _ = sb.WriteString(black)\n\t\t\tcase sameColor(col, q.BackgroundColor):\n\t\t\t\t_, _ = sb.WriteString(white)\n\t\t\tdefault:\n\t\t\t\treturn \"\", fmt.Errorf(\"error at (%d,%d): %+v: %w\", x, y, col, ErrUnknowColor)\n\t\t\t}\n\t\t}\n\n\t\t_, _ = sb.WriteString(\"\\n\")\n\t}\n\n\treturn sb.String(), nil\n}\n\nfunc sameColor(a color.Color, b color.Color) bool {\n\tr1, g1, b1, a1 := a.RGBA()\n\tr2, g2, b2, a2 := b.RGBA()\n\n\tif r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/qrcon/qrcon_test.go",
    "content": "package qrcon\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ExampleQRCode() { //nolint:testableexamples\n\tcode, err := QRCode(\"foo\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(code)\n}\n\nfunc TestQRCode(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := QRCode(\"https://www.gopass.pw/\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "pkg/set/filter.go",
    "content": "package set\n\n// Filter filters all elements in r from the input list.\nfunc Filter[K comparable](in []K, r ...K) []K {\n\trs := Map(r)\n\tvar out []K\n\n\tfor _, i := range in {\n\t\tif !rs[i] {\n\t\t\tout = append(out, i)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Contains returns true if e is contained in the input list.\nfunc Contains[K comparable](in []K, e K) bool {\n\trs := Map(in)\n\n\t_, found := rs[e]\n\n\treturn found\n}\n"
  },
  {
    "path": "pkg/set/filter_test.go",
    "content": "package set\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFilter(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tout := Filter(in, 6, 7, 8, 9)\n\n\tassert.Equal(t, []int{1, 2, 3, 4, 5}, out)\n}\n\nfunc TestFilter_EmptyInput(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{}\n\tout := Filter(in, 1, 2, 3)\n\n\tassert.Equal(t, []int(nil), out)\n}\n\nfunc TestFilter_NoElementsToRemove(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{1, 2, 3, 4, 5}\n\tout := Filter(in)\n\n\tassert.Equal(t, []int{1, 2, 3, 4, 5}, out)\n}\n\nfunc TestFilter_RemoveNonExistentElements(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{1, 2, 3, 4, 5}\n\tout := Filter(in, 6, 7, 8)\n\n\tassert.Equal(t, []int{1, 2, 3, 4, 5}, out)\n}\n\nfunc TestContains(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{1, 2, 3, 4, 5}\n\n\tassert.True(t, Contains(in, 3))\n\tassert.False(t, Contains(in, 6))\n}\n\nfunc TestContains_EmptyInput(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{}\n\n\tassert.False(t, Contains(in, 1))\n}\n"
  },
  {
    "path": "pkg/set/map.go",
    "content": "package set\n\n// Map takes a slice of a given type and creates a boolean map with keys\n// of that type.\nfunc Map[K comparable](in []K) map[K]bool {\n\tm := make(map[K]bool, len(in))\n\tfor _, i := range in {\n\t\tm[i] = true\n\t}\n\n\treturn m\n}\n\n// Apply applies the given function to every element of the slice.\nfunc Apply[K comparable](in []K, f func(K) K) []K {\n\tout := make([]K, len(in))\n\tfor i, v := range in {\n\t\tout[i] = f(v)\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "pkg/set/map_test.go",
    "content": "package set\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMapFunc(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, map[int]bool{1: true, 2: true, 3: true}, Map([]int{1, 2, 3}))\n}\n\nfunc TestApplyFunc(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, []int{2, 3, 4}, Apply([]int{1, 2, 3}, func(i int) int { return i + 1 }))\n}\n"
  },
  {
    "path": "pkg/set/set.go",
    "content": "// Package set provides a generic set implementation.\npackage set\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Set is a generic set type, implemented as a map of keys to booleans.\ntype Set[K cmp.Ordered] map[K]bool\n\n// New initializes a new Set with the given elements.\nfunc New[K cmp.Ordered](elems ...K) Set[K] {\n\ts := make(map[K]bool, len(elems))\n\n\tfor _, e := range elems {\n\t\ts[e] = true\n\t}\n\n\treturn s\n}\n\n// String returns a string representation of the set.\nfunc (s Set[K]) String() string {\n\tif s.Empty() {\n\t\treturn \"ø\"\n\t}\n\telems := make([]string, len(s))\n\tfor i, e := range s.Elements() {\n\t\telems[i] = fmt.Sprintf(\"%v\", e)\n\t}\n\n\treturn fmt.Sprintf(\"{%s}\", strings.Join(elems, \", \"))\n}\n\n// Elements returns the elements of the set in\n// sorted order.\nfunc (s Set[K]) Elements() []K {\n\treturn SortedKeys(s)\n}\n\n// Empty returns true if the set is empty.\nfunc (s Set[K]) Empty() bool {\n\treturn len(s) == 0\n}\n\n// Len returns the length of the set.\nfunc (s Set[K]) Len() int {\n\treturn len(s)\n}\n\n// Clone creates a copy of the set.\nfunc (s Set[K]) Clone() Set[K] {\n\tc := Set[K]{}\n\tc.Update(s)\n\n\treturn c\n}\n\n// Update adds all elements from s2 to the set.\nfunc (s *Set[K]) Update(s2 Set[K]) bool {\n\til := len(*s)\n\tif *s == nil && len(s2) > 0 {\n\t\t*s = make(Set[K], len(s2))\n\t}\n\tfor k := range s2 {\n\t\t(*s)[k] = true\n\t}\n\n\treturn len(*s) != il\n}\n\n// Equals returns true if s and s2 contain\n// exactly the same elements.\nfunc (s Set[K]) Equals(s2 Set[K]) bool {\n\treturn len(s) == len(s2) && s.IsSubset(s2)\n}\n\n// Contains returns true if the set contains the given\n// element.\nfunc (s Set[K]) Contains(e K) bool {\n\tfor k := range s {\n\t\tif k == e {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// IsSubset returns true if all elements of s\n// are contained in s2.\nfunc (s Set[K]) IsSubset(s2 Set[K]) bool {\n\tif s.Empty() {\n\t\treturn true\n\t}\n\tif len(s) > len(s2) {\n\t\treturn false\n\t}\n\n\tfor k := range s {\n\t\tif !s2[k] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Union returns a new set containing all elements from\n// s and s2. A ∪ B.\nfunc (s Set[K]) Union(s2 Set[K]) Set[K] {\n\tif s.Empty() {\n\t\treturn s2\n\t}\n\tif s2.Empty() {\n\t\treturn s\n\t}\n\n\tset := make(Set[K])\n\tfor k := range s {\n\t\tset[k] = true\n\t}\n\tfor k := range s2 {\n\t\tset[k] = true\n\t}\n\n\treturn set\n}\n\n// Difference returns the set difference. That is all the things that are in s\n// but not in s2. A \\ B.\nfunc (s Set[K]) Difference(s2 Set[K]) Set[K] {\n\tif s2.Empty() {\n\t\treturn s\n\t}\n\tif s.Empty() {\n\t\treturn New[K]()\n\t}\n\n\tset := make(Set[K])\n\tfor k := range s {\n\t\tif s2[k] {\n\t\t\tcontinue\n\t\t}\n\n\t\tset[k] = true\n\t}\n\n\treturn set\n}\n\n// SymmetricDifference returns the symmetric difference. That is all the things that are\n// in s or s2 but not in both. A Δ B.\nfunc (s Set[K]) SymmetricDifference(s2 Set[K]) Set[K] {\n\tif s2.Empty() {\n\t\treturn s\n\t}\n\tif s.Empty() {\n\t\treturn s2\n\t}\n\n\tset := make(Set[K])\n\tfor k := range s {\n\t\tif s2[k] {\n\t\t\tcontinue\n\t\t}\n\n\t\tset[k] = true\n\t}\n\tfor k := range s2 {\n\t\tif s[k] {\n\t\t\tcontinue\n\t\t}\n\n\t\tset[k] = true\n\t}\n\n\treturn set\n}\n\n// Add adds the given elements to the set.\nfunc (s *Set[K]) Add(elems ...K) bool {\n\til := len(*s)\n\tif *s == nil {\n\t\t*s = make(Set[K])\n\t}\n\tfor _, k := range elems {\n\t\t(*s)[k] = true\n\t}\n\n\treturn len(*s) != il\n}\n\n// Remove deletes the given elements from the set.\nfunc (s Set[K]) Remove(s2 Set[K]) bool {\n\tif s.Empty() {\n\t\treturn false\n\t}\n\til := len(s)\n\tfor k := range s2 {\n\t\tdelete(s, k)\n\t}\n\n\treturn len(s) != il\n}\n\n// Discard deletes the given elements from the set.\nfunc (s Set[K]) Discard(elems ...K) bool {\n\tif s.Empty() {\n\t\treturn false\n\t}\n\til := len(s)\n\tfor _, e := range elems {\n\t\tdelete(s, e)\n\t}\n\n\treturn len(s) != il\n}\n\n// Map returns a new set by applying the function f\n// to all its elements.\nfunc (s Set[K]) Map(f func(K) K) Set[K] {\n\tout := make(Set[K], len(s))\n\tfor k := range s {\n\t\tout.Add(f(k))\n\t}\n\n\treturn out\n}\n\n// Each applies the function f to all its elements.\nfunc (s Set[K]) Each(f func(K)) {\n\tfor k := range s {\n\t\tf(k)\n\t}\n}\n\n// Select returns a new set with all the elements for\n// which f returns true.\nfunc (s Set[K]) Select(f func(K) bool) Set[K] {\n\tout := make(Set[K], len(s))\n\tfor k := range s {\n\t\tif f(k) {\n\t\t\tout.Add(k)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Partition returns two new sets: the first contains all\n// the elements for which f returns true. The second contains the others.\nfunc (s Set[K]) Partition(f func(K) bool) (Set[K], Set[K]) {\n\tyes := make(Set[K], len(s))\n\tno := make(Set[K], len(s))\n\tfor k := range s {\n\t\tif f(k) {\n\t\t\tyes.Add(k)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tno.Add(k)\n\t}\n\n\treturn yes, no\n}\n\n// Choose returns the first element for which f returns true.\nfunc (s Set[K]) Choose(f func(K) bool) (K, bool) {\n\tif f == nil {\n\t\tfor k := range s {\n\t\t\treturn k, true\n\t\t}\n\t}\n\tfor k := range s {\n\t\tif f(k) {\n\t\t\treturn k, true\n\t\t}\n\t}\n\n\tvar zero K\n\n\treturn zero, false\n}\n\n// Count returns the number of elements for which f returns true.\nfunc (s Set[K]) Count(f func(K) bool) int {\n\tn := 0\n\n\tfor k := range s {\n\t\tif f(k) {\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn n\n}\n"
  },
  {
    "path": "pkg/set/set_test.go",
    "content": "package set\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestString(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New[string]()\n\tassert.Equal(t, \"ø\", s1.String())\n\n\ts1.Add(\"a\", \"b\", \"c\")\n\tassert.Equal(t, \"{a, b, c}\", s1.String())\n\n\ts2 := New(1, 2, 3, 4)\n\tassert.Equal(t, \"{1, 2, 3, 4}\", s2.String())\n}\n\nfunc TestElements(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"c\", \"b\", \"a\")\n\tassert.Equal(t, []string{\"a\", \"b\", \"c\"}, s1.Elements())\n}\n\nfunc TestClone(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"c\")\n\ts2 := s1.Clone()\n\n\tassert.Equal(t, s1, s2)\n\n\ts1.Add(\"d\")\n\tassert.NotEqual(t, s1, s2)\n}\n\nfunc TestUpdate(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"c\")\n\ts1.Update(New(\"c\", \"d\", \"e\"))\n\n\tassert.Equal(t, New(\"a\", \"b\", \"c\", \"d\", \"e\"), s1)\n\n\tvar s2 Set[string]\n\ts2.Update(s1)\n\tassert.Equal(t, New(\"a\", \"b\", \"c\", \"d\", \"e\"), s2)\n}\n\nfunc TestEquals(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"c\")\n\ts2 := s1.Clone()\n\n\tassert.True(t, s1.Equals(s2))\n\n\ts1.Add(\"d\")\n\tassert.False(t, s1.Equals(s2))\n}\n\nfunc TestIsSubset(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"b\", \"c\", \"d\")\n\ts2 := New(\"c\", \"d\")\n\n\tassert.True(t, s2.IsSubset(s1))\n\n\ts3 := New[string]()\n\tassert.True(t, s3.IsSubset(s2))\n\tassert.False(t, s2.IsSubset(s3))\n\n\ts4 := New(\"foo\")\n\tassert.False(t, s4.IsSubset(s1))\n}\n\nfunc TestUnion(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"b\", \"c\", \"d\")\n\ts2 := New(\"c\", \"d\")\n\ts3 := New(\"foo\", \"bar\")\n\n\tus := s1.Union(s2).Union(s3)\n\n\tassert.Equal(t, New(\"a\", \"b\", \"c\", \"d\", \"foo\", \"bar\"), us)\n\n\ts4 := New[string]()\n\tassert.Equal(t, s3, s4.Union(s3))\n\n\tassert.Equal(t, s3, s3.Union(s4))\n}\n\nfunc TestDifference(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"c\", \"d\")\n\ts2 := New(\"c\", \"d\")\n\n\tds := s1.Difference(s2)\n\n\tassert.Equal(t, New(\"a\", \"b\"), ds)\n}\n\nfunc TestSymmetricDifference(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"c\", \"d\")\n\ts2 := New(\"c\", \"d\", \"e\")\n\n\tds := s1.SymmetricDifference(s2)\n\n\tassert.Equal(t, New(\"a\", \"b\", \"e\"), ds)\n}\n\nfunc TestAdd(t *testing.T) {\n\tt.Parallel()\n\n\tvar s1 Set[string]\n\ts1.Add(\"a\")\n\n\tassert.Equal(t, New(\"a\"), s1)\n}\n\nfunc TestRemove(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"b\", \"c\", \"d\")\n\ts2 := New(\"c\", \"d\")\n\ts1.Remove(s2)\n\n\tassert.Equal(t, New(\"a\", \"b\"), s1)\n\n\ts3 := New[string]()\n\tassert.False(t, s3.Remove(New(\"a\")))\n}\n\nfunc TestDiscard(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(\"a\", \"b\", \"b\", \"c\", \"d\")\n\ts1.Discard(\"c\", \"d\")\n\n\tassert.Equal(t, New(\"a\", \"b\"), s1)\n\n\ts3 := New[string]()\n\tassert.False(t, s3.Discard(\"a\"))\n}\n\nfunc TestMap(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(1, 2, 3)\n\ts2 := s1.Map(func(i int) int {\n\t\treturn i + 1\n\t})\n\n\tassert.Equal(t, New(2, 3, 4), s2)\n}\n\nfunc TestEach(t *testing.T) {\n\tt.Parallel()\n\n\tseen := 0\n\n\ts1 := New(1, 2, 3)\n\ts1.Each(func(i int) {\n\t\tseen++\n\t})\n\tassert.Equal(t, s1.Len(), seen)\n}\n\nfunc TestSelect(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\ts2 := s1.Select(func(i int) bool {\n\t\treturn i%2 == 0\n\t})\n\n\tassert.Equal(t, New(2, 4, 6, 8, 10), s2)\n}\n\nfunc TestPartition(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\ts2, s3 := s1.Partition(func(i int) bool {\n\t\treturn i%2 == 0\n\t})\n\n\tassert.Equal(t, New(2, 4, 6, 8, 10), s2)\n\tassert.Equal(t, New(1, 3, 5, 7, 9), s3)\n}\n\nfunc TestChoose(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\tv, ok := s1.Choose(func(i int) bool {\n\t\treturn i%3 == 0 && i/3 == 3\n\t})\n\tassert.Equal(t, 9, v)\n\tassert.True(t, ok)\n\n\tv, ok = s1.Choose(func(i int) bool {\n\t\treturn i > 1024\n\t})\n\tassert.Equal(t, 0, v)\n\tassert.False(t, ok)\n\n\ts2 := New(1)\n\tv, ok = s2.Choose(nil)\n\tassert.Equal(t, 1, v)\n\tassert.True(t, ok)\n}\n\nfunc TestCount(t *testing.T) {\n\tt.Parallel()\n\n\ts1 := New(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\tc := s1.Count(func(i int) bool {\n\t\treturn i%3 == 0\n\t})\n\tassert.Equal(t, 3, c)\n}\n"
  },
  {
    "path": "pkg/set/sorted.go",
    "content": "package set\n\nimport (\n\t\"cmp\"\n\t\"maps\"\n\t\"slices\"\n)\n\n// SortedKeys returns the sorted keys of the map.\n// The keys are sorted in ascending order.\nfunc SortedKeys[K cmp.Ordered, V any](m map[K]V) []K {\n\t// sort\n\tkeys := maps.Keys(m)\n\n\treturn slices.Sorted(keys)\n}\n\n// Sorted returns a sorted set of the input.\n// Duplicates are removed.\nfunc Sorted[K cmp.Ordered](l []K) []K {\n\treturn SortedFiltered(l, func(k K) bool {\n\t\treturn true\n\t})\n}\n\n// SortedFiltered returns a sorted set of the input, filtered by the predicate.\n// Duplicates are removed.\nfunc SortedFiltered[K cmp.Ordered](l []K, want func(K) bool) []K {\n\tif len(l) == 0 {\n\t\treturn l\n\t}\n\n\t// deduplicate\n\tm := make(map[K]struct{}, len(l))\n\tfor _, k := range l {\n\t\tif !want(k) {\n\t\t\tcontinue\n\t\t}\n\t\tm[k] = struct{}{}\n\t}\n\n\t// sort\n\treturn SortedKeys(m)\n}\n"
  },
  {
    "path": "pkg/set/sorted_test.go",
    "content": "package set\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSorted(t *testing.T) {\n\tt.Parallel()\n\n\twant := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tin := append(want, want...)\n\trand.Shuffle(len(in), func(i, j int) {\n\t\tin[i], in[j] = in[j], in[i]\n\t})\n\tassert.Equal(t, want, Sorted(in))\n}\n\nfunc TestSortedFiltered(t *testing.T) {\n\tt.Parallel()\n\n\tin := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\tin = append(in, in...)\n\trand.Shuffle(len(in), func(i, j int) {\n\t\tin[i], in[j] = in[j], in[i]\n\t})\n\n\twant := []int{2, 4, 6, 8, 10}\n\tassert.Equal(t, want, SortedFiltered(in, func(i int) bool {\n\t\treturn i%2 == 0\n\t}))\n\n\tassert.Equal(t, []int{}, SortedFiltered([]int{}, func(i int) bool { return true }))\n}\n"
  },
  {
    "path": "pkg/tempfile/file.go",
    "content": "// Package tempfile is a wrapper around os.MkdirTemp, providing an OO pattern\n// as well as secure placement on a temporary ramdisk.\npackage tempfile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// ErrNotInit is returned when the file is not initialized.\nvar ErrNotInit = fmt.Errorf(\"not initialized\")\n\n// globalPrefix is prefixed to all temporary dirs.\nvar globalPrefix string\n\n// File is a temporary file that is stored on a ramdisk if possible.\ntype File struct {\n\tdir string\n\tdev string\n\tfh  *os.File\n}\n\n// New returns a new tempfile wrapper.\n// It will create a temporary directory and a file inside it.\n// If possible, it will mount a ramdisk to the temporary directory.\nfunc New(ctx context.Context, prefix string) (*File, error) {\n\ttd, err := os.MkdirTemp(tempdirBase(), globalPrefix+prefix)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create tempdir: %w\", err)\n\t}\n\n\ttf := &File{\n\t\tdir: td,\n\t}\n\n\tif err := tf.mount(ctx); err != nil {\n\t\t_ = os.RemoveAll(tf.dir)\n\n\t\treturn nil, fmt.Errorf(\"failed to mount %s: %w\", tf.dir, err)\n\t}\n\n\tfn := filepath.Join(tf.dir, \"secret\")\n\n\tfh, err := os.OpenFile(fn, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open file %s: %w\", fn, err)\n\t}\n\n\ttf.fh = fh\n\n\treturn tf, nil\n}\n\n// Name returns the name of the tempfile.\nfunc (t *File) Name() string {\n\tif t.fh == nil {\n\t\treturn \"\"\n\t}\n\n\treturn t.fh.Name()\n}\n\n// Write implements io.Writer.\nfunc (t *File) Write(p []byte) (int, error) {\n\tif t.fh == nil {\n\t\treturn 0, ErrNotInit\n\t}\n\n\treturn t.fh.Write(p) //nolint:wrapcheck\n}\n\n// Close implements io.WriteCloser.\nfunc (t *File) Close() error {\n\tif t.fh == nil {\n\t\treturn nil\n\t}\n\n\treturn t.fh.Close() //nolint:wrapcheck\n}\n\n// Remove attempts to remove the tempfile.\nfunc (t *File) Remove(ctx context.Context) error {\n\t_ = t.Close()\n\n\tif err := t.unmount(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmount %s from %s: %w\", t.dev, t.dir, err)\n\t}\n\n\tif t.dir == \"\" {\n\t\treturn nil\n\t}\n\n\tif err := os.RemoveAll(t.dir); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove %s: %w\", t.dir, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tempfile/file_test.go",
    "content": "package tempfile\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Example() {\n\tctx := config.NewContextInMemory()\n\n\ttempfile, err := New(ctx, \"gopass-secure-\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdefer func() {\n\t\tif err := tempfile.Remove(ctx); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n\n\tfmt.Fprintln(tempfile, \"foobar\")\n\n\tif err := tempfile.Close(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tout, err := os.ReadFile(tempfile.Name())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(out))\n\n\t// Output: foobar\n}\n\nfunc TestTempdirBase(t *testing.T) {\n\tt.Parallel()\n\n\ttempdir := t.TempDir()\n\trequire.NotEmpty(t, tempdir)\n\n\tdefer func() {\n\t\t_ = os.RemoveAll(tempdir)\n\t}()\n}\n\nfunc TestTempdirBaseEmpty(t *testing.T) {\n\toldShm := shmDir\n\tdefer func() {\n\t\tshmDir = oldShm\n\t}()\n\n\tshmDir = \"/this/should/not/exist\"\n\n\tassert.Empty(t, tempdirBase())\n}\n\nfunc TestTempFiler(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\t// regular tempfile\n\ttf, err := New(ctx, \"gp-test-\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\trequire.NoError(t, tf.Close())\n\t}()\n\n\tt.Logf(\"Name: %s\", tf.Name())\n\t_, err = fmt.Fprintf(tf, \"foobar\")\n\trequire.NoError(t, err)\n\n\t// uninitialized tempfile\n\tutf := File{}\n\tassert.Empty(t, utf.Name())\n\t_, err = utf.Write([]byte(\"foo\"))\n\trequire.Error(t, err)\n\trequire.NoError(t, utf.Remove(ctx))\n\trequire.NoError(t, utf.Close())\n}\n\nfunc TestGlobalPrefix(t *testing.T) {\n\tassertPrefix := func(file *File, prefix string) {\n\t\trequirePrefix := filepath.Join(tempdirBase(), prefix)\n\t\tfileOrDirName := file.Name()\n\n\t\tif runtime.GOOS != \"linux\" {\n\t\t\tdir := filepath.Dir(fileOrDirName)\n\t\t\tfileOrDirName = filepath.Base(dir)\n\t\t}\n\n\t\tassert.True(t, strings.HasPrefix(fileOrDirName, requirePrefix))\n\t}\n\tctx := config.NewContextInMemory()\n\n\tassert.Empty(t, globalPrefix)\n\n\t// without global prefix\n\twithoutGlobalPrefix, err := New(ctx, \"some-prefix\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\trequire.NoError(t, withoutGlobalPrefix.Close())\n\t}()\n\n\tassertPrefix(withoutGlobalPrefix, \"some-prefix\")\n\n\t// with global prefix\n\tglobalPrefix = \"global-prefix.\"\n\twithGlobalPrefix, err := New(ctx, \"some-prefix\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tglobalPrefix = \"\"\n\n\t\trequire.NoError(t, withGlobalPrefix.Close())\n\t}()\n\n\tassertPrefix(withGlobalPrefix, \"global-prefix.some-prefix\")\n}\n"
  },
  {
    "path": "pkg/tempfile/mount_darwin.go",
    "content": "//go:build darwin\n\npackage tempfile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gopasspw/gopass/pkg/debug\"\n)\n\nvar shmDir = \"\"\n\nfunc tempdirBase() string {\n\treturn \"\"\n}\n\nfunc (t *File) mount(ctx context.Context) error {\n\t// create 32MB ramdisk\n\tcmd := exec.CommandContext(ctx, \"hdid\", \"-drivekey\", \"system-image=yes\", \"-nomount\", \"ram://32768\")\n\tcmd.Stderr = os.Stderr\n\n\tdebug.Log(\"CMD: %s %+v\", cmd.Path, cmd.Args)\n\tcmdout, err := cmd.Output()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create disk with hdid: %w\", err)\n\t}\n\n\tdebug.Log(\"Output: %s\\n\", cmdout)\n\n\tp := strings.Split(string(cmdout), \" \")\n\tif len(p) < 1 {\n\t\treturn fmt.Errorf(\"unhandeled hdid output: %s\", string(cmdout))\n\t}\n\tt.dev = p[0]\n\n\t// create filesystem on ramdisk\n\tcmd = exec.CommandContext(ctx, \"newfs_hfs\", \"-M\", \"700\", t.dev)\n\tcmd.Stderr = os.Stderr\n\n\tif debug.IsEnabled() {\n\t\tcmd.Stdout = os.Stdout\n\t}\n\n\tdebug.Log(\"CMD: %s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to make filesystem on %s: %w\", t.dev, err)\n\t}\n\n\t// mount ramdisk\n\tcmd = exec.CommandContext(ctx, \"diskutil\", \"mount\", \"nobrowse\", \"-mountOptions\", \"noatime\", \"-mountpoint\", t.dir, t.dev)\n\tcmd.Stderr = os.Stderr\n\tif debug.IsEnabled() {\n\t\tcmd.Stdout = os.Stdout\n\t}\n\n\tdebug.Log(\"CMD: %s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to mount filesystem %s to %s: %w\", t.dev, t.dir, err)\n\t}\n\n\t// Wait for the mount to settle. This is a hack.\n\ttime.Sleep(100 * time.Millisecond)\n\n\treturn nil\n}\n\nfunc (t *File) unmount(ctx context.Context) error {\n\tbo := backoff.NewExponentialBackOff()\n\tbo.MaxElapsedTime = 10 * time.Second\n\n\treturn backoff.Retry(func() error {\n\t\treturn t.tryUnmount(ctx)\n\t}, bo)\n}\n\nfunc (t *File) tryUnmount(ctx context.Context) error {\n\tif t.dir == \"\" || t.dev == \"\" {\n\t\treturn nil\n\t}\n\n\t// unmount ramdisk\n\tcmd := exec.CommandContext(ctx, \"diskutil\", \"unmountDisk\", t.dev)\n\tcmd.Stderr = os.Stderr\n\tif debug.IsEnabled() {\n\t\tcmd.Stdout = os.Stdout\n\t}\n\n\tdebug.Log(\"CMD: %s %+v\", cmd.Path, cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run command '%+v': %w\", cmd.Args, err)\n\t}\n\n\t// eject disk\n\tcmd = exec.CommandContext(ctx, \"diskutil\", \"quiet\", \"eject\", t.dev)\n\tcmd.Stderr = os.Stderr\n\tif debug.IsEnabled() {\n\t\tcmd.Stdout = os.Stdout\n\t}\n\n\tdebug.Log(\"CMD: %s %+v\", cmd.Path, cmd.Args)\n\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "pkg/tempfile/mount_linux.go",
    "content": "//go:build linux\n\npackage tempfile\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nvar shmDir = \"/dev/shm\"\n\n// tempdir returns a temporary directory suiteable for sensitive data. It tries\n// /dev/shm but if this isn't working it will return an empty string. Using\n// this with ioutil.Tempdir will ensure that we're getting the \"best\" tempdir.\nfunc tempdirBase() string {\n\tif fi, err := os.Stat(shmDir); err == nil {\n\t\tif fi.IsDir() {\n\t\t\tif unix.Access(shmDir, unix.W_OK) == nil {\n\t\t\t\treturn shmDir\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc (t *File) mount(context.Context) error {\n\treturn nil\n}\n\nfunc (t *File) unmount(context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tempfile/mount_others.go",
    "content": "//go:build !linux && !darwin\n\npackage tempfile\n\nimport \"context\"\n\nvar shmDir = \"\"\n\n// tempdir returns a temporary directory suiteable for sensitive data. On\n// Windows, just return empty string for ioutil.TempFile.\nfunc tempdirBase() string {\n\treturn \"\"\n}\n\nfunc (t *File) mount(context.Context) error {\n\t_ = t.dev // to trick megacheck\n\treturn nil\n}\n\nfunc (t *File) unmount(context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/termio/ask.go",
    "content": "package termio\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n)\n\nvar (\n\t// Stderr is exported for tests.\n\tStderr io.Writer = os.Stderr\n\t// Stdin is exported for tests.\n\tStdin io.Reader = os.Stdin\n\t// ErrAborted is returned if the user aborts an action.\n\tErrAborted = fmt.Errorf(\"user aborted\")\n\t// ErrInvalidInput is returned if the user enters invalid input.\n\tErrInvalidInput = fmt.Errorf(\"no valid user input\")\n)\n\nconst (\n\tmaxTries = 42\n)\n\n// AskForString asks for a string once, using the default if the\n// answer is empty. Errors are only returned on I/O errors.\nfunc AskForString(ctx context.Context, text, def string) (string, error) {\n\tif ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) {\n\t\treturn def, nil\n\t}\n\n\t// check for context cancelation\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn def, ErrAborted\n\tdefault:\n\t}\n\n\tfmt.Fprintf(Stderr, \"%s [%s]: \", text, def)\n\n\tinput, err := NewReader(ctx, Stdin).ReadLine()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read user input: %w\", err)\n\t}\n\n\tinput = strings.TrimSpace(input)\n\tif input == \"\" {\n\t\tinput = def\n\t}\n\n\treturn input, nil\n}\n\n// AskForBool ask for a bool (yes or no) exactly once.\n// The empty answer uses the specified default, any other answer\n// is an error.\nfunc AskForBool(ctx context.Context, text string, def bool) (bool, error) {\n\tif ctxutil.IsAlwaysYes(ctx) {\n\t\treturn def, nil\n\t}\n\n\tchoices := \"y/N/q\"\n\tif def {\n\t\tchoices = \"Y/n/q\"\n\t}\n\n\tstr, err := AskForString(ctx, text, choices)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to read user input: %w\", err)\n\t}\n\n\tswitch str {\n\tcase \"Y/n/q\":\n\t\treturn true, nil\n\tcase \"y/N/q\":\n\t\treturn false, nil\n\t}\n\n\tstr = strings.ToLower(string(str[0]))\n\tswitch str {\n\tcase \"y\":\n\t\treturn true, nil\n\tcase \"n\":\n\t\treturn false, nil\n\tcase \"q\":\n\t\treturn false, ErrAborted\n\tdefault:\n\t\treturn false, fmt.Errorf(\"unknown answer '%s': %w\", str, ErrInvalidInput)\n\t}\n}\n\n// AskForInt asks for a valid integer once. If the input\n// can not be converted to an int it returns an error.\nfunc AskForInt(ctx context.Context, text string, def int) (int, error) {\n\tif ctxutil.IsAlwaysYes(ctx) {\n\t\treturn def, nil\n\t}\n\n\tstr, err := AskForString(ctx, text+\" (q to abort)\", strconv.Itoa(def))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif str == \"q\" {\n\t\treturn 0, ErrAborted\n\t}\n\n\tintVal, err := strconv.Atoi(str)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to convert to number: %w\", err)\n\t}\n\n\treturn intVal, nil\n}\n\n// AskForConfirmation asks a yes/no question until the user\n// replies yes or no.\n// It will retry until a valid answer is given or the user aborts.\nfunc AskForConfirmation(ctx context.Context, text string) bool {\n\tif ctxutil.IsAlwaysYes(ctx) {\n\t\treturn true\n\t}\n\n\tfor range maxTries {\n\t\tchoice, err := AskForBool(ctx, text, false)\n\t\tif err == nil {\n\t\t\treturn choice\n\t\t}\n\n\t\tif errors.Is(err, ErrAborted) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn false\n}\n\n// AskForKeyImport asks for permissions to import the named key.\n// It will ask the user for confirmation.\nfunc AskForKeyImport(ctx context.Context, key string, names []string) bool {\n\tif ctxutil.IsAlwaysYes(ctx) {\n\t\treturn true\n\t}\n\n\tif !ctxutil.IsInteractive(ctx) {\n\t\treturn false\n\t}\n\n\tok, err := AskForBool(ctx, fmt.Sprintf(\"Do you want to import the public key %q (Names: %+v) into your keyring?\", key, names), false)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn ok\n}\n\n// AskForPassword prompts for a password, optionally prompting twice until both match.\n// It will retry until the passwords match or the user aborts.\nfunc AskForPassword(ctx context.Context, name string, repeat bool) (string, error) {\n\tif ctxutil.IsAlwaysYes(ctx) {\n\t\treturn \"\", nil\n\t}\n\n\taskFn := GetPassPromptFunc(ctx)\n\n\tfor range maxTries {\n\t\t// check for context cancellation\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", ErrAborted\n\t\tdefault:\n\t\t}\n\n\t\tpass, err := askFn(ctx, fmt.Sprintf(\"Enter %s\", name))\n\t\tif !repeat {\n\t\t\treturn pass, err\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tpassAgain, err := askFn(ctx, fmt.Sprintf(\"Retype %s\", name))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif pass == passAgain {\n\t\t\treturn pass, nil\n\t\t}\n\n\t\tout.Errorf(ctx, \"Error: the entered password do not match\")\n\t}\n\n\treturn \"\", ErrInvalidInput\n}\n"
  },
  {
    "path": "pkg/termio/ask_test.go",
    "content": "package termio\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAskForString(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tsv, err := AskForString(ctx, \"test\", \"foobar\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", sv)\n\n\tt.Logf(\"Stderr: %s\", buf.String())\n\tbuf.Reset()\n\n\t// provide value on redirected stdin\n\tinput := `foobaz\nbar\n\n`\n\tStdin = strings.NewReader(input)\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\tsv, err = AskForString(ctx, \"test\", \"foobar\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobaz\", sv)\n\n\tsv, err = AskForString(ctx, \"test\", \"foobar\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"bar\", sv)\n\n\tStdin = os.Stdin\n\n\tsv, err = AskForString(ctx, \"test\", \"foobar\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", sv)\n\n\tStdin = os.Stdin\n\n\tt.Logf(\"Stderr: %s\", buf.String())\n\tbuf.Reset()\n}\n\nfunc TestAskForBool(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tbv, err := AskForBool(ctx, \"test\", false)\n\trequire.NoError(t, err)\n\tassert.False(t, bv)\n\n\t// provide value on redirected stdin\n\tinput := `n\ny\nN\nY\n\n\nz\n`\n\tStdin = strings.NewReader(input)\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\tbv, err = AskForBool(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.False(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", false)\n\trequire.NoError(t, err)\n\tassert.True(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.False(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", false)\n\trequire.NoError(t, err)\n\tassert.True(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.True(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", false)\n\trequire.NoError(t, err)\n\tassert.False(t, bv)\n\n\tbv, err = AskForBool(ctx, \"test\", false)\n\trequire.Error(t, err)\n\tassert.False(t, bv)\n}\n\nfunc TestAskForInt(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tgot, err := AskForInt(ctx, \"test\", 42)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 42, got)\n\n\t// provide value on redirected stdin\n\tinput := `23\n-1\n0xDEADBEEF\n0755\n0.123\n\n`\n\tStdin = strings.NewReader(input)\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\n\tiv, err := AskForInt(ctx, \"test\", 42)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 23, iv)\n}\n\nfunc TestAskForConfirmation(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\tassert.True(t, AskForConfirmation(ctx, \"test\"))\n\n\t// provide value on redirected stdin\n\tvar input strings.Builder\n\tinput.WriteString(`y\nn\n`)\n\tfor range maxTries + 1 {\n\t\tinput.WriteString(\"z\\n\")\n\t}\n\n\tStdin = strings.NewReader(input.String())\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\n\tassert.True(t, AskForConfirmation(ctx, \"test\"))\n\tassert.False(t, AskForConfirmation(ctx, \"test\"))\n\tassert.False(t, AskForConfirmation(ctx, \"test\"))\n}\n\nfunc TestAskForKeyImport(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\tassert.True(t, AskForKeyImport(ctx, \"test\", []string{}))\n\n\t// provide value on redirected stdin\n\tinput := `y\nn\nz\n`\n\n\tStdin = strings.NewReader(input)\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\tassert.False(t, AskForKeyImport(ctxutil.WithInteractive(ctx, false), \"\", nil))\n\tassert.True(t, AskForKeyImport(ctx, \"\", nil))\n\tassert.False(t, AskForKeyImport(ctx, \"\", nil))\n\tassert.False(t, AskForKeyImport(ctx, \"\", nil))\n}\n\nfunc TestAskForPasswordNonInteractive(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithInteractive(ctx, false)\n\n\t_, err := AskForPassword(ctx, \"test\", true)\n\trequire.Error(t, err)\n\n\t// provide value on redirected stdin\n\tinput := `foo\nfoo\nfoobar\nfoobaz\nfoobat\n`\n\n\tStdin = strings.NewReader(input)\n\tctx = ctxutil.WithAlwaysYes(ctx, false)\n\tctx = ctxutil.WithInteractive(ctx, true)\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tsv, err := AskForPassword(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foo\", sv)\n\n\tsv, err = AskForPassword(ctx, \"test\", false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foobar\", sv)\n\n\tsv, err = AskForPassword(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.Empty(t, sv)\n}\n\nfunc TestAskForPasswordInteractive(t *testing.T) {\n\tbuf := &bytes.Buffer{}\n\tout.Stderr = buf\n\tStderr = buf\n\n\tdefer func() {\n\t\tout.Stderr = os.Stderr\n\t\tStderr = os.Stderr\n\t}()\n\n\tctx := config.NewContextInMemory()\n\taskFn := func(ctx context.Context, prompt string) (string, error) {\n\t\treturn \"test\", nil\n\t}\n\tctx = ctxutil.WithInteractive(ctx, true)\n\tctx = WithPassPromptFunc(ctx, askFn)\n\n\tpw, err := AskForPassword(ctx, \"test\", true)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test\", pw)\n}\n"
  },
  {
    "path": "pkg/termio/context.go",
    "content": "package termio\n\nimport \"context\"\n\ntype contextKey int\n\nconst (\n\tctxKeyPassPromptFunc contextKey = iota\n\tctxKeyWorkdir\n)\n\n// PassPromptFunc is a password prompt function.\ntype PassPromptFunc func(context.Context, string) (string, error)\n\n// WithPassPromptFunc returns a context with the password prompt function set.\nfunc WithPassPromptFunc(ctx context.Context, ppf PassPromptFunc) context.Context {\n\treturn context.WithValue(ctx, ctxKeyPassPromptFunc, ppf)\n}\n\n// HasPassPromptFunc returns true if a value for the pass prompt func has been\n// set in this context.\nfunc HasPassPromptFunc(ctx context.Context) bool {\n\tppf, ok := ctx.Value(ctxKeyPassPromptFunc).(PassPromptFunc)\n\n\treturn ok && ppf != nil\n}\n\n// GetPassPromptFunc will return the password prompt func or a default one.\n// Note: will never return nil.\nfunc GetPassPromptFunc(ctx context.Context) PassPromptFunc {\n\tppf, ok := ctx.Value(ctxKeyPassPromptFunc).(PassPromptFunc)\n\tif !ok || ppf == nil {\n\t\treturn promptPass\n\t}\n\n\treturn ppf\n}\n\n// WithWorkdir returns a context with the working directory option set.\n// The working directory is used to resolve relative paths.\nfunc WithWorkdir(ctx context.Context, dir string) context.Context {\n\treturn context.WithValue(ctx, ctxKeyWorkdir, dir)\n}\n\n// GetWorkdir returns the working directory from the context or an empty\n// string if it is not set.\n// The working directory is used to resolve relative paths.\nfunc GetWorkdir(ctx context.Context) string {\n\tsv, ok := ctx.Value(ctxKeyWorkdir).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sv\n}\n"
  },
  {
    "path": "pkg/termio/context_test.go",
    "content": "package termio\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPassPromptFunc(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\n\tassert.False(t, HasPassPromptFunc(ctx))\n\tassert.NotNil(t, GetPassPromptFunc(ctx))\n\n\tctx = WithPassPromptFunc(ctx, func(context.Context, string) (string, error) {\n\t\treturn \"test\", nil\n\t})\n\tassert.True(t, HasPassPromptFunc(ctx))\n\tassert.NotNil(t, GetPassPromptFunc(ctx))\n\tsv, err := GetPassPromptFunc(ctx)(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test\", sv)\n}\n"
  },
  {
    "path": "pkg/termio/identity.go",
    "content": "package termio\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gitconfig\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar (\n\t// NameVars are the env vars checked for a valid name.\n\tNameVars = []string{\n\t\t\"GIT_AUTHOR_NAME\",\n\t\t\"DEBFULLNAME\",\n\t\t\"USER\",\n\t}\n\t// EmailVars are the env vars checked for a valid email.\n\tEmailVars = []string{\n\t\t\"GIT_AUTHOR_EMAIL\",\n\t\t\"DEBEMAIL\",\n\t\t\"EMAIL\",\n\t}\n)\n\n// DetectName tries to guess the name of the logged in user.\n// It checks the context, the command line flags, environment variables,\n// and the git config.\nfunc DetectName(ctx context.Context, c *cli.Context) string {\n\tcand := make([]string, 0, 10)\n\tcand = append(cand, ctxutil.GetUsername(ctx))\n\n\tif c != nil {\n\t\tcand = append(cand, c.String(\"name\"))\n\t}\n\n\tfor _, k := range NameVars {\n\t\tcand = append(cand, os.Getenv(k))\n\t}\n\n\tcfg := gitconfig.New().LoadAll(GetWorkdir(ctx))\n\tcand = append(cand, cfg.Get(\"user.name\"))\n\n\tfor _, e := range cand {\n\t\tif e != \"\" {\n\t\t\treturn e\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// DetectEmail tries to guess the email of the logged in user.\n// It checks the context, the command line flags, environment variables,\n// and the git config.\nfunc DetectEmail(ctx context.Context, c *cli.Context) string {\n\tcand := make([]string, 0, 10)\n\tcand = append(cand, ctxutil.GetEmail(ctx))\n\n\tif c != nil {\n\t\tcand = append(cand, c.String(\"email\"))\n\t}\n\n\tfor _, k := range EmailVars {\n\t\tcand = append(cand, os.Getenv(k))\n\t}\n\n\tcfg := gitconfig.New().LoadAll(GetWorkdir(ctx))\n\tcand = append(cand, cfg.Get(\"user.email\"))\n\n\tfor _, e := range cand {\n\t\tif e != \"\" {\n\t\t\treturn e\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/termio/identity_test.go",
    "content": "package termio\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDetectName(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\ttd := t.TempDir()\n\tt.Setenv(\"XDG_CONFIG_HOME\", td)\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tt.Setenv(\"GIT_AUTHOR_NAME\", \"\")\n\tt.Setenv(\"DEBFULLNAME\", \"\")\n\tt.Setenv(\"USER\", \"\")\n\n\tassert.Empty(t, DetectName(ctx, nil))\n\n\tt.Setenv(\"USER\", \"foo\")\n\tassert.Equal(t, \"foo\", DetectName(ctx, nil))\n}\n\nfunc TestDetectEmail(t *testing.T) {\n\tctx := config.NewContextInMemory()\n\ttd := t.TempDir()\n\tt.Setenv(\"XDG_CONFIG_HOME\", td)\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\tt.Setenv(\"GIT_AUTHOR_EMAIL\", \"\")\n\tt.Setenv(\"DEBEMAIL\", \"\")\n\tt.Setenv(\"EMAIL\", \"\")\n\n\tassert.Empty(t, DetectEmail(ctx, nil))\n\n\tt.Setenv(\"EMAIL\", \"foo@bar.de\")\n\tassert.Equal(t, \"foo@bar.de\", DetectEmail(ctx, nil))\n}\n"
  },
  {
    "path": "pkg/termio/progress.go",
    "content": "package termio\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/fatih/color\"\n\t\"golang.org/x/term\"\n)\n\nconst (\n\tfps = 25\n)\n\nvar now = time.Now\n\n// ProgressBar is a gopass progress bar.\n// It is used to display progress for long-running operations.\ntype ProgressBar struct {\n\t// keep both int64 fields at the top to ensure correct\n\t// 8-byte alignment on 32 bit systems. See https://golang.org/pkg/sync/atomic/#pkg-note-BUG\n\t// and https://github.com/golang/go/issues/36606\n\ttotal   int64\n\tcurrent int64\n\n\tmutex   chan struct{}\n\tlastUpd time.Time\n\n\tHidden bool\n\tBytes  bool\n}\n\n// NewProgressBar creates a new progress bar.\nfunc NewProgressBar(total int64) *ProgressBar {\n\treturn &ProgressBar{\n\t\ttotal:   total,\n\t\tcurrent: 0,\n\t\tmutex:   make(chan struct{}, 1),\n\t}\n}\n\n// Add adds the given amount to the progress.\nfunc (p *ProgressBar) Add(v int64) {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tcur := atomic.AddInt64(&p.current, v)\n\tif maxVal := atomic.LoadInt64(&p.total); cur > maxVal {\n\t\tatomic.StoreInt64(&p.total, cur)\n\t}\n\n\tp.print()\n}\n\n// Inc adds one to the progress.\nfunc (p *ProgressBar) Inc() {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tcur := atomic.AddInt64(&p.current, 1)\n\tif maxVal := atomic.LoadInt64(&p.total); cur > maxVal {\n\t\tatomic.StoreInt64(&p.total, cur)\n\t}\n\n\tp.print()\n}\n\n// Set sets an arbitrary progress.\nfunc (p *ProgressBar) Set(v int64) {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tatomic.StoreInt64(&p.current, v)\n\n\tif maxVal := atomic.LoadInt64(&p.total); v > maxVal {\n\t\tatomic.StoreInt64(&p.total, v)\n\t}\n\n\tp.print()\n}\n\n// Done finalizes the progress bar.\nfunc (p *ProgressBar) Done() {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tif p.Hidden {\n\t\treturn\n\t}\n\n\tfmt.Fprintln(Stderr, \"\")\n}\n\n// Clear removes the progress bar.\nfunc (p *ProgressBar) Clear() {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tclearLine()\n}\n\n// print will print the progress bar, if necessary.\nfunc (p *ProgressBar) print() {\n\tif p == nil {\n\t\treturn\n\t}\n\n\tif p.Hidden {\n\t\treturn\n\t}\n\n\t// try to lock\n\tselect {\n\tcase p.mutex <- struct{}{}:\n\t\t// lock acquired\n\t\tp.tryPrint()\n\t\t<-p.mutex\n\tdefault:\n\t\t// lock not acquired\n\t\treturn\n\t}\n}\n\nfunc (p *ProgressBar) tryPrint() {\n\tts := now()\n\tif p.current == 0 || p.current >= p.total-1 || ts.Sub(p.lastUpd) > time.Second/fps {\n\t\tp.lastUpd = ts\n\t\tp.doPrint()\n\t}\n}\n\n// doPrint redraws the current line.\n// This method is based on https://github.com/muesli/goprogressbar/blob/master/progressbar.go#L96\nfunc (p *ProgressBar) doPrint() {\n\tclearLine()\n\n\tcur, maxVal, pct := p.percent()\n\tpctStr := fmt.Sprintf(\"%.2f%%\", pct*100)\n\t// ensure consistent length\n\tfor len(pctStr) < 7 {\n\t\tpctStr = \" \" + pctStr\n\t}\n\n\ttermWidth, _, _ := term.GetSize(int(syscall.Stdin)) //nolint:unconvert\n\tif termWidth < 0 {\n\t\t// if we can determine the size (e.g. windows, fake term, mock)\n\t\t// assume a sane default of 80\n\t\ttermWidth = 80\n\t}\n\n\tbarWidth := uint(termWidth)\n\tdigits := int(math.Log10(float64(maxVal))) + 1\n\t// Log10(0) is undefined\n\tif maxVal < 1 {\n\t\tdigits = 1\n\t}\n\n\ttext := fmt.Sprintf(fmt.Sprintf(\" %%%dd / %%%dd \", digits, digits), cur, maxVal)\n\n\tif p.Bytes {\n\t\tcurStr := humanize.Bytes(uint64(cur))\n\t\tmaxStr := humanize.Bytes(uint64(maxVal))\n\t\tdigits := len(maxStr) + 1\n\t\ttext = fmt.Sprintf(fmt.Sprintf(\" %%%ds / %%%ds \", digits, digits), curStr, maxStr)\n\t}\n\n\tsize := int(barWidth) - len(text) - len(pctStr) - 5\n\tfill := int(math.Max(2, math.Floor((float64(size)*pct)+.5)))\n\n\tfmt.Fprint(Stderr, text)\n\n\t// not enough space\n\tif size < 11 {\n\t\treturn\n\t}\n\n\t// Rgggggggggggmcyy\n\t// Gooooooooooopass\n\ttg := color.RedString(\"G\")\n\tto := strings.Repeat(color.GreenString(\"o\"), gteZero(fill-5))\n\ttp := strings.Repeat(color.YellowString(\"p\"), boundedMin(1, fill-4))\n\tta := strings.Repeat(color.MagentaString(\"a\"), boundedMin(1, fill-3))\n\tts := strings.Repeat(color.CyanString(\"s\"), boundedMin(2, fill-1))\n\tspc := strings.Repeat(\" \", gteZero(size-fill))\n\tfmt.Fprintf(Stderr, \"[%s%s%s%s%s%s] %s \",\n\t\ttg,\n\t\tto,\n\t\ttp,\n\t\tta,\n\t\tts,\n\t\tspc,\n\t\tpctStr,\n\t)\n}\n\nfunc gteZero(a int) int {\n\tif a >= 0 {\n\t\treturn a\n\t}\n\n\treturn 0\n}\n\nfunc boundedMin(a, b int) int {\n\treturn gteZero(min(a, b))\n}\n\nfunc (p *ProgressBar) percent() (int64, int64, float64) {\n\tcur := atomic.LoadInt64(&p.current)\n\tmaxVal := atomic.LoadInt64(&p.total)\n\tpct := float64(cur) / float64(maxVal)\n\n\tif p.total < 1 {\n\t\tif p.current < 1 {\n\t\t\tpct = 1\n\t\t} else {\n\t\t\tpct = 0\n\t\t}\n\t}\n\n\t// normalized between 0.0 and 1.0\n\treturn cur, maxVal, math.Min(1, math.Max(0, pct))\n}\n\nfunc clearLine() {\n\tfmt.Fprintf(Stderr, \"\\033[2K\\r]\")\n}\n"
  },
  {
    "path": "pkg/termio/progress_test.go",
    "content": "package termio\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc ExampleProgressBar() { //nolint:testableexamples\n\tmaxVal := 100\n\tpb := NewProgressBar(int64(maxVal))\n\n\tfor range maxVal + 20 {\n\t\tpb.Inc()\n\t\tpb.Add(23)\n\t\tpb.Set(42)\n\t\ttime.Sleep(150 * time.Millisecond)\n\t}\n\n\ttime.Sleep(5 * time.Second)\n\tpb.Done()\n}\n\nfunc TestProgress(t *testing.T) {\n\tmaxVal := 2\n\tpb := NewProgressBar(int64(maxVal))\n\tpb.Hidden = true\n\tpb.Inc()\n\tassert.Equal(t, int64(1), pb.current)\n}\n\nfunc TestProgressNil(t *testing.T) {\n\tt.Parallel()\n\n\tvar pb *ProgressBar\n\tpb.Inc()\n\tpb.Add(4)\n\tpb.Done()\n}\n\nfunc TestProgressBytes(t *testing.T) {\n\tmaxSize := 2 << 24\n\tpb := NewProgressBar(int64(maxSize))\n\tpb.Hidden = true\n\tpb.Bytes = true\n\n\tfor i := range 24 {\n\t\tpb.Set(2 << (i + 1))\n\t}\n\n\tassert.Equal(t, int64(maxSize), pb.current)\n\tpb.Done()\n}\n"
  },
  {
    "path": "pkg/termio/promptpass_others.go",
    "content": "//go:build !windows\n\npackage termio\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/gopasspw/gopass/internal/out\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"golang.org/x/term\"\n)\n\n// promptPass will prompt user's for a password by terminal.\nfunc promptPass(ctx context.Context, prompt string) (string, error) {\n\tif !ctxutil.IsTerminal(ctx) {\n\t\treturn AskForString(ctx, prompt, \"\")\n\t}\n\n\t// Make a copy of STDIN's state to restore afterward\n\tfd := int(os.Stdin.Fd())\n\n\toldState, err := term.GetState(fd)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not get state of terminal: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif err := term.Restore(fd, oldState); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to restore terminal: %s\", err)\n\t\t}\n\t}()\n\n\t// Restore STDIN in the event of a signal interruption\n\tsigch := make(chan os.Signal, 1)\n\tsignal.Notify(sigch, os.Interrupt)\n\n\tgo func() {\n\t\t<-sigch\n\n\t\tif err := term.Restore(fd, oldState); err != nil {\n\t\t\tout.Errorf(ctx, \"Failed to restore terminal: %s\", err)\n\t\t}\n\n\t\tos.Exit(1)\n\t}()\n\n\tfmt.Fprintf(Stderr, \"%s: \", prompt)\n\n\tpassBytes, err := term.ReadPassword(fd)\n\n\tfmt.Fprintln(Stderr, \"\")\n\n\treturn string(passBytes), err\n}\n"
  },
  {
    "path": "pkg/termio/promptpass_test.go",
    "content": "package termio\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPromptPass(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tctx = ctxutil.WithTerminal(ctx, false)\n\tctx = ctxutil.WithAlwaysYes(ctx, true)\n\n\t_, err := promptPass(ctx, \"foo\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "pkg/termio/promptpass_windows.go",
    "content": "//go:build windows\n\npackage termio\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gopasspw/gopass/pkg/ctxutil\"\n\t\"golang.org/x/crypto/ssh/terminal\"\n)\n\n// promptPass will prompt user's for a password by terminal.\nfunc promptPass(ctx context.Context, prompt string) (string, error) {\n\tif !ctxutil.IsTerminal(ctx) {\n\t\treturn AskForString(ctx, prompt, \"\")\n\t}\n\n\tfmt.Fprintf(Stderr, \"%s: \", prompt)\n\tpassBytes, err := terminal.ReadPassword(int(os.Stdin.Fd()))\n\tfmt.Fprintln(Stderr, \"\")\n\treturn string(passBytes), err\n}\n"
  },
  {
    "path": "pkg/termio/reader.go",
    "content": "// Package termio provides helpers and functions to work with\n// terminal input and output.\npackage termio\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n)\n\n// LineReader is an unbuffered line reader.\ntype LineReader struct {\n\tr   io.Reader\n\tctx context.Context //nolint:containedctx\n}\n\n// NewReader creates a new line reader.\nfunc NewReader(ctx context.Context, r io.Reader) *LineReader {\n\treturn &LineReader{r: r, ctx: ctx}\n}\n\n// Read implements io.Reader.\nfunc (lr LineReader) Read(p []byte) (int, error) {\n\treturn lr.r.Read(p) //nolint:wrapcheck\n}\n\n// rr is a composite value to transport the result of Read through a channel.\ntype rr struct {\n\tn   int\n\terr error\n}\n\n// ReadLine reads one line w/o buffering.\nfunc (lr LineReader) ReadLine() (string, error) {\n\tout := &bytes.Buffer{}\n\tbuf := make([]byte, 1) // important: we must only read one byte at a time!\n\n\tfor {\n\t\t// we wait for the user input in the background so we can use the\n\t\t// select statement below to be able to immediately quit when the\n\t\t// user presses Ctrl+C\n\t\tmsg := make(chan rr, 1)\n\n\t\tgo func() {\n\t\t\tn, err := lr.r.Read(buf)\n\t\t\tmsg <- rr{n, err}\n\t\t}()\n\n\t\tvar n int\n\n\t\tvar err error\n\n\t\t// wait for a user input (or a signal to abort)\n\t\tselect {\n\t\tcase <-lr.ctx.Done():\n\t\t\treturn \"\", ErrAborted\n\t\tcase s := <-msg:\n\t\t\tn = s.n\n\t\t\terr = s.err\n\t\t}\n\n\t\t// process the user input\n\t\tfor i := range n {\n\t\t\tif buf[i] == '\\n' {\n\t\t\t\treturn out.String(), nil\n\t\t\t}\n\t\t\t// err is always nil\n\t\t\t_ = out.WriteByte(buf[i])\n\t\t}\n\t\t// Callers should always process the n > 0 bytes returned before considering the error err.\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn out.String(), nil\n\t\t\t}\n\n\t\t\treturn out.String(), err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/termio/reader_test.go",
    "content": "package termio\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/iotest\"\n\n\t\"github.com/gopasspw/gopass/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReadLines(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tc := range [][]string{\n\t\t{\"foo\", \"bar\"},\n\t\t{\"foo\", \"bar\", \"\", \"baz\"},\n\t\t{\"foo\", \"µ\"},\n\t\t{\"µ\", \"ĸ\", \"aŧ\", \"¶a\"},\n\t} {\n\t\tstdin := strings.NewReader(strings.Join(tc, \"\\n\"))\n\t\tfor _, s := range tc {\n\t\t\tassert.Equal(t, s, mustReadLine(stdin))\n\t\t}\n\t}\n}\n\nfunc TestReadLineError(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tstdin := strings.NewReader(\"fo\")\n\tlr := NewReader(ctx, iotest.TimeoutReader(stdin))\n\n\tline, err := lr.ReadLine()\n\trequire.Error(t, err)\n\tassert.Equal(t, \"f\", line)\n}\n\nfunc TestRead(t *testing.T) {\n\tt.Parallel()\n\n\tctx := config.NewContextInMemory()\n\tstdin := strings.NewReader(`foobarbazzabzabzab`)\n\tlr := NewReader(ctx, stdin)\n\n\tb := make([]byte, 10)\n\tn, err := lr.Read(b)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 10, n)\n\tassert.Equal(t, \"foobarbazz\", string(b))\n}\n\nfunc mustReadLine(r io.Reader) string {\n\tctx := config.NewContextInMemory()\n\n\tline, err := NewReader(ctx, r).ReadLine()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn line\n}\n"
  },
  {
    "path": "tests/age_agent_test.go",
    "content": "package tests\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAgeAgent(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping test on windows for now\")\n\t}\n\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\t// create a new age identity\n\tout, err := ts.runCmd([]string{ts.Binary, \"age\", \"identities\", \"keygen\", \"--password\", \"foo\"}, []byte(\"test\\ntest\\n\"))\n\trequire.NoError(t, err, out)\n}\n"
  },
  {
    "path": "tests/audit_test.go",
    "content": "package tests\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAudit(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\tts.initSecrets(\"\")\n\n\tt.Run(\"audit the test store\", func(t *testing.T) {\n\t\tout, err := ts.run(\"audit\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"crunchy\")\n\t})\n}\n"
  },
  {
    "path": "tests/binary_test.go",
    "content": "package tests\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBinaryCopy(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"empty store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fscopy\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"no args\", func(t *testing.T) {\n\t\tout, err := ts.run(\"fscopy\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: usage: gopass fscopy from to\\n\", out)\n\t})\n\n\tfn := filepath.Join(ts.tempDir, \"copy\")\n\tdat := []byte(\"foobar\")\n\trequire.NoError(t, os.WriteFile(fn, dat, 0o644))\n\n\tt.Run(\"copy file to store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fscopy \" + fn + \" foo/bar\")\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, os.Remove(fn))\n\t})\n\n\tt.Run(\"copy store to file\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fscopy foo/bar \" + fn)\n\t\trequire.NoError(t, err)\n\n\t\tbuf, err := os.ReadFile(fn)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, buf, dat)\n\t})\n\n\tt.Run(\"cat from store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"cat foo/bar\")\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestBinaryMove(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"empty store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fsmove\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"no args\", func(t *testing.T) {\n\t\tout, err := ts.run(\"fsmove\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: usage: gopass fsmove from to\\n\", out)\n\t})\n\n\tfn := filepath.Join(ts.tempDir, \"move\")\n\tdat := []byte(\"foobar\")\n\trequire.NoError(t, os.WriteFile(fn, dat, 0o644))\n\n\tt.Run(\"move fs to store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fsmove \" + fn + \" foo/bar\")\n\t\trequire.NoError(t, err)\n\t\trequire.Error(t, os.Remove(fn))\n\t})\n\n\tt.Run(\"move store to fs\", func(t *testing.T) {\n\t\t_, err := ts.run(\"fsmove foo/bar \" + fn)\n\t\trequire.NoError(t, err)\n\n\t\tbuf, err := os.ReadFile(fn)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, buf, dat)\n\t})\n\n\tt.Run(\"cat secret\", func(t *testing.T) {\n\t\t_, err := ts.run(\"cat foo/bar\")\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestBinaryShasum(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"shasum on empty store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"sha256\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"shasum w/o args\", func(t *testing.T) {\n\t\tout, err := ts.run(\"sha256\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: gopass sha256 name\\n\", out)\n\t})\n\n\tt.Run(\"populate store\", func(t *testing.T) {\n\t\tfn := filepath.Join(ts.tempDir, \"shasum\")\n\t\tdat := []byte(\"foobar\")\n\t\trequire.NoError(t, os.WriteFile(fn, dat, 0o644))\n\n\t\t_, err := ts.run(\"fsmove \" + fn + \" foo/bar\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"shasum on binary secret\", func(t *testing.T) {\n\t\tout, err := ts.run(\"sha256 foo/bar\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2\", out)\n\t})\n}\n"
  },
  {
    "path": "tests/can/can.go",
    "content": "// Package can provides access to the embedded key material used for testing.\n// The key material is embedded in the binary and is used for testing\n// purposes only.\npackage can\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n)\n\n//go:embed gnupg/*\nvar can embed.FS\n\nfunc EmbeddedKeyRing() openpgp.EntityList {\n\tfh, err := can.Open(\"gnupg/pubring.gpg\")\n\tif err != nil {\n\t\t// This must not happen. Since the key material is embedded into the\n\t\t// binary, it must be available in in the correct format. If it is not\n\t\t// we ca only panic. Since this is used for tests only this shouldn't\n\t\t// affect users.\n\t\tpanic(err)\n\t}\n\tdefer fh.Close() //nolint:errcheck\n\n\tel, err := openpgp.ReadKeyRing(fh)\n\tif err != nil {\n\t\t// See reasoning above.\n\t\tpanic(err)\n\t}\n\n\treturn el\n}\n\nfunc KeyID() string {\n\tel := EmbeddedKeyRing()\n\tif len(el) != 1 {\n\t\tpanic(\"pubring.gpg must contain exactly one key\")\n\t}\n\n\treturn el[0].PrimaryKey.KeyIdShortString()\n}\n\n// WriteTo writes the embedded content to the given output\n// directory.\nfunc WriteTo(path string) error {\n\tfes, err := can.ReadDir(\"gnupg\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read can dir: %w\", err)\n\t}\n\tfor _, fe := range fes {\n\t\tfrom := \"gnupg/\" + fe.Name()\n\t\tto := filepath.Join(path, fe.Name())\n\t\tbuf, err := can.ReadFile(from)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read %s: %w\", from, err)\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(to), 0o700); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create dir %s: %w\", filepath.Dir(to), err)\n\t\t}\n\n\t\tif err := os.WriteFile(to, buf, 0o600); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write %s: %w\", to, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "tests/can/can_test.go",
    "content": "package can\n\nimport (\n\t\"testing\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPubring(t *testing.T) {\n\tt.Parallel()\n\n\tfh, err := can.Open(\"gnupg/pubring.gpg\")\n\trequire.NoError(t, err)\n\tdefer fh.Close() //nolint:errcheck\n\n\tel, err := openpgp.ReadKeyRing(fh)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, el, 1)\n\tassert.Equal(t, \"BE73F104\", el[0].PrimaryKey.KeyIdShortString())\n}\n"
  },
  {
    "path": "tests/completion_test.go",
    "content": "package tests\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCompletion(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping test on windows.\")\n\t}\n\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"completion help\", func(t *testing.T) {\n\t\tout, err := ts.run(\"completion\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, \"Source for auto completion in bash\")\n\t\tassert.Contains(t, out, \"Source for auto completion in zsh\")\n\t})\n\n\tt.Run(\"bash completion\", func(t *testing.T) {\n\t\tbash := `_gopass_bash_autocomplete() {\n     local cur opts base\n     COMPREPLY=()\n     cur=\"${COMP_WORDS[COMP_CWORD]}\"\n     # Use error handling to prevent crashes from invalid flags\n     opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion 2>/dev/null ) || opts=\"\"\n     local IFS=$'\\n'\n     COMPREPLY=( $(compgen -W \"${opts}\" -- ${cur}) )\n     return 0\n }\n\ncomplete -F _gopass_bash_autocomplete gopass`\n\n\t\tout, err := ts.run(\"completion bash\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, bash, out)\n\t})\n\n\tt.Run(\"zsh completion\", func(t *testing.T) {\n\t\tout, err := ts.run(\"completion zsh\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, \"compdef gopass\")\n\t})\n\n\tt.Run(\"fish completion\", func(t *testing.T) {\n\t\tout, err := ts.run(\"completion fish\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, \"complete\")\n\t})\n}\n\nfunc TestCompletionNoPath(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Setenv(\"PATH\", \"/tmp/foobar\")\n\n\tt.Run(\"generate bash\", func(t *testing.T) {\n\t\t_, err := ts.run(\"--generate-bash-completion\")\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "tests/config_test.go",
    "content": "package tests\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBaseConfig(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tout, err := ts.run(\"config\")\n\trequire.NoError(t, err)\n\n\twanted := `age.agent-enabled = false\nage.agent-timeout = 0\ncore.autopush = true\ncore.autosync = true\ncore.cliptimeout = 45\ncore.exportkeys = false\ncore.follow-references = false\ncore.notifications = true\n`\n\twanted += \"mounts.path = \" + ts.storeDir(\"root\") + \"\\n\" +\n\t\t\"pwgen.xkcd-lang = en\"\n\n\tassert.Equal(t, wanted, out)\n\n\tinvertables := []string{\n\t\t\"core.autoimport\",\n\t\t\"show.safecontent\",\n\t}\n\n\tfor _, invert := range invertables {\n\t\tt.Run(\"invert \"+invert, func(t *testing.T) {\n\t\t\targ := \"config \" + invert + \" false\"\n\t\t\tout, err = ts.run(arg)\n\t\t\trequire.NoError(t, err, \"Running gopass \"+arg)\n\t\t\tassert.Equal(t, \"false\", out, \"Output of gopass \"+arg)\n\n\t\t\targ = \"config \" + invert\n\t\t\tout, err = ts.run(arg)\n\t\t\trequire.NoError(t, err, \"Running gopass \"+arg)\n\t\t\tassert.Equal(t, \"false\", out, \"Output of gopass \"+arg)\n\t\t})\n\t}\n\n\tt.Run(\"cliptimeout\", func(t *testing.T) {\n\t\tout, err = ts.run(\"config core.cliptimeout 120\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"120\", out)\n\n\t\tout, err = ts.run(\"config core.cliptimeout\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"120\", out)\n\t})\n}\n\nfunc TestMountConfig(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\t// we add a mount:\n\t_, err := ts.run(\"init --store mnt/m1 --path \" + ts.storeDir(\"m1\") + \" --storage=fs \" + keyID)\n\trequire.NoError(t, err)\n\n\t_, err = ts.run(\"config\")\n\trequire.NoError(t, err)\n\n\twanted := `age.agent-enabled = false\nage.agent-timeout = 0\ncore.autopush = true\ncore.autosync = true\ncore.cliptimeout = 45\ncore.exportkeys = false\ncore.follow-references = false\ncore.notifications = true\n`\n\twanted += \"mounts.mnt/m1.path = \" + ts.storeDir(\"m1\") + \"\\n\"\n\twanted += \"mounts.path = \" + ts.storeDir(\"root\") + \"\\n\"\n\twanted += \"pwgen.xkcd-lang = en\\n\"\n\twanted += \"recipients.mnt/m1.hash = 9a4c4b1e0eb9ade2e692ff948f43d9668145eca3df88ffff67e0e21426252907\\n\"\n\n\tout, err := ts.run(\"config\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(wanted), out)\n}\n"
  },
  {
    "path": "tests/copy_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCopy(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"copy w/ empty store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"copy\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"copy usage\", func(t *testing.T) {\n\t\tout, err := ts.run(\"copy\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" cp <FROM> <TO>\\n\", out)\n\t})\n\n\tt.Run(\"copy w/o destination\", func(t *testing.T) {\n\t\tout, err := ts.run(\"copy foo\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" cp <FROM> <TO>\\n\", out)\n\t})\n\n\tt.Run(\"copy non existing source\", func(t *testing.T) {\n\t\tout, err := ts.run(\"copy foo bar\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: foo does not exist\\n\", out)\n\t})\n\n\tts.initSecrets(\"\")\n\n\tt.Run(\"recursive copy\", func(t *testing.T) {\n\t\t_, err := ts.run(\"copy foo/ bar\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"copy existing secret to non-existing destination\", func(t *testing.T) {\n\t\tout, err := ts.run(\"copy foo/bar foo/baz\")\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, out)\n\n\t\torig, err := ts.run(\"show -f foo/bar\")\n\t\trequire.NoError(t, err)\n\n\t\tcp, err := ts.run(\"show -f foo/baz\")\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, orig, cp)\n\t})\n}\n"
  },
  {
    "path": "tests/delete_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDelete(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"delete\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" rm name\\n\", out)\n\n\tout, err = ts.run(\"delete foobarbaz\")\n\trequire.Error(t, err)\n\tassert.Contains(t, out, \"does not exist\", out)\n\n\tts.initSecrets(\"\")\n\n\tsecrets := []string{\"baz\", \"foo/bar\"}\n\tfor _, secret := range secrets {\n\t\tout, err = ts.run(\"delete -f \" + secret)\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, out)\n\n\t\tout, err = ts.run(\"delete -f \" + secret)\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"does not exist\\n\", out)\n\t}\n}\n"
  },
  {
    "path": "tests/find_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFind(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"find\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" find <pattern>\\n\", out)\n\n\t_, err = ts.run(\"config show.safecontent false\")\n\trequire.NoError(t, err)\n\n\tout, err = ts.run(\"find bar\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: no results found\\n\", out)\n\n\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"foo/bar\"}, []byte(\"baz\"))\n\trequire.NoError(t, err)\n\n\tout, err = ts.run(\"find bar\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, \"Found exact match in 'foo/bar'\\nbaz\", out)\n\n\tout, err = ts.run(\"find Bar\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, \"Found exact match in 'foo/bar'\\nbaz\", out)\n\n\tout, err = ts.run(\"find b\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, \"Found exact match in 'foo/bar'\\nbaz\", out)\n\n\t_, err = ts.run(\"config show.safecontent true\")\n\trequire.NoError(t, err)\n\n\tout, err = ts.run(\"find bar\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"foo/bar\")\n\n\tout, err = ts.run(\"find -f bar\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"foo/bar\")\n}\n"
  },
  {
    "path": "tests/generate_test.go",
    "content": "package tests\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGenerate(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"generate\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: please provide a password name\\n\", out)\n\n\tout, err = ts.run(\"generate foo 0\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: password length must not be zero\\n\", out)\n\n\tout, err = ts.run(\"generate -p baz 42\")\n\trequire.NoError(t, err)\n\n\tlines := strings.Split(out, \"\\n\")\n\n\trequire.Greater(t, len(lines), 2)\n\tassert.Contains(t, out, \"The generated password is:\")\n\tassert.Len(t, lines[3], 42)\n\n\tt.Setenv(\"GOPASS_CHARACTER_SET\", \"a\")\n\n\tout, err = ts.run(\"generate -p zab 4\")\n\trequire.NoError(t, err)\n\n\tlines = strings.Split(out, \"\\n\")\n\n\trequire.Greater(t, len(lines), 2)\n\tassert.Contains(t, out, \"The generated password is:\")\n\tassert.Equal(t, \"aaaa\", lines[3])\n}\n"
  },
  {
    "path": "tests/gptest/gunit.go",
    "content": "package gptest\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/armor\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/packet\"\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/gopasspw/gopass/tests/can\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar gpgDefaultRecipients = []string{\"BE73F104\"}\n\n// GUnit is a gopass unit test helper.\ntype GUnit struct {\n\tt          *testing.T\n\tEntries    []string\n\tRecipients []string\n\tDir        string\n\tenv        map[string]string\n}\n\n// GPConfig returns the gopass config location.\nfunc (u GUnit) GPConfig() string {\n\treturn filepath.Join(u.Dir, \".config\", \"gopass\", \"config\")\n}\n\n// GPGHome returns the GnuPG homedir.\nfunc (u GUnit) GPGHome() string {\n\treturn filepath.Join(u.Dir, \".gnupg\")\n}\n\n// NewGUnitTester creates a new unit test helper.\nfunc NewGUnitTester(t *testing.T) *GUnit {\n\tt.Helper()\n\n\tclipboard.ForceUnsupported = true\n\n\ttd := t.TempDir()\n\tu := &GUnit{\n\t\tt:          t,\n\t\tEntries:    defaultEntries,\n\t\tRecipients: gpgDefaultRecipients,\n\t\tDir:        td,\n\t}\n\n\tu.env = map[string]string{\n\t\t\"CHECKPOINT_DISABLE\":       \"true\",\n\t\t\"GNUPGHOME\":                u.GPGHome(),\n\t\t\"GOPASS_CONFIG_NOSYSTEM\":   \"true\",\n\t\t\"GOPASS_CONFIG_NO_MIGRATE\": \"true\",\n\t\t\"GOPASS_HOMEDIR\":           u.Dir,\n\t\t\"NO_COLOR\":                 \"true\",\n\t\t\"GOPASS_NO_NOTIFY\":         \"true\",\n\t\t\"PAGER\":                    \"\",\n\t\t\"GIT_AUTHOR_NAME\":          \"gopass-tests\",\n\t\t\"GIT_AUTHOR_EMAIL\":         \"tests@gopass.pw\",\n\t}\n\tsetupEnv(t, u.env)\n\n\trequire.NoError(t, os.Mkdir(u.GPGHome(), 0o700))\n\trequire.NoError(t, u.initConfig())\n\trequire.NoError(t, u.InitStore(\"\"))\n\n\treturn u\n}\n\nfunc (u GUnit) initConfig() error {\n\tif err := os.MkdirAll(filepath.Dir(u.GPConfig()), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize the directory at %q: %w\", filepath.Dir(u.GPConfig()), err)\n\t}\n\terr := os.WriteFile(\n\t\tu.GPConfig(),\n\t\t[]byte(gopassConfig+\"\\texportkeys = false\\n[mounts]\\npath = \"+u.StoreDir(\"\")+\"\\n\"),\n\t\t0o600,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// StoreDir returns the password store dir.\nfunc (u GUnit) StoreDir(mount string) string {\n\tif mount != \"\" {\n\t\tmount = \"-\" + mount\n\t}\n\n\treturn filepath.Join(u.Dir, \"password-store\"+mount)\n}\n\nfunc (u GUnit) recipients() []byte {\n\treturn []byte(strings.Join(u.Recipients, \"\\n\"))\n}\n\nfunc (u GUnit) writeRecipients(name string) error {\n\tdir := u.StoreDir(name)\n\tif err := os.MkdirAll(dir, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create store dir %s: %w\", dir, err)\n\t}\n\n\tfn := filepath.Join(dir, \".gpg-id\") // gpgcli.IDFile\n\t_ = os.Remove(fn)\n\n\tif err := os.WriteFile(fn, u.recipients(), 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write IDFile %s: %w\", fn, err)\n\t}\n\n\treturn nil\n}\n\n// InitStore initializes the test store.\nfunc (u GUnit) InitStore(name string) error {\n\tif err := u.writeRecipients(name); err != nil {\n\t\treturn fmt.Errorf(\"failed to write recipients: %w\", err)\n\t}\n\n\tif err := can.WriteTo(u.GPGHome()); err != nil {\n\t\treturn fmt.Errorf(\"failed to write to GPG home %s: %w\", u.GPGHome(), err)\n\t}\n\n\tdir := u.StoreDir(name)\n\n\t// write embedded public keys to the store so we can import them\n\tel := can.EmbeddedKeyRing()\n\tfor _, e := range el {\n\t\ttfn := filepath.Join(dir, \".public-keys\", e.PrimaryKey.KeyIdShortString())\n\t\tif err := os.MkdirAll(filepath.Dir(tfn), 0o700); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create public-keys dir %s: %w\", filepath.Dir(tfn), err)\n\t\t}\n\t\tfh, err := os.Create(tfn)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create public-keys file %s: %w\", tfn, err)\n\t\t}\n\t\tdefer fh.Close() //nolint:errcheck\n\n\t\twc, err := armor.Encode(fh, openpgp.PublicKeyType, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := e.Serialize(wc); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := wc.Close(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (u *GUnit) AddExpiredRecipient() string {\n\tu.t.Helper()\n\n\te, err := openpgp.NewEntity(\"Expired\", \"\", \"expired@example.com\", &packet.Config{\n\t\tRSABits: 4096,\n\t})\n\trequire.NoError(u.t, err)\n\n\tfor _, id := range e.Identities {\n\t\terr := id.SelfSignature.SignUserId(id.UserId.Id, e.PrimaryKey, e.PrivateKey, &packet.Config{\n\t\t\tSigLifetimeSecs: 1, // we can not use negative or zero here\n\t\t})\n\t\trequire.NoError(u.t, err)\n\t}\n\n\tel := can.EmbeddedKeyRing()\n\tel = append(el, e)\n\n\tfn := filepath.Join(u.GPGHome(), \"pubring.gpg\")\n\tfh, err := os.Create(fn)\n\trequire.NoError(u.t, err)\n\n\tfor _, e := range el {\n\t\trequire.NoError(u.t, e.Serialize(fh))\n\t\t// u.t.Logf(\"wrote %X to %s\", e.PrimaryKey.Fingerprint, fn)\n\t}\n\trequire.NoError(u.t, fh.Close())\n\n\t// wait for the key to expire\n\ttime.Sleep(time.Second)\n\n\tid := fmt.Sprintf(\"%X\", e.PrimaryKey.Fingerprint)\n\tu.Recipients = append(u.Recipients, id)\n\n\trequire.NoError(u.t, u.writeRecipients(\"\"))\n\n\treturn id\n}\n"
  },
  {
    "path": "tests/gptest/unit.go",
    "content": "package gptest\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/clipboard\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tgopassConfig = `[generate]\n\tautoclip = true\n[core]\n\tautoimport = true\n\tcliptimeout = 45\n\tnotifications = true\n\tnopager = true\n` // it's important the [core] subsection is the last one here\n)\n\nvar (\n\tdefaultEntries    = []string{\"foo\"}\n\tdefaultRecipients = []string{\"0xDEADBEEF\"}\n)\n\n// Unit is a gopass unit test helper.\ntype Unit struct {\n\tt          *testing.T\n\tEntries    []string\n\tRecipients []string\n\tDir        string\n\tenv        map[string]string\n}\n\n// GPConfig returns the gopass config location.\nfunc (u Unit) GPConfig() string {\n\treturn filepath.Join(u.Dir, \".config\", \"gopass\", \"config\")\n}\n\n// GPGHome returns the GnuPG homedir.\nfunc (u Unit) GPGHome() string {\n\treturn filepath.Join(u.Dir, \".gnupg\")\n}\n\n// NewUnitTester creates a new unit test helper.\nfunc NewUnitTester(t *testing.T) *Unit {\n\tt.Helper()\n\n\tclipboard.ForceUnsupported = true\n\n\ttd := t.TempDir()\n\tu := &Unit{\n\t\tt:          t,\n\t\tEntries:    defaultEntries,\n\t\tRecipients: defaultRecipients,\n\t\tDir:        td,\n\t}\n\n\tu.env = map[string]string{\n\t\t\"CHECKPOINT_DISABLE\":        \"true\",\n\t\t\"GNUPGHOME\":                 u.GPGHome(),\n\t\t\"GOPASS_CONFIG_NOSYSTEM\":    \"true\",\n\t\t\"GOPASS_CONFIG_NO_MIGRATE\":  \"true\",\n\t\t\"GOPASS_DISABLE_ENCRYPTION\": \"true\",\n\t\t\"GOPASS_HOMEDIR\":            u.Dir,\n\t\t\"NO_COLOR\":                  \"true\",\n\t\t\"GOPASS_NO_NOTIFY\":          \"true\",\n\t\t\"PAGER\":                     \"\",\n\t\t\"GIT_AUTHOR_NAME\":           \"gopass-tests\",\n\t\t\"GIT_AUTHOR_EMAIL\":          \"tests@gopass.pw\",\n\t}\n\tsetupEnv(t, u.env)\n\n\trequire.NoError(t, os.Mkdir(u.GPGHome(), 0o700))\n\t// we need to init store before init config, so that the right folders exist\n\trequire.NoError(t, u.InitStore(\"\"), \"init store\")\n\trequire.NoError(t, u.initConfig(), \"pre-populate config\")\n\n\treturn u\n}\n\nfunc (u Unit) initConfig() error {\n\tif err := os.MkdirAll(filepath.Dir(u.GPConfig()), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize the test config at %q: %w\", u.GPConfig(), err)\n\t}\n\n\terr := os.WriteFile(\n\t\tu.GPConfig(),\n\t\t[]byte(gopassConfig+\"\\texportkeys = true\\n[mounts]\\n\\tpath = \"+u.StoreDir(\"\")+\"\\n\"),\n\t\t0o600,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write config: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// StoreDir returns the password store dir.\nfunc (u Unit) StoreDir(mount string) string {\n\tif mount != \"\" {\n\t\tmount = \"-\" + mount\n\t}\n\n\treturn filepath.Join(u.Dir, \"password-store\"+mount)\n}\n\nfunc (u Unit) recipients() []byte {\n\treturn []byte(strings.Join(u.Recipients, \"\\n\"))\n}\n\n// InitStore initializes the test store.\nfunc (u Unit) InitStore(name string) error {\n\tdir := u.StoreDir(name)\n\tif err := os.MkdirAll(dir, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create store dir %s: %w\", dir, err)\n\t}\n\n\tfn := filepath.Join(dir, \".plain-id\") // plain.IDFile\n\t_ = os.Remove(fn)\n\n\tif err := os.WriteFile(fn, u.recipients(), 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write IDFile %s: %w\", fn, err)\n\t}\n\n\tfor _, p := range AllPathsToSlash(u.Entries) {\n\t\tfn := filepath.Join(dir, p+\".txt\") // plain.Ext\n\t\t_ = os.Remove(fn)\n\n\t\tif err := os.MkdirAll(filepath.Dir(fn), 0o700); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create dir %s: %w\", filepath.Dir(fn), err)\n\t\t}\n\n\t\tif err := os.WriteFile(fn, []byte(\"secret\\nsecond\\nthird\"), 0o600); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write file %s: %w\", fn, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "tests/gptest/utils.go",
    "content": "// Package gptest contains test helpers for gopass, including\n// creating temporary directories, setting up environment variables,\n// and creating CLI contexts for testing.\npackage gptest\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// AllPathsToSlash converts a list of paths to their correct\n// platform specific slash representation.\nfunc AllPathsToSlash(paths []string) []string {\n\tr := make([]string, len(paths))\n\tfor i, p := range paths {\n\t\tr[i] = filepath.ToSlash(p)\n\t}\n\n\treturn r\n}\n\nfunc setupEnv(t *testing.T, em map[string]string) {\n\tt.Helper()\n\n\tfor k, v := range em {\n\t\tt.Setenv(k, v)\n\t}\n}\n\n// CliCtx create a new cli context with the given args parsed.\nfunc CliCtx(ctx context.Context, t *testing.T, args ...string) *cli.Context {\n\tt.Helper()\n\n\treturn CliCtxWithFlags(ctx, t, nil, args...)\n}\n\n// CliCtxWithFlags creates a new cli context with the given args and flags parsed.\nfunc CliCtxWithFlags(ctx context.Context, t *testing.T, flags map[string]string, args ...string) *cli.Context {\n\tt.Helper()\n\n\tapp := cli.NewApp()\n\n\tfs := flagset(t, flags, args)\n\tc := cli.NewContext(app, fs, nil)\n\tc.Context = ctx\n\n\treturn c\n}\n\nfunc flagset(t *testing.T, flags map[string]string, args []string) *flag.FlagSet {\n\tt.Helper()\n\n\tfs := flag.NewFlagSet(\"default\", flag.ContinueOnError)\n\n\tfor k, v := range flags {\n\t\tif v == \"true\" || v == \"false\" {\n\t\t\tf := cli.BoolFlag{\n\t\t\t\tName:  k,\n\t\t\t\tUsage: k,\n\t\t\t}\n\t\t\trequire.NoError(t, f.Apply(fs))\n\t\t} else if _, err := strconv.Atoi(v); err == nil {\n\t\t\tf := cli.IntFlag{\n\t\t\t\tName:  k,\n\t\t\t\tUsage: k,\n\t\t\t}\n\t\t\trequire.NoError(t, f.Apply(fs))\n\t\t} else {\n\t\t\tf := cli.StringFlag{\n\t\t\t\tName:  k,\n\t\t\t\tUsage: k,\n\t\t\t}\n\t\t\trequire.NoError(t, f.Apply(fs))\n\t\t}\n\t}\n\n\targl := []string{}\n\tfor k, v := range flags {\n\t\targl = append(argl, \"--\"+k+\"=\"+v)\n\t}\n\n\targl = append(argl, args...)\n\trequire.NoError(t, fs.Parse(argl))\n\n\treturn fs\n}\n\n// UnsetVars will unset the specified env vars and return a restore func.\nfunc UnsetVars(ls ...string) func() {\n\told := make(map[string]string, len(ls))\n\tfor _, k := range ls {\n\t\told[k] = os.Getenv(k)\n\t\t_ = os.Unsetenv(k)\n\t}\n\n\treturn func() {\n\t\tfor k, v := range old {\n\t\t\t_ = os.Setenv(k, v)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tests/grep_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGrep(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"grep\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" grep arg\\n\", out)\n\n\tout, err = ts.run(\"grep BOOM\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"Scanned 0 secrets. 0 matches, 0 errors\")\n\n\tts.initSecrets(\"\")\n\n\tout, err = ts.run(\"grep moar\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"fixed/secret matches\")\n}\n"
  },
  {
    "path": "tests/init_test.go",
    "content": "package tests\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInit(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tout, err := ts.run(\"init\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"Initializing a new password store ...\")\n\tassert.Contains(t, out, \"initialized\")\n\n\tts = newTester(t)\n\tdefer ts.teardown()\n\n\tout, err = ts.run(\"init \" + keyID)\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"initialized for\")\n\n\tts = newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\t// try to init again\n\tout, err = ts.run(\"init \" + keyID)\n\trequire.Error(t, err)\n\n\tfor _, o := range []string{\n\t\t\"found already initialized store at \",\n\t\t\"You can add secondary stores with 'gopass init --path <path to secondary store> --store <mount name>'\",\n\t} {\n\t\tassert.Contains(t, out, o)\n\t}\n}\n"
  },
  {
    "path": "tests/insert_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInsert(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"insert\")\n\trequire.Error(t, err)\n\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" insert name\\n\", out)\n\n\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/secret\"}, []byte(\"moar\"))\n\trequire.NoError(t, err)\n\n\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/newsecret\"}, []byte(\"and\\nmoar\"))\n\trequire.NoError(t, err)\n\n\tt.Run(\"Regression test for #1573 without actual pipes\", func(t *testing.T) {\n\t\tout, err = ts.run(\"show -f some/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\n\t\tout, err = ts.run(\"show -f some/newsecret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"and\\nmoar\", out)\n\n\t\tout, err = ts.run(\"show -f some/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\n\t\tout, err = ts.run(\"show -f some/newsecret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"and\\nmoar\", out)\n\t})\n\n\tt.Run(\"Regression test for #1595\", func(t *testing.T) {\n\t\tt.Skip(\"TODO\")\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/other\"}, []byte(\"nope\"))\n\t\trequire.NoError(t, err)\n\n\t\tout, err = ts.run(\"insert some/other\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: not overwriting your current secret\\n\", out)\n\n\t\tout, err = ts.run(\"show -o some/other\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"nope\", out)\n\n\t\tout, err = ts.run(\"--yes insert some/other\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"Warning: Password is empty or all whitespace\", out)\n\n\t\tout, err = ts.run(\"insert -f some/other\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"Warning: Password is empty or all whitespace\", out)\n\n\t\tout, err = ts.run(\"show -o some/other\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: empty secret\\n\", out)\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"-f\", \"some/other\"}, []byte(\"final\"))\n\t\trequire.NoError(t, err)\n\n\t\tout, err = ts.run(\"show -o some/other\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"final\", out)\n\n\t\t// This is arguably not a good behaviour: it should not overwrite the password when we are only working on a key:value.\n\t\tout, err = ts.run(\"insert -f some/other test:inline\")\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, out)\n\n\t\tout, err = ts.run(\"show some/other test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"inline\", out)\n\n\t\tout, err = ts.run(\"insert some/other test:inline2\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: not overwriting your current secret\\n\", out)\n\n\t\tout, err = ts.run(\"show some/other Test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"inline\", out)\n\n\t\tout, err = ts.run(\"--yes insert some/other test:inline2\")\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, out)\n\n\t\tout, err = ts.run(\"show some/other Test\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"inline2\", out)\n\t})\n\n\tt.Run(\"Regression test for #1650 with JSON\", func(t *testing.T) {\n\t\tjson := `Password: SECRET\n--\nglossary\": {\n    \"title\": \"example glossary\",\n    \"GlossDiv\": {\n        \"title\": \"S\",\n        \"GlossList\": {\n            \"GlossEntry\": {\n                \"ID\": \"SGML\",\n                \"SortAs\": \"SGML\",\n                \"GlossTerm\": \"Standard Generalized Markup Language\",\n                \"Acronym\": \"SGML\",\n                \"Abbrev\": \"ISO 8879:1986\",\n                \"GlossDef\": {\n                    \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n                    \"GlossSeeAlso\": [\"GML\", \"XML\"]\n                },\n                \"GlossSee\": \"markup\"\n            }\n        }\n    }\n}`\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/json\"}, []byte(json))\n\t\trequire.NoError(t, err)\n\n\t\t// using show -n to disable parsing\n\t\tout, err = ts.run(\"show -f -n some/json\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, json, out) //nolint:testifylint\n\t})\n\n\tt.Run(\"Regression test for #1600\", func(t *testing.T) {\n\t\tinput := `test1\ntest2\n{\n  \"Creator\": \"the creator\"\n}`\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/multilinewithbraces\"}, []byte(input))\n\t\trequire.NoError(t, err)\n\n\t\t// using show -n to disable parsing\n\t\tout, err = ts.run(\"show -f -n some/multilinewithbraces\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, input, out)\n\t})\n\n\tt.Run(\"Regression test for #1601\", func(t *testing.T) {\n\t\tinput := `thepassword\nuser: a user\nweb: test.com\nuser: second user`\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/multikey\"}, []byte(input))\n\t\trequire.NoError(t, err)\n\n\t\t// using show -n to disable parsing\n\t\tout, err = ts.run(\"show -f -n some/multikey\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, input, out)\n\t})\n\n\tt.Run(\"Regression test full support for #1601\", func(t *testing.T) {\n\t\tt.Skip(\"Skipping until we support actual key-valueS for KV\")\n\n\t\tinput := `thepassword\nuser: a user\nweb: test.com\nuser: second user`\n\n\t\toutput := `thepassword\nweb: test.com\nuser: a user\nuser: second user`\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/multikeyvalues\"}, []byte(input))\n\t\trequire.NoError(t, err)\n\n\t\tout, err = ts.run(\"show -f some/multikeyvalues\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, output, out)\n\t})\n\n\tt.Run(\"Regression test for #1614\", func(t *testing.T) {\n\t\tinput := `yamltest\n---\nuser: 0123`\n\n\t\toutput := `yamltest\n---\nuser: 83`\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/yamloctal\"}, []byte(input))\n\t\trequire.NoError(t, err)\n\n\t\t// with parsing we have 0123 interpreted as octal for 83\n\t\tout, err = ts.run(\"show -f some/yamloctal\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, output, out)\n\n\t\t// using show -n to disable parsing\n\t\tout, err = ts.run(\"show -f -n some/yamloctal\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, input, out)\n\t})\n\n\tt.Run(\"Regression test for #1594\", func(t *testing.T) {\n\t\tinput := `somepasswd\n---\nTest / test.com\nuser:myuser\nurl: test.com/`\n\n\t\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"some/kvwithspace\"}, []byte(input))\n\t\trequire.NoError(t, err)\n\n\t\tout, err = ts.run(\"show -f some/kvwithspace\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, input, out)\n\n\t\tout, err = ts.run(\"show -f some/kvwithspace url\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"test.com/\", out)\n\n\t\tout, err = ts.run(\"show -f some/kvwithspace user\")\n\t\trequire.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "tests/list_test.go",
    "content": "package tests\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestList(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"gopass\", out)\n\n\tout, err = ts.run(\"ls\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"gopass\", out)\n\n\tts.initSecrets(\"\")\n\n\tlist := `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n└── foo/\n    └── bar\n`\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n\n\tlist = `\nfoo/\n└── bar\n`\n\tout, err = ts.run(\"list foo\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n\n\tlist = `fixed/\nfoo/\n`\n\tout, err = ts.run(\"list --folders\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n}\n\n// regression test for #1628.\nfunc TestListRegressions1628(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"gopass\", out)\n\n\t_, err = ts.run(\"insert misc/file1\")\n\trequire.NoError(t, err)\n\t_, err = ts.run(\"insert misc/folder1/folder2/folder3/file2\")\n\trequire.NoError(t, err)\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\n\texp := `gopass\n└── misc/\n    ├── file1\n    └── folder1/\n        └── folder2/\n            └── folder3/\n                └── file2`\n\tassert.Equal(t, exp, out)\n}\n"
  },
  {
    "path": "tests/mount_test.go",
    "content": "package tests\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSingleMount(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\tts.initSecrets(\"\")\n\n\tout, err := ts.run(\"init --store mnt/m1 --path \" + ts.storeDir(\"m1\") + \" --storage=fs \" + keyID)\n\tt.Logf(\"Output: %s\", out)\n\trequire.NoError(t, err)\n\n\tout, err = ts.run(\"mounts\")\n\trequire.NoError(t, err)\n\n\twant := \"gopass (\" + ts.storeDir(\"root\") + \")\\n\"\n\twant += \"└── mnt/\\n    └── m1 (\" + ts.storeDir(\"m1\") + \")\"\n\tassert.Equal(t, strings.TrimSpace(want), out)\n\n\tout, err = ts.run(\"show mnt/m1/secret\")\n\trequire.Error(t, err)\n\tassert.Contains(t, out, \"entry is not in the password store\")\n\n\tts.initSecrets(\"mnt/m1/\")\n\n\tlist := `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n├── foo/\n│   └── bar\n└── mnt/\n    └── m1 (%s)\n        ├── baz\n        ├── fixed/\n        │   ├── secret\n        │   └── twoliner\n        └── foo/\n            └── bar\n`\n\tlist = fmt.Sprintf(list, ts.storeDir(\"m1\"))\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n}\n\nfunc TestMountShadowing(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\tts.initSecrets(\"\")\n\n\t// insert some secret at a place that will be shadowed by a mount\n\t_, err := ts.runCmd([]string{ts.Binary, \"insert\", \"mnt/m1/secret\"}, []byte(\"moar\"))\n\trequire.NoError(t, err)\n\n\tout, err := ts.run(\"show -f mnt/m1/secret\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"moar\", out)\n\n\tout, err = ts.run(\"init --store mnt/m1 --path \" + ts.storeDir(\"m1\") + \" --storage=fs \" + keyID)\n\tt.Logf(\"Output: %s\", out)\n\trequire.NoError(t, err)\n\n\t// check the mount is there\n\tout, err = ts.run(\"mounts\")\n\trequire.NoError(t, err)\n\n\twant := \"gopass (\" + ts.storeDir(\"root\") + \")\\n\"\n\twant += \"└── mnt/\\n    └── m1 (\" + ts.storeDir(\"m1\") + \")\"\n\tassert.Equal(t, strings.TrimSpace(want), out)\n\n\t// check that the mount is not containing our shadowed secret\n\tout, err = ts.run(\"show -f mnt/m1/secret\")\n\trequire.Error(t, err)\n\tassert.Contains(t, out, \"entry is not in the password store\")\n\n\t// insert some secret at the place that is shadowed by the mount\n\t_, err = ts.runCmd([]string{ts.Binary, \"insert\", \"mnt/m1/secret\"}, []byte(\"food\"))\n\trequire.NoError(t, err)\n\n\t// check that the mount is containing our new secret shadowing the old one\n\tout, err = ts.run(\"show -f mnt/m1/secret\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"food\", out)\n\n\t// add more secrets\n\tts.initSecrets(\"mnt/m1/\")\n\n\t// check that the mount is listed\n\tlist := `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n├── foo/\n│   └── bar\n└── mnt/\n    └── m1 (%s)\n        ├── baz\n        ├── fixed/\n        │   ├── secret\n        │   └── twoliner\n        ├── foo/\n        │   └── bar\n        └── secret\n`\n\tlist = fmt.Sprintf(list, ts.storeDir(\"m1\"))\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n\n\t// check that unmounting works:\n\t_, err = ts.run(\"mounts rm mnt/m1\")\n\trequire.NoError(t, err)\n\n\tlist = `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n├── foo/\n│   └── bar\n└── mnt/\n    └── m1/\n        └── secret\n`\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n\n\tout, err = ts.run(\"show -o mnt/m1/secret\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"moar\", out)\n}\n\nfunc TestMultiMount(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\tts.initSecrets(\"\")\n\n\t// mount m1\n\tout, err := ts.run(\"init --store mnt/m1 --path \" + ts.storeDir(\"m1\") + \" --storage=fs \" + keyID)\n\tt.Logf(\"Output: %s\", out)\n\trequire.NoError(t, err)\n\n\tts.initSecrets(\"mnt/m1/\")\n\n\tlist := `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n├── foo/\n│   └── bar\n└── mnt/\n    └── m1 (%s)\n        ├── baz\n        ├── fixed/\n        │   ├── secret\n        │   └── twoliner\n        └── foo/\n            └── bar\n`\n\tlist = fmt.Sprintf(list, ts.storeDir(\"m1\"))\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n\n\t// mount m2\n\tout, err = ts.run(\"init --store mnt/m2 --path \" + ts.storeDir(\"m2\") + \" --storage=fs \" + keyID)\n\tt.Logf(\"Output: %s\", out)\n\trequire.NoError(t, err)\n\n\tts.initSecrets(\"mnt/m2/\")\n\n\tlist = `\ngopass\n├── baz\n├── fixed/\n│   ├── secret\n│   └── twoliner\n├── foo/\n│   └── bar\n└── mnt/\n    ├── m1 (%s)\n    │   ├── baz\n    │   ├── fixed/\n    │   │   ├── secret\n    │   │   └── twoliner\n    │   └── foo/\n    │       └── bar\n    └── m2 (%s)\n        ├── baz\n        ├── fixed/\n        │   ├── secret\n        │   └── twoliner\n        └── foo/\n            └── bar\n`\n\tlist = fmt.Sprintf(list, ts.storeDir(\"m1\"), ts.storeDir(\"m2\"))\n\n\tout, err = ts.run(\"list\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, strings.TrimSpace(list), out)\n}\n"
  },
  {
    "path": "tests/move_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMove(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"move before init\", func(t *testing.T) {\n\t\t_, err := ts.run(\"move\")\n\t\trequire.Error(t, err)\n\t})\n\n\t// init store so it does exist, but empty so far\n\tts.initStore()\n\n\tt.Run(\"move w/o args\", func(t *testing.T) {\n\t\tout, err := ts.run(\"move\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" mv old-path new-path\\n\", out)\n\t})\n\n\tt.Run(\"move w/o destination\", func(t *testing.T) {\n\t\tout, err := ts.run(\"move foo\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" mv old-path new-path\\n\", out)\n\t})\n\n\tt.Run(\"move non existing source\", func(t *testing.T) {\n\t\tout, err := ts.run(\"move foo bar\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: source foo does not exist in source store : entry is not in the password store\\n\", out)\n\t})\n\n\t// populate store with secrets\n\tts.initSecrets(\"\")\n\n\tt.Run(\"move a secret\", func(t *testing.T) {\n\t\t_, err := ts.run(\"move foo bar\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"move existing secret from non-existing destination\", func(t *testing.T) {\n\t\tout, _ := ts.run(\"move foo/bar foo/baz\")\n\t\tassert.Equal(t, \"\\nError: source foo/bar does not exist in source store : entry is not in the password store\\n\", out)\n\t})\n\n\tt.Run(\"show source secret\", func(t *testing.T) {\n\t\t_, err := ts.run(\"show -f bar/bar\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"show secret\", func(t *testing.T) {\n\t\t_, err := ts.run(\"show -f baz\")\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "tests/show_test.go",
    "content": "package tests\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar goldenQr = \"\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[40m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\n\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\\x1b[47m  \\x1b[0m\"\n\nfunc TestShow(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\t_, err := ts.run(\"show\")\n\trequire.Error(t, err)\n\n\tts.initStore()\n\n\tt.Run(\"test usage\", func(t *testing.T) {\n\t\tout, err := ts.run(\"show\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, \"\\nError: Usage: \"+filepath.Base(ts.Binary)+\" show [name]\\n\", out)\n\t})\n\n\tt.Run(\"test show with non-existing secret\", func(t *testing.T) {\n\t\tout, err := ts.run(\"show foo\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"entry is not in the password store\", out)\n\t})\n\n\tts.initSecrets(\"\")\n\n\tt.Run(\"show folder foo\", func(t *testing.T) {\n\t\t_, err = ts.run(\"show foo\")\n\t\trequire.NoError(t, err)\n\t\t_, err = ts.run(\"show -u foo\")\n\t\trequire.NoError(t, err)\n\t\t_, err = ts.run(\"show foo -unsafe\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"show w/o safecontent\", func(t *testing.T) {\n\t\t_, err = ts.run(\"config show.safecontent false\")\n\t\trequire.NoError(t, err)\n\n\t\tout, err := ts.run(\"show fixed/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\n\t\tout, err = ts.run(\"show fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"first line\\nsecond line\", out)\n\n\t\tout, err = ts.run(\"show --qr fixed/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, goldenQr, out)\n\t})\n\n\tt.Run(\"show w/o autoclip\", func(t *testing.T) {\n\t\t_, err = ts.run(\"config generate.autoclip false\")\n\t\trequire.NoError(t, err)\n\t\t_, err = ts.run(\"show fixed/secret\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"show with safecontent\", func(t *testing.T) {\n\t\t_, err = ts.run(\"config show.safecontent true\")\n\t\trequire.NoError(t, err, \"set show.safecontent = true\")\n\n\t\tout, err := ts.run(\"config show.safecontent\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, \"true\", \"verify show.safecontent = true\")\n\n\t\tout, err = ts.run(\"show fixed/secret\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"safecontent\", \"output should contain a safecontent warning\")\n\n\t\tout, err = ts.run(\"show fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotContains(t, out, \"password: ***\")\n\t\tassert.Contains(t, out, \"second line\")\n\t\tassert.NotContains(t, out, \"first line\", \"safecontent = true should remove the first (password) line\")\n\t})\n\n\tt.Run(\"force showing full secret\", func(t *testing.T) {\n\t\t_, err = ts.run(\"config show.safecontent true\")\n\t\trequire.NoError(t, err)\n\n\t\tout, err := ts.run(\"show -u fixed/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\n\t\tout, err = ts.run(\"show -o fixed/secret\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\n\t\tout, err = ts.run(\"show -u fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"first line\\nsecond line\", out)\n\n\t\tout, err = ts.run(\"show -o fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"first line\", out)\n\n\t\tout, err = ts.run(\"show -c fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotContains(t, out, \"***\")\n\t\tassert.NotContains(t, out, \"safecontent=true\")\n\t\tassert.NotContains(t, out, \"first line\")\n\t\tassert.NotContains(t, out, \"second line\")\n\n\t\tout, err = ts.run(\"show -C fixed/twoliner\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, \"second line\")\n\t\tassert.NotContains(t, out, \"first line\")\n\t})\n\n\tt.Run(\"Regression test for #1574 and #1575\", func(t *testing.T) {\n\t\tt.Setenv(\"GOPASS_CHARACTER_SET\", \"a\")\n\n\t\t_, err = ts.run(\"config show.safecontent true\")\n\t\trequire.NoError(t, err)\n\n\t\t_, err := ts.run(\"generate fo2 5\")\n\t\trequire.NoError(t, err)\n\n\t\tout, err := ts.run(\"show fo2\")\n\t\trequire.Error(t, err)\n\t\tassert.NotContains(t, out, \"password: *****\")\n\t\tassert.NotContains(t, out, \"aaaaa\")\n\t\tassert.Contains(t, out, \"safecontent=true\")\n\n\t\tout, err = ts.run(\"show -u fo2\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"aaaaa\", out)\n\n\t\t_, err = ts.run(\"generate fo6 5\")\n\t\trequire.NoError(t, err)\n\n\t\tout, err = ts.run(\"show fo6\")\n\t\trequire.Error(t, err)\n\t\tassert.NotContains(t, out, \"password: ***\")\n\t\tassert.NotContains(t, out, \"aaaaa\")\n\t\tassert.Contains(t, out, \"safecontent=true\")\n\n\t\tout, err = ts.run(\"show -u fo6\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"aaaaa\", out)\n\t\tassert.NotContains(t, out, \"\\n\\n\")\n\t})\n}\n"
  },
  {
    "path": "tests/sync_test.go",
    "content": "package tests\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/packet\"\n\t\"github.com/gopasspw/gopass/tests/can\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSync(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tts.initStore()\n\n\tout, err := ts.run(\"sync\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, out, \"All done\")\n}\n\nfunc createGPGKey(t *testing.T, ts *tester, name, email string) string {\n\tt.Helper()\n\te, err := openpgp.NewEntity(name, \"\", email, &packet.Config{\n\t\tRSABits: 4096,\n\t})\n\trequire.NoError(t, err)\n\n\tfor _, id := range e.Identities {\n\t\terr := id.SelfSignature.SignUserId(id.UserId.Id, e.PrimaryKey, e.PrivateKey, &packet.Config{})\n\t\trequire.NoError(t, err)\n\t}\n\n\tel := can.EmbeddedKeyRing()\n\tel = append(el, e)\n\n\tfn := filepath.Join(ts.gpgDir(), \"pubring.gpg\")\n\tfh, err := os.Create(fn)\n\trequire.NoError(t, err)\n\n\tfor _, e := range el {\n\t\trequire.NoError(t, e.Serialize(fh))\n\t}\n\trequire.NoError(t, fh.Close())\n\n\tfn = filepath.Join(ts.gpgDir(), \"secring.gpg\")\n\tfh, err = os.Create(fn)\n\trequire.NoError(t, err)\n\n\tfor _, e := range el {\n\t\tif e.PrivateKey != nil {\n\t\t\trequire.NoError(t, e.SerializePrivate(fh, nil))\n\t\t}\n\t}\n\trequire.NoError(t, fh.Close())\n\n\treturn e.PrimaryKey.KeyIdShortString()\n}\n\nfunc TestSyncKeepSubkey(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\t// init store\n\tts.initStore()\n\n\t// create a new key\n\tkeyID := createGPGKey(t, ts, \"sub-store-key\", \"sub-store-key@example.com\")\n\n\t// create a secret in a subdirectory\n\tsecretPath := \"project_123/secret1\"\n\tout, err := ts.run(\"insert -f \" + secretPath)\n\trequire.NoError(t, err, \"failed to insert secret: %s\", out)\n\n\t// create .gpg-id file\n\tsubDir := filepath.Join(ts.storeDir(\"root\"), \"project_123\")\n\trequire.NoError(t, os.MkdirAll(subDir, 0o755))\n\tgpgIDFile := filepath.Join(subDir, \".gpg-id\")\n\terr = os.WriteFile(gpgIDFile, []byte(keyID), 0o644)\n\trequire.NoError(t, err)\n\n\t// re-encrypt the secret\n\tout, err = ts.run(\"fsck --decrypt \" + filepath.Dir(secretPath))\n\trequire.NoError(t, err, \"failed to fsck: %s\", out)\n\n\t// sync the store\n\tout, err = ts.run(\"sync\")\n\trequire.NoError(t, err, \"failed to sync: %s\", out)\n\n\t// export the public key\n\tpubKeyFile := filepath.Join(ts.storeDir(\"root\"), \".public-keys\", keyID)\n\tout, err = ts.runCmd([]string{\"gpg\", \"--armor\", \"--export\", keyID}, nil)\n\trequire.NoError(t, err, \"failed to export key: %s\", out)\n\terr = os.WriteFile(pubKeyFile, []byte(out), 0o644)\n\trequire.NoError(t, err)\n\n\t// run sync again\n\tout, err = ts.run(\"sync\")\n\trequire.NoError(t, err, \"failed to sync: %s\", out)\n\n\t// check if the public key file still exists\n\tassert.FileExists(t, pubKeyFile, \"public key file should exist\")\n}\n"
  },
  {
    "path": "tests/tester.go",
    "content": "// Package tests contains common test helpers\npackage tests\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gopasspw/gopass/tests/can\"\n\t\"github.com/gopasspw/gopass/tests/gptest\"\n\tshellquote \"github.com/kballard/go-shellquote\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tgopassConfig = `[core]\nexportkeys = false\n`\n\tkeyID = \"BE73F104\"\n)\n\n// ErrNoCommand is returned when the command is missing.\nvar ErrNoCommand = fmt.Errorf(\"no command\")\n\ntype tester struct {\n\tt *testing.T\n\n\t// Binary is the path to the gopass binary used for testing\n\tBinary    string\n\tsourceDir string\n\ttempDir   string\n\tresetFn   func()\n}\n\n// newTester is not compatible with t.Parallel because it uses t.Setenv.\nfunc newTester(t *testing.T) *tester {\n\tt.Helper()\n\n\tsourceDir := \".\"\n\tif d := os.Getenv(\"GOPASS_TEST_DIR\"); d != \"\" {\n\t\tsourceDir = d\n\t}\n\n\tgopassBin := \"\"\n\tif b := os.Getenv(\"GOPASS_BINARY\"); b != \"\" {\n\t\tgopassBin = b\n\t}\n\n\tfi, err := os.Stat(gopassBin)\n\tif err != nil {\n\t\tt.Skipf(\"Failed to stat GOPASS_BINARY %s: %s\", gopassBin, err)\n\t}\n\n\tif !strings.HasSuffix(gopassBin, \".exe\") && fi.Mode()&0o111 == 0 {\n\t\tt.Fatalf(\"GOPASS_BINARY is not executeable\")\n\t}\n\n\tt.Logf(\"Using gopass binary: %s\", gopassBin)\n\n\tts := &tester{\n\t\tt:         t,\n\t\tsourceDir: sourceDir,\n\t\tBinary:    gopassBin,\n\t}\n\n\t// create tempDir\n\ttd := t.TempDir()\n\trequire.NotEmpty(t, td)\n\trequire.NoError(t, err)\n\n\tt.Logf(\"Tempdir: %s\", td)\n\tts.tempDir = td\n\n\t// prepare ENVIRONMENT\n\tts.resetFn = gptest.UnsetVars(\"GNUPGHOME\", \"GOPASS_DEBUG\", \"NO_COLOR\", \"GOPASS_CONFIG\", \"GOPASS_NO_NOTIFY\", \"GOPASS_HOMEDIR\")\n\tt.Setenv(\"GNUPGHOME\", ts.gpgDir())\n\tt.Setenv(\"GOPASS_DEBUG\", \"\")\n\tt.Setenv(\"NO_COLOR\", \"true\")\n\tt.Setenv(\"GOPASS_CONFIG_NOSYSTEM\", \"true\")\n\tt.Setenv(\"GOPASS_CONFIG_NO_MIGRATE\", \"true\")\n\tt.Setenv(\"GOPASS_NO_NOTIFY\", \"true\")\n\tt.Setenv(\"GOPASS_HOMEDIR\", td)\n\n\t// write config\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(ts.gopassConfig()), 0o700))\n\t// we need to set the root path to something else than the root directory otherwise the mounts will show as regular entries\n\tif err := os.WriteFile(ts.gopassConfig(), []byte(gopassConfig+\"\\n[mounts]\\npath = \"+ts.storeDir(\"root\")+\"\\n\"), 0o600); err != nil {\n\t\tt.Fatalf(\"Failed to write gopass config to %s: %s\", ts.gopassConfig(), err)\n\t}\n\n\t// copy gpg test files\n\trequire.NoError(t, can.WriteTo(ts.gpgDir()))\n\n\treturn ts\n}\n\nfunc (ts tester) gpgDir() string {\n\treturn filepath.Join(ts.tempDir, \".gnupg\")\n}\n\nfunc (ts tester) gopassConfig() string {\n\treturn filepath.Join(ts.tempDir, \".config\", \"gopass\", \"config\")\n}\n\nfunc (ts tester) storeDir(mount string) string {\n\treturn filepath.Join(ts.tempDir, \".local\", \"share\", \"gopass\", \"stores\", mount)\n}\n\nfunc (ts tester) workDir() string {\n\treturn filepath.Dir(ts.tempDir)\n}\n\nfunc (ts tester) teardown() {\n\tts.resetFn() // restore env vars\n\n\tif ts.tempDir == \"\" {\n\t\treturn\n\t}\n\n\terr := os.RemoveAll(ts.tempDir)\n\trequire.NoError(ts.t, err)\n}\n\nfunc (ts tester) runCmd(args []string, in []byte) (string, error) {\n\tts.t.Helper()\n\n\tif len(args) < 1 {\n\t\treturn \"\", fmt.Errorf(\"invalid args %v: %w\", args, ErrNoCommand)\n\t}\n\n\tcmd := exec.CommandContext(context.Background(), args[0], args[1:]...)\n\tcmd.Dir = ts.workDir()\n\tcmd.Stdin = bytes.NewReader(in)\n\n\tts.t.Logf(\"%+v\", cmd.Args)\n\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn string(out), err\n\t}\n\n\treturn strings.TrimSpace(string(out)), nil\n}\n\nfunc (ts tester) run(arg string) (string, error) {\n\tts.t.Helper()\n\n\tif runtime.GOOS == \"windows\" {\n\t\targ = strings.ReplaceAll(arg, \"\\\\\", \"\\\\\\\\\")\n\t}\n\n\targs, err := shellquote.Split(arg)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to split args %v: %w\", arg, err)\n\t}\n\n\tcmd := exec.CommandContext(context.Background(), ts.Binary, args...)\n\tcmd.Dir = ts.workDir()\n\n\tts.t.Logf(\"%+v\", cmd.Args)\n\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn string(out), err\n\t}\n\n\treturn strings.TrimSpace(string(out)), nil\n}\n\nfunc (ts tester) runWithInput(arg, input string) ([]byte, error) { //nolint:unused\n\tts.t.Helper()\n\n\treader := strings.NewReader(input)\n\n\treturn ts.runWithInputReader(arg, reader)\n}\n\nfunc (ts tester) runWithInputReader(arg string, input io.Reader) ([]byte, error) { //nolint:unused\n\tts.t.Helper()\n\n\targs, err := shellquote.Split(arg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to split args %v: %w\", arg, err)\n\t}\n\n\tcmd := exec.Command(ts.Binary, args...)\n\tcmd.Dir = ts.workDir()\n\tcmd.Stdin = input\n\n\tts.t.Logf(\"%+v\", cmd.Args)\n\n\tbuf, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn buf, fmt.Errorf(\"%s %v failed: %w\", ts.Binary, args, err)\n\t}\n\n\treturn buf, nil\n}\n\nfunc (ts *tester) initStore() {\n\tout, err := ts.run(\"init --crypto=gpgcli --storage=fs \" + keyID)\n\trequire.NoError(ts.t, err, \"failed to init password store:\\n%s\", out)\n}\n\nfunc (ts *tester) initSecrets(prefix string) {\n\tout, err := ts.run(\"generate -p \" + prefix + \"foo/bar 20\")\n\trequire.NoError(ts.t, err, \"failed to generate password:\\n%s\", out)\n\n\tout, err = ts.run(\"generate -p \" + prefix + \"baz 40\")\n\trequire.NoError(ts.t, err, \"failed to generate password:\\n%s\", out)\n\n\tout, err = ts.runCmd([]string{ts.Binary, \"insert\", prefix + \"fixed/secret\"}, []byte(\"moar\"))\n\trequire.NoError(ts.t, err, \"failed to insert password:\\n%s\", out)\n\n\tout, err = ts.runCmd([]string{ts.Binary, \"insert\", prefix + \"fixed/twoliner\"}, []byte(\"first line\\nsecond line\"))\n\trequire.NoError(ts.t, err, \"failed to insert password:\\n%s\", out)\n}\n"
  },
  {
    "path": "tests/uninitialized_test.go",
    "content": "package tests\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUninitialized(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tcommands := []string{\n\t\t\"\",\n\t\t\"copy\",\n\t\t\"cp\",\n\t\t\"delete\",\n\t\t\"edit\",\n\t\t\"find\",\n\t\t\"generate\",\n\t\t\"grep\",\n\t\t\"insert\",\n\t\t\"list\",\n\t\t\"ls\",\n\t\t\"mount\",\n\t\t\"move\",\n\t\t\"mv\",\n\t\t\"remove\",\n\t\t\"rm\",\n\t\t\"show\",\n\t}\n\n\tfor _, command := range commands {\n\t\tt.Run(command, func(t *testing.T) {\n\t\t\tout, err := ts.run(command)\n\t\t\trequire.Error(t, err)\n\t\t\tassert.Contains(t, out, \"password-store is not initialized. Try \")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "tests/yaml_test.go",
    "content": "package tests\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestYAMLAndSecret(t *testing.T) {\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"show key from uninitialized store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"show foo/bar baz\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"default action (show) from initialized store\", func(t *testing.T) {\n\t\tout, err := ts.run(\"foo/bar baz\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"entry is not in the password store\")\n\t})\n\n\tt.Run(\"insert key\", func(t *testing.T) {\n\t\t_, err := ts.runCmd([]string{ts.Binary, \"insert\", \"foo/bar\", \"password\"}, []byte(\"moar\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"insert another key\", func(t *testing.T) {\n\t\t_, err := ts.runCmd([]string{ts.Binary, \"insert\", \"foo/bar\", \"baz\"}, []byte(\"moar\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"insert into the body\", func(t *testing.T) {\n\t\tout, err := ts.runCmd([]string{ts.Binary, \"insert\", \"-a\", \"foo/bar\"}, []byte(\"body\"))\n\t\trequire.NoError(t, err, out)\n\t})\n\n\tt.Run(\"show a key\", func(t *testing.T) {\n\t\tout, err := ts.run(\"show foo/bar baz\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"moar\", out)\n\t})\n\n\tt.Run(\"show the whole secret\", func(t *testing.T) {\n\t\tout, err := ts.run(\"show foo/bar\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"password: moar\\nbaz: moar\\nbody\", out)\n\t})\n}\n\nfunc TestInvalidYAML(t *testing.T) {\n\ttestBody := `somepasswd\n---\nTest / test.com\nusername: myuser@test.com\npassword: someotherpasswd\nurl: http://www.test.com/`\n\n\tts := newTester(t)\n\tdefer ts.teardown()\n\n\tt.Run(\"show secret from uninitialized store\", func(t *testing.T) {\n\t\t_, err := ts.run(\"show foo/bar\")\n\t\trequire.Error(t, err)\n\t})\n\n\tts.initStore()\n\n\tt.Run(\"show non-existing secret\", func(t *testing.T) {\n\t\tout, err := ts.run(\"foo/bar\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, out, \"entry is not in the password store\")\n\t})\n\n\tt.Run(\"insert new secret\", func(t *testing.T) {\n\t\t_, err := ts.runCmd([]string{ts.Binary, \"insert\", \"foo/bar\"}, []byte(testBody))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"show newly inserted secret\", func(t *testing.T) {\n\t\t_, err := ts.run(\"show foo/bar\")\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "version.go",
    "content": "package main\n\nimport (\n\t\"strings\"\n\n\t\"github.com/blang/semver/v4\"\n)\n\nfunc getVersion() semver.Version {\n\tsv, err := semver.Parse(strings.TrimPrefix(version, \"v\"))\n\tif err == nil {\n\t\treturn sv\n\t}\n\n\treturn semver.Version{\n\t\tMajor: 1,\n\t\tMinor: 16,\n\t\tPatch: 1,\n\t\tPre: []semver.PRVersion{\n\t\t\t{VersionStr: \"git\"},\n\t\t},\n\t\tBuild: []string{\"d601d3ef\"},\n\t}\n}\n"
  },
  {
    "path": "zsh.completion",
    "content": "#compdef gopass\n\n_gopass () {\n    local cmd\n    if (( CURRENT > 2)); then\n\tcmd=${words[2]}\n\tcurcontext=\"${curcontext%:*:*}:gopass-$cmd\"\n\t(( CURRENT-- ))\n\tshift words\n\tcase \"${cmd}\" in\n\t  age)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"agent:Manage the age agent\"\n\t      \"identities:List age identities used for decryption and encryption\"\n\t      \"lock:Lock the age agent\"\n\t      )\n\t      _describe -t commands \"gopass age\" subcommands\n\t      _arguments : \"--age-ssh-key-path[Custom path to SSH key or directory for age backend]\"\n\t      \n\t      \n\t      ;;\n\t  alias)\n\t      \n\t      \n\t      \n\t      ;;\n\t  audit)\n\t      _arguments : \"--format[Output format. text, csv or html. Default: text]\" \"--output-file[Output filename. Used for csv and html]\" \"--template[HTML template. If not set use the built-in default.]\" \"--full[Print full details of all findings. Default: false]\" \"--summary[Print a summary of the audit results. Default: true (print summary)]\"\n\t      \n\t      \n\t      ;;\n\t  cat)\n\t      \n\t      \n\t      \n\t      ;;\n\t  clone)\n\t      _arguments : \"--path[Path to clone the repo to]\" \"--crypto[Select crypto backend \\[age gpgcli plain\\]]\" \"--storage[Select storage backend \\[cryptfs fossilfs gitfs jjfs\\]]\" \"--check-keys[Check for valid decryption keys. Generate new keys if none are found.]\"\n\t      \n\t      \n\t      ;;\n\t  completion)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"bash:Source for auto completion in bash\"\n\t      \"zsh:Source for auto completion in zsh\"\n\t      \"fish:Source for auto completion in fish\"\n\t      \"openbsdksh:Source for auto completion in OpenBSD's ksh\"\n\t      \"help:Shows a list of commands or help for one command\"\n\t      )\n\t      _describe -t commands \"gopass completion\" subcommands\n\t      _arguments : \"--help[show help]\"\n\t      \n\t      \n\t      ;;\n\t  config)\n\t      _arguments : \"--store[Set options to a specific store]\"\n\t      \n\t      \n\t      ;;\n\t  convert)\n\t      _arguments : \"--store[Specify which store to convert]\" \"--move[Replace store?]\" \"--crypto[Which crypto backend? \\[age gpgcli plain\\]]\" \"--storage[Which storage backend? \\[cryptfs fossilfs fs gitfs jjfs\\]]\"\n\t      \n\t      \n\t      ;;\n\t  copy|cp)\n\t      _arguments : \"--force[Force to copy the secret and overwrite existing one]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  create|new)\n\t      _arguments : \"--store[Which store to use]\" \"--force[Force path selection]\"\n\t      \n\t      \n\t      ;;\n\t  delete|remove|rm)\n\t      _arguments : \"--recursive[Recursive delete files and folders]\" \"--force[Force to delete the secret]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  edit|set)\n\t      _arguments : \"--editor[Use this editor binary]\" \"--create[Create a new secret if none found]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  env)\n\t      _arguments : \"--keep-case[Do not capitalize the environment variable and instead retain the original capitalization]\"\n\t      \n\t      \n\t      ;;\n\t  find|search)\n\t      _arguments : \"--unsafe[In the case of an exact match, display the password even if safecontent is enabled]\" \"--regex[Interpret pattern as regular expression]\"\n\t      \n\t      \n\t      ;;\n\t  fsck)\n\t      _arguments : \"--decrypt[Decrypt and reencrypt during fsck.]\" \"--store[Limit fsck to this mount point]\"\n\t      \n\t      \n\t      ;;\n\t  fscopy)\n\t      \n\t      \n\t      \n\t      ;;\n\t  fsmove)\n\t      \n\t      \n\t      \n\t      ;;\n\t  generate)\n\t      _arguments : \"--clip[Copy the generated password to the clipboard]\" \"--print[Print the generated password to the terminal]\" \"--force[Force to overwrite existing password]\" \"--edit[Open secret for editing after generating a password]\" \"--symbols[Use symbols in the password]\" \"--generator[Choose a password generator, use one of: cryptic, memorable, xkcd or external. Default: cryptic]\" \"--strict[Require strict character class rules]\" \"--force-regen[Force full re-generation, incl. evaluation of templates. Will overwrite the entire secret!]\" \"--sep[Word separator for generated passwords. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised.]\" \"--lang[Language to generate password from, currently only en (english, default) or de are supported]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      _gopass_complete_folders\n\t      _gopass_complete_passwords\n\t      ;;\n\t  git)\n\t      _arguments : \"--store[Store to operate on]\"\n\t      \n\t      \n\t      ;;\n\t  grep)\n\t      _arguments : \"--regexp[Interpret pattern as RE2 regular expression]\"\n\t      \n\t      \n\t      ;;\n\t  history|hist)\n\t      _arguments : \"--password[Include passwords in output]\"\n\t      \n\t      \n\t      ;;\n\t  init)\n\t      _arguments : \"--path[Set the sub-store path to operate on]\" \"--store[Set the name of the sub-store]\" \"--crypto[Select crypto backend \\[age gpgcli plain\\]]\" \"--storage[Select storage backend \\[cryptfs fossilfs fs gitfs jjfs\\]]\"\n\t      \n\t      \n\t      ;;\n\t  insert)\n\t      _arguments : \"--echo[Display secret while typing]\" \"--multiline[Insert using $EDITOR]\" \"--force[Overwrite any existing secret and do not prompt to confirm recipients]\" \"--append[Append data read from STDIN to existing data]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      _gopass_complete_folders\n\t      _gopass_complete_passwords\n\t      ;;\n\t  link|ln|symlink)\n\t      \n\t      \n\t      \n\t      ;;\n\t  list|ls)\n\t      _arguments : \"--limit[Display no more than this many levels of the tree]\" \"--flat[Print a flat list]\" \"--folders[Print a flat list of folders]\" \"--strip-prefix[Strip this prefix from filtered entries]\"\n\t      _gopass_complete_folders\n\t      \n\t      ;;\n\t  merge)\n\t      _arguments : \"--delete[Remove merged entries]\" \"--force[Skip editor, merge entries unattended]\"\n\t      \n\t      \n\t      ;;\n\t  mounts)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"add:Mount a password store\"\n\t      \"remove:Umount an mounted password store\"\n\t      \"versions:Display mount provider versions\"\n\t      )\n\t      _describe -t commands \"gopass mounts\" subcommands\n\t      \n\t      \n\t      \n\t      ;;\n\t  move|mv)\n\t      _arguments : \"--force[Force to move the secret and overwrite existing one]\" \"--commit-message[Set the commit message]\" \"--interactive-commit[Open an editor for the commit message]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  otp|totp|hotp)\n\t      _arguments : \"--alsoclip[Copy the time-based token and show it]\" \"--clip[Copy the time-based token into the clipboard]\" \"--qr[Write QR code to FILE]\" \"--chained[chain the token to the password]\" \"--password[Only display the token]\" \"--snip[Scan screen content to insert a OTP QR code into provided entry]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  process)\n\t      \n\t      \n\t      \n\t      ;;\n\t  pwgen)\n\t      _arguments : \"--no-numerals[Do not include numerals in the generated passwords.]\" \"--no-capitalize[Do not include capital letter in the generated passwords.]\" \"--ambiguous[Do not include characters that could be easily confused with each other, like '1' and 'l' or '0' and 'O']\" \"--symbols[Include at least one symbol in the password.]\" \"--one-per-line[Print one password per line]\" \"--xkcd[Use multiple random english words combined to a password. By default, space is used as separator and all words are lowercase]\" \"--sep[Word separator for generated xkcd style password. If no separator is specified, the words are combined without spaces/separator and the first character of words is capitalised. This flag implies -xkcd]\" \"--lang[Language to generate password from, currently only en (english, default) or de are supported]\" \"--xkcdcapitalize[Capitalize first letter of each word in generated xkcd style password. This flag implies -xkcd]\" \"--xkcdnumbers[Add a random number to the end of the generated xkcd style password. This flag implies -xkcd]\"\n\t      \n\t      \n\t      ;;\n\t  rcs)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"init:Init RCS repo\"\n\t      \"status:RCS status\"\n\t      )\n\t      _describe -t commands \"gopass rcs\" subcommands\n\t      \n\t      \n\t      \n\t      ;;\n\t  recipients)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"ack:Update recipients.hash\"\n\t      \"add:Add any number of Recipients to any store\"\n\t      \"remove:Remove any number of Recipients from any store\"\n\t      )\n\t      _describe -t commands \"gopass recipients\" subcommands\n\t      _arguments : \"--pretty[Pretty print recipients]\"\n\t      \n\t      \n\t      ;;\n\t  reorg)\n\t      \n\t      \n\t      \n\t      ;;\n\t  setup)\n\t      _arguments : \"--remote[URL to a git remote, will attempt to join this team]\" \"--alias[Local mount point for the given remote]\" \"--create[Create a new team (default: false, i.e. join an existing team)]\" \"--name[Firstname and Lastname for unattended GPG key generation]\" \"--email[EMail for unattended GPG key generation]\" \"--crypto[Select crypto backend \\[age gpgcli plain\\]]\" \"--storage[Select storage backend \\[cryptfs fossilfs fs gitfs jjfs\\]]\"\n\t      \n\t      \n\t      ;;\n\t  show)\n\t      _arguments : \"--yes[Always answer yes to yes/no questions]\" \"--clip[Copy the password value into the clipboard]\" \"--alsoclip[Copy the password and show everything]\" \"--qr[Print the password as a QR Code]\" \"--qrbody[Print the body as a QR Code]\" \"--unsafe[Display unsafe content (e.g. the password) even if safecontent is enabled]\" \"--safe[Do not display unsafe content (e.g. the password) even if safecontent is disabled]\" \"--password[Display only the password. Takes precedence over all other flags.]\" \"--revision[Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry.]\" \"--noparsing[Do not parse the output.]\" \"--nosync[Disable auto-sync]\" \"--chars[Print specific characters from the secret]\"\n\t      \n\t      _gopass_complete_passwords\n\t      ;;\n\t  sum|sha|sha256)\n\t      \n\t      \n\t      \n\t      ;;\n\t  sync)\n\t      _arguments : \"--store[Select the store to sync]\"\n\t      \n\t      \n\t      ;;\n\t  templates)\n\t      local -a subcommands\n\t      subcommands=(\n\t      \"show:Show a secret template.\"\n\t      \"edit:Edit secret templates.\"\n\t      \"remove:Remove secret templates.\"\n\t      )\n\t      _describe -t commands \"gopass templates\" subcommands\n\t      \n\t      \n\t      \n\t      ;;\n\t  unclip)\n\t      _arguments : \"--timeout[Time to wait]\" \"--force[Clear clipboard even if checksum mismatches]\"\n\t      \n\t      \n\t      ;;\n\t  update)\n\t      \n\t      \n\t      \n\t      ;;\n\t  version)\n\t      \n\t      \n\t      \n\t      ;;\n\t  help|h)\n\t      \n\t      \n\t      \n\t      ;;\n\t  *)\n\t      _gopass_complete_passwords\n\t      ;;\n\tesac\n    else\n\tlocal -a subcommands\n\tsubcommands=(\n\t  \"age:age commands\"\n\t  \"alias:Print domain aliases\"\n\t  \"audit:Decrypt all secrets and scan for weak or leaked passwords\"\n\t  \"cat:Decode and print content of a binary secret to stdout, or encode and insert from stdin\"\n\t  \"clone:Clone a password store from a git repository\"\n\t  \"completion:Bash and ZSH completion\"\n\t  \"config:Display and edit the configuration file\"\n\t  \"convert:Convert a store to different backends\"\n\t  \"copy:Copy secrets from one location to another\"\n\t  \"create:Easy creation of new secrets\"\n\t  \"delete:Remove one or many secrets from the store\"\n\t  \"edit:Edit new or existing secrets\"\n\t  \"env:Run a subprocess with a pre-populated environment\"\n\t  \"find:Search for secrets\"\n\t  \"fsck:Check store integrity, clean up artifacts and possibly re-write secrets\"\n\t  \"fscopy:Copy files from or to the password store\"\n\t  \"fsmove:Move files from or to the password store\"\n\t  \"generate:Generate a new password\"\n\t  \"git:Run a git command inside a password store: gopass git [--store=<store>] <git-command>\"\n\t  \"grep:Search for secrets files containing search-string when decrypted.\"\n\t  \"history:Show password history\"\n\t  \"init:Initialize new password store.\"\n\t  \"insert:Insert a new secret\"\n\t  \"link:Create a symlink\"\n\t  \"list:List existing secrets\"\n\t  \"merge:Merge multiple secrets into one\"\n\t  \"mounts:Edit mounted stores\"\n\t  \"move:Move secrets from one location to another\"\n\t  \"otp:Generate time- or hmac-based tokens\"\n\t  \"process:Process a template file\"\n\t  \"pwgen:Generate passwords\"\n\t  \"rcs:Run a RCS command inside a password store\"\n\t  \"recipients:Edit recipient permissions\"\n\t  \"reorg:Reorganize a password store by editing a text file\"\n\t  \"setup:Initialize a new password store\"\n\t  \"show:Display the content of a secret\"\n\t  \"sum:Compute the SHA256 checksum\"\n\t  \"sync:Sync all local stores with their remotes\"\n\t  \"templates:Edit templates\"\n\t  \"unclip:Internal command to clear clipboard\"\n\t  \"update:Check for updates\"\n\t  \"version:Display version\"\n\t  \"help:Shows a list of commands or help for one command\"\n\t)\n\t_describe -t command 'gopass' subcommands\n\t_arguments : \"--yes[Always answer yes to yes/no questions]\" \"--clip[Copy the password value into the clipboard]\" \"--alsoclip[Copy the password and show everything]\" \"--qr[Print the password as a QR Code]\" \"--qrbody[Print the body as a QR Code]\" \"--unsafe[Display unsafe content (e.g. the password) even if safecontent is enabled]\" \"--safe[Do not display unsafe content (e.g. the password) even if safecontent is disabled]\" \"--password[Display only the password. Takes precedence over all other flags.]\" \"--revision[Show a past revision. Does NOT support RCS specific shortcuts. Use exact revision or -<N> to select the Nth oldest revision of this entry.]\" \"--noparsing[Do not parse the output.]\" \"--nosync[Disable auto-sync]\" \"--chars[Print specific characters from the secret]\" \"--help[show help]\" \"--version[print the version]\" \n\t_gopass_complete_passwords\n    fi\n}\n\n_gopass_complete_keys () {\n    local IFS=$'\\n'\n    _values 'gpg keys' $(gpg2 --list-secret-keys --with-colons 2> /dev/null | cut -d : -f 10 | sort -u | sed '/^$/d')\n}\n\n_gopass_complete_passwords () {\n    local IFS=$'\\n'\n    _arguments : \\\n\t\"--clip[Copy the first line of the secret into the clipboard]\"\n    _values 'passwords' $(gopass ls --flat | sed 's/\\\\/\\\\\\\\\\\\\\\\/g; s/:/\\\\\\\\:/g; s/\\[/\\\\\\\\[/g; s/\\]/\\\\\\\\]/g')\n}\n\n_gopass_complete_folders () {\n    local -a folders\n    folders=(\"${(@f)$(gopass ls --folders --flat)}\")\n    _describe -t folders \"folders\" folders -qS /\n}\n\ncompdef _gopass gopass\n"
  }
]