From 8d373ce45c03f3a2c257fe4fbfd8bb569c9b81d9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Aug 2025 12:34:00 +0200 Subject: [PATCH] cccd3287c1c5bc92ff55e6545f52814cf48564d3 --- .../key/protection/PassphraseTest.java | 11 +-- .../pgpainless/sop/ChangeKeyPasswordImpl.kt | 2 +- .../kotlin/org/pgpainless/sop/DecryptImpl.kt | 11 +-- .../org/pgpainless/sop/DetachedSignImpl.kt | 4 +- .../kotlin/org/pgpainless/sop/EncryptImpl.kt | 6 +- .../org/pgpainless/sop/GenerateKeyImpl.kt | 2 +- .../org/pgpainless/sop/InlineSignImpl.kt | 4 +- .../org/pgpainless/sop/PasswordHelper.kt | 84 +++++++++++++++++++ .../org/pgpainless/sop/RevokeKeyImpl.kt | 12 +-- 9 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/PasswordHelper.kt 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/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt index a9aaf1e4..e0286f29 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -76,6 +76,6 @@ class ChangeKeyPasswordImpl : 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 de2b2b3c..dec83da3 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 : Decrypt { @@ -103,16 +101,11 @@ class DecryptImpl : 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 19bc782b..033c4bbc 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -20,14 +20,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 : DetachedSign { @@ -109,7 +107,7 @@ class DetachedSignImpl : 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 b227561e..b43018c7 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -27,7 +27,6 @@ import sop.ReadyWithResult 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 : Encrypt { @@ -132,11 +131,12 @@ class EncryptImpl : 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 f8297c56..488e9b5f 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -80,7 +80,7 @@ class GenerateKeyImpl : 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 bd77b553..44cb5f39 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -19,12 +19,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 : InlineSign { @@ -113,7 +111,7 @@ class InlineSignImpl : 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 ecc87e62..82685f78 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -17,11 +17,9 @@ import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.util.RevocationAttributes import org.pgpainless.util.ArmoredOutputStreamFactory -import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException import sop.operation.RevokeKey -import sop.util.UTF8Util class RevokeKeyImpl : RevokeKey { @@ -82,14 +80,6 @@ class RevokeKeyImpl : 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) } }