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
|
@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);
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue