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 extends Operation>[] 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.");
+ }
+}