QtPass is actively maintained and security updates are provided for the latest releases. Older versions are not supported.
| Version | Supported |
|---|---|
| 1.8.x | ✅ |
| < 1.8 | ❌ |
If you discover a security vulnerability in QtPass, please report it responsibly:
- Do NOT create a public GitHub issue for security vulnerabilities
- Do email the maintainer directly at:
help@qtpass.org - Alternative: Open a private security advisory via GitHub
- Include in your report:
- Description of the vulnerability
- Steps to reproduce the issue
- Potential impact assessment
- Any suggested fixes (optional)
- Acknowledgment: Within 48 hours
- Initial assessment: Within 7 days
- Fix timeline: Depends on severity; critical issues are prioritized
QtPass is a GUI for pass, the standard Unix password manager. Keep in mind:
- QtPass does not encrypt passwords - encryption is handled by GPG
- Passwords are stored in your local password store (typically
~/.password-store) - QtPass requires GPG to be installed and configured on your system
- The clipboard is cleared after a configurable timeout (default: 45 seconds)
- A password store's
.gitdirectory is not inert data. With Git enabled, QtPass runsgitin the store, and Git runs the hooks in.git/hookswith your rights. A plaingit clonedoes not copy a remote's hooks, but a store received as an archive or an existing checkout, or one on storage others can write to, may carry them. Treat such a.gitas you would treat a script. - Git stays in the store, as with
pass. With "use a parent repository" off, QtPass keeps Git from a repository above the store (GIT_CEILING_DIRECTORIES, and the store's ownGIT_DIRwhen the parent's path holds:, on Unix only for a store and.gitthe user owns or thatsafe.directorytrusts, as Git's own check would require). - That ownership is judged each time Git runs, not once: a
.gitthat appears or changes hands later is refused then. The check is not repeated on Windows, where Git checks owners by SID (#2089). safe.directorycounts only from system, global and command-line configuration, never the repository's own. A relative entry such as., which Git resolves against its working directory, trusts nothing here.- With the
passbackend,passruns the Git steps itself;pass1.7.4 sets only the ceiling, so a parent path holding:lets it climb. - A signed
.gpg-idprotects the recipient list against someone who can change the store but does not hold the signing key. QtPass verifies the signature over the exact bytes it then encrypts to, so a file swapped between the check and its use is not a way in. - A signature proves authentic, not current, and not placed. An older list, genuinely signed, that still names a member since removed can be put back by the same someone, or copied into a folder that never had a list of its own.
- A revoked signing key vouches for nothing. A signature gpg reports as made by a revoked key does not verify, so revoking a leaked signing key stops it from signing lists QtPass accepts, also while it is still configured. A signature by an expired key still does.
- The signature binds a list to a folder of the store, not to the store. The folder line names a path relative to the store, so an authentic list signed for another store, with the same signing key and the same folder, verifies here too, and someone who can write to this store can put it in place. Use a separate signing key for each store whose recipients differ (#2050).
- The profile settings warn when two profiles for different stores name the same signing-key fingerprint (a key named by its primary fingerprint in one and by a subkey's in the other is not recognised as one); the warning does not stop Settings from being saved.
- A
.gpg-idQtPass writes for a store with a signing key therefore starts with two comment lines the signature covers:# QtPass-GpgId-Generation: Nand# QtPass-GpgId-Folder: <store-relative folder>. - QtPass remembers, per list, the highest generation it has accepted or written on this device, with a SHA-256 of the exact bytes, and refuses a signed list with a lower generation, one of the same generation with other bytes, or one written for another folder.
- Without a readable and writable record nothing is accepted or written; the record is one per user, and a second QtPass process waits for it or is refused.
- A holder of the signing key saving the recipients from QtPass writes the next generation; that is the way back after a deliberate revert. The dialog preselects only a list that verifies (an older but authentic one with a warning; one that does not verify not at all), so what is ticked is never an unsigned edit.
- The limits of that: it detects a rollback only on a device that has already accepted or written a higher generation; a device seeing the store for the first time, or one that was offline for the change, has nothing to compare against.
- A folder moved or renamed carries a list bound to its old place; saving its recipients once binds it anew.
- A sub-folder whose own list was removed or replaced by a link is refused, not encrypted to its parent folder's list instead: a list that was not a regular file, or one this device accepted before and that is now gone, has to be saved again from the Users dialog by a signing-key holder. Without a signing key the parent's list applies, as with
pass. - The
passbackend gets the same checks: before QtPass haspassinsert, move or copy an entry, or save a folder's recipients (pass init, which re-encrypts what is under it), it verifies the listspasswill use (signature, generation, folder, a sub-folder's own list), and refuses as above. - For a folder moved or copied with the
passbackend, that includes the lists inside it, used by an entry or not (a copy merging into a folder of the same name brings them to the entries there): they are checked where they lie before the move. - Moving or copying into an existing folder has
passre-encrypt all of that folder, so every list in it is checked too, also one no entry uses yet: a copy merges into a folder of the same name there.passthen reads each list again when it encrypts, so a list swapped in between haspass's own check only, the signature (a concurrent writer, see below). - The generation is monotonic per device, not a distributed sequence number: two devices can both produce the same next generation, and Git synchronisation resolves that.
- The device that wrote or accepted one of two such lists then meets the other as the same generation with other bytes: a conflict, refused and not preselected, until a signing-key holder checks the recipients and saves, writing the next generation.
- Compatibility: the lines are comments to
pass1.7.4 and later and to QtPass 1.8 and later;passup to 1.7.3 and Android Password Store take a comment for a recipient, so a signed store's clients need to be current. Stores without a signing key get plain lists, as before. - A list written by
pass(also through QtPass'spassbackend) or by QtPass before 2.0 carries no generation and reads as 0: once a device has accepted a higher one, such a list is refused, and the Users dialog preselects nothing from it (without a folder line it may be any folder's old pair), until a signing-key holder selects and saves the recipients from QtPass. - At generation 0 nothing is pinned:
passwrites a headerless list for every change (also through QtPass'spassbackend), so different bytes there are the normal course of a store kept withpass, not a conflict. Freshness begins with the first list QtPass writes. - Path checks catch mistakes, not a hostile local writer. Store-boundary checks (
.., symlinks and junctions, absolute paths) run before a file operation, not atomically with it. Another process that can write to your store while QtPass runs can also replace what is in it; that is outside what QtPass can protect. - What QtPass does hold to when it adds, edits, copies or re-encrypts an entry is that
gpgnever opens a path in the store for output: it encrypts into a directory of QtPass's own outside the store, and the bytes go into the store through a file created next to the entry, written by its open handle, and the operating system's rename. - That rename replaces a file or a link under the name by then (a link planted since the check included) as an entry and follows nothing; a directory under the name cannot be replaced by a file, so the write fails and leaves it.
- Afterwards the object under the name is opened without following and compared, by device and inode, with the file that was written: a file swapped under the temporary's name before the rename (a hard link to something of yours, which passes every check by name) is reported instead of taken for the entry.
- What is under the name then is left as it is: removing it by name could take another writer's file that landed there since. Where the system cannot give both identities the write stands rather than reporting a swap it cannot prove: on FAT and exFAT a rename moves the file ID, and neither has hard links to swap in.
- A
.gpg-id(from the Users dialog, a new profile, or a new folder taking its parent's list) and its.gpg-id.sigare written the same way. On Windows a junction planted under one of these names makes the write fail rather than replace it. gpgsigns the bytes QtPass just wrote, fed on its standard input (not whatever is under the file's name by the time it would read it), and writes the signature into a directory of QtPass's own, from where it is placed next to the list.- It signs with
--local-user, so a missing secret key fails the signing instead ofgpgtaking another key, and the new signature is verified as one by a configured signing key before it is placed. - Copying an entry reads the source as the regular file it is at the moment of the open (
O_NOFOLLOW; on Windows the reparse point itself), not as whatever a link planted under the name since the check points to, and writes the copy the same staged way; a co-writer cannot have the bytes of a file only you can read copied into the store and committed. - The
passbackend runspass insert, which writes the entry's path directly; a store kept withpasshaspass's semantics there. - A link inside the store is not part of it. A store is often shared (a team's Git repository, a synced folder), and Git carries symbolic links, so a co-writer can make
git pullcreateBank.gpg -> /elsewhere/secret.gpgon your machine. QtPass treats a symbolic link or NTFS junction found inside the store as neither an entry, a folder nor metadata. - Concretely: every walk skips links (re-encryption, search, staging); every operation on a link, or on anything behind one, is refused (show, edit, add, move, copy, re-key); a linked
.gpg-idor.gpg-id.sigis not a recipient list; deleting a link removes the link. - The configured store root itself may be a link (
~/.password-storepointing at a synced folder is a normal setup): that is your configuration, not something found inside the store. - Nothing is logged that is fed to
gpgorpasson standard input (passwords, passphrases). WithQT_LOGGING_RULES=qtpass.debug=truethe commands and their arguments are logged; values of options such as--passphraseare redacted should they ever appear.
A build option (qmake6 CONFIG+=rnp, see #2005) adds OpenPGP through the RNP library, for a future mode that needs no gpg, git or pass program (Windows without Gpg4win, Android). Nothing uses it yet, and GnuPG stays the default and the recommended way. The trade-off, for when that mode exists:
- With GnuPG, QtPass never holds your secret key.
gpg-agentkeeps it in a process of its own, pinentry asks for the passphrase, and a smartcard or YubiKey can hold the key so that it never reaches the computer at all. - Without GnuPG, QtPass holds it. The secret key is exported from GnuPG and imported into a keyring of QtPass's own. While an entry is decrypted, the unlocked key is in QtPass's memory, and so is the passphrase you type into QtPass's dialog instead of pinentry.
- On disk, secret keys keep their own passphrase. The keyring is written to a folder of QtPass's own (owner-only; on Windows as private as the user profile it is in; never
~/.gnupg), each file replaced whole. A secret key without a passphrase is refused unless the user agrees to it explicitly, and one can be given a passphrase first. Keys stay locked until an operation needs them. - No smartcards or YubiKeys in that mode: RNP cannot decrypt on a card. (Sequoia can, through the openpgp-card crates; a later option, not this one.)
- What it refuses, as GnuPG does: a message without integrity protection (CFB without MDC, the kind EFAIL abuses), and plaintext beyond 64 MiB, so a small compressed entry cannot fill memory.
- Signed recipient lists: it signs and checks
.gpg-id.sigas QtPass does with GnuPG, and never falls back to another secret key. A fingerprint names exactly one key, an email address only a key with that address, a revoked user ID none. A signature by a revoked key counts for nothing; one by a key that has since expired still counts. - Choosing that mode will therefore be a deliberate step, explained where it is offered (the first-run wizard and the settings), never a silent fallback when
gpgis missing.
QtPass depends on:
- Qt 6.8+ (use
qmake6) - GUI framework; the 1.8.x releases also build with Qt 5.15 - GPG (gpg2) - encryption
- pass (optional) - password store CLI
- Git (optional) - version control
- RNP (optional,
CONFIG+=rnpbuilds only) - in-process OpenPGP, see above
Ensure your system dependencies are kept up to date for security patches.