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:
parent
bd991d0a78
commit
cccd3287c1
11 changed files with 102 additions and 40 deletions
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue