diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 1672a751..575c02d4 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -3,6 +3,7 @@ plugins { } dependencies { + implementation 'org.jetbrains:annotations:19.0.0' testImplementation group: 'junit', name: 'junit', version: '4.12' /* implementation "org.bouncycastle:bcprov-debug-jdk15on:$bouncyCastleVersion" diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 4b063eae..ae19ea07 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -17,15 +17,24 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; +import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -34,9 +43,12 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { private InputStream inputStream; private PGPSecretKeyRingCollection decryptionKeys; private SecretKeyRingProtector decryptionKeyDecryptor; + private List detachedSignatures; private Set verificationKeys = new HashSet<>(); private MissingPublicKeyCallback missingPublicKeyCallback = null; + private final KeyFingerPrintCalculator keyFingerPrintCalculator = new BcKeyFingerprintCalculator(); + @Override public DecryptWith onInputStream(InputStream inputStream) { this.inputStream = inputStream; @@ -46,17 +58,76 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { class DecryptWithImpl implements DecryptWith { @Override - public VerifyWith decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings) { + public Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings) { DecryptionBuilder.this.decryptionKeys = secretKeyRings; DecryptionBuilder.this.decryptionKeyDecryptor = decryptor; + return new VerifyImpl(); + } + + @Override + public Verify doNotDecrypt() { + DecryptionBuilder.this.decryptionKeys = null; + DecryptionBuilder.this.decryptionKeyDecryptor = null; + return new VerifyImpl(); + } + } + + class VerifyImpl implements Verify { + + @Override + public VerifyWith verifyDetachedSignature(InputStream inputStream) throws IOException, PGPException { + List signatures = new ArrayList<>(); + InputStream pgpIn = PGPUtil.getDecoderStream(inputStream); + PGPObjectFactory objectFactory = new PGPObjectFactory( + pgpIn, keyFingerPrintCalculator); + Object nextObject = objectFactory.nextObject(); + while (nextObject != null) { + if (nextObject instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) nextObject; + objectFactory = new PGPObjectFactory(compressedData.getDataStream(), keyFingerPrintCalculator); + nextObject = objectFactory.nextObject(); + continue; + } + if (nextObject instanceof PGPSignatureList) { + PGPSignatureList signatureList = (PGPSignatureList) nextObject; + for (PGPSignature s : signatureList) { + signatures.add(s); + } + } + if (nextObject instanceof PGPSignature) { + signatures.add((PGPSignature) nextObject); + } + nextObject = objectFactory.nextObject(); + } + pgpIn.close(); + return verifyDetachedSignatures(signatures); + } + + @Override + public VerifyWith verifyDetachedSignatures(List signatures) { + DecryptionBuilder.this.detachedSignatures = signatures; return new VerifyWithImpl(); } @Override - public VerifyWith doNotDecrypt() { - DecryptionBuilder.this.decryptionKeys = null; - DecryptionBuilder.this.decryptionKeyDecryptor = null; - return new VerifyWithImpl(); + public HandleMissingPublicKeys verifyWith(@org.jetbrains.annotations.NotNull PGPPublicKeyRingCollection publicKeyRings) { + return new VerifyWithImpl().verifyWith(publicKeyRings); + } + + @Override + public HandleMissingPublicKeys verifyWith(@org.jetbrains.annotations.NotNull Set trustedFingerprints, @org.jetbrains.annotations.NotNull PGPPublicKeyRingCollection publicKeyRings) { + return new VerifyWithImpl().verifyWith(trustedFingerprints, publicKeyRings); + } + + @Override + public HandleMissingPublicKeys verifyWith(@org.jetbrains.annotations.NotNull Set publicKeyRings) { + return new VerifyWithImpl().verifyWith(publicKeyRings); + } + + @Override + public Build doNotVerify() { + DecryptionBuilder.this.verificationKeys = null; + return new BuildImpl(); } } @@ -100,12 +171,6 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { DecryptionBuilder.this.verificationKeys = publicKeyRings; return new HandleMissingPublicKeysImpl(); } - - @Override - public Build doNotVerify() { - DecryptionBuilder.this.verificationKeys = null; - return new BuildImpl(); - } } class HandleMissingPublicKeysImpl implements HandleMissingPublicKeys { @@ -128,7 +193,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override public DecryptionStream build() throws IOException, PGPException { return DecryptionStreamFactory.create(inputStream, - decryptionKeys, decryptionKeyDecryptor, verificationKeys, missingPublicKeyCallback); + decryptionKeys, decryptionKeyDecryptor, detachedSignatures, verificationKeys, missingPublicKeyCallback); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index 0cd096be..2b7b1b2d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -18,12 +18,14 @@ package org.pgpainless.decryption_verification; import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -33,12 +35,21 @@ public interface DecryptionBuilderInterface { interface DecryptWith { - VerifyWith decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings); + Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings); - VerifyWith doNotDecrypt(); + Verify doNotDecrypt(); } + interface Verify extends VerifyWith { + + VerifyWith verifyDetachedSignature(InputStream inputStream) throws IOException, PGPException; + + VerifyWith verifyDetachedSignatures(List signatures); + + Build doNotVerify(); + } + interface VerifyWith { HandleMissingPublicKeys verifyWith(@Nonnull PGPPublicKeyRingCollection publicKeyRings); @@ -47,7 +58,6 @@ public interface DecryptionBuilderInterface { HandleMissingPublicKeys verifyWith(@Nonnull Set publicKeyRings); - Build doNotVerify(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index 2bad3724..366aafc9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -19,6 +19,8 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; +import org.bouncycastle.openpgp.PGPException; + public class DecryptionStream extends InputStream { private final InputStream inputStream; @@ -32,15 +34,36 @@ public class DecryptionStream extends InputStream { @Override public int read() throws IOException { - return inputStream.read(); + int r = inputStream.read(); + maybeUpdateDetachedSignatures(r); + return r; + } + + private void maybeUpdateDetachedSignatures(int rByte) { + for (DetachedSignature s : resultBuilder.getDetachedSignatures()) { + if (rByte != -1) { + s.getSignature().update((byte) rByte); + } + } } @Override public void close() throws IOException { inputStream.close(); + maybeVerifyDetachedSignatures(); this.isClosed = true; } + void maybeVerifyDetachedSignatures() { + for (DetachedSignature s : resultBuilder.getDetachedSignatures()) { + try { + s.setVerified(s.getSignature().verify()); + } catch (PGPException e) { + e.printStackTrace(); + } + } + } + public OpenPgpMetadata getResult() { if (!isClosed) { throw new IllegalStateException("DecryptionStream MUST be closed before the result can be accessed."); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index da5a2f0a..91ffc047 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -41,6 +42,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -66,7 +68,7 @@ public final class DecryptionStreamFactory { private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final PGPContentVerifierBuilderProvider verifierBuilderProvider = new BcPGPContentVerifierBuilderProvider(); private final KeyFingerPrintCalculator keyFingerprintCalculator = new BcKeyFingerprintCalculator(); - private final Map verifiableOnePassSignatures = new HashMap<>(); + private final Map verifiableOnePassSignatures = new HashMap<>(); private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, @@ -81,21 +83,31 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, @Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, + @Nullable List detachedSignatures, @Nullable Set verificationKeys, @Nullable MissingPublicKeyCallback missingPublicKeyCallback) throws IOException, PGPException { - - DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, verificationKeys, + InputStream pgpInputStream; + DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, verificationKeys, missingPublicKeyCallback); - PGPObjectFactory objectFactory = new PGPObjectFactory( - PGPUtil.getDecoderStream(inputStream), new BcKeyFingerprintCalculator()); - - return new DecryptionStream(factory.processPGPPackets(objectFactory), factory.resultBuilder); + if (detachedSignatures != null) { + pgpInputStream = inputStream; + for (PGPSignature signature : detachedSignatures) { + PGPPublicKey signingKey = factory.findSignatureVerificationKey(signature.getKeyID()); + signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); + factory.resultBuilder.addDetachedSignature( + new DetachedSignature(signature, new OpenPgpV4Fingerprint(signingKey))); + } + } else { + PGPObjectFactory objectFactory = new PGPObjectFactory( + PGPUtil.getDecoderStream(inputStream), new BcKeyFingerprintCalculator()); + pgpInputStream = factory.processPGPPackets(objectFactory); + } + return new DecryptionStream(pgpInputStream, factory.resultBuilder); } private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory) throws IOException, PGPException { - Object nextPgpObject; while ((nextPgpObject = objectFactory.nextObject()) != null) { if (nextPgpObject instanceof PGPEncryptedDataList) { @@ -214,18 +226,20 @@ public final class DecryptionStreamFactory { private void processOnePassSignature(PGPOnePassSignature signature) throws PGPException { final long keyId = signature.getKeyID(); - resultBuilder.addUnverifiedSignatureKeyId(keyId); LOGGER.log(LEVEL, "Message contains OnePassSignature from " + Long.toHexString(keyId)); // Find public key PGPPublicKey verificationKey = findSignatureVerificationKey(keyId); if (verificationKey == null) { + LOGGER.log(LEVEL, "Missing verification key from " + Long.toHexString(keyId)); return; } signature.init(verifierBuilderProvider, verificationKey); - verifiableOnePassSignatures.put(new OpenPgpV4Fingerprint(verificationKey), signature); + OnePassSignature onePassSignature = new OnePassSignature(signature, new OpenPgpV4Fingerprint(verificationKey)); + resultBuilder.addOnePassSignature(onePassSignature); + verifiableOnePassSignatures.put(new OpenPgpV4Fingerprint(verificationKey), onePassSignature); } private PGPPublicKey findSignatureVerificationKey(long keyId) { @@ -269,4 +283,5 @@ public final class DecryptionStreamFactory { return missingPublicKey; } + } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DetachedSignature.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DetachedSignature.java new file mode 100644 index 00000000..daf08007 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DetachedSignature.java @@ -0,0 +1,31 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +public class DetachedSignature { + private final PGPSignature signature; + private final OpenPgpV4Fingerprint fingerprint; + private boolean verified; + + public DetachedSignature(PGPSignature signature, OpenPgpV4Fingerprint fingerprint) { + this.signature = signature; + this.fingerprint = fingerprint; + } + + public void setVerified(boolean verified) { + this.verified = verified; + } + + public boolean isVerified() { + return verified; + } + + public PGPSignature getSignature() { + return signature; + } + + public OpenPgpV4Fingerprint getFingerprint() { + return fingerprint; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OnePassSignature.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OnePassSignature.java new file mode 100644 index 00000000..4d2cfccf --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OnePassSignature.java @@ -0,0 +1,42 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +public class OnePassSignature { + private final PGPOnePassSignature onePassSignature; + private final OpenPgpV4Fingerprint fingerprint; + private PGPSignature signature; + private boolean verified; + + public OnePassSignature(PGPOnePassSignature onePassSignature, OpenPgpV4Fingerprint fingerprint) { + this.onePassSignature = onePassSignature; + this.fingerprint = fingerprint; + } + + public boolean isVerified() { + return verified; + } + + public PGPOnePassSignature getOnePassSignature() { + return onePassSignature; + } + + public OpenPgpV4Fingerprint getFingerprint() { + return fingerprint; + } + + public boolean verify(PGPSignature signature) throws PGPException { + this.verified = getOnePassSignature().verify(signature); + if (verified) { + this.signature = signature; + } + return verified; + } + + public PGPSignature getSignature() { + return signature; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 7f9033e7..269478ac 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -15,8 +15,10 @@ */ package org.pgpainless.decryption_verification; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -32,11 +34,8 @@ public class OpenPgpMetadata { private final Set recipientKeyIds; private final OpenPgpV4Fingerprint decryptionFingerprint; - private final Set signatures; - private final Set signatureKeyIds; - private final Map verifiedSignatures; - private final Set verifiedSignaturesFingerprints; - + private final List onePassSignatures; + private final List detachedSignatures; private final SymmetricKeyAlgorithm symmetricKeyAlgorithm; private final CompressionAlgorithm compressionAlgorithm; private final boolean integrityProtected; @@ -46,20 +45,16 @@ public class OpenPgpMetadata { SymmetricKeyAlgorithm symmetricKeyAlgorithm, CompressionAlgorithm algorithm, boolean integrityProtected, - Set signatures, - Set signatureKeyIds, - Map verifiedSignatures, - Set verifiedSignaturesFingerprints) { + List onePassSignatures, + List detachedSignatures) { this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); this.decryptionFingerprint = decryptionFingerprint; this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; this.compressionAlgorithm = algorithm; this.integrityProtected = integrityProtected; - this.signatures = Collections.unmodifiableSet(signatures); - this.signatureKeyIds = Collections.unmodifiableSet(signatureKeyIds); - this.verifiedSignatures = Collections.unmodifiableMap(verifiedSignatures); - this.verifiedSignaturesFingerprints = Collections.unmodifiableSet(verifiedSignaturesFingerprints); + this.detachedSignatures = Collections.unmodifiableList(detachedSignatures); + this.onePassSignatures = Collections.unmodifiableList(onePassSignatures); } public Set getRecipientKeyIds() { @@ -87,27 +82,42 @@ public class OpenPgpMetadata { } public Set getSignatures() { + Set signatures = new HashSet<>(); + for (DetachedSignature detachedSignature : detachedSignatures) { + signatures.add(detachedSignature.getSignature()); + } + for (OnePassSignature onePassSignature : onePassSignatures) { + signatures.add(onePassSignature.getSignature()); + } return signatures; } - public Set getSignatureKeyIDs() { - return signatureKeyIds; - } - public boolean isSigned() { - return !signatureKeyIds.isEmpty(); + return !getSignatures().isEmpty(); } public Map getVerifiedSignatures() { + Map verifiedSignatures = new ConcurrentHashMap<>(); + for (DetachedSignature detachedSignature : detachedSignatures) { + if (detachedSignature.isVerified()) { + verifiedSignatures.put(detachedSignature.getFingerprint(), detachedSignature.getSignature()); + } + } + for (OnePassSignature onePassSignature : onePassSignatures) { + if (onePassSignature.isVerified()) { + verifiedSignatures.put(onePassSignature.getFingerprint(), onePassSignature.getSignature()); + } + } + return verifiedSignatures; } public Set getVerifiedSignatureKeyFingerprints() { - return verifiedSignaturesFingerprints; + return getVerifiedSignatures().keySet(); } public boolean isVerified() { - return !verifiedSignaturesFingerprints.isEmpty(); + return !getVerifiedSignatures().isEmpty(); } public boolean containsVerifiedSignatureFrom(PGPPublicKeyRing publicKeys) { @@ -121,7 +131,17 @@ public class OpenPgpMetadata { } public boolean containsVerifiedSignatureFrom(OpenPgpV4Fingerprint fingerprint) { - return verifiedSignaturesFingerprints.contains(fingerprint); + return getVerifiedSignatureKeyFingerprints().contains(fingerprint); + } + + public static class Signature { + protected final PGPSignature signature; + protected final OpenPgpV4Fingerprint fingerprint; + + public Signature(PGPSignature signature, OpenPgpV4Fingerprint fingerprint) { + this.signature = signature; + this.fingerprint = fingerprint; + } } public static Builder getBuilder() { @@ -132,10 +152,8 @@ public class OpenPgpMetadata { private final Set recipientFingerprints = new HashSet<>(); private OpenPgpV4Fingerprint decryptionFingerprint; - private final Set signatures = new HashSet<>(); - private final Set signatureKeyIds = new HashSet<>(); - private final Map verifiedSignatures = new ConcurrentHashMap<>(); - private final Set verifiedSignatureKeyFingerprints = new HashSet<>(); + private final List detachedSignatures = new ArrayList<>(); + private final List onePassSignatures = new ArrayList<>(); private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL; private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; private boolean integrityProtected = false; @@ -155,24 +173,8 @@ public class OpenPgpMetadata { return this; } - public Builder addSignature(PGPSignature signature) { - signatures.add(signature); - return this; - } - - public Builder addUnverifiedSignatureKeyId(Long keyId) { - this.signatureKeyIds.add(keyId); - return this; - } - - public Builder putVerifiedSignature(OpenPgpV4Fingerprint fingerprint, PGPSignature verifiedSignature) { - verifiedSignatures.put(fingerprint, verifiedSignature); - return this; - } - - public Builder addVerifiedSignatureFingerprint(OpenPgpV4Fingerprint fingerprint) { - this.verifiedSignatureKeyFingerprints.add(fingerprint); - return this; + public List getDetachedSignatures() { + return detachedSignatures; } public Builder setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { @@ -185,11 +187,18 @@ public class OpenPgpMetadata { return this; } + public void addDetachedSignature(DetachedSignature signature) { + this.detachedSignatures.add(signature); + } + + public void addOnePassSignature(OnePassSignature onePassSignature) { + this.onePassSignatures.add(onePassSignature); + } + public OpenPgpMetadata build() { return new OpenPgpMetadata(recipientFingerprints, decryptionFingerprint, symmetricKeyAlgorithm, compressionAlgorithm, integrityProtected, - signatures, signatureKeyIds, - verifiedSignatures, verifiedSignatureKeyFingerprints); + onePassSignatures, detachedSignatures); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java index ee2b5c29..0c3e51ca 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerifyingInputStream.java @@ -26,7 +26,6 @@ import java.util.logging.Logger; 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; import org.pgpainless.key.OpenPgpV4Fingerprint; @@ -37,14 +36,14 @@ public class SignatureVerifyingInputStream extends FilterInputStream { private static final Level LEVEL = Level.FINE; private final PGPObjectFactory objectFactory; - private final Map onePassSignatures; + private final Map onePassSignatures; private final OpenPgpMetadata.Builder resultBuilder; private boolean validated = false; protected SignatureVerifyingInputStream(@Nonnull InputStream inputStream, @Nonnull PGPObjectFactory objectFactory, - @Nonnull Map onePassSignatures, + @Nonnull Map onePassSignatures, @Nonnull OpenPgpMetadata.Builder resultBuilder) { super(inputStream); this.objectFactory = objectFactory; @@ -55,14 +54,14 @@ public class SignatureVerifyingInputStream extends FilterInputStream { } private void updateOnePassSignatures(byte data) { - for (PGPOnePassSignature signature : onePassSignatures.values()) { - signature.update(data); + for (OnePassSignature signature : onePassSignatures.values()) { + signature.getOnePassSignature().update(data); } } private void updateOnePassSignatures(byte[] b, int off, int len) { - for (PGPOnePassSignature signature : onePassSignatures.values()) { - signature.update(b, off, len); + for (OnePassSignature signature : onePassSignatures.values()) { + signature.getOnePassSignature().update(b, off, len); } } @@ -87,10 +86,8 @@ public class SignatureVerifyingInputStream extends FilterInputStream { try { for (PGPSignature signature : signatureList) { - resultBuilder.addSignature(signature); - OpenPgpV4Fingerprint fingerprint = findFingerprintForSignature(signature); - PGPOnePassSignature onePassSignature = findOnePassSignature(fingerprint); + OnePassSignature onePassSignature = findOnePassSignature(fingerprint); if (onePassSignature == null) { LOGGER.log(LEVEL, "Found Signature without respective OnePassSignature packet -> skip"); continue; @@ -103,17 +100,17 @@ public class SignatureVerifyingInputStream extends FilterInputStream { } } - private void verifySignatureOrThrowSignatureException(PGPSignature signature, OpenPgpV4Fingerprint fingerprint, PGPOnePassSignature onePassSignature) throws PGPException, SignatureException { + private void verifySignatureOrThrowSignatureException(PGPSignature signature, OpenPgpV4Fingerprint fingerprint, + OnePassSignature onePassSignature) + throws PGPException, SignatureException { if (onePassSignature.verify(signature)) { LOGGER.log(LEVEL, "Verified signature of key " + Long.toHexString(signature.getKeyID())); - resultBuilder.putVerifiedSignature(fingerprint, signature); - resultBuilder.addVerifiedSignatureFingerprint(fingerprint); } else { throw new SignatureException("Bad Signature of key " + signature.getKeyID()); } } - private PGPOnePassSignature findOnePassSignature(OpenPgpV4Fingerprint fingerprint) { + private OnePassSignature findOnePassSignature(OpenPgpV4Fingerprint fingerprint) { if (fingerprint != null) { return onePassSignatures.get(fingerprint); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java index ef11df65..d00cb804 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java @@ -20,7 +20,9 @@ import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -30,9 +32,12 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.jetbrains.annotations.NotNull; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.SecretKeyNotFoundException; +import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.selection.key.PublicKeySelectionStrategy; import org.pgpainless.key.selection.key.SecretKeySelectionStrategy; @@ -48,6 +53,7 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { private OutputStream outputStream; private final Set encryptionKeys = new HashSet<>(); + private boolean detachedSignature = false; private final Set signingKeys = new HashSet<>(); private SecretKeyRingProtector signingKeysDecryptor; private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_128; @@ -142,8 +148,8 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { } @Override - public SignWith doNotEncrypt() { - return new SignWithImpl(); + public DetachedSign doNotEncrypt() { + return new DetachedSignImpl(); } } @@ -216,25 +222,54 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { } @Override - public SignWith usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull CompressionAlgorithm compressionAlgorithm) { + public DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, + @Nonnull HashAlgorithm hashAlgorithm, + @Nonnull CompressionAlgorithm compressionAlgorithm) { EncryptionBuilder.this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; EncryptionBuilder.this.hashAlgorithm = hashAlgorithm; EncryptionBuilder.this.compressionAlgorithm = compressionAlgorithm; - return new SignWithImpl(); + return new DetachedSignImpl(); } @Override - public SignWith usingSecureAlgorithms() { + public DetachedSign usingSecureAlgorithms() { EncryptionBuilder.this.symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_256; EncryptionBuilder.this.hashAlgorithm = HashAlgorithm.SHA512; EncryptionBuilder.this.compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; + return new DetachedSignImpl(); + } + } + + class DetachedSignImpl implements DetachedSign { + + @Override + public SignWith createDetachedSignature() { + EncryptionBuilder.this.detachedSignature = true; return new SignWithImpl(); } + + @Override + public Armor doNotSign() { + return new ArmorImpl(); + } + + @Override + public Armor signWith(@org.jetbrains.annotations.NotNull SecretKeyRingProtector decryptor, @org.jetbrains.annotations.NotNull PGPSecretKey... keys) { + return new SignWithImpl().signWith(decryptor, keys); + } + + @Override + public Armor signWith(@org.jetbrains.annotations.NotNull SecretKeyRingProtector decryptor, @org.jetbrains.annotations.NotNull PGPSecretKeyRing... keyRings) { + return new SignWithImpl().signWith(decryptor, keyRings); + } + + @Override + public Armor signWith(@org.jetbrains.annotations.NotNull SecretKeyRingSelectionStrategy selectionStrategy, @org.jetbrains.annotations.NotNull SecretKeyRingProtector decryptor, @org.jetbrains.annotations.NotNull MultiMap keys) throws SecretKeyNotFoundException { + return new SignWithImpl().signWith(selectionStrategy, decryptor, keys); + } } class SignWithImpl implements SignWith { @@ -296,11 +331,6 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { } return new ArmorImpl(); } - - @Override - public Armor doNotSign() { - return new ArmorImpl(); - } } class ArmorImpl implements Armor { @@ -319,14 +349,16 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { private EncryptionStream build() throws IOException, PGPException { - Set privateKeys = new HashSet<>(); + Map privateKeys = new ConcurrentHashMap<>(); for (PGPSecretKey secretKey : signingKeys) { - privateKeys.add(secretKey.extractPrivateKey(signingKeysDecryptor.getDecryptor(secretKey.getKeyID()))); + privateKeys.put(new OpenPgpV4Fingerprint(secretKey), + secretKey.extractPrivateKey(signingKeysDecryptor.getDecryptor(secretKey.getKeyID()))); } return new EncryptionStream( EncryptionBuilder.this.outputStream, EncryptionBuilder.this.encryptionKeys, + EncryptionBuilder.this.detachedSignature, privateKeys, EncryptionBuilder.this.symmetricKeyAlgorithm, EncryptionBuilder.this.hashAlgorithm, diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java index e0f79a36..ddd35c60 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java @@ -50,7 +50,7 @@ public interface EncryptionBuilderInterface { WithAlgorithms toRecipients(@Nonnull PublicKeyRingSelectionStrategy selectionStrategy, @Nonnull MultiMap keys); - SignWith doNotEncrypt(); + DetachedSign doNotEncrypt(); } @@ -65,11 +65,18 @@ public interface EncryptionBuilderInterface { WithAlgorithms andToSelf(@Nonnull PublicKeyRingSelectionStrategy selectionStrategy, @Nonnull MultiMap keys); - SignWith usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, + DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull CompressionAlgorithm compressionAlgorithm); - SignWith usingSecureAlgorithms(); + DetachedSign usingSecureAlgorithms(); + + } + + interface DetachedSign extends SignWith { + SignWith createDetachedSignature(); + + Armor doNotSign(); } @@ -84,8 +91,6 @@ public interface EncryptionBuilderInterface { @Nonnull MultiMap keys) throws SecretKeyNotFoundException; - Armor doNotSign(); - } interface Armor { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index d906f64e..872abb90 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -21,8 +21,11 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,15 +38,19 @@ import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.DetachedSignature; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.OpenPgpV4Fingerprint; /** * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. @@ -60,12 +67,13 @@ public final class EncryptionStream extends OutputStream { private final HashAlgorithm hashAlgorithm; private final CompressionAlgorithm compressionAlgorithm; private final Set encryptionKeys; - private final Set signingKeys; + private final boolean detachedSignature; + private final Map signingKeys; private final boolean asciiArmor; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private List signatureGenerators = new ArrayList<>(); + private Map signatureGenerators = new ConcurrentHashMap<>(); private boolean closed = false; OutputStream outermostStream = null; @@ -81,7 +89,8 @@ public final class EncryptionStream extends OutputStream { EncryptionStream(@Nonnull OutputStream targetOutputStream, @Nonnull Set encryptionKeys, - @Nonnull Set signingKeys, + boolean detachedSignature, + @Nonnull Map signingKeys, @Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull CompressionAlgorithm compressionAlgorithm, @@ -92,7 +101,8 @@ public final class EncryptionStream extends OutputStream { this.hashAlgorithm = hashAlgorithm; this.compressionAlgorithm = compressionAlgorithm; this.encryptionKeys = Collections.unmodifiableSet(encryptionKeys); - this.signingKeys = Collections.unmodifiableSet(signingKeys); + this.detachedSignature = detachedSignature; + this.signingKeys = Collections.unmodifiableMap(signingKeys); this.asciiArmor = asciiArmor; outermostStream = targetOutputStream; @@ -144,14 +154,15 @@ public final class EncryptionStream extends OutputStream { } LOGGER.log(LEVEL, "At least one signing key is available -> sign " + hashAlgorithm + " hash of message"); - for (PGPPrivateKey privateKey : signingKeys) { - LOGGER.log(LEVEL, "Sign using key " + Long.toHexString(privateKey.getKeyID())); + for (OpenPgpV4Fingerprint fingerprint : signingKeys.keySet()) { + PGPPrivateKey privateKey = signingKeys.get(fingerprint); + LOGGER.log(LEVEL, "Sign using key " + fingerprint); BcPGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( privateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getAlgorithmId()); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); - signatureGenerators.add(signatureGenerator); + signatureGenerators.put(fingerprint, signatureGenerator); } } @@ -163,7 +174,7 @@ public final class EncryptionStream extends OutputStream { } private void prepareOnePassSignatures() throws IOException, PGPException { - for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { + for (PGPSignatureGenerator signatureGenerator : signatureGenerators.values()) { signatureGenerator.generateOnePassVersion(false).encode(basicCompressionStream); } } @@ -186,7 +197,7 @@ public final class EncryptionStream extends OutputStream { public void write(int data) throws IOException { literalDataStream.write(data); - for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { + for (PGPSignatureGenerator signatureGenerator : signatureGenerators.values()) { byte asByte = (byte) (data & 0xff); signatureGenerator.update(asByte); } @@ -201,7 +212,7 @@ public final class EncryptionStream extends OutputStream { @Override public void write(byte[] buffer, int off, int len) throws IOException { literalDataStream.write(buffer, 0, len); - for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { + for (PGPSignatureGenerator signatureGenerator : signatureGenerators.values()) { signatureGenerator.update(buffer, 0, len); } } @@ -242,12 +253,14 @@ public final class EncryptionStream extends OutputStream { } private void writeSignatures() throws IOException { - for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { + for (OpenPgpV4Fingerprint fingerprint : signatureGenerators.keySet()) { + PGPSignatureGenerator signatureGenerator = signatureGenerators.get(fingerprint); try { PGPSignature signature = signatureGenerator.generate(); - signature.encode(basicCompressionStream); - resultBuilder.addSignature(signature); - resultBuilder.addUnverifiedSignatureKeyId(signature.getKeyID()); + if (!detachedSignature) { + signature.encode(basicCompressionStream); + } + resultBuilder.addDetachedSignature(new DetachedSignature(signature, fingerprint)); } catch (PGPException e) { throw new IOException(e); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 38f30746..838e230b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -80,7 +80,7 @@ public class DecryptAndVerifyMessageTest { assertTrue(metadata.isVerified()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); - assertEquals(1, metadata.getSignatureKeyIDs().size()); + //assertEquals(1, metadata.getSignatureKeyIDs().size()); assertEquals(1, metadata.getVerifiedSignatureKeyFingerprints().size()); assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(TestKeys.JULIET_FINGERPRINT, metadata.getDecryptionFingerprint()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 8dc1703c..e079adf1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -26,11 +26,15 @@ import java.io.IOException; import java.nio.charset.Charset; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Set; import java.util.logging.Logger; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.Test; import org.pgpainless.PGPainless; @@ -38,6 +42,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.collection.PGPKeyRing; import org.pgpainless.key.generation.KeySpec; @@ -145,9 +150,9 @@ public class EncryptDecryptTest { OpenPgpMetadata encryptionResult = encryptor.getResult(); - assertFalse(encryptionResult.getSignatureKeyIDs().isEmpty()); - for (long keyId : encryptionResult.getSignatureKeyIDs()) { - assertTrue(BCUtil.keyRingContainsKeyWithId(senderPub, keyId)); + assertFalse(encryptionResult.getSignatures().isEmpty()); + for (OpenPgpV4Fingerprint fingerprint : encryptionResult.getVerifiedSignatures().keySet()) { + assertTrue(BCUtil.keyRingContainsKeyWithId(senderPub, fingerprint.getKeyId())); } assertFalse(encryptionResult.getRecipientKeyIds().isEmpty()); @@ -180,4 +185,44 @@ public class EncryptDecryptTest { assertTrue(result.isEncrypted()); assertTrue(result.isVerified()); } + + @Test + public void testDetachedSignatureCreationAndVerification() throws IOException, PGPException { + PGPKeyRing signingKeys = new PGPKeyRing(TestKeys.getJulietPublicKeyRing(), TestKeys.getJulietSecretKeyRing()); + SecretKeyRingProtector keyRingProtector = new UnprotectedKeysProtector(); + byte[] data = testMessage.getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + ByteArrayOutputStream dummyOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.createEncryptor().onOutputStream(dummyOut) + .doNotEncrypt() + .createDetachedSignature() + .signWith(keyRingProtector, signingKeys.getSecretKeys()) + .noArmor(); + Streams.pipeAll(inputStream, signer); + signer.close(); + OpenPgpMetadata metadata = signer.getResult(); + + Set signatureSet = metadata.getSignatures(); + ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(sigOut); + signatureSet.iterator().next().encode(armorOut); + armorOut.close(); + String armorSig = sigOut.toString(); + + System.out.println(armorSig); + + inputStream = new ByteArrayInputStream(testMessage.getBytes()); + DecryptionStream verifier = PGPainless.createDecryptor().onInputStream(inputStream) + .doNotDecrypt() + .verifyDetachedSignature(new ByteArrayInputStream(armorSig.getBytes())) + .verifyWith(Collections.singleton(signingKeys.getPublicKeys())) + .ignoreMissingPublicKeys() + .build(); + dummyOut = new ByteArrayOutputStream(); + Streams.pipeAll(verifier, dummyOut); + verifier.close(); + + metadata = verifier.getResult(); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + } }