Commit 0bee5cfc authored by Eibad Ali's avatar Eibad Ali Committed by Abdullah Danish

[GAFP-4] : PGP Encryption/Decryption

parent ca8d081a
...@@ -2,7 +2,19 @@ package com.nisum.demo.blobStorage; ...@@ -2,7 +2,19 @@ package com.nisum.demo.blobStorage;
import com.microsoft.azure.functions.annotation.*; import com.microsoft.azure.functions.annotation.*;
import com.microsoft.azure.functions.*; import com.microsoft.azure.functions.*;
import java.io.IOException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
/** /**
* Azure Functions with Azure Blob trigger. * Azure Functions with Azure Blob trigger.
...@@ -13,12 +25,53 @@ public class BlobTriggerFunction { ...@@ -13,12 +25,53 @@ public class BlobTriggerFunction {
*/ */
@FunctionName("BlobTriggerFunc") @FunctionName("BlobTriggerFunc")
public void run( public void run(
@BlobTrigger(name = "inbound", path = "test/{name}", dataType = "binary", @BlobTrigger(name = "file", path = "test/{name}", dataType = "binary", connection = "AzureWebJobsStorage") byte[] content,
connection = "AzureWebJobsStorage") byte[] content,
@BindingName("name") String name, @BindingName("name") String name,
final ExecutionContext context final ExecutionContext context
) throws IOException { ) throws IOException {
context.getLogger().info("Java Blob trigger function processed a blob. Name: " + ResourceBundle resourceBundle = new PropertyResourceBundle(new FileInputStream("/Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/application.properties"));
name + "\n Size: " + content.length + " Bytes"); if (Pattern.matches("[a-z|A-Z]*.csv", name)) {
String directoryPath = resourceBundle.getString("files.directory.path") + name;
if (!Files.exists(Paths.get(directoryPath))) {
Files.createFile(Paths.get(directoryPath));
}
try (FileOutputStream fileOutputStream = new FileOutputStream(directoryPath, true)) {
fileOutputStream.write(content);
}
InputStream inputStreamSecretKey = new FileInputStream(resourceBundle.getString("gpg.keychain.secret.key"));
InputStream inputStreamPublicKey = new FileInputStream(resourceBundle.getString("gpg.keychain.public.key"));
char[] pass = {'n', 'i', 's', 'u', 'm', '1', '2', '3', '4'};
// Writes data to the output stream
OutputStream outbound = new FileOutputStream(resourceBundle.getString("encrypted.files.directory.path") + name.replaceFirst(".csv",".asc"));
String inbound= resourceBundle.getString("files.directory.path") + name;
try {
// use it when using only public key
// PGPPublicKey key = PGPUtils.readPublicKey(inputStream);
// use it to get secret key when using public + private key
// PGPSecretKey pgpSecretKey = PGPUtils.readSecretKey(inputStream);
// for encryption
PGPUtils.encryptAndSignFile(outbound, inbound, inputStreamPublicKey, inputStreamSecretKey, true, true, pass);
context.getLogger().info("---File Encrypted---");
// for decryption
// InputStream fileToBeDecrypt = new FileInputStream(resourceBundle.getString("encrypted.files.directory.path") + name.replaceFirst(".csv", ".asc"));
// OutputStream inboundN = new FileOutputStream(resourceBundle.getString("decrypted.files.directory.path"));
// PGPUtils.decryptFile(fileToBeDecrypt, inboundN, inputStreamSecretKey, pass, inputStreamPublicKey);
} catch (Exception e) {
e.printStackTrace();
}
// Closes the output stream
}
context.getLogger().info("Java Blob trigger function processed a blob. Name: " + name + "\n Size: " + content.length + " Bytes");
} }
} }
package com.nisum.demo.blobStorage;
import java.io.IOException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import org.bouncycastle.openpgp.PGPException;
public abstract class DecryptService {
/**
* Decrypt the input file and move it to output folder.
*
* @param inputPath encrypted file path
* @param outputPath destination file path
* @param secretKeyPath secret key file path
* @param partnerPublicKeyPath partner public key file path
* @param passPhrase pass phrase use to generate secret key
* @throws NoSuchProviderException thrown when security provider not found
* @throws IOException throws when unable accessing file path.
* @throws PGPException thrown when error performing PGP decryption
* @throws SignatureException when error performing verification of signature
*/
abstract void decrypt(String inputPath, String outputPath, String secretKeyPath, String partnerPublicKeyPath, String passPhrase)
throws NoSuchProviderException, IOException, PGPException, SignatureException;
}
package com.nisum.demo.blobStorage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.util.Date;
import java.util.Iterator;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
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.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.util.io.Streams;
public class PGPUtils {
/**
* private constructor for utility class.
*/
private PGPUtils() {
}
/**
* Load a secret key ring collection from keyIn and find the secret key
* corresponding to keyID if it exists.
*
* @param keyIn input stream representing a key ring collection.
* @param keyID keyID we want.
* @param pass passphrase to decrypt secret key with.
* @return private key
* @throws IOException exception
* @throws PGPException exception
* @throws NoSuchProviderException exception
*/
static PGPPrivateKey findSecretKey(InputStream keyIn, long keyID, char[] pass)
throws IOException, PGPException, NoSuchProviderException {
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
org.bouncycastle.openpgp.PGPUtil.getDecoderStream(keyIn));
PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
if (pgpSecKey == null) {
return null;
}
return pgpSecKey.extractPrivateKey(pass, "BC");
}
/**
* Iterate through key ring collection to find secret key.
*
* @param secretKeyInputStream path to secret key
* @return PGP secret key
* @throws IOException exception when performing file operation
* @throws PGPException exception when finding key in ring collection
*/
static PGPSecretKey readSecretKey(InputStream secretKeyInputStream) throws IOException, PGPException {
PGPSecretKeyRingCollection keyRingCollection = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(secretKeyInputStream));
Iterator keyRingIterator = keyRingCollection.getKeyRings();
while (keyRingIterator.hasNext()) {
PGPSecretKeyRing keyRing = (PGPSecretKeyRing) keyRingIterator.next();
Iterator keyIterator = keyRing.getSecretKeys();
while (keyIterator.hasNext()) {
PGPSecretKey secretKey = (PGPSecretKey) keyIterator.next();
if (secretKey.isSigningKey()) {
return secretKey;
}
}
}
throw new IllegalArgumentException("Unable to find signing key in key ring.");
}
/**
* Iterate through key ring collection to find public key.
*
* @param publicKeyInputStream path to public key
* @return PGP public key
* @throws IOException exception when performing file operation
* @throws PGPException exception when finding key in ring collection
*/
static PGPPublicKey readPublicKey(InputStream publicKeyInputStream) throws IOException, PGPException {
publicKeyInputStream = PGPUtil.getDecoderStream(publicKeyInputStream);
PGPPublicKeyRingCollection keyRingCollection = new PGPPublicKeyRingCollection(publicKeyInputStream);
PGPPublicKey partnerPublicKey = null;
// iterate through the key rings.
Iterator<PGPPublicKeyRing> keyRings = keyRingCollection.getKeyRings();
while (partnerPublicKey == null && keyRings.hasNext()) {
PGPPublicKeyRing keyRing = keyRings.next();
Iterator<PGPPublicKey> publicKeys = keyRing.getPublicKeys();
while (partnerPublicKey == null && publicKeys.hasNext()) {
PGPPublicKey publicKey = publicKeys.next();
if (publicKey.isEncryptionKey()) {
partnerPublicKey = publicKey;
}
}
}
if (partnerPublicKey == null) {
throw new IllegalArgumentException("Unable to find encryption key in key ring.");
}
return partnerPublicKey;
}
/**
* decrypt and verify the passed in message stream.
*
* @param in input stream for encrypted file
* @param out output stream for decrypted file
* @param keyIn input stream for secret key file
* @param passwd passphrase
* @param publicKeyIn public key of partner
* @throws IOException exception
* @throws PGPException exception
* @throws NoSuchProviderException exception
* @throws SignatureException exception
*/
public static void decryptFile(InputStream in, OutputStream out, InputStream keyIn, char[] passwd, InputStream publicKeyIn)
throws IOException, NoSuchProviderException, SignatureException, PGPException {
Security.addProvider(new BouncyCastleProvider());
in = PGPUtil.getDecoderStream(in);
PGPObjectFactory pgpF = new PGPObjectFactory(in);
PGPEncryptedDataList enc;
Object o = pgpF.nextObject();
// the first object might be a PGP marker packet.
if (o instanceof PGPEncryptedDataList) {
enc = (PGPEncryptedDataList) o;
} else {
enc = (PGPEncryptedDataList) pgpF.nextObject();
}
// find the secret key
Iterator<PGPPublicKeyEncryptedData> it = enc.getEncryptedDataObjects();
PGPPrivateKey sKey = null;
PGPPublicKeyEncryptedData pbe = null;
while (sKey == null && it.hasNext()) {
pbe = it.next();
sKey = findSecretKey(keyIn, pbe.getKeyID(), passwd);
}
if (sKey == null) {
throw new IllegalArgumentException("secret key for message not found.");
}
InputStream clear = pbe.getDataStream(sKey, "BC");
PGPObjectFactory plainFact = new PGPObjectFactory(clear);
PGPOnePassSignatureList onePassSignatureList = null;
PGPSignatureList signatureList = null;
PGPCompressedData compressedData;
Object message = plainFact.nextObject();
ByteArrayOutputStream actualOutput = new ByteArrayOutputStream();
while (message != null) {
if (message instanceof PGPCompressedData) {
compressedData = (PGPCompressedData) message;
plainFact = new PGPObjectFactory(compressedData.getDataStream());
message = plainFact.nextObject();
}
if (message instanceof PGPLiteralData) {
// have to read it and keep it somewhere.
Streams.pipeAll(((PGPLiteralData) message).getInputStream(), actualOutput);
} else if (message instanceof PGPOnePassSignatureList) {
onePassSignatureList = (PGPOnePassSignatureList) message;
} else if (message instanceof PGPSignatureList) {
signatureList = (PGPSignatureList) message;
} else {
throw new PGPException("message unknown message type.");
}
message = plainFact.nextObject();
}
actualOutput.close();
PGPPublicKey publicKey = null;
byte[] output = actualOutput.toByteArray();
if (onePassSignatureList == null || signatureList == null) {
throw new PGPException("Poor PGP. Signatures not found.");
} else {
for (int i = 0; i < onePassSignatureList.size(); i++) {
PGPOnePassSignature ops = onePassSignatureList.get(0);
PGPPublicKeyRingCollection pgpRing = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(publicKeyIn));
publicKey = pgpRing.getPublicKey(ops.getKeyID());
if (publicKey != null) {
ops.initVerify(publicKey, "BC");
ops.update(output);
PGPSignature signature = signatureList.get(i);
if (!ops.verify(signature)) {
throw new SignatureException("Signature Verification failed");
}
}
}
}
if (pbe.isIntegrityProtected() && !pbe.verify()) {
throw new PGPException("Data is integrity protected but integrity is lost.");
} else if (publicKey == null) {
throw new SignatureException("Signature not found");
} else {
out.write(output);
out.flush();
out.close();
}
}
/**
* Encrypt file using public key provided and sign it with secret key
* provided.
*
* @param encryptedFileStream destination file path
* @param sourceFilePath source file path
* @param partnerPublicKeyStream public key to encrypt
* @param secretKeyStream secret key to sign
* @param armor armor
* @param withIntegrityCheck integrity check
* @param pass pass phrase for secret key
* @throws IOException exception when performing file operations
* @throws NoSuchProviderException exception when provider not found
* @throws NoSuchAlgorithmException exception when generating signature
* @throws PGPException exception while encryption
* @throws SignatureException exception when unable to update signature
*/
public static void encryptAndSignFile(OutputStream encryptedFileStream, String sourceFilePath, InputStream partnerPublicKeyStream,
InputStream secretKeyStream, boolean armor, boolean withIntegrityCheck, char[] pass)
throws IOException, NoSuchProviderException, NoSuchAlgorithmException, PGPException, SignatureException {
Security.addProvider(new BouncyCastleProvider());
PGPSecretKey secretKeyToSign = readSecretKey(secretKeyStream);
PGPPublicKey partnerPublicKey = readPublicKey(partnerPublicKeyStream);
if (armor) {
encryptedFileStream = new ArmoredOutputStream(encryptedFileStream);
}
PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, withIntegrityCheck, new SecureRandom(), "BC");
encryptedDataGenerator.addMethod(partnerPublicKey);
OutputStream encryptedOut = encryptedDataGenerator.open(encryptedFileStream, new byte[10000]);
PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
try (OutputStream compressedData = comData.open(encryptedOut)) {
PGPPrivateKey pgpPrivKey = secretKeyToSign.extractPrivateKey(pass, "BC");
PGPSignatureGenerator sGen = new PGPSignatureGenerator(secretKeyToSign.getPublicKey().getAlgorithm(), PGPUtil.SHA1, "BC");
sGen.initSign(PGPSignature.BINARY_DOCUMENT, pgpPrivKey);
Iterator it = secretKeyToSign.getPublicKey().getUserIDs();
if (it.hasNext()) {
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
spGen.setSignerUserID(false, (String) it.next());
sGen.setHashedSubpackets(spGen.generate());
}
sGen.generateOnePassVersion(false).encode(compressedData); // bOut
File file = new File(sourceFilePath);
PGPLiteralDataGenerator lGen = new PGPLiteralDataGenerator();
try (OutputStream lOut = lGen.open(compressedData, PGPLiteralData.BINARY, file.getName(), new Date(), new byte[10000])) {
try (FileInputStream fIn = new FileInputStream(file)) {
int ch;
while ((ch = fIn.read()) >= 0) {
lOut.write(ch);
sGen.update((byte) ch);
}
}
}
lGen.close();
sGen.generate().encode(compressedData);
comData.close();
}
encryptedOut.close();
encryptedDataGenerator.close();
if (armor) {
encryptedFileStream.close();
}
}
}
files.directory.path = /Users/adanish/Documents/GitHub/Gap Repos/myazurefunctionsdemo/src/main/resources/files/ files.directory.path = /Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/files/
\ No newline at end of file encrypted.files.directory.path = /Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/encrypted/
decrypted.files.directory.path = /Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/decrypted/a.csv
gpg.keychain.public.key = /Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/keys/gappocPublic.asc
gpg.keychain.secret.key = /Users/eali/Projects/azure/myazurefunctionsdemo/src/main/resources/keys/gappoc.asc
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
GOZWWua17KtDIL4yx3hP
=wJXX
-----END PGP PRIVATE KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGMi5bUBEADVb+Dh6PYyKOprvfhB0iDxloV8oLL5R0u3cjpTysHhY8Pu+4BU
c02HLT/B5VENex0GkOkqlzTB3OO+rmzC5fCM+pkfQAj6mCM+i76ZzlgOQ4LOjXk9
rXdr4m6MHCmafJnK9+/Dgf5KRrkQIMnTJgCU/xurS6/2e+GN3gBlLhWXWf5xKumn
Z/k95L0Fm7pt1BNPg4u0XQRJs2wDRuqo47iJFPRjlBsJemCbnWOSVWLuBKjllVOn
40lBMKHbmkw5DZxH/kmjYXOIBBVj3g+QrvGFgW9+PBaOzlQ8bvyvfnejIQXSwq5F
icago3mc1UT+X/S2vUKSLIC6nz7H+eTRTyQ60BCDvSUOpEITdLFy1BJeJd5q5ph8
2oyBS/2wqVGM/4a5JRh2ZZhyHE5wQJPIrSBLg6tMMABNQOqxL0FlklXGby+xLjlD
t4X6lMINiZXdj/e4nZoaTMZf9X4niOM+EHT90ujO+G6BETPOMJPMYoJtg8g50G9h
vagP8jkGwyrrkTlRTkHJNqwyuMlPSb6Qq5BgPVqHYrx6WezEGwi0gkKB5nzvn3eE
DzWviEStkDZ5UcfVBY63qK7POcl8sLsd7Aspwe1tihTn3fOakgZQpEh2Y9KD6hXr
Hwl8v1UA30tP0hGP67Q/SzvZ1+yXvIwy+HlM4U+396ZigZna7eWqBjzrowARAQAB
tAZnYXBwb2OJAlQEEwEIAD4WIQQ+opYXLTrY3haSr86kBH+9rHfGrAUCYyLltQIb
AwUJB4YfAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCkBH+9rHfGrNVTEACT
BVlJ6cAsoNOQdJH1YHr63U00qpdYdRMJoTQQJJHxbJZ6lc+E9Adct9rOIWocOKjm
3SAq0a1PK9ZzHzQn2+2mpnKPUCyOunIfcn56+i/lQS0E6ebeFWk2NnCcmmmhMofV
mc90a0XcP3/281KUx92TWRp8XwyUwBnL6Z6mpHpaFqnVmQ4u6mfthRHZoQGFVSaG
suv1BXa4jbUnnErNf6kP3m1fy5whs4newTPt7M5y9GeIf17EJcmsfbn5VoyDTr1v
ZTJWVftLJ7G0Y9I5oVheyu0VmaOAdzBr0rs1GxE2HFQFInFYAh8xQwDzyGWD3ems
FZv9Ya/Z2Bmw99FPZ1DxQt0EeoBNQ5hzWYGcB2o4Kc2VlVZekDLLbMRF/eHP+TEG
njcVwTrBkuCCL8YagxVjO40qkOQptAJiMmadMlAZEUkvvuD+SxDL9ulc3nj0RHa3
7Ln0Ipk67UcM9KycEL220AQ94Skt0qH2OhUoN09INiMFNr9cMfCiMQeAufkemzbs
XBxQRlXQxkziPj1hVUkO9274UT8YIoFkY2zDeFQfTNT1yMtUjP2MXb9LkfGkqaPQ
r79bzLPNwW8zjR1XaBewCSWWG9Ot9Trj47Z8FXr5CbSo5qxQR4b9AIvLYC801rI+
iJ301mxmlSW9UQd7LoVJzpfV8gu+pKRWB+72qcMn8rkCDQRjIuW1ARAA8u91KW78
WthxYg8Oe+0D1uDs8kgf88c4Dy4L9illpNPeyh/w7euGQOUl+Ex+iRDuPQI1tRW8
nIgVBpt0SteKOyWwOMq1RcIVU1oqa9a8m1haDMxKPWYcaNMJn/4aPsA0VbQeJhK0
cEhsDnHZN2K4Rj2UB4Wb9NoHQeybMSjfXa4gyp8yUzqzsMJ5IrJQhdrSoiShksAf
crONlns2Y1LxpaVdjXpEJAt0MbJOuzwEvvmXu2jWsFLuOXKEuHTRkrl4rBf/eD/2
SG9sT08oH2OqW8ZYmOTjY6XHwXnNGW6jyKwV7Nu9EMx3Qe+Wc5eFL58Zd5FUh9j1
KGlh9G1A2y1dXGMddhXcDimeXLNYUenOOrLTaiebwkNcO9YouBL5MBxa0h+FqGTB
x60SNi87++p0jDwkQSzvgWmEJK7HgoKVTCmTwOrlCVIMAD2aKlSdEYjVf7xNr+Yj
daUpl8j/Xw7Jor3+5OutlKwAQWuncvP7NCwvUSXqmZFfqz4Gnk0zp9bO/JzQ7lRT
LzRZzgbWeKCq8eOHhqBGDREZZjAKajGTxzI04eHuLPtc/3vpK3ra/yWqeTkDHPIO
UNOQpSyRrxf7G1sZUAahSjV8PgdMkcZJVjbIeLoIJy36PuV2A+vIA5V9N0KEryv3
YLB7Q4gTil4Qmfsd46BeeT8UFqMOyJFcpqcAEQEAAYkCPAQYAQgAJhYhBD6ilhct
OtjeFpKvzqQEf72sd8asBQJjIuW1AhsMBQkHhh8AAAoJEKQEf72sd8asEg4P/A+F
xSX3AOuXsJHHnI2eFgEB1nAVqQeisBK24IR14bjz5eDvRWBkWYXcE7uFo1SJsYL0
9af5IIZx6gvpGAomgTuNFdvlek+26dgeCc32asI0Ca0e2NIRw6AO1JJSSNPrXsg5
5AkbXt/enjzFkHXc87g8ff8t/OaaXzP0m0SPP3iUNflC4FoCBMmPBEtuVGlfKY2J
FMvg3cLXqbdffBcVGr3fzjBoWhE1X2PzVnhkD5FKg9q6JLMujLQTpqoc3ZIWecZ+
89YK/lHpE55ON0C7M9B3cAFi28dipkoQVdaa+RuleNBH0MpDF4sVijUiddPzLSwx
gAYQVbTwRWevP2IPFFp7/CPoA6X1mNqW3rODqsLNUvfWhi+oFjJ0Fq3BIXOrl8/D
LSemQwDFKm7WzAVuSZHZQr9nuOvkEx45IUV4kKcNfCiEe6sy2GqpE9nplMkkCdJv
CG9CCdK6pXUYlXkQCaklznYsrjsK7GuTEm+TXI0FMkQP+VxiYTENvQ1YH1Kgp20b
HpAvhoEge+SoptEaPVWieP6kclcJcob6sPOVnBr45N6QfxmLJiMGfIVopIQmoFri
GUm0lYGdeUlKztsHpc/ytRnwNEwLMoLAZLz/cSuA1z5WImCUvGnatUQTYFsbykv7
lh0cTXo1EENddx9LhWiCGOZWWua17KtDIL4yx3hP
=PJ5t
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=PJ5t
-----END PGP PUBLIC KEY BLOCK-----
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment