From 5d8effb0c545bf912325b4029ce425e0ce95aac1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Jun 2018 18:46:41 +0200 Subject: [PATCH] Fix decryption, WiP --- .../vanitasvitae/crypto/pgpainless/Main.java | 171 ++++++++++++++++ .../crypto/pgpainless/PGPv4Fingerprint.java | 40 ++++ .../crypto/pgpainless/PainlessResult.java | 135 +++++++++++- .../crypto/pgpainless/PainlessStream.java | 62 ++++++ .../DecryptionBuilder.java | 94 ++++++++- .../DecryptionBuilderInterface.java | 49 +++++ .../InputStreamFactory.java | 192 ++++++++++++++++++ .../MissingPublicKeyCallback.java | 7 + .../SignatureVerifyingInputStream.java | 130 ++++++++++++ .../VerificationFeedbackCallback.java | 6 + 10 files changed, 883 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/Main.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/PGPv4Fingerprint.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessStream.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilderInterface.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/InputStreamFactory.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/MissingPublicKeyCallback.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/SignatureVerifyingInputStream.java create mode 100644 src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/VerificationFeedbackCallback.java diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/Main.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/Main.java new file mode 100644 index 00000000..a51b52db --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/Main.java @@ -0,0 +1,171 @@ +package de.vanitasvitae.crypto.pgpainless; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.util.Collections; + +import de.vanitasvitae.crypto.pgpainless.algorithm.CompressionAlgorithm; +import de.vanitasvitae.crypto.pgpainless.algorithm.HashAlgorithm; +import de.vanitasvitae.crypto.pgpainless.algorithm.SymmetricKeyAlgorithm; +import de.vanitasvitae.crypto.pgpainless.encryption_signing.SecretKeyRingDecryptor; +import de.vanitasvitae.crypto.pgpainless.key.generation.type.length.RsaLength; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.io.Streams; + +public class Main { + + public static void main(String[] args) + throws PGPException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, + IOException { + + Security.addProvider(new BouncyCastleProvider()); + + PGPSecretKeyRing a = PGPainless.generateKeyRing().simpleRsaKeyRing("a@b.c", RsaLength._2048); + PGPSecretKeyRing b = PGPainless.generateKeyRing().simpleRsaKeyRing("b@c.d", RsaLength._2048); + + SecretKeyRingDecryptor secretKeyRingDecryptor = new SecretKeyRingDecryptor() { + @Override + public PBESecretKeyDecryptor getDecryptor(Long keyId) { + return null; + } + + @Override + public PBESecretKeyEncryptor getEncryptor(Long keyId) { + return null; + } + }; + + byte[] m = "Dies ist ein verschlüsselter Text.".getBytes(); + ByteArrayInputStream fromPlain = new ByteArrayInputStream(m); + ByteArrayOutputStream toEncrypted = new ByteArrayOutputStream(); + + OutputStream encryptor = PGPainless.createEncryptor().onOutputStream(toEncrypted) + .toRecipient(b.getPublicKey()) + .usingAlgorithms(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA512, CompressionAlgorithm.UNCOMPRESSED) + .signWith(a.getSecretKey(), secretKeyRingDecryptor) + .asciiArmor(); + + Streams.pipeAll(fromPlain, encryptor); + fromPlain.close(); + encryptor.close(); + + System.out.println(new String(toEncrypted.toByteArray())); + + ByteArrayInputStream fromEncrypted = new ByteArrayInputStream(toEncrypted.toByteArray()); + ByteArrayOutputStream toPlain = new ByteArrayOutputStream(); + + PainlessResult.ResultAndInputStream resultAndInputStream = PGPainless.createDecryptor() + .onInputStream(fromEncrypted) + .decryptWith(new PGPSecretKeyRingCollection(Collections.singleton(b)), secretKeyRingDecryptor) + .verifyWith(Collections.singleton(a.getPublicKey().getKeyID()), + Collections.singleton(new PGPPublicKeyRing(a.getPublicKey().getEncoded(), new BcKeyFingerprintCalculator()))) + .ignoreMissingPublicKeys() + .build(); + + InputStream decryptor = resultAndInputStream.getInputStream(); + + Streams.pipeAll(decryptor, toPlain); + decryptor.close(); + toPlain.close(); + + PainlessResult result = resultAndInputStream.getResult(); + + System.out.println(b.getPublicKey().getKeyID() + " " + result.getDecryptionKeyId()); + System.out.println(new String(toPlain.toByteArray())); + } + + private static void gpg(PGPSecretKeyRing a, PGPSecretKeyRing b, SecretKeyRingDecryptor secretKeyRingDecryptor) + throws IOException, PGPException { + String gpg = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "hQGMAwAAAAAAAAAAAQv+JyovfiPxDiLe9XlgQAG6zD+YdRtZRuUJD+A+ZX4Sn0w0\n" + + "2Dl9Ehf7lKjIo0cIfVOUrgITnWIRWAyfrk5KiXdXcZ6dXxz/YJFnLSlgqUwq7GWi\n" + + "NYf3Uqg+/8f3Ucl0x6sr1oddwB9OI7zRJwDqEzTORjLBu1vtDlFPMPWwAeqDtZgz\n" + + "ikT6vSFfhVjVbgx4mztw7hatWNjXzNkl9+lojzo9IyiA+3SBsRe/2My3ZBjPx97f\n" + + "3YGMCvbggdX3C/MRV2iek2pFX7YTKasFeEy5Y1c09upqaEIpaJq8vi1Fu44dv0Rt\n" + + "gv4sdljaJXsFn9aoVFrp/xU4SyPiC1Z/KjqE3Zfyh+OMWKoWmtYH07/g8IGkkBCh\n" + + "xuDiyy813WS3xtCyX405Vd+rxYC3y1h1FtthdO/AIrYSWj6qI6hyK2tyYmwsg+oY\n" + + "1oaXhcTbWXsBO89v0YtmVK1bPVXq8ao/DQvrs84JsYsKzXA17gKyBLBUoTNn8h8A\n" + + "AXg455AN8iHi2u5pAKr7hQIMAwAAAAAAAAAAAQ/7Bg8SGEfMPmtDy/BazrYWaXvX\n" + + "1+WWRNM630ULPEq7LG4BKJJOOrhk0kNkjIsaXhgqn9bt3YxxuE1eOQksjn0sNOD7\n" + + "3NAOicHzQ7xOarvN9OUSGirc+EIn4ETRGKYF1TXHBSYnBeb+DLCbRZkBZhRrA5Lc\n" + + "z08kWrGRfq5Bz6eMatBTO1L8XTIxHPgc9/LNv7OqcIfT0udjOQMkA7oxCz5mLl2b\n" + + "dApsDEFNKNaGgRzSf2rDqw0SGkDxYsXI6IYrVSEm6uDt+ScybS0KkcEgg+I9l91n\n" + + "XgqQQaXpYnHgETqKYfcUOk5iEND5Lvik/XhHNViaL3CdOkxFLTa0wfy0y0IsV2Y3\n" + + "xGkMOWdDjXlY8UWRgoK61M91phgZ48zfSoVvXNDrjOJzm1jn8CFFFov4Gse7CtlM\n" + + "A+3ntVdjL94jkp+2mU3e9kzCOG+ChylLuqlGTvavbHt/rzuZooi/6g1VHy1r+v9I\n" + + "rWKX6q55H8JzZXZOrfED39QocK9b1BjtEca/Qnqw82+IVY/CufBmnmbOWUkHq1zP\n" + + "6nj840HxH1zV5vHf8vlXxV7/iBesAF94dLT/Hp0E7+Ilyp/pQaQjMS2RLycMUJQJ\n" + + "pQey81gpuOWD6YIbvgnrMBMrJLyJSk3r3sMdJ3DCPxHC+OyvHxddA5TdL2e4aP3L\n" + + "OzKql59v1w+9Doe3LEPSPgEVAdUUg0nEl5lg9LLqaepaYp8NfsEC1Rnk/MLxciJu\n" + + "9oNjqPqQxKTv4aQO/Qb8gHFb3O34OnNKz+CzrX5Q\n" + + "=sVVl\n" + + "-----END PGP MESSAGE-----"; + + ByteArrayInputStream inputStream = new ByteArrayInputStream(gpg.getBytes()); + + InputStream decryptor = PGPainless.createDecryptor().onInputStream(inputStream) + .decryptWith(new PGPSecretKeyRingCollection(Collections.singleton(b)), secretKeyRingDecryptor) + .doNotVerify() + .build() + .getInputStream(); + + } + + public static void symm(PGPSecretKeyRing a, PGPSecretKeyRing b, SecretKeyRingDecryptor secretKeyRingDecryptor) + throws IOException, PGPException { + byte[] bytes = "Diese Nachricht ist streng geheim!!!".getBytes(Charset.forName("UTF-8")); + ByteArrayInputStream fromPlain = new ByteArrayInputStream(bytes); + ByteArrayOutputStream toEncrypted = new ByteArrayOutputStream(); + + OutputStream encryptor = PGPainless.createEncryptor() + .onOutputStream(toEncrypted) + .toRecipient(b.getPublicKey()) + .usingAlgorithms(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA512, CompressionAlgorithm.UNCOMPRESSED) + .signWith(a.getSecretKey(), secretKeyRingDecryptor) + .noArmor(); + + Streams.pipeAll(fromPlain, encryptor); + encryptor.close(); + + System.out.println(new String(toEncrypted.toByteArray(), Charset.forName("UTF-8"))); + + ByteArrayInputStream fromEncrypted = new ByteArrayInputStream(toEncrypted.toByteArray()); + ByteArrayOutputStream toPlain = new ByteArrayOutputStream(); + + PainlessResult.ResultAndInputStream resultAndInputStream = PGPainless.createDecryptor() + .onInputStream(fromEncrypted) + .decryptWith(new PGPSecretKeyRingCollection(Collections.singleton(b)), secretKeyRingDecryptor) + .verifyWith(Collections.singleton(a.getPublicKey().getKeyID()), + Collections.singleton(new PGPPublicKeyRing(a.getPublicKey().getEncoded(), new BcKeyFingerprintCalculator()))) + .ignoreMissingPublicKeys() + .build(); + + InputStream decryptor = resultAndInputStream.getInputStream(); + + Streams.pipeAll(decryptor, toPlain); + decryptor.close(); + + PainlessResult result = resultAndInputStream.getResult(); + } +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/PGPv4Fingerprint.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/PGPv4Fingerprint.java new file mode 100644 index 00000000..888b7834 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/PGPv4Fingerprint.java @@ -0,0 +1,40 @@ +package de.vanitasvitae.crypto.pgpainless; + +import java.util.Arrays; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; + +public class PGPv4Fingerprint { + + private final byte[] fingerprintBytes; + + public PGPv4Fingerprint(PGPPublicKey publicKey) { + if (publicKey.getVersion() != 4) { + throw new IllegalArgumentException("PublicKey is not a OpenPGP v4 Public Key."); + } + this.fingerprintBytes = publicKey.getFingerprint(); + } + + public PGPv4Fingerprint(PGPSecretKey secretKey) { + this(secretKey.getPublicKey()); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (!(o instanceof PGPv4Fingerprint)) { + return false; + } + + return Arrays.equals(fingerprintBytes, ((PGPv4Fingerprint) o).fingerprintBytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(fingerprintBytes); + } +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessResult.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessResult.java index c5c9d87d..8d0a62ed 100644 --- a/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessResult.java +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessResult.java @@ -1,8 +1,139 @@ package de.vanitasvitae.crypto.pgpainless; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import de.vanitasvitae.crypto.pgpainless.algorithm.CompressionAlgorithm; +import de.vanitasvitae.crypto.pgpainless.algorithm.SymmetricKeyAlgorithm; + public class PainlessResult { - Set signingKeys; - Long decryptingKey; + + private final Set recipientKeyIds; + private final Long decryptionKeyId; + private final SymmetricKeyAlgorithm symmetricKeyAlgorithm; + private final CompressionAlgorithm compressionAlgorithm; + private final boolean integrityProtected; + private final Set signatureKeyIds; + private final Set verifiedSignatureKeyIds; + + public PainlessResult(Set recipientKeyIds, + Long decryptionKeyId, + SymmetricKeyAlgorithm symmetricKeyAlgorithm, + CompressionAlgorithm algorithm, + boolean integrityProtected, + Set signatureKeyIds, + Set verifiedSignatureKeyIds) { + + this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); + this.decryptionKeyId = decryptionKeyId; + this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + this.compressionAlgorithm = algorithm; + this.integrityProtected = integrityProtected; + this.signatureKeyIds = Collections.unmodifiableSet(signatureKeyIds); + this.verifiedSignatureKeyIds = Collections.unmodifiableSet(verifiedSignatureKeyIds); + } + + public Set getRecipientKeyIds() { + return recipientKeyIds; + } + + public Long getDecryptionKeyId() { + return decryptionKeyId; + } + + public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { + return symmetricKeyAlgorithm; + } + + public CompressionAlgorithm getCompressionAlgorithm() { + return compressionAlgorithm; + } + + public boolean isIntegrityProtected() { + return integrityProtected; + } + + public Set getSignatureKeyIds() { + return signatureKeyIds; + } + + public Set getVerifiedSignatureKeyIds() { + return verifiedSignatureKeyIds; + } + + public static Builder getBuilder() { + return new Builder(); + } + + public static class Builder { + + private final Set recipientKeyIds = new HashSet<>(); + private Long decryptionKeyId; + private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL; + private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; + private boolean integrityProtected = false; + private final Set signatureKeyIds = new HashSet<>(); + private final Set verifiedSignatureKeyIds = new HashSet<>(); + + public Builder addRecipientKeyId(long id) { + this.recipientKeyIds.add(id); + return this; + } + + public Builder setDecryptionKeyId(long id) { + this.decryptionKeyId = id; + return this; + } + + public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { + this.compressionAlgorithm = algorithm; + return this; + } + + public Builder addSignatureKeyId(long id) { + this.signatureKeyIds.add(id); + return this; + } + + public Builder addVerifiedSignatureKeyId(long id) { + this.verifiedSignatureKeyIds.add(id); + return this; + } + + public Builder setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { + this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + return this; + } + + public Builder setIntegrityProtected(boolean integrityProtected) { + this.integrityProtected = integrityProtected; + return this; + } + + public PainlessResult build() { + return new PainlessResult(recipientKeyIds, decryptionKeyId, symmetricKeyAlgorithm, compressionAlgorithm, integrityProtected, signatureKeyIds, verifiedSignatureKeyIds); + } + } + + public static class ResultAndInputStream { + private final PainlessResult.Builder resultBuilder; + private final PainlessStream.In inputStream; + + public ResultAndInputStream(PainlessResult.Builder resultBuilder, PainlessStream.In inputStream) { + this.resultBuilder = resultBuilder; + this.inputStream = inputStream; + } + + public PainlessResult getResult() { + if (!inputStream.isClosed()) { + throw new IllegalStateException("InputStream must be closed before the PainlessResult can be accessed."); + } + return resultBuilder.build(); + } + + public PainlessStream.In getInputStream() { + return inputStream; + } + } } diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessStream.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessStream.java new file mode 100644 index 00000000..067459a8 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/PainlessStream.java @@ -0,0 +1,62 @@ +package de.vanitasvitae.crypto.pgpainless; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface PainlessStream { + + boolean isClosed(); + + class In extends InputStream implements PainlessStream { + private final InputStream inputStream; + + private boolean isClosed = false; + + public In(InputStream inputStream) { + this.inputStream = inputStream; + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + + @Override + public void close() throws IOException { + inputStream.close(); + isClosed = true; + } + + @Override + public boolean isClosed() { + return isClosed; + } + } + + class Out extends OutputStream implements PainlessStream { + + private final OutputStream outputStream; + private boolean isClosed = false; + + public Out(OutputStream outputStream) { + this.outputStream = outputStream; + } + + @Override + public boolean isClosed() { + return isClosed; + } + + @Override + public void write(int i) throws IOException { + outputStream.write(i); + } + + @Override + public void close() throws IOException { + outputStream.close(); + this.isClosed = true; + } + } +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilder.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilder.java index 7149b43f..d2a64875 100644 --- a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilder.java @@ -1,5 +1,97 @@ package de.vanitasvitae.crypto.pgpainless.decryption_verification; -public class DecryptionBuilder { +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import de.vanitasvitae.crypto.pgpainless.PainlessResult; +import de.vanitasvitae.crypto.pgpainless.encryption_signing.SecretKeyRingDecryptor; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; + +public class DecryptionBuilder implements DecryptionBuilderInterface { + + private InputStream inputStream; + private PGPSecretKeyRingCollection decryptionKeys; + private SecretKeyRingDecryptor decryptionKeyDecryptor; + private Set verificationKeys = new HashSet<>(); + private Set trustedKeyIds = new HashSet<>(); + private MissingPublicKeyCallback missingPublicKeyCallback = null; + + @Override + public DecryptWith onInputStream(InputStream inputStream) { + this.inputStream = inputStream; + return new DecryptWithImpl(); + } + + class DecryptWithImpl implements DecryptWith { + + @Override + public VerifyWith decryptWith(PGPSecretKeyRingCollection secretKeyRings, SecretKeyRingDecryptor decryptor) { + DecryptionBuilder.this.decryptionKeys = secretKeyRings; + DecryptionBuilder.this.decryptionKeyDecryptor = decryptor; + return new VerifyWithImpl(); + } + + @Override + public VerifyWith doNotDecrypt() { + DecryptionBuilder.this.decryptionKeys = null; + DecryptionBuilder.this.decryptionKeyDecryptor = null; + return new VerifyWithImpl(); + } + } + + class VerifyWithImpl implements VerifyWith { + + @Override + public MissingPublicKeyFeedback verifyWith(Set trustedKeyIds, + PGPPublicKeyRingCollection publicKeyRingCollection) { + Set publicKeyRings = new HashSet<>(); + for (Iterator i = publicKeyRingCollection.getKeyRings(); i.hasNext(); ) { + publicKeyRings.add(i.next()); + } + return verifyWith(trustedKeyIds, publicKeyRings); + } + + @Override + public MissingPublicKeyFeedback verifyWith(Set trustedKeyIds, Set publicKeyRings) { + DecryptionBuilder.this.verificationKeys = publicKeyRings; + DecryptionBuilder.this.trustedKeyIds = trustedKeyIds; + return new MissingPublicKeyFeedbackImpl(); + } + + @Override + public Build doNotVerify() { + DecryptionBuilder.this.verificationKeys = null; + DecryptionBuilder.this.trustedKeyIds = null; + return new BuildImpl(); + } + } + + class MissingPublicKeyFeedbackImpl implements MissingPublicKeyFeedback { + + @Override + public Build handleMissingPublicKeysWith(MissingPublicKeyCallback callback) { + DecryptionBuilder.this.missingPublicKeyCallback = callback; + return new BuildImpl(); + } + + @Override + public Build ignoreMissingPublicKeys() { + return new BuildImpl(); + } + } + + class BuildImpl implements Build { + + @Override + public PainlessResult.ResultAndInputStream build() throws IOException, PGPException { + return InputStreamFactory.create(inputStream, + decryptionKeys, decryptionKeyDecryptor, verificationKeys, trustedKeyIds, missingPublicKeyCallback); + } + } } diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilderInterface.java new file mode 100644 index 00000000..b2b152c2 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -0,0 +1,49 @@ +package de.vanitasvitae.crypto.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import de.vanitasvitae.crypto.pgpainless.PainlessResult; +import de.vanitasvitae.crypto.pgpainless.encryption_signing.SecretKeyRingDecryptor; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; + +public interface DecryptionBuilderInterface { + + DecryptWith onInputStream(InputStream inputStream); + + interface DecryptWith { + + VerifyWith decryptWith(PGPSecretKeyRingCollection secretKeyRings, SecretKeyRingDecryptor decryptor); + + VerifyWith doNotDecrypt(); + + } + + interface VerifyWith { + + MissingPublicKeyFeedback verifyWith(Set trustedFingerprints, PGPPublicKeyRingCollection publicKeyRings); + + MissingPublicKeyFeedback verifyWith(Set trustedFingerprints, Set publicKeyRings); + + Build doNotVerify(); + + } + + interface MissingPublicKeyFeedback { + + Build handleMissingPublicKeysWith(MissingPublicKeyCallback callback); + + Build ignoreMissingPublicKeys(); + } + + interface Build { + + PainlessResult.ResultAndInputStream build() throws IOException, PGPException; + + } + +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/InputStreamFactory.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/InputStreamFactory.java new file mode 100644 index 00000000..59352fbb --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/InputStreamFactory.java @@ -0,0 +1,192 @@ +package de.vanitasvitae.crypto.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import de.vanitasvitae.crypto.pgpainless.PainlessResult; +import de.vanitasvitae.crypto.pgpainless.PainlessStream; +import de.vanitasvitae.crypto.pgpainless.algorithm.CompressionAlgorithm; +import de.vanitasvitae.crypto.pgpainless.algorithm.SymmetricKeyAlgorithm; +import de.vanitasvitae.crypto.pgpainless.encryption_signing.SecretKeyRingDecryptor; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; + +public class InputStreamFactory { + + private InputStream inputStream; + + private final PGPSecretKeyRingCollection decryptionKeys; + private final SecretKeyRingDecryptor decryptionKeyDecryptor; + private final Set verificationKeys = new HashSet<>(); + private final Set trustedKeyIds = new HashSet<>(); + private final MissingPublicKeyCallback missingPublicKeyCallback; + + private final PainlessResult.Builder resultBuilder = PainlessResult.getBuilder(); + private final PGPContentVerifierBuilderProvider verifierBuilderProvider = new BcPGPContentVerifierBuilderProvider(); + private final Map verifiableOnePassSignatures = new HashMap<>(); + + private InputStreamFactory(PGPSecretKeyRingCollection decryptionKeys, + SecretKeyRingDecryptor decryptor, + Set verificationKeys, + Set trustedKeyIds, + MissingPublicKeyCallback missingPublicKeyCallback) + throws IOException { + this.decryptionKeys = decryptionKeys; + this.decryptionKeyDecryptor = decryptor; + this.verificationKeys.addAll(verificationKeys != null ? verificationKeys : Collections.emptyList()); + this.trustedKeyIds.addAll(trustedKeyIds != null ? trustedKeyIds : Collections.emptyList()); + this.missingPublicKeyCallback = missingPublicKeyCallback; + } + + public static PainlessResult.ResultAndInputStream create(InputStream inputStream, + PGPSecretKeyRingCollection decryptionKeys, + SecretKeyRingDecryptor decryptor, + Set verificationKeys, + Set trustedKeyIds, + MissingPublicKeyCallback missingPublicKeyCallback) + throws IOException, PGPException { + + InputStreamFactory factory = new InputStreamFactory(decryptionKeys, + decryptor, + verificationKeys, + trustedKeyIds, + missingPublicKeyCallback); + + PGPObjectFactory objectFactory = new PGPObjectFactory( + PGPUtil.getDecoderStream(inputStream), new BcKeyFingerprintCalculator()); + + return new PainlessResult.ResultAndInputStream( + factory.resultBuilder, + new PainlessStream.In(factory.wrap(objectFactory))); + } + + private InputStream wrap(PGPObjectFactory objectFactory) throws IOException, PGPException { + KeyFingerPrintCalculator fingerCalc = new BcKeyFingerprintCalculator(); + + Object pgpObj = null; + while ((pgpObj = objectFactory.nextObject()) != null) { + + if (pgpObj instanceof PGPEncryptedDataList) { + PGPEncryptedDataList encDataList = (PGPEncryptedDataList) pgpObj; + InputStream nextStream = decrypt(encDataList); + objectFactory = new PGPObjectFactory(PGPUtil.getDecoderStream(nextStream), fingerCalc); + return wrap(objectFactory); + } + + if (pgpObj instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) pgpObj; + InputStream nextStream = compressedData.getDataStream(); + resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + objectFactory = new PGPObjectFactory(PGPUtil.getDecoderStream(nextStream), fingerCalc); + return wrap(objectFactory); + } + + if (pgpObj instanceof PGPOnePassSignatureList) { + PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) pgpObj; + verify(onePassSignatures); + } + + if (pgpObj instanceof PGPLiteralData) { + PGPLiteralData literalData = (PGPLiteralData) pgpObj; + InputStream literalDataInputStream = literalData.getInputStream(); + + if (verifiableOnePassSignatures.isEmpty()) { + return literalDataInputStream; + } + + return new SignatureVerifyingInputStream(literalDataInputStream, + objectFactory, verifiableOnePassSignatures, resultBuilder); + } + } + + throw new PGPException("No Literal Data Packet found!"); + } + + private InputStream decrypt(PGPEncryptedDataList encryptedDataList) + throws PGPException { + Iterator iterator = encryptedDataList.getEncryptedDataObjects(); + if (!iterator.hasNext()) { + throw new PGPException("Decryption failed - No encrypted data found!"); + } + + PGPPrivateKey decryptionKey = null; + PGPPublicKeyEncryptedData encryptedSessionKey = null; + while (iterator.hasNext()) { + encryptedSessionKey = (PGPPublicKeyEncryptedData) iterator.next(); + long keyId = encryptedSessionKey.getKeyID(); + + resultBuilder.addRecipientKeyId(keyId); + + if (decryptionKey != null) { + continue; + } + + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + if (secretKey != null) { + decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); + resultBuilder.setDecryptionKeyId(keyId); + } + } + + if (decryptionKey == null) { + throw new PGPException("Decryption failed - No suitable decryption key found!"); + } + + PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); + resultBuilder.setSymmetricKeyAlgorithm( + SymmetricKeyAlgorithm.forId(encryptedSessionKey.getSymmetricAlgorithm(keyDecryptor))); + resultBuilder.setIntegrityProtected(encryptedSessionKey.isIntegrityProtected()); + + InputStream decryptionStream = encryptedSessionKey.getDataStream(keyDecryptor); + + return decryptionStream; + } + + private void verify(PGPOnePassSignatureList onePassSignatureList) throws PGPException { + Iterator iterator = onePassSignatureList.iterator(); + if (!iterator.hasNext()) { + throw new PGPException("Verification failed - No OnePassSignatures found!"); + } + + while (iterator.hasNext()) { + PGPOnePassSignature signature = iterator.next(); + long keyId = signature.getKeyID(); + resultBuilder.addSignatureKeyId(keyId); + + // Find public key + PGPPublicKey verificationKey = null; + for (PGPPublicKeyRing publicKeyRing : verificationKeys) { + verificationKey = publicKeyRing.getPublicKey(signature.getKeyID()); + } + + if (verificationKey != null) { + signature.init(verifierBuilderProvider, verificationKey); + verifiableOnePassSignatures.put(signature.getKeyID(), signature); + } + } + } +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/MissingPublicKeyCallback.java new file mode 100644 index 00000000..7fe238a8 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -0,0 +1,7 @@ +package de.vanitasvitae.crypto.pgpainless.decryption_verification; + +public interface MissingPublicKeyCallback { + + void onMissingPublicKeyEncountered(Long keyId); + +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/SignatureVerifyingInputStream.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/SignatureVerifyingInputStream.java new file mode 100644 index 00000000..e5469779 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/SignatureVerifyingInputStream.java @@ -0,0 +1,130 @@ +package de.vanitasvitae.crypto.pgpainless.decryption_verification; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SignatureException; +import java.util.Iterator; +import java.util.Map; + +import de.vanitasvitae.crypto.pgpainless.PainlessResult; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; + +public class SignatureVerifyingInputStream extends FilterInputStream { + + private final PGPObjectFactory objectFactory; + private final Map onePassSignatures; + private final PainlessResult.Builder resultBuilder; + + protected SignatureVerifyingInputStream(InputStream inputStream, + PGPObjectFactory objectFactory, + Map onePassSignatures, + PainlessResult.Builder resultBuilder) { + super(inputStream); + this.objectFactory = objectFactory; + this.resultBuilder = resultBuilder; + this.onePassSignatures = onePassSignatures; + } + + private void updateOnePassSignatures(byte data) { + for (PGPOnePassSignature signature : onePassSignatures.values()) { + signature.update(data); + } + } + + private void updateOnePassSignatures(byte[] b, int off, int len) { + for (PGPOnePassSignature signature : onePassSignatures.values()) { + signature.update(b, off, len); + } + } + + private void validateOnePassSignatures() throws IOException { + if (onePassSignatures.isEmpty()) { + return; + } + + try { + PGPSignatureList signatureList = null; + Iterator objectIterator = objectFactory.iterator(); + while (objectIterator.hasNext() && signatureList == null) { + Object object = objectIterator.next(); + if (object instanceof PGPSignatureList) { + signatureList = (PGPSignatureList) object; + } + } + + if (signatureList == null || signatureList.isEmpty()) { + throw new IOException("Verification failed - No Signatures found!"); + } + + for (PGPSignature signature : signatureList) { + PGPOnePassSignature onePassSignature = onePassSignatures.get(signature.getKeyID()); + if (onePassSignature == null) { + continue; + } + if (!onePassSignature.verify(signature)) { + throw new SignatureException("Bad Signature of key " + signature.getKeyID()); + } else { + resultBuilder.addVerifiedSignatureKeyId(signature.getKeyID()); + } + } + } catch (PGPException | SignatureException e) { + throw new IOException(e.getMessage(), e); + } + + } + + @Override + public int read() throws IOException { + final int data = super.read(); + final boolean endOfStream = data == -1; + if (endOfStream) { + validateOnePassSignatures(); + } else { + updateOnePassSignatures((byte) data); + } + return data; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = super.read(b, off, len); + + final boolean endOfStream = read == -1; + if (endOfStream) { + validateOnePassSignatures(); + } else { + updateOnePassSignatures(b, off, read); + } + return read; + } + + @Override + public long skip(long n) { + throw new UnsupportedOperationException("skip() is not supported."); + } + + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException("mark() not supported."); + } + + @Override + public synchronized void reset() { + throw new UnsupportedOperationException("reset() is not supported."); + } + + @Override + public boolean markSupported() { + return false; + } +} diff --git a/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/VerificationFeedbackCallback.java b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/VerificationFeedbackCallback.java new file mode 100644 index 00000000..9f3ab6d0 --- /dev/null +++ b/src/main/java/de/vanitasvitae/crypto/pgpainless/decryption_verification/VerificationFeedbackCallback.java @@ -0,0 +1,6 @@ +package de.vanitasvitae.crypto.pgpainless.decryption_verification; + +public class VerificationFeedbackCallback { + + +}