pbnightingale 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pbnightingale-0.2.1/.claude/scheduled_tasks.lock +1 -0
- pbnightingale-0.2.1/.github/workflows/ci.yml +172 -0
- pbnightingale-0.2.1/.gitignore +53 -0
- pbnightingale-0.2.1/.markdownlint-cli2.yaml +12 -0
- pbnightingale-0.2.1/.pre-commit-config.yaml +30 -0
- pbnightingale-0.2.1/.readthedocs.yaml +20 -0
- pbnightingale-0.2.1/CHANGELOG.md +85 -0
- pbnightingale-0.2.1/CLAUDE.md +218 -0
- pbnightingale-0.2.1/CODING.md +2029 -0
- pbnightingale-0.2.1/LICENSE +674 -0
- pbnightingale-0.2.1/LICENSE-DOCS +437 -0
- pbnightingale-0.2.1/LICENSE-ICONS +437 -0
- pbnightingale-0.2.1/Makefile +292 -0
- pbnightingale-0.2.1/PKG-INFO +125 -0
- pbnightingale-0.2.1/README.md +86 -0
- pbnightingale-0.2.1/babel.cfg +2 -0
- pbnightingale-0.2.1/docs/_static/.gitkeep +0 -0
- pbnightingale-0.2.1/docs/_static/custom.css +26 -0
- pbnightingale-0.2.1/docs/api.rst +274 -0
- pbnightingale-0.2.1/docs/conf.py +258 -0
- pbnightingale-0.2.1/docs/index.rst +37 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/index.po +64 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/concepts.po +408 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/email_clients.po +219 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/first_key.po +125 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/importing_keys.po +200 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/index.po +43 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/managing_keys.po +432 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/preferences.po +63 -0
- pbnightingale-0.2.1/docs/locale/fr/LC_MESSAGES/manual/trust_and_signing.po +229 -0
- pbnightingale-0.2.1/docs/manual/concepts.rst +239 -0
- pbnightingale-0.2.1/docs/manual/email_clients.rst +87 -0
- pbnightingale-0.2.1/docs/manual/first_key.rst +67 -0
- pbnightingale-0.2.1/docs/manual/importing_keys.rst +77 -0
- pbnightingale-0.2.1/docs/manual/index.rst +20 -0
- pbnightingale-0.2.1/docs/manual/managing_keys.rst +169 -0
- pbnightingale-0.2.1/docs/manual/preferences.rst +22 -0
- pbnightingale-0.2.1/docs/manual/trust_and_signing.rst +88 -0
- pbnightingale-0.2.1/docs/requirements.txt +3 -0
- pbnightingale-0.2.1/hatch_build.py +35 -0
- pbnightingale-0.2.1/hooks/pyi_rth_fonts.py +49 -0
- pbnightingale-0.2.1/pbnightingale.spec +182 -0
- pbnightingale-0.2.1/pixi.lock +7818 -0
- pbnightingale-0.2.1/pyproject.toml +159 -0
- pbnightingale-0.2.1/src/pbnightingale/__init__.py +17 -0
- pbnightingale-0.2.1/src/pbnightingale/__main__.py +192 -0
- pbnightingale-0.2.1/src/pbnightingale/core/__init__.py +5 -0
- pbnightingale-0.2.1/src/pbnightingale/core/gpg_backend.py +2853 -0
- pbnightingale-0.2.1/src/pbnightingale/core/passphrase_cache.py +89 -0
- pbnightingale-0.2.1/src/pbnightingale/core/password_strength.py +111 -0
- pbnightingale-0.2.1/src/pbnightingale/core/secret.py +52 -0
- pbnightingale-0.2.1/src/pbnightingale/i18n.py +185 -0
- pbnightingale-0.2.1/src/pbnightingale/locale/en/LC_MESSAGES/pbnightingale.po +1459 -0
- pbnightingale-0.2.1/src/pbnightingale/locale/fr/LC_MESSAGES/pbnightingale.po +1494 -0
- pbnightingale-0.2.1/src/pbnightingale/platform/__init__.py +14 -0
- pbnightingale-0.2.1/src/pbnightingale/platform/auto_update.py +406 -0
- pbnightingale-0.2.1/src/pbnightingale/platform/dirs.py +203 -0
- pbnightingale-0.2.1/src/pbnightingale/platform/locale.py +34 -0
- pbnightingale-0.2.1/src/pbnightingale/preferences.py +110 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/__init__.py +21 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/edit-copy.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/help-about.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/help-contextual.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/help-user-manual.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-backup.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-delete.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-expire.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-export.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-import.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-new.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-revoke.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/key-sign.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/locked-black.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/password-change.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/password-mask.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/password-view.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/pbnightingale.icns +0 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/pbnightingale.ico +0 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/pbnightingale.png +0 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/photo-add.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/photo-delete.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/preferences-system.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/previous.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/quit.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/server-publish.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/server-refresh.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/server-search.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/subkey-add.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/subkey-expire.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/subkey-revoke.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/trust-refresh.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/trust-set.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/unlocked-black.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/user-add.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/user-default.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/user-delete.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/view-refresh.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/resources/view.svg +1 -0
- pbnightingale-0.2.1/src/pbnightingale/settings.py +39 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/__init__.py +1 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/about_dialog.py +23 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/about_dialog_ui.py +66 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_photo_dialog.py +153 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_photo_dialog_ui.py +58 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_subkey_dialog.py +82 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_subkey_dialog_ui.py +41 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_uid_dialog.py +96 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/add_uid_dialog_ui.py +29 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/backup_private_key_dialog.py +110 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/backup_private_key_dialog_ui.py +23 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/change_passphrase_dialog.py +116 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/change_passphrase_dialog_ui.py +38 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/delete_key_dialog.py +82 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/delete_key_dialog_ui.py +23 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/download_signatures_report_dialog.py +52 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/download_signatures_report_dialog_ui.py +40 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/geometry_mixin.py +109 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/gpg_worker.py +113 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/import_key_dialog.py +218 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/import_key_dialog_ui.py +95 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/key_list_view.py +977 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/key_list_view_ui.py +257 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/key_operation_dialog.py +194 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/key_operation_dialog_ui.py +85 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/main_window.py +824 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/main_window_ui.py +598 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/new_key_wizard.py +319 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/new_key_wizard_ui.py +124 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/password_line_edit.py +49 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/password_strength_meter.py +65 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/password_strength_meter_ui.py +21 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/photo_viewer_dialog.py +41 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/photo_viewer_dialog_ui.py +31 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/refresh_keys_dialog.py +60 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/refresh_keys_dialog_ui.py +26 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/refresh_keys_report_dialog.py +49 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/refresh_keys_report_dialog_ui.py +30 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_key_dialog.py +77 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_key_dialog_ui.py +24 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_photo_dialog.py +86 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_photo_dialog_ui.py +33 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_subkey_dialog.py +77 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_subkey_dialog_ui.py +24 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_uid_dialog.py +77 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/revoke_uid_dialog_ui.py +24 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/search_key_dialog.py +185 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/search_key_dialog_ui.py +67 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_expiration_dialog.py +132 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_expiration_dialog_ui.py +43 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_owner_trust_dialog.py +121 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_owner_trust_dialog_ui.py +55 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_primary_uid_dialog.py +72 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/set_primary_uid_dialog_ui.py +23 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/settings_dialog.py +58 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/settings_dialog_ui.py +62 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/sign_key_dialog.py +130 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/sign_key_dialog_ui.py +56 -0
- pbnightingale-0.2.1/src/pbnightingale/ui/window_state.py +146 -0
- pbnightingale-0.2.1/tests/__init__.py +0 -0
- pbnightingale-0.2.1/tests/conftest.py +135 -0
- pbnightingale-0.2.1/tests/gpg_test_helpers.py +86 -0
- pbnightingale-0.2.1/tests/test_about_dialog.py +43 -0
- pbnightingale-0.2.1/tests/test_add_photo_dialog.py +258 -0
- pbnightingale-0.2.1/tests/test_add_subkey_dialog.py +138 -0
- pbnightingale-0.2.1/tests/test_add_uid_dialog.py +145 -0
- pbnightingale-0.2.1/tests/test_backup_private_key_dialog.py +186 -0
- pbnightingale-0.2.1/tests/test_change_passphrase_dialog.py +156 -0
- pbnightingale-0.2.1/tests/test_delete_key_dialog.py +137 -0
- pbnightingale-0.2.1/tests/test_download_signatures_report_dialog.py +62 -0
- pbnightingale-0.2.1/tests/test_geometry_mixin.py +152 -0
- pbnightingale-0.2.1/tests/test_gpg_backend.py +2905 -0
- pbnightingale-0.2.1/tests/test_gpg_worker.py +48 -0
- pbnightingale-0.2.1/tests/test_i18n.py +158 -0
- pbnightingale-0.2.1/tests/test_import_key_dialog.py +323 -0
- pbnightingale-0.2.1/tests/test_key_list_view.py +1346 -0
- pbnightingale-0.2.1/tests/test_key_operation_dialog.py +239 -0
- pbnightingale-0.2.1/tests/test_main.py +164 -0
- pbnightingale-0.2.1/tests/test_main_window.py +1948 -0
- pbnightingale-0.2.1/tests/test_new_key_wizard.py +289 -0
- pbnightingale-0.2.1/tests/test_passphrase_cache.py +78 -0
- pbnightingale-0.2.1/tests/test_password_line_edit.py +36 -0
- pbnightingale-0.2.1/tests/test_password_strength.py +82 -0
- pbnightingale-0.2.1/tests/test_password_strength_meter.py +82 -0
- pbnightingale-0.2.1/tests/test_photo_viewer_dialog.py +52 -0
- pbnightingale-0.2.1/tests/test_platform_auto_update.py +452 -0
- pbnightingale-0.2.1/tests/test_platform_dirs.py +150 -0
- pbnightingale-0.2.1/tests/test_platform_locale.py +69 -0
- pbnightingale-0.2.1/tests/test_preferences.py +65 -0
- pbnightingale-0.2.1/tests/test_refresh_keys_dialog.py +50 -0
- pbnightingale-0.2.1/tests/test_refresh_keys_report_dialog.py +81 -0
- pbnightingale-0.2.1/tests/test_resources.py +22 -0
- pbnightingale-0.2.1/tests/test_revoke_key_dialog.py +128 -0
- pbnightingale-0.2.1/tests/test_revoke_photo_dialog.py +154 -0
- pbnightingale-0.2.1/tests/test_revoke_subkey_dialog.py +150 -0
- pbnightingale-0.2.1/tests/test_revoke_uid_dialog.py +137 -0
- pbnightingale-0.2.1/tests/test_search_key_dialog.py +211 -0
- pbnightingale-0.2.1/tests/test_secret.py +64 -0
- pbnightingale-0.2.1/tests/test_set_expiration_dialog.py +209 -0
- pbnightingale-0.2.1/tests/test_set_owner_trust_dialog.py +96 -0
- pbnightingale-0.2.1/tests/test_set_primary_uid_dialog.py +114 -0
- pbnightingale-0.2.1/tests/test_settings.py +22 -0
- pbnightingale-0.2.1/tests/test_settings_dialog.py +149 -0
- pbnightingale-0.2.1/tests/test_sign_key_dialog.py +213 -0
- pbnightingale-0.2.1/tests/test_window_state.py +71 -0
- pbnightingale-0.2.1/tools/bump_version.py +92 -0
- pbnightingale-0.2.1/tools/extract_changelog.py +65 -0
- pbnightingale-0.2.1/tools/fix_po_files.py +45 -0
- pbnightingale-0.2.1/tools/git_version.sh +10 -0
- pbnightingale-0.2.1/tools/po_check.py +197 -0
- pbnightingale-0.2.1/tools/update_icons.py +252 -0
- pbnightingale-0.2.1/xx.log +1512 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"8adf1241-358a-437f-87d1-2fcef27cfd94","pid":382052,"procStart":"3651907","acquiredAt":1789588198490}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["**"]
|
|
6
|
+
# Semver tags only (glob syntax — + quantifier not supported by GitHub)
|
|
7
|
+
tags: ["v[0-9]*.[0-9]*.[0-9]*"]
|
|
8
|
+
pull_request:
|
|
9
|
+
|
|
10
|
+
# Cancel in-flight runs for the same ref when a newer push arrives.
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
# ── Tests ────────────────────────────────────────────────────────────────────
|
|
17
|
+
test:
|
|
18
|
+
name: Tests
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- name: Set up pixi environment
|
|
24
|
+
uses: prefix-dev/setup-pixi@v0.10.0
|
|
25
|
+
with:
|
|
26
|
+
pixi-version: latest
|
|
27
|
+
cache: true
|
|
28
|
+
|
|
29
|
+
- name: Compile translation catalogues
|
|
30
|
+
run: pixi run -- pybabel compile -d src/pbnightingale/locale -D pbnightingale
|
|
31
|
+
|
|
32
|
+
- name: Run test suite
|
|
33
|
+
env:
|
|
34
|
+
QT_QPA_PLATFORM: offscreen
|
|
35
|
+
run: make test
|
|
36
|
+
|
|
37
|
+
- name: Upload coverage to Codecov
|
|
38
|
+
# v6+ is required to pick up codecov-action's own Node 24 runtime
|
|
39
|
+
# (it was pinned to actions/github-script's Node 20 build through
|
|
40
|
+
# v5.5.5); no input/behavior change otherwise.
|
|
41
|
+
uses: codecov/codecov-action@v7
|
|
42
|
+
with:
|
|
43
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
44
|
+
files: coverage.xml
|
|
45
|
+
fail_ci_if_error: true
|
|
46
|
+
|
|
47
|
+
# ── Pre-commit hooks ─────────────────────────────────────────────────────────
|
|
48
|
+
hooks:
|
|
49
|
+
name: Pre-commit hooks
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v6
|
|
53
|
+
|
|
54
|
+
- name: Set up pixi environment
|
|
55
|
+
uses: prefix-dev/setup-pixi@v0.10.0
|
|
56
|
+
with:
|
|
57
|
+
pixi-version: latest
|
|
58
|
+
cache: true
|
|
59
|
+
|
|
60
|
+
- name: Cache pre-commit hook environments
|
|
61
|
+
uses: actions/cache@v5
|
|
62
|
+
with:
|
|
63
|
+
path: ~/.cache/pre-commit
|
|
64
|
+
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
|
65
|
+
|
|
66
|
+
- name: Run hooks on all files
|
|
67
|
+
run: pixi run -- pre-commit run --all-files
|
|
68
|
+
|
|
69
|
+
# ── Documentation ───────────────────────────────────────────────────────────
|
|
70
|
+
docs:
|
|
71
|
+
name: Build docs
|
|
72
|
+
needs: [test]
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v6
|
|
76
|
+
|
|
77
|
+
- name: Set up pixi environment
|
|
78
|
+
uses: prefix-dev/setup-pixi@v0.10.0
|
|
79
|
+
with:
|
|
80
|
+
pixi-version: latest
|
|
81
|
+
cache: true
|
|
82
|
+
|
|
83
|
+
- name: Build HTML documentation
|
|
84
|
+
run: make docs
|
|
85
|
+
|
|
86
|
+
- name: Upload documentation artifact
|
|
87
|
+
uses: actions/upload-artifact@v7
|
|
88
|
+
with:
|
|
89
|
+
name: docs-html
|
|
90
|
+
path: docs/_build/html/
|
|
91
|
+
if-no-files-found: error
|
|
92
|
+
|
|
93
|
+
# ── Build executables ────────────────────────────────────────────────────────
|
|
94
|
+
build:
|
|
95
|
+
name: Build / ${{ matrix.os }}
|
|
96
|
+
needs: [test, hooks]
|
|
97
|
+
strategy:
|
|
98
|
+
fail-fast: false
|
|
99
|
+
matrix:
|
|
100
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
101
|
+
runs-on: ${{ matrix.os }}
|
|
102
|
+
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v6
|
|
105
|
+
|
|
106
|
+
- name: Set up pixi environment
|
|
107
|
+
uses: prefix-dev/setup-pixi@v0.10.0
|
|
108
|
+
with:
|
|
109
|
+
pixi-version: latest
|
|
110
|
+
cache: true
|
|
111
|
+
|
|
112
|
+
- name: Compile translation catalogues
|
|
113
|
+
# .mo files are gitignored generated artifacts; rebuild from .po sources.
|
|
114
|
+
run: pixi run -- pybabel compile -d src/pbnightingale/locale -D pbnightingale
|
|
115
|
+
|
|
116
|
+
- name: Build with PyInstaller
|
|
117
|
+
# Call PyInstaller directly to avoid GNU Make portability issues
|
|
118
|
+
# on Windows runners. Single-line command: the `\` line-continuation
|
|
119
|
+
# used by bash is not valid PowerShell, the default shell on
|
|
120
|
+
# windows-latest runners.
|
|
121
|
+
run: pixi run -- pyinstaller --clean --noconfirm --distpath dist --workpath build/pyinstaller pbnightingale.spec
|
|
122
|
+
|
|
123
|
+
- name: Zip .app bundle (macOS)
|
|
124
|
+
# A .app bundle is a directory; zip it for upload and release attachment.
|
|
125
|
+
if: runner.os == 'macOS'
|
|
126
|
+
shell: bash
|
|
127
|
+
run: |
|
|
128
|
+
cd dist
|
|
129
|
+
for app in *.app; do
|
|
130
|
+
zip -r "${app%.app}.zip" "$app"
|
|
131
|
+
rm -rf "$app"
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
- name: Upload build artifact
|
|
135
|
+
uses: actions/upload-artifact@v7
|
|
136
|
+
with:
|
|
137
|
+
# One artifact per runner; merged in the release job.
|
|
138
|
+
name: build-${{ runner.os }}
|
|
139
|
+
path: dist/pbnightingale-*
|
|
140
|
+
if-no-files-found: error
|
|
141
|
+
|
|
142
|
+
# ── GitHub release ───────────────────────────────────────────────────────────
|
|
143
|
+
release:
|
|
144
|
+
name: Release ${{ github.ref_name }}
|
|
145
|
+
needs: [build]
|
|
146
|
+
# Only create a release on semver tags; the tag filter on push already
|
|
147
|
+
# restricts the trigger, but this guard makes the intent explicit.
|
|
148
|
+
if: github.ref_type == 'tag'
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
permissions:
|
|
151
|
+
contents: write # required to create a release and upload assets
|
|
152
|
+
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/checkout@v6
|
|
155
|
+
|
|
156
|
+
- name: Download all build artifacts
|
|
157
|
+
uses: actions/download-artifact@v8
|
|
158
|
+
with:
|
|
159
|
+
# Merge all per-OS artifacts into a single flat directory.
|
|
160
|
+
path: dist
|
|
161
|
+
merge-multiple: true
|
|
162
|
+
|
|
163
|
+
- name: Extract changelog entry for ${{ github.ref_name }}
|
|
164
|
+
id: changelog
|
|
165
|
+
run: python3 tools/extract_changelog.py
|
|
166
|
+
|
|
167
|
+
- name: Create GitHub release
|
|
168
|
+
uses: softprops/action-gh-release@v3
|
|
169
|
+
with:
|
|
170
|
+
body: ${{ steps.changelog.outputs.body }}
|
|
171
|
+
files: dist/pbnightingale-*
|
|
172
|
+
fail_on_unmatched_files: true
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
|
|
15
|
+
# Testing
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
coverage.xml
|
|
21
|
+
|
|
22
|
+
# Sphinx
|
|
23
|
+
docs/_build/
|
|
24
|
+
docs/changelog.rst
|
|
25
|
+
docs/_static/pbnightingale.png
|
|
26
|
+
docs/_static/icons/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*~
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
37
|
+
|
|
38
|
+
# Make stamp files
|
|
39
|
+
.translate.stamp
|
|
40
|
+
|
|
41
|
+
# i18n — generated artifacts, rebuilt on demand by make translate
|
|
42
|
+
*.pot
|
|
43
|
+
*.mo
|
|
44
|
+
tools/_lang_name_stub.py
|
|
45
|
+
|
|
46
|
+
# Claude Code
|
|
47
|
+
.claude/settings.local.json
|
|
48
|
+
|
|
49
|
+
# pixi (defensive: environments should live in ~/PIXI/ via the global
|
|
50
|
+
# `pixi config set --global detached-environments`, but this covers
|
|
51
|
+
# machines/accounts where that global config isn't set, so .pixi/
|
|
52
|
+
# never ends up committed)
|
|
53
|
+
.pixi/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
config:
|
|
2
|
+
# Line length not enforced — CHANGELOG bullets and docs are naturally long
|
|
3
|
+
MD013: false
|
|
4
|
+
# Duplicate headings allowed when they live under different parent sections
|
|
5
|
+
# (standard Keep-a-Changelog pattern: ### Added, ### Fixed repeat per release)
|
|
6
|
+
MD024:
|
|
7
|
+
siblings_only: true
|
|
8
|
+
|
|
9
|
+
# CLAUDE.md is intentionally a compact tool-specific file
|
|
10
|
+
ignores:
|
|
11
|
+
- CLAUDE.md
|
|
12
|
+
- .pytest_cache/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: mixed-line-ending
|
|
8
|
+
args: [--fix=lf]
|
|
9
|
+
- id: check-merge-conflict
|
|
10
|
+
- id: check-added-large-files
|
|
11
|
+
# Default 500 KB is too low for pbnightingale.icns: a macOS .icns
|
|
12
|
+
# bundles several raster sizes and is a few MB even for a simple
|
|
13
|
+
# source image — legitimate packaging content, not accidental bloat.
|
|
14
|
+
args: [--maxkb=3072]
|
|
15
|
+
- id: check-yaml
|
|
16
|
+
- id: check-toml
|
|
17
|
+
- id: check-json
|
|
18
|
+
- id: debug-statements # Python only
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
21
|
+
rev: v0.16.7
|
|
22
|
+
hooks:
|
|
23
|
+
- id: ruff
|
|
24
|
+
args: [--fix]
|
|
25
|
+
- id: ruff-format
|
|
26
|
+
|
|
27
|
+
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
|
28
|
+
rev: v0.22.1
|
|
29
|
+
hooks:
|
|
30
|
+
- id: markdownlint-cli2
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Read the Docs configuration
|
|
2
|
+
# https://docs.readthedocs.io/en/stable/config-file/v2.html
|
|
3
|
+
version: 2
|
|
4
|
+
|
|
5
|
+
build:
|
|
6
|
+
os: ubuntu-22.04
|
|
7
|
+
tools:
|
|
8
|
+
python: "3.14"
|
|
9
|
+
|
|
10
|
+
python:
|
|
11
|
+
install:
|
|
12
|
+
- method: pip
|
|
13
|
+
path: .
|
|
14
|
+
extra_requirements:
|
|
15
|
+
- dev
|
|
16
|
+
|
|
17
|
+
sphinx:
|
|
18
|
+
configuration: docs/conf.py
|
|
19
|
+
builder: html
|
|
20
|
+
fail_on_warning: false
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.1] - 2026-09-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `-d`/`--debug` and `-q`/`--quiet` CLI flags: `--debug` enables debug-level
|
|
12
|
+
logging, including a trace of every operation launched through an
|
|
13
|
+
external program (`gpg`, `gpg-connect-agent`, …); `--quiet` restricts
|
|
14
|
+
logging to warnings and errors
|
|
15
|
+
- `--print-completion {bash,zsh,tcsh,fish,powershell}` (via `shtab`), in
|
|
16
|
+
the "tools" option group, to generate a shell completion script
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- A short program history note added to the README and the user manual
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- gpg's purely informational `[GNUPG:] KEYEXPIRED <timestamp>` status
|
|
25
|
+
lines no longer appear in diagnostic/error messages
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- Passphrases are now carried through the application as a dedicated
|
|
30
|
+
`Passphrase` wrapper (`core/secret.py`) instead of a bare string:
|
|
31
|
+
`str()`/`repr()` on it always show a fixed placeholder, so an enhanced
|
|
32
|
+
traceback tool or debugger dumping a stack frame's local variables can
|
|
33
|
+
no longer expose a passphrase in the clear
|
|
34
|
+
- Debug-mode tracing of external `gpg` invocations redacts every
|
|
35
|
+
passphrase from the logged command input; the real value sent to gpg is
|
|
36
|
+
unaffected
|
|
37
|
+
|
|
38
|
+
## [0.2.0] - 2026-09-17
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Sortable main key list: clicking a column header (Name, Email, Key ID,
|
|
43
|
+
Expires) sorts the keys inside each group, clicking it again reverses the
|
|
44
|
+
order. The "My keys" and "Other keys" groups themselves keep their fixed
|
|
45
|
+
order, and keys that never expire always sort last in the Expires column.
|
|
46
|
+
The list is sorted by name ascending on first launch, and the chosen sort
|
|
47
|
+
column and direction are remembered across sessions
|
|
48
|
+
- Numpydoc docstring convention adopted project-wide (`core/`, `platform/`,
|
|
49
|
+
top-level modules, and every `ui/*.py` except `*_ui.py` layout files),
|
|
50
|
+
documented in CODING.md
|
|
51
|
+
- `pydocstyle` (ruff `D`, `numpy` convention) enforced in lint, scoped to
|
|
52
|
+
match the documented docstring convention
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Sphinx docs: `napoleon_numpy_docstring` was disabled, so numpydoc
|
|
57
|
+
sections in docstrings were never actually parsed or rendered
|
|
58
|
+
- CI: bumped `codecov-action` to v7 to pick up its own Node 24 runtime
|
|
59
|
+
(it was pinned to a deprecated Node 20 build via `actions/github-script`)
|
|
60
|
+
|
|
61
|
+
## [0.1.0] - 2026-09-16
|
|
62
|
+
|
|
63
|
+
This program is an evolution (heavily improved) of an older Python
|
|
64
|
+
program, itself an evolution of an ancient (~2014) Perl program.
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- Personal key generation (RSA or ED25519) with a passphrase strength meter
|
|
69
|
+
- Subkey management: add, revoke, export, back up, delete
|
|
70
|
+
- Editable identities: text and photo user IDs
|
|
71
|
+
- Expiration date management for keys and subkeys
|
|
72
|
+
- Passphrase changes, with in-memory caching and show/hide fields
|
|
73
|
+
- Key signing, owner trust, and trust refresh, including a signatures view
|
|
74
|
+
- Key import from a file or a keyserver, with a review step before merging
|
|
75
|
+
- Keyserver search, publish, and refresh
|
|
76
|
+
- French and English interface, with a preferences dialog
|
|
77
|
+
- Packaging (PyInstaller) and Sphinx user documentation
|
|
78
|
+
- GitHub Actions CI: tests, pre-commit hooks, documentation build, per-OS
|
|
79
|
+
PyInstaller builds, and GitHub Releases cut from this changelog on
|
|
80
|
+
semver tags
|
|
81
|
+
- Help menu entry opening the online user manual in the interface's
|
|
82
|
+
current language
|
|
83
|
+
- Keyboard shortcuts for standard actions (new, import, export, back up,
|
|
84
|
+
delete, settings, help, quit) and a refresh family (F5 for keys,
|
|
85
|
+
Shift+F5 for trust, Ctrl+F5 for keyservers)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Mode
|
|
2
|
+
`full-en` — code and all communication in English.
|
|
3
|
+
|
|
4
|
+
# Project: PBNightingale
|
|
5
|
+
GUI GPG key manager (Linux/Windows/macOS). Python 3.14+, PySide6 (Qt6), src layout.
|
|
6
|
+
Publisher: PBMou | Author: Marcel Spock <mrspock@cardolan.net>
|
|
7
|
+
License: GPLv3 (code); icons under `resources/` (sourced from PBIcons) are
|
|
8
|
+
CC BY-NC-SA 4.0 instead — see LICENSE-ICONS
|
|
9
|
+
|
|
10
|
+
## Current state
|
|
11
|
+
|
|
12
|
+
Published to GitHub (`origin` → `git@github.com:ppoilbarbe/PBNightingale.git`,
|
|
13
|
+
`main` tracks `origin/main`). `.github/workflows/ci.yml` runs tests, hooks,
|
|
14
|
+
docs build and per-OS PyInstaller builds on every push/PR, and cuts a
|
|
15
|
+
GitHub release from `CHANGELOG.md` on semver tags. **Still never push without an explicit request from the user**
|
|
16
|
+
(see global CLAUDE.md). `make ci` (lint → hooks → test) remains the local
|
|
17
|
+
gate to run before pushing.
|
|
18
|
+
|
|
19
|
+
Implementation proceeds milestone by milestone; each milestone is validated
|
|
20
|
+
(`make ci` green + manual check) before the next one starts.
|
|
21
|
+
|
|
22
|
+
## Stack
|
|
23
|
+
|
|
24
|
+
- GUI: PySide6; UI layouts hand-written (`*_ui.py`), no Qt Designer
|
|
25
|
+
- GPG backend: `python-gnupg` wrapping the system `gpg` binary (portable;
|
|
26
|
+
avoids compiling GPGME bindings on Windows/macOS)
|
|
27
|
+
- Password strength: `zxcvbn` (conda-forge), thresholds/tiers ported from
|
|
28
|
+
KeePassXC's `PasswordHealth` — see CODING.md, "Password strength"
|
|
29
|
+
- Tests: pytest + pytest-qt (`qtbot`; never instantiate `QApplication`
|
|
30
|
+
manually) — GPG tests run against an isolated temporary `GNUPGHOME`, never
|
|
31
|
+
the user's real keyring
|
|
32
|
+
- Lint: ruff (line-length 88, target py314), default rule set + `I`/`PGH`/
|
|
33
|
+
`UP`/`BLE`/`S`/`D` (no explicit `select` — see pyproject.toml comment);
|
|
34
|
+
`D` uses the `numpy` pydocstyle convention, matching this project's
|
|
35
|
+
numpydoc docstrings (see CODING.md, "Coding conventions")
|
|
36
|
+
- Env: pixi (conda-forge channel only) — `make venv`
|
|
37
|
+
- CI/CD: GitHub Actions (`.github/workflows/ci.yml`) + `make ci` locally
|
|
38
|
+
before pushing
|
|
39
|
+
- i18n: French + English only, pybabel/gettext (see `i18n.py`)
|
|
40
|
+
- Auto-update: GitHub-releases self-update, `--auto-update` CLI flag —
|
|
41
|
+
only meaningful (and only registered) in a PyInstaller-frozen executable
|
|
42
|
+
(milestone 15); inert everywhere else
|
|
43
|
+
|
|
44
|
+
## Layout
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
src/pbnightingale/
|
|
48
|
+
├── __init__.py version, author metadata; builtins._ no-op fallback
|
|
49
|
+
├── __main__.py entry point; --auto-update dispatch (frozen only)
|
|
50
|
+
├── i18n.py gettext bootstrap (setup(app), language override)
|
|
51
|
+
├── settings.py config-dir root (configure() override for tests)
|
|
52
|
+
├── preferences.py general app preferences (preferred key algorithm, …)
|
|
53
|
+
├── locale/ en/fr catalogues (.po committed, .mo gitignored)
|
|
54
|
+
├── core/
|
|
55
|
+
│ ├── gpg_backend.py GPGBackend (python-gnupg wrapper) + Key/Subkey model;
|
|
56
|
+
│ │ configure()/default_backend() override for tests
|
|
57
|
+
│ ├── secret.py Passphrase — frozen wrapper hiding a passphrase from
|
|
58
|
+
│ │ str()/repr() (e.g. in a traceback's local-var dump)
|
|
59
|
+
│ ├── passphrase_cache.py in-memory {fingerprint: Passphrase} cache, TTL-based
|
|
60
|
+
│ └── password_strength.py zxcvbn-based entropy/quality for a new passphrase
|
|
61
|
+
├── platform/
|
|
62
|
+
│ ├── dirs.py AppDirs factory: XdgDirs / _MacDirs / _WindowsDirs
|
|
63
|
+
│ ├── locale.py system_language()
|
|
64
|
+
│ └── auto_update.py perform_auto_update() — GitHub release self-update
|
|
65
|
+
├── resources/ bundled SVG/PNG icons (resources.path(name))
|
|
66
|
+
└── ui/
|
|
67
|
+
├── main_window.py main window: loads keys, signals
|
|
68
|
+
├── main_window_ui.py main window layout: actions, toolbars (one per theme), menus
|
|
69
|
+
├── key_list_view(_ui).py central widget: key list + detail panel
|
|
70
|
+
├── new_key_wizard(_ui).py personal key creation wizard
|
|
71
|
+
├── password_strength_meter(_ui).py colored entropy bar for a new passphrase
|
|
72
|
+
├── add_subkey_dialog(_ui).py Add Subkey dialog
|
|
73
|
+
├── revoke_subkey_dialog(_ui).py Revoke Subkey dialog (strong confirmation)
|
|
74
|
+
├── change_passphrase_dialog(_ui).py Change Passphrase dialog
|
|
75
|
+
├── add_uid_dialog(_ui).py Add User ID dialog
|
|
76
|
+
├── set_primary_uid_dialog(_ui).py Set Primary User ID dialog
|
|
77
|
+
├── revoke_uid_dialog(_ui).py Revoke User ID dialog (strong confirmation)
|
|
78
|
+
├── add_photo_dialog(_ui).py Add Photo dialog
|
|
79
|
+
├── revoke_photo_dialog(_ui).py Revoke Photo dialog (strong confirmation)
|
|
80
|
+
├── set_expiration_dialog(_ui).py Set Expiration dialog (key or subkey)
|
|
81
|
+
├── sign_key_dialog(_ui).py Sign Key dialog
|
|
82
|
+
├── set_owner_trust_dialog(_ui).py Set Owner Trust dialog
|
|
83
|
+
├── about_dialog(_ui).py
|
|
84
|
+
├── settings_dialog(_ui).py language selector
|
|
85
|
+
├── gpg_worker.py run_async() — QThreadPool bridge for GPGBackend calls
|
|
86
|
+
├── window_state.py persisted window geometry / splitter sizes (QSettings)
|
|
87
|
+
└── geometry_mixin.py GeometryMixin — wires window_state into a window
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`tools/update_icons.py` syncs `resources/` icons from the shared PBIcons
|
|
91
|
+
asset repo (local checkout sibling, else GitHub) — see CODING.md.
|
|
92
|
+
|
|
93
|
+
## Key conventions
|
|
94
|
+
|
|
95
|
+
- All OS-specific code goes in `src/pbnightingale/platform/`.
|
|
96
|
+
- Never test against the real user's GPG keyring — always an isolated
|
|
97
|
+
`GNUPGHOME`. Enforced automatically:
|
|
98
|
+
`gpg_backend.configure()`/`default_backend()` mirror
|
|
99
|
+
`settings.configure()` exactly, and `tests/conftest.py`'s autouse
|
|
100
|
+
`_isolated_gnupghome`/`_isolated_config` fixtures redirect both to a tmp
|
|
101
|
+
dir for every test — never the real user's keyring or config directory.
|
|
102
|
+
- After any user-visible string change: `make translate`, fill every new
|
|
103
|
+
`msgstr ""` in both `en` and `fr` `.po` files, `make translate` again to
|
|
104
|
+
compile. Verify with `python tools/po_check.py --empty` — never grep/msgfmt
|
|
105
|
+
directly on `.po` files (they can have multi-line entries). Run
|
|
106
|
+
`po_check.py --empty` even when a milestone feels string-free — milestones
|
|
107
|
+
6 and 7 both merged without it and left 65 strings untranslated in both
|
|
108
|
+
languages until milestone 8 caught it.
|
|
109
|
+
- Every top-level window/dialog persists its geometry (and splitter sizes)
|
|
110
|
+
via `ui/geometry_mixin.py::GeometryMixin` (`self._init_geometry(key,
|
|
111
|
+
splitters={...})` after `setupUi()`) — apply it to new windows too, except
|
|
112
|
+
small fixed-purpose dialogs like `AboutDialog`.
|
|
113
|
+
- ED25519 key generation cannot fold `encrypt` usage into the primary key
|
|
114
|
+
(gpg rejects it outright — EdDSA is signature-only); `NewKeyRequest`/
|
|
115
|
+
`add_subkey()` always add a dedicated Curve25519 subkey for encryption
|
|
116
|
+
under ED25519, and the wizard/Add Subkey dialog force-check-and-disable
|
|
117
|
+
that checkbox accordingly. Don't "simplify" this away — RSA's "fold into
|
|
118
|
+
primary" behavior genuinely doesn't generalize to ED25519.
|
|
119
|
+
- Subkey revocation has no python-gnupg wrapper — `GPGBackend.
|
|
120
|
+
revoke_subkey()` drives `gpg --edit-key` directly via `--command-fd`/
|
|
121
|
+
`--status-fd` scripting. The passphrase line in that script must be
|
|
122
|
+
positioned exactly where gpg's `GET_HIDDEN passphrase.enter` status
|
|
123
|
+
appears (right after confirming the revocation reason) — anywhere else in
|
|
124
|
+
the sequence gets silently consumed as a bogus `keyedit.prompt` command
|
|
125
|
+
instead. See CODING.md — "Subkey management".
|
|
126
|
+
- `self._gpg.gnupghome` is `None` in production (no `configure()`
|
|
127
|
+
override) — never assume it's a usable path. Anything that shells out
|
|
128
|
+
directly (`_restart_agent()`, `revoke_subkey()`) must go through
|
|
129
|
+
`GPGBackend._homedir_args()` rather than building a `--homedir` arg by
|
|
130
|
+
hand; a bare `self._gpg.gnupghome` in a `subprocess.run()` arg list
|
|
131
|
+
crashes with `TypeError: ... not NoneType` in production, since every
|
|
132
|
+
test's `GPGBackend` has a real temp-dir `gnupghome` and never exercises
|
|
133
|
+
the `None` case — this class of bug won't show up in `make ci`.
|
|
134
|
+
- `GPGBackend.change_passphrase()` drives gpg's dedicated
|
|
135
|
+
`--change-passphrase` command (never `--edit-key`'s `passwd`
|
|
136
|
+
sub-command): unlike `passwd`, a wrong old passphrase there gets a
|
|
137
|
+
proper machine-readable `[GNUPG:] ERROR keyedit.passwd <code>` status
|
|
138
|
+
line — but only when a wrong passphrase gets exactly the right number of
|
|
139
|
+
retries: gpg retries the old-passphrase prompt once per secret-key part
|
|
140
|
+
(primary + each subkey), and a wrong one can't be told apart from a
|
|
141
|
+
correct one until each retry has played out, so the script sent is
|
|
142
|
+
`old`, `new`, then `old` again `_MAX_SECRET_KEY_PARTS` (64) more times as
|
|
143
|
+
filler. A real 4-subkey-part key with too few retries queued ran the
|
|
144
|
+
script dry mid-sequence, downgrading the failure to gpg's generic
|
|
145
|
+
"operation canceled" code (99, not 11) and dumping its whole raw
|
|
146
|
+
multi-attempt diagnostic text into the dialog instead of a clean
|
|
147
|
+
message — reported against real usage; see CODING.md. A *correct*
|
|
148
|
+
passphrase only ever needs the first two lines (`old`, `new`) — no
|
|
149
|
+
repeat-to-confirm step in the protocol itself, that's the UI's own job
|
|
150
|
+
(`ChangePassphraseDialog`'s `txtNewPassphrase`/`txtNewPassphraseConfirm`).
|
|
151
|
+
Always clears gpg-agent's passphrase cache first via `gpg-connect-agent
|
|
152
|
+
RELOADAGENT` (never `_restart_agent()`'s `gpgconf --kill`, which is async
|
|
153
|
+
and races the next call) — otherwise an already-cached key silently
|
|
154
|
+
skips the old-passphrase check and misapplies its value as the new
|
|
155
|
+
passphrase instead. Only works on a key that already has a real
|
|
156
|
+
passphrase — an
|
|
157
|
+
unprotected key given `old_passphrase=""` reports success but stays
|
|
158
|
+
unprotected (`--edit-key passwd` handles that transition correctly, but
|
|
159
|
+
loses the machine-readable error code). See CODING.md — "Changing a
|
|
160
|
+
key's passphrase".
|
|
161
|
+
- Detect a wrong GPG passphrase by the numeric code on gpg's `[GNUPG:]
|
|
162
|
+
ERROR <location> <code>` status line (`& 0xFFFF == 11`, libgpg-error's
|
|
163
|
+
`GPG_ERR_BAD_PASSPHRASE`) — never by matching gpg's own human-readable
|
|
164
|
+
text ("Mauvaise phrase secrète"/"Bad Passphrase"/…), which is localized
|
|
165
|
+
to the system's own language and has nothing to do with this app's own
|
|
166
|
+
`en`/`fr` catalogues. See `GPGBackendError`/`BadPassphraseError` in
|
|
167
|
+
`core/gpg_backend.py`.
|
|
168
|
+
- There is no reliable "primary UID" signal in gpg's plain `--list-keys`
|
|
169
|
+
output — verified empirically, don't re-derive it: UID listing order
|
|
170
|
+
looks tempting but isn't a stable stand-in (a clean test with UIDs added
|
|
171
|
+
in order A, B, C came back as `C, A, B`, not creation or timestamp
|
|
172
|
+
order). The real primary flag only shows up in `gpg --edit-key`'s own
|
|
173
|
+
`--with-colons` listing, at the cost of one extra `gpg` subprocess per
|
|
174
|
+
key — not worth paying for a cosmetic label, so `Uid` has no `primary`
|
|
175
|
+
field. See CODING.md — "Editable user IDs".
|
|
176
|
+
- python-gnupg's `list_keys()` silently drops photo user IDs ("uat" colon
|
|
177
|
+
records) — its own record-type allowlist doesn't include them (verified
|
|
178
|
+
against its source, not just observed). `GPGBackend._load_photos()`
|
|
179
|
+
works around this with a separate raw `--attribute-file` listing.
|
|
180
|
+
`addphoto` (scripted via `--edit-key`, like subkey revocation) needs
|
|
181
|
+
`--no-tty` or it fails outright in any headless/GUI-launched process,
|
|
182
|
+
and its passphrase line goes right after the file path, *before*
|
|
183
|
+
`save` — unlike every other scripted `--edit-key` flow in this app,
|
|
184
|
+
where the passphrase goes last. See CODING.md — "Photo user IDs".
|
|
185
|
+
- `Key.trust` (validity — is the UID/key binding genuine) and
|
|
186
|
+
`Key.owner_trust` (how much you personally trust this owner to certify
|
|
187
|
+
*others*) are different gpg concepts that happen to share colon-format
|
|
188
|
+
letter codes — never conflate them. gpg auto-assigns "ultimate"
|
|
189
|
+
ownertrust to any key you hold the secret part of unless explicitly
|
|
190
|
+
overridden, which masks signing's effect on validity in tests unless you
|
|
191
|
+
override it first. `--min-cert-level` (default 2) disregards level-1
|
|
192
|
+
signatures for validity, but level 0 is always accepted — a signing
|
|
193
|
+
dialog defaulting to cert level 0 is a real default, not a placebo. See
|
|
194
|
+
CODING.md — "Key signing & web of trust".
|
|
195
|
+
- Every passphrase in this codebase is a `core/secret.py::Passphrase`
|
|
196
|
+
(frozen dataclass), never a bare `str` — `GPGBackend` method parameters,
|
|
197
|
+
`NewKeyRequest.passphrase`, `passphrase_cache`'s stored values, what a
|
|
198
|
+
dialog reads off `txtPassphrase`. `str()`/`repr()` on it always print
|
|
199
|
+
`"**********"`, closing off a leak a bare `str` local doesn't: an
|
|
200
|
+
enhanced traceback tool or debugger dumping a frame's locals on an
|
|
201
|
+
unhandled exception would otherwise print the real value at every stack
|
|
202
|
+
frame it passed through. `bool(passphrase)`/`==` work on the wrapper
|
|
203
|
+
directly — only unwrap via `.passphrase` at the one point gpg's own
|
|
204
|
+
protocol needs the raw text (a `--command-fd` script, an `input=`
|
|
205
|
+
payload, a python-gnupg kwarg, or the widget echoing it back). Don't
|
|
206
|
+
interpolate a `Passphrase` into an f-string unwrapped — with no
|
|
207
|
+
`__format__` override it falls back to `str()` and sends the literal
|
|
208
|
+
`"**********"` to gpg instead of the real passphrase, a real protocol
|
|
209
|
+
bug, not just a leak. See CODING.md — "GPG backend".
|
|
210
|
+
- New external-program calls in `core/gpg_backend.py` must go through
|
|
211
|
+
`_traced_run()`/`_traced_popen()`, never a bare `subprocess.run()`/
|
|
212
|
+
`Popen()` — that's what makes `-d`/`--debug` trace every gpg invocation,
|
|
213
|
+
and it's also where `_clean_stderr()` strips gpg's purely-informational
|
|
214
|
+
`[GNUPG:] KEYEXPIRED …` status lines from `result.stderr` before
|
|
215
|
+
anything (an exception message, a retry check) ever sees them. A
|
|
216
|
+
passphrase fed via `input=`/a script must be listed in `secrets=` so the
|
|
217
|
+
debug trace redacts it — the real value sent to gpg is never affected,
|
|
218
|
+
only what gets logged.
|