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 92da93c5..8f2d6002 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() } + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } .add(it.replace("\n", "\\n").replace("\r", "\\r")) } // X-1 further identities diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index 2cdd4131..8e607d07 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -40,7 +40,7 @@ public class GenerateKeyWithAdditionalUserIdTest { .addUserId(UserId.onlyEmail("primary@user.id")) .addUserId(UserId.onlyEmail("additional@user.id")) .addUserId(UserId.onlyEmail("additional2@user.id")) - .addUserId("\ttrimThis@user.id ") + .addUserId("\twithWhitespace@user.id ") .setExpirationDate(expiration) .build() .getPGPSecretKeyRing(); @@ -52,7 +52,7 @@ public class GenerateKeyWithAdditionalUserIdTest { assertEquals("", userIds.next()); assertEquals("", userIds.next()); assertEquals("", userIds.next()); - assertEquals("trimThis@user.id", userIds.next()); + assertEquals("\twithWhitespace@user.id ", userIds.next()); assertFalse(userIds.hasNext()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java index 83477b28..fb428349 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseTest.java @@ -34,19 +34,20 @@ public class PassphraseTest { @Test public void testTrimming() { - Passphrase leadingSpace = Passphrase.fromPassword(" space"); + Passphrase leadingSpace = Passphrase.fromPassword(" space").withTrimmedWhitespace(); assertArrayEquals("space".toCharArray(), leadingSpace.getChars()); assertFalse(leadingSpace.isEmpty()); - Passphrase trailingSpace = Passphrase.fromPassword("space "); + Passphrase trailingSpace = Passphrase.fromPassword("space ").withTrimmedWhitespace(); assertArrayEquals("space".toCharArray(), trailingSpace.getChars()); assertFalse(trailingSpace.isEmpty()); - Passphrase leadingTrailingWhitespace = new Passphrase("\t Such whitespace, much wow\n ".toCharArray()); + Passphrase leadingTrailingWhitespace = new Passphrase("\t Such whitespace, much wow\n ".toCharArray()) + .withTrimmedWhitespace(); assertArrayEquals("Such whitespace, much wow".toCharArray(), leadingTrailingWhitespace.getChars()); assertFalse(leadingTrailingWhitespace.isEmpty()); - Passphrase fromEmptyChars = new Passphrase(" ".toCharArray()); + Passphrase fromEmptyChars = new Passphrase(" ".toCharArray()).withTrimmedWhitespace(); assertNull(fromEmptyChars.getChars()); assertTrue(fromEmptyChars.isEmpty()); } @@ -54,7 +55,7 @@ public class PassphraseTest { @ParameterizedTest @ValueSource(strings = {"", " ", " ", "\t", "\t\t"}) public void testEmptyPassphrases(String empty) { - Passphrase passphrase = Passphrase.fromPassword(empty); + Passphrase passphrase = Passphrase.fromPassword(empty).withTrimmedWhitespace(); assertTrue(passphrase.isEmpty()); assertEquals(Passphrase.emptyPassphrase(), passphrase); diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt index cbd97934..2eedc13e 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt @@ -10,7 +10,6 @@ import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import org.pgpainless.exception.KeyException import org.pgpainless.util.OpenPGPCertificateUtil -import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException import sop.operation.CertifyUserId @@ -79,6 +78,6 @@ class CertifyUserIdImpl(private val api: PGPainless) : CertifyUserId { override fun userId(userId: String): CertifyUserId = apply { this.userIds.add(userId) } override fun withKeyPassword(password: ByteArray): CertifyUserId = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt index a7d0c530..0cdfdbf7 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -73,6 +73,6 @@ class ChangeKeyPasswordImpl(private val api: PGPainless) : ChangeKeyPassword { override fun noArmor(): ChangeKeyPassword = apply { armor = false } override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { - oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)) + PasswordHelper.addPassphrasePlusRemoveWhitespace(oldPassphrase, oldProtector) } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt index ddd11d50..e7993492 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -16,13 +16,11 @@ import org.pgpainless.decryption_verification.ConsumerOptions import org.pgpainless.exception.MalformedOpenPgpMessageException import org.pgpainless.exception.MissingDecryptionMethodException import org.pgpainless.exception.WrongPassphraseException -import org.pgpainless.util.Passphrase import sop.DecryptionResult import sop.ReadyWithResult import sop.SessionKey import sop.exception.SOPGPException import sop.operation.Decrypt -import sop.util.UTF8Util /** Implementation of the `decrypt` operation using PGPainless. */ class DecryptImpl(private val api: PGPainless) : Decrypt { @@ -98,16 +96,11 @@ class DecryptImpl(private val api: PGPainless) : Decrypt { } override fun withKeyPassword(password: ByteArray): Decrypt = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } override fun withPassword(password: String): Decrypt = apply { - consumerOptions.addMessagePassphrase(Passphrase.fromPassword(password)) - password.trimEnd().let { - if (it != password) { - consumerOptions.addMessagePassphrase(Passphrase.fromPassword(it)) - } - } + PasswordHelper.addMessagePassphrasePlusRemoveWhitespace(password, consumerOptions) } override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt index e23ca1c3..ca661ff2 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -19,14 +19,12 @@ import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException import org.pgpainless.util.ArmoredOutputStreamFactory -import org.pgpainless.util.Passphrase import sop.MicAlg import sop.ReadyWithResult import sop.SigningResult import sop.enums.SignAs import sop.exception.SOPGPException import sop.operation.DetachedSign -import sop.util.UTF8Util /** Implementation of the `sign` operation using PGPainless. */ class DetachedSignImpl(private val api: PGPainless) : DetachedSign { @@ -108,7 +106,7 @@ class DetachedSignImpl(private val api: PGPainless) : DetachedSign { override fun noArmor(): DetachedSign = apply { armor = false } override fun withKeyPassword(password: ByteArray): DetachedSign = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } private fun modeToSigType(mode: SignAs): DocumentSignatureType { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt index c8a71c24..6c083acd 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -30,7 +30,6 @@ import sop.SessionKey import sop.enums.EncryptAs import sop.exception.SOPGPException import sop.operation.Encrypt -import sop.util.UTF8Util /** Implementation of the `encrypt` operation using PGPainless. */ class EncryptImpl(private val api: PGPainless) : Encrypt { @@ -148,11 +147,12 @@ class EncryptImpl(private val api: PGPainless) : Encrypt { } override fun withKeyPassword(password: ByteArray): Encrypt = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } override fun withPassword(password: String): Encrypt = apply { - encryptionOptions.addMessagePassphrase(Passphrase.fromPassword(password)) + encryptionOptions.addMessagePassphrase( + Passphrase.fromPassword(password).withTrimmedWhitespace()) } private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt index e220f1bf..72998b69 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -99,7 +99,7 @@ class GenerateKeyImpl(private val api: PGPainless) : GenerateKey { override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) } override fun withKeyPassword(password: String): GenerateKey = apply { - this.passphrase = Passphrase.fromPassword(password) + this.passphrase = Passphrase.fromPassword(password).withTrimmedWhitespace() } private fun generateKeyWithProfile( diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt index fd7abfac..4c9024f7 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -18,12 +18,10 @@ import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException -import org.pgpainless.util.Passphrase import sop.Ready import sop.enums.InlineSignAs import sop.exception.SOPGPException import sop.operation.InlineSign -import sop.util.UTF8Util /** Implementation of the `inline-sign` operation using PGPainless. */ class InlineSignImpl(private val api: PGPainless) : InlineSign { @@ -112,7 +110,7 @@ class InlineSignImpl(private val api: PGPainless) : InlineSign { override fun noArmor(): InlineSign = apply { armor = false } override fun withKeyPassword(password: ByteArray): InlineSign = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/PasswordHelper.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/PasswordHelper.kt new file mode 100644 index 00000000..3baa6337 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/PasswordHelper.kt @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.util.Passphrase +import sop.exception.SOPGPException +import sop.util.UTF8Util + +class PasswordHelper { + companion object { + + /** + * Add the given [password] as a message passphrase to the given [consumerOptions] instance. + * If the [password] contains trailing or leading whitespace, additionally add the + * [password] with these whitespace characters removed. + * + * @param password password + * @param consumerOptions consumer options for message decryption + */ + @JvmStatic + fun addMessagePassphrasePlusRemoveWhitespace( + password: String, + consumerOptions: ConsumerOptions + ) { + Passphrase.fromPassword(password).let { + consumerOptions.addMessagePassphrase(it) + val trimmed = it.withTrimmedWhitespace() + if (!it.getChars().contentEquals(trimmed.getChars())) { + consumerOptions.addMessagePassphrase(trimmed) + } + } + } + + /** + * Add the given [password] to the given [protector] instance. If the [password] contains + * trailing or leading whitespace, additionally add the [password] with these whitespace + * characters removed. + * + * @param password password + * @param protector secret key ring protector + * @throws SOPGPException.PasswordNotHumanReadable if the password is not a valid UTF-8 + * string representation. + */ + @JvmStatic + fun addPassphrasePlusRemoveWhitespace( + password: ByteArray, + protector: MatchMakingSecretKeyRingProtector + ) { + val string = + try { + UTF8Util.decodeUTF8(password) + } catch (e: CharacterCodingException) { + throw SOPGPException.PasswordNotHumanReadable( + "Cannot UTF8-decode password: ${e.stackTraceToString()}") + } + addPassphrasePlusRemoveWhitespace(string, protector) + } + + /** + * Add the given [password] to the given [protector] instance. If the [password] contains + * trailing or leading whitespace, additionally add the [password] with these whitespace + * characters removed. + * + * @param password password + * @param protector secret key ring protector + */ + @JvmStatic + fun addPassphrasePlusRemoveWhitespace( + password: String, + protector: MatchMakingSecretKeyRingProtector + ) { + Passphrase.fromPassword(password).let { + protector.addPassphrase(it) + val trimmed = it.withTrimmedWhitespace() + if (!it.getChars().contentEquals(trimmed.getChars())) { + protector.addPassphrase(trimmed) + } + } + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt index 0c7e3585..0fe4c6be 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -15,11 +15,9 @@ import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.util.RevocationAttributes import org.pgpainless.util.OpenPGPCertificateUtil -import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException import sop.operation.RevokeKey -import sop.util.UTF8Util class RevokeKeyImpl(private val api: PGPainless) : RevokeKey { @@ -78,14 +76,6 @@ class RevokeKeyImpl(private val api: PGPainless) : RevokeKey { override fun noArmor(): RevokeKey = apply { armor = false } override fun withKeyPassword(password: ByteArray): RevokeKey = apply { - val string = - try { - UTF8Util.decodeUTF8(password) - } catch (e: CharacterCodingException) { - // TODO: Add cause - throw SOPGPException.PasswordNotHumanReadable( - "Cannot UTF8-decode password: ${e.stackTraceToString()}") - } - protector.addPassphrase(Passphrase.fromPassword(string)) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt index 37767424..dd801367 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt @@ -13,7 +13,6 @@ import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.PGPainless import org.pgpainless.key.modification.secretkeyring.OpenPGPKeyUpdater import org.pgpainless.util.OpenPGPCertificateUtil -import org.pgpainless.util.Passphrase import sop.Ready import sop.operation.UpdateKey @@ -81,6 +80,6 @@ class UpdateKeyImpl(private val api: PGPainless) : UpdateKey { override fun signingOnly(): UpdateKey = apply { signingOnly = true } override fun withKeyPassword(password: ByteArray): UpdateKey = apply { - protector.addPassphrase(Passphrase.fromPassword(String(password))) + PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector) } } 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 521cdfe0..3abf80bc 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -108,6 +109,7 @@ public class GenerateKeyTest { .generate() .getBytes(); - assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + OpenPGPKey key = PGPainless.getInstance().readKey().parseKey(keyBytes); + assertTrue(key.getValidUserIds().get(0).getUserId().equals("Foo\n\nBar")); } }