From 9a545a29360678cfec793bef26ec6b899f91ea5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 7 Jun 2022 08:55:10 +0200 Subject: [PATCH] Wip: SOP 4 --- .../key/generation/KeyRingTemplates.java | 12 +- pgpainless-sop/build.gradle | 2 + .../java/org/pgpainless/sop/DecryptImpl.java | 37 ++++-- .../{SignImpl.java => DetachedSignImpl.java} | 15 ++- ...erifyImpl.java => DetachedVerifyImpl.java} | 12 +- .../java/org/pgpainless/sop/EncryptImpl.java | 50 +++++--- .../org/pgpainless/sop/GenerateKeyImpl.java | 10 +- ...MessageImpl.java => InlineDetachImpl.java} | 6 +- .../org/pgpainless/sop/InlineSignImpl.java | 42 +++++++ .../org/pgpainless/sop/InlineVerifyImpl.java | 37 ++++++ .../MatchMakingSecretKeyRingProtector.java | 101 +++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 40 ++++-- .../java/org/pgpainless/sop/ArmorTest.java | 2 +- .../DetachInbandSignatureAndMessageTest.java | 2 +- .../sop/EncryptDecryptRoundTripTest.java | 117 +++++++++++++++++- version.gradle | 2 +- 16 files changed, 429 insertions(+), 58 deletions(-) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{SignImpl.java => DetachedSignImpl.java} (92%) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{VerifyImpl.java => DetachedVerifyImpl.java} (86%) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{DetachInbandSignatureAndMessageImpl.java => InlineDetachImpl.java} (92%) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 41d39a1a..e2cf7190 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -200,7 +200,7 @@ public final class KeyRingTemplates { * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing modernKeyRing(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, null); + return modernKeyRing(userId, (Passphrase) null); } /** @@ -217,13 +217,19 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing modernKeyRing(String userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : null); + return modernKeyRing(userId, passphrase); + } + + public PGPSecretKeyRing modernKeyRing(String userId, Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .addUserId(userId); - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); + if (passphrase != null && !passphrase.isEmpty()) { + builder.setPassphrase(passphrase); } return builder.build(); } diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index d6b97e64..4eca8a7f 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -10,9 +10,11 @@ group 'org.pgpainless' repositories { mavenCentral() + mavenLocal() } dependencies { + implementation 'org.jetbrains:annotations:20.1.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 413136da..89eb29db 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -7,9 +7,12 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -23,9 +26,8 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; import sop.DecryptionResult; import sop.ReadyWithResult; @@ -37,6 +39,8 @@ import sop.operation.Decrypt; public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = new ConsumerOptions(); + private final Set keys = new HashSet<>(); + private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption { @@ -96,29 +100,34 @@ public class DecryptImpl implements Decrypt { } @Override - public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { + public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { try { - PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing() + PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); - - for (PGPSecretKeyRing secretKey : secretKeys) { - KeyRingInfo info = new KeyRingInfo(secretKey); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); - } + for (PGPSecretKeyRing key : secretKeyCollection) { + keys.add(key); } - - consumerOptions.addDecryptionKeys(secretKeys, SecretKeyRingProtector.unprotectedKeys()); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } return this; } + @Override + public Decrypt withKeyPassword(byte[] password) { + String string = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(string)); + return this; + } + @Override public ReadyWithResult ciphertext(InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg { + for (PGPSecretKeyRing key : keys) { + protector.addSecretKey(key); + consumerOptions.addDecryptionKey(key, protector); + } if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); @@ -131,8 +140,12 @@ public class DecryptImpl implements Decrypt { .withOptions(consumerOptions); } catch (MissingDecryptionMethodException e) { throw new SOPGPException.CannotDecrypt(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); } catch (PGPException | IOException e) { throw new SOPGPException.BadData(e); + } finally { + protector.clear(); } return new ReadyWithResult() { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java similarity index 92% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 286c5262..3341712a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -32,28 +32,28 @@ import sop.ReadyWithResult; import sop.SigningResult; import sop.enums.SignAs; import sop.exception.SOPGPException; -import sop.operation.Sign; +import sop.operation.DetachedSign; -public class SignImpl implements Sign { +public class DetachedSignImpl implements DetachedSign { private boolean armor = true; private SignAs mode = SignAs.Binary; private final SigningOptions signingOptions = new SigningOptions(); @Override - public Sign noArmor() { + public DetachedSign noArmor() { armor = false; return this; } @Override - public Sign mode(SignAs mode) { + public DetachedSign mode(SignAs mode) { this.mode = mode; return this; } @Override - public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); @@ -70,6 +70,11 @@ public class SignImpl implements Sign { return this; } + @Override + public DetachedSign withKeyPassword(byte[] password) { + return null; + } + @Override public ReadyWithResult data(InputStream data) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java similarity index 86% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index c874b452..d4db494f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -21,26 +21,26 @@ import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.SubkeyIdentifier; import sop.Verification; import sop.exception.SOPGPException; -import sop.operation.Verify; +import sop.operation.DetachedVerify; -public class VerifyImpl implements Verify { +public class DetachedVerifyImpl implements DetachedVerify { private final ConsumerOptions options = new ConsumerOptions(); @Override - public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotBefore(timestamp); return this; } @Override - public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotAfter(timestamp); return this; } @Override - public Verify cert(InputStream cert) throws SOPGPException.BadData { + public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { PGPPublicKeyRingCollection certificates; try { certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); @@ -52,7 +52,7 @@ public class VerifyImpl implements Verify { } @Override - public VerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { + public DetachedVerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { try { options.addVerificationOfDetachedSignatures(signatures); } catch (IOException | PGPException e) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index d6bd7709..bfa80f47 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -7,9 +7,13 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; @@ -20,7 +24,6 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; import sop.util.ProxyOutputStream; import sop.Ready; @@ -33,6 +36,9 @@ public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = new EncryptionOptions(); SigningOptions signingOptions = null; + Set signingKeys = new HashSet<>(); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -49,7 +55,7 @@ public class EncryptImpl implements Encrypt { } @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); if (keys.size() != 1) { @@ -59,23 +65,20 @@ public class EncryptImpl implements Encrypt { if (signingOptions == null) { signingOptions = SigningOptions.get(); } - try { - signingOptions.addInlineSignatures( - SecretKeyRingProtector.unprotectedKeys(), - keys, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (IllegalArgumentException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } + signingKeys.add(keys.getKeyRings().next()); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } return this; } + @Override + public Encrypt withKeyPassword(byte[] password) { + String passphrase = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(passphrase)); + return this; + } + @Override public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); @@ -97,6 +100,26 @@ public class EncryptImpl implements Encrypt { @Override public Ready plaintext(InputStream plaintext) throws IOException { + for (PGPSecretKeyRing signingKey : signingKeys) { + protector.addSecretKey(signingKey); + } + + if (signingOptions != null) { + try { + signingOptions.addInlineSignatures( + protector, + signingKeys, + (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ); + } catch (IllegalArgumentException e) { + throw new SOPGPException.KeyCannotSign(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); + } catch (PGPException e) { + throw new SOPGPException.BadData(e); + } + } + ProducerOptions producerOptions = signingOptions != null ? ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : ProducerOptions.encrypt(encryptionOptions); @@ -125,7 +148,6 @@ public class EncryptImpl implements Encrypt { private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { switch (encryptAs) { case Binary: - case MIME: return StreamEncoding.BINARY; case Text: return StreamEncoding.UTF8; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 6a2c09f9..893c8dd8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; +import org.pgpainless.util.Passphrase; import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; @@ -27,6 +28,7 @@ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); + private Passphrase passphrase; @Override public GenerateKey noArmor() { @@ -40,6 +42,12 @@ public class GenerateKeyImpl implements GenerateKey { return this; } + @Override + public GenerateKey withKeyPassword(String password) { + this.passphrase = Passphrase.fromPassword(password); + return this; + } + @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); @@ -50,7 +58,7 @@ public class GenerateKeyImpl implements GenerateKey { PGPSecretKeyRing key; try { key = PGPainless.generateKeyRing() - .modernKeyRing(userIdIterator.next(), null); + .modernKeyRing(userIdIterator.next(), passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java similarity index 92% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 34d2f618..f23434c2 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -20,14 +20,14 @@ import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.ReadyWithResult; import sop.Signatures; import sop.exception.SOPGPException; -import sop.operation.DetachInbandSignatureAndMessage; +import sop.operation.InlineDetach; -public class DetachInbandSignatureAndMessageImpl implements DetachInbandSignatureAndMessage { +public class InlineDetachImpl implements InlineDetach { private boolean armor = true; @Override - public DetachInbandSignatureAndMessage noArmor() { + public InlineDetach noArmor() { this.armor = false; return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java new file mode 100644 index 00000000..7eaeaa6f --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import sop.ReadyWithResult; +import sop.SigningResult; +import sop.enums.InlineSignAs; +import sop.exception.SOPGPException; +import sop.operation.DetachedSign; +import sop.operation.InlineSign; + +import java.io.IOException; +import java.io.InputStream; + +public class InlineSignImpl implements InlineSign { + @Override + public DetachedSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public DetachedSign noArmor() { + return null; + } + + @Override + public InlineSign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + return null; + } + + @Override + public InlineSign withKeyPassword(byte[] password) { + return null; + } + + @Override + public ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText { + return null; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java new file mode 100644 index 00000000..a230e0fd --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import sop.ReadyWithResult; +import sop.Verification; +import sop.exception.SOPGPException; +import sop.operation.InlineVerify; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; +import java.util.List; + +public class InlineVerifyImpl implements InlineVerify { + @Override + public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + return null; + } + + @Override + public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public InlineVerify cert(InputStream cert) throws SOPGPException.BadData { + return null; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java new file mode 100644 index 00000000..df54583e --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.jetbrains.annotations.Nullable; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyInfo; +import org.pgpainless.key.protection.CachingSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { + + private final Set passphrases = new HashSet<>(); + private final Set keys = new HashSet<>(); + private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + + public void addPassphrase(Passphrase passphrase) { + if (passphrase.isEmpty()) { + return; + } + + if (!passphrases.add(passphrase)) { + return; + } + + for (PGPSecretKeyRing key : keys) { + for (PGPSecretKey subkey : key) { + if (protector.hasPassphrase(subkey.getKeyID())) { + continue; + } + + testPassphrase(passphrase, subkey); + } + } + } + + public void addSecretKey(PGPSecretKeyRing key) { + if (!keys.add(key)) { + return; + } + + for (PGPSecretKey subkey : key) { + if (KeyInfo.isDecrypted(subkey)) { + protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase()); + } else { + for (Passphrase passphrase : passphrases) { + testPassphrase(passphrase, subkey); + } + } + } + } + + private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) { + try { + PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); + UnlockSecretKey.unlockSecretKey(subkey, decryptor); + protector.addPassphrase(subkey.getKeyID(), passphrase); + } catch (PGPException e) { + // wrong password + } + } + + @Override + public boolean hasPassphraseFor(Long keyId) { + return protector.hasPassphrase(keyId); + } + + @Nullable + @Override + public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { + return protector.getDecryptor(keyId); + } + + @Nullable + @Override + public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { + return protector.getEncryptor(keyId); + } + + public void clear() { + for (Passphrase passphrase : passphrases) { + passphrase.clear(); + } + + for (PGPSecretKeyRing key : keys) { + protector.forgetPassphrase(key); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index cfa426a5..35ff8994 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -8,12 +8,14 @@ import sop.SOP; import sop.operation.Armor; import sop.operation.Dearmor; import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; +import sop.operation.DetachedSign; +import sop.operation.DetachedVerify; +import sop.operation.InlineDetach; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; +import sop.operation.InlineSign; +import sop.operation.InlineVerify; import sop.operation.Version; public class SOPImpl implements SOP { @@ -34,13 +36,33 @@ public class SOPImpl implements SOP { } @Override - public Sign sign() { - return new SignImpl(); + public DetachedSign sign() { + return detachedSign(); } @Override - public Verify verify() { - return new VerifyImpl(); + public DetachedSign detachedSign() { + return new DetachedSignImpl(); + } + + @Override + public InlineSign inlineSign() { + return new InlineSignImpl(); + } + + @Override + public DetachedVerify verify() { + return detachedVerify(); + } + + @Override + public DetachedVerify detachedVerify() { + return new DetachedVerifyImpl(); + } + + @Override + public InlineVerify inlineVerify() { + return new InlineVerifyImpl(); } @Override @@ -64,7 +86,7 @@ public class SOPImpl implements SOP { } @Override - public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { - return new DetachInbandSignatureAndMessageImpl(); + public InlineDetach inlineDetach() { + return new InlineDetachImpl(); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index ad6da440..95129dfa 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -28,7 +28,7 @@ public class ArmorTest { @Test public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice", null).getEncoded(); + byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data).getBytes(StandardCharsets.UTF_8); byte[] armored = new SOPImpl() .armor() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java index 06c11226..b76f069c 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java @@ -58,7 +58,7 @@ public class DetachInbandSignatureAndMessageTest { signingStream.close(); // actually detach the message - ByteArrayAndResult detachedMsg = sop.detachInbandSignatureAndMessage() + ByteArrayAndResult detachedMsg = sop.inlineDetach() .message(out.toByteArray()) .toByteArrayAndResult(); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index d0699ae7..45a26586 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.bouncycastle.util.io.Streams; @@ -24,12 +25,13 @@ import sop.exception.SOPGPException; public class EncryptDecryptRoundTripTest { + private static final Charset utf8 = Charset.forName("UTF8"); private static SOP sop; private static byte[] aliceKey; private static byte[] aliceCert; private static byte[] bobKey; private static byte[] bobCert; - private static byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + private static byte[] message = "Hello, World!\n".getBytes(utf8); @BeforeAll public static void setup() throws IOException { @@ -218,8 +220,119 @@ public class EncryptDecryptRoundTripTest { "=MUYS\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + String msg = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Doj0CaB2GRvISAQdAhV5sjUCxanM68jG9qaq2rep1KKQx2o+9yrK0Rsrtqkww\n" + + "mb4uVv/SD3ixDztUSgUset0jeUeZHZAWfTB9cWawX4fiB2BdbcxhxFqQR8VPJ2SZ\n" + + "0jcB+wH1gq05AkMaCfoEIio3o3QcZq2In8tqj69U3AFRQApoH/p+ZLDz2pcnFBn+\n" + + "x1Y+C6wNg/3g\n" + + "=6vge\n" + + "-----END PGP MESSAGE-----"; + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.decrypt() - .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8))); + .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8)) + .ciphertext(msg.getBytes(utf8))); + } + + @Test + public void encryptDecryptRoundTripWithProtectedKey() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKeyPassword(passphrase) + .withKey(key) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripWithTwoProtectedKeysAndOnePassphrase() throws IOException { + byte[] passphrase1 = "sw0rdf1sh".getBytes(utf8); + + byte[] key1 = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase1) + .generate().getBytes(); + + byte[] cert1 = sop.extractCert() + .key(key1) + .getBytes(); + + byte[] passphrase2 = "fooBar".getBytes(utf8); + + byte[] key2 = sop.generateKey() + .userId("Bob ") + .withKeyPassword(passphrase2) + .generate().getBytes(); + + byte[] cert2 = sop.extractCert() + .key(key2) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert1) + .withCert(cert2) + .plaintext(plaintext) + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKey(key1) + .withKey(key2) + .withKeyPassword(passphrase2) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripFailsWithProtectedKeyAndWrongPassphrase() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, + () -> sop.decrypt() + .withKeyPassword("foobar") + .withKey(key) + .ciphertext(ciphertext)); } @Test diff --git a/version.gradle b/version.gradle index f890e8d6..ddb6de7d 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.36' logbackVersion = '1.2.11' junitVersion = '5.8.2' - sopJavaVersion = '1.2.3' + sopJavaVersion = '1.2.4-SNAPSHOT' } }