1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 10:19:39 +02:00

Properly handle passphrases with leading/trailing whitespace

This commit is contained in:
Paul Schaub 2025-08-30 12:34:00 +02:00
parent bd991d0a78
commit cccd3287c1
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
11 changed files with 102 additions and 40 deletions

View file

@ -34,19 +34,20 @@ public class PassphraseTest {
@Test @Test
public void testTrimming() { public void testTrimming() {
Passphrase leadingSpace = Passphrase.fromPassword(" space"); Passphrase leadingSpace = Passphrase.fromPassword(" space").withTrimmedWhitespace();
assertArrayEquals("space".toCharArray(), leadingSpace.getChars()); assertArrayEquals("space".toCharArray(), leadingSpace.getChars());
assertFalse(leadingSpace.isEmpty()); assertFalse(leadingSpace.isEmpty());
Passphrase trailingSpace = Passphrase.fromPassword("space "); Passphrase trailingSpace = Passphrase.fromPassword("space ").withTrimmedWhitespace();
assertArrayEquals("space".toCharArray(), trailingSpace.getChars()); assertArrayEquals("space".toCharArray(), trailingSpace.getChars());
assertFalse(trailingSpace.isEmpty()); 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()); assertArrayEquals("Such whitespace, much wow".toCharArray(), leadingTrailingWhitespace.getChars());
assertFalse(leadingTrailingWhitespace.isEmpty()); assertFalse(leadingTrailingWhitespace.isEmpty());
Passphrase fromEmptyChars = new Passphrase(" ".toCharArray()); Passphrase fromEmptyChars = new Passphrase(" ".toCharArray()).withTrimmedWhitespace();
assertNull(fromEmptyChars.getChars()); assertNull(fromEmptyChars.getChars());
assertTrue(fromEmptyChars.isEmpty()); assertTrue(fromEmptyChars.isEmpty());
} }
@ -54,7 +55,7 @@ public class PassphraseTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = {"", " ", " ", "\t", "\t\t"}) @ValueSource(strings = {"", " ", " ", "\t", "\t\t"})
public void testEmptyPassphrases(String empty) { public void testEmptyPassphrases(String empty) {
Passphrase passphrase = Passphrase.fromPassword(empty); Passphrase passphrase = Passphrase.fromPassword(empty).withTrimmedWhitespace();
assertTrue(passphrase.isEmpty()); assertTrue(passphrase.isEmpty());
assertEquals(Passphrase.emptyPassphrase(), passphrase); assertEquals(Passphrase.emptyPassphrase(), passphrase);

View file

@ -10,7 +10,6 @@ import org.bouncycastle.openpgp.api.OpenPGPKey
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException
import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.OpenPGPCertificateUtil
import org.pgpainless.util.Passphrase
import sop.Ready import sop.Ready
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.CertifyUserId 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 userId(userId: String): CertifyUserId = apply { this.userIds.add(userId) }
override fun withKeyPassword(password: ByteArray): CertifyUserId = apply { override fun withKeyPassword(password: ByteArray): CertifyUserId = apply {
protector.addPassphrase(Passphrase.fromPassword(String(password))) PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector)
} }
} }

View file

@ -73,6 +73,6 @@ class ChangeKeyPasswordImpl(private val api: PGPainless) : ChangeKeyPassword {
override fun noArmor(): ChangeKeyPassword = apply { armor = false } override fun noArmor(): ChangeKeyPassword = apply { armor = false }
override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply {
oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)) PasswordHelper.addPassphrasePlusRemoveWhitespace(oldPassphrase, oldProtector)
} }
} }

View file

@ -16,13 +16,11 @@ import org.pgpainless.decryption_verification.ConsumerOptions
import org.pgpainless.exception.MalformedOpenPgpMessageException import org.pgpainless.exception.MalformedOpenPgpMessageException
import org.pgpainless.exception.MissingDecryptionMethodException import org.pgpainless.exception.MissingDecryptionMethodException
import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.exception.WrongPassphraseException
import org.pgpainless.util.Passphrase
import sop.DecryptionResult import sop.DecryptionResult
import sop.ReadyWithResult import sop.ReadyWithResult
import sop.SessionKey import sop.SessionKey
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.Decrypt import sop.operation.Decrypt
import sop.util.UTF8Util
/** Implementation of the `decrypt` operation using PGPainless. */ /** Implementation of the `decrypt` operation using PGPainless. */
class DecryptImpl(private val api: PGPainless) : Decrypt { 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 { 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 { override fun withPassword(password: String): Decrypt = apply {
consumerOptions.addMessagePassphrase(Passphrase.fromPassword(password)) PasswordHelper.addMessagePassphrasePlusRemoveWhitespace(password, consumerOptions)
password.trimEnd().let {
if (it != password) {
consumerOptions.addMessagePassphrase(Passphrase.fromPassword(it))
}
}
} }
override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply {

View file

@ -19,14 +19,12 @@ import org.pgpainless.encryption_signing.SigningOptions
import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.MissingSecretKeyException
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
import org.pgpainless.util.ArmoredOutputStreamFactory import org.pgpainless.util.ArmoredOutputStreamFactory
import org.pgpainless.util.Passphrase
import sop.MicAlg import sop.MicAlg
import sop.ReadyWithResult import sop.ReadyWithResult
import sop.SigningResult import sop.SigningResult
import sop.enums.SignAs import sop.enums.SignAs
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.DetachedSign import sop.operation.DetachedSign
import sop.util.UTF8Util
/** Implementation of the `sign` operation using PGPainless. */ /** Implementation of the `sign` operation using PGPainless. */
class DetachedSignImpl(private val api: PGPainless) : DetachedSign { 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 noArmor(): DetachedSign = apply { armor = false }
override fun withKeyPassword(password: ByteArray): DetachedSign = apply { 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 { private fun modeToSigType(mode: SignAs): DocumentSignatureType {

View file

@ -30,7 +30,6 @@ import sop.SessionKey
import sop.enums.EncryptAs import sop.enums.EncryptAs
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.Encrypt import sop.operation.Encrypt
import sop.util.UTF8Util
/** Implementation of the `encrypt` operation using PGPainless. */ /** Implementation of the `encrypt` operation using PGPainless. */
class EncryptImpl(private val api: PGPainless) : Encrypt { 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 { 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 { override fun withPassword(password: String): Encrypt = apply {
encryptionOptions.addMessagePassphrase(Passphrase.fromPassword(password)) encryptionOptions.addMessagePassphrase(
Passphrase.fromPassword(password).withTrimmedWhitespace())
} }
private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding {

View file

@ -99,7 +99,7 @@ class GenerateKeyImpl(private val api: PGPainless) : GenerateKey {
override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) } override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) }
override fun withKeyPassword(password: String): GenerateKey = apply { override fun withKeyPassword(password: String): GenerateKey = apply {
this.passphrase = Passphrase.fromPassword(password) this.passphrase = Passphrase.fromPassword(password).withTrimmedWhitespace()
} }
private fun generateKeyWithProfile( private fun generateKeyWithProfile(

View file

@ -18,12 +18,10 @@ import org.pgpainless.encryption_signing.ProducerOptions
import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.encryption_signing.SigningOptions
import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.MissingSecretKeyException
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
import org.pgpainless.util.Passphrase
import sop.Ready import sop.Ready
import sop.enums.InlineSignAs import sop.enums.InlineSignAs
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.InlineSign import sop.operation.InlineSign
import sop.util.UTF8Util
/** Implementation of the `inline-sign` operation using PGPainless. */ /** Implementation of the `inline-sign` operation using PGPainless. */
class InlineSignImpl(private val api: PGPainless) : InlineSign { 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 noArmor(): InlineSign = apply { armor = false }
override fun withKeyPassword(password: ByteArray): InlineSign = apply { 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 { private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType {

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
//
// 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)
}
}
}
}
}

View file

@ -15,11 +15,9 @@ import org.pgpainless.exception.WrongPassphraseException
import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.key.util.RevocationAttributes import org.pgpainless.key.util.RevocationAttributes
import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.OpenPGPCertificateUtil
import org.pgpainless.util.Passphrase
import sop.Ready import sop.Ready
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.operation.RevokeKey import sop.operation.RevokeKey
import sop.util.UTF8Util
class RevokeKeyImpl(private val api: PGPainless) : RevokeKey { 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 noArmor(): RevokeKey = apply { armor = false }
override fun withKeyPassword(password: ByteArray): RevokeKey = apply { override fun withKeyPassword(password: ByteArray): RevokeKey = apply {
val string = PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector)
try {
UTF8Util.decodeUTF8(password)
} catch (e: CharacterCodingException) {
// TODO: Add cause
throw SOPGPException.PasswordNotHumanReadable(
"Cannot UTF8-decode password: ${e.stackTraceToString()}")
}
protector.addPassphrase(Passphrase.fromPassword(string))
} }
} }

View file

@ -13,7 +13,6 @@ import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import org.pgpainless.key.modification.secretkeyring.OpenPGPKeyUpdater import org.pgpainless.key.modification.secretkeyring.OpenPGPKeyUpdater
import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.OpenPGPCertificateUtil
import org.pgpainless.util.Passphrase
import sop.Ready import sop.Ready
import sop.operation.UpdateKey import sop.operation.UpdateKey
@ -81,6 +80,6 @@ class UpdateKeyImpl(private val api: PGPainless) : UpdateKey {
override fun signingOnly(): UpdateKey = apply { signingOnly = true } override fun signingOnly(): UpdateKey = apply { signingOnly = true }
override fun withKeyPassword(password: ByteArray): UpdateKey = apply { override fun withKeyPassword(password: ByteArray): UpdateKey = apply {
protector.addPassphrase(Passphrase.fromPassword(String(password))) PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector)
} }
} }