diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-03 13:44:42 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-03 13:44:42 +0100 |
| commit | 6a614fe7dc1cbbec5783828d58b246a362edea07 (patch) | |
| tree | a949264fdde608b789a215f63318af7bae29da96 /app/src/test/java | |
| parent | 9d4b14306444f42ba8a5f11711f0f7381fd7db0f (diff) | |
| download | HardenedPass-6a614fe7dc1cbbec5783828d58b246a362edea07.tar.gz HardenedPass-6a614fe7dc1cbbec5783828d58b246a362edea07.zip | |
feat(app): add the setup model and stricter key checks
URL checks and fixes, authentication modes per URL, the clone target,
the store's recipients, a size- and symlink-checked .gpg-id read and
the .gpg-id text for a new store (uppercase long key IDs), as pure
functions for the Compose setup screens. The SSH key import now also
requires a format sshj recognizes (an OpenPGP private key block
matched the old marker check and was imported as the SSH key) and no
longer crashes on an empty size cursor. KeyUtils tells secret keys
apart and no longer throws on a key without user ID.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/test/java')
| -rw-r--r-- | app/src/test/java/app/passwordstore/ui/setup/SetupModelTest.kt | 121 | ||||
| -rw-r--r-- | app/src/test/java/app/passwordstore/util/git/sshj/SshKeyFileTest.kt | 33 |
2 files changed, 154 insertions, 0 deletions
diff --git a/app/src/test/java/app/passwordstore/ui/setup/SetupModelTest.kt b/app/src/test/java/app/passwordstore/ui/setup/SetupModelTest.kt new file mode 100644 index 00000000..8d8243eb --- /dev/null +++ b/app/src/test/java/app/passwordstore/ui/setup/SetupModelTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.ui.setup + +import app.passwordstore.crypto.PGPIdentifier +import app.passwordstore.util.settings.AuthMode +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull + +class SetupModelTest { + + private val root = Files.createTempDirectory("aps").toFile() + + @AfterTest + fun cleanUp() { + root.deleteRecursively() + } + + @Test + fun urlProblems() { + assertEquals(UrlProblem.HttpsPort, urlProblem("https://example.org:8443/store.git")) + assertEquals(UrlProblem.SshScheme, urlProblem("git@example.org:2222/srv/store.git")) + assertEquals(UrlProblem.GitScheme, urlProblem("git://example.org/store.git")) + assertEquals(UrlProblem.GitScheme, urlProblem("git://example.org:9418/store.git")) + assertNull(urlProblem("ssh://git@example.org:2222/srv/store.git")) + assertNull(urlProblem("git@example.org:srv/store.git")) + assertNull(urlProblem("https://example.org/store.git")) + assertNull(urlProblem("http://example.org:8080/store.git")) + } + + @Test + fun urlFixes() { + assertEquals( + "https://example.org/store.git", + fixUrl("https://example.org:8443/store.git", UrlProblem.HttpsPort), + ) + assertEquals( + "ssh://git@example.org:2222/srv/store.git", + fixUrl("git@example.org:2222/srv/store.git", UrlProblem.SshScheme), + ) + assertNull(fixUrl("git://example.org/store.git", UrlProblem.GitScheme)) + } + + @Test + fun authModesFollowTheUrl() { + val ssh = listOf(AuthMode.SshKey, AuthMode.Password) + val http = listOf(AuthMode.None, AuthMode.Password) + assertEquals(ssh, authModes("ssh://git@example.org/store.git")) + assertEquals(ssh, authModes("git@example.org:store.git")) + assertEquals(ssh, authModes("")) + assertEquals(http, authModes("https://example.org/store.git")) + assertEquals(http, authModes("http://example.org/store.git")) + assertEquals(AuthMode.None, fitAuthMode("https://example.org/x", AuthMode.SshKey)) + assertEquals(AuthMode.Password, fitAuthMode("https://example.org/x", AuthMode.Password)) + assertEquals(AuthMode.SshKey, fitAuthMode("git@example.org:x", AuthMode.None)) + assertEquals(AuthMode.Password, fitAuthMode("git@example.org:x", AuthMode.Password)) + } + + @Test + fun cloneTargets() { + val store = File(root, "store") + assertEquals(CloneTarget.Empty, cloneTarget(store)) + store.mkdirs() + assertEquals(CloneTarget.Empty, cloneTarget(store)) + File(store, ".git").mkdirs() + assertEquals(CloneTarget.OnlyGit, cloneTarget(store)) + File(store, "a.gpg").writeText("x") + assertEquals(CloneTarget.Occupied, cloneTarget(store)) + File(store, ".git").deleteRecursively() + assertEquals(CloneTarget.Occupied, cloneTarget(store)) + } + + @Test + fun storeRecipientsSkipBlankAndInvalidLines() { + val ids = + storeRecipients( + "4882E8257A3700D43119B8D3F8993A789CDFB014\n\n Alice <alice@example.org> \nnot an id\n" + ) + assertEquals(listOf("f8993a789cdfb014", "alice@example.org"), ids.map(PGPIdentifier::toString)) + assertEquals(emptyList(), storeRecipients("")) + } + + @Test + fun gpgIdTextIsUppercaseKeyIdsOnePerLine() { + val ids = + listOf("f8993a789cdfb014", "0x79E8208280490C77").map { + assertIs<PGPIdentifier.KeyId>(PGPIdentifier.fromString(it)) + } + val text = gpgIdText(ids) + assertEquals("F8993A789CDFB014\n79E8208280490C77\n", text) + // pass and this app read it back as the same key IDs. + assertEquals(ids, storeRecipients(text)) + } + + @Test + fun gpgIdIsReadOnlyFromASmallRegularFile() { + assertNull(readGpgId(root)) + File(root, ".gpg-id").writeText("F8993A789CDFB014\n") + assertEquals("F8993A789CDFB014\n", readGpgId(root)) + + val linked = File(root, "linked").apply { mkdirs() } + val outside = File(root, "outside").apply { writeText("F8993A789CDFB014\n") } + Files.createSymbolicLink(File(linked, ".gpg-id").toPath(), outside.toPath()) + assertNull(readGpgId(linked)) + + val big = File(root, "big").apply { mkdirs() } + File(big, ".gpg-id").writeText("A".repeat(64 * 1024 + 1)) + assertNull(readGpgId(big)) + + val folder = File(root, "folder").apply { File(this, ".gpg-id").mkdirs() } + assertNull(readGpgId(folder)) + } +} diff --git a/app/src/test/java/app/passwordstore/util/git/sshj/SshKeyFileTest.kt b/app/src/test/java/app/passwordstore/util/git/sshj/SshKeyFileTest.kt new file mode 100644 index 00000000..1a9914fb --- /dev/null +++ b/app/src/test/java/app/passwordstore/util/git/sshj/SshKeyFileTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.git.sshj + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SshKeyFileTest { + + private fun pem(type: String) = + listOf("-----BEGIN $type-----", "b3BlbnNzaC1rZXktdjEAAAAABG5vbmU", "-----END $type-----") + + @Test + fun acceptsSshPrivateKeys() { + assertTrue(isSshPrivateKey(pem("OPENSSH PRIVATE KEY"))) + assertTrue(isSshPrivateKey(pem("RSA PRIVATE KEY"))) + assertTrue(isSshPrivateKey(pem("EC PRIVATE KEY"))) + } + + @Test + fun refusesOtherFiles() { + // The step 4 finding: an OpenPGP key block has the same markers and was accepted. + assertFalse(isSshPrivateKey(pem("PGP PRIVATE KEY BLOCK"))) + assertFalse(isSshPrivateKey(pem("PGP PUBLIC KEY BLOCK"))) + assertFalse(isSshPrivateKey(listOf("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA test@example.org"))) + assertFalse(isSshPrivateKey(emptyList())) + assertFalse(isSshPrivateKey(pem("OPENSSH PRIVATE KEY").dropLast(1))) + } +} |
