From 0ee31b232ac7b4beda0940bcbd52fc078209dfc4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:23:34 +0200 Subject: [PATCH 1/3] Allow UserIDs with trailing/leading whitespace and escape newlines in ASCII armor --- .../org/pgpainless/key/generation/KeyRingBuilder.kt | 2 +- .../modification/secretkeyring/SecretKeyRingEditor.kt | 7 ++++--- .../src/main/kotlin/org/pgpainless/util/ArmorUtils.kt | 3 ++- .../test/java/org/pgpainless/sop/GenerateKeyTest.java | 10 ++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 05adf7d9..aacfcceb 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -45,7 +45,7 @@ class KeyRingBuilder : KeyRingBuilderInterface { } override fun addUserId(userId: CharSequence): KeyRingBuilder = apply { - userIds[userId.toString().trim()] = null + userIds[userId.toString()] = null } override fun addUserId(userId: ByteArray): KeyRingBuilder = diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index ec93c6d6..56fb7695 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -569,9 +569,10 @@ class SecretKeyRingEditor( } private fun sanitizeUserId(userId: CharSequence): CharSequence = - // TODO: Further research how to sanitize user IDs. - // e.g. what about newlines? - userId.toString().trim() + // I'm not sure, what kind of sanitization is needed. + // Newlines are allowed, they just need to be escaped when emitted in an ASCII armor header + // Trailing/Leading whitespace is also fine. + userId.toString() private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = object : RevocationSignatureSubpackets.Callback { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index b5d5b839..b6e802b2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -247,7 +247,8 @@ class ArmorUtils { .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) // Primary / First User ID (primary ?: first)?.let { - headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(it.replace("\n", "\\n").replace("\r", "\\r")) } // X-1 further identities when (userIds.size) { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index ca6df790..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -100,4 +100,14 @@ public class GenerateKeyTest { assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop.generateKey().profile("invalid")); } + + @Test + public void generateKeyWithNewlinesInUserId() throws IOException { + byte[] keyBytes = sop.generateKey() + .userId("Foo\n\nBar") + .generate() + .getBytes(); + + assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + } } From 9b0a3cd4c7ffe8866ab9622fde866b79cd34a62b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:24:10 +0200 Subject: [PATCH 2/3] Do not trim passphrases automatically --- .../main/kotlin/org/pgpainless/util/Passphrase.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt index 4d1e49d2..bd25f2b9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -11,14 +11,9 @@ import org.bouncycastle.util.Arrays * * @param chars may be null for empty passwords. */ -class Passphrase(chars: CharArray?) { +class Passphrase(private val chars: CharArray?) { private val lock = Any() private var valid = true - private val chars: CharArray? - - init { - this.chars = trimWhitespace(chars) - } /** * Return a copy of the underlying char array. A return value of null represents an empty @@ -67,6 +62,13 @@ class Passphrase(chars: CharArray?) { override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() + /** + * Return a copy of this [Passphrase], but with whitespace characters trimmed off. + * + * @return copy with trimmed whitespace + */ + fun withTrimmedWhitespace(): Passphrase = Passphrase(trimWhitespace(chars)) + companion object { /** From 0d807cb6b8317f2e878509b5cb5f4a9a91e5287d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:25:31 +0200 Subject: [PATCH 3/3] Fix typo in error message --- .../key/modification/secretkeyring/SecretKeyRingEditor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 56fb7695..5480442d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -478,7 +478,7 @@ class SecretKeyRingEditor( val prevBinding = inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) ?: throw NoSuchElementException( - "Previous subkey binding signaure for ${keyId.openPgpKeyId()} MUST NOT be null.") + "Previous subkey binding signature for ${keyId.openPgpKeyId()} MUST NOT be null.") val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) }