From c55fd2e5522e0e25e5e5fd75e173c2f0a05b72e6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Oct 2021 14:58:17 +0200 Subject: [PATCH] Implement decryption with - and access of session keys --- .../ConsumerOptions.java | 19 +- .../DecryptionStreamFactory.java | 61 ++++++- .../OpenPgpMetadata.java | 27 +-- .../CleartextSignatureProcessor.java | 2 - .../BcImplementationFactory.java | 8 + .../implementation/ImplementationFactory.java | 4 + .../JceImplementationFactory.java | 8 + .../java/org/pgpainless/util/SessionKey.java | 35 ++++ .../java/org/pgpainless/sop/DecryptImpl.java | 22 ++- .../sop/EncryptDecryptRoundTripTest.java | 163 ++++++++++++++++++ sop-java/src/main/java/sop/SessionKey.java | 15 ++ .../test/java/sop/util/SessionKeyTest.java | 7 + 12 files changed, 334 insertions(+), 37 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index a80d1fee..436d9356 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -24,10 +24,10 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; /** * Options for decryption and signature verification. @@ -46,7 +46,7 @@ public class ConsumerOptions { private MissingPublicKeyCallback missingCertificateCallback = null; // Session key for decryption without passphrase/key - private byte[] sessionKey = null; + private SessionKey sessionKey = null; private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -162,16 +162,15 @@ public class ConsumerOptions { * Attempt decryption using a session key. * * Note: PGPainless does not yet support decryption with session keys. - * TODO: Add support for decryption using session key. * * @see RFC4880 on Session Keys * * @param sessionKey session key * @return options */ - public ConsumerOptions setSessionKey(@Nonnull byte[] sessionKey) { + public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) { this.sessionKey = sessionKey; - throw new NotYetImplementedException(); + return this; } /** @@ -179,14 +178,8 @@ public class ConsumerOptions { * * @return session key or null */ - public @Nullable byte[] getSessionKey() { - if (sessionKey == null) { - return null; - } - - byte[] sk = new byte[sessionKey.length]; - System.arraycopy(sessionKey, 0, sk, 0, sessionKey.length); - return sk; + public @Nullable SessionKey getSessionKey() { + return sessionKey; } /** 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 560a4d39..459bfb8d 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 @@ -34,12 +34,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -63,6 +65,7 @@ import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.CRCingArmoredInputStreamWrapper; import org.pgpainless.util.PGPUtilWrapper; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -210,12 +213,53 @@ public final class DecryptionStreamFactory { private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) throws PGPException, IOException { LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); + + SessionKey sessionKey = options.getSessionKey(); + if (sessionKey != null) { + integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); + InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream); + PGPObjectFactory factory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator); + return processPGPPackets(factory, ++depth); + } + InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); PGPObjectFactory factory = new PGPObjectFactory(decodedDataStream, keyFingerprintCalculator); return processPGPPackets(factory, ++depth); } + private IntegrityProtectedInputStream decryptWithProvidedSessionKey(PGPEncryptedDataList pgpEncryptedDataList, SessionKey sessionKey) throws PGPException { + PGPSessionKey pgpSessionKey = new PGPSessionKey(sessionKey.getAlgorithm().getAlgorithmId(), sessionKey.getKey()); + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().provideSessionKeyDataDecryptorFactory(pgpSessionKey); + InputStream decryptedDataStream = null; + PGPEncryptedData encryptedData = null; + for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { + encryptedData = pgpEncryptedData; + if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + if (encryptedData instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData; + decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory); + break; + } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData; + decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory); + break; + } + } + + if (decryptedDataStream == null) { + throw new PGPException("No valid PGP data encountered."); + } + + resultBuilder.setSessionKey(sessionKey); + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options); + return integrityProtectedEncryptedInputStream; + } + private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) throws PGPException, IOException { CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.fromId(pgpCompressedData.getAlgorithm()); @@ -294,10 +338,11 @@ public final class DecryptionStreamFactory { try { InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( - pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); + + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); @@ -454,15 +499,17 @@ public final class DecryptionStreamFactory { PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(decryptionKey); - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm - .fromId(encryptedSessionKey.getSymmetricAlgorithm(dataDecryptor)); + PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); + + SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm(); if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { LOGGER.debug("Message is unencrypted"); } else { LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); } throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); return integrityProtectedEncryptedInputStream; 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 9dfc8076..c0d8284f 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 @@ -25,6 +25,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.SessionKey; public class OpenPgpMetadata { @@ -34,7 +35,7 @@ public class OpenPgpMetadata { private final List invalidInbandSignatures; private final List verifiedDetachedSignatures; private final List invalidDetachedSignatures; - private final SymmetricKeyAlgorithm symmetricKeyAlgorithm; + private final SessionKey sessionKey; private final CompressionAlgorithm compressionAlgorithm; private final String fileName; private final Date modificationDate; @@ -42,7 +43,7 @@ public class OpenPgpMetadata { public OpenPgpMetadata(Set recipientKeyIds, SubkeyIdentifier decryptionKey, - SymmetricKeyAlgorithm symmetricKeyAlgorithm, + SessionKey sessionKey, CompressionAlgorithm algorithm, List verifiedInbandSignatures, List invalidInbandSignatures, @@ -54,7 +55,7 @@ public class OpenPgpMetadata { this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); this.decryptionKey = decryptionKey; - this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + this.sessionKey = sessionKey; this.compressionAlgorithm = algorithm; this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures); this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures); @@ -80,7 +81,7 @@ public class OpenPgpMetadata { * @return true if encrypted, false otherwise */ public boolean isEncrypted() { - return symmetricKeyAlgorithm != SymmetricKeyAlgorithm.NULL && !getRecipientKeyIds().isEmpty(); + return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL && !getRecipientKeyIds().isEmpty(); } /** @@ -100,7 +101,11 @@ public class OpenPgpMetadata { * @return encryption algorithm */ public @Nullable SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { - return symmetricKeyAlgorithm; + return sessionKey == null ? null : sessionKey.getAlgorithm(); + } + + public @Nullable SessionKey getSessionKey() { + return sessionKey; } /** @@ -271,8 +276,8 @@ public class OpenPgpMetadata { public static class Builder { private final Set recipientFingerprints = new HashSet<>(); + private SessionKey sessionKey; private SubkeyIdentifier decryptionKey; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.NULL; private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; private String fileName; private StreamEncoding fileEncoding; @@ -294,13 +299,13 @@ public class OpenPgpMetadata { return this; } - public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; + public Builder setSessionKey(SessionKey sessionKey) { + this.sessionKey = sessionKey; return this; } - public Builder setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { - this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; + public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { + this.compressionAlgorithm = algorithm; return this; } @@ -322,7 +327,7 @@ public class OpenPgpMetadata { public OpenPgpMetadata build() { return new OpenPgpMetadata( recipientFingerprints, decryptionKey, - symmetricKeyAlgorithm, compressionAlgorithm, + sessionKey, compressionAlgorithm, verifiedInbandSignatures, invalidInbandSignatures, verifiedDetachedSignatures, invalidDetachedSignatures, fileName, modificationDate, fileEncoding); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java index 352321ca..26e33a96 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java @@ -14,7 +14,6 @@ import org.bouncycastle.openpgp.PGPSignatureList; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; @@ -58,7 +57,6 @@ public class CleartextSignatureProcessor { public DecryptionStream getVerificationStream() throws IOException, PGPException { OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setSymmetricKeyAlgorithm(SymmetricKeyAlgorithm.NULL) .setFileEncoding(StreamEncoding.TEXT); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java index ea9b0eb0..0be562a2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -14,6 +14,7 @@ import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; @@ -25,6 +26,7 @@ import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; @@ -38,6 +40,7 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; @@ -137,6 +140,11 @@ public class BcImplementationFactory extends ImplementationFactory { .build(passphrase.getChars()); } + @Override + public SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { + return new BcSessionKeyDataDecryptorFactory(sessionKey); + } + private AsymmetricCipherKeyPair jceToBcKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java index 774dcad0..50937a30 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -12,6 +12,7 @@ import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; @@ -24,6 +25,7 @@ import org.bouncycastle.openpgp.operator.PGPDigestCalculator; import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; @@ -103,6 +105,8 @@ public abstract class ImplementationFactory { HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException; + public abstract SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey); + @Override public String toString() { return getClass().getSimpleName(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java index cbacdf1b..c480ed10 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -12,6 +12,7 @@ import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; @@ -24,6 +25,7 @@ import org.bouncycastle.openpgp.operator.PGPDigestCalculator; import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; @@ -36,6 +38,7 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; @@ -124,4 +127,9 @@ public class JceImplementationFactory extends ImplementationFactory { .setProvider(ProviderFactory.getProvider()) .build(passphrase.getChars()); } + + @Override + public SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { + return new JceSessionKeyDataDecryptorFactoryBuilder().build(sessionKey); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java new file mode 100644 index 00000000..cea8639d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPSessionKey; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; + +public class SessionKey { + + private SymmetricKeyAlgorithm algorithm; + private byte[] key; + + public SessionKey(@Nonnull PGPSessionKey sessionKey) { + this(SymmetricKeyAlgorithm.fromId(sessionKey.getAlgorithm()), sessionKey.getKey()); + } + + public SessionKey(@Nonnull SymmetricKeyAlgorithm algorithm, @Nonnull byte[] key) { + this.algorithm = algorithm; + this.key = key; + } + + public SymmetricKeyAlgorithm getAlgorithm() { + return algorithm; + } + + public byte[] getKey() { + byte[] copy = new byte[key.length]; + System.arraycopy(key, 0, copy, 0, copy.length); + return copy; + } +} 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 7298065e..606f8673 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -18,6 +18,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; @@ -67,7 +68,11 @@ public class DecryptImpl implements Decrypt { @Override public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption("Setting custom session key not supported."); + consumerOptions.setSessionKey( + new org.pgpainless.util.SessionKey( + SymmetricKeyAlgorithm.fromId(sessionKey.getAlgorithm()), + sessionKey.getKey())); + return this; } @Override @@ -118,8 +123,8 @@ public class DecryptImpl implements Decrypt { throws SOPGPException.BadData, SOPGPException.MissingArg { - if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty()) { - throw new SOPGPException.MissingArg("Missing decryption key or passphrase."); + if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { + throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); } DecryptionStream decryptionStream; @@ -153,7 +158,16 @@ public class DecryptImpl implements Decrypt { } } - return new DecryptionResult(null, verificationList); + SessionKey sessionKey = null; + if (metadata.getSessionKey() != null) { + org.pgpainless.util.SessionKey sk = metadata.getSessionKey(); + sessionKey = new SessionKey( + (byte) sk.getAlgorithm().getAlgorithmId(), + sk.getKey() + ); + } + + return new DecryptionResult(sessionKey, verificationList); } }; } 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 f6f6c235..807c9dd3 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -18,6 +19,7 @@ import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.SOP; +import sop.SessionKey; import sop.exception.SOPGPException; public class EncryptDecryptRoundTripTest { @@ -235,4 +237,165 @@ public class EncryptDecryptRoundTripTest { assertThrows(SOPGPException.BadData.class, () -> sop.decrypt() .verifyWithCert(new byte[0])); } + + @Test + public void testPassphraseDecryptionYieldsSessionKey() throws IOException { + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCdFswArqHpj1g0j4BLDTkZhCC1crZf0EFq1xPIMUtnyRmfJJ7IzsdMJ5Y\n" + + "EhKbBc2h6wIX7B/GxUbyNj1xh5JRzt2ZX8KL2d6HAQ==\n" + + "=zZ0/\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + String passphrase = "sw0rdf1sh"; + ByteArrayAndResult bytesAndResult = sop.decrypt().withPassword(passphrase).ciphertext(ciphertext).toByteArrayAndResult(); + assertArrayEquals(message, bytesAndResult.getBytes()); + assertTrue(bytesAndResult.getResult().getSessionKey().isPresent()); + assertEquals("9:7BCB7383D23E20D4BA8980B26D6C0813769056546C45B7E55F4612BFAD5B4B1C", bytesAndResult.getResult().getSessionKey().get().toString()); + } + + @Test + public void testPublicKeyDecryptionYieldsSessionKey() throws IOException { + byte[] key = ("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: D94A AA9C 5F73 48B2 5D81 72E7 F20C F71E 93FE 897F\n" + + "Comment: Alice\n" + + "\n" + + "lFgEYWlyaRYJKwYBBAHaRw8BAQdAJsJfjByLE+8HNVGbEKiIbSGXBYwR6L61bT5E\n" + + "Hhu642kAAP49D4TOaI+Z3G5ko4C4D1bOzLajLRpIuPLuwYHpF1xD0RHmtAVBbGlj\n" + + "ZYh4BBMWCgAgBQJhaXJpAhsBBRYCAwEABRUKCQgLBAsJCAcCHgECGQEACgkQ8gz3\n" + + "HpP+iX/c8AD9Hx0PUu97n8ZlrpuA6YuJL3rONPQnaXMz9eE+KHxJS6sBAM06X8Wm\n" + + "XRGUVURsoerwYTbUnXcUnqH/U/JhwlUerJAInF0EYWlyaRIKKwYBBAGXVQEFAQEH\n" + + "QJOHyxI5K8ZqX+v/AmTLHAIjWd8wHO8eGld4KHniCFx9AwEIBwAA/0zVZYYWsr3w\n" + + "GKkmqfIZlB+wIeJlWrho87kvXiNAe0LIEIGIdQQYFgoAHQUCYWlyaQIbDAUWAgMB\n" + + "AAUVCgkICwQLCQgHAh4BAAoJEPIM9x6T/ol/vggA/ilxi5UTjDYDR7sGrYyaGPRK\n" + + "Sg0KNn2SV4c5M5ZmZR7sAP4kKz6kQ4UtYmSmUmMBu+A3mMTN8VQY+6LSTdekvU0N\n" + + "ApxYBGFpcmkWCSsGAQQB2kcPAQEHQJiiZENQ52jyt8wBwX7fD1vQkvgTg5T3v1S1\n" + + "yzr1yI0RAAD+KOTcMdv8rz3U6K42PNE4b983KoMfyQ/hgjIWOi2BYBwP94jVBBgW\n" + + "CgB9BQJhaXJpAhsCBRYCAwEABRUKCQgLBAsJCAcCHgFfIAQZFgoABgUCYWlyaQAK\n" + + "CRDP7lemqmadIYLuAP9oAm+OFzyMNrmWRcvdHqH/DAfJTM2+ZmANSm44geZDEAD9\n" + + "HfeCHev1H1H1wOd0S3tW9gZwonrYFoqOBW/YTmf5XwYACgkQ8gz3HpP+iX+veQEA\n" + + "sWC+xDo+lc6oJr4q0mTJkxzYfgUBtQ0VjUWNcGyOdegBAL8hMzb9+e4wbP2F0tMb\n" + + "ZFA2MgHsvqGhXyAXi50arZYF\n" + + "=k66N\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n").getBytes(StandardCharsets.UTF_8); + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DrJ3c2YF1IKUSAQdA9VL6OwIwOwB4GnE4yR5JJ5OjcC76WTpdm85I6WHvhD4w\n" + + "hqHpf6UGaDDQ7xAcSd7YnEGVMBOOBnJfD1PRuNWE5hwgqqsqpMDrvvMHjUsg3HNH\n" + + "0j4BriMU8XQ6MLdvCaFmeQqFwBD4mlI/x32wj0I9VyBIKysopA8HNV4ES2rOhGuW\n" + + "T/zFmI9Tm9eWvNwv0LUNhQ==\n" + + "=4Z+m\n" + + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); + + ByteArrayAndResult bytesAndResult = sop.decrypt().withKey(key).ciphertext(ciphertext).toByteArrayAndResult(); + DecryptionResult result = bytesAndResult.getResult(); + assertArrayEquals(message, bytesAndResult.getBytes()); + assertTrue(result.getSessionKey().isPresent()); + assertEquals("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE", result.getSessionKey().get().toString()); + } + + @Test + public void testDecryptionWithSessionKey() throws IOException { + byte[] message = "Hello\nWorld\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DrJ3c2YF1IKUSAQdA9VL6OwIwOwB4GnE4yR5JJ5OjcC76WTpdm85I6WHvhD4w\n" + + "hqHpf6UGaDDQ7xAcSd7YnEGVMBOOBnJfD1PRuNWE5hwgqqsqpMDrvvMHjUsg3HNH\n" + + "0j4BriMU8XQ6MLdvCaFmeQqFwBD4mlI/x32wj0I9VyBIKysopA8HNV4ES2rOhGuW\n" + + "T/zFmI9Tm9eWvNwv0LUNhQ==\n" + + "=4Z+m\n" + + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); + SessionKey sessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); + + ByteArrayAndResult bytesAndResult = sop.decrypt().withSessionKey(sessionKey) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + DecryptionResult result = bytesAndResult.getResult(); + assertTrue(result.getSessionKey().isPresent()); + assertEquals(sessionKey, result.getSessionKey().get()); + + assertArrayEquals(message, bytesAndResult.getBytes()); + } + + @Test + public void testDecryptionWithSessionKey_VerificationWithCert() throws IOException { + byte[] plaintext = "This is a test message.\nSit back and relax.\n".getBytes(StandardCharsets.UTF_8); + byte[] key = ("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9C26 EFAB 1C65 00A2 28E8 A9C2 658E E420 C824 D191\n" + + "Comment: Alice\n" + + "\n" + + "lFgEYWl4ixYJKwYBBAHaRw8BAQdAv6+cd8R/ICS/z9hlT99g++wyquxVsO0FCb8F\n" + + "MSkTplUAAP9gPoBi8fxdfLaEyt6GWeIBTeYsVxsogbKzXXnjp3MbiRE/tAVBbGlj\n" + + "ZYh4BBMWCgAgBQJhaXiLAhsBBRYCAwEABRUKCQgLBAsJCAcCHgECGQEACgkQZY7k\n" + + "IMgk0ZEZuAEA3hWzfqCXGUjlv+miWey1AyWRu9eQvTdE9YqbIMuxIk4BAMtGlo6l\n" + + "d3E868q0zLOOktmsBxnzaE7knbd9nAlK3FUJnF0EYWl4ixIKKwYBBAGXVQEFAQEH\n" + + "QK8vS3T3Yf3Gpy9iWOTR0jdhV4XgtchcvKCpFMgc5uwFAwEIBwAA/1tNle5cT9kS\n" + + "8yzNxL16ElEREtEX+5kpkt6JZyTx0xfAEPGIdQQYFgoAHQUCYWl4iwIbDAUWAgMB\n" + + "AAUVCgkICwQLCQgHAh4BAAoJEGWO5CDIJNGRM80BANJ6EGKIkVNxYj7wOaEqyRh1\n" + + "Rtv3tLAnEzLl/b0mZx3WAQDADAPNCl5xnjTt5InyfrwV90kM4vDGcl4mQE8FD7dD\n" + + "B5xYBGFpeIsWCSsGAQQB2kcPAQEHQFuEaBKUllw+MfdkkSNE0CncJCeFGCbHvmsc\n" + + "Ma/DPgrpAAEAlsoxcTyTFfHxV2CayDCFvBSHYXOSOg6fyMdh0SxzjC0PVIjVBBgW\n" + + "CgB9BQJhaXiLAhsCBRYCAwEABRUKCQgLBAsJCAcCHgFfIAQZFgoABgUCYWl4iwAK\n" + + "CRBGMq3j1oKUXenjAP974AvBOAVIdNUkVAishoDL7ee7/eAU3Ni7V2Kn47cusQD/\n" + + "c8c9phtf2NIL23K4bvBdvsU3opV2DIVJwRutV4v6jgAACgkQZY7kIMgk0ZG1dwEA\n" + + "sFp1AuPcn3dGF05D6ohlqunoBwBWEcwZLjx+v5X27R8A/17V5nzC+eny3XjCF8Ib\n" + + "qw1VTfR84stki65Xhm2lxFAN\n" + + "=TQO7\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n").getBytes(StandardCharsets.UTF_8); + byte[] cert = sop.extractCert().key(key).getBytes(); + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DSjXDMRql2RASAQdAkhJyA9GX5ios8PNlti7v7BieggiiR9trqrKFQwomU2Aw\n" + + "elEFuDA3ugJO2rNiyQQH1riFFJuod6BiQuxFhdf/mmsFFDzHmJeUOx9pQeNemzST\n" + + "0sAdAQQYC+iXUNn2y15kTqbFQFgfOWObgsqspGY04V17fZdVI7bEORLM+YT6KoZA\n" + + "uq2WO49ze9jp2jdvTsjjNNseZDhmxtgOCfi1/Fi3IHPnBJW7M3UWaJCSLozWkO95\n" + + "FztCSWL22jDGPGIjgQ589hYW+WuJMvMv6ltTOo+l70S5dHSObijbcOqfNSmrxlpw\n" + + "hqZfkU0BA01I9Pf3lBPCNyMbCPZP0oaIiWACnm6svWp4oH5u5ClhS9BVJTptzwXv\n" + + "mMj+lTi5ahGQJ3Nr8krloTSsjpkssz6D2+FDnvjwu6E=\n" + + "=BYOB\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + String sessionKey = "9:87C0870598AD908ABEECCAE265DCEEA146CF557AAF698D097024404A00EBD072"; + + // Decrypt with public key + ByteArrayAndResult bytesAndResult = + sop.decrypt().withKey(key).verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); + assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); + assertArrayEquals(plaintext, bytesAndResult.getBytes()); + assertEquals(1, bytesAndResult.getResult().getVerifications().size()); + + // Decrypt with session key + bytesAndResult = sop.decrypt().withSessionKey(SessionKey.fromString(sessionKey)) + .verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); + assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); + assertArrayEquals(plaintext, bytesAndResult.getBytes()); + assertEquals(1, bytesAndResult.getResult().getVerifications().size()); + } + + @Test + public void decryptWithWrongSessionKey() { + byte[] ciphertext = ("-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DSjXDMRql2RASAQdAkhJyA9GX5ios8PNlti7v7BieggiiR9trqrKFQwomU2Aw\n" + + "elEFuDA3ugJO2rNiyQQH1riFFJuod6BiQuxFhdf/mmsFFDzHmJeUOx9pQeNemzST\n" + + "0sAdAQQYC+iXUNn2y15kTqbFQFgfOWObgsqspGY04V17fZdVI7bEORLM+YT6KoZA\n" + + "uq2WO49ze9jp2jdvTsjjNNseZDhmxtgOCfi1/Fi3IHPnBJW7M3UWaJCSLozWkO95\n" + + "FztCSWL22jDGPGIjgQ589hYW+WuJMvMv6ltTOo+l70S5dHSObijbcOqfNSmrxlpw\n" + + "hqZfkU0BA01I9Pf3lBPCNyMbCPZP0oaIiWACnm6svWp4oH5u5ClhS9BVJTptzwXv\n" + + "mMj+lTi5ahGQJ3Nr8krloTSsjpkssz6D2+FDnvjwu6E=\n" + + "=BYOB\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + SessionKey wrongSessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); + + assertThrows(SOPGPException.BadData.class, () -> + sop.decrypt().withSessionKey(wrongSessionKey).ciphertext(ciphertext)); + } } diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java index 2cb054d0..2adcec4d 100644 --- a/sop-java/src/main/java/sop/SessionKey.java +++ b/sop-java/src/main/java/sop/SessionKey.java @@ -5,11 +5,15 @@ package sop; import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import sop.util.HexUtil; public class SessionKey { + private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9a-fA-F]+)$"); + private final byte algorithm; private final byte[] sessionKey; @@ -57,6 +61,17 @@ public class SessionKey { return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey()); } + public static SessionKey fromString(String string) { + Matcher matcher = PATTERN.matcher(string); + if (!matcher.matches()) { + throw new IllegalArgumentException("Provided session key does not match expected format."); + } + byte algorithm = Byte.parseByte(matcher.group(1)); + String key = matcher.group(2); + + return new SessionKey(algorithm, HexUtil.hexToBytes(key)); + } + @Override public String toString() { return "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey); diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/util/SessionKeyTest.java index b79fd81b..2d03279d 100644 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ b/sop-java/src/test/java/sop/util/SessionKeyTest.java @@ -12,6 +12,13 @@ import sop.SessionKey; public class SessionKeyTest { + @Test + public void fromStringTest() { + String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; + SessionKey sessionKey = SessionKey.fromString(string); + assertEquals(string, sessionKey.toString()); + } + @Test public void toStringTest() { SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"));