From fcbd5999f28f287507844b55f2bee82e714620c8 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 3 Nov 2026 13:57:05 +0100 Subject: fix(app): harden the setup key imports - PGP key import reads at most 1 MiB (bounded read, buffer zeroed) and refuses larger files with a new message. - SshKeyViewModel catches any Exception from import and generation (CancellationException rethrown) instead of a short list, so an unexpected provider or sshj error no longer crashes the app. - Pending PGP key bytes are zeroed in a finally block, also on cancellation during addKey after a replace, and an earlier pending pick is zeroed before a new one takes its place. - PGPKeyManager never replaces a stored secret key with a public-only key of the same ID, with or without replace (KeyDowngradeException); the setup ViewModel shows a localized message for it. Unit test added. - storeRecipients strips '#' comments and a trailing '!' as pass reads .gpg-id; tests added. - GitServerViewModel.clearStore returns the failure when the store folder cannot be emptied, so the clone does not start; the plan's Task 3 call site reports it. - readGpgId opens the file with NOFOLLOW_LINKS and reads at most 64 KiB + 1 bytes, refusing more. - SshKey.import writes the new private key to a temp file and moves it over atomically before dropping the old key's Keystore entry, public key and passphrase, so an I/O error keeps the old key. - KeyUtils.isSecretKey notes (ponytail) that GnuPG stub and smartcard keyrings count as secret. Co-Authored-By: Claude Opus 5.5 --- docs/superpowers/plans/2026-11-03-compose-setup.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/superpowers/plans/2026-11-03-compose-setup.md b/docs/superpowers/plans/2026-11-03-compose-setup.md index e4bcd5da..d62aeff6 100644 --- a/docs/superpowers/plans/2026-11-03-compose-setup.md +++ b/docs/superpowers/plans/2026-11-03-compose-setup.md @@ -1555,7 +1555,11 @@ fun GitServerDestination( viewModel.progress = R.string.git_clone_progress activity.lifecycleScope.launch { try { - viewModel.clearStore(root) + // A folder that could not be emptied is reported as such, not as a vague clone error. + viewModel.clearStore(root)?.let { failure -> + say(activity.getString(failure)) + return@launch + } activity .launchGitOperation(GitOp.CLONE) .fold( @@ -3757,7 +3761,7 @@ deleting `$OUT` if a visual check is wanted (copy them elsewhere first). `sshKeySummary`, `SshKeyType`, `SshKeyViewModel.import/generate/fail/dismissFailure/announced`, `type`, `protect`, `busy`, `version`, `imported`, `generated`, `failure` (Task 2) match Task 4; `GitServerViewModel.url/changeUrl/authMode/progress/cloned/hasHostKey/sshKey/refresh/ - clearHostKey/save/clearStore` (suspend) `/finishClone` (not suspend, launches in + clearHostKey/save/clearStore` (suspend, returns null or a failure string resource) `/finishClone` (not suspend, launches in `viewModelScope`) `/cloneEnded` (Task 2) match Task 3; `KeyRow(userId, keyId)` (no `gpgId` member any more), `KeyImport`, `PgpKeysViewModel.keys/recipients/loadRecipients/busy/ importResult/done/import/replace/dismissImport/deleteKey/createStore/markDone` (Task 2) match -- cgit v1.2.3