Implement C# for Encryption/Decryption by PGP

จากบทความที่แล้ว เราได้สร้าง PGP Public Key / Private Key ด้วย Kleopatra กันแล้ว ในบทความนี้เราจะพัฒนาระบบ Encryption/Decryption ด้วยภาษา C# กัน

  1. Go to -> Visual Studio 2019
  2. Choose -> Console App (.NET Framework ) -> .NET Framework 4.7.2
  3. Right Click on Project -> Manage Nuget Package
  • Install BouncyCastle

Source Code Structure

PGPEncyptDecrypt.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.IO;

using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities.IO;

namespace PGPNET1
{
public static class PGPEncryptDecrypt
{
private const int BufferSize = 0x10000;

public static void EncryptFile(string inputFile, string outputFile, string publicKeyFile, bool armor, bool withIntegrityChec)
{
try
{
using (Stream publicKeyStream = File.OpenRead(publicKeyFile))
{
PgpPublicKey encKey = ReadPublicKey(publicKeyStream);
using (MemoryStream bOut = new MemoryStream())
{
PgpCompressedDataGenerator comData = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
PgpUtilities.WriteFileToLiteralData(comData.Open(bOut), PgpLiteralData.Binary, new FileInfo(inputFile));
comData.Close();

PgpEncryptedDataGenerator cPk = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Cast5, withIntegrityChec, new SecureRandom());
cPk.AddMethod(encKey);
byte[] bytes = bOut.ToArray();

using(Stream outputStream = File.Create(outputFile))
{
if(armor)
{
using(ArmoredOutputStream armoredStream = new ArmoredOutputStream(outputStream))
{
using(Stream cOut = cPk.Open(armoredStream, bytes.Length))
{
cOut.Write(bytes, 0, bytes.Length);
}
}
}
else
{
using(Stream cOut = cPk.Open(outputStream, bytes.Length))
{
cOut.Write(bytes, 0, bytes.Length);
}
}
}

}
}

}
catch (PgpException e)
{
throw;
}

}

public static void EncryptAndSign(string inputFile,string outputFile, string publicKeyFile,string privateKeyFile,string passPhrase, bool armor)
{
PgpEncryptionKeys encryptionKeys = new PgpEncryptionKeys(publicKeyFile, privateKeyFile, passPhrase);
if (!File.Exists(inputFile))
throw new FileNotFoundException(String.Format(“Input file [{0}] does not exist.”, inputFile));

if (!File.Exists(publicKeyFile))
throw new FileNotFoundException(String.Format(“Public Key file [{0}] does not exist.”, publicKeyFile));

if (!File.Exists(privateKeyFile))
throw new FileNotFoundException(String.Format(“Private Key file [{0}] does not exist.”, privateKeyFile));

if (String.IsNullOrEmpty(passPhrase))
throw new ArgumentNullException(“Invalid Pass Phrase.”);

if (encryptionKeys == null)
throw new ArgumentNullException(“Encryption Key not found.”);

using (Stream outputStream = File.Create(outputFile))
{
if (armor)
using (ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream))
{
OutputEncrypted(inputFile, armoredOutputStream, encryptionKeys);
}
else
OutputEncrypted(inputFile, outputStream, encryptionKeys);
}
}

private static void OutputEncrypted(string inputFile,Stream outputStream, PgpEncryptionKeys encryptionKeys)
{
using (Stream encryptedOut = ChainEncryptedOut(outputStream, encryptionKeys))
{
FileInfo unencryptedFileInfo = new FileInfo(inputFile);
using (Stream compressedOut = ChainCompressedOut(encryptedOut))
{
PgpSignatureGenerator signatureGenerator = InitSignatureGenerator(compressedOut, encryptionKeys);
using (Stream literalOut = ChainLiteralOut(compressedOut, unencryptedFileInfo))
{
using (FileStream inputFileStream = unencryptedFileInfo.OpenRead())
{
WriteOutputAndSign(compressedOut, literalOut, inputFileStream, signatureGenerator);
inputFileStream.Close();
}
}
}
}
}

private static void WriteOutputAndSign(Stream compressedOut, Stream literalOut, FileStream inputFile, PgpSignatureGenerator signatureGenerator)
{
int length = 0;
byte[] buf = new byte[BufferSize];
while ((length = inputFile.Read(buf, 0, buf.Length)) > 0)
{
literalOut.Write(buf, 0, length);
signatureGenerator.Update(buf, 0, length);
}
signatureGenerator.Generate().Encode(compressedOut);
}

private static Stream ChainEncryptedOut(Stream outputStream, PgpEncryptionKeys m_encryptionKeys)
{
PgpEncryptedDataGenerator encryptedDataGenerator;
encryptedDataGenerator = new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.TripleDes, new SecureRandom());
encryptedDataGenerator.AddMethod(m_encryptionKeys.PublicKey);
return encryptedDataGenerator.Open(outputStream, new byte[BufferSize]);
}

private static Stream ChainCompressedOut(Stream encryptedOut)
{
PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip);
return compressedDataGenerator.Open(encryptedOut);
}

private static Stream ChainLiteralOut(Stream compressedOut, FileInfo file)
{
PgpLiteralDataGenerator pgpLiteralDataGenerator = new PgpLiteralDataGenerator();
return pgpLiteralDataGenerator.Open(compressedOut, PgpLiteralData.Binary, file);
}

private static PgpSignatureGenerator InitSignatureGenerator(Stream compressedOut, PgpEncryptionKeys m_encryptionKeys)
{
const bool IsCritical = false;
const bool IsNested = false;
PublicKeyAlgorithmTag tag = m_encryptionKeys.SecretKey.PublicKey.Algorithm;
PgpSignatureGenerator pgpSignatureGenerator = new PgpSignatureGenerator(tag, HashAlgorithmTag.Sha1);
pgpSignatureGenerator.InitSign(PgpSignature.BinaryDocument, m_encryptionKeys.PrivateKey);
foreach (string userId in m_encryptionKeys.SecretKey.PublicKey.GetUserIds())
{
PgpSignatureSubpacketGenerator subPacketGenerator = new PgpSignatureSubpacketGenerator();
subPacketGenerator.SetSignerUserId(IsCritical, userId);
pgpSignatureGenerator.SetHashedSubpackets(subPacketGenerator.Generate());
// Just the first one!
break;
}
pgpSignatureGenerator.GenerateOnePassVersion(IsNested).Encode(compressedOut);
return pgpSignatureGenerator;
}

/*
* decrypt a given stream.
*/

public static void Decrypt(string inputfile, string privateKeyFile, string passPhrase, string outputFile)
{
if (!File.Exists(inputfile))
throw new FileNotFoundException(String.Format(“Encrypted File [{0}] not found.”, inputfile));

if (!File.Exists(privateKeyFile))
throw new FileNotFoundException(String.Format(“Private Key File [{0}] not found.”, privateKeyFile));

if (String.IsNullOrEmpty(outputFile))
throw new ArgumentNullException(“Invalid Output file path.”);

using (Stream inputStream = File.OpenRead(inputfile))
{
using (Stream keyIn = File.OpenRead(privateKeyFile))
{
Decrypt(inputStream, keyIn, passPhrase, outputFile);
}
}
}

/*
* decrypt a given stream.
*/

public static void Decrypt(Stream inputStream, Stream privateKeyStream, string passPhrase, string outputFile)
{
try
{
PgpObjectFactory pgpF = null;
PgpEncryptedDataList enc = null;
PgpObject o = null;
PgpPrivateKey sKey = null;
PgpPublicKeyEncryptedData pbe = null;
PgpSecretKeyRingBundle pgpSec = null;

pgpF = new PgpObjectFactory(PgpUtilities.GetDecoderStream(inputStream));
// find secret key
pgpSec = new PgpSecretKeyRingBundle(PgpUtilities.GetDecoderStream(privateKeyStream));

if (pgpF != null)
o = pgpF.NextPgpObject();

// the first object might be a PGP marker packet.
if (o is PgpEncryptedDataList)
enc = (PgpEncryptedDataList)o;
else
enc = (PgpEncryptedDataList)pgpF.NextPgpObject();

// decrypt
foreach (PgpPublicKeyEncryptedData pked in enc.GetEncryptedDataObjects())
{
sKey = FindSecretKey(pgpSec, pked.KeyId, passPhrase.ToCharArray());

if (sKey != null)
{
pbe = pked;
break;
}
}

if (sKey == null)
throw new ArgumentException(“Secret key for message not found.”);

PgpObjectFactory plainFact = null;

using (Stream clear = pbe.GetDataStream(sKey))
{
plainFact = new PgpObjectFactory(clear);
}

PgpObject message = plainFact.NextPgpObject();

if (message is PgpCompressedData)
{
PgpCompressedData cData = (PgpCompressedData)message;
PgpObjectFactory of = null;

using (Stream compDataIn = cData.GetDataStream())
{
of = new PgpObjectFactory(compDataIn);
}

message = of.NextPgpObject();
if (message is PgpOnePassSignatureList)
{
message = of.NextPgpObject();
PgpLiteralData Ld = null;
Ld = (PgpLiteralData)message;
using (Stream output = File.Create(outputFile))
{
Stream unc = Ld.GetInputStream();
Streams.PipeAll(unc, output);
}
}
else
{
PgpLiteralData Ld = null;
Ld = (PgpLiteralData)message;
using (Stream output = File.Create(outputFile))
{
Stream unc = Ld.GetInputStream();
Streams.PipeAll(unc, output);
}
}
}
else if (message is PgpLiteralData)
{
PgpLiteralData ld = (PgpLiteralData)message;
string outFileName = ld.FileName;

using (Stream fOut = File.Create(outputFile))
{
Stream unc = ld.GetInputStream();
Streams.PipeAll(unc, fOut);
}
}
else if (message is PgpOnePassSignatureList)
throw new PgpException(“Encrypted message contains a signed message — not literal data.”);
else
throw new PgpException(“Message is not a simple encrypted file — type unknown.”);

}
catch (PgpException ex)
{
throw;
}
}

private static PgpPublicKey ReadPublicKey(Stream inputStream)
{
inputStream = PgpUtilities.GetDecoderStream(inputStream);

PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(inputStream);

foreach (PgpPublicKeyRing kRing in pgpPub.GetKeyRings())
{
foreach (PgpPublicKey k in kRing.GetPublicKeys())
{
if (k.IsEncryptionKey)
return k;
}
}
throw new ArgumentException(“Can’t find encryption key in key ring.”);
}
private static PgpPrivateKey FindSecretKey(PgpSecretKeyRingBundle pgpSec, long keyId, char[] pass)
{
PgpSecretKey pgpSecKey = pgpSec.GetSecretKey(keyId);
if (pgpSecKey == null)
return null;
return pgpSecKey.ExtractPrivateKey(pass);
}

}
}

PgpEncryptionKeys.cs

using Org.BouncyCastle.Bcpg.OpenPgp;

using System;

using System.Collections.Generic;

using System.IO;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace PGPNET1

{

public class PgpEncryptionKeys

{

public PgpPublicKey PublicKey { get; set; }

public PgpPrivateKey PrivateKey { get; set; }

public PgpSecretKey SecretKey { get; set; }

public PgpEncryptionKeys(string publicKeyPath, string privateKeyPath, string passPhrase)

{

if (!File.Exists(publicKeyPath))

throw new ArgumentException(“Public key file not found”, “publicKeyPath”);

if (!File.Exists(privateKeyPath))

throw new ArgumentException(“Private key file not found”, “privateKeyPath”);

if (String.IsNullOrEmpty(passPhrase))

throw new ArgumentException(“passPhrase is null or empty.”, “passPhrase”);

//PublicKey = ReadPublicKey(publicKeyPath);

//PrivateKey = ReadPrivateKey(privateKeyPath);

//SecretKey = ReadSecretKey(passPhrase);

PublicKey = ReadPublicKey(publicKeyPath);

SecretKey = ReadSecretKey(privateKeyPath);

PrivateKey = ReadPrivateKey(passPhrase);

}

private PgpSecretKey ReadSecretKey(string privateKeyPath)

{

using (Stream keyIn = File.OpenRead(privateKeyPath))

{

using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))

{

PgpSecretKeyRingBundle secretKeyRingBundle = new PgpSecretKeyRingBundle(inputStream);

PgpSecretKey foundKey = GetFirstSecretKey(secretKeyRingBundle);

if (foundKey != null)

return foundKey;

}

}

throw new ArgumentException(“Can’t find signing key in key ring.”);

}

private PgpSecretKey GetFirstSecretKey(PgpSecretKeyRingBundle secretKeyRingBundle)

{

foreach(PgpSecretKeyRing kRing in secretKeyRingBundle.GetKeyRings())

{

PgpSecretKey key = kRing.GetSecretKeys()

.Cast<PgpSecretKey>()

.Where(k => k.IsSigningKey)

.FirstOrDefault();

if (key != null)

return key;

}

return null;

}

private PgpPublicKey ReadPublicKey(string publicKeyPath)

{

using (Stream keyIn = File.OpenRead(publicKeyPath))

{

using (Stream inputStream = PgpUtilities.GetDecoderStream(keyIn))

{

PgpPublicKeyRingBundle publicKeyRingBundle = new PgpPublicKeyRingBundle(inputStream);

PgpPublicKey foundKey = GetFirstPublicKey(publicKeyRingBundle);

if (foundKey != null)

return foundKey;

}

}

throw new ArgumentException(“No encryption key found in public key ring.”);

}

private PgpPublicKey GetFirstPublicKey(PgpPublicKeyRingBundle publicKeyRingBundle)

{

foreach (PgpPublicKeyRing kRing in publicKeyRingBundle.GetKeyRings())

{

PgpPublicKey key = kRing.GetPublicKeys()

.Cast<PgpPublicKey>()

.Where(k => k.IsEncryptionKey)

.FirstOrDefault();

if (key != null)

return key;

}

return null;

}

private PgpPrivateKey ReadPrivateKey(string passPhrase)

{

PgpPrivateKey privateKey = SecretKey.ExtractPrivateKey(passPhrase.ToCharArray());

if (privateKey != null)

return privateKey;

throw new ArgumentException(“No private key found in secret key.”);

}

}

}

Program.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace PGPNET1

{

class Program

{

static void Main(string[] args)

{

PGPEncryptDecrypt.EncryptFile(@”D:\PGPProject\TESTDATA.txt”, @”D:\PGPProject\TESTDATAEncrypt.pgp”, @”D:\PGPProject\PublicKey\TESTGPG_0xCBEDD228_public.asc”, false, true);

//PGPEncryptDecrypt.Decrypt(@”D:\PGPProject\TESTDATAEncrypt.pgp”, @”D:\PGPProject\PrivateKey\TESTGPG_0xCBEDD228_SECRET.asc”, “Test@1234”, @”D:\PGPProject\TESTDATA_Out.txt”);

}

}

}

ทดสอบกันเถอะ !!!!

1.เราจะทำการ Encryt TESTDATA

Run:

PGPEncryptDecrypt.EncryptFile(@”D:\PGPProject\TESTDATA.txt”, @”D:\PGPProject\TESTDATAEncrypt.pgp”, @”D:\PGPProject\PublicKey\TESTGPG_0xCBEDD228_public.asc”, false, true);

Result: จะได้ TESTDATAEncrypt.pgp

TESTDATA.txt ก่อนการ Encrypt หน้าตาจะประมาณนี้

ลองเปิดไฟล์ TESTDATAEncrypt.pgp (ภาษาไรไม่รุ้ไม่รุ้เรื่อง)

2. Decrypt: TESTDATAEncrypt.pgp

Run:

PGPEncryptDecrypt.Decrypt(@”D:\PGPProject\TESTDATAEncrypt.pgp”, @”D:\PGPProject\PrivateKey\TESTGPG_0xCBEDD228_SECRET.asc”, “Test@1234”, @”D:\PGPProject\TESTDATA_Out.txt”);

Result:

TESTDATA_OUT.txt คือไฟล์ที่ถูก Decrypt เรีนบร้อยแล้ส

TESTDATA_OUT.txt หน้าตาประมาณนี้

สรุป

บทความนี้น่าจะช่วยอธิบายการ Encryption/Decryption by PGP ด้วย C# ให้รายๆคนที่มองหาวิธีในการ Encryption/Decryption ทั้งไฟล์ได้

References

  1. https://stackoverflow.com/questions/4192296/c-sharp-how-to-simply-encrypt-a-text-file-with-a-pgp-public-key
  2. https://www.codeproject.com/Questions/69856/Encrypting-and-Decrypting-files-with-PGP-and-C
  3. https://stackoverflow.com/questions/25441366/bouncycastle-in-memory-pgp-encryption-in-c-sharp
  4. https://stackoverflow.com/questions/10209291/pgp-encrypt-and-decrypt