Skip to content

fix(security): chmod 0600 on .gpg-id after write#1465

Merged
annejan merged 5 commits into
mainfrom
fix/gpg-id-permissions
May 12, 2026
Merged

fix(security): chmod 0600 on .gpg-id after write#1465
annejan merged 5 commits into
mainfrom
fix/gpg-id-permissions

Conversation

@annejan
Copy link
Copy Markdown
Member

@annejan annejan commented May 12, 2026

Summary

.gpg-id lists the GPG key IDs (often full fingerprints) that a password store sub-tree is encrypted to. Default-umask creation leaves it world-readable (0644 with the typical 0022 umask).

While ~/.password-store itself is normally 0700 and protects the file in practice, users who relocate the store onto NFS / SMB / USB / a FS with a more permissive parent mode lose that protection. The file leaks both who can decrypt the store and (for full fingerprints) the long-term identity of the recipients.

Change

Apply QFile::setPermissions(ReadOwner | WriteOwner) (= 0600 on Unix; best-effort no-op on Windows) immediately after the close() call. Two write sites:

  • ImitatePass::writeGpgIdFile — recipient edits via the Users dialog
  • MainWindow::addFolder — when "Add a .gpg-id when creating a new folder" is enabled in settings

Tests

  • tst_util::writeGpgIdFileSetsOwnerOnlyPerms — forces umask 0022, runs ImitatePass::writeGpgIdFile, asserts QFile::permissions is ReadOwner|WriteOwner only. Unix-only (Qt's permission bits don't round-trip through Windows ACLs).

Test plan

  • qmake6 -r && make -j4 clean
  • tests/auto/util/tst_util — 114 passed (was 113, +1 new)
  • doxygen Doxyfile — zero warnings
  • clang-format applied
  • Manual: created a new folder with addGPGId enabled, verified ls -la .gpg-id shows -rw-------
  • Manual: edited recipients via Users dialog, same result

Diff

3 files changed, 55 insertions (≈9 lines of production code + comments, the rest is the new test).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Files created by the app (including when adding folders via the UI) are now explicitly set to owner-only read/write permissions after creation to reduce exposure on permissive filesystems.
  • Tests

    • Added an automated, platform-aware test that verifies .gpg-id files are created with owner-only permissions; the test is skipped on unsupported platforms.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR sets owner-only read/write permissions on created .gpg-id files in the core writer and folder-creation path, and adds a Unix-aware test that verifies the file permissions.

Changes

GPG-ID File Permissions Security Hardening

Layer / File(s) Summary
Core permission-setting in writeGpgIdFile
src/imitatepass.cpp
ImitatePass::writeGpgIdFile adds a QFile::setPermissions call after closing the generated .gpg-id file, setting permissions to owner-only read/write (best-effort on platforms where permission changes may be no-op).
Integrated permission-setting in folder creation
src/mainwindow.cpp
MainWindow::addFolder() sets the .gpg-id file permissions to owner-only read/write via QFile::setPermissions after writing and closing the file.
Test validation of owner-only permissions
tests/auto/util/tst_util.cpp
Adds a non-Windows <sys/stat.h> include, a new test slot declaration, and implements writeGpgIdFileSetsOwnerOnlyPerms() which sets a permissive umask, invokes ImitatePass::writeGpgIdFile, restores umask, and asserts the file has owner-only read/write bits while group/other bits are unset (skips on Windows).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

size:L

Suggested reviewers

  • nogeenhenk

Poem

🐰 I nibble code with careful paws,
I tuck .gpg-id behind secure jaws,
Owner-only bits snug in their den,
Quiet hops away, I check again,
A tiny guard against wandering claws.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(security): chmod 0600 on .gpg-id after write' directly and specifically describes the main security fix: applying strict 0600 permissions to .gpg-id files after writing them.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gpg-id-permissions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/auto/util/tst_util.cpp`:
- Around line 2216-2222: Replace the bare QVERIFY() assertions that check
QFile::Permissions in the test (the local variable perms from
QFile(gpgIdFile).permissions()) with QVERIFY2() calls that include descriptive
messages so failing tests show which specific flag is wrong; for each check
(testFlag(QFile::ReadOwner), WriteOwner, ReadGroup, WriteGroup, ReadOther,
WriteOther) update the assertion to QVERIFY2(perms.testFlag(...), "expected
<flag> to be set" or "expected <flag> to be unset") so diagnostics clearly
identify which permission failed.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a911a0d-e017-416b-a7af-e14ecad8d8f0

📥 Commits

Reviewing files that changed from the base of the PR and between b162da3 and b7aeefd.

📒 Files selected for processing (3)
  • src/imitatepass.cpp
  • src/mainwindow.cpp
  • tests/auto/util/tst_util.cpp

Comment thread tests/auto/util/tst_util.cpp Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

❌ Failed to clone repository into sandbox. Please try again.

@coveralls
Copy link
Copy Markdown

coveralls commented May 12, 2026

Coverage Status

coverage: 28.601% (+0.2%) from 28.416% — fix/gpg-id-permissions into main

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • tests/auto/util/tst_util.cpp

Commit: be70f24580850f183c3f2badeae72d1623825b4e

The changes have been pushed to the fix/gpg-id-permissions branch.

Time taken: 1m 58s

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/auto/util/tst_util.cpp`:
- Around line 2197-2215: Two assertions in the test block use bare QVERIFY(...)
and must be converted to QVERIFY2(...) with descriptive messages: replace
QVERIFY(tempDir.isValid()) (verify tempDir validity) with
QVERIFY2(tempDir.isValid(), "tempDir should be valid after creation") and
replace QVERIFY(QFile::exists(gpgIdFile)) (verify the gpg id file was written by
pass.writeGpgIdFile) with QVERIFY2(QFile::exists(gpgIdFile), "gpg id file must
exist after writeGpgIdFile"); locate these around the tempDir/gpgIdFile setup
and the call to ImitatePass::writeGpgIdFile and update the assertions to include
the provided messages.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: a73bc7e1-8458-421e-b982-fe70e052b4b3

📥 Commits

Reviewing files that changed from the base of the PR and between b7aeefd and be70f24.

📒 Files selected for processing (1)
  • tests/auto/util/tst_util.cpp

Comment thread tests/auto/util/tst_util.cpp Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • tests/auto/util/tst_util.cpp

Commit: 2b44e73c7f1ae858ba87788e20cf82556fb06488

The changes have been pushed to the fix/gpg-id-permissions branch.

Time taken: 1m 15s

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/auto/util/tst_util.cpp`:
- Around line 2202-2213: Introduce an RAII umask guard (e.g., class UmaskGuard
with ctor that calls ::umask(newMask) and stores oldMask_ and a dtor that
restores ::umask(oldMask_); delete copy ops) and use it around the test code
that currently calls ::umask(0022) / ::umask(oldUmask): construct UmaskGuard
guard(0022) before creating UserInfo/ImitatePass and calling
ImitatePass::writeGpgIdFile(...) and remove the explicit ::umask(oldUmask)
restore; this mirrors the SshAuthSockGuard pattern used elsewhere and ensures
automatic restoration if writeGpgIdFile throws.
- Around line 2216-2222: Add assertions to verify execute bits are unset: use
the existing perms variable (from QFile(gpgIdFile).permissions()) and add
QVERIFY2(!perms.testFlag(QFile::ExeOwner), "expected ExeOwner to be unset"),
QVERIFY2(!perms.testFlag(QFile::ExeGroup), "expected ExeGroup to be unset"), and
QVERIFY2(!perms.testFlag(QFile::ExeOther), "expected ExeOther to be unset") so
the test explicitly checks that execute permissions are not set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: b60e6d37-22ad-47ce-a050-df2a903bc65e

📥 Commits

Reviewing files that changed from the base of the PR and between be70f24 and 2b44e73.

📒 Files selected for processing (1)
  • tests/auto/util/tst_util.cpp

Comment thread tests/auto/util/tst_util.cpp
Comment thread tests/auto/util/tst_util.cpp Outdated
annejan and others added 3 commits May 12, 2026 14:01
The .gpg-id file lists the GPG key IDs (often full fingerprints) that a
password store / sub-tree is encrypted to. Default-umask creation leaves
it world-readable (0644 with the typical 0022 umask).

While the standard ~/.password-store is itself 0700 and protects the
file in practice, users who relocate the store onto an NFS share, an
SMB mount, a USB stick, or any FS where the parent directory's mode is
more permissive lose that protection. The .gpg-id leaks both who can
decrypt the store and (for full fingerprints) the long-term identity of
the recipients.

Lock the file to owner-only access (0600 on Unix; best-effort no-op on
Windows) immediately after close. Applied to both write sites:

- ImitatePass::writeGpgIdFile (used when the recipient list is edited
  through the Users dialog)
- MainWindow::addFolder (used when "Add a .gpg-id when creating a new
  folder" is enabled in settings)

Tests:
- tst_util::writeGpgIdFileSetsOwnerOnlyPerms — forces umask 0022, calls
  the production writeGpgIdFile path, asserts QFile::permissions is
  ReadOwner|WriteOwner only. Unix-only (Qt's permission bits don't
  round-trip on Windows).

Build clean, 114/114 util tests pass, doxygen zero warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@annejan annejan force-pushed the fix/gpg-id-permissions branch from 2b44e73 to 2850c36 Compare May 12, 2026 12:02
Defense-in-depth — the production writeGpgIdFile uses ReadOwner|WriteOwner
exclusively, so the execute bits are always unset by construction; assert
that explicitly so a future change to the permission mask can't silently
leave the file executable.

Reviewer nit from CodeRabbit (#1465). Skipping the sibling RAII-umask
suggestion: Qt Test doesn't throw on assertion failures and the umask
is restored on the next line, so the theoretical risk doesn't justify
the boilerplate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/imitatepass.cpp (1)

256-277: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Switch both .gpg-id write paths to QSaveFile for atomic writes.

Two functions currently use QFile with manual setPermissions(), creating a window where the .gpg-id file can be left corrupted if the process is interrupted during write/close. Follow the existing pattern in Util::writeTemplates: use QSaveFile, call setPermissions() before commit(), and check the commit() return value. This applies to:

  • ImitatePass::writeGpgIdFile() (lines 256–277)
  • MainWindow::addFolder() (lines 1487–1533, the second .gpg-id write path)
Pattern reference from Util::writeTemplates
QSaveFile saveFile(path);
if (!saveFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
  return false;
}
// ... write operations ...
out.flush();
if (out.status() != QTextStream::Ok) {
  return false;
}
return saveFile.commit();  // atomic: renames temp file only on success
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/imitatepass.cpp` around lines 256 - 277, Replace the QFile-based writes
of .gpg-id with QSaveFile in ImitatePass::writeGpgIdFile (and the second .gpg-id
write path in MainWindow::addFolder) following the Util::writeTemplates pattern:
open a QSaveFile, write the key lines, flush and check QTextStream::status(),
call QFile::setPermissions on the temporary save file before calling
saveFile.commit(), and verify commit() returned true; if any step fails, emit
the same error/return as the existing code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/imitatepass.cpp`:
- Around line 256-277: Replace the QFile-based writes of .gpg-id with QSaveFile
in ImitatePass::writeGpgIdFile (and the second .gpg-id write path in
MainWindow::addFolder) following the Util::writeTemplates pattern: open a
QSaveFile, write the key lines, flush and check QTextStream::status(), call
QFile::setPermissions on the temporary save file before calling
saveFile.commit(), and verify commit() returned true; if any step fails, emit
the same error/return as the existing code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba723a88-f444-421e-954b-dfa55c07979e

📥 Commits

Reviewing files that changed from the base of the PR and between 2b44e73 and 2850c36.

📒 Files selected for processing (3)
  • src/imitatepass.cpp
  • src/mainwindow.cpp
  • tests/auto/util/tst_util.cpp

@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 33.72%. Comparing base (b6f38ea) to head (e3e6dba).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/mainwindow.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1465      +/-   ##
==========================================
+ Coverage   33.47%   33.72%   +0.25%     
==========================================
  Files          44       44              
  Lines        4377     4379       +2     
==========================================
+ Hits         1465     1477      +12     
+ Misses       2912     2902      -10     
Flag Coverage Δ
qtpass 33.72% <50.00%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

CodeRabbit's auto-fix and my follow-up exceeded the 80-column limit on
three of the new QVERIFY2 calls. Reformatted to break after the
condition; matches the style applied elsewhere in the file.

No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@annejan annejan merged commit 318fd78 into main May 12, 2026
24 of 25 checks passed
@annejan annejan deleted the fix/gpg-id-permissions branch May 12, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants