├── .codeclimate.yml ├── .codecov.yml ├── .errcheck.excl ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── autorelease.yml │ ├── build.yml │ ├── codeql-analysis.yml │ ├── container.yml │ ├── golangci-lint.yml │ ├── grype.yml │ └── scorecard.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .license-lint.yml ├── .revive.toml ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── GOVERNANCE.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── bash.completion ├── docs ├── backends.md ├── backends │ ├── age.md │ ├── fs.md │ ├── gitfs.md │ └── gpg.md ├── commands │ ├── audit.md │ ├── cat.md │ ├── clone.md │ ├── config.md │ ├── convert.md │ ├── create.md │ ├── delete.md │ ├── edit.md │ ├── env.md │ ├── find.md │ ├── fsck.md │ ├── generate.md │ ├── gopass.md │ ├── grep.md │ ├── history.md │ ├── init.md │ ├── insert.md │ ├── link.md │ ├── list.md │ ├── mounts.md │ ├── move.md │ ├── otp.md │ ├── process.md │ ├── pwgen.md │ ├── recipients.md │ ├── show.md │ ├── sync.md │ ├── templates.md │ └── update.md ├── components.dot ├── components.png ├── config.md ├── entropy.md ├── faq.md ├── features.md ├── hacking.md ├── hooks.md ├── logo-small.png ├── logo.ico ├── logo.png ├── logo.svg ├── releases.md ├── secrets.md ├── security.md ├── setup.md ├── showcase.png └── usecases │ ├── gpaste.md │ ├── multi-store.md │ ├── readonly-store.md │ ├── secure-otp.md │ └── secure-otp │ ├── Sign-In.png │ ├── Sign-Up.png │ ├── sign-in.puml │ └── sign-up.puml ├── fish.completion ├── go.mod ├── go.sum ├── gopass.1 ├── helpers ├── changelog │ ├── main.go │ └── main_test.go ├── gitutils │ └── gitutils.go ├── man │ ├── main.go │ └── main_test.go ├── modinfo │ └── main.go ├── msipkg │ └── main.go ├── postrel │ ├── main.go │ └── main_test.go ├── proxy │ ├── Dockerfile.debian │ ├── README-3111.md │ ├── apt.debughttp │ ├── gopass.sources │ └── main.go └── release │ ├── main.go │ └── main_test.go ├── internal ├── action │ ├── action.go │ ├── action_test.go │ ├── aliases.go │ ├── aliases_test.go │ ├── audit.go │ ├── audit_test.go │ ├── binary.go │ ├── binary_test.go │ ├── clihelper.go │ ├── clihelper_test.go │ ├── clone.go │ ├── clone_test.go │ ├── commands.go │ ├── commands_test.go │ ├── completion.go │ ├── completion_test.go │ ├── config.go │ ├── config_test.go │ ├── context.go │ ├── context_test.go │ ├── convert.go │ ├── convert_test.go │ ├── copy.go │ ├── copy_test.go │ ├── create.go │ ├── create_test.go │ ├── delete.go │ ├── delete_test.go │ ├── doc.go │ ├── edit.go │ ├── edit_test.go │ ├── env.go │ ├── env_test.go │ ├── exit │ │ ├── errors.go │ │ └── errors_test.go │ ├── find.go │ ├── find_test.go │ ├── fsck.go │ ├── fsck_test.go │ ├── generate.go │ ├── generate_test.go │ ├── git.go │ ├── grep.go │ ├── grep_test.go │ ├── history.go │ ├── history_test.go │ ├── init.go │ ├── init_test.go │ ├── insert.go │ ├── insert_test.go │ ├── link.go │ ├── link_test.go │ ├── list.go │ ├── list_test.go │ ├── merge.go │ ├── merge_test.go │ ├── mount.go │ ├── mount_test.go │ ├── move.go │ ├── move_test.go │ ├── otp.go │ ├── otp_test.go │ ├── process.go │ ├── process_test.go │ ├── pwgen │ │ ├── commands.go │ │ ├── commands_test.go │ │ ├── pwgen.go │ │ └── pwgen_test.go │ ├── rcs.go │ ├── rcs_test.go │ ├── recipients.go │ ├── recipients_test.go │ ├── reminder.go │ ├── repl.go │ ├── repl_test.go │ ├── setup.go │ ├── setup_test.go │ ├── show.go │ ├── show_test.go │ ├── sync.go │ ├── sync_test.go │ ├── templates.go │ ├── templates_test.go │ ├── unclip.go │ ├── unclip_test.go │ ├── update.go │ ├── update_test.go │ ├── version.go │ └── version_test.go ├── audit │ ├── audit.go │ ├── audit_test.go │ ├── excludes.go │ ├── excludes_test.go │ ├── output.go │ ├── output_test.go │ ├── report.go │ ├── report_test.go │ └── single.go ├── backend │ ├── context.go │ ├── context_test.go │ ├── crypto.go │ ├── crypto │ │ ├── age.go │ │ ├── age │ │ │ ├── age.go │ │ │ ├── age_test.go │ │ │ ├── askpass.go │ │ │ ├── clientUI.go │ │ │ ├── commands.go │ │ │ ├── context.go │ │ │ ├── context_test.go │ │ │ ├── decrypt.go │ │ │ ├── encrypt.go │ │ │ ├── encrypt_test.go │ │ │ ├── identities.go │ │ │ ├── identities_test.go │ │ │ ├── keyring.go │ │ │ ├── loader.go │ │ │ ├── loader_test.go │ │ │ ├── recipients.go │ │ │ ├── recipients_test.go │ │ │ ├── ssh.go │ │ │ └── unsupported.go │ │ ├── doc.go │ │ ├── gpg │ │ │ ├── cli │ │ │ │ ├── decrypt.go │ │ │ │ ├── encrypt.go │ │ │ │ ├── encrypt_test.go │ │ │ │ ├── generate.go │ │ │ │ ├── gpg.go │ │ │ │ ├── gpg_others_test.go │ │ │ │ ├── gpg_test.go │ │ │ │ ├── gpg_windows_test.go │ │ │ │ ├── identities.go │ │ │ │ ├── keyring.go │ │ │ │ ├── keyring_test.go │ │ │ │ ├── loader.go │ │ │ │ ├── recipients.go │ │ │ │ ├── recipients_test.go │ │ │ │ └── version.go │ │ │ ├── colons │ │ │ │ ├── parse_colons.go │ │ │ │ ├── parse_colons_test.go │ │ │ │ ├── parse_fuzz.go │ │ │ │ └── utils.go │ │ │ ├── context.go │ │ │ ├── context_test.go │ │ │ ├── doc.go │ │ │ ├── gpgconf │ │ │ │ ├── binary.go │ │ │ │ ├── binary_others.go │ │ │ │ ├── binary_windows.go │ │ │ │ ├── binary_windows_test.go │ │ │ │ ├── gpgconf.go │ │ │ │ ├── utils.go │ │ │ │ ├── utils_linux.go │ │ │ │ ├── utils_linux_test.go │ │ │ │ ├── utils_others.go │ │ │ │ ├── utils_test.go │ │ │ │ ├── utils_windows.go │ │ │ │ ├── version.go │ │ │ │ └── version_test.go │ │ │ ├── identity.go │ │ │ ├── identity_test.go │ │ │ ├── key.go │ │ │ ├── key_list.go │ │ │ ├── key_list_test.go │ │ │ └── key_test.go │ │ ├── gpgcli.go │ │ ├── plain.go │ │ └── plain │ │ │ ├── backend.go │ │ │ ├── backend_test.go │ │ │ └── loader.go │ ├── crypto_test.go │ ├── doc.go │ ├── rcs.go │ ├── rcs_test.go │ ├── registry.go │ ├── registry_test.go │ ├── storage.go │ ├── storage │ │ ├── doc.go │ │ ├── fossilfs.go │ │ ├── fossilfs │ │ │ ├── context.go │ │ │ ├── context_test.go │ │ │ ├── fossil.go │ │ │ ├── fossil_test.go │ │ │ ├── loader.go │ │ │ ├── loader_test.go │ │ │ ├── settings.go │ │ │ ├── status.go │ │ │ ├── storage.go │ │ │ └── storage_test.go │ │ ├── fs.go │ │ ├── fs │ │ │ ├── fsck.go │ │ │ ├── fsck_test.go │ │ │ ├── link.go │ │ │ ├── link_test.go │ │ │ ├── loader.go │ │ │ ├── rcs.go │ │ │ ├── rcs_test.go │ │ │ ├── store.go │ │ │ ├── store_others.go │ │ │ ├── store_test.go │ │ │ ├── store_windows.go │ │ │ ├── walk.go │ │ │ └── walk_test.go │ │ ├── gitfs.go │ │ └── gitfs │ │ │ ├── commands.go │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── git.go │ │ │ ├── git_test.go │ │ │ ├── loader.go │ │ │ ├── ssh_darwin.go │ │ │ ├── ssh_others.go │ │ │ ├── ssh_windows.go │ │ │ └── storage.go │ └── storage_test.go ├── cache │ ├── disk.go │ ├── disk_test.go │ ├── ghssh │ │ ├── cache.go │ │ ├── cache_test.go │ │ ├── github.go │ │ └── github_test.go │ ├── inmem.go │ └── inmem_test.go ├── completion │ ├── fish │ │ ├── completion.go │ │ ├── completion_test.go │ │ └── template.go │ └── zsh │ │ ├── completion.go │ │ ├── completion_test.go │ │ └── template.go ├── config │ ├── config.go │ ├── config_test.go │ ├── context.go │ ├── docs_test.go │ ├── legacy.go │ ├── legacy │ │ ├── config.go │ │ ├── config_test.go │ │ ├── io.go │ │ ├── io_test.go │ │ ├── legacy.go │ │ ├── location.go │ │ └── location_xdg_test.go │ ├── location.go │ ├── location_test.go │ ├── location_xdg_test.go │ ├── utils.go │ └── utils_test.go ├── create │ ├── helpers.go │ ├── helpers_test.go │ ├── templates.go │ ├── wizard.go │ └── wizard_test.go ├── cui │ ├── actions.go │ ├── actions_test.go │ ├── cui.go │ ├── cui_test.go │ ├── recipients.go │ └── recipients_test.go ├── diff │ ├── diff.go │ └── diff_test.go ├── editor │ ├── edit_linux.go │ ├── edit_others.go │ ├── edit_others_test.go │ ├── edit_test.go │ ├── edit_windows.go │ ├── edit_windows_test.go │ └── editor.go ├── env │ ├── doc.go │ ├── env_darwin.go │ └── env_others.go ├── hashsum │ ├── hashsums.go │ └── hashsums_test.go ├── hook │ └── hook.go ├── notify │ ├── doc.go │ ├── icon.go │ ├── notify_darwin.go │ ├── notify_darwin_test.go │ ├── notify_dbus.go │ ├── notify_others.go │ ├── notify_test.go │ └── notify_windows.go ├── out │ ├── context.go │ ├── context_test.go │ ├── print.go │ └── print_test.go ├── pwschemes │ ├── argon2i │ │ ├── argon2i.go │ │ └── argon2i_test.go │ ├── argon2id │ │ ├── argon2id.go │ │ └── argon2id_test.go │ └── bcrypt │ │ ├── bcrypt.go │ │ └── bcrypt_test.go ├── queue │ ├── background.go │ └── background_test.go ├── recipients │ ├── recipients.go │ └── recipients_test.go ├── reminder │ ├── reminder.go │ └── reminder_test.go ├── store │ ├── err.go │ ├── leaf │ │ ├── context.go │ │ ├── context_test.go │ │ ├── convert.go │ │ ├── crypto.go │ │ ├── crypto_test.go │ │ ├── fsck.go │ │ ├── fsck_test.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── list.go │ │ ├── list_test.go │ │ ├── move.go │ │ ├── move_test.go │ │ ├── rcs.go │ │ ├── rcs_test.go │ │ ├── read.go │ │ ├── recipients.go │ │ ├── recipients_test.go │ │ ├── reencrypt.go │ │ ├── storage.go │ │ ├── store.go │ │ ├── store_test.go │ │ ├── templates.go │ │ ├── templates_test.go │ │ ├── write.go │ │ └── write_test.go │ ├── mockstore │ │ ├── inmem │ │ │ └── store.go │ │ ├── store.go │ │ └── store_test.go │ ├── root │ │ ├── convert.go │ │ ├── crypto.go │ │ ├── crypto_test.go │ │ ├── errors.go │ │ ├── fsck.go │ │ ├── fsck_test.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── link.go │ │ ├── list.go │ │ ├── list_test.go │ │ ├── mount.go │ │ ├── mount_test.go │ │ ├── move.go │ │ ├── move_test.go │ │ ├── rcs.go │ │ ├── rcs_test.go │ │ ├── read.go │ │ ├── read_test.go │ │ ├── recipients.go │ │ ├── recipients_test.go │ │ ├── store.go │ │ ├── store_test.go │ │ ├── templates.go │ │ ├── templates_test.go │ │ ├── write.go │ │ └── write_test.go │ ├── sort.go │ ├── sort_test.go │ └── store.go ├── tpl │ ├── funcs.go │ ├── funcs_test.go │ ├── template.go │ └── template_test.go ├── tree │ ├── node.go │ ├── node_test.go │ ├── root.go │ ├── root_test.go │ ├── tree.go │ └── tree_test.go └── updater │ ├── README.md │ ├── access_others.go │ ├── access_windows.go │ ├── download.go │ ├── extract.go │ ├── extract_test.go │ ├── github.go │ ├── github_test.go │ ├── update.go │ ├── update_test.go │ ├── updateable.go │ ├── verify.go │ └── verify_test.go ├── main.go ├── main_test.go ├── main_unix.go ├── pkg ├── appdir │ ├── appdir.go │ ├── appdir_test.go │ ├── appdir_windows.go │ ├── appdir_xdg.go │ └── appdir_xdg_test.go ├── clipboard │ ├── clipboard.go │ ├── clipboard_others.go │ ├── clipboard_test.go │ ├── clipboard_windows.go │ ├── kill_others.go │ ├── kill_ps.go │ ├── unclip.go │ ├── unclip_linux.go │ ├── unclip_others.go │ └── unclip_test.go ├── ctxutil │ ├── ctxutil.go │ ├── ctxutil_test.go │ └── helper.go ├── debug │ ├── debug.go │ ├── debug_test.go │ ├── doc.go │ ├── version.go │ └── version_test.go ├── fsutil │ ├── fsutil.go │ ├── fsutil_test.go │ ├── umask.go │ └── umask_test.go ├── gopass │ ├── api │ │ ├── api.go │ │ └── api_test.go │ ├── apimock │ │ └── mock.go │ ├── doc.go │ ├── secrets │ │ ├── akv.go │ │ ├── akv_test.go │ │ ├── error.go │ │ ├── ident.go │ │ ├── new.go │ │ ├── secparse │ │ │ ├── .gitignore │ │ │ ├── mime.go │ │ │ ├── parse.go │ │ │ └── parse_test.go │ │ ├── yaml.go │ │ └── yaml_test.go │ └── store.go ├── otp │ ├── otp.go │ ├── otp_test.go │ ├── screenshot_others.go │ └── screenshot_supported.go ├── passkey │ ├── passkey.go │ └── passkey_test.go ├── pinentry │ └── cli │ │ ├── fallback.go │ │ └── fallback_test.go ├── protect │ ├── protect.go │ ├── protect_openbsd.go │ └── protect_test.go ├── pwgen │ ├── cryptic.go │ ├── cryptic_test.go │ ├── external.go │ ├── memorable.go │ ├── pwgen.go │ ├── pwgen_others_test.go │ ├── pwgen_test.go │ ├── pwgen_windows_test.go │ ├── pwrules │ │ ├── aliases.go │ │ ├── aliases_test.go │ │ ├── change.go │ │ ├── change_test.go │ │ ├── gen.go │ │ ├── pwrules.go │ │ ├── pwrules_gen.go │ │ └── pwrules_test.go │ ├── rand.go │ ├── validate.go │ ├── validate_test.go │ ├── wordlist.go │ └── xkcdgen │ │ ├── pwgen.go │ │ └── pwgen_test.go ├── qrcon │ ├── qrcon.go │ └── qrcon_test.go ├── set │ ├── filter.go │ ├── filter_test.go │ ├── map.go │ ├── map_test.go │ ├── set.go │ ├── set_test.go │ ├── sorted.go │ └── sorted_test.go ├── tempfile │ ├── file.go │ ├── file_test.go │ ├── mount_darwin.go │ ├── mount_linux.go │ └── mount_others.go └── termio │ ├── ask.go │ ├── ask_test.go │ ├── context.go │ ├── context_test.go │ ├── identity.go │ ├── identity_test.go │ ├── progress.go │ ├── progress_test.go │ ├── promptpass_others.go │ ├── promptpass_test.go │ ├── promptpass_windows.go │ ├── reader.go │ └── reader_test.go ├── tests ├── audit_test.go ├── binary_test.go ├── can │ ├── can.go │ ├── can_test.go │ └── gnupg │ │ ├── pubring.gpg │ │ ├── random_seed │ │ ├── secring.gpg │ │ └── trustdb.gpg ├── completion_test.go ├── config_test.go ├── copy_test.go ├── delete_test.go ├── find_test.go ├── generate_test.go ├── gptest │ ├── gunit.go │ ├── unit.go │ └── utils.go ├── grep_test.go ├── init_test.go ├── insert_test.go ├── list_test.go ├── mount_test.go ├── move_test.go ├── show_test.go ├── tester.go ├── uninitialized_test.go └── yaml_test.go ├── version.go └── zsh.completion /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | checks: 4 | argument-count: 5 | config: 6 | threshold: 4 7 | complex-logic: 8 | config: 9 | threshold: 4 10 | file-lines: 11 | config: 12 | threshold: 250 13 | method-complexity: 14 | config: 15 | threshold: 16 16 | method-count: 17 | config: 18 | threshold: 20 19 | method-lines: 20 | config: 21 | threshold: 100 22 | nested-control-flow: 23 | config: 24 | threshold: 4 25 | return-statements: 26 | config: 27 | threshold: 4 28 | 29 | plugins: 30 | gofmt: 31 | enabled: true 32 | golint: 33 | enabled: true 34 | govet: 35 | enabled: true 36 | 37 | ratings: 38 | paths: 39 | - "**.go" 40 | 41 | exclude_patterns: 42 | - "vendor/" 43 | - "utils/notify/icon.go" 44 | - "**/*_test.go" 45 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 40..90 3 | round: nearest 4 | precision: 2 5 | status: 6 | project: 7 | default: on 8 | patch: 9 | default: off 10 | changes: 11 | default: off 12 | ignore: 13 | - "vendor/" 14 | -------------------------------------------------------------------------------- /.errcheck.excl: -------------------------------------------------------------------------------- 1 | fmt.Fprintf 2 | fmt.Fprintln 3 | fmt.Fprint 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | CHANGELOG.md merge=union 2 | 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dominikschulz 2 | patreon: gopass 3 | custom: "https://paypal.me/doschulz" 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve gopass 4 | --- 5 | 6 | ### Summary 7 | 10 | 11 | ### Steps To Reproduce 12 | 15 | 16 | ### Expected behavior 17 | 20 | 21 | ### Environment 22 | 25 | 26 | - OS: [e.g. Mac OS X High Sierra, Ubuntu 18.04, Windows 10, ...] 27 | - OS version: [uname -a] 28 | - gopass Version: [gopass version] 29 | - Installation method: [e.g. from source, brew, gopass repo] 30 | 31 | 39 | 40 | ### Additional context 41 | 44 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | open-pull-requests-limit: 15 9 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 120 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 60 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | # Set to true to ignore issues in a milestone (defaults to false) 19 | exemptMilestones: true 20 | -------------------------------------------------------------------------------- /.github/workflows/grype.yml: -------------------------------------------------------------------------------- 1 | name: Scan gopass 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | linux: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Harden Runner 23 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 24 | with: 25 | egress-policy: audit 26 | 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | fetch-depth: 0 30 | - name: Set up Go 31 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 32 | with: 33 | go-version: '1.24' 34 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 35 | with: 36 | path: ~/go/pkg/mod 37 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 38 | restore-keys: | 39 | ${{ runner.os }}-go- 40 | - name: Scan current project 41 | uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0 42 | with: 43 | path: "." 44 | fail-build: true 45 | severity-cutoff: critical 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gopass 2 | gopass-*-amd64 3 | gopass-full 4 | dev.sh 5 | !pkg/gopass/ 6 | coverage.out 7 | coverage-all.* 8 | .vscode/ 9 | 10 | # Profiling 11 | *.out 12 | 13 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 14 | *.o 15 | *.a 16 | *.so 17 | 18 | # Folders 19 | _obj 20 | _test 21 | 22 | # Architecture specific extensions/prefixes 23 | *.[568vq] 24 | [568vq].out 25 | 26 | *.cgo1.go 27 | *.cgo2.c 28 | _cgo_defun.c 29 | _cgo_gotypes.go 30 | _cgo_export.* 31 | 32 | _testmain.go 33 | 34 | *.exe 35 | *.test 36 | *.prof 37 | 38 | # gopass specific ignores 39 | *.sublime-* 40 | *.swp 41 | /.env 42 | 43 | # package files 44 | *.deb 45 | *.pkg.tar.xz 46 | *.rpm 47 | *.tar.bz2 48 | 49 | releases/ 50 | dist/ 51 | 52 | manifest-*.json 53 | 54 | # go-fuzz 55 | *-fuzz.zip 56 | workdir/ 57 | 58 | .vscode/ 59 | NOTICE.new 60 | 61 | debian/ 62 | -------------------------------------------------------------------------------- /.license-lint.yml: -------------------------------------------------------------------------------- 1 | unrestricted_licenses: 2 | - Apache-2.0 3 | - MIT 4 | - BSD-3-Clause 5 | - BSD-2-Clause 6 | - 0BSD 7 | - WTFPL 8 | - CC0-1.0 9 | reciprocal_licenses: 10 | - MPL-2.0 11 | - MPL-2.0-no-copyleft-exception 12 | allowlisted_modules: 13 | # Simplified BSD (BSD-2-Clause): https://github.com/russross/blackfriday/blob/master/LICENSE.txt 14 | - github.com/russross/blackfriday 15 | - github.com/russross/blackfriday/v2 16 | # Apache license 17 | - github.com/dgraph-io/ristretto 18 | - github.com/spf13/afero 19 | # Modified BSD-2-Clause with extra no-Google clause: https://github.com/jezek/xgb/blob/master/LICENSE 20 | - github.com/jezek/xgb 21 | # MIT 22 | - github.com/jwalton/go-supportscolor -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | # Ignores files with "GENERATED" header, similar to golint 2 | ignoreGeneratedHeader = false 3 | 4 | # Sets the default severity to "warning" 5 | severity = "error" 6 | 7 | # Sets the default failure confidence. This means that linting errors 8 | # with less than 0.8 confidence will be ignored. 9 | confidence = 0.6 10 | 11 | # Sets the error code for failures with severity "error" 12 | errorCode = 1 13 | 14 | # Sets the error code for failures with severity "warning" 15 | warningCode = 1 16 | 17 | [rule.argument-limit] 18 | arguments = [10] 19 | [rule.blank-imports] 20 | [rule.context-as-argument] 21 | [rule.context-keys-type] 22 | [rule.cyclomatic] 23 | arguments = [21] 24 | [rule.dot-imports] 25 | [rule.error-naming] 26 | [rule.error-return] 27 | [rule.error-strings] 28 | [rule.errorf] 29 | [rule.exported] 30 | [rule.if-return] 31 | [rule.increment-decrement] 32 | [rule.indent-error-flow] 33 | [rule.package-comments] 34 | [rule.range] 35 | [rule.receiver-naming] 36 | [rule.time-naming] 37 | [rule.unexported-return] 38 | [rule.var-declaration] 39 | [rule.var-naming] 40 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # gopass project governance 2 | 3 | ## Overview 4 | 5 | The gopass project uses a governance model commonly described as Benevolent 6 | Dictator For Life (BDFL). This document outlines our understanding of what this 7 | means. It is derived from the [i3 window manager project 8 | governance](https://raw.githubusercontent.com/i3/i3/next/.github/GOVERNANCE.md). 9 | 10 | ## Roles 11 | 12 | * user: anyone who interacts with the gopass project 13 | * core contributor: a handful of people who have contributed significantly to 14 | the project by any means (issue triage, support, documentation, code, etc.). 15 | Core contributors are recognizable via GitHub’s “Member” badge. 16 | * Benevolent Dictator For Life (BDFL): a single individual who makes decisions 17 | when consensus cannot be reached. gopass’s current BDFL is [@dominikschulz](https://github.com/dominikschulz). 18 | 19 | ## Decision making process 20 | 21 | In general, we try to reach consensus in discussions. In case consensus cannot 22 | be reached, the BDFL makes a decision. 23 | 24 | ## Contribution process 25 | 26 | Please see [CONTRIBUTING](CONTRIBUTING.md). 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2017 JustWatch GmbH 4 | 5 | Permission 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE 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. 10 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.15.16 2 | -------------------------------------------------------------------------------- /bash.completion: -------------------------------------------------------------------------------- 1 | _gopass_bash_autocomplete() { 2 | local cur opts base 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) 6 | local IFS=$'\n' 7 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 8 | return 0 9 | } 10 | 11 | complete -F _gopass_bash_autocomplete gopass 12 | -------------------------------------------------------------------------------- /docs/backends/fs.md: -------------------------------------------------------------------------------- 1 | # fs storage backend 2 | 3 | The simplest storage backend, often used for testing. 4 | It stores data directly in the filesystem without any RCS support. 5 | -------------------------------------------------------------------------------- /docs/backends/gitfs.md: -------------------------------------------------------------------------------- 1 | # `gitfs` storage backend 2 | 3 | This 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. 4 | 5 | gopass configures git to use persistent ssh connections. If you do not want 6 | this set `GIT_SSH_COMMAND` to an empty string to override the built-in default. 7 | -------------------------------------------------------------------------------- /docs/commands/audit.md: -------------------------------------------------------------------------------- 1 | # `audit` command 2 | 3 | The `audit` command will decrypt all secrets and scan for weak passwords or other common flaws. 4 | 5 | ## Synopsis 6 | 7 | ``` 8 | $ gopass audit 9 | ``` 10 | 11 | ## Excludes 12 | 13 | You 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: 14 | 15 | ``` 16 | # Lines starting with # are ignored. Trailing comments are not supported. 17 | # Exclude all secrets in the pin folder. 18 | # Note: These are RE2, not Glob patterns! 19 | pin/.* 20 | # Literal matches are also valid RE2 patterns 21 | test_folder/ignore_this 22 | # Gopass internally uses forward slashes as path separators, even on Windows. So no need to escape backslashes. 23 | ``` 24 | 25 | ## Password strength backends 26 | 27 | | Backend | Description | 28 | |-------------------------------------------------|------------------------------------------------------------------------| 29 | | [`crunchy`](https://github.com/muesli/crunchy) | Crunchy password strength checker | 30 | | `name` | Checks if password equals the name of the secret | 31 | -------------------------------------------------------------------------------- /docs/commands/clone.md: -------------------------------------------------------------------------------- 1 | # `clone` command 2 | 3 | The `clone` command allows cloning and setting up a new password store 4 | from a remote location, e.g. a remote git repo. 5 | 6 | ## Synopsis 7 | 8 | ``` 9 | $ gopass clone git@example.com/store.git 10 | $ gopass clone git@example.com/store.git sub/store 11 | ``` 12 | 13 | ## Flags 14 | 15 | | Flag | Aliases | Description | 16 | |------------|---------|-----------------------------------------------------------------| 17 | | `--path` | | The path to clone the repo to. | 18 | | `--crypto` | | Override the crypto backend to use if the auto-detection fails. | 19 | -------------------------------------------------------------------------------- /docs/commands/config.md: -------------------------------------------------------------------------------- 1 | # `config` command 2 | 3 | The config command allows displaying and altering configuration options. 4 | 5 | Note: To manage mounts use `gopass mounts`. 6 | 7 | ## Synopsis 8 | 9 | ```bash 10 | gopass config 11 | gopass config generate.autoclip 12 | gopass config generate.autoclip false 13 | ``` 14 | 15 | ## Flags 16 | 17 | | Flag | Description | 18 | |-----------|--------------------------------| 19 | | `--store` | Only sync a specific sub store | 20 | -------------------------------------------------------------------------------- /docs/commands/convert.md: -------------------------------------------------------------------------------- 1 | # `convert` command 2 | 3 | The `convert` command exists to migrate stores between different backend 4 | implementations. 5 | 6 | Note: This command exists to enable a possible migration path. If we agree 7 | on a single set of backend implementations the multiple backend support 8 | might go away and this command as well. 9 | 10 | Warning: 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. 11 | 12 | ## Synopsis 13 | 14 | ``` 15 | $ gopass convert --store=foo --move=true --storage=gitfs --crypto=age 16 | $ gopass convert --store=bar --move=false --storage=fs --crypto=plain 17 | ``` 18 | 19 | ## Flags 20 | 21 | Flag | Description 22 | ---- | ----------- 23 | `--store` | Substore to convert. 24 | `--move` | Remove backup after converting? (default: `false`) 25 | `--storage` | Target storage backend. 26 | `--crypto` | Target crypto backend. 27 | -------------------------------------------------------------------------------- /docs/commands/delete.md: -------------------------------------------------------------------------------- 1 | # `delete` command 2 | 3 | The `delete` command is used to remove a single secret or a whole subtree. 4 | 5 | Note: Recursive operations crossing mount points are intentionally not supported. 6 | 7 | ## Synopsis 8 | 9 | ``` 10 | $ gopass delete entry 11 | $ gopass rm -r path/to/folder 12 | $ gopass rm -f entry 13 | $ gopass delete entry key 14 | ``` 15 | 16 | ## Modes of operation 17 | 18 | * Delete a single secret 19 | * Delete a single key from an existing secret 20 | * Delete a directoy of secrets 21 | 22 | ## Flags 23 | 24 | | Flag | Aliases | Description | 25 | |---------------|---------|---------------------------------------| 26 | | `--recursive` | `-r` | Recursively delete files and folders. | 27 | | `--force` | `-f` | Do not ask for confirmation. | 28 | 29 | ## Details 30 | 31 | * Removing a single key will need to decrypt the secret 32 | -------------------------------------------------------------------------------- /docs/commands/env.md: -------------------------------------------------------------------------------- 1 | # `env` command 2 | 3 | The `env` command runs a binary as a subprocess with a pre-populated environment. 4 | The environment of the subprocess is populated with a set of environment variables corresponding 5 | to the secret subtree specified on the command line. 6 | 7 | ## Synopsis 8 | 9 | ``` 10 | $ gopass env entry env 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /docs/commands/find.md: -------------------------------------------------------------------------------- 1 | # `find` command 2 | 3 | The `find` command will attempt to do a simple substring match on the names of all secrets. 4 | If there is a single match it will directly invoke `show` and display the result. 5 | If there are multiple matches a selection will be shown. 6 | 7 | Note: The find command will not fall back to a fuzzy search. 8 | 9 | ## Synopsis 10 | 11 | ``` 12 | $ gopass find entry 13 | $ gopass find -f entry 14 | $ gopass find -c entry 15 | ``` 16 | 17 | ## Flags 18 | 19 | | Flag | Aliases | Description | 20 | |------------|---------|---------------------------------------------------------------| 21 | | `--clip` | `-c` | Copy the password into the clipboard. | 22 | | `--unsafe` | `-u` | Display any unsafe content, even if `safecontent` is enabled. | 23 | 24 | -------------------------------------------------------------------------------- /docs/commands/fsck.md: -------------------------------------------------------------------------------- 1 | # `fsck` command 2 | 3 | `gopass` can check integrity of it's password stores with the `fsck` command. 4 | It will ensure proper file and directory permissions as well as proper 5 | recipient coverage (on supported crypto backends, only). 6 | 7 | ## Synopsis 8 | 9 | ``` 10 | $ gopass fsck 11 | ``` 12 | 13 | ## Modes of operation 14 | 15 | * Check the entire password store, incl. all mounts 16 | * Check only the specified mount 17 | 18 | ## Flags 19 | 20 | Flag | Aliases | Description 21 | ---- | ------- | ----------- 22 | `--decrypt` | | Decrypt and reencrypt all secrets. 23 | -------------------------------------------------------------------------------- /docs/commands/grep.md: -------------------------------------------------------------------------------- 1 | # `grep` command 2 | 3 | The `grep` command works like the Unix `grep` tool. It decrypts all secrets 4 | and performs a substring or regexp match on the given pattern. 5 | 6 | ## Synopsis 7 | 8 | ``` 9 | $ gopass grep foobar 10 | ``` 11 | 12 | ## Modes of operations 13 | 14 | * Search for the given pattern in all secrets 15 | 16 | ## Flags 17 | 18 | None. 19 | Flag | Aliases | Description 20 | ---- | ------- | ----------- 21 | `--regexp` | | Parse the pattern as a RE2 regular expression. 22 | -------------------------------------------------------------------------------- /docs/commands/history.md: -------------------------------------------------------------------------------- 1 | # `history` command 2 | 3 | The `gopass history` command will show all revisions of a given secret. 4 | 5 | ## Synopsis 6 | 7 | ``` 8 | $ gopass history entry 9 | ``` 10 | 11 | ## Modes of operation 12 | 13 | * Display all revisions of the given secret. 14 | 15 | ## Flags 16 | 17 | None. 18 | -------------------------------------------------------------------------------- /docs/commands/link.md: -------------------------------------------------------------------------------- 1 | # `link` command 2 | 3 | The `link` (or `ln`) command is used to create a symlink from one secret in a 4 | store to a target in the same store. 5 | 6 | Note: Symlinks across different stores / mounts are currently not supported! 7 | 8 | Note: `audit` and `list` do not recognize symlinks, yet. They will treat 9 | symlinks as regular (different) entries. 10 | 11 | ## Synopsis 12 | 13 | ``` 14 | $ gopass ln foo/bar bar/baz 15 | $ gopass show foo/bar 16 | $ gopass show bar/baz 17 | ``` 18 | 19 | ## Modes of operations 20 | 21 | * Create a symlink from an existing secret to a new name, the target must not exist, yet 22 | 23 | Note: Use `gopass rm` to remove a symlink. 24 | 25 | ## Flags 26 | 27 | None. 28 | 29 | -------------------------------------------------------------------------------- /docs/commands/mounts.md: -------------------------------------------------------------------------------- 1 | # `mounts` commands 2 | 3 | The `mounts` commands allow managing mounted substores. This is one of the 4 | distinctive core features of `gopass` and we aim making working with substores 5 | as seamless as possible. 6 | 7 | Instead of support for encrypting different parts of a store for different 8 | recipients we instead encourage users to mount different stores - each 9 | encrypted to a uniform set of recipients - into a semless virtual tree structure. 10 | 11 | This feature is modeled after standard POSIX mount semantics. 12 | 13 | ## Synopsis 14 | 15 | ``` 16 | $ gopass mounts 17 | $ gopass mounts add mount/point /path/to/store 18 | $ gopass mounts remove mount/point 19 | ``` 20 | 21 | ## Modes of operation 22 | 23 | * Add a new mount 24 | * List existing mounts 25 | * Remove an existing mount 26 | 27 | ## Creating new mounts 28 | 29 | You can also create new mounts using `init` even if your store is already initialized: 30 | 31 | ``` 32 | gopass init --store mynewsubstore pgpkeyidentitfier 33 | ``` 34 | 35 | (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.) 36 | -------------------------------------------------------------------------------- /docs/commands/otp.md: -------------------------------------------------------------------------------- 1 | # `otp` command 2 | 3 | The `otp` command generates TOTP tokens from an OTP URL (`otpauth://`). 4 | The command tries to parse the password and the totp fields as an OTP URL. 5 | 6 | Note: HTOP is currently not supported. 7 | 8 | Note: If `show.safecontent` is enabled, OTP URLs are hidden from the `show` command. 9 | 10 | ## Modes of operation 11 | 12 | * Generate the current TOTP token from a valid OTP URL 13 | * Snip the screen to add a TOTP QR code as an OTP field to an entry. 14 | 15 | ## Flags 16 | 17 | | Flag | Aliases | Description | 18 | |--------------|---------|--------------------------------------------------------------------------| 19 | | `--clip` | `-c` | Copy the time-based token into the clipboard. | 20 | | `--alsoclip` | `-C` | Copy the time-based token into the clipboard and show it. | 21 | | `--qr` | `-q` | Write QR code to file. | 22 | | `--chained` | `-p` | chain the token to the password | 23 | | `--password` | `-o` | Only display the token. For use in scripts. | 24 | | `--snip` | `-s` | Try and find a QR code in the screen content to add as OTP to the entry. | 25 | -------------------------------------------------------------------------------- /docs/commands/pwgen.md: -------------------------------------------------------------------------------- 1 | # `pwgen` command 2 | 3 | The `pwgen` command implements a subset of the features of the Unix/Linux 4 | `pwgen` command line tool. It aims to eventually support most of the `pwgen` 5 | flags and mirror it's behaviour. It is mainly implemented as a curtosy for 6 | Windows users. 7 | 8 | ## Modes of operation 9 | 10 | * Generate a few dozen random passwords with the chosen length 11 | 12 | ## Usage 13 | 14 | ```bash 15 | gopass pwgen [optional length] 16 | ``` 17 | 18 | ## Synopsis 19 | 20 | ```bash 21 | gopass pwgen 22 | gopass pwgen 24 23 | ``` 24 | 25 | ## Flags 26 | 27 | Flag | Aliases | Description 28 | ---- | ------- | ----------- 29 | `--no-numerals` | `-0` | Do not include numerals in the generated passwords. 30 | `--one-per-line` | `-1` | Print one password per line. 31 | `--xkcd` | `-x` | Use multiple random english words combined to a password. 32 | `--sep` | `--xs` | Word separator for multi-word passwords. 33 | `--lang` | `--xl` | Language to generate password from. Currently only supports english (en, default). 34 | -------------------------------------------------------------------------------- /docs/commands/sync.md: -------------------------------------------------------------------------------- 1 | # `sync` command 2 | 3 | The `sync` command is the preferred way to manually synchronize changes between 4 | your local stores and any configured remotes. 5 | 6 | You can also `cd` into a git-based store and manually perform git operations, 7 | or use the `gopass git` command to automatically run a command in the correct 8 | directory. 9 | 10 | Note: `gopass sync` only supports one remote per store. 11 | 12 | ## Flags 13 | 14 | | Flag | Description | 15 | |-----------|--------------------------------| 16 | | `--store` | Only sync a specific sub store | 17 | -------------------------------------------------------------------------------- /docs/commands/update.md: -------------------------------------------------------------------------------- 1 | # `update` command 2 | 3 | The `update` command will attempt to auto-update `gopass` by downloading the 4 | latest release from GitHub. It performs several pre-flight checks in order to 5 | determine if the binary can be updated or not (e.g. if managed by a package 6 | manager). 7 | 8 | ## Synopsis 9 | 10 | ``` 11 | $ gopass update 12 | $ gopass update --pre 13 | ``` 14 | 15 | ## Flags 16 | 17 | Flag | Description 18 | ---- | ----------- 19 | `--pre` | Update to pre-releases / release candidates (default: `false`). 20 | -------------------------------------------------------------------------------- /docs/components.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | gopass [shape=box,style=filled,color=".2 .2 .6",peripheries=2]; 3 | gopass -> action; 4 | action [label="internal/action"]; 5 | action -> root; 6 | root [label="internal/store/root"]; 7 | root -> leaf; 8 | root -> tree; 9 | tree [label="internal/tree"]; 10 | leaf [label="internal/store/leaf"]; 11 | leaf -> gitfs; 12 | gitfs [label="internal/backend/storage/gitfs"]; 13 | gitfs -> gitcli; 14 | gitcli [label="git binary",shape=Mdiamond]; 15 | leaf -> gpg; 16 | gpg [label="internal/backend/crypto/gpg/cli"]; 17 | leaf -> age [style="dotted"]; 18 | age [label="internal/backend/crypto/age"]; 19 | gpg -> gpgcli; 20 | gpgcli [label="gpg/gpg2 binary",shape=Mdiamond]; 21 | leaf -> secret; 22 | secret [label="pkg/gopass/secrets"]; 23 | secret -> root; 24 | jsonapi [label="gopass-jsonapi",shape=box]; 25 | jsonapi -> api; 26 | api [label="pkg/gopass/api"]; 27 | api -> root; 28 | api -> config; 29 | gopass -> config; 30 | config [label="internal/config"]; 31 | summon -> api; 32 | summon [label="gopass-summon-provider",shape=box]; 33 | hibp -> api; 34 | hibp [label="gopass-hibp",shape=box]; 35 | hibp -> pkghibp; 36 | pkghibp [label="pkg/hibp"]; 37 | gitcreds -> api; 38 | gitcreds [label="git-credential-gopass",shape=box]; 39 | } 40 | -------------------------------------------------------------------------------- /docs/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/components.png -------------------------------------------------------------------------------- /docs/entropy.md: -------------------------------------------------------------------------------- 1 | # Entropy 2 | 3 | Generating cryptographic keys needs a lot of entropy. Especially `gnupg --gen-key` 4 | depletes the kernel entropy pool (`/dev/random`) quite fast and may appear to be 5 | stuck when it's waiting for new entropy. 6 | 7 | If you wonder how to speed this up consider installing `rng-tools` 8 | if this is available on your platform. 9 | 10 | After installing `rng-tools` please make sure `rngd` is actually running and 11 | replenishing your entropy pool. 12 | 13 | You can do so by keeping a watch on your available entropy and running an entropy 14 | consuming process as follows: 15 | 16 | ```bash 17 | watch -n1 cat /proc/sys/kernel/random/entropy_avail 18 | # switch to another terminal / screen 19 | cat /dev/random | rngtest -c 1000 20 | ``` 21 | 22 | The second command should complete within a few seconds and report no errors. 23 | If it takes much longer you probably don't have an hardware RNG and will have 24 | to generate some entropy by triggering some network activity and input. 25 | 26 | You should avoid `havaged`. 27 | 28 | ### Debian / Ubuntu 29 | 30 | ```bash 31 | sudo apt-get install rng-tools 32 | ``` 33 | 34 | ### CentOS / Fedora / Red Hat 35 | 36 | ```bash 37 | sudo yum install rng-tools 38 | ``` 39 | 40 | ## Further Information 41 | 42 | * [RNG-Tools on the Arch Linux Wiki](https://wiki.archlinux.org/index.php/Rng-tools) 43 | * [gopass Issue #486](https://github.com/gopasspw/gopass/issues/486) 44 | -------------------------------------------------------------------------------- /docs/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/logo-small.png -------------------------------------------------------------------------------- /docs/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/logo.ico -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/logo.png -------------------------------------------------------------------------------- /docs/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/showcase.png -------------------------------------------------------------------------------- /docs/usecases/multi-store.md: -------------------------------------------------------------------------------- 1 | # Use case: Multiple Stores 2 | 3 | `gopass` aims to support up to 100 mounted substores without noticeable 4 | impact on most operations. 5 | 6 | Using multiple stores is the preferred approach to solving different tasks 7 | like encrypting different sets of secrets for different recipients (as 8 | opposed to e.g. recipient lists in sub directories). 9 | 10 | We understand that being able to use multiple stores is a key features of 11 | `gopass` and we commit to maintaining and improving this feature in the 12 | long term. 13 | 14 | -------------------------------------------------------------------------------- /docs/usecases/secure-otp/Sign-In.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/usecases/secure-otp/Sign-In.png -------------------------------------------------------------------------------- /docs/usecases/secure-otp/Sign-Up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/docs/usecases/secure-otp/Sign-Up.png -------------------------------------------------------------------------------- /docs/usecases/secure-otp/sign-in.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | title Sign-In with local otp 3 | 4 | actor User 5 | database "Git-Passwordstore" 6 | database "Local-Passwordstore" 7 | database "gpg-key" 8 | 9 | activate User 10 | 11 | activate "Git-Passwordstore" 12 | User -> "Git-Passwordstore": show (login,password) 13 | "Git-Passwordstore" -> "gpg-key": decrypt 14 | "gpg-key" -> User: enter passphrase 15 | deactivate "Git-Passwordstore" 16 | 17 | activate Website 18 | User -> Website: sign-in (login,password) 19 | Website -> User: request: otp-code 20 | 21 | User -> "Local-Passwordstore": otp 22 | activate "Local-Passwordstore" 23 | "Local-Passwordstore" -> "gpg-key": decrypt(otp-token) 24 | "Local-Passwordstore" -> "Local-Passwordstore": generate otp-code (otp-token,local-time) 25 | deactivate "Local-Passwordstore" 26 | 27 | User -> Website: enter (otp-code) 28 | Website -> Website: validate (otp-code,website-time,otp-token) 29 | User <-- Website: success 30 | 31 | @enduml 32 | -------------------------------------------------------------------------------- /docs/usecases/secure-otp/sign-up.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | title Sign-Up with local otp 3 | 4 | actor User 5 | database "Git-Passwordstore" 6 | database "Local-Passwordstore" 7 | database "gpg-key" 8 | 9 | activate User 10 | activate Website 11 | User -> Website: sign-up(login,password) 12 | Website -> Website: create otp-token 13 | User <-- Website: show otp-token 14 | deactivate Website 15 | 16 | User -> "Git-Passwordstore": store (login,password) for Website 17 | "Git-Passwordstore" -> "gpg-key": encrypt 18 | 19 | User -> "Local-Passwordstore": store (otp-token) for Website 20 | "Local-Passwordstore" -> "gpg-key": encrypt 21 | 22 | 23 | @enduml -------------------------------------------------------------------------------- /helpers/changelog/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The gopass Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license, 3 | // that can be found in the LICENSE file. 4 | 5 | // Changelog implements the changelog extractor that is called by the autorelease GitHub action 6 | // and used to extract the changelog from the CHANGELOG.md file. It's content is then used to 7 | // populate the release description on GitHub. 8 | // 9 | // This tool will extract every line between the first and the second subheading (##). 10 | // This way the changelog can have a common header under the top most heading (#) and we 11 | // still only get the content of the latest release in the GitHub release notes. 12 | package main 13 | 14 | import ( 15 | "bufio" 16 | "fmt" 17 | "os" 18 | "strings" 19 | ) 20 | 21 | var filename = "CHANGELOG.md" 22 | 23 | func main() { 24 | fh, err := os.Open(filename) 25 | if err != nil { 26 | panic(err) 27 | } 28 | defer fh.Close() 29 | 30 | s := bufio.NewScanner(fh) 31 | var in bool 32 | for s.Scan() { 33 | line := s.Text() 34 | if strings.HasPrefix(line, "## ") { 35 | if in { 36 | break 37 | } 38 | in = true 39 | } 40 | 41 | if !in { 42 | continue 43 | } 44 | 45 | fmt.Println(line) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /helpers/changelog/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "os" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestMain(t *testing.T) { 14 | // Create a temporary file 15 | tmpfile, err := os.CreateTemp("", "changelog_test_*.md") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer os.Remove(tmpfile.Name()) 20 | 21 | // Write test data to the temporary file 22 | content := `# Changelog 23 | ## [1.0.1] - 2021-01-01 24 | ### Added 25 | - New feature 26 | 27 | ## [1.0.0] - 2020-12-31 28 | ### Added 29 | - Initial release 30 | ` 31 | if _, err := tmpfile.Write([]byte(content)); err != nil { 32 | t.Fatal(err) 33 | } 34 | if err := tmpfile.Close(); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | // Override the global variable filename 39 | filename = tmpfile.Name() 40 | 41 | // Capture the output 42 | old := os.Stdout 43 | r, w, _ := os.Pipe() 44 | os.Stdout = w 45 | 46 | main() 47 | 48 | w.Close() 49 | os.Stdout = old 50 | 51 | var output strings.Builder 52 | scanner := bufio.NewScanner(r) 53 | for scanner.Scan() { 54 | output.WriteString(scanner.Text() + "\n") 55 | } 56 | 57 | expected := `## [1.0.1] - 2021-01-01 58 | ### Added 59 | - New feature 60 | 61 | ` 62 | if output.String() != expected { 63 | t.Errorf("expected %q, got %q", expected, output.String()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /helpers/modinfo/main.go: -------------------------------------------------------------------------------- 1 | // modinfo a small helper to print the build info and module versions. 2 | // 3 | // Test builds don't have build info, so this will only work in a real build. 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | rd "runtime/debug" 9 | 10 | _ "github.com/blang/semver/v4" 11 | "github.com/gopasspw/gopass/pkg/debug" 12 | ) 13 | 14 | func main() { 15 | info, ok := rd.ReadBuildInfo() 16 | if !ok { 17 | panic("could not read build info") 18 | } 19 | 20 | fmt.Printf("Build Info: %+v\n", info) 21 | 22 | for _, v := range []string{ 23 | "github.com/blang/semver/v4", 24 | "github.com/gopasspw/gopass/internal/backend/storage/fs", 25 | } { 26 | mv := debug.ModuleVersion(v) 27 | fmt.Printf("Module Version: %s %s\n", v, mv) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /helpers/proxy/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | RUN apt update && apt install -y \ 4 | sudo \ 5 | curl \ 6 | iproute2 \ 7 | iputils-ping \ 8 | vim 9 | RUN curl -L -q https://packages.gopass.pw/repos/gopass/gopass-archive-keyring.gpg | sudo tee /usr/share/keyrings/gopass-archive-keyring.gpg 10 | ADD helpers/proxy/apt.debughttp /etc/apt/apt.conf.d/99debughttp 11 | ADD helpers/proxy/gopass.sources /etc/apt/sources.list.d/gopass.sources 12 | 13 | CMD /bin/bash 14 | -------------------------------------------------------------------------------- /helpers/proxy/README-3111.md: -------------------------------------------------------------------------------- 1 | # Debugging Issue 3111 2 | 3 | - Run the proxy on the host: `go run helpers/proxy/main.go` 4 | - Turn off the firewall / open the port! 5 | - Modify apt.debughttp and replace the HOST with the IP of the Docker host 6 | - `docker build -t debian:gopass -f helpers/proxy/Dockerfile.debian .` 7 | - `docker run --rm -ti debian:gopass` 8 | - Inside the container: 9 | - `apt update` 10 | -------------------------------------------------------------------------------- /helpers/proxy/apt.debughttp: -------------------------------------------------------------------------------- 1 | Debug::Acquire::http "true"; 2 | Debug::Acquire::https "true"; 3 | Debug::pkgAcquire "true"; 4 | Acquire::http::Proxy "http://HOST:8080/"; 5 | -------------------------------------------------------------------------------- /helpers/proxy/gopass.sources: -------------------------------------------------------------------------------- 1 | Types: deb 2 | URIs: https://packages.gopass.pw/repos/gopass 3 | Suites: stable 4 | Architectures: all amd64 arm64 armhf 5 | Components: main 6 | Signed-By: /usr/share/keyrings/gopass-archive-keyring.gpg 7 | -------------------------------------------------------------------------------- /internal/action/aliases.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/gopasspw/gopass/internal/out" 8 | "github.com/gopasspw/gopass/pkg/pwgen/pwrules" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | // AliasesPrint prints all configured aliases. 13 | func (s *Action) AliasesPrint(c *cli.Context) error { 14 | out.Printf(c.Context, "Configured aliases:") 15 | aliases := pwrules.AllAliases(c.Context) 16 | keys := make([]string, 0, len(aliases)) 17 | for k := range aliases { 18 | keys = append(keys, k) 19 | } 20 | 21 | sort.Strings(keys) 22 | for _, k := range keys { 23 | out.Printf(c.Context, "- %s -> %s", k, strings.Join(aliases[k], ", ")) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/action/aliases_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/gopasspw/gopass/tests/gptest" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestAliases(t *testing.T) { 16 | u := gptest.NewUnitTester(t) 17 | 18 | ctx := config.NewContextInMemory() 19 | ctx = ctxutil.WithAlwaysYes(ctx, true) 20 | ctx = ctxutil.WithHidden(ctx, true) 21 | act, err := newMock(ctx, u.StoreDir("")) 22 | require.NoError(t, err) 23 | require.NotNil(t, act) 24 | ctx = act.cfg.WithConfig(ctx) 25 | 26 | buf := &bytes.Buffer{} 27 | out.Stdout = buf 28 | stdout = buf 29 | defer func() { 30 | out.Stdout = os.Stdout 31 | stdout = os.Stdout 32 | }() 33 | 34 | require.NoError(t, act.AliasesPrint(gptest.CliCtx(ctx, t))) 35 | } 36 | -------------------------------------------------------------------------------- /internal/action/clihelper.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | type argList []string 10 | 11 | func (a argList) Get(n int) string { 12 | if len(a) > n { 13 | return a[n] 14 | } 15 | 16 | return "" 17 | } 18 | 19 | func parseArgs(c *cli.Context) (argList, map[string]string) { 20 | args := make(argList, 0, c.Args().Len()) 21 | kvps := make(map[string]string, c.Args().Len()) 22 | if c.Args().Len() == 1 { 23 | // If there is only one arg, assume it is 24 | // the secret name, so don't attempt to 25 | // parse into args and kvps 26 | args = append(args, c.Args().Get(0)) 27 | 28 | return args, kvps 29 | } 30 | OUTER: 31 | for _, arg := range c.Args().Slice() { 32 | for _, sep := range []string{":", "="} { 33 | if !strings.Contains(arg, sep) { 34 | continue 35 | } 36 | p := strings.Split(arg, sep) 37 | if len(p) < 2 { 38 | args = append(args, arg) 39 | 40 | continue OUTER 41 | } 42 | key := p[0] 43 | kvps[key] = strings.Join(p[1:], ":") 44 | 45 | continue OUTER 46 | } 47 | args = append(args, arg) 48 | } 49 | 50 | return args, kvps 51 | } 52 | -------------------------------------------------------------------------------- /internal/action/commands_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/tests/gptest" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | func testCommand(t *testing.T, cmd *cli.Command) { 15 | t.Helper() 16 | 17 | if len(cmd.Subcommands) < 1 { 18 | assert.NotNil(t, cmd.Action, cmd.Name) 19 | } 20 | 21 | assert.NotEmpty(t, cmd.Usage) 22 | assert.NotEmpty(t, cmd.Description) 23 | 24 | for _, flag := range cmd.Flags { 25 | switch v := flag.(type) { 26 | case *cli.StringFlag: 27 | assert.NotContains(t, v.Name, ",") 28 | assert.NotEmpty(t, v.Usage) 29 | case *cli.BoolFlag: 30 | assert.NotContains(t, v.Name, ",") 31 | assert.NotEmpty(t, v.Usage) 32 | } 33 | } 34 | 35 | for _, scmd := range cmd.Subcommands { 36 | testCommand(t, scmd) 37 | } 38 | } 39 | 40 | func TestCommands(t *testing.T) { 41 | u := gptest.NewUnitTester(t) 42 | 43 | ctx := config.NewContextInMemory() 44 | ctx = ctxutil.WithInteractive(ctx, false) 45 | act, err := newMock(ctx, u.StoreDir("")) 46 | require.NoError(t, err) 47 | require.NotNil(t, act) 48 | 49 | for _, cmd := range act.GetCommands() { 50 | t.Run(cmd.Name, func(t *testing.T) { 51 | testCommand(t, cmd) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/action/create_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/clipboard" 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/gopasspw/gopass/internal/out" 11 | "github.com/gopasspw/gopass/pkg/ctxutil" 12 | "github.com/gopasspw/gopass/tests/gptest" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestCreate(t *testing.T) { 17 | u := gptest.NewUnitTester(t) 18 | 19 | clipboard.ForceUnsupported = true 20 | 21 | ctx := config.NewContextInMemory() 22 | ctx = ctxutil.WithAlwaysYes(ctx, true) 23 | 24 | act, err := newMock(ctx, u.StoreDir("")) 25 | require.NoError(t, err) 26 | require.NotNil(t, act) 27 | ctx = act.cfg.WithConfig(ctx) 28 | 29 | require.NoError(t, act.cfg.Set("", "core.notifications", "false")) 30 | require.NoError(t, act.cfg.Set("", "core.cliptimeout", "1")) 31 | 32 | buf := &bytes.Buffer{} 33 | out.Stdout = buf 34 | defer func() { 35 | out.Stdout = os.Stdout 36 | }() 37 | 38 | // create 39 | c := gptest.CliCtx(ctx, t) 40 | 41 | require.Error(t, act.Create(c)) 42 | buf.Reset() 43 | } 44 | -------------------------------------------------------------------------------- /internal/action/doc.go: -------------------------------------------------------------------------------- 1 | // Package action implements all the handlers that are available as subcommands 2 | // for gopass. 3 | package action 4 | -------------------------------------------------------------------------------- /internal/action/exit/errors_test.go: -------------------------------------------------------------------------------- 1 | package exit 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestError(t *testing.T) { 15 | buf := &bytes.Buffer{} 16 | out.Stdout = buf 17 | defer func() { 18 | out.Stdout = os.Stdout 19 | }() 20 | 21 | require.Error(t, Error(Unknown, fmt.Errorf("test"), "test")) 22 | assert.NotContains(t, buf.String(), "Stacktrace") 23 | } 24 | -------------------------------------------------------------------------------- /internal/action/git.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/gopasspw/gopass/internal/action/exit" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | // Git passes the git command to the underlying backend. 15 | func (s *Action) Git(c *cli.Context) error { 16 | ctx := ctxutil.WithGlobalFlags(c) 17 | store := c.String("store") 18 | 19 | sub, err := s.Store.GetSubStore(store) 20 | if err != nil || sub == nil { 21 | return exit.Error(exit.Git, err, "failed to get sub store %s: %s", store, err) 22 | } 23 | 24 | args := c.Args().Slice() 25 | out.Noticef(ctx, "Running 'git %s' in %s...", strings.Join(args, " "), sub.Path()) 26 | cmd := exec.CommandContext(ctx, "git", args...) 27 | cmd.Dir = sub.Path() 28 | cmd.Stdout = os.Stdout 29 | cmd.Stderr = os.Stderr 30 | cmd.Stdin = os.Stdin 31 | 32 | return cmd.Run() 33 | } 34 | -------------------------------------------------------------------------------- /internal/action/history.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gopasspw/gopass/internal/action/exit" 7 | "github.com/gopasspw/gopass/internal/out" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/pkg/debug" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // History displays the history of a given secret. 14 | func (s *Action) History(c *cli.Context) error { 15 | ctx := ctxutil.WithGlobalFlags(c) 16 | name := c.Args().Get(0) 17 | showPassword := c.Bool("password") 18 | 19 | if name == "" { 20 | return exit.Error(exit.Usage, nil, "Usage: %s history ", s.Name) 21 | } 22 | 23 | if !s.Store.Exists(ctx, name) { 24 | return exit.Error(exit.NotFound, nil, "Secret not found") 25 | } 26 | 27 | revs, err := s.Store.ListRevisions(ctx, name) 28 | if err != nil { 29 | return exit.Error(exit.Unknown, err, "Failed to get revisions: %s", err) 30 | } 31 | 32 | for _, rev := range revs { 33 | pw := "" 34 | if showPassword { 35 | _, sec, err := s.Store.GetRevision(ctx, name, rev.Hash) 36 | if err != nil { 37 | debug.Log("Failed to get revision %q of %q: %s", rev.Hash, name, err) 38 | } 39 | if err == nil { 40 | pw = " - " + sec.Password() 41 | } 42 | } 43 | out.Printf(ctx, "%s - %s <%s> - %s - %s%s\n", rev.Hash, rev.AuthorName, rev.AuthorEmail, rev.Date.Format(time.RFC3339), rev.Subject, pw) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/action/link.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/gopasspw/gopass/internal/action/exit" 5 | "github.com/gopasspw/gopass/pkg/ctxutil" 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | // Link creates a symlink. 10 | func (s *Action) Link(c *cli.Context) error { 11 | ctx := ctxutil.WithGlobalFlags(c) 12 | 13 | from := c.Args().Get(0) 14 | to := c.Args().Get(1) 15 | 16 | if from == "" || to == "" { 17 | return exit.Error(exit.Usage, nil, "Usage: link ") 18 | } 19 | 20 | return s.Store.Link(ctx, from, to) 21 | } 22 | -------------------------------------------------------------------------------- /internal/action/move.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gopasspw/gopass/internal/action/exit" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/pkg/termio" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | // Move the content from one secret to another. 13 | func (s *Action) Move(c *cli.Context) error { 14 | ctx := ctxutil.WithGlobalFlags(c) 15 | 16 | if c.Args().Len() != 2 { 17 | return exit.Error(exit.Usage, nil, "Usage: %s mv old-path new-path", s.Name) 18 | } 19 | 20 | from := c.Args().Get(0) 21 | to := c.Args().Get(1) 22 | 23 | if !c.Bool("force") { 24 | if s.Store.Exists(ctx, to) && !termio.AskForConfirmation(ctx, fmt.Sprintf("%s already exists. Overwrite it?", to)) { 25 | return exit.Error(exit.Aborted, nil, "not overwriting your current secret") 26 | } 27 | } 28 | 29 | if err := s.Store.Move(ctx, from, to); err != nil { 30 | return exit.Error(exit.Unknown, err, "%s", err) 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/action/move_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/gopasspw/gopass/tests/gptest" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestMove(t *testing.T) { 16 | u := gptest.NewUnitTester(t) 17 | 18 | ctx := config.NewContextInMemory() 19 | ctx = ctxutil.WithAlwaysYes(ctx, true) 20 | ctx = ctxutil.WithInteractive(ctx, false) 21 | 22 | act, err := newMock(ctx, u.StoreDir("")) 23 | require.NoError(t, err) 24 | require.NotNil(t, act) 25 | ctx = act.cfg.WithConfig(ctx) 26 | 27 | buf := &bytes.Buffer{} 28 | out.Stdout = buf 29 | out.Stderr = buf 30 | defer func() { 31 | out.Stdout = os.Stdout 32 | out.Stderr = os.Stderr 33 | }() 34 | 35 | t.Run("move foo to bar", func(t *testing.T) { 36 | defer buf.Reset() 37 | require.NoError(t, act.Move(gptest.CliCtx(ctx, t, "foo", "bar"))) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/action/process.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gopasspw/gopass/internal/action/exit" 7 | "github.com/gopasspw/gopass/internal/out" 8 | "github.com/gopasspw/gopass/internal/tpl" 9 | "github.com/gopasspw/gopass/pkg/ctxutil" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // Process is a command to process a template and replace secrets contained in it. 14 | func (s *Action) Process(c *cli.Context) error { 15 | ctx := ctxutil.WithGlobalFlags(c) 16 | file := c.Args().First() 17 | if file == "" { 18 | return exit.Error(exit.Usage, nil, "Usage: %s process ", s.Name) 19 | } 20 | 21 | buf, err := os.ReadFile(file) 22 | if err != nil { 23 | return exit.Error(exit.IO, err, "Failed to read file: %s", file) 24 | } 25 | 26 | obuf, err := tpl.Execute(ctx, string(buf), file, nil, s.Store) 27 | if err != nil { 28 | return exit.Error(exit.IO, err, "Failed to process file: %s", file) 29 | } 30 | 31 | out.Print(ctx, string(obuf)) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/action/pwgen/commands_test.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/tests/gptest" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func testCommand(t *testing.T, cmd *cli.Command) { 12 | t.Helper() 13 | 14 | if len(cmd.Subcommands) < 1 { 15 | assert.NotNil(t, cmd.Action, cmd.Name) 16 | } 17 | 18 | assert.NotEmpty(t, cmd.Usage) 19 | assert.NotEmpty(t, cmd.Description) 20 | 21 | for _, flag := range cmd.Flags { 22 | switch v := flag.(type) { 23 | case *cli.StringFlag: 24 | assert.NotContains(t, v.Name, ",") 25 | assert.NotEmpty(t, v.Usage) 26 | case *cli.BoolFlag: 27 | assert.NotContains(t, v.Name, ",") 28 | assert.NotEmpty(t, v.Usage) 29 | } 30 | } 31 | 32 | for _, scmd := range cmd.Subcommands { 33 | testCommand(t, scmd) 34 | } 35 | } 36 | 37 | func TestCommands(t *testing.T) { 38 | // necessary for setting up the env 39 | u := gptest.NewGUnitTester(t) 40 | assert.NotNil(t, u) 41 | 42 | for _, cmd := range GetCommands() { 43 | testCommand(t, cmd) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/action/pwgen/pwgen_test.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/gopasspw/gopass/tests/gptest" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestPwgen(t *testing.T) { 17 | u := gptest.NewUnitTester(t) 18 | assert.NotNil(t, u) 19 | 20 | ctx := config.NewContextInMemory() 21 | ctx = ctxutil.WithAlwaysYes(ctx, true) 22 | 23 | buf := &bytes.Buffer{} 24 | out.Stdout = buf 25 | defer func() { 26 | out.Stdout = os.Stdout 27 | }() 28 | 29 | require.NoError(t, Pwgen(gptest.CliCtxWithFlags(ctx, t, map[string]string{"one-per-line": "true"}, "24", "1"))) 30 | assert.GreaterOrEqual(t, len(buf.Bytes()), 24, buf.String()) 31 | } 32 | -------------------------------------------------------------------------------- /internal/action/sync_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/gopasspw/gopass/tests/gptest" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestSync(t *testing.T) { 16 | u := gptest.NewUnitTester(t) 17 | 18 | buf := &bytes.Buffer{} 19 | out.Stdout = buf 20 | out.Stderr = buf 21 | defer func() { 22 | out.Stdout = os.Stdout 23 | out.Stderr = os.Stderr 24 | }() 25 | 26 | ctx := config.NewContextInMemory() 27 | ctx = ctxutil.WithAlwaysYes(ctx, true) 28 | act, err := newMock(ctx, u.StoreDir("")) 29 | require.NoError(t, err) 30 | require.NotNil(t, act) 31 | ctx = act.cfg.WithConfig(ctx) 32 | 33 | t.Run("default", func(t *testing.T) { 34 | defer buf.Reset() 35 | require.NoError(t, act.Sync(gptest.CliCtx(ctx, t))) 36 | }) 37 | 38 | t.Run("sync --store=root", func(t *testing.T) { 39 | defer buf.Reset() 40 | require.NoError(t, act.Sync(gptest.CliCtxWithFlags(ctx, t, map[string]string{"store": "root"}))) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/action/unclip.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/gopasspw/gopass/internal/action/exit" 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/clipboard" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | // Unclip tries to erase the content of the clipboard. 15 | func (s *Action) Unclip(c *cli.Context) error { 16 | ctx := ctxutil.WithGlobalFlags(c) 17 | force := c.Bool("force") 18 | timeout := c.Int("timeout") 19 | name := os.Getenv("GOPASS_UNCLIP_NAME") 20 | checksum := os.Getenv("GOPASS_UNCLIP_CHECKSUM") 21 | 22 | time.Sleep(time.Second * time.Duration(timeout)) 23 | 24 | mp := s.Store.MountPoint(name) 25 | ctx = config.WithMount(ctx, mp) 26 | 27 | if err := clipboard.Clear(ctx, name, checksum, force); err != nil { 28 | return exit.Error(exit.IO, err, "Failed to clear clipboard: %s", err) 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/action/unclip_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | _ "github.com/gopasspw/gopass/internal/backend/crypto" 9 | _ "github.com/gopasspw/gopass/internal/backend/storage" 10 | "github.com/gopasspw/gopass/internal/config" 11 | "github.com/gopasspw/gopass/internal/out" 12 | "github.com/gopasspw/gopass/tests/gptest" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestUnclip(t *testing.T) { 17 | u := gptest.NewUnitTester(t) 18 | 19 | buf := &bytes.Buffer{} 20 | out.Stdout = buf 21 | stdout = buf 22 | defer func() { 23 | out.Stdout = os.Stdout 24 | stdout = os.Stdout 25 | }() 26 | 27 | ctx := config.NewContextInMemory() 28 | act, err := newMock(ctx, u.StoreDir("")) 29 | require.NoError(t, err) 30 | require.NotNil(t, act) 31 | ctx = act.cfg.WithConfig(ctx) 32 | 33 | t.Run("unlcip should fail", func(t *testing.T) { 34 | require.Error(t, act.Unclip(gptest.CliCtxWithFlags(ctx, t, map[string]string{"timeout": "0"}))) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /internal/action/update.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/gopasspw/gopass/internal/action/exit" 5 | "github.com/gopasspw/gopass/internal/out" 6 | "github.com/gopasspw/gopass/internal/updater" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | // Update will start the interactive update assistant. 12 | func (s *Action) Update(c *cli.Context) error { 13 | _ = s.rem.Reset("update") 14 | 15 | ctx := ctxutil.WithGlobalFlags(c) 16 | 17 | if s.version.String() == "0.0.0+HEAD" { 18 | out.Errorf(ctx, "Can not check version against HEAD") 19 | 20 | return nil 21 | } 22 | 23 | out.Printf(ctx, "⚒ Checking for available updates ...") 24 | if err := updater.Update(ctx, s.version); err != nil { 25 | return exit.Error(exit.Unknown, err, "Failed to update gopass: %s", err) 26 | } 27 | 28 | out.OKf(ctx, "gopass is up to date") 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/action/version_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | _ "github.com/gopasspw/gopass/internal/backend/crypto" 9 | _ "github.com/gopasspw/gopass/internal/backend/storage" 10 | "github.com/gopasspw/gopass/internal/config" 11 | "github.com/gopasspw/gopass/internal/out" 12 | "github.com/gopasspw/gopass/pkg/ctxutil" 13 | "github.com/gopasspw/gopass/tests/gptest" 14 | "github.com/stretchr/testify/require" 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | func TestVersion(t *testing.T) { 19 | u := gptest.NewUnitTester(t) 20 | 21 | ctx := config.NewContextInMemory() 22 | ctx = ctxutil.WithAlwaysYes(ctx, true) 23 | ctx = ctxutil.WithInteractive(ctx, false) 24 | 25 | act, err := newMock(ctx, u.StoreDir("")) 26 | require.NoError(t, err) 27 | 28 | buf := &bytes.Buffer{} 29 | out.Stdout = buf 30 | stdout = buf 31 | defer func() { 32 | out.Stdout = os.Stdout 33 | stdout = os.Stdout 34 | }() 35 | 36 | cli.VersionPrinter = func(*cli.Context) { 37 | out.Printf(ctx, "gopass version 0.0.0-test") 38 | } 39 | 40 | t.Run("print fixed version", func(t *testing.T) { 41 | require.NoError(t, act.Version(gptest.CliCtx(ctx, t))) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/audit/report_test.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFinalize(t *testing.T) { 10 | r := newReport() 11 | r.AddPassword("foo", "bar") 12 | r.AddPassword("baz", "bar") 13 | r.AddPassword("zab", "bar") 14 | r.AddPassword("foo", "bar") 15 | r.AddFinding("foo", "foo", "bar", "warning") 16 | r.AddFinding("bar", "foo", "bar", "warning") 17 | 18 | sr := r.Finalize() 19 | assert.NotNil(t, sr) 20 | } 21 | -------------------------------------------------------------------------------- /internal/audit/single.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gopasspw/gopass/internal/out" 8 | "github.com/muesli/crunchy" 9 | ) 10 | 11 | // Single runs a password strength audit on a single password. 12 | func Single(ctx context.Context, password string) { 13 | validator := crunchy.NewValidator() 14 | if err := validator.Check(password); err != nil { 15 | out.Printf(ctx, fmt.Sprintf("Warning: %s", err)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/backend/context_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCryptoBackend(t *testing.T) { 11 | t.Parallel() 12 | 13 | ctx := config.NewContextInMemory() 14 | 15 | assert.Equal(t, GPGCLI, GetCryptoBackend(ctx)) 16 | assert.Equal(t, GPGCLI, GetCryptoBackend(WithCryptoBackendString(ctx, "gpgcli"))) 17 | assert.Equal(t, GPGCLI, GetCryptoBackend(WithCryptoBackend(ctx, GPGCLI))) 18 | assert.True(t, HasCryptoBackend(WithCryptoBackend(ctx, GPGCLI))) 19 | } 20 | 21 | func TestStorageBackend(t *testing.T) { 22 | t.Parallel() 23 | 24 | ctx := config.NewContextInMemory() 25 | 26 | assert.Equal(t, "fs", StorageBackendName(FS)) 27 | assert.Equal(t, FS, GetStorageBackend(ctx)) 28 | assert.Equal(t, FS, GetStorageBackend(WithStorageBackendString(ctx, "fs"))) 29 | assert.Equal(t, FS, GetStorageBackend(WithStorageBackend(ctx, FS))) 30 | assert.True(t, HasStorageBackend(WithStorageBackend(ctx, FS))) 31 | } 32 | 33 | func TestComposite(t *testing.T) { 34 | t.Parallel() 35 | 36 | ctx := config.NewContextInMemory() 37 | ctx = WithCryptoBackend(ctx, Age) 38 | ctx = WithStorageBackend(ctx, FS) 39 | 40 | assert.Equal(t, Age, GetCryptoBackend(ctx)) 41 | assert.Equal(t, FS, GetStorageBackend(ctx)) 42 | } 43 | -------------------------------------------------------------------------------- /internal/backend/crypto/age.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/crypto/age" // registers age backend 4 | -------------------------------------------------------------------------------- /internal/backend/crypto/age/clientUI.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import ( 4 | "context" 5 | 6 | "filippo.io/age/plugin" 7 | "github.com/gopasspw/gopass/internal/cui" 8 | "github.com/gopasspw/gopass/internal/out" 9 | "github.com/gopasspw/gopass/pkg/termio" 10 | ) 11 | 12 | var pluginTerminalUI = &plugin.ClientUI{ 13 | DisplayMessage: func(name, message string) error { 14 | out.Printf(context.Background(), "%s plugin: %s", name, message) 15 | 16 | return nil 17 | }, 18 | RequestValue: func(name, message string, _ bool) (string, error) { 19 | var err error 20 | defer func() { 21 | if err != nil { 22 | out.Warningf(context.Background(), "could not read value for age-plugin-%s: %v", name, err) 23 | } 24 | }() 25 | secret, err := termio.AskForPassword(context.Background(), "secret", false) 26 | if err != nil { 27 | return "", err 28 | } 29 | 30 | return secret, nil 31 | }, 32 | Confirm: func(name, message, yes, no string) (bool, error) { 33 | rep, _ := cui.GetSelection(context.Background(), message, []string{yes, no}) 34 | if rep == yes { 35 | return true, nil 36 | } 37 | 38 | return false, nil 39 | }, 40 | 41 | WaitTimer: func(name string) { 42 | out.Printf(context.Background(), "waiting on %s plugin...", name) 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /internal/backend/crypto/age/context.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import "context" 4 | 5 | type contextKey int 6 | 7 | const ( 8 | ctxKeyOnlyNative contextKey = iota 9 | ) 10 | 11 | // WithOnlyNative will return a context with the flag for only native set. 12 | func WithOnlyNative(ctx context.Context, at bool) context.Context { 13 | return context.WithValue(ctx, ctxKeyOnlyNative, at) 14 | } 15 | 16 | // IsOnlyNative will return the value of the only native flag or the default 17 | // (false). 18 | func IsOnlyNative(ctx context.Context) bool { 19 | bv, ok := ctx.Value(ctxKeyOnlyNative).(bool) 20 | if !ok { 21 | return false 22 | } 23 | 24 | return bv 25 | } 26 | -------------------------------------------------------------------------------- /internal/backend/crypto/age/context_test.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithOnlyNative(t *testing.T) { 8 | ctx := t.Context() 9 | ctx = WithOnlyNative(ctx, true) 10 | 11 | val := ctx.Value(ctxKeyOnlyNative) 12 | if val == nil { 13 | t.Errorf("Expected value to be set, got nil") 14 | } 15 | 16 | boolVal, ok := val.(bool) 17 | if !ok { 18 | t.Errorf("Expected value to be of type bool, got %T", val) 19 | } 20 | 21 | if !boolVal { 22 | t.Errorf("Expected value to be true, got false") 23 | } 24 | } 25 | 26 | func TestIsOnlyNative(t *testing.T) { 27 | ctx := t.Context() 28 | 29 | // Test default value 30 | if IsOnlyNative(ctx) { 31 | t.Errorf("Expected default value to be false, got true") 32 | } 33 | 34 | // Test set value 35 | ctx = WithOnlyNative(ctx, true) 36 | if !IsOnlyNative(ctx) { 37 | t.Errorf("Expected value to be true, got false") 38 | } 39 | 40 | // Test reset value 41 | ctx = WithOnlyNative(ctx, false) 42 | if IsOnlyNative(ctx) { 43 | t.Errorf("Expected value to be false, got true") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/backend/crypto/age/encrypt_test.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | "sort" 7 | "testing" 8 | 9 | "filippo.io/age" 10 | "filippo.io/age/agessh" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "golang.org/x/crypto/ssh" 14 | ) 15 | 16 | func TestDedupe(t *testing.T) { 17 | t.Parallel() 18 | 19 | i1, err := age.GenerateX25519Identity() 20 | require.NoError(t, err) 21 | 22 | i2, err := age.GenerateX25519Identity() 23 | require.NoError(t, err) 24 | 25 | i3pub, _, err := ed25519.GenerateKey(nil) 26 | require.NoError(t, err) 27 | i3ssh, err := ssh.NewPublicKey(i3pub) 28 | require.NoError(t, err) 29 | i3, err := agessh.NewEd25519Recipient(i3ssh) 30 | require.NoError(t, err) 31 | 32 | in := []age.Recipient{i1.Recipient(), i2.Recipient(), i2.Recipient(), i3, i3} 33 | out := dedupe(in) 34 | want := []age.Recipient{i3, i3, i1.Recipient(), i2.Recipient()} 35 | 36 | sort.Sort(Recipients(out)) 37 | sort.Sort(Recipients(want)) 38 | assert.Equal(t, want, out) 39 | } 40 | 41 | type Recipients []age.Recipient 42 | 43 | func (r Recipients) Len() int { 44 | return len(r) 45 | } 46 | 47 | func (r Recipients) Swap(i, j int) { 48 | r[i], r[j] = r[j], r[i] 49 | } 50 | 51 | func (r Recipients) Less(i, j int) bool { 52 | return fmt.Sprintf("%s", r[i]) < fmt.Sprintf("%s", r[j]) 53 | } 54 | -------------------------------------------------------------------------------- /internal/backend/crypto/age/unsupported.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // FormatKey returns the key id. 9 | func (a *Age) FormatKey(ctx context.Context, id, tpl string) string { 10 | return id 11 | } 12 | 13 | // Fingerprint returns the id. 14 | func (a *Age) Fingerprint(ctx context.Context, id string) string { 15 | return id 16 | } 17 | 18 | // ListRecipients is not supported for the age backend. 19 | func (a *Age) ListRecipients(context.Context) ([]string, error) { 20 | return nil, fmt.Errorf("not implemented") 21 | } 22 | 23 | // ReadNamesFromKey is not supported for the age backend. 24 | func (a *Age) ReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error) { 25 | return nil, fmt.Errorf("not implemented") 26 | } 27 | 28 | // RecipientIDs is not supported for the age backend. 29 | func (a *Age) RecipientIDs(ctx context.Context, buf []byte) ([]string, error) { 30 | return nil, fmt.Errorf("reading recipient IDs is not supported by the age backend by design") 31 | } 32 | -------------------------------------------------------------------------------- /internal/backend/crypto/doc.go: -------------------------------------------------------------------------------- 1 | // Package crypto provides a pluggable crypto backend for gopass. 2 | 3 | package crypto 4 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/decrypt.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | ) 12 | 13 | // Decrypt will try to decrypt the given file. 14 | func (g *GPG) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) { 15 | ctx, cancel := context.WithTimeout(ctx, Timeout) 16 | defer cancel() 17 | 18 | args := append(g.args, "--decrypt") 19 | // Useful information may appear there 20 | if debug.IsEnabled() { 21 | args = append(args, "--verbose", "--verbose") 22 | } 23 | cmd := exec.CommandContext(ctx, g.binary, args...) 24 | cmd.Stdin = bytes.NewReader(ciphertext) 25 | // If gopass-jsonapi is used, there is no way to reach this os.Stderr, so 26 | // we write this stderr to the log file as well. 27 | cmd.Stderr = io.MultiWriter(os.Stderr, debug.LogWriter) 28 | 29 | debug.V(1).Log("Running %s %+v", cmd.Path, cmd.Args) 30 | stdout, err := cmd.Output() 31 | if err != nil { 32 | debug.Log("GPG decrypt failed: %s %+v: %+v", cmd.Path, cmd.Args, err) 33 | } 34 | 35 | return stdout, err 36 | } 37 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/generate.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os/exec" 8 | 9 | "github.com/gopasspw/gopass/pkg/debug" 10 | ) 11 | 12 | // GenerateIdentity will create a new GPG keypair in batch mode. 13 | func (g *GPG) GenerateIdentity(ctx context.Context, name, email, passphrase string) error { 14 | buf := &bytes.Buffer{} 15 | // 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 16 | _, _ = buf.WriteString(`%echo Generating a RSA/RSA key pair 17 | Key-Type: RSA 18 | Key-Length: 2048 19 | Subkey-Type: RSA 20 | Subkey-Length: 2048 21 | Expire-Date: 0 22 | `) 23 | _, _ = buf.WriteString("Name-Real: " + name + "\n") 24 | _, _ = buf.WriteString("Name-Email: " + email + "\n") 25 | _, _ = buf.WriteString("Passphrase: " + passphrase + "\n") 26 | 27 | args := []string{"--batch", "--gen-key"} 28 | cmd := exec.CommandContext(ctx, g.binary, args...) 29 | cmd.Stdin = bytes.NewReader(buf.Bytes()) 30 | 31 | out := &bytes.Buffer{} 32 | cmd.Stdout = out 33 | cmd.Stderr = out 34 | 35 | debug.Log("%s %+v", cmd.Path, cmd.Args) 36 | if err := cmd.Run(); err != nil { 37 | return fmt.Errorf("failed to run command: '%s %+v': %q - %w", cmd.Path, cmd.Args, out.String(), err) 38 | } 39 | 40 | g.privKeys = nil 41 | g.pubKeys = nil 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/gpg_others_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package cli 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestEncrypt(t *testing.T) { 14 | t.Parallel() 15 | 16 | ctx := config.NewContextInMemory() 17 | 18 | g := &GPG{} 19 | g.binary = "true" 20 | 21 | _, err := g.Encrypt(ctx, []byte("foo"), nil) 22 | // No recipients are configured so it will fail 23 | require.Error(t, err) 24 | } 25 | 26 | func TestDecrypt(t *testing.T) { 27 | t.Parallel() 28 | 29 | ctx := config.NewContextInMemory() 30 | 31 | g := &GPG{} 32 | g.binary = "true" 33 | 34 | _, err := g.Decrypt(ctx, []byte("foo")) 35 | require.NoError(t, err) 36 | } 37 | 38 | func TestGenerateIdentity(t *testing.T) { 39 | t.Parallel() 40 | 41 | ctx := config.NewContextInMemory() 42 | 43 | g := &GPG{} 44 | g.binary = "true" 45 | 46 | require.NoError(t, g.GenerateIdentity(ctx, "foo", "foo@bar.com", "bar")) 47 | } 48 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/gpg_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGPG(t *testing.T) { 12 | if testing.Short() { 13 | t.Skip("skipping test in short mode.") 14 | } 15 | 16 | td := t.TempDir() 17 | t.Setenv("GNUPGHOME", td) 18 | 19 | ctx := config.NewContextInMemory() 20 | 21 | var err error 22 | var g *GPG 23 | 24 | assert.Empty(t, g.Binary()) 25 | 26 | g, err = New(ctx, Config{}) 27 | require.NoError(t, err) 28 | assert.NotEmpty(t, g.Binary()) 29 | 30 | _, err = g.ListRecipients(ctx) 31 | require.NoError(t, err) 32 | 33 | _, err = g.ListIdentities(ctx) 34 | require.NoError(t, err) 35 | 36 | _, err = g.RecipientIDs(ctx, []byte{}) 37 | require.Error(t, err) 38 | 39 | require.NoError(t, g.Initialized(ctx)) 40 | assert.Equal(t, "gpg", g.Name()) 41 | assert.Equal(t, "gpg", g.Ext()) 42 | assert.Equal(t, ".gpg-id", g.IDFile()) 43 | } 44 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/gpg_windows_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEncrypt(t *testing.T) { 12 | t.Parallel() 13 | 14 | ctx, cancel := context.WithCancel(config.NewContextInMemory()) 15 | 16 | g := &GPG{} 17 | g.binary = "rundll32" 18 | 19 | _, err := g.Encrypt(ctx, []byte("foo"), nil) 20 | 21 | // No recipients are configured so it will fail 22 | require.Error(t, err) 23 | cancel() 24 | } 25 | 26 | func TestDecrypt(t *testing.T) { 27 | t.Parallel() 28 | 29 | ctx, cancel := context.WithCancel(config.NewContextInMemory()) 30 | 31 | g := &GPG{} 32 | g.binary = "rundll32" 33 | 34 | _, err := g.Decrypt(ctx, []byte("foo")) 35 | require.NoError(t, err) 36 | cancel() 37 | } 38 | 39 | func TestGenerateIdentity(t *testing.T) { 40 | t.Parallel() 41 | 42 | ctx, cancel := context.WithCancel(config.NewContextInMemory()) 43 | 44 | g := &GPG{} 45 | g.binary = "rundll32" 46 | 47 | require.NoError(t, g.GenerateIdentity(ctx, "foo", "foo@bar.com", "bar")) 48 | cancel() 49 | } 50 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/identities.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gopasspw/gopass/internal/backend/crypto/gpg" 7 | ) 8 | 9 | // ListIdentities returns a parsed list of GPG secret keys. 10 | func (g *GPG) ListIdentities(ctx context.Context) ([]string, error) { 11 | if g.privKeys == nil { 12 | kl, err := g.listKeys(ctx, "secret") 13 | if err != nil { 14 | return nil, err 15 | } 16 | g.privKeys = kl 17 | } 18 | 19 | if gpg.IsAlwaysTrust(ctx) { 20 | return g.privKeys.Recipients(), nil 21 | } 22 | 23 | return g.privKeys.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients(), nil 24 | } 25 | 26 | // FindIdentities searches for the given private keys. 27 | func (g *GPG) FindIdentities(ctx context.Context, search ...string) ([]string, error) { 28 | kl, err := g.listKeys(ctx, "secret", search...) 29 | if err != nil || kl == nil { 30 | return nil, err 31 | } 32 | 33 | if gpg.IsAlwaysTrust(ctx) { 34 | return kl.Recipients(), nil 35 | } 36 | 37 | return kl.UseableKeys(gpg.IsAlwaysTrust(ctx)).Recipients(), nil 38 | } 39 | 40 | func (g *GPG) findKey(ctx context.Context, id string) (gpg.Key, bool) { 41 | kl, _ := g.listKeys(ctx, "secret", id) 42 | if len(kl) >= 1 { 43 | return kl[0], true 44 | } 45 | 46 | kl, _ = g.listKeys(ctx, "public", id) 47 | if len(kl) >= 1 { 48 | return kl[0], true 49 | } 50 | 51 | return gpg.Key{ 52 | Fingerprint: id, 53 | }, false 54 | } 55 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/loader.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/gopasspw/gopass/internal/backend" 9 | "github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf" 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | "github.com/gopasspw/gopass/pkg/fsutil" 12 | ) 13 | 14 | const ( 15 | name = "gpgcli" 16 | ) 17 | 18 | func init() { 19 | backend.CryptoRegistry.Register(backend.GPGCLI, name, &loader{}) 20 | } 21 | 22 | type loader struct{} 23 | 24 | // New implements backend.CryptoLoader. 25 | func (l loader) New(ctx context.Context) (backend.Crypto, error) { 26 | debug.Log("Using Crypto Backend: %s", name) 27 | 28 | return New(ctx, Config{ 29 | Umask: fsutil.Umask(), 30 | Args: gpgconf.GPGOpts(), 31 | Binary: os.Getenv("GOPASS_GPG_BINARY"), 32 | }) 33 | } 34 | 35 | func (l loader) Handles(ctx context.Context, s backend.Storage) error { 36 | if s.Exists(ctx, IDFile) { 37 | return nil 38 | } 39 | 40 | return fmt.Errorf("not supported") 41 | } 42 | 43 | func (l loader) Priority() int { 44 | return 1 45 | } 46 | 47 | func (l loader) String() string { 48 | return name 49 | } 50 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/recipients_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSplitPacket(t *testing.T) { 10 | t.Parallel() 11 | 12 | for in, out := range map[string]map[string]string{ 13 | "": {}, 14 | ":pubkey enc packet: version 3, algo 1, keyid 00F0FF00FFC00F0F": { 15 | "algo": "1", 16 | "keyid": "00F0FF00FFC00F0F", 17 | "version": "3", 18 | }, 19 | ":encrypted data packet:": {}, 20 | } { 21 | assert.Equal(t, out, splitPacket(in)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/cli/version.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/blang/semver/v4" 7 | "github.com/gopasspw/gopass/internal/backend/crypto/gpg/gpgconf" 8 | ) 9 | 10 | // Version will return GPG version information. 11 | func (g *GPG) Version(ctx context.Context) semver.Version { 12 | return gpgconf.Version(ctx, g.Binary()) 13 | } 14 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/colons/parse_fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package colons 5 | 6 | import "bytes" 7 | 8 | func Fuzz(data []byte) int { 9 | if kl := Parse(bytes.NewReader(data)); len(kl) != 0 { 10 | return 1 11 | } 12 | return 0 13 | } 14 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/colons/utils.go: -------------------------------------------------------------------------------- 1 | package colons 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // parseTS parses the passed string as an Epoch int and returns 9 | // the time struct or the zero time struct. 10 | func parseTS(str string) time.Time { 11 | t := time.Time{} 12 | 13 | if sec, err := strconv.ParseInt(str, 10, 64); err == nil { 14 | t = time.Unix(sec, 0) 15 | } 16 | 17 | return t 18 | } 19 | 20 | // parseInt parses the passed string as an int and returns it 21 | // or 0 on errors. 22 | func parseInt(str string) int { 23 | i := 0 24 | 25 | if iv, err := strconv.ParseInt(str, 10, 32); err == nil { 26 | i = int(iv) 27 | } 28 | 29 | return i 30 | } 31 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/context.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import "context" 4 | 5 | type contextKey int 6 | 7 | const ( 8 | ctxKeyAlwaysTrust contextKey = iota 9 | ctxKeyUseCache 10 | ) 11 | 12 | // WithAlwaysTrust will return a context with the flag for always trust set. 13 | func WithAlwaysTrust(ctx context.Context, at bool) context.Context { 14 | return context.WithValue(ctx, ctxKeyAlwaysTrust, at) 15 | } 16 | 17 | // IsAlwaysTrust will return the value of the always trust flag or the default 18 | // (false). 19 | func IsAlwaysTrust(ctx context.Context) bool { 20 | bv, ok := ctx.Value(ctxKeyAlwaysTrust).(bool) 21 | if !ok { 22 | return false 23 | } 24 | 25 | return bv 26 | } 27 | 28 | // WithUseCache returns a context with the value of NoCache set. 29 | func WithUseCache(ctx context.Context, nc bool) context.Context { 30 | return context.WithValue(ctx, ctxKeyUseCache, nc) 31 | } 32 | 33 | // UseCache returns true if this request should ignore the cache. 34 | func UseCache(ctx context.Context) bool { 35 | nc, ok := ctx.Value(ctxKeyUseCache).(bool) 36 | if !ok { 37 | return false 38 | } 39 | 40 | return nc 41 | } 42 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/context_test.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | ) 8 | 9 | func TestAlwaysTrust(t *testing.T) { 10 | t.Parallel() 11 | 12 | ctx := config.NewContextInMemory() 13 | 14 | if IsAlwaysTrust(ctx) { 15 | t.Errorf("AlwaysTrust should be false") 16 | } 17 | 18 | if !IsAlwaysTrust(WithAlwaysTrust(ctx, true)) { 19 | t.Errorf("AlwaysTrust should be true") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/doc.go: -------------------------------------------------------------------------------- 1 | // Package gpg provides a GPG crypto backend for gopass. 2 | // It does not provide a full GPG implementation, but rather 3 | // building blocks used by other packages to provide GPG 4 | // support. 5 | 6 | package gpg 7 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/binary.go: -------------------------------------------------------------------------------- 1 | package gpgconf 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/gopasspw/gopass/pkg/debug" 8 | ) 9 | 10 | // Binary returns the GPG binary location. 11 | func Binary(ctx context.Context, bin string) (string, error) { 12 | if sv := os.Getenv("GOPASS_GPG_BINARY"); sv != "" { 13 | debug.Log("Using GOPASS_GPG_BINARY: %s", sv) 14 | 15 | return sv, nil 16 | } 17 | 18 | return detectBinary(ctx, bin) 19 | } 20 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/binary_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package gpgconf 5 | 6 | import ( 7 | "context" 8 | "os/exec" 9 | 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | "github.com/gopasspw/gopass/pkg/fsutil" 12 | ) 13 | 14 | func detectBinary(_ context.Context, name string) (string, error) { 15 | // user supplied binaries take precedence 16 | if name != "" { 17 | return exec.LookPath(name) 18 | } 19 | 20 | // try to get the proper binary from gpgconf(1) 21 | p, err := Path("gpg") 22 | if err != nil || p == "" || !fsutil.IsFile(p) { 23 | debug.Log("gpgconf failed (%q), falling back to path lookup: %q", p, err) 24 | // otherwise fall back to the default and try 25 | // to look up "gpg" 26 | return exec.LookPath("gpg") 27 | } 28 | 29 | debug.V(3).Log("gpgconf returned %q for gpg", p) 30 | 31 | return p, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/binary_windows_test.go: -------------------------------------------------------------------------------- 1 | package gpgconf 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDetectBinaryCandidates(t *testing.T) { 12 | bins, err := detectBinaryCandidates("foobar") 13 | require.NoError(t, err) 14 | // the install locations differ depending on : 15 | // - chocolatey install path prefix 16 | // - 64bit/32bit windows 17 | var stripped []string 18 | for _, bin := range bins { 19 | stripped = append(stripped, filepath.Base(bin)) 20 | } 21 | assert.Contains(t, stripped, "gpg.exe") 22 | } 23 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/gpgconf.go: -------------------------------------------------------------------------------- 1 | package gpgconf 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | // Path returns the path to a GPG component. 12 | func Path(key string) (string, error) { 13 | buf := &bytes.Buffer{} 14 | cmd := exec.Command("gpgconf") 15 | cmd.Stdout = buf 16 | cmd.Stderr = os.Stderr 17 | 18 | if err := cmd.Run(); err != nil { 19 | return "", err 20 | } 21 | 22 | key = strings.TrimSpace(strings.ToLower(key)) 23 | sc := bufio.NewScanner(buf) 24 | for sc.Scan() { 25 | p := strings.Split(strings.TrimSpace(sc.Text()), ":") 26 | if len(p) < 3 { 27 | continue 28 | } 29 | if key == p[0] { 30 | return p[2], nil 31 | } 32 | } 33 | 34 | return "", nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/utils_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package gpgconf 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | var fd0 = "/proc/self/fd/0" 12 | 13 | // TTY returns the tty of the current process. 14 | // see https://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html 15 | func TTY() string { 16 | dest, err := os.Readlink(fd0) 17 | if err != nil { 18 | return "" 19 | } 20 | 21 | return dest 22 | } 23 | 24 | // Umask sets the desired umask. 25 | func Umask(mask int) int { 26 | return syscall.Umask(mask) 27 | } 28 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/utils_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package gpgconf 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTTY(t *testing.T) { 13 | t.Parallel() 14 | 15 | fd0 = "/tmp/foobar" 16 | assert.Empty(t, TTY()) 17 | } 18 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/utils_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | package gpgconf 5 | 6 | import ( 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | ) 11 | 12 | func TTY() string { 13 | cmd := exec.Command("/usr/bin/tty") 14 | cmd.Stdin = os.Stdin 15 | out, err := cmd.Output() 16 | if err != nil { 17 | return "" 18 | } 19 | 20 | return string(out) 21 | } 22 | 23 | func Umask(mask int) int { 24 | return syscall.Umask(mask) 25 | } 26 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/utils_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package gpgconf 5 | 6 | func TTY() string { 7 | return "" 8 | } 9 | 10 | func Umask(mask int) int { 11 | return -1 12 | } 13 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/version.go: -------------------------------------------------------------------------------- 1 | package gpgconf 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/blang/semver/v4" 9 | ) 10 | 11 | type gpgBin struct { 12 | path string 13 | ver semver.Version 14 | } 15 | 16 | type byVersion []gpgBin 17 | 18 | func (v byVersion) Len() int { 19 | return len(v) 20 | } 21 | 22 | func (v byVersion) Swap(i, j int) { 23 | v[i], v[j] = v[j], v[i] 24 | } 25 | 26 | func (v byVersion) Less(i, j int) bool { 27 | return v[i].ver.LT(v[j].ver) 28 | } 29 | 30 | // Version return the version of the gpg binary. 31 | func Version(ctx context.Context, binary string) semver.Version { 32 | v := semver.Version{} 33 | 34 | cmd := exec.CommandContext(ctx, binary, "--version") 35 | out, err := cmd.Output() 36 | if err != nil { 37 | return v 38 | } 39 | 40 | for _, line := range strings.Split(string(out), "\n") { 41 | line = strings.TrimSpace(line) 42 | if !strings.HasPrefix(line, "gpg ") { 43 | continue 44 | } 45 | 46 | p := strings.Fields(line) 47 | if len(p) < 1 { 48 | continue 49 | } 50 | 51 | sv, err := semver.Parse(p[len(p)-1]) 52 | if err != nil { 53 | continue 54 | } 55 | 56 | return sv 57 | } 58 | 59 | return v 60 | } 61 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/gpgconf/version_test.go: -------------------------------------------------------------------------------- 1 | package gpgconf 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/blang/semver/v4" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSort(t *testing.T) { 12 | t.Parallel() 13 | 14 | for _, tc := range []struct { 15 | name string 16 | in []gpgBin 17 | out []semver.Version 18 | }{ 19 | { 20 | name: "simple", 21 | in: []gpgBin{ 22 | { 23 | path: "/usr/local/bin/gpg", 24 | ver: semver.MustParse("1.9.1"), 25 | }, 26 | { 27 | path: "/usr/bin/gpg", 28 | ver: semver.MustParse("2.4.0"), 29 | }, 30 | { 31 | path: "/usr/local/bin/gpg2", 32 | ver: semver.MustParse("2.1.11"), 33 | }, 34 | }, 35 | out: []semver.Version{ 36 | semver.MustParse("1.9.1"), 37 | semver.MustParse("2.1.11"), 38 | semver.MustParse("2.4.0"), 39 | }, 40 | }, 41 | } { 42 | t.Run(tc.name, func(t *testing.T) { 43 | t.Parallel() 44 | 45 | sort.Sort(byVersion(tc.in)) 46 | 47 | require.Len(t, tc.in, len(tc.out)) 48 | for i, v := range tc.out { 49 | if !tc.in[i].ver.Equals(v) { 50 | t.Errorf("wrong sort order at %d: %s != %s", i, tc.in[i].ver, v) 51 | } 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/identity.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import "time" 4 | 5 | // Identity is a GPG identity, one key can have many IDs. 6 | type Identity struct { 7 | Name string 8 | Comment string 9 | Email string 10 | CreationDate time.Time 11 | ExpirationDate time.Time 12 | } 13 | 14 | // ID returns the GPG ID format. 15 | func (i Identity) ID() string { 16 | out := i.Name 17 | 18 | if i.Comment != "" { 19 | out += " (" + i.Comment + ")" 20 | } 21 | 22 | out += " <" + i.Email + ">" 23 | 24 | return out 25 | } 26 | 27 | // String implement fmt.Stringer. This method resembles the output gpg uses 28 | // for user-ids. 29 | func (i Identity) String() string { 30 | return "uid " + i.ID() 31 | } 32 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpg/identity_test.go: -------------------------------------------------------------------------------- 1 | package gpg 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIdentity(t *testing.T) { 11 | t.Parallel() 12 | 13 | id := Identity{ 14 | Name: "John Doe", 15 | Comment: "johnny", 16 | Email: "john.doe@example.org", 17 | CreationDate: time.Now(), 18 | ExpirationDate: time.Now().Add(time.Hour), 19 | } 20 | 21 | assert.Equal(t, "John Doe (johnny) ", id.ID()) 22 | assert.Equal(t, "uid "+id.ID(), id.String()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/backend/crypto/gpgcli.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/crypto/gpg/cli" // register gpg cli backend 4 | -------------------------------------------------------------------------------- /internal/backend/crypto/plain.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/crypto/plain" // register plaintext backend 4 | -------------------------------------------------------------------------------- /internal/backend/crypto/plain/loader.go: -------------------------------------------------------------------------------- 1 | package plain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gopasspw/gopass/internal/backend" 8 | "github.com/gopasspw/gopass/pkg/debug" 9 | ) 10 | 11 | const ( 12 | name = "plain" 13 | ) 14 | 15 | func init() { 16 | backend.CryptoRegistry.Register(backend.Plain, name, &loader{}) 17 | } 18 | 19 | type loader struct{} 20 | 21 | // New implements backend.CryptoLoader. 22 | func (l loader) New(ctx context.Context) (backend.Crypto, error) { 23 | debug.Log("Using Crypto Backend: %s (NO ENCRYPTION)", name) 24 | 25 | return New(), nil 26 | } 27 | 28 | func (l loader) Handles(ctx context.Context, s backend.Storage) error { 29 | if s.Exists(ctx, IDFile) { 30 | return nil 31 | } 32 | 33 | return fmt.Errorf("not supported") 34 | } 35 | 36 | func (l loader) Priority() int { 37 | return 1000 38 | } 39 | 40 | func (l loader) String() string { 41 | return name 42 | } 43 | -------------------------------------------------------------------------------- /internal/backend/crypto_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDetectCrypto(t *testing.T) { 14 | for _, tc := range []struct { 15 | name string 16 | file string 17 | }{ 18 | { 19 | name: "plain", 20 | file: ".plain-id", 21 | }, 22 | { 23 | name: "gpg", 24 | file: ".gpg-id", 25 | }, 26 | { 27 | name: "age", 28 | file: ".age-recipients", 29 | }, 30 | } { 31 | t.Run(tc.name, func(t *testing.T) { 32 | ctx := config.NewContextInMemory() 33 | 34 | fsDir := filepath.Join(t.TempDir(), "fs") 35 | _ = os.RemoveAll(fsDir) 36 | require.NoError(t, os.MkdirAll(fsDir, 0o700)) 37 | require.NoError(t, os.WriteFile(filepath.Join(fsDir, tc.file), []byte("foo"), 0o600)) 38 | 39 | r, err := DetectStorage(ctx, fsDir) 40 | require.NoError(t, err) 41 | assert.NotNil(t, r) 42 | assert.Equal(t, "fs", r.Name()) 43 | 44 | c, err := DetectCrypto(ctx, r) 45 | require.NoError(t, err, tc.name) 46 | require.NotNil(t, c, tc.name) 47 | assert.Equal(t, tc.name, c.Name()) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/backend/doc.go: -------------------------------------------------------------------------------- 1 | // Package backend implements a registry to register differnet plugable backends for encryption and storage (incl. version control). 2 | // The actual backends are implemented in the subpackages. They register themselves in the registry with blank imports. 3 | package backend 4 | -------------------------------------------------------------------------------- /internal/backend/rcs_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/gopasspw/gopass/internal/config" 11 | "github.com/gopasspw/gopass/internal/out" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestClone(t *testing.T) { 17 | ctx := config.NewContextInMemory() 18 | 19 | td := t.TempDir() 20 | 21 | repo := filepath.Join(td, "repo") 22 | require.NoError(t, os.MkdirAll(repo, 0o700)) 23 | 24 | store := filepath.Join(td, "store") 25 | require.NoError(t, os.MkdirAll(store, 0o700)) 26 | 27 | cmd := exec.Command("git", "init", repo) 28 | require.NoError(t, cmd.Run()) 29 | 30 | r, err := Clone(ctx, GitFS, repo, store) 31 | require.NoError(t, err) 32 | assert.NotNil(t, r) 33 | } 34 | 35 | func TestInitRCS(t *testing.T) { 36 | ctx := config.NewContextInMemory() 37 | 38 | td := t.TempDir() 39 | 40 | buf := &bytes.Buffer{} 41 | out.Stdout = buf 42 | out.Stderr = buf 43 | defer func() { 44 | out.Stdout = os.Stdout 45 | out.Stderr = os.Stderr 46 | }() 47 | 48 | gitDir := filepath.Join(td, "git") 49 | require.NoError(t, os.MkdirAll(filepath.Join(gitDir, ".git"), 0o700)) 50 | 51 | r, err := InitStorage(ctx, GitFS, gitDir) 52 | require.NoError(t, err) 53 | assert.NotNil(t, r) 54 | } 55 | -------------------------------------------------------------------------------- /internal/backend/storage/doc.go: -------------------------------------------------------------------------------- 1 | // Package storage provides a pluggable storage backend for gopass. 2 | 3 | package storage 4 | -------------------------------------------------------------------------------- /internal/backend/storage/fossilfs.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/storage/fossilfs" // register fossilfs backend 4 | -------------------------------------------------------------------------------- /internal/backend/storage/fossilfs/context.go: -------------------------------------------------------------------------------- 1 | package fossilfs 2 | 3 | import "context" 4 | 5 | type contextKey int 6 | 7 | const ( 8 | ctxKeyPathOverride contextKey = iota 9 | ) 10 | 11 | func withPathOverride(ctx context.Context, path string) context.Context { 12 | return context.WithValue(ctx, ctxKeyPathOverride, path) 13 | } 14 | 15 | func getPathOverride(ctx context.Context, def string) string { 16 | if sv, ok := ctx.Value(ctxKeyPathOverride).(string); ok && sv != "" { 17 | return sv 18 | } 19 | 20 | return def 21 | } 22 | -------------------------------------------------------------------------------- /internal/backend/storage/fossilfs/context_test.go: -------------------------------------------------------------------------------- 1 | package fossilfs 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithPathOverride(t *testing.T) { 8 | ctx := t.Context() 9 | path := "/test/path" 10 | ctx = withPathOverride(ctx, path) 11 | 12 | if val, ok := ctx.Value(ctxKeyPathOverride).(string); !ok || val != path { 13 | t.Errorf("Expected path %s, but got %v", path, val) 14 | } 15 | } 16 | 17 | func TestGetPathOverride(t *testing.T) { 18 | ctx := t.Context() 19 | defaultPath := "/default/path" 20 | 21 | // Test with no override 22 | if path := getPathOverride(ctx, defaultPath); path != defaultPath { 23 | t.Errorf("Expected default path %s, but got %s", defaultPath, path) 24 | } 25 | 26 | // Test with override 27 | overridePath := "/override/path" 28 | ctx = withPathOverride(ctx, overridePath) 29 | if path := getPathOverride(ctx, defaultPath); path != overridePath { 30 | t.Errorf("Expected override path %s, but got %s", overridePath, path) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/backend/storage/fossilfs/loader.go: -------------------------------------------------------------------------------- 1 | package fossilfs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/gopasspw/gopass/internal/backend" 9 | "github.com/gopasspw/gopass/pkg/fsutil" 10 | ) 11 | 12 | const ( 13 | name = "fossilfs" 14 | ) 15 | 16 | func init() { 17 | backend.StorageRegistry.Register(backend.FossilFS, name, &loader{}) 18 | } 19 | 20 | type loader struct{} 21 | 22 | func (l loader) New(ctx context.Context, path string) (backend.Storage, error) { 23 | return New(path) 24 | } 25 | 26 | func (l loader) Open(ctx context.Context, path string) (backend.Storage, error) { 27 | return New(path) 28 | } 29 | 30 | func (l loader) Clone(ctx context.Context, repo, path string) (backend.Storage, error) { 31 | return Clone(ctx, repo, path) 32 | } 33 | 34 | func (l loader) Init(ctx context.Context, path string) (backend.Storage, error) { 35 | return Init(ctx, path, "", "") 36 | } 37 | 38 | func (l loader) Handles(ctx context.Context, path string) error { 39 | path = fsutil.ExpandHomedir(path) 40 | 41 | marker := filepath.Join(path, CheckoutMarker) 42 | if !fsutil.IsFile(marker) { 43 | return fmt.Errorf("no fossil checkout marker found at %s", marker) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (l loader) Priority() int { 50 | return 2 51 | } 52 | 53 | func (l loader) String() string { 54 | return name 55 | } 56 | -------------------------------------------------------------------------------- /internal/backend/storage/fs.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/storage/fs" // register fs backend 4 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/fsck_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/ctxutil" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestFsck(t *testing.T) { 14 | t.Parallel() 15 | 16 | ctx := config.NewContextInMemory() 17 | ctx = ctxutil.WithHidden(ctx, true) 18 | 19 | path := t.TempDir() 20 | 21 | l := &loader{} 22 | s, err := l.Init(ctx, path) 23 | require.NoError(t, err) 24 | require.NoError(t, l.Handles(ctx, path)) 25 | 26 | for _, fn := range []string{ 27 | filepath.Join(path, ".plain-ids"), 28 | filepath.Join(path, "foo", "bar"), 29 | filepath.Join(path, "foo", "zen"), 30 | } { 31 | require.NoError(t, os.MkdirAll(filepath.Dir(fn), 0o777)) 32 | require.NoError(t, os.WriteFile(fn, []byte(fn), 0o663)) 33 | } 34 | 35 | require.NoError(t, s.Fsck(ctx)) 36 | } 37 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/link_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLongestCommonPrefix(t *testing.T) { 10 | t.Parallel() 11 | 12 | for _, tc := range []struct { 13 | Src string 14 | Dst string 15 | Prefix string 16 | }{ 17 | { 18 | Src: "foo/bar/baz/zab.txt", 19 | Dst: "foo/baz/foo.txt", 20 | Prefix: "foo", 21 | }, 22 | } { 23 | prefix := longestCommonPrefix(tc.Src, tc.Dst) 24 | assert.Equal(t, tc.Prefix, prefix) 25 | } 26 | } 27 | 28 | func TestAddRel(t *testing.T) { 29 | t.Parallel() 30 | 31 | for _, tc := range []struct { 32 | Src string 33 | Dst string 34 | Out string 35 | }{ 36 | { 37 | Src: "bar/baz.txt", 38 | Dst: "baz/foo.txt", 39 | Out: "../bar/baz.txt", 40 | }, 41 | } { 42 | assert.Equal(t, tc.Out, addRel(tc.Src, tc.Dst)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/rcs_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRCS(t *testing.T) { 12 | t.Parallel() 13 | 14 | ctx := config.NewContextInMemory() 15 | path := t.TempDir() 16 | 17 | g := New(path) 18 | // the fs backend does not support the RCS operations 19 | require.Error(t, g.Add(ctx, "foo", "bar")) 20 | require.Error(t, g.Commit(ctx, "foobar")) 21 | require.Error(t, g.Push(ctx, "foo", "bar")) 22 | require.Error(t, g.Pull(ctx, "foo", "bar")) 23 | require.NoError(t, g.Cmd(ctx, "foo", "bar")) 24 | require.Error(t, g.Init(ctx, "foo", "bar")) 25 | require.NoError(t, g.InitConfig(ctx, "foo", "bar")) 26 | assert.Equal(t, "fs", g.Name()) 27 | require.Error(t, g.AddRemote(ctx, "foo", "bar")) 28 | revs, err := g.Revisions(ctx, "foo") 29 | require.Error(t, err) 30 | assert.Len(t, revs, 1) 31 | body, err := g.GetRevision(ctx, "foo", "latest") 32 | require.Error(t, err) 33 | assert.Empty(t, string(body)) 34 | require.Error(t, g.RemoveRemote(ctx, "foo")) 35 | } 36 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/store_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package fs 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "syscall" 10 | ) 11 | 12 | func notEmptyErr(err error) bool { 13 | var perr *os.PathError 14 | if errors.As(err, &perr) { 15 | return errors.Is(perr.Err, syscall.ENOTEMPTY) 16 | } 17 | 18 | return false 19 | } 20 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/store_windows.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func notEmptyErr(err error) bool { 9 | return err.(*os.PathError).Err == syscall.ERROR_DIR_NOT_EMPTY 10 | } 11 | -------------------------------------------------------------------------------- /internal/backend/storage/fs/walk.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func walkSymlinks(path string, walkFn filepath.WalkFunc) error { 10 | return walk(path, path, walkFn) 11 | } 12 | 13 | func walk(filename, linkDir string, walkFn filepath.WalkFunc) error { 14 | sWalkFn := func(path string, info fs.FileInfo, _ error) error { 15 | fname, err := filepath.Rel(filename, path) 16 | if err != nil { 17 | return err 18 | } 19 | path = filepath.Join(linkDir, fname) 20 | 21 | // handle non-symlinks 22 | if info.Mode()&fs.ModeSymlink != fs.ModeSymlink { 23 | return walkFn(path, info, err) 24 | } 25 | 26 | // handle symlinks 27 | destPath, err := filepath.EvalSymlinks(path) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | destInfo, err := os.Lstat(destPath) 33 | if err != nil { 34 | return walkFn(path, destInfo, err) 35 | } 36 | 37 | if destInfo.IsDir() { 38 | return walk(destPath, path, walkFn) 39 | } 40 | 41 | return walkFn(path, info, err) 42 | } 43 | 44 | return filepath.Walk(filename, sWalkFn) 45 | } 46 | -------------------------------------------------------------------------------- /internal/backend/storage/gitfs.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import _ "github.com/gopasspw/gopass/internal/backend/storage/gitfs" // register gitfs backend 4 | -------------------------------------------------------------------------------- /internal/backend/storage/gitfs/config_test.go: -------------------------------------------------------------------------------- 1 | package gitfs 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/gopasspw/gopass/internal/out" 11 | "github.com/gopasspw/gopass/pkg/ctxutil" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestGitConfig(t *testing.T) { 17 | gitdir := filepath.Join(t.TempDir(), "git") 18 | require.NoError(t, os.Mkdir(gitdir, 0o755)) 19 | 20 | ctx := config.NewContextInMemory() 21 | ctx = ctxutil.WithAlwaysYes(ctx, true) 22 | 23 | buf := &bytes.Buffer{} 24 | out.Stdout = buf 25 | defer func() { 26 | out.Stdout = os.Stdout 27 | }() 28 | 29 | git, err := Init(ctx, gitdir, "Dead Beef", "dead.beef@example.org") 30 | require.NoError(t, err) 31 | un, err := git.ConfigGet(ctx, "user.name") 32 | require.NoError(t, err) 33 | assert.Equal(t, "Dead Beef", un) 34 | 35 | require.NoError(t, git.InitConfig(ctx, "Foo Bar", "foo.bar@example.org")) 36 | un, err = git.ConfigGet(ctx, "user.name") 37 | require.NoError(t, err) 38 | assert.Equal(t, "Foo Bar", un) 39 | 40 | require.NoError(t, git.ConfigSet(ctx, "user.name", "foo")) 41 | un, err = git.ConfigGet(ctx, "user.name") 42 | require.NoError(t, err) 43 | assert.Equal(t, "foo", un) 44 | } 45 | -------------------------------------------------------------------------------- /internal/backend/storage/gitfs/ssh_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package gitfs 5 | 6 | // gitSSHCommand returns a SSH command instructing git to use SSH 7 | // with persistent connections through a custom socket. 8 | // See https://linux.die.net/man/5/ssh_config and 9 | // https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand 10 | // 11 | // Note: Setting GIT_SSH_COMMAND, possibly to an empty string, will take 12 | // precedence over this setting. 13 | // 14 | // %C is a hash of %l%h%p%r and should avoid "path too long for unix domain socket" 15 | // errors. On MacOS this doesn't always seem to work, so we're using a hardcoded 16 | // /tmp instead. 17 | func gitSSHCommand() string { 18 | return "ssh -oControlMaster=auto -oControlPersist=600 -oControlPath=/tmp/.ssh-%C" 19 | } 20 | -------------------------------------------------------------------------------- /internal/backend/storage/gitfs/ssh_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin 2 | // +build !windows,!darwin 3 | 4 | package gitfs 5 | 6 | import "os" 7 | 8 | // gitSSHCommand returns a SSH command instructing git to use SSH 9 | // with persistent connections through a custom socket. 10 | // See https://linux.die.net/man/5/ssh_config and 11 | // https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand 12 | // 13 | // Note: Setting GIT_SSH_COMMAND, possibly to an empty string, will take 14 | // precedence over this setting. 15 | // 16 | // %C is a hash of %l%h%p%r and should avoid "path too long for unix domain socket" 17 | // errors. If you still encounter this error set TMPDIR to a short path, e.g. /tmp. 18 | func gitSSHCommand() string { 19 | return "ssh -oControlMaster=auto -oControlPersist=600 -oControlPath=" + os.TempDir() + "/.ssh-%C" 20 | } 21 | -------------------------------------------------------------------------------- /internal/backend/storage/gitfs/ssh_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package gitfs 5 | 6 | func gitSSHCommand() string { 7 | return "" 8 | } 9 | -------------------------------------------------------------------------------- /internal/backend/storage_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/ctxutil" 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestDetectStorage(t *testing.T) { 16 | ctx := config.NewContextInMemory() 17 | 18 | td := t.TempDir() 19 | 20 | // all tests involving age should set GOPASS_HOMEDIR 21 | t.Setenv("GOPASS_HOMEDIR", td) 22 | ctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) { 23 | debug.Log("static test password callback") 24 | 25 | return []byte("gopass"), nil 26 | }) 27 | 28 | fsDir := filepath.Join(td, "fs") 29 | require.NoError(t, os.MkdirAll(fsDir, 0o700)) 30 | 31 | t.Run("detect fs", func(t *testing.T) { 32 | r, err := DetectStorage(ctx, fsDir) 33 | require.NoError(t, err) 34 | assert.NotNil(t, r) 35 | assert.Equal(t, "fs", r.Name()) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/cache/disk_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestOnDisk(t *testing.T) { 12 | t.Parallel() 13 | 14 | td := t.TempDir() 15 | 16 | odc, err := NewOnDiskWithDir("test", td, time.Hour) 17 | require.NoError(t, err) 18 | 19 | require.NoError(t, odc.Set("foo", []string{"bar"})) 20 | res, err := odc.Get("foo") 21 | require.NoError(t, err) 22 | assert.Equal(t, []string{"bar"}, res) 23 | 24 | require.Error(t, odc.Remove("bar")) 25 | require.NoError(t, odc.Remove("foo")) 26 | require.NoError(t, odc.Purge()) 27 | } 28 | 29 | func TestOnDiskExpiry(t *testing.T) { 30 | t.Parallel() 31 | 32 | td := t.TempDir() 33 | 34 | odc, err := NewOnDiskWithDir("test", td, time.Second) 35 | require.NoError(t, err) 36 | require.NoError(t, odc.Set("foo", []string{"bar"})) 37 | res, err := odc.Get("foo") 38 | require.NoError(t, err) 39 | assert.Equal(t, []string{"bar"}, res) 40 | 41 | time.Sleep(time.Second + 100*time.Millisecond) 42 | res, err = odc.Get("foo") 43 | require.Error(t, err) 44 | assert.NotEqual(t, []string{"bar"}, res) 45 | } 46 | -------------------------------------------------------------------------------- /internal/cache/ghssh/cache.go: -------------------------------------------------------------------------------- 1 | package ghssh 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gopasspw/gopass/internal/cache" 8 | ) 9 | 10 | // Cache is a disk-backed GitHub SSH public key cache. 11 | type Cache struct { 12 | disk *cache.OnDisk 13 | Timeout time.Duration 14 | } 15 | 16 | // New creates a new github cache. 17 | func New() (*Cache, error) { 18 | cDir, err := cache.NewOnDisk("github-ssh", 6*time.Hour) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &Cache{ 24 | disk: cDir, 25 | Timeout: 30 * time.Second, 26 | }, nil 27 | } 28 | 29 | func (c *Cache) String() string { 30 | return fmt.Sprintf("Github SSH key cache (OnDisk: %s)", c.disk.String()) 31 | } 32 | -------------------------------------------------------------------------------- /internal/cache/ghssh/cache_test.go: -------------------------------------------------------------------------------- 1 | package ghssh 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | // Mock GOPASS_HOMEDIR to point to a temp directory 13 | tempDir := t.TempDir() 14 | t.Setenv("GOPASS_HOMEDIR", tempDir) 15 | 16 | c, err := New() 17 | require.NoError(t, err) 18 | assert.NotNil(t, c) 19 | assert.Equal(t, 30*time.Second, c.Timeout) 20 | assert.NotNil(t, c.disk) 21 | } 22 | 23 | func TestCache_String(t *testing.T) { 24 | // Mock GOPASS_HOMEDIR to point to a temp directory 25 | tempDir := t.TempDir() 26 | t.Setenv("GOPASS_HOMEDIR", tempDir) 27 | 28 | c, err := New() 29 | require.NoError(t, err) 30 | assert.NotNil(t, c) 31 | 32 | assert.Contains(t, c.String(), "Github SSH key cache (OnDisk:") 33 | assert.Contains(t, c.String(), tempDir) 34 | } 35 | -------------------------------------------------------------------------------- /internal/config/context.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gopasspw/gitconfig" 7 | "github.com/gopasspw/gopass/pkg/debug" 8 | ) 9 | 10 | type contextKey int 11 | 12 | const ( 13 | ctxKeyConfig contextKey = iota 14 | ctxKeyMountPoint 15 | ) 16 | 17 | func (c *Config) WithConfig(ctx context.Context) context.Context { 18 | return context.WithValue(ctx, ctxKeyConfig, c) 19 | } 20 | 21 | func WithMount(ctx context.Context, mp string) context.Context { 22 | return context.WithValue(ctx, ctxKeyMountPoint, mp) 23 | } 24 | 25 | // FromContext returns a config from a context, as well as the current mount point (store name) if found. 26 | func FromContext(ctx context.Context) (*Config, string) { 27 | mount := "" 28 | if m, found := ctx.Value(ctxKeyMountPoint).(string); found && m != "" { 29 | mount = m 30 | } 31 | 32 | if c, found := ctx.Value(ctxKeyConfig).(*Config); found && c != nil { 33 | return c, mount 34 | } 35 | 36 | debug.Log("no config in context, loading anew") 37 | 38 | cfg := &Config{ 39 | root: newGitconfig().LoadAll(""), 40 | } 41 | cfg.root.Preset = gitconfig.NewFromMap(defaults) 42 | 43 | return cfg, mount 44 | } 45 | -------------------------------------------------------------------------------- /internal/config/legacy.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gopasspw/gopass/internal/config/legacy" 7 | "github.com/gopasspw/gopass/pkg/debug" 8 | ) 9 | 10 | func migrateConfigs() error { 11 | cfg := legacy.LoadWithOptions(true, false) 12 | if cfg == nil { 13 | debug.V(2).Log("no legacy config found. not migrating.") 14 | 15 | return nil 16 | } 17 | 18 | c := newGitconfig().LoadAll(cfg.Path) 19 | 20 | for k, v := range cfg.ConfigMap() { 21 | var fk string 22 | switch k { 23 | case "keychain": 24 | fk = "age.usekeychain" 25 | case "path": 26 | fk = "mounts.path" 27 | case "safecontent": 28 | fk = "show.safecontent" 29 | case "autoclip": 30 | fk = "generate.autoclip" 31 | case "showautoclip": 32 | fk = "show.autoclip" 33 | default: 34 | fk = "core." + k 35 | } 36 | 37 | if err := c.SetGlobal(fk, v); err != nil { 38 | return fmt.Errorf("failed to write new config: %w", err) 39 | } 40 | } 41 | for alias, path := range cfg.Mounts { 42 | if err := c.SetGlobal(mpk(alias), path); err != nil { 43 | return fmt.Errorf("failed to write new config: %w", err) 44 | } 45 | } 46 | 47 | debug.Log("migrated legacy config from %s", cfg.ConfigPath) 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/config/legacy/location_xdg_test.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !windows 2 | // +build !darwin,!windows 3 | 4 | package legacy 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestConfigLocations(t *testing.T) { 15 | gpcfg := filepath.Join(os.TempDir(), "config", ".gopass.yml") 16 | xdghome := filepath.Join(os.TempDir(), "xdg") 17 | gphome := filepath.Join(os.TempDir(), "home") 18 | 19 | xdgcfg := filepath.Join(xdghome, "gopass", "config.yml") 20 | curcfg := filepath.Join(gphome, ".config", "gopass", "config.yml") 21 | oldcfg := filepath.Join(gphome, ".gopass.yml") 22 | 23 | t.Run("GOPASS_CONFIG, GOPASS_HOMEDIR set", func(t *testing.T) { 24 | t.Setenv("GOPASS_CONFIG", gpcfg) 25 | t.Setenv("GOPASS_HOMEDIR", gphome) 26 | 27 | assert.Equal(t, []string{gpcfg, curcfg, curcfg, oldcfg}, ConfigLocations()) 28 | }) 29 | 30 | t.Run("GOPASS_CONFIG, GOPASS_HOMEDIR, XDG_CONFIG_HOME set", func(t *testing.T) { 31 | t.Setenv("GOPASS_CONFIG", gpcfg) 32 | t.Setenv("GOPASS_HOMEDIR", gphome) 33 | t.Setenv("XDG_CONFIG_HOME", xdghome) 34 | 35 | assert.Equal(t, []string{gpcfg, curcfg, curcfg, oldcfg}, ConfigLocations()) 36 | }) 37 | 38 | t.Run("XDG_CONFIG_HOME set only", func(t *testing.T) { 39 | t.Setenv("GOPASS_HOMEDIR", "") 40 | t.Setenv("XDG_CONFIG_HOME", xdghome) 41 | assert.Equal(t, xdgcfg, ConfigLocations()[0]) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/config/location_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/pkg/appdir" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPwStoreDirNoEnv(t *testing.T) { 13 | if runtime.GOOS != "windows" { 14 | t.Setenv("GOPASS_HOMEDIR", "/tmp") 15 | } 16 | 17 | baseDir := filepath.Join(appdir.UserHome(), ".local", "share", "gopass", "stores") 18 | if runtime.GOOS == "windows" { 19 | baseDir = filepath.Join(appdir.UserHome(), "AppData", "Local", "gopass", "stores") 20 | } 21 | 22 | for in, out := range map[string]string{ 23 | "": filepath.Join(baseDir, "root"), 24 | "work": filepath.Join(baseDir, "work"), 25 | filepath.Join("foo", "bar"): filepath.Join(baseDir, "foo-bar"), 26 | } { 27 | assert.Equal(t, out, PwStoreDir(in), in, "mount "+in) 28 | } 29 | } 30 | 31 | func TestDirectory(t *testing.T) { 32 | t.Parallel() 33 | 34 | loc := configLocation() 35 | dir := filepath.Dir(loc) 36 | assert.Equal(t, dir, Directory()) 37 | } 38 | -------------------------------------------------------------------------------- /internal/create/helpers.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/fatih/color" 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | "github.com/gopasspw/gopass/pkg/fsutil" 12 | ) 13 | 14 | func fmtfn(d int, n string, t string) string { 15 | strlen := 40 - d 16 | // indent - [N] - text (trailing spaces) 17 | fmtStr := "%" + strconv.Itoa(d) + "s%s %-" + strconv.Itoa(strlen) + "s" 18 | debug.Log("d: %d, n: %q, t: %q, strlen: %d, fmtStr: %q", d, n, t, strlen, fmtStr) 19 | 20 | return fmt.Sprintf(fmtStr, "", color.GreenString("["+n+"]"), t) 21 | } 22 | 23 | // extractHostname tries to extract the hostname from a URL in a filepath-safe 24 | // way for use in the name of a secret. 25 | func extractHostname(in string) string { 26 | if in == "" { 27 | return "" 28 | } 29 | // help url.Parse by adding a scheme if one is missing. This should still 30 | // allow for any scheme, but by default we assume http (only for parsing) 31 | urlStr := in 32 | if !strings.Contains(urlStr, "://") { 33 | urlStr = "http://" + urlStr 34 | } 35 | 36 | u, err := url.Parse(urlStr) 37 | if err == nil { 38 | if ch := fsutil.CleanFilename(u.Hostname()); ch != "" { 39 | return ch 40 | } 41 | } 42 | 43 | return fsutil.CleanFilename(in) 44 | } 45 | -------------------------------------------------------------------------------- /internal/cui/actions.go: -------------------------------------------------------------------------------- 1 | package cui 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | // Action is a action which can be selected. 11 | type Action struct { 12 | Name string 13 | Fn func(context.Context, *cli.Context) error 14 | } 15 | 16 | // Actions is a list of actions. 17 | type Actions []Action 18 | 19 | // Selection return the list of actions. 20 | func (ca Actions) Selection() []string { 21 | keys := make([]string, 0, len(ca)) 22 | for _, a := range ca { 23 | keys = append(keys, a.Name) 24 | } 25 | 26 | return keys 27 | } 28 | 29 | // Run executes the selected action. 30 | func (ca Actions) Run(ctx context.Context, c *cli.Context, i int) error { 31 | if len(ca) < i || i >= len(ca) { 32 | return errors.New("action not found") 33 | } 34 | if ca[i].Fn == nil { 35 | return errors.New("action invalid") 36 | } 37 | 38 | return ca[i].Fn(ctx, c) 39 | } 40 | -------------------------------------------------------------------------------- /internal/cui/actions_test.go: -------------------------------------------------------------------------------- 1 | package cui 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func TestCreateActions(t *testing.T) { 14 | t.Parallel() 15 | 16 | ctx := config.NewContextInMemory() 17 | cas := Actions{ 18 | { 19 | Name: "foo", 20 | }, 21 | { 22 | Name: "bar", 23 | Fn: func(context.Context, *cli.Context) error { 24 | return nil 25 | }, 26 | }, 27 | } 28 | assert.Equal(t, []string{"foo", "bar"}, cas.Selection()) 29 | require.Error(t, cas.Run(ctx, nil, 0)) 30 | require.NoError(t, cas.Run(ctx, nil, 1)) 31 | require.Error(t, cas.Run(ctx, nil, 2)) 32 | require.Error(t, cas.Run(ctx, nil, 66)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/cui/cui.go: -------------------------------------------------------------------------------- 1 | // Package cui provides a simple command line user interface 2 | // for gopass. It is used to interact with the user. 3 | package cui 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/fatih/color" 11 | "github.com/gopasspw/gopass/pkg/ctxutil" 12 | "github.com/gopasspw/gopass/pkg/termio" 13 | ) 14 | 15 | // GetSelection show a navigable multiple-choice list to the user 16 | // and returns the selected entry along with the action. 17 | func GetSelection(ctx context.Context, prompt string, choices []string) (string, int) { 18 | if ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) { 19 | return "impossible", 0 20 | } 21 | 22 | for i, c := range choices { 23 | fmt.Print(color.GreenString("[% d]", i)) 24 | fmt.Printf(" %s\n", c) 25 | } 26 | fmt.Println() 27 | var i int 28 | for { 29 | var err error 30 | i, err = termio.AskForInt(ctx, prompt, 0) 31 | if err == nil && i < len(choices) { 32 | break 33 | } 34 | if errors.Is(err, termio.ErrAborted) { 35 | return "aborted", 0 36 | } 37 | if err != nil { 38 | fmt.Println(err.Error()) 39 | } 40 | } 41 | fmt.Println(i) 42 | 43 | return "default", i 44 | } 45 | -------------------------------------------------------------------------------- /internal/cui/cui_test.go: -------------------------------------------------------------------------------- 1 | package cui 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetSelection(t *testing.T) { 12 | t.Parallel() 13 | 14 | ctx := config.NewContextInMemory() 15 | ctx = ctxutil.WithInteractive(ctx, false) 16 | 17 | act, sel := GetSelection(ctx, "foo", []string{"foo", "bar"}) 18 | assert.Equal(t, "impossible", act) 19 | assert.Equal(t, 0, sel) 20 | } 21 | -------------------------------------------------------------------------------- /internal/diff/diff.go: -------------------------------------------------------------------------------- 1 | // Package diff implements diffing of two lists. 2 | package diff 3 | 4 | // Stat returnes the number of items added to and removed from the first to 5 | // the second list. 6 | func Stat[K comparable](l, r []K) (int, int) { 7 | added, removed := List(l, r) 8 | 9 | return len(added), len(removed) 10 | } 11 | 12 | // List returns two lists, the first one contains the items that were added from left 13 | // to right, the second one contains the items that were removed from left to right. 14 | func List[K comparable](l, r []K) ([]K, []K) { 15 | ml := listToMap(l) 16 | mr := listToMap(r) 17 | 18 | var added []K 19 | 20 | for k := range mr { 21 | if _, found := ml[k]; !found { 22 | added = append(added, k) 23 | } 24 | } 25 | 26 | var removed []K 27 | 28 | for k := range ml { 29 | if _, found := mr[k]; !found { 30 | removed = append(removed, k) 31 | } 32 | } 33 | 34 | return added, removed 35 | } 36 | 37 | func listToMap[K comparable](l []K) map[K]struct{} { 38 | m := make(map[K]struct{}, len(l)) 39 | for _, e := range l { 40 | m[e] = struct{}{} 41 | } 42 | 43 | return m 44 | } 45 | -------------------------------------------------------------------------------- /internal/editor/edit_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package editor 5 | 6 | import ( 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/gopasspw/gopass/internal/config" 11 | "github.com/gopasspw/gopass/pkg/debug" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | // Path return the name/path of the preferred editor. 16 | func Path(c *cli.Context) string { 17 | if c != nil { 18 | if ed := c.String("editor"); ed != "" { 19 | debug.Log("Using editor from command line: %s", ed) 20 | 21 | return ed 22 | } 23 | } 24 | if ed := config.String(c.Context, "edit.editor"); ed != "" { 25 | debug.Log("Using editor from config: %s", ed) 26 | 27 | return ed 28 | } 29 | if ed := os.Getenv("EDITOR"); ed != "" { 30 | debug.Log("Using editor from $EDITOR: %s", ed) 31 | 32 | return ed 33 | } 34 | if p, err := exec.LookPath("editor"); err == nil { 35 | debug.Log("Using editor from $PATH: %s", p) 36 | 37 | return p 38 | } 39 | // if neither EDITOR is set nor "editor" available we'll just assume that vi 40 | // is installed. If this fails the user will have to set `$EDITOR`. 41 | debug.Log("Using default editor: %s", "vi") 42 | 43 | return "vi" 44 | } 45 | -------------------------------------------------------------------------------- /internal/editor/edit_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows 2 | // +build !linux,!windows 3 | 4 | package editor 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // Path return the name/path of the preferred editor. 14 | func Path(c *cli.Context) string { 15 | if c != nil { 16 | if ed := c.String("editor"); ed != "" { 17 | return ed 18 | } 19 | } 20 | 21 | if ed := config.String(c.Context, "edit.editor"); ed != "" { 22 | return ed 23 | } 24 | 25 | if ed := os.Getenv("EDITOR"); ed != "" { 26 | return ed 27 | } 28 | 29 | // given, this is a very opinionated default, but this should be available 30 | // on virtually any UNIX system and the user can still set EDITOR to get 31 | // his favorite one 32 | return "vi" 33 | } 34 | -------------------------------------------------------------------------------- /internal/editor/edit_test.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestEdit(t *testing.T) { 15 | ctx := config.NewContextInMemory() 16 | ctx = ctxutil.WithAlwaysYes(ctx, true) 17 | ctx = ctxutil.WithTerminal(ctx, false) 18 | 19 | buf := &bytes.Buffer{} 20 | out.Stdout = buf 21 | defer func() { 22 | out.Stdout = os.Stdout 23 | }() 24 | 25 | _, err := Invoke(ctx, "true", []byte{}) 26 | require.Error(t, err) 27 | buf.Reset() 28 | } 29 | -------------------------------------------------------------------------------- /internal/editor/edit_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package editor 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // Path return the name/path of the preferred editor 14 | func Path(c *cli.Context) string { 15 | if c != nil { 16 | if ed := c.String("editor"); ed != "" { 17 | return ed 18 | } 19 | } 20 | if ed := config.String(c.Context, "edit.editor"); ed != "" { 21 | return ed 22 | } 23 | if ed := os.Getenv("EDITOR"); ed != "" { 24 | return ed 25 | } 26 | return "notepad.exe" 27 | } 28 | -------------------------------------------------------------------------------- /internal/env/doc.go: -------------------------------------------------------------------------------- 1 | // Package env provides a way to validate the environment 2 | // and the configuration of gopass. 3 | 4 | package env 5 | -------------------------------------------------------------------------------- /internal/env/env_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package env 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | // Stdin is exported for tests. 18 | Stdin io.Reader = os.Stdin 19 | // Stderr is exported for tests. 20 | Stderr io.Writer = os.Stderr 21 | ) 22 | 23 | // Check validates the runtime environment on MacOS. 24 | // It checks if the keychain is used. 25 | func Check(ctx context.Context) (string, error) { 26 | buf := &bytes.Buffer{} 27 | 28 | cmd := exec.CommandContext(ctx, "defaults", "read", "org.gpgtools.common", "UseKeychain") 29 | cmd.Stdin = Stdin 30 | cmd.Stdout = buf 31 | cmd.Stderr = Stderr 32 | 33 | if err := cmd.Run(); err != nil { 34 | return "", fmt.Errorf("`default read org.gpgtools.common UseKeychain` failed: %w", err) 35 | } 36 | 37 | // if the keychain is not used, we can skip the rest 38 | if strings.ToUpper(strings.TrimSpace(buf.String())) == "NO" { 39 | return "", nil 40 | } 41 | 42 | // gpg uses the keychain to store the passphrase, warn once in a while that users 43 | // might want to change that because it's not secure. 44 | return "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 45 | } 46 | -------------------------------------------------------------------------------- /internal/env/env_others.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin 2 | // +build !darwin 3 | 4 | package env 5 | 6 | import "context" 7 | 8 | // Check does nothing on these OSes, yet. 9 | func Check(ctx context.Context) (string, error) { 10 | return "", nil 11 | } 12 | -------------------------------------------------------------------------------- /internal/hashsum/hashsums.go: -------------------------------------------------------------------------------- 1 | // Package hashsum provides hash functions for various algorithms. 2 | package hashsum 3 | 4 | import ( 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "crypto/sha256" 8 | "crypto/sha512" 9 | "fmt" 10 | 11 | "github.com/zeebo/blake3" 12 | ) 13 | 14 | func MD5Hex(in string) string { 15 | return fmt.Sprintf("%x", md5.Sum([]byte(in))) 16 | } 17 | 18 | func SHA1Hex(in string) string { 19 | return fmt.Sprintf("%x", sha1.Sum([]byte(in))) 20 | } 21 | 22 | func SHA256Hex(in string) string { 23 | return fmt.Sprintf("%x", sha256.Sum256([]byte(in))) 24 | } 25 | 26 | func SHA512Hex(in string) string { 27 | return fmt.Sprintf("%x", sha512.Sum512([]byte(in))) 28 | } 29 | 30 | func Blake3Hex(in string) string { 31 | return fmt.Sprintf("%x", blake3.Sum256([]byte(in))) 32 | } 33 | -------------------------------------------------------------------------------- /internal/notify/doc.go: -------------------------------------------------------------------------------- 1 | // Package notify provides a notification system for gopass. 2 | 3 | package notify 4 | -------------------------------------------------------------------------------- /internal/notify/notify_dbus.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package notify 5 | 6 | import ( 7 | "context" 8 | "os" 9 | 10 | "github.com/godbus/dbus/v5" 11 | "github.com/gopasspw/gopass/internal/config" 12 | "github.com/gopasspw/gopass/pkg/debug" 13 | ) 14 | 15 | // Notify displays a desktop notification with dbus. 16 | func Notify(ctx context.Context, subj, msg string) error { 17 | if os.Getenv("GOPASS_NO_NOTIFY") != "" || !config.Bool(ctx, "core.notifications") { 18 | debug.Log("Notifications disabled") 19 | 20 | return nil 21 | } 22 | conn, err := dbus.SessionBus() 23 | if err != nil { 24 | debug.Log("DBus failure: %s", err) 25 | 26 | return err 27 | } 28 | 29 | obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") 30 | call := 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)) 31 | if call.Err != nil { 32 | debug.Log("DBus notification failure: %s", call.Err) 33 | 34 | return call.Err 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/notify/notify_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !windows && !darwin 2 | // +build !linux,!windows,!darwin 3 | 4 | package notify 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "runtime" 10 | ) 11 | 12 | // Notify is not yet implemented on this platform 13 | func Notify(ctx context.Context, subj, msg string) error { 14 | return fmt.Errorf("GOOS %s not yet supported", runtime.GOOS) 15 | } 16 | -------------------------------------------------------------------------------- /internal/notify/notify_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "image/png" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gopasspw/gopass/internal/config" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNotify(t *testing.T) { 14 | ctx := config.NewContextInMemory() 15 | 16 | t.Setenv("GOPASS_NO_NOTIFY", "true") 17 | require.NoError(t, Notify(ctx, "foo", "bar")) 18 | } 19 | 20 | func TestIcon(t *testing.T) { 21 | t.Parallel() 22 | 23 | ctx := t.Context() 24 | 25 | fn := strings.TrimPrefix(iconURI(ctx), "file://") 26 | require.NoError(t, os.Remove(fn)) 27 | _ = iconURI(ctx) 28 | fh, err := os.Open(fn) 29 | require.NoError(t, err) 30 | 31 | defer func() { 32 | require.NoError(t, fh.Close()) 33 | }() 34 | 35 | require.NotNil(t, fh) 36 | _, err = png.Decode(fh) 37 | require.NoError(t, err) 38 | } 39 | -------------------------------------------------------------------------------- /internal/notify/notify_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package notify 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/gopasspw/gopass/internal/config" 12 | ) 13 | 14 | // Notify displays a desktop notification through msg 15 | func Notify(ctx context.Context, subj, msg string) error { 16 | if os.Getenv("GOPASS_NO_NOTIFY") != "" || !config.Bool(ctx, "core.notifications") { 17 | return nil 18 | } 19 | winmsg, err := exec.LookPath("msg") 20 | if err != nil { 21 | return err 22 | } 23 | 24 | return exec.Command(winmsg, 25 | "*", 26 | "/TIME:3", 27 | subj+"\n\n"+msg, 28 | ).Start() 29 | } 30 | -------------------------------------------------------------------------------- /internal/out/context.go: -------------------------------------------------------------------------------- 1 | package out 2 | 3 | import "context" 4 | 5 | type contextKey int 6 | 7 | const ( 8 | ctxKeyPrefix contextKey = iota 9 | ctxKeyNewline 10 | ) 11 | 12 | // WithPrefix returns a context with the given prefix set. 13 | func WithPrefix(ctx context.Context, prefix string) context.Context { 14 | return context.WithValue(ctx, ctxKeyPrefix, prefix) 15 | } 16 | 17 | // AddPrefix returns a context with the given prefix added to end of the 18 | // existing prefix. 19 | func AddPrefix(ctx context.Context, prefix string) context.Context { 20 | if prefix == "" { 21 | return ctx 22 | } 23 | 24 | pfx := Prefix(ctx) 25 | if pfx == "" { 26 | return WithPrefix(ctx, prefix) 27 | } 28 | 29 | return WithPrefix(ctx, pfx+prefix) 30 | } 31 | 32 | // Prefix returns the prefix or an empty string. 33 | func Prefix(ctx context.Context) string { 34 | sv, ok := ctx.Value(ctxKeyPrefix).(string) 35 | if !ok { 36 | return "" 37 | } 38 | 39 | return sv 40 | } 41 | 42 | // WithNewline returns a context with the flag value for newline set. 43 | func WithNewline(ctx context.Context, nl bool) context.Context { 44 | return context.WithValue(ctx, ctxKeyNewline, nl) 45 | } 46 | 47 | // HasNewline returns the value of newline or the default (true). 48 | func HasNewline(ctx context.Context) bool { 49 | bv, ok := ctx.Value(ctxKeyNewline).(bool) 50 | if !ok { 51 | return true 52 | } 53 | 54 | return bv 55 | } 56 | -------------------------------------------------------------------------------- /internal/out/context_test.go: -------------------------------------------------------------------------------- 1 | package out 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrefix(t *testing.T) { 11 | t.Parallel() 12 | 13 | ctx := config.NewContextInMemory() 14 | 15 | assert.Empty(t, Prefix(ctx)) 16 | 17 | ctx = AddPrefix(ctx, "[foo] ") 18 | assert.Equal(t, "[foo] ", Prefix(ctx)) 19 | 20 | ctx = AddPrefix(ctx, "[bar] ") 21 | assert.Equal(t, "[foo] [bar] ", Prefix(ctx)) 22 | 23 | ctx = AddPrefix(ctx, "") 24 | assert.Equal(t, "[foo] [bar] ", Prefix(ctx)) 25 | } 26 | 27 | func TestNewline(t *testing.T) { 28 | t.Parallel() 29 | 30 | ctx := config.NewContextInMemory() 31 | 32 | assert.True(t, HasNewline(ctx)) 33 | assert.False(t, HasNewline(WithNewline(ctx, false))) 34 | } 35 | -------------------------------------------------------------------------------- /internal/out/print_test.go: -------------------------------------------------------------------------------- 1 | package out 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/ctxutil" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPrint(t *testing.T) { 14 | ctx := config.NewContextInMemory() 15 | buf := &bytes.Buffer{} 16 | Stdout = buf 17 | defer func() { 18 | Stdout = os.Stdout 19 | }() 20 | 21 | Printf(ctx, "%s = %d", "foo", 42) 22 | assert.Equal(t, "foo = 42\n", buf.String()) 23 | buf.Reset() 24 | 25 | Printf(ctxutil.WithHidden(ctx, true), "%s = %d", "foo", 42) 26 | assert.Empty(t, buf.String()) 27 | buf.Reset() 28 | 29 | Printf(WithNewline(ctx, false), "%s = %d", "foo", 42) 30 | assert.Equal(t, "foo = 42", buf.String()) 31 | buf.Reset() 32 | } 33 | -------------------------------------------------------------------------------- /internal/pwschemes/argon2i/argon2i_test.go: -------------------------------------------------------------------------------- 1 | package argon2i 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestArgon2I(t *testing.T) { 11 | t.Parallel() 12 | 13 | pw := "foobar" 14 | hash, err := Generate(pw, 0) 15 | require.NoError(t, err) 16 | 17 | t.Logf("PW: %s - Hash: %s", pw, hash) 18 | ok, err := Validate(pw, hash) 19 | require.NoError(t, err) 20 | assert.True(t, ok) 21 | } 22 | -------------------------------------------------------------------------------- /internal/pwschemes/argon2id/argon2id_test.go: -------------------------------------------------------------------------------- 1 | package argon2id 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestArgon2ID(t *testing.T) { 11 | t.Parallel() 12 | 13 | pw := "foobar" 14 | hash, err := Generate(pw, 0) 15 | require.NoError(t, err) 16 | 17 | t.Logf("PW: %s - Hash: %s", pw, hash) 18 | ok, err := Validate(pw, hash) 19 | require.NoError(t, err) 20 | assert.True(t, ok) 21 | } 22 | -------------------------------------------------------------------------------- /internal/pwschemes/bcrypt/bcrypt.go: -------------------------------------------------------------------------------- 1 | // Package bcrypt provides a bcrypt password hashing scheme. 2 | // It is compatible with Dovecot and other systems that use bcrypt. 3 | package bcrypt 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "golang.org/x/crypto/bcrypt" 10 | ) 11 | 12 | const ( 13 | cost = 12 14 | ) 15 | 16 | // Prefix is set to be compatible with Dovecot. Can be set to an empty string. 17 | var Prefix = "{BLF-CRYPT}" 18 | 19 | // Generate generates a new Bcrypt hash with recommended values for it's 20 | // cost parameter. 21 | func Generate(password string) (string, error) { 22 | h, err := bcrypt.GenerateFromPassword([]byte(password), cost) 23 | if err != nil { 24 | return "", fmt.Errorf("failed to generate password hash: %w", err) 25 | } 26 | 27 | return Prefix + string(h), nil 28 | } 29 | 30 | // Validate validates the password against the given hash. 31 | func Validate(password, hash string) error { 32 | hash = strings.TrimPrefix(hash, Prefix) 33 | 34 | if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { 35 | return fmt.Errorf("failed to validate password hash %s: %w", hash, err) 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/pwschemes/bcrypt/bcrypt_test.go: -------------------------------------------------------------------------------- 1 | package bcrypt 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestBcrypt(t *testing.T) { 10 | t.Parallel() 11 | 12 | pw := "foobar" 13 | 14 | hash, err := Generate(pw) 15 | require.NoError(t, err) 16 | 17 | t.Logf("PW: %s - Hash: %s", pw, hash) 18 | 19 | require.NoError(t, Validate(pw, hash)) 20 | } 21 | -------------------------------------------------------------------------------- /internal/store/leaf/crypto_test.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestGPG(t *testing.T) { 14 | ctx := config.NewContextInMemory() 15 | 16 | obuf := &bytes.Buffer{} 17 | out.Stdout = obuf 18 | defer func() { 19 | out.Stdout = os.Stdout 20 | }() 21 | 22 | s, err := createSubStore(t) 23 | require.NoError(t, err) 24 | 25 | require.NoError(t, s.ImportMissingPublicKeys(ctx)) 26 | 27 | newRecp := "A3683834" 28 | err = s.AddRecipient(ctx, newRecp) 29 | require.NoError(t, err) 30 | 31 | require.NoError(t, s.ImportMissingPublicKeys(ctx)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/store/leaf/init_test.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestInit(t *testing.T) { 11 | ctx := config.NewContextInMemory() 12 | 13 | s, err := createSubStore(t) 14 | require.NoError(t, err) 15 | require.Error(t, s.Init(ctx, "", "0xDEADBEEF")) 16 | } 17 | -------------------------------------------------------------------------------- /internal/store/leaf/link.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/gopasspw/gopass/internal/queue" 9 | "github.com/gopasspw/gopass/internal/store" 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | ) 12 | 13 | // Link creates a symlink. 14 | func (s *Store) Link(ctx context.Context, from, to string) error { 15 | if !s.Exists(ctx, from) { 16 | return fmt.Errorf("source %q does not exists", from) 17 | } 18 | 19 | if s.Exists(ctx, to) { 20 | return fmt.Errorf("destination %q already exists", to) 21 | } 22 | 23 | if err := s.storage.Link(ctx, s.Passfile(from), s.Passfile(to)); err != nil { 24 | return fmt.Errorf("failed to create symlink from %q to %q: %w", from, to, err) 25 | } 26 | 27 | debug.Log("created symlink from %q to %q", from, to) 28 | 29 | if err := s.storage.Add(ctx, s.Passfile(to)); err != nil { 30 | if errors.Is(err, store.ErrGitNotInit) { 31 | return nil 32 | } 33 | 34 | return fmt.Errorf("failed to add %q to git: %w", to, err) 35 | } 36 | 37 | // try to enqueue this task, if the queue is not available 38 | // it will return the task and we will execute it inline 39 | t := queue.GetQueue(ctx).Add(func(ctx context.Context) (context.Context, error) { 40 | return nil, s.gitCommitAndPush(ctx, to) 41 | }) 42 | 43 | _, err := t(ctx) 44 | 45 | return err 46 | } 47 | -------------------------------------------------------------------------------- /internal/store/leaf/link_test.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/gopass/secrets" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLink(t *testing.T) { 13 | ctx := config.NewContextInMemory() 14 | 15 | s, err := createSubStore(t) 16 | require.NoError(t, err) 17 | 18 | sec := secrets.NewAKV() 19 | sec.SetPassword("foo") 20 | _, err = sec.Write([]byte("bar")) 21 | require.NoError(t, err) 22 | require.NoError(t, s.Set(ctx, "zab/zab", sec)) 23 | 24 | require.NoError(t, s.Link(ctx, "zab/zab", "foo/123")) 25 | 26 | p, err := s.Get(ctx, "foo/123") 27 | require.NoError(t, err) 28 | assert.Equal(t, "foo", p.Password()) 29 | } 30 | -------------------------------------------------------------------------------- /internal/store/leaf/list.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/gopasspw/gopass/pkg/debug" 8 | ) 9 | 10 | // Sep is the separator used in lists to separate folders from entries. 11 | var Sep = "/" 12 | 13 | // List will list all entries in this store. 14 | func (s *Store) List(ctx context.Context, prefix string) ([]string, error) { 15 | if s.storage == nil || s.crypto == nil { 16 | return nil, nil 17 | } 18 | 19 | lst, err := s.storage.List(ctx, prefix) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | debug.Log("Listing storage content of %s: %+v", prefix, lst) 25 | out := make([]string, 0, len(lst)) 26 | cExt := "." + s.crypto.Ext() 27 | for _, path := range lst { 28 | if !strings.HasSuffix(path, cExt) { 29 | continue 30 | } 31 | path = strings.TrimSuffix(path, cExt) 32 | if s.alias != "" { 33 | path = s.alias + Sep + path 34 | } 35 | out = append(out, path) 36 | } 37 | debug.Log("Leaf store entries: %+v", out) 38 | 39 | return out, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/store/leaf/read.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gopasspw/gopass/internal/out" 7 | "github.com/gopasspw/gopass/internal/store" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/pkg/debug" 10 | "github.com/gopasspw/gopass/pkg/gopass" 11 | "github.com/gopasspw/gopass/pkg/gopass/secrets" 12 | "github.com/gopasspw/gopass/pkg/gopass/secrets/secparse" 13 | ) 14 | 15 | // Get returns the plaintext of a single key. 16 | func (s *Store) Get(ctx context.Context, name string) (gopass.Secret, error) { 17 | p := s.Passfile(name) 18 | 19 | ciphertext, err := s.storage.Get(ctx, p) 20 | if err != nil { 21 | debug.Log("File %s not found: %s", p, err) 22 | 23 | return nil, store.ErrNotFound 24 | } 25 | 26 | content, err := s.crypto.Decrypt(ctx, ciphertext) 27 | if err != nil { 28 | out.Errorf(ctx, "Decryption failed: %s\n%s", err, string(content)) 29 | 30 | return nil, store.ErrDecrypt 31 | } 32 | 33 | if !ctxutil.IsShowParsing(ctx) { 34 | debug.Log("secrets parsing is disabled. parsing as AKV") 35 | 36 | return secrets.ParseAKV(content), nil 37 | } 38 | 39 | debug.Log("secrets parsing is enabled") 40 | 41 | return secparse.Parse(content) 42 | } 43 | -------------------------------------------------------------------------------- /internal/store/leaf/storage.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gopasspw/gopass/internal/backend" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | ) 10 | 11 | func (s *Store) initStorageBackend(ctx context.Context) error { 12 | ctx = ctxutil.WithAlias(ctx, s.alias) 13 | 14 | store, err := backend.DetectStorage(ctx, s.path) 15 | if err != nil { 16 | return fmt.Errorf("unknown storage backend: %w", err) 17 | } 18 | 19 | s.storage = store 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/store/leaf/write_test.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/gopasspw/gopass/internal/backend/crypto/gpg" 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/gopass/secrets" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestSet(t *testing.T) { 14 | ctx := gpg.WithAlwaysTrust(config.NewContextInMemory(), true) 15 | 16 | s, err := createSubStore(t) 17 | require.NoError(t, err) 18 | 19 | sec := secrets.NewAKV() 20 | sec.SetPassword("foo") 21 | _, err = sec.Write([]byte("bar")) 22 | require.NoError(t, err) 23 | require.NoError(t, s.Set(ctx, "zab/zab", sec)) 24 | 25 | if runtime.GOOS != "windows" { 26 | require.Error(t, s.Set(ctx, "../../../../../etc/passwd", sec)) 27 | } else { 28 | require.NoError(t, s.Set(ctx, "../../../../../etc/passwd", sec)) 29 | } 30 | 31 | require.NoError(t, s.Set(ctx, "zab", sec)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/store/root/convert.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gopasspw/gopass/internal/backend" 8 | "github.com/gopasspw/gopass/pkg/debug" 9 | ) 10 | 11 | // Convert will try to convert a given mount to a different set of 12 | // backends. 13 | func (r *Store) Convert(ctx context.Context, name string, cryptoBe backend.CryptoBackend, storageBe backend.StorageBackend, move bool) error { 14 | sub, err := r.GetSubStore(name) 15 | if err != nil { 16 | return fmt.Errorf("mount %q not found: %w", name, err) 17 | } 18 | 19 | debug.Log("converting %s to crypto: %s, storage: %s", name, cryptoBe, storageBe) 20 | 21 | if err := sub.Convert(ctx, cryptoBe, storageBe, move); err != nil { 22 | return fmt.Errorf("conversion failed: %w", err) 23 | } 24 | 25 | if name == "" { 26 | debug.Log("success. updating root path to %s", sub.Path()) 27 | 28 | return r.cfg.Set("", "mounts.path", sub.Path()) 29 | } 30 | 31 | debug.Log("success. updating path for %s to %s", name, sub.Path()) 32 | 33 | return r.cfg.Set("", "mounts."+name+".path", sub.Path()) 34 | } 35 | -------------------------------------------------------------------------------- /internal/store/root/crypto.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gopasspw/gopass/internal/backend" 7 | "github.com/gopasspw/gopass/pkg/debug" 8 | ) 9 | 10 | // Crypto returns the crypto backend. 11 | func (r *Store) Crypto(ctx context.Context, name string) backend.Crypto { 12 | sub, _ := r.getStore(name) 13 | if !sub.Valid() { 14 | debug.Log("Sub-Store not found for %s. Returning nil crypto backend", name) 15 | 16 | return nil 17 | } 18 | 19 | return sub.Crypto() 20 | } 21 | -------------------------------------------------------------------------------- /internal/store/root/crypto_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCrypto(t *testing.T) { 15 | u := gptest.NewUnitTester(t) 16 | 17 | ctx := config.NewContextInMemory() 18 | ctx = ctxutil.WithAlwaysYes(ctx, true) 19 | ctx = ctxutil.WithHidden(ctx, true) 20 | color.NoColor = true 21 | 22 | rs, err := createRootStore(ctx, u) 23 | require.NoError(t, err) 24 | 25 | assert.NotNil(t, rs.Crypto(ctx, "")) 26 | } 27 | -------------------------------------------------------------------------------- /internal/store/root/errors.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import "fmt" 4 | 5 | // AlreadyMountedError is an error that is returned when 6 | // a store is already mounted on a given mount point. 7 | type AlreadyMountedError string 8 | 9 | func (a AlreadyMountedError) Error() string { 10 | // important: must pass a as string(a)! 11 | return fmt.Sprintf("%s is already mounted", string(a)) 12 | } 13 | 14 | // NotInitializedError is an error that is returned when 15 | // a not initialized store should be mounted. 16 | type NotInitializedError struct { 17 | alias string 18 | path string 19 | } 20 | 21 | // Alias returns the store alias this error was generated for. 22 | func (n NotInitializedError) Alias() string { return n.alias } 23 | 24 | // Path returns the store path this error was generated for. 25 | func (n NotInitializedError) Path() string { return n.path } 26 | 27 | func (n NotInitializedError) Error() string { 28 | return fmt.Sprintf("password store %s is not initialized. Try gopass init --store %s --path %s", n.alias, n.alias, n.path) 29 | } 30 | -------------------------------------------------------------------------------- /internal/store/root/fsck.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/gopasspw/gopass/internal/out" 9 | "github.com/gopasspw/gopass/pkg/debug" 10 | ) 11 | 12 | // Fsck checks all stores/entries matching the given prefix. 13 | func (r *Store) Fsck(ctx context.Context, store, path string) error { 14 | var result []error 15 | 16 | for alias, sub := range r.mounts { 17 | if sub == nil { 18 | continue 19 | } 20 | 21 | if store != "" && alias != store { 22 | continue 23 | } 24 | 25 | if path != "" && !strings.HasPrefix(path, alias+"/") { 26 | continue 27 | } 28 | 29 | path = strings.TrimPrefix(path, alias+"/") 30 | 31 | // check sub store 32 | debug.Log("Checking mount point %s", alias) 33 | 34 | if err := sub.Fsck(ctx, path); err != nil { 35 | out.Errorf(ctx, "fsck failed on sub store %s: %s", alias, err) 36 | result = append(result, err) 37 | } 38 | 39 | debug.Log("Checked mount point %s", alias) 40 | } 41 | 42 | // check root store 43 | debug.Log("Checking root store") 44 | if err := r.store.Fsck(ctx, path); err != nil { 45 | out.Errorf(ctx, "fsck failed on root store: %s", err) 46 | result = append(result, err) 47 | } 48 | 49 | debug.Log("Checked root store") 50 | 51 | return errors.Join(result...) 52 | } 53 | -------------------------------------------------------------------------------- /internal/store/root/fsck_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/tests/gptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestFsck(t *testing.T) { 13 | u := gptest.NewUnitTester(t) 14 | 15 | ctx := config.NewContextInMemory() 16 | ctx = ctxutil.WithAlwaysYes(ctx, true) 17 | ctx = ctxutil.WithHidden(ctx, true) 18 | 19 | rs, err := createRootStore(ctx, u) 20 | require.NoError(t, err) 21 | require.NotNil(t, rs) 22 | 23 | require.NoError(t, rs.Fsck(ctx, "", "")) 24 | } 25 | -------------------------------------------------------------------------------- /internal/store/root/init_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/backend" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestInit(t *testing.T) { 15 | u := gptest.NewUnitTester(t) 16 | 17 | ctx := config.NewContextInMemory() 18 | ctx = ctxutil.WithAlwaysYes(ctx, true) 19 | ctx = ctxutil.WithHidden(ctx, true) 20 | ctx = backend.WithCryptoBackend(ctx, backend.Plain) 21 | 22 | cfg := config.NewInMemory() 23 | require.NoError(t, cfg.SetPath(u.StoreDir("rs"))) 24 | rs := New(cfg) 25 | 26 | inited, err := rs.IsInitialized(ctx) 27 | require.NoError(t, err) 28 | assert.False(t, inited) 29 | require.NoError(t, rs.Init(ctx, "", u.StoreDir("rs"), "0xDEADBEEF")) 30 | 31 | inited, err = rs.IsInitialized(ctx) 32 | require.NoError(t, err) 33 | assert.True(t, inited) 34 | require.NoError(t, rs.Init(ctx, "rs2", u.StoreDir("rs2"), "0xDEADBEEF")) 35 | } 36 | -------------------------------------------------------------------------------- /internal/store/root/link.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // Link creates a symlink. 9 | func (r *Store) Link(ctx context.Context, from, to string) error { 10 | subFrom, fName := r.getStore(from) 11 | subTo, tName := r.getStore(to) 12 | 13 | if !subFrom.Equals(subTo) { 14 | return fmt.Errorf("sylinks across stores are not supported") 15 | } 16 | 17 | return subFrom.Link(ctx, fName, tName) 18 | } 19 | -------------------------------------------------------------------------------- /internal/store/root/list_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/internal/tree" 9 | "github.com/gopasspw/gopass/pkg/ctxutil" 10 | "github.com/gopasspw/gopass/tests/gptest" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestList(t *testing.T) { 16 | u := gptest.NewUnitTester(t) 17 | 18 | ctx := config.NewContextInMemory() 19 | ctx = ctxutil.WithAlwaysYes(ctx, true) 20 | ctx = ctxutil.WithHidden(ctx, true) 21 | color.NoColor = true 22 | 23 | rs, err := createRootStore(ctx, u) 24 | require.NoError(t, err) 25 | 26 | es, err := rs.List(ctx, tree.INF) 27 | require.NoError(t, err) 28 | assert.Equal(t, []string{"foo"}, es) 29 | 30 | sd, err := rs.HasSubDirs(ctx, "foo") 31 | require.NoError(t, err) 32 | assert.False(t, sd) 33 | 34 | str, err := rs.Format(ctx, -1) 35 | require.NoError(t, err) 36 | assert.Equal(t, `gopass 37 | └── foo 38 | `, str) 39 | } 40 | -------------------------------------------------------------------------------- /internal/store/root/rcs_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestRCS(t *testing.T) { 15 | u := gptest.NewUnitTester(t) 16 | 17 | ctx := config.NewContextInMemory() 18 | ctx = ctxutil.WithAlwaysYes(ctx, true) 19 | ctx = ctxutil.WithHidden(ctx, true) 20 | color.NoColor = true 21 | 22 | rs, err := createRootStore(ctx, u) 23 | require.NoError(t, err) 24 | 25 | require.Error(t, rs.RCSStatus(ctx, "")) 26 | 27 | revs, err := rs.ListRevisions(ctx, "foo") 28 | require.Error(t, err) 29 | assert.Len(t, revs, 1) 30 | } 31 | -------------------------------------------------------------------------------- /internal/store/root/read.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/pkg/gopass" 9 | ) 10 | 11 | // Get returns the plaintext of a single key. 12 | func (r *Store) Get(ctx context.Context, name string) (gopass.Secret, error) { 13 | store, name := r.getStore(name) 14 | 15 | sec, err := store.Get(ctx, name) 16 | if err != nil { 17 | return sec, err 18 | } 19 | 20 | if ref, ok := sec.Ref(); ctxutil.IsFollowRef(ctx) && ok { 21 | refSec, err := store.Get(ctx, ref) 22 | if err != nil { 23 | return sec, fmt.Errorf("failed to read reference %s by %s: %w", ref, name, err) 24 | } 25 | 26 | sec.SetPassword(refSec.Password()) 27 | } 28 | 29 | return sec, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/store/root/read_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/tests/gptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGet(t *testing.T) { 13 | u := gptest.NewUnitTester(t) 14 | 15 | ctx := config.NewContextInMemory() 16 | ctx = ctxutil.WithAlwaysYes(ctx, true) 17 | ctx = ctxutil.WithHidden(ctx, true) 18 | 19 | rs, err := createRootStore(ctx, u) 20 | require.NoError(t, err) 21 | 22 | _, err = rs.Get(ctx, "foo") 23 | require.NoError(t, err) 24 | } 25 | -------------------------------------------------------------------------------- /internal/store/root/recipients_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestRecipients(t *testing.T) { 15 | u := gptest.NewUnitTester(t) 16 | 17 | ctx := config.NewContextInMemory() 18 | ctx = ctxutil.WithAlwaysYes(ctx, true) 19 | ctx = ctxutil.WithHidden(ctx, true) 20 | color.NoColor = true 21 | 22 | rs, err := createRootStore(ctx, u) 23 | require.NoError(t, err) 24 | 25 | assert.Equal(t, []string{"0xDEADBEEF"}, rs.ListRecipients(ctx, "")) 26 | rt, err := rs.RecipientsTree(ctx, false) 27 | require.NoError(t, err) 28 | assert.Equal(t, "gopass\n└── 0xDEADBEEF\n", rt.Format(0)) 29 | } 30 | -------------------------------------------------------------------------------- /internal/store/root/templates_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/gopasspw/gopass/pkg/ctxutil" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestTemplate(t *testing.T) { 15 | u := gptest.NewUnitTester(t) 16 | 17 | ctx := config.NewContextInMemory() 18 | ctx = ctxutil.WithAlwaysYes(ctx, true) 19 | ctx = ctxutil.WithHidden(ctx, true) 20 | color.NoColor = true 21 | 22 | rs, err := createRootStore(ctx, u) 23 | require.NoError(t, err) 24 | 25 | tt, err := rs.TemplateTree(ctx) 26 | require.NoError(t, err) 27 | assert.Equal(t, "gopass\n", tt.Format(0)) 28 | 29 | assert.False(t, rs.HasTemplate(ctx, "foo")) 30 | _, err = rs.GetTemplate(ctx, "foo") 31 | require.Error(t, err) 32 | require.Error(t, rs.RemoveTemplate(ctx, "foo")) 33 | 34 | require.NoError(t, rs.SetTemplate(ctx, "foo", []byte("foobar"))) 35 | assert.True(t, rs.HasTemplate(ctx, "foo")) 36 | 37 | b, err := rs.GetTemplate(ctx, "foo") 38 | require.NoError(t, err) 39 | assert.Equal(t, "foobar", string(b)) 40 | 41 | _, b, found := rs.LookupTemplate(ctx, "foo/bar") 42 | assert.True(t, found) 43 | assert.Equal(t, "foobar", string(b)) 44 | require.NoError(t, rs.RemoveTemplate(ctx, "foo")) 45 | } 46 | -------------------------------------------------------------------------------- /internal/store/root/write.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gopasspw/gopass/pkg/gopass" 7 | ) 8 | 9 | // Set encodes and write the ciphertext of one entry to disk. 10 | func (r *Store) Set(ctx context.Context, name string, sec gopass.Byter) error { 11 | store, name := r.getStore(name) 12 | 13 | return store.Set(ctx, name, sec) 14 | } 15 | -------------------------------------------------------------------------------- /internal/store/root/write_test.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/gopasspw/gopass/pkg/gopass/secrets" 9 | "github.com/gopasspw/gopass/tests/gptest" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestSet(t *testing.T) { 14 | u := gptest.NewUnitTester(t) 15 | 16 | ctx := config.NewContextInMemory() 17 | ctx = ctxutil.WithAlwaysYes(ctx, true) 18 | ctx = ctxutil.WithHidden(ctx, true) 19 | 20 | rs, err := createRootStore(ctx, u) 21 | require.NoError(t, err) 22 | 23 | sec := secrets.NewAKV() 24 | sec.SetPassword("foo") 25 | _, err = sec.Write([]byte("bar")) 26 | require.NoError(t, err) 27 | require.NoError(t, rs.Set(ctx, "zab", sec)) 28 | 29 | err = rs.Set(ctx, "zab2", sec) 30 | require.NoError(t, err) 31 | } 32 | -------------------------------------------------------------------------------- /internal/store/sort.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "strings" 4 | 5 | // ByPathLen sorts mount points by the number of level / path separators. 6 | type ByPathLen []string 7 | 8 | func (s ByPathLen) Len() int { return len(s) } 9 | 10 | func (s ByPathLen) Less(i, j int) bool { 11 | return strings.Count(s[i], "/") < strings.Count(s[j], "/") 12 | } 13 | 14 | func (s ByPathLen) Swap(i, j int) { 15 | s[i], s[j] = s[j], s[i] 16 | } 17 | 18 | // ByLen is a list of mount points (string) that can be sorted by length. 19 | type ByLen []string 20 | 21 | // Len return the number of mount points in the list. 22 | func (s ByLen) Len() int { return len(s) } 23 | 24 | // Less returns if a Mount point is shorter than another. 25 | func (s ByLen) Less(i, j int) bool { return len(s[i]) > len(s[j]) } 26 | 27 | // Swap Mount Point in the list of Mount Points. 28 | func (s ByLen) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 29 | -------------------------------------------------------------------------------- /internal/store/store.go: -------------------------------------------------------------------------------- 1 | // Package store provides the interface for the gopass password store. 2 | // It defines the methods and types used to interact with the password store. 3 | package store 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // RecipientCallback is a callback to verify the list of recipients. 10 | type RecipientCallback func(context.Context, string, []string) ([]string, error) 11 | 12 | // ImportCallback is a callback to ask the user if they want to import 13 | // a certain recipients public key into their keystore. 14 | type ImportCallback func(context.Context, string, []string) bool 15 | 16 | // FsckCallback is a callback to ask the user to confirm certain fsck 17 | // corrective actions. 18 | type FsckCallback func(context.Context, string) bool 19 | -------------------------------------------------------------------------------- /internal/tpl/template.go: -------------------------------------------------------------------------------- 1 | // Package tpl provides functions to handle templates. 2 | // It can parse templates from various formats and generate output for them. 3 | package tpl 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "path/filepath" 10 | "text/template" 11 | 12 | "github.com/gopasspw/gopass/pkg/gopass" 13 | ) 14 | 15 | type kvstore interface { 16 | Get(context.Context, string) (gopass.Secret, error) 17 | } 18 | 19 | type payload struct { 20 | Dir string 21 | DirName string 22 | Path string 23 | Name string 24 | Content string 25 | } 26 | 27 | // Execute executes the given template. 28 | func Execute(ctx context.Context, tpl, name string, content []byte, s kvstore) ([]byte, error) { 29 | funcs := funcMap(ctx, s) 30 | 31 | dir := filepath.Dir(name) 32 | 33 | pl := payload{ 34 | Dir: dir, 35 | DirName: filepath.Base(dir), 36 | Path: name, 37 | Name: filepath.Base(name), 38 | Content: string(content), 39 | } 40 | 41 | tmpl, err := template.New(tpl).Funcs(funcs).Parse(tpl) 42 | if err != nil { 43 | return []byte{}, fmt.Errorf("failed to parse template: %w", err) 44 | } 45 | 46 | buff := &bytes.Buffer{} 47 | if err := tmpl.Execute(buff, pl); err != nil { 48 | return []byte{}, fmt.Errorf("failed to execute template: %w", err) 49 | } 50 | 51 | return buff.Bytes(), nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/updater/access_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package updater 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | func canWrite(path string) error { 9 | return unix.Access(path, unix.W_OK) //nolint:wrapcheck 10 | } 11 | 12 | func removeOldBinary(dir, dest string) error { 13 | // no need, os.Rename will replace the destination 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/updater/access_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package updater 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func canWrite(path string) error { 13 | return nil 14 | } 15 | 16 | // Windows won't allow us to remove the binary that's currently being executed. 17 | // So rename the binary and then the updater should be able to write it's 18 | // update to the correct location. 19 | // 20 | // See https://stackoverflow.com/a/459860 21 | func removeOldBinary(dir, dest string) error { 22 | bakFile := filepath.Join(dir, filepath.Base(dest)+".bak") 23 | // check if the bakup file already exists 24 | if _, err := os.Stat(bakFile); err == nil { 25 | // ... then remove it 26 | _ = os.Remove(bakFile) 27 | } 28 | // we can't remove the currently running binary, but should be able to 29 | // rename it. 30 | if err := os.Rename(dest, bakFile); err != nil { 31 | return fmt.Errorf("unable to rename %s to %s: %w", dest, bakFile, err) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /main_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func init() { 12 | // workaround for https://github.com/golang/go/issues/37942 13 | signal.Ignore(syscall.SIGURG) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/appdir/appdir_test.go: -------------------------------------------------------------------------------- 1 | package appdir 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUserHome(t *testing.T) { 10 | td := t.TempDir() 11 | t.Setenv("GOPASS_HOMEDIR", td) 12 | 13 | assert.Equal(t, td, UserHome()) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/appdir/appdir_windows.go: -------------------------------------------------------------------------------- 1 | package appdir 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // UserConfig returns the users config dir 9 | func (a *Appdir) UserConfig() string { 10 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 11 | return filepath.Join(hd, ".config", a.name) 12 | } 13 | 14 | return filepath.Join(os.Getenv("APPDATA"), a.name) 15 | } 16 | 17 | // UserCache returns the users cache dir 18 | func (a *Appdir) UserCache() string { 19 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 20 | return filepath.Join(hd, ".cache", a.name) 21 | } 22 | 23 | return filepath.Join(os.Getenv("LOCALAPPDATA"), a.name) 24 | } 25 | 26 | // UserData returns the users data dir 27 | func (a *Appdir) UserData() string { 28 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 29 | return filepath.Join(hd, ".local", "share", a.name) 30 | } 31 | return filepath.Join(os.Getenv("LOCALAPPDATA"), a.name) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/appdir/appdir_xdg.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package appdir 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gopasspw/gopass/pkg/debug" 11 | ) 12 | 13 | // UserConfig returns the users config dir. 14 | func (a *Appdir) UserConfig() string { 15 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 16 | debug.V(3).Log("GOPASS_HOMEDIR is set to %s", hd) 17 | 18 | return filepath.Join(hd, ".config", a.name) 19 | } 20 | 21 | base := os.Getenv("XDG_CONFIG_HOME") 22 | if base == "" { 23 | base = filepath.Join(os.Getenv("HOME"), ".config") 24 | } 25 | 26 | return filepath.Join(base, a.name) 27 | } 28 | 29 | // UserCache returns the users cache dir. 30 | func (a *Appdir) UserCache() string { 31 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 32 | return filepath.Join(hd, ".cache", a.name) 33 | } 34 | 35 | base := os.Getenv("XDG_CACHE_HOME") 36 | if base == "" { 37 | base = filepath.Join(os.Getenv("HOME"), ".cache") 38 | } 39 | 40 | return filepath.Join(base, a.name) 41 | } 42 | 43 | // UserData returns the users data dir. 44 | func (a *Appdir) UserData() string { 45 | if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" { 46 | return filepath.Join(hd, ".local", "share", a.name) 47 | } 48 | 49 | base := os.Getenv("XDG_DATA_HOME") 50 | if base == "" { 51 | base = filepath.Join(os.Getenv("HOME"), ".local", "share") 52 | } 53 | 54 | return filepath.Join(base, a.name) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/clipboard/clipboard_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package clipboard 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | 12 | "github.com/gopasspw/gopass/internal/config" 13 | "github.com/gopasspw/gopass/internal/pwschemes/argon2id" 14 | ) 15 | 16 | // clearClip will spawn a copy of gopass that waits in a detached background 17 | // process group until the timeout is expired. It will then compare the contents 18 | // of the clipboard and erase it if it still contains the data gopass copied 19 | // to it. 20 | func clearClip(ctx context.Context, name string, content []byte, timeout int) error { 21 | hash, err := argon2id.Generate(string(content), 0) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | cmd := exec.CommandContext(ctx, os.Args[0], "unclip", "--timeout", strconv.Itoa(timeout)) 27 | cmd.Env = append(os.Environ(), "GOPASS_UNCLIP_NAME="+name) 28 | cmd.Env = append(cmd.Env, "GOPASS_UNCLIP_CHECKSUM="+hash) 29 | if !config.Bool(ctx, "core.notifications") { 30 | cmd.Env = append(cmd.Env, "GOPASS_NO_NOTIFY=true") 31 | } 32 | return cmd.Start() 33 | } 34 | 35 | func walkFn(int, func(int)) {} 36 | -------------------------------------------------------------------------------- /pkg/clipboard/kill_others.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !linux && !solaris && !windows && !freebsd 2 | // +build !darwin,!linux,!solaris,!windows,!freebsd 3 | 4 | package clipboard 5 | 6 | func killPrecedessors() error { 7 | return nil 8 | } 9 | -------------------------------------------------------------------------------- /pkg/clipboard/kill_ps.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || (freebsd && amd64) || linux || solaris || windows || (freebsd && arm) || (freebsd && arm64) 2 | // +build darwin freebsd,amd64 linux solaris windows freebsd,arm freebsd,arm64 3 | 4 | package clipboard 5 | 6 | import ( 7 | "fmt" 8 | 9 | ps "github.com/mitchellh/go-ps" 10 | ) 11 | 12 | // killPrecedessors will kill any previous "gopass unclip" invocations to avoid 13 | // erasing the clipboard prematurely in case the the same content is copied to 14 | // the clipboard repeatedly. 15 | func killPrecedessors() error { 16 | procs, err := ps.Processes() 17 | if err != nil { 18 | return fmt.Errorf("failed to list processes: %w", err) 19 | } 20 | 21 | for _, proc := range procs { 22 | walkFn(proc.Pid(), killProc) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/clipboard/unclip_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package clipboard 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/godbus/dbus/v5" 12 | ) 13 | 14 | func clearClipboardHistory(ctx context.Context) error { 15 | conn, err := dbus.SessionBus() 16 | if err != nil { 17 | return fmt.Errorf("failed to connect to session bus: %w", err) 18 | } 19 | 20 | obj := conn.Object("org.kde.klipper", "/klipper") 21 | call := obj.Call("org.kde.klipper.klipper.clearClipboardHistory", 0) 22 | 23 | if call.Err != nil { 24 | if strings.HasPrefix(call.Err.Error(), "The name org.kde.klipper was not provided") { 25 | return nil 26 | } 27 | 28 | if strings.HasPrefix(call.Err.Error(), "The name is not activatable") { 29 | return nil 30 | } 31 | 32 | return call.Err 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/clipboard/unclip_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package clipboard 5 | 6 | import "context" 7 | 8 | func clearClipboardHistory(ctx context.Context) error { 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /pkg/clipboard/unclip_test.go: -------------------------------------------------------------------------------- 1 | package clipboard 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/internal/out" 10 | "github.com/gopasspw/gopass/pkg/ctxutil" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestNotExistingClipboardClearCommand(t *testing.T) { 16 | ctx := config.NewContextInMemory() 17 | ctx = ctxutil.WithAlwaysYes(ctx, true) 18 | 19 | t.Setenv("GOPASS_CLIPBOARD_CLEAR_CMD", "not_existing_command") 20 | 21 | maybeErr := Clear(ctx, "", "", false) 22 | require.Error(t, maybeErr) 23 | assert.Contains(t, maybeErr.Error(), "\"not_existing_command\": executable file not found in") 24 | } 25 | 26 | func TestUnclip(t *testing.T) { 27 | t.Parallel() 28 | 29 | ctx := config.NewContextInMemory() 30 | ctx = ctxutil.WithAlwaysYes(ctx, true) 31 | 32 | buf := &bytes.Buffer{} 33 | out.Stdout = buf 34 | 35 | defer func() { 36 | out.Stdout = os.Stdout 37 | }() 38 | 39 | require.EqualError(t, Clear(ctx, "", "", false), ErrNotSupported.Error()) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/debug/doc.go: -------------------------------------------------------------------------------- 1 | // Package debug provides logging of debug information. 2 | // 3 | // This package is heavily based on github.com/restic/restic/internal/debug 4 | package debug 5 | -------------------------------------------------------------------------------- /pkg/fsutil/umask.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | // Umask extracts the umask from env. 9 | func Umask() int { 10 | for _, en := range []string{"GOPASS_UMASK", "PASSWORD_STORE_UMASK"} { 11 | um := os.Getenv(en) 12 | if um == "" { 13 | continue 14 | } 15 | 16 | iv, err := strconv.ParseInt(um, 8, 32) 17 | if err != nil { 18 | continue 19 | } 20 | 21 | if iv >= 0 && iv <= 0o777 { 22 | return int(iv) 23 | } 24 | } 25 | 26 | return 0o77 27 | } 28 | -------------------------------------------------------------------------------- /pkg/fsutil/umask_test.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUmask(t *testing.T) { 10 | for _, vn := range []string{"GOPASS_UMASK", "PASSWORD_STORE_UMASK"} { 11 | for in, out := range map[string]int{ 12 | "002": 0o2, 13 | "0777": 0o777, 14 | "000": 0, 15 | "07557575": 0o77, 16 | } { 17 | t.Run(vn, func(t *testing.T) { 18 | t.Setenv(vn, in) 19 | assert.Equal(t, out, Umask()) 20 | }) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/gopass/doc.go: -------------------------------------------------------------------------------- 1 | // Package gopass contains the public gopass API. 2 | // 3 | // WARNING: This package is incomplete and unstable. DO NOT USE! 4 | // 5 | // Feel free to report feedback on API design and missing features but please 6 | // note that bug reports will be silently ignored and the API WILL CHANGE 7 | // WITHOUT NOTICE until this note is gone. 8 | // 9 | // If you want to try it anyway please look at the following examples: 10 | // * https://github.com/gopasspw/gopass-hibp 11 | // * https://github.com/gopasspw/gopass-jsonapi 12 | // * https://github.com/gopasspw/git-credential-gopass 13 | // * https://github.com/gopasspw/gopass-summon-provider 14 | package gopass 15 | -------------------------------------------------------------------------------- /pkg/gopass/secrets/error.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | // PermanentError signal that parsing should not attempt other formats. 4 | type PermanentError struct { 5 | Err error 6 | } 7 | 8 | func (p *PermanentError) Error() string { 9 | return p.Err.Error() 10 | } 11 | -------------------------------------------------------------------------------- /pkg/gopass/secrets/ident.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | const ( 4 | // Ident is the header of the deprecated Gopass MIME secret. 5 | Ident = "GOPASS-SECRET-1.0" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/gopass/secrets/new.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "github.com/gopasspw/gopass/pkg/gopass" 5 | ) 6 | 7 | // New creates a new secret. 8 | func New() gopass.Secret { //nolint:ireturn 9 | return NewAKV() 10 | } 11 | -------------------------------------------------------------------------------- /pkg/gopass/secrets/secparse/.gitignore: -------------------------------------------------------------------------------- 1 | testdata/ -------------------------------------------------------------------------------- /pkg/otp/screenshot_others.go: -------------------------------------------------------------------------------- 1 | //go:build !((arm || arm64 || amd64 || 386) && (linux || windows || (cgo && darwin) || freebsd || netbsd)) 2 | 3 | package otp 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | ) 9 | 10 | // ParseScreen will attempt to parse all available screen and will look for otpauth QR codes. It returns the first one 11 | // it has found. 12 | func ParseScreen(ctx context.Context) (string, error) { 13 | return "", fmt.Errorf("not supported on your platform") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/passkey/passkey_test.go: -------------------------------------------------------------------------------- 1 | package passkey_test 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "testing" 8 | 9 | "github.com/gopasspw/gopass/pkg/passkey" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var flags passkey.CredentialFlags = passkey.CredentialFlags{ 15 | UserPresent: true, 16 | UserVerified: true, 17 | AttestationData: false, 18 | ExtensionData: false, 19 | } 20 | 21 | func TestCreate(t *testing.T) { 22 | cred, err := passkey.CreateCredential("test.com", "user", flags) 23 | require.NoError(t, err) 24 | assert.Equal(t, "test.com", cred.Rp) 25 | assert.Equal(t, uint32(0), cred.Counter) 26 | } 27 | 28 | func TestGetAssertion(t *testing.T) { 29 | cred, err := passkey.CreateCredential("test.com", "user", flags) 30 | require.NoError(t, err) 31 | 32 | rsp, err := cred.GetAssertion(base64.RawURLEncoding.EncodeToString([]byte("test_challenge")), "test") 33 | require.NoError(t, err) 34 | 35 | // Verify signature 36 | clientDataHash := sha256.Sum256(rsp.ClientDataJSON) 37 | 38 | authData := rsp.AuthenticatorData 39 | require.NoError(t, err) 40 | 41 | message := sha256.Sum256(append(authData[:], clientDataHash[:]...)) 42 | assert.True(t, ecdsa.VerifyASN1(&cred.SecretKey.PublicKey, message[:], rsp.Signature)) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/pinentry/cli/fallback.go: -------------------------------------------------------------------------------- 1 | // Package cli provides a pinentry client that uses the terminal 2 | // for input and output. It is a drop-in replacement for the 3 | // pinentry program. It is used to ask for a passphrase or PIN 4 | // in the terminal. 5 | package cli 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "github.com/gopasspw/gopass/pkg/termio" 12 | ) 13 | 14 | // Client is pinentry CLI drop-in. 15 | type Client struct { 16 | repeat bool 17 | } 18 | 19 | // New creates a new client. 20 | func New() *Client { 21 | return &Client{repeat: false} 22 | } 23 | 24 | // Set is a no-op unless you're requesting a repeat. 25 | func (c *Client) Set(key string) error { 26 | if key == "REPEAT" { 27 | c.repeat = true 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // Option is a no-op. 34 | func (c *Client) Option(string) error { 35 | return nil 36 | } 37 | 38 | // GetPINContext prompts for the pin in the termnial and returns the output. 39 | // The context is only used for tests. 40 | func (c *Client) GetPINContext(ctx context.Context) (string, error) { 41 | pw, err := termio.AskForPassword(ctx, "your PIN", c.repeat) 42 | if err != nil { 43 | return "", fmt.Errorf("failed to ask for PIN: %w", err) 44 | } 45 | 46 | return pw, nil 47 | } 48 | 49 | // GetPIN prompts for the pin in the termnial and returns the output. 50 | func (c *Client) GetPIN() (string, error) { 51 | return c.GetPINContext(context.TODO()) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/pinentry/cli/fallback_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gopasspw/gopass/pkg/termio" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNew(t *testing.T) { 13 | client := New() 14 | assert.NotNil(t, client) 15 | assert.False(t, client.repeat) 16 | } 17 | 18 | func TestSet(t *testing.T) { 19 | client := New() 20 | 21 | err := client.Set("REPEAT") 22 | require.NoError(t, err) 23 | assert.True(t, client.repeat) 24 | 25 | err = client.Set("OTHER") 26 | require.NoError(t, err) 27 | assert.True(t, client.repeat) 28 | } 29 | 30 | func TestOption(t *testing.T) { 31 | client := New() 32 | 33 | err := client.Option("ANY") 34 | require.NoError(t, err) 35 | } 36 | 37 | func TestGetPIN(t *testing.T) { 38 | client := New() 39 | 40 | ctx := termio.WithPassPromptFunc(t.Context(), func(ctx context.Context, s string) (string, error) { 41 | return "1234", nil 42 | }) 43 | 44 | pin, err := client.GetPINContext(ctx) 45 | require.NoError(t, err) 46 | assert.Equal(t, "1234", pin) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/protect/protect.go: -------------------------------------------------------------------------------- 1 | //go:build !openbsd 2 | // +build !openbsd 3 | 4 | // Package protect provides an interface to the pledge syscall. 5 | // It is used to limit the system calls a process can make. 6 | // This is used to limit the attack surface of the process. 7 | // The pledge syscall is only available on OpenBSD. 8 | // It is not available on other systems. 9 | // This package is a no-op on other systems. 10 | package protect 11 | 12 | // ProtectEnabled lets us know if we have protection or not. 13 | var ProtectEnabled = false 14 | 15 | // Pledge on any other system than OpenBSD doesn't do anything. 16 | func Pledge(s string) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/protect/protect_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package protect 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | // ProtectEnabled lets us know if we have protection or not 9 | var ProtectEnabled = true 10 | 11 | // Pledge on OpenBSD lets us "promise" to only run a subset of 12 | // system calls: http://man.openbsd.org/pledge 13 | func Pledge(s string) error { 14 | return unix.PledgePromises(s) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/protect/protect_test.go: -------------------------------------------------------------------------------- 1 | package protect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestProtect(t *testing.T) { 10 | t.Parallel() 11 | 12 | require.NoError(t, Pledge("")) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/pwgen/cryptic_test.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/gopasspw/gopass/internal/config" 9 | "github.com/gopasspw/gopass/pkg/pwgen/pwrules" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCrypticForDomain(t *testing.T) { 15 | t.Parallel() 16 | 17 | rules := pwrules.AllRules() 18 | keys := make([]string, 0, len(rules)) 19 | 20 | for k := range rules { 21 | keys = append(keys, k) 22 | } 23 | 24 | sort.Strings(keys) 25 | 26 | for _, domain := range keys { 27 | t.Run(domain, func(t *testing.T) { 28 | for _, length := range []int{1, 4, 8, 100} { 29 | tcName := fmt.Sprintf("%s: generated password with %d chars", domain, length) 30 | c := NewCrypticForDomain(config.NewContextInMemory(), length, domain) 31 | c.MaxTries = 1024 32 | 33 | require.NotNil(t, c, tcName) 34 | 35 | pw := c.Password() 36 | 37 | assert.NotEmpty(t, pw, tcName) 38 | t.Logf("%s -> %s (%d)", tcName, pw, len(pw)) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestUniqueChars(t *testing.T) { 45 | t.Parallel() 46 | 47 | for in, out := range map[string]string{ 48 | "foobar": "abfor", 49 | "abced": "abcde", 50 | } { 51 | assert.Equal(t, out, uniqueChars(in)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/pwgen/external.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strconv" 8 | "strings" 9 | 10 | shellquote "github.com/kballard/go-shellquote" 11 | ) 12 | 13 | var ( 14 | // ErrNoExternal is returned when no external generator is set. 15 | ErrNoExternal = fmt.Errorf("no external generator") 16 | // ErrNoCommand is returned when no command is set. 17 | ErrNoCommand = fmt.Errorf("no command") 18 | ) 19 | 20 | // GenerateExternal will invoke an external password generator, 21 | // if set, and return it's output. 22 | func GenerateExternal(pwlen int) (string, error) { 23 | c := os.Getenv("GOPASS_EXTERNAL_PWGEN") 24 | if c == "" { 25 | return "", ErrNoExternal 26 | } 27 | 28 | cmdArgs, err := shellquote.Split(c) 29 | if err != nil { 30 | return "", fmt.Errorf("failed to split %s: %w", c, err) 31 | } 32 | 33 | if len(cmdArgs) < 1 { 34 | return "", ErrNoCommand 35 | } 36 | 37 | exe := cmdArgs[0] 38 | args := []string{} 39 | 40 | if len(cmdArgs) > 1 { 41 | args = cmdArgs[1:] 42 | } 43 | 44 | args = append(args, strconv.Itoa(pwlen)) 45 | 46 | out, err := exec.Command(exe, args...).Output() 47 | if err != nil { 48 | return "", fmt.Errorf("failed to execute %s %v: %w", exe, args, err) 49 | } 50 | 51 | return strings.TrimSpace(string(out)), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/pwgen/memorable.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import "strings" 4 | 5 | // GenerateMemorablePassword will generate a memorable password 6 | // with a minimum length. 7 | func GenerateMemorablePassword(minLength int, symbols bool, capitals bool) string { 8 | var sb strings.Builder 9 | 10 | upper := false 11 | 12 | for sb.Len() < minLength { 13 | // when requesting uppercase, we randomly uppercase words 14 | if capitals && randomInteger(2) == 0 { 15 | // We control the input so we can safely ignore the linter. 16 | sb.WriteString(strings.Title(randomWord())) //nolint:staticcheck 17 | 18 | upper = true 19 | } else { 20 | sb.WriteString(randomWord()) 21 | } 22 | 23 | sb.WriteByte(Digits[randomInteger(len(Digits))]) 24 | 25 | if !symbols { 26 | continue 27 | } 28 | 29 | sb.WriteByte(Syms[randomInteger(len(Syms))]) 30 | } 31 | // If there isn't already a capitalized word, capitalize the first letter 32 | if capitals && !upper { 33 | str := sb.String() 34 | 35 | return strings.Title(string(str[0])) + str[1:] //nolint:staticcheck 36 | } 37 | 38 | return sb.String() 39 | } 40 | 41 | func randomWord() string { 42 | return wordlist[randomInteger(len(wordlist))] 43 | } 44 | -------------------------------------------------------------------------------- /pkg/pwgen/pwgen_others_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package pwgen 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestPwgenExternal(t *testing.T) { 14 | t.Setenv("GOPASS_EXTERNAL_PWGEN", "echo foobar") 15 | 16 | pw, err := GenerateExternal(4) 17 | 18 | require.NoError(t, err) 19 | assert.Equal(t, "foobar 4", pw) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/pwgen/pwgen_windows_test.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPwgenExternal(t *testing.T) { 10 | t.Setenv("GOPASS_EXTERNAL_PWGEN", "powershell.exe -Command write-output 1234 #") 11 | ans, err := GenerateExternal(4) 12 | if err != nil { 13 | panic("Unable to generate using external generator") 14 | } 15 | assert.Equal(t, "1234", ans) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/pwgen/pwrules/aliases_test.go: -------------------------------------------------------------------------------- 1 | package pwrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestLoadCustomRules(t *testing.T) { 12 | t.Parallel() 13 | 14 | cfg := config.NewInMemory() 15 | aliases := map[string]string{ 16 | "real.com": "alias.com", 17 | "real.de": "copy.de", 18 | } 19 | 20 | for k, v := range aliases { 21 | require.NoError(t, cfg.Set("", "domain-alias."+k+".insteadOf", v)) 22 | } 23 | 24 | ctx := t.Context() 25 | ctx = cfg.WithConfig(ctx) 26 | 27 | a := LookupAliases(ctx, "alias.com") 28 | assert.Equal(t, []string{"real.com"}, a) 29 | 30 | a = LookupAliases(ctx, "copy.de") 31 | assert.Equal(t, []string{"real.de"}, a) 32 | 33 | assert.Greater(t, len(AllAliases(ctx)), 256) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/pwgen/pwrules/change.go: -------------------------------------------------------------------------------- 1 | package pwrules 2 | 3 | import "context" 4 | 5 | var changeURLs = map[string]string{} 6 | 7 | func init() { 8 | for k, v := range genChange { 9 | // filter out invalid entries 10 | if v == "" { 11 | continue 12 | } 13 | 14 | changeURLs[k] = v 15 | } 16 | } 17 | 18 | // LookupChangeURL looks up a change URL, either directly or through 19 | // one of it's know aliases. 20 | func LookupChangeURL(ctx context.Context, domain string) string { 21 | if u, found := changeURLs[domain]; found { 22 | return u 23 | } 24 | 25 | for _, alias := range LookupAliases(ctx, domain) { 26 | if u, found := changeURLs[alias]; found { 27 | return u 28 | } 29 | } 30 | 31 | return "" 32 | } 33 | -------------------------------------------------------------------------------- /pkg/pwgen/pwrules/change_test.go: -------------------------------------------------------------------------------- 1 | package pwrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLookupChangeURL(t *testing.T) { 11 | t.Parallel() 12 | 13 | ctx := config.NewContextInMemory() 14 | assert.Equal(t, "https://account.gmx.net/ciss/security/edit/passwordChange", LookupChangeURL(ctx, "gmx.net")) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/pwgen/pwrules/pwrules_test.go: -------------------------------------------------------------------------------- 1 | package pwrules 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseRule(t *testing.T) { 10 | t.Parallel() 11 | 12 | for _, tc := range []struct { 13 | in string 14 | out Rule 15 | }{ 16 | { 17 | in: "minlength: 8; maxlength: 20; required: upper; required: lower; required: digit; max-consecutive: 3; allowed: [@#*()+={}/?~;,.-_];", 18 | out: Rule{ 19 | Minlen: 8, 20 | Maxlen: 20, 21 | Required: []string{ 22 | "digit", 23 | "lower", 24 | "upper", 25 | }, 26 | Allowed: []string{ 27 | "[@#*()+={}/?~;,.-_]", 28 | }, 29 | Maxconsec: 3, 30 | }, 31 | }, 32 | { 33 | in: "minlength: 7; maxlength: 16; required: lower, upper; required: digit; required: [`!@#$%^&*()+~{}'\";:<>?]];", 34 | out: Rule{ 35 | Minlen: 7, 36 | Maxlen: 16, 37 | Required: []string{ 38 | "[`!@#$%^&*()+~{}'\";:<>?]]", 39 | "digit", 40 | "lower", 41 | "upper", 42 | }, 43 | Allowed: []string{}, 44 | }, 45 | }, 46 | { 47 | in: "minlength: 8; maxlength: 16;", 48 | out: Rule{ 49 | Minlen: 8, 50 | Maxlen: 16, 51 | Required: []string{}, 52 | Allowed: []string{}, 53 | }, 54 | }, 55 | } { 56 | t.Run(tc.in, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | assert.Equal(t, tc.out, ParseRule(tc.in)) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/pwgen/rand.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "fmt" 6 | "math/big" 7 | "math/rand" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func init() { 13 | // seed math/rand in case we have to fall back to using it 14 | randFallback = rand.New(rand.NewSource(time.Now().Unix() + int64(os.Getpid()+os.Getppid()))) 15 | } 16 | 17 | var randFallback *rand.Rand 18 | 19 | func randomInteger(maxVal int) int { 20 | i, err := crand.Int(crand.Reader, big.NewInt(int64(maxVal))) 21 | if err == nil { 22 | return int(i.Int64()) 23 | } 24 | 25 | fmt.Fprintln(os.Stderr, "WARNING: No crypto/rand available. Falling back to PRNG") 26 | 27 | return randFallback.Intn(maxVal) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/pwgen/validate.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // containsAllClasses validates that the password contains at least one 8 | // character from each given character class. Can also contain other classes. 9 | func containsAllClasses(pw string, classes ...string) bool { 10 | CLASSES: 11 | for _, class := range classes { 12 | for _, ch := range class { 13 | if strings.Contains(pw, string(ch)) { 14 | continue CLASSES 15 | } 16 | } 17 | 18 | return false 19 | } 20 | 21 | return true 22 | } 23 | 24 | // containsOnlyClasses validates that the password only contains characters 25 | // from the given classes. Must not satisfy all classes. 26 | func containsOnlyClasses(pw string, classes ...string) bool { 27 | for _, c := range pw { 28 | for _, class := range classes { 29 | if !strings.Contains(class, string(c)) { 30 | return false 31 | } 32 | } 33 | } 34 | 35 | return true 36 | } 37 | 38 | func containsMaxConsecutive(pw string, n int) bool { 39 | last := "" 40 | repCnt := 1 41 | 42 | for _, r := range pw { 43 | if last == string(r) { 44 | repCnt++ 45 | if repCnt >= n { 46 | return false 47 | } 48 | } else { 49 | repCnt = 1 50 | } 51 | 52 | last = string(r) 53 | } 54 | 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /pkg/pwgen/validate_test.go: -------------------------------------------------------------------------------- 1 | package pwgen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMaxConsec(t *testing.T) { 10 | t.Parallel() 11 | 12 | // good 13 | for _, tc := range []string{ 14 | "abcd", 15 | "foobar", 16 | "nope", 17 | "AaAa", 18 | "aaabbbaaa", 19 | } { 20 | assert.True(t, containsMaxConsecutive(tc, 4)) 21 | } 22 | // bad 23 | for _, tc := range []string{ 24 | "aaaa", 25 | "bbb", 26 | "fooobar", 27 | "AaaaA", 28 | } { 29 | assert.False(t, containsMaxConsecutive(tc, 3)) 30 | } 31 | } 32 | 33 | func TestContainsOnly(t *testing.T) { 34 | t.Parallel() 35 | 36 | // good 37 | for _, tc := range []string{ 38 | "aBcDeF", 39 | } { 40 | assert.True(t, containsOnlyClasses(tc, Upper+Lower)) 41 | } 42 | 43 | // bad 44 | for _, tc := range []string{ 45 | "aBcDeF3", 46 | } { 47 | assert.False(t, containsOnlyClasses(tc, Upper+Lower)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/pwgen/xkcdgen/pwgen.go: -------------------------------------------------------------------------------- 1 | // Package xkcdgen provides a simple wrapper around the xkcdpwgen 2 | // package to generate random passphrases. 3 | package xkcdgen 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/martinhoefling/goxkcdpwgen/xkcdpwgen" 9 | ) 10 | 11 | // Random returns a random passphrase combined from four words. 12 | func Random() string { 13 | password, _ := RandomLength(4, "en") 14 | 15 | return password 16 | } 17 | 18 | // RandomLength returns a random passphrase combined from the desired number. 19 | // of words. Words are drawn from lang. 20 | func RandomLength(length int, lang string) (string, error) { 21 | return RandomLengthDelim(length, " ", lang, false, false) 22 | } 23 | 24 | // RandomLengthDelim returns a random passphrase combined from the desired number 25 | // of words and the given delimiter. Words are drawn from lang. 26 | func RandomLengthDelim(length int, delim, lang string, capitalize, numbers bool) (string, error) { 27 | g := xkcdpwgen.NewGenerator() 28 | g.SetNumWords(length) 29 | g.SetDelimiter(delim) 30 | g.SetCapitalize(delim == "" || capitalize) 31 | g.SetRandomNumbers(numbers) 32 | 33 | if err := g.UseLangWordlist(lang); err != nil { 34 | return "", fmt.Errorf("failed to use wordlist for lang %s: %w", lang, err) 35 | } 36 | 37 | return g.GeneratePasswordString(), nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/pwgen/xkcdgen/pwgen_test.go: -------------------------------------------------------------------------------- 1 | package xkcdgen 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRandom(t *testing.T) { 11 | t.Parallel() 12 | 13 | pw := Random() 14 | if len(pw) < 4 { 15 | t.Errorf("too short") 16 | } 17 | 18 | if len(strings.Fields(pw)) < 4 { 19 | t.Errorf("too few words") 20 | } 21 | } 22 | 23 | func TestRandomLengthDelim(t *testing.T) { 24 | t.Parallel() 25 | 26 | _, err := RandomLengthDelim(10, " ", "cn_ZH", false, false) 27 | require.Error(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/qrcon/qrcon_test.go: -------------------------------------------------------------------------------- 1 | package qrcon 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func ExampleQRCode() { //nolint:testableexamples 11 | code, err := QRCode("foo") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | fmt.Println(code) 17 | } 18 | 19 | func TestQRCode(t *testing.T) { 20 | t.Parallel() 21 | 22 | _, err := QRCode("https://www.gopass.pw/") 23 | require.NoError(t, err) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/set/filter.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | // Filter filters all r's from the input list. 4 | func Filter[K comparable](in []K, r ...K) []K { 5 | rs := Map(r) 6 | var out []K 7 | 8 | for _, i := range in { 9 | if !rs[i] { 10 | out = append(out, i) 11 | } 12 | } 13 | 14 | return out 15 | } 16 | 17 | // Contains returns true if e is contained in the input list. 18 | func Contains[K comparable](in []K, e K) bool { 19 | rs := Map(in) 20 | 21 | _, found := rs[e] 22 | 23 | return found 24 | } 25 | -------------------------------------------------------------------------------- /pkg/set/filter_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFilter(t *testing.T) { 10 | t.Parallel() 11 | 12 | in := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 13 | out := Filter(in, 6, 7, 8, 9) 14 | 15 | assert.Equal(t, []int{1, 2, 3, 4, 5}, out) 16 | } 17 | 18 | func TestFilter_EmptyInput(t *testing.T) { 19 | t.Parallel() 20 | 21 | in := []int{} 22 | out := Filter(in, 1, 2, 3) 23 | 24 | assert.Equal(t, []int(nil), out) 25 | } 26 | 27 | func TestFilter_NoElementsToRemove(t *testing.T) { 28 | t.Parallel() 29 | 30 | in := []int{1, 2, 3, 4, 5} 31 | out := Filter(in) 32 | 33 | assert.Equal(t, []int{1, 2, 3, 4, 5}, out) 34 | } 35 | 36 | func TestFilter_RemoveNonExistentElements(t *testing.T) { 37 | t.Parallel() 38 | 39 | in := []int{1, 2, 3, 4, 5} 40 | out := Filter(in, 6, 7, 8) 41 | 42 | assert.Equal(t, []int{1, 2, 3, 4, 5}, out) 43 | } 44 | 45 | func TestContains(t *testing.T) { 46 | t.Parallel() 47 | 48 | in := []int{1, 2, 3, 4, 5} 49 | 50 | assert.True(t, Contains(in, 3)) 51 | assert.False(t, Contains(in, 6)) 52 | } 53 | 54 | func TestContains_EmptyInput(t *testing.T) { 55 | t.Parallel() 56 | 57 | in := []int{} 58 | 59 | assert.False(t, Contains(in, 1)) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/set/map.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | // Map takes a slice of a given type and create a boolean map with keys 4 | // of that type. 5 | func Map[K comparable](in []K) map[K]bool { 6 | m := make(map[K]bool, len(in)) 7 | for _, i := range in { 8 | m[i] = true 9 | } 10 | 11 | return m 12 | } 13 | 14 | // Apply applies the given function to every element of the slice. 15 | func Apply[K comparable](in []K, f func(K) K) []K { 16 | out := make([]K, len(in)) 17 | for i, v := range in { 18 | out[i] = f(v) 19 | } 20 | 21 | return out 22 | } 23 | -------------------------------------------------------------------------------- /pkg/set/map_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMapFunc(t *testing.T) { 10 | t.Parallel() 11 | 12 | assert.Equal(t, map[int]bool{1: true, 2: true, 3: true}, Map([]int{1, 2, 3})) 13 | } 14 | 15 | func TestApplyFunc(t *testing.T) { 16 | t.Parallel() 17 | 18 | assert.Equal(t, []int{2, 3, 4}, Apply([]int{1, 2, 3}, func(i int) int { return i + 1 })) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/set/sorted.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "maps" 5 | "slices" 6 | 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | // SortedKeys returns the sorted keys of the map. 11 | func SortedKeys[K constraints.Ordered, V any](m map[K]V) []K { 12 | // sort 13 | keys := maps.Keys(m) 14 | 15 | return slices.Sorted(keys) 16 | } 17 | 18 | // Sorted returns a sorted set of the input. 19 | func Sorted[K constraints.Ordered](l []K) []K { 20 | return SortedFiltered(l, func(k K) bool { 21 | return true 22 | }) 23 | } 24 | 25 | // SortedFiltered returns a sorted set of the input, filtered by the predicate. 26 | func SortedFiltered[K constraints.Ordered](l []K, want func(K) bool) []K { 27 | if len(l) == 0 { 28 | return l 29 | } 30 | 31 | // deduplicate 32 | m := make(map[K]struct{}, len(l)) 33 | for _, k := range l { 34 | if !want(k) { 35 | continue 36 | } 37 | m[k] = struct{}{} 38 | } 39 | 40 | // sort 41 | return SortedKeys(m) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/set/sorted_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSorted(t *testing.T) { 11 | t.Parallel() 12 | 13 | want := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 14 | in := append(want, want...) 15 | rand.Shuffle(len(in), func(i, j int) { 16 | in[i], in[j] = in[j], in[i] 17 | }) 18 | assert.Equal(t, want, Sorted(in)) 19 | } 20 | 21 | func TestSortedFiltered(t *testing.T) { 22 | t.Parallel() 23 | 24 | in := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 25 | in = append(in, in...) 26 | rand.Shuffle(len(in), func(i, j int) { 27 | in[i], in[j] = in[j], in[i] 28 | }) 29 | 30 | want := []int{2, 4, 6, 8, 10} 31 | assert.Equal(t, want, SortedFiltered(in, func(i int) bool { 32 | return i%2 == 0 33 | })) 34 | 35 | assert.Equal(t, []int{}, SortedFiltered([]int{}, func(i int) bool { return true })) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/tempfile/mount_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package tempfile 5 | 6 | import ( 7 | "context" 8 | "os" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | var shmDir = "/dev/shm" 14 | 15 | // tempdir returns a temporary directory suiteable for sensitive data. It tries 16 | // /dev/shm but if this isn't working it will return an empty string. Using 17 | // this with ioutil.Tempdir will ensure that we're getting the "best" tempdir. 18 | func tempdirBase() string { 19 | if fi, err := os.Stat(shmDir); err == nil { 20 | if fi.IsDir() { 21 | if unix.Access(shmDir, unix.W_OK) == nil { 22 | return shmDir 23 | } 24 | } 25 | } 26 | 27 | return "" 28 | } 29 | 30 | func (t *File) mount(context.Context) error { 31 | return nil 32 | } 33 | 34 | func (t *File) unmount(context.Context) error { 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/tempfile/mount_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | // +build !linux,!darwin 3 | 4 | package tempfile 5 | 6 | import "context" 7 | 8 | var shmDir = "" 9 | 10 | // tempdir returns a temporary directory suiteable for sensitive data. On 11 | // Windows, just return empty string for ioutil.TempFile. 12 | func tempdirBase() string { 13 | return "" 14 | } 15 | 16 | func (t *File) mount(context.Context) error { 17 | _ = t.dev // to trick megacheck 18 | return nil 19 | } 20 | 21 | func (t *File) unmount(context.Context) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/termio/context_test.go: -------------------------------------------------------------------------------- 1 | package termio 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gopasspw/gopass/internal/config" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPassPromptFunc(t *testing.T) { 13 | t.Parallel() 14 | 15 | ctx := config.NewContextInMemory() 16 | 17 | assert.False(t, HasPassPromptFunc(ctx)) 18 | assert.NotNil(t, GetPassPromptFunc(ctx)) 19 | 20 | ctx = WithPassPromptFunc(ctx, func(context.Context, string) (string, error) { 21 | return "test", nil 22 | }) 23 | assert.True(t, HasPassPromptFunc(ctx)) 24 | assert.NotNil(t, GetPassPromptFunc(ctx)) 25 | sv, err := GetPassPromptFunc(ctx)(ctx, "") 26 | require.NoError(t, err) 27 | assert.Equal(t, "test", sv) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/termio/identity_test.go: -------------------------------------------------------------------------------- 1 | package termio 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDetectName(t *testing.T) { 11 | ctx := config.NewContextInMemory() 12 | td := t.TempDir() 13 | t.Setenv("XDG_CONFIG_HOME", td) 14 | t.Setenv("GOPASS_HOMEDIR", td) 15 | 16 | t.Setenv("GIT_AUTHOR_NAME", "") 17 | t.Setenv("DEBFULLNAME", "") 18 | t.Setenv("USER", "") 19 | 20 | assert.Empty(t, DetectName(ctx, nil)) 21 | 22 | t.Setenv("USER", "foo") 23 | assert.Equal(t, "foo", DetectName(ctx, nil)) 24 | } 25 | 26 | func TestDetectEmail(t *testing.T) { 27 | ctx := config.NewContextInMemory() 28 | td := t.TempDir() 29 | t.Setenv("XDG_CONFIG_HOME", td) 30 | t.Setenv("GOPASS_HOMEDIR", td) 31 | 32 | t.Setenv("GIT_AUTHOR_EMAIL", "") 33 | t.Setenv("DEBEMAIL", "") 34 | t.Setenv("EMAIL", "") 35 | 36 | assert.Empty(t, DetectEmail(ctx, nil)) 37 | 38 | t.Setenv("EMAIL", "foo@bar.de") 39 | assert.Equal(t, "foo@bar.de", DetectEmail(ctx, nil)) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/termio/progress_test.go: -------------------------------------------------------------------------------- 1 | package termio 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleProgressBar() { //nolint:testableexamples 11 | maxVal := 100 12 | pb := NewProgressBar(int64(maxVal)) 13 | 14 | for range maxVal + 20 { 15 | pb.Inc() 16 | pb.Add(23) 17 | pb.Set(42) 18 | time.Sleep(150 * time.Millisecond) 19 | } 20 | 21 | time.Sleep(5 * time.Second) 22 | pb.Done() 23 | } 24 | 25 | func TestProgress(t *testing.T) { 26 | maxVal := 2 27 | pb := NewProgressBar(int64(maxVal)) 28 | pb.Hidden = true 29 | pb.Inc() 30 | assert.Equal(t, int64(1), pb.current) 31 | } 32 | 33 | func TestProgressNil(t *testing.T) { 34 | t.Parallel() 35 | 36 | var pb *ProgressBar 37 | pb.Inc() 38 | pb.Add(4) 39 | pb.Done() 40 | } 41 | 42 | func TestProgressBytes(t *testing.T) { 43 | maxSize := 2 << 24 44 | pb := NewProgressBar(int64(maxSize)) 45 | pb.Hidden = true 46 | pb.Bytes = true 47 | 48 | for i := range 24 { 49 | pb.Set(2 << (i + 1)) 50 | } 51 | 52 | assert.Equal(t, int64(maxSize), pb.current) 53 | pb.Done() 54 | } 55 | -------------------------------------------------------------------------------- /pkg/termio/promptpass_test.go: -------------------------------------------------------------------------------- 1 | package termio 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gopasspw/gopass/internal/config" 7 | "github.com/gopasspw/gopass/pkg/ctxutil" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPromptPass(t *testing.T) { 12 | t.Parallel() 13 | 14 | ctx := config.NewContextInMemory() 15 | ctx = ctxutil.WithTerminal(ctx, false) 16 | ctx = ctxutil.WithAlwaysYes(ctx, true) 17 | 18 | _, err := promptPass(ctx, "foo") 19 | require.NoError(t, err) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/termio/promptpass_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package termio 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/gopasspw/gopass/pkg/ctxutil" 12 | "golang.org/x/crypto/ssh/terminal" 13 | ) 14 | 15 | // promptPass will prompt user's for a password by terminal. 16 | func promptPass(ctx context.Context, prompt string) (string, error) { 17 | if !ctxutil.IsTerminal(ctx) { 18 | return AskForString(ctx, prompt, "") 19 | } 20 | 21 | fmt.Fprintf(Stderr, "%s: ", prompt) 22 | passBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) 23 | fmt.Fprintln(Stderr, "") 24 | return string(passBytes), err 25 | } 26 | -------------------------------------------------------------------------------- /tests/audit_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAudit(t *testing.T) { 11 | ts := newTester(t) 12 | defer ts.teardown() 13 | 14 | ts.initStore() 15 | ts.initSecrets("") 16 | 17 | t.Run("audit the test store", func(t *testing.T) { 18 | out, err := ts.run("audit") 19 | require.Error(t, err) 20 | assert.Contains(t, out, "crunchy") 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /tests/can/can_test.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ProtonMail/go-crypto/openpgp" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPubring(t *testing.T) { 12 | t.Parallel() 13 | 14 | fh, err := can.Open("gnupg/pubring.gpg") 15 | require.NoError(t, err) 16 | defer fh.Close() //nolint:errcheck 17 | 18 | el, err := openpgp.ReadKeyRing(fh) 19 | require.NoError(t, err) 20 | 21 | require.Len(t, el, 1) 22 | assert.Equal(t, "BE73F104", el[0].PrimaryKey.KeyIdShortString()) 23 | } 24 | -------------------------------------------------------------------------------- /tests/can/gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/tests/can/gnupg/pubring.gpg -------------------------------------------------------------------------------- /tests/can/gnupg/random_seed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/tests/can/gnupg/random_seed -------------------------------------------------------------------------------- /tests/can/gnupg/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/tests/can/gnupg/secring.gpg -------------------------------------------------------------------------------- /tests/can/gnupg/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopasspw/gopass/46dfddaef094fd4161afa1ab2885cb6b0f4f3b6a/tests/can/gnupg/trustdb.gpg -------------------------------------------------------------------------------- /tests/delete_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDelete(t *testing.T) { 12 | ts := newTester(t) 13 | defer ts.teardown() 14 | 15 | ts.initStore() 16 | 17 | out, err := ts.run("delete") 18 | require.Error(t, err) 19 | assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" rm name\n", out) 20 | 21 | out, err = ts.run("delete foobarbaz") 22 | require.Error(t, err) 23 | assert.Contains(t, out, "does not exist", out) 24 | 25 | ts.initSecrets("") 26 | 27 | secrets := []string{"baz", "foo/bar"} 28 | for _, secret := range secrets { 29 | out, err = ts.run("delete -f " + secret) 30 | require.NoError(t, err) 31 | assert.Empty(t, out) 32 | 33 | out, err = ts.run("delete -f " + secret) 34 | require.Error(t, err) 35 | assert.Contains(t, out, "does not exist\n", out) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/generate_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenerate(t *testing.T) { 12 | ts := newTester(t) 13 | defer ts.teardown() 14 | 15 | ts.initStore() 16 | 17 | out, err := ts.run("generate") 18 | require.Error(t, err) 19 | assert.Equal(t, "\nError: please provide a password name\n", out) 20 | 21 | out, err = ts.run("generate foo 0") 22 | require.Error(t, err) 23 | assert.Equal(t, "\nError: password length must not be zero\n", out) 24 | 25 | out, err = ts.run("generate -p baz 42") 26 | require.NoError(t, err) 27 | 28 | lines := strings.Split(out, "\n") 29 | 30 | require.Greater(t, len(lines), 2) 31 | assert.Contains(t, out, "The generated password is:") 32 | assert.Len(t, lines[3], 42) 33 | 34 | t.Setenv("GOPASS_CHARACTER_SET", "a") 35 | 36 | out, err = ts.run("generate -p zab 4") 37 | require.NoError(t, err) 38 | 39 | lines = strings.Split(out, "\n") 40 | 41 | require.Greater(t, len(lines), 2) 42 | assert.Contains(t, out, "The generated password is:") 43 | assert.Equal(t, "aaaa", lines[3]) 44 | } 45 | -------------------------------------------------------------------------------- /tests/grep_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGrep(t *testing.T) { 12 | ts := newTester(t) 13 | defer ts.teardown() 14 | 15 | ts.initStore() 16 | 17 | out, err := ts.run("grep") 18 | require.Error(t, err) 19 | assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" grep arg\n", out) 20 | 21 | out, err = ts.run("grep BOOM") 22 | require.NoError(t, err) 23 | assert.Contains(t, out, "Scanned 0 secrets. 0 matches, 0 errors") 24 | 25 | ts.initSecrets("") 26 | 27 | out, err = ts.run("grep moar") 28 | require.NoError(t, err) 29 | assert.Contains(t, out, "fixed/secret matches") 30 | } 31 | -------------------------------------------------------------------------------- /tests/init_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestInit(t *testing.T) { 11 | ts := newTester(t) 12 | defer ts.teardown() 13 | 14 | out, err := ts.run("init") 15 | require.NoError(t, err) 16 | assert.Contains(t, out, "Initializing a new password store ...") 17 | assert.Contains(t, out, "initialized") 18 | 19 | ts = newTester(t) 20 | defer ts.teardown() 21 | 22 | out, err = ts.run("init " + keyID) 23 | require.NoError(t, err) 24 | assert.Contains(t, out, "initialized for") 25 | 26 | ts = newTester(t) 27 | defer ts.teardown() 28 | 29 | ts.initStore() 30 | // try to init again 31 | out, err = ts.run("init " + keyID) 32 | require.Error(t, err) 33 | 34 | for _, o := range []string{ 35 | "found already initialized store at ", 36 | "You can add secondary stores with 'gopass init --path --store '", 37 | } { 38 | assert.Contains(t, out, o) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/uninitialized_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestUninitialized(t *testing.T) { 11 | ts := newTester(t) 12 | defer ts.teardown() 13 | 14 | commands := []string{ 15 | "", 16 | "copy", 17 | "cp", 18 | "delete", 19 | "edit", 20 | "find", 21 | "generate", 22 | "grep", 23 | "insert", 24 | "list", 25 | "ls", 26 | "mount", 27 | "move", 28 | "mv", 29 | "remove", 30 | "rm", 31 | "show", 32 | } 33 | 34 | for _, command := range commands { 35 | t.Run(command, func(t *testing.T) { 36 | out, err := ts.run(command) 37 | require.Error(t, err) 38 | assert.Contains(t, out, "password-store is not initialized. Try ") 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/blang/semver/v4" 7 | ) 8 | 9 | func getVersion() semver.Version { 10 | sv, err := semver.Parse(strings.TrimPrefix(version, "v")) 11 | if err == nil { 12 | return sv 13 | } 14 | 15 | return semver.Version{ 16 | Major: 1, 17 | Minor: 15, 18 | Patch: 16, 19 | Pre: []semver.PRVersion{ 20 | {VersionStr: "git"}, 21 | }, 22 | Build: []string{"9090fcc4"}, 23 | } 24 | } 25 | --------------------------------------------------------------------------------