diff --git a/pom.xml b/pom.xml index a51500a..c94987b 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,12 @@ bcprov-jdk18on 1.81 + + + org.bouncycastle + bcpg-jdk18on + 1.81 + com.fasterxml.jackson.core diff --git a/src/main/java/de/usd/cstchef/Utils.java b/src/main/java/de/usd/cstchef/Utils.java index 9b327c6..0869e58 100644 --- a/src/main/java/de/usd/cstchef/Utils.java +++ b/src/main/java/de/usd/cstchef/Utils.java @@ -87,6 +87,8 @@ import de.usd.cstchef.operations.encryption.AesEncryption; import de.usd.cstchef.operations.encryption.DesDecryption; import de.usd.cstchef.operations.encryption.DesEncryption; +import de.usd.cstchef.operations.encryption.PgpDecryption; +import de.usd.cstchef.operations.encryption.PgpEncryption; import de.usd.cstchef.operations.encryption.RsaDecryption; import de.usd.cstchef.operations.encryption.RsaEncryption; import de.usd.cstchef.operations.encryption.SM4Decryption; @@ -139,6 +141,7 @@ import de.usd.cstchef.operations.setter.LineSetter; import de.usd.cstchef.operations.signature.JWTDecode; import de.usd.cstchef.operations.signature.JWTSign; +import de.usd.cstchef.operations.signature.PgpSignature; import de.usd.cstchef.operations.signature.RsaSignature; import de.usd.cstchef.operations.signature.SM2Signature; import de.usd.cstchef.operations.signature.SoapMultiSignature; @@ -404,6 +407,7 @@ public static Class[] getOperationsDev() { NoOperation.class, NumberCompare.class, Prefix.class, PlainRequest.class, RandomNumber.class, RandomUUID.class, ReadFile.class, RegexExtractor.class, RegexMatch.class, RequestBuilder.class, Reverse.class, + PgpDecryption.class, PgpEncryption.class, PgpSignature.class, Replace.class, RIPEMD.class, RsaDecryption.class, RsaEncryption.class, RsaSignature.class, RemoveWhitespace.class, RequestToResponse.class, SM2Signature.class, SM3.class, SM4Encryption.class, SM4Decryption.class, SoapMultiSignature.class, StopOperation.class, SetIfEmpty.class, SHA1.class, SHA2.class, SHA3.class, Skein.class, SplitAndSelect.class, diff --git a/src/main/java/de/usd/cstchef/operations/encryption/PgpDecryption.java b/src/main/java/de/usd/cstchef/operations/encryption/PgpDecryption.java new file mode 100644 index 0000000..8c185a1 --- /dev/null +++ b/src/main/java/de/usd/cstchef/operations/encryption/PgpDecryption.java @@ -0,0 +1,160 @@ +package de.usd.cstchef.operations.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Iterator; + +import javax.swing.JComboBox; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; + +import burp.api.montoya.core.ByteArray; +import de.usd.cstchef.operations.Operation; +import de.usd.cstchef.operations.Operation.OperationInfos; +import de.usd.cstchef.operations.OperationCategory; +import de.usd.cstchef.view.ui.FormatTextField; +import de.usd.cstchef.view.ui.VariableTextArea; + +@OperationInfos(name = "PGP Decrypt", category = OperationCategory.ENCRYPTION, description = "Decrypt a PGP message with an OpenPGP private key.") +public class PgpDecryption extends Operation { + + private static String[] outModes = new String[] { "Raw", "Hex", "Base64" }; + + private VariableTextArea privateKeyTextArea; + private FormatTextField passphraseField; + private JComboBox outputMode; + + @Override + public void createUI() { + this.privateKeyTextArea = new VariableTextArea(); + this.addUIElement("Private Key", this.privateKeyTextArea); + + this.passphraseField = new FormatTextField(); + this.addUIElement("Passphrase", this.passphraseField); + + this.outputMode = new JComboBox<>(outModes); + this.addUIElement("Output", this.outputMode); + } + + protected String getPrivateKey() { + return this.privateKeyTextArea.getText(); + } + + protected String getPassphrase() throws Exception { + return this.passphraseField.getText().toString(); + } + + protected String getOutputMode() { + return (String) this.outputMode.getSelectedItem(); + } + + @Override + protected ByteArray perform(ByteArray input) throws Exception { + String privateKey = getPrivateKey(); + if (privateKey == null || privateKey.trim().isEmpty()) { + throw new IllegalArgumentException("No private key available."); + } + + byte[] decrypted = decrypt(input.getBytes(), privateKey, getPassphrase()); + + String mode = getOutputMode(); + if ("Hex".equals(mode)) { + decrypted = Hex.encode(decrypted); + } else if ("Base64".equals(mode)) { + decrypted = Base64.encode(decrypted); + } + + return factory.createByteArray(decrypted); + } + + /** + * Decrypts an armored (or binary) PGP message {@code message} using {@code armoredPrivateKey} + * (an ASCII-armored OpenPGP private key) unlocked with {@code passphrase}. Returns the raw + * decrypted bytes; integrity (MDC) is verified when present. Uses BouncyCastle's lightweight + * operators so it does not depend on a signed JCE provider. + */ + protected byte[] decrypt(byte[] message, String armoredPrivateKey, String passphrase) throws Exception { + PGPSecretKeyRingCollection secretKeys = new PGPSecretKeyRingCollection( + PGPUtil.getDecoderStream(new ByteArrayInputStream(armoredPrivateKey.getBytes())), + new BcKeyFingerprintCalculator()); + + InputStream in = PGPUtil.getDecoderStream(new ByteArrayInputStream(message)); + JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(in); + + Object firstObject = pgpFact.nextObject(); + PGPEncryptedDataList encList; + if (firstObject instanceof PGPEncryptedDataList) { + encList = (PGPEncryptedDataList) firstObject; + } else { + encList = (PGPEncryptedDataList) pgpFact.nextObject(); + } + + PGPPrivateKey privateKey = null; + PGPPublicKeyEncryptedData encryptedData = null; + Iterator it = encList.getEncryptedDataObjects(); + while (it.hasNext()) { + PGPEncryptedData data = it.next(); + if (data instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pubKeyData = (PGPPublicKeyEncryptedData) data; + PGPSecretKey secretKey = secretKeys.getSecretKey(pubKeyData.getKeyID()); + if (secretKey != null) { + privateKey = secretKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder( + new BcPGPDigestCalculatorProvider()).build(passphrase.toCharArray())); + encryptedData = pubKeyData; + break; + } + } + } + + if (privateKey == null) { + throw new IllegalArgumentException("No matching private key found for the encrypted message."); + } + + InputStream clear = encryptedData.getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey)); + + JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear); + Object messageObject = plainFact.nextObject(); + + if (messageObject instanceof PGPCompressedData) { + PGPCompressedData compressed = (PGPCompressedData) messageObject; + plainFact = new JcaPGPObjectFactory(compressed.getDataStream()); + messageObject = plainFact.nextObject(); + } + + if (!(messageObject instanceof PGPLiteralData)) { + throw new PGPException("Unexpected PGP packet; message does not contain literal data."); + } + + PGPLiteralData literalData = (PGPLiteralData) messageObject; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream literalIn = literalData.getInputStream(); + byte[] buffer = new byte[4096]; + int len; + while ((len = literalIn.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + + if (encryptedData.isIntegrityProtected() && !encryptedData.verify()) { + throw new PGPException("Message failed integrity check."); + } + + return out.toByteArray(); + } +} diff --git a/src/main/java/de/usd/cstchef/operations/encryption/PgpEncryption.java b/src/main/java/de/usd/cstchef/operations/encryption/PgpEncryption.java new file mode 100644 index 0000000..d9e7970 --- /dev/null +++ b/src/main/java/de/usd/cstchef/operations/encryption/PgpEncryption.java @@ -0,0 +1,108 @@ +package de.usd.cstchef.operations.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.Date; +import java.util.Iterator; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; + +import burp.api.montoya.core.ByteArray; +import de.usd.cstchef.operations.Operation; +import de.usd.cstchef.operations.Operation.OperationInfos; +import de.usd.cstchef.operations.OperationCategory; +import de.usd.cstchef.view.ui.VariableTextArea; + +@OperationInfos(name = "PGP Encrypt", category = OperationCategory.ENCRYPTION, description = "Encrypt input with an OpenPGP public key (armored output).") +public class PgpEncryption extends Operation { + + private VariableTextArea publicKeyTextArea; + + @Override + public void createUI() { + this.publicKeyTextArea = new VariableTextArea(); + this.addUIElement("Public Key", this.publicKeyTextArea); + } + + protected String getPublicKey() { + return this.publicKeyTextArea.getText(); + } + + @Override + protected ByteArray perform(ByteArray input) throws Exception { + String publicKey = getPublicKey(); + if (publicKey == null || publicKey.trim().isEmpty()) { + throw new IllegalArgumentException("No public key available."); + } + return factory.createByteArray(encrypt(input.getBytes(), publicKey)); + } + + /** + * Encrypts {@code data} to {@code armoredPublicKey} (an ASCII-armored OpenPGP public key) + * using an AES-256 session key with integrity protection (MDC) and returns an armored + * PGP message block. Uses BouncyCastle's lightweight operators so it does not depend on a + * signed JCE provider (the assembled extension jar is unsigned). + */ + protected byte[] encrypt(byte[] data, String armoredPublicKey) throws Exception { + PGPPublicKey encryptionKey = readPublicKey(armoredPublicKey); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armoredOut = new ArmoredOutputStream(out); + + PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator( + new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256) + .setWithIntegrityPacket(true) + .setSecureRandom(new SecureRandom())); + encGen.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(encryptionKey)); + + OutputStream encOut = encGen.open(armoredOut, new byte[4096]); + + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(encOut); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date()); + litOut.write(data); + litGen.close(); + + compGen.close(); + encGen.close(); + armoredOut.close(); + + return out.toByteArray(); + } + + private PGPPublicKey readPublicKey(String armoredPublicKey) throws Exception { + PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(new ByteArrayInputStream(armoredPublicKey.getBytes())), + new BcKeyFingerprintCalculator()); + + Iterator ringIt = pgpPub.getKeyRings(); + while (ringIt.hasNext()) { + PGPPublicKeyRing ring = ringIt.next(); + Iterator keyIt = ring.getPublicKeys(); + while (keyIt.hasNext()) { + PGPPublicKey key = keyIt.next(); + if (key.isEncryptionKey()) { + return key; + } + } + } + throw new IllegalArgumentException("No encryption key found in the provided public key."); + } +} diff --git a/src/main/java/de/usd/cstchef/operations/signature/PgpSignature.java b/src/main/java/de/usd/cstchef/operations/signature/PgpSignature.java new file mode 100644 index 0000000..4a543b0 --- /dev/null +++ b/src/main/java/de/usd/cstchef/operations/signature/PgpSignature.java @@ -0,0 +1,105 @@ +package de.usd.cstchef.operations.signature; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Iterator; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; + +import burp.api.montoya.core.ByteArray; +import de.usd.cstchef.operations.Operation; +import de.usd.cstchef.operations.Operation.OperationInfos; +import de.usd.cstchef.operations.OperationCategory; +import de.usd.cstchef.view.ui.FormatTextField; +import de.usd.cstchef.view.ui.VariableTextArea; + +@OperationInfos(name = "PGP Signature", category = OperationCategory.SIGNATURE, description = "Create a detached OpenPGP signature (armored).") +public class PgpSignature extends Operation { + + private VariableTextArea privateKeyTextArea; + private FormatTextField passphraseField; + + @Override + public void createUI() { + this.privateKeyTextArea = new VariableTextArea(); + this.addUIElement("Private Key", this.privateKeyTextArea); + + this.passphraseField = new FormatTextField(); + this.addUIElement("Passphrase", this.passphraseField); + } + + protected String getPrivateKey() { + return this.privateKeyTextArea.getText(); + } + + protected String getPassphrase() throws Exception { + return this.passphraseField.getText().toString(); + } + + @Override + protected ByteArray perform(ByteArray input) throws Exception { + String privateKey = getPrivateKey(); + if (privateKey == null || privateKey.trim().isEmpty()) { + throw new IllegalArgumentException("No private key available."); + } + return factory.createByteArray(sign(input.getBytes(), privateKey, getPassphrase())); + } + + /** + * Creates a detached, ASCII-armored OpenPGP signature (SHA-256) over {@code data} using + * {@code armoredPrivateKey} (an ASCII-armored OpenPGP private key) unlocked with + * {@code passphrase}. Uses BouncyCastle's lightweight operators so it does not depend on a + * signed JCE provider. + */ + protected byte[] sign(byte[] data, String armoredPrivateKey, String passphrase) throws Exception { + PGPSecretKey secretKey = readSigningKey(armoredPrivateKey); + PGPPrivateKey privateKey = secretKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder( + new BcPGPDigestCalculatorProvider()).build(passphrase.toCharArray())); + + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256)); + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); + signatureGenerator.update(data); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armoredOut = new ArmoredOutputStream(out); + BCPGOutputStream bcpgOut = new BCPGOutputStream(armoredOut); + signatureGenerator.generate().encode(bcpgOut); + bcpgOut.close(); + armoredOut.close(); + + return out.toByteArray(); + } + + private PGPSecretKey readSigningKey(String armoredPrivateKey) throws Exception { + PGPSecretKeyRingCollection secretKeys = new PGPSecretKeyRingCollection( + PGPUtil.getDecoderStream(new ByteArrayInputStream(armoredPrivateKey.getBytes())), + new BcKeyFingerprintCalculator()); + + Iterator ringIt = secretKeys.getKeyRings(); + while (ringIt.hasNext()) { + PGPSecretKeyRing ring = ringIt.next(); + Iterator keyIt = ring.getSecretKeys(); + while (keyIt.hasNext()) { + PGPSecretKey key = keyIt.next(); + if (key.isSigningKey()) { + return key; + } + } + } + throw new IllegalArgumentException("No signing key found in the provided private key."); + } +}