Last active
October 29, 2018 08:56
-
-
Save zaus/c0ea1fd8dad5d9590af1 to your computer and use it in GitHub Desktop.
PGP Encryption/Decryption in .NET, via http://blogs.microsoft.co.il/kim/2009/01/23/pgp-zip-encrypted-files-with-c/#comment-611002
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// PGP round-trip encryption -- http://blogs.microsoft.co.il/kim/2009/01/23/pgp-zip-encrypted-files-with-c/#comment-611002 | |
/// </summary> | |
public class PGPEncryptDecrypt | |
{ | |
/** | |
* A simple routine that opens a key ring file and loads the first available key suitable for | |
* encryption. | |
* | |
* @param in | |
* @return | |
* @m_out | |
* @ | |
*/ | |
private static PgpPublicKey ReadPublicKey(Stream inputStream) | |
{ | |
inputStream = PgpUtilities.GetDecoderStream(inputStream); | |
PgpPublicKeyRingBundle pgpPub = new PgpPublicKeyRingBundle(inputStream); | |
// | |
// we just loop through the collection till we find a key suitable for encryption, in the real | |
// world you would probably want to be a bit smarter about this. | |
// | |
// | |
// iterate through the key rings. | |
// | |
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."); | |
} | |
/** | |
* Search a secret key ring collection for a secret key corresponding to | |
* keyId if it exists. | |
* | |
* @param pgpSec a secret key ring collection. | |
* @param keyId keyId we want. | |
* @param pass passphrase to decrypt secret key with. | |
* @return | |
*/ | |
private static PgpPrivateKey FindSecretKey(PgpSecretKeyRingBundle pgpSec, long keyId, char[] pass) | |
{ | |
PgpSecretKey pgpSecKey = pgpSec.GetSecretKey(keyId); | |
if (pgpSecKey == null) | |
{ | |
return null; | |
} | |
return pgpSecKey.ExtractPrivateKey(pass); | |
} | |
/** | |
* decrypt the passed in message stream | |
*/ | |
private static void DecryptStream(Stream inputStream, Stream keyIn, char[] passwd, Stream outputStream) | |
{ | |
inputStream = PgpUtilities.GetDecoderStream(inputStream); | |
var pgpF = new PgpObjectFactory(inputStream); | |
PgpEncryptedDataList enc; | |
var o = pgpF.NextPgpObject(); | |
// | |
// the first object might be a PGP marker packet. | |
// | |
if (o is PgpEncryptedDataList) | |
{ | |
enc = (PgpEncryptedDataList)o; | |
} | |
else | |
{ | |
enc = (PgpEncryptedDataList)pgpF.NextPgpObject(); | |
} | |
// | |
// find the secret key | |
// | |
PgpPrivateKey sKey = null; | |
PgpPublicKeyEncryptedData pbe = null; | |
var pgpSec = new PgpSecretKeyRingBundle(PgpUtilities.GetDecoderStream(keyIn)); | |
foreach (PgpPublicKeyEncryptedData pked in enc.GetEncryptedDataObjects()) | |
{ | |
sKey = FindSecretKey(pgpSec, pked.KeyId, passwd); | |
if (sKey == null) continue; | |
pbe = pked; | |
break; | |
} | |
if (sKey == null) | |
{ | |
throw new ArgumentException("secret key for message not found."); | |
} | |
var clear = pbe.GetDataStream(sKey); | |
var plainFact = new PgpObjectFactory(clear); | |
var message = plainFact.NextPgpObject(); | |
if (message is PgpCompressedData) | |
{ | |
var cData = (PgpCompressedData)message; | |
var pgpFact = new PgpObjectFactory(cData.GetDataStream()); | |
message = pgpFact.NextPgpObject(); | |
} | |
if (message is PgpLiteralData) | |
{ | |
var ld = (PgpLiteralData)message; | |
//string outFileName = ld.FileName; | |
//if (outFileName.Length == 0) | |
//{ | |
// outFileName = defaultFileName; | |
//} | |
using (outputStream) | |
{ | |
var unc = ld.GetInputStream(); | |
Streams.PipeAll(unc, outputStream); | |
} | |
} | |
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."); | |
} | |
if (pbe.IsIntegrityProtected() && !pbe.Verify()) throw new PgpDataValidationException("Message failed integrity check"); | |
} | |
private static void EncryptFile(Stream outputStream, FileInfo fileInfo, PgpPublicKey encKey, bool armor, bool withIntegrityCheck) | |
{ | |
if (armor) outputStream = new ArmoredOutputStream(outputStream); | |
using (var buffer = new MemoryStream()) | |
{ | |
var compressor = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip); | |
PgpUtilities.WriteFileToLiteralData( | |
compressor.Open(buffer), | |
PgpLiteralData.Binary, | |
fileInfo); | |
compressor.Close(); | |
var encryptor = new PgpEncryptedDataGenerator( | |
SymmetricKeyAlgorithmTag.Cast5, | |
withIntegrityCheck, | |
new SecureRandom()); | |
encryptor.AddMethod(encKey); | |
var bytes = buffer.ToArray(); | |
using (var cOut = encryptor.Open(outputStream, bytes.Length)) | |
{ | |
cOut.Write(bytes, 0, bytes.Length); | |
} | |
} | |
if (armor) outputStream.Close(); | |
} | |
#region ------- public methods -------- | |
/// <summary> | |
/// File-based encryption | |
/// </summary> | |
/// <param name="filePath"></param> | |
/// <param name="publicKeyFile"></param> | |
/// <param name="outputFilePath"></param> | |
public static void Encrypt(string filePath, string publicKeyFile, string outputFilePath) | |
{ | |
var fi = new FileInfo(filePath); | |
using (var keyIn = File.OpenRead(publicKeyFile)) | |
using (var fos = File.Create(outputFilePath)) | |
Encrypt(fi, keyIn, fos); | |
} | |
/// <summary> | |
/// File-based decryption | |
/// </summary> | |
/// <param name="filePath"></param> | |
/// <param name="privateKeyFile"></param> | |
/// <param name="passPhrase"></param> | |
/// <param name="pathToSaveFile"></param> | |
public static void Decrypt(string filePath, string privateKeyFile, string passPhrase, string pathToSaveFile) | |
{ | |
using (var output = File.Create(pathToSaveFile)) | |
using (var keyIn = File.OpenRead(privateKeyFile)) | |
using (var fin = File.OpenRead(filePath)) | |
Decrypt(fin, keyIn, passPhrase, output); | |
} | |
/// <summary> | |
/// Stream-based encryption, which relies on a local file for input source | |
/// </summary> | |
/// <param name="input">source to encrypt (local file)</param> | |
/// <param name="publicKey">the public key</param> | |
/// <param name="output">where to save the encrypted data</param> | |
public static void Encrypt(FileInfo input, Stream publicKey, Stream output) | |
{ | |
EncryptFile(output, input, ReadPublicKey(publicKey), true, true); | |
} | |
/// <summary> | |
/// Stream-based decryption | |
/// </summary> | |
/// <param name="input">input source to decrypt</param> | |
/// <param name="privateKey">the private key</param> | |
/// <param name="passPhrase">the password to open the private key</param> | |
/// <param name="output">where to save the decrypted data</param> | |
public static void Decrypt(Stream input, Stream privateKey, string passPhrase, Stream output) | |
{ | |
DecryptStream(input, privateKey, passPhrase.ToCharArray(), output); | |
} | |
public static void GenerateKey(string username, string password, string keyStoreUrl) | |
{ | |
IAsymmetricCipherKeyPairGenerator kpg = GeneratorUtilities.GetKeyPairGenerator("RSA"); | |
// new RsaKeyPairGenerator(); | |
kpg.Init(new RsaKeyGenerationParameters(BigInteger.ValueOf(0x13), new SecureRandom(), 1024, 8)); | |
AsymmetricCipherKeyPair kp = kpg.GenerateKeyPair(); | |
FileStream out1 = new FileInfo(GetPrivateKeyPath(keyStoreUrl)).OpenWrite(); | |
FileStream out2 = new FileInfo(GetPublicKeyPath(keyStoreUrl)).OpenWrite(); | |
ExportKeyPair(out1, out2, kp.Public, kp.Private, PublicKeyAlgorithmTag.RsaGeneral, SymmetricKeyAlgorithmTag.Cast5, username, password.ToCharArray(), true); | |
out1.Close(); | |
out2.Close(); | |
/* | |
IAsymmetricCipherKeyPairGenerator dsaKpg = GeneratorUtilities.GetKeyPairGenerator("DSA"); | |
DsaParametersGenerator pGen = new DsaParametersGenerator(); | |
pGen.Init(1024, 80, new SecureRandom()); | |
DsaParameters dsaParams = pGen.GenerateParameters(); | |
DsaKeyGenerationParameters kgp = new DsaKeyGenerationParameters(new SecureRandom(), dsaParams); | |
dsaKpg.Init(kgp); | |
// | |
// this takes a while as the key generator has to Generate some DSA parameters | |
// before it Generates the key. | |
// | |
AsymmetricCipherKeyPair dsaKp = dsaKpg.GenerateKeyPair(); | |
IAsymmetricCipherKeyPairGenerator elgKpg = GeneratorUtilities.GetKeyPairGenerator("ELGAMAL"); | |
ElGamalParametersGenerator eGen = new ElGamalParametersGenerator(); | |
eGen.Init(1024,80,new SecureRandom()); | |
ElGamalParameters elParams = eGen.GenerateParameters(); | |
ElGamalKeyGenerationParameters elKgp = new ElGamalKeyGenerationParameters(new SecureRandom(), elParams); | |
elgKpg.Init(elKgp); | |
// | |
// this is quicker because we are using preGenerated parameters. | |
// | |
AsymmetricCipherKeyPair elgKp = elgKpg.GenerateKeyPair(); | |
FileStream out3 = new FileInfo(string.Format("{0}_PrivateKey_ELGMAL.txt", keyStoreUrl)).OpenWrite(); | |
FileStream out4 = new FileInfo(string.Format("{0}_PublicKey_ELGMAL.txt", keyStoreUrl)).OpenWrite(); | |
ExportKeyPair(out3, out4, dsaKp, elgKp, username, password.ToCharArray(), true); | |
out3.Close(); | |
out4.Close(); | |
*/ | |
} | |
public void SignAndEncryptFile(string actualFileName, | |
string embeddedFileName, | |
Stream keyIn, | |
long keyId, | |
string OutputFileName, | |
char[] password, | |
bool armor, | |
bool withIntegrityCheck, | |
PgpPublicKey encKey) | |
{ | |
const int BUFFER_SIZE = 1 << 16; // should always be power of 2 | |
Stream outputStream = File.Open(OutputFileName, FileMode.Create); | |
if( armor ) | |
outputStream = new ArmoredOutputStream(outputStream); | |
// Init encrypted data generator | |
PgpEncryptedDataGenerator encryptedDataGenerator = | |
new PgpEncryptedDataGenerator(SymmetricKeyAlgorithmTag.Cast5, withIntegrityCheck, new SecureRandom()); | |
encryptedDataGenerator.AddMethod(encKey); | |
Stream encryptedOut = encryptedDataGenerator.Open(outputStream, new byte[BUFFER_SIZE]); | |
// Init compression | |
PgpCompressedDataGenerator compressedDataGenerator = new PgpCompressedDataGenerator(CompressionAlgorithmTag.Zip); | |
Stream compressedOut = compressedDataGenerator.Open(encryptedOut); | |
// Init signature | |
PgpSecretKeyRingBundle pgpSecBundle = new PgpSecretKeyRingBundle(PgpUtilities.GetDecoderStream(keyIn)); | |
PgpSecretKey pgpSecKey = pgpSecBundle.GetSecretKey(keyId); | |
if( pgpSecKey == null ) | |
throw new ArgumentException(keyId.ToString("X") + " could not be found in specified key ring bundle.", "keyId"); | |
PgpPrivateKey pgpPrivKey = pgpSecKey.ExtractPrivateKey(password); | |
PgpSignatureGenerator signatureGenerator = new PgpSignatureGenerator(pgpSecKey.PublicKey.Algorithm, | |
HashAlgorithmTag.Sha1); | |
signatureGenerator.InitSign(PgpSignature.BinaryDocument, pgpPrivKey); | |
foreach (string userId in pgpSecKey.PublicKey.GetUserIds()) | |
{ | |
PgpSignatureSubpacketGenerator spGen = new PgpSignatureSubpacketGenerator(); | |
spGen.SetSignerUserId(false, userId); | |
signatureGenerator.SetHashedSubpackets(spGen.Generate()); | |
// Just the first one! | |
break; | |
} | |
signatureGenerator.GenerateOnePassVersion(false).Encode(compressedOut); | |
// Create the Literal Data generator output stream | |
PgpLiteralDataGenerator literalDataGenerator = new PgpLiteralDataGenerator(); | |
FileInfo embeddedFile = new FileInfo(embeddedFileName); | |
FileInfo actualFile = new FileInfo(actualFileName); | |
// TODO: Use lastwritetime from source file | |
Stream literalOut = literalDataGenerator.Open(compressedOut, | |
PgpLiteralData.Binary, | |
embeddedFile.Name, | |
actualFile.LastWriteTime, | |
new byte[BUFFER_SIZE]); | |
// Open the input file | |
FileStream inputStream = actualFile.OpenRead(); | |
byte[] buf = new byte[BUFFER_SIZE]; | |
int len; | |
while( (len = inputStream.Read(buf, 0, buf.Length)) > 0 ) | |
{ | |
literalOut.Write(buf, 0, len); | |
signatureGenerator.Update(buf, 0, len); | |
} | |
literalOut.Close(); | |
literalDataGenerator.Close(); | |
signatureGenerator.Generate().Encode(compressedOut); | |
compressedOut.Close(); | |
compressedDataGenerator.Close(); | |
encryptedOut.Close(); | |
encryptedDataGenerator.Close(); | |
inputStream.Close(); | |
if( armor ) | |
outputStream.Close(); | |
} | |
#endregion ------- public methods -------- | |
private static void ExportKeyPair( | |
Stream secretOut, | |
Stream publicOut, | |
AsymmetricKeyParameter publicKey, | |
AsymmetricKeyParameter privateKey, | |
PublicKeyAlgorithmTag PublicKeyAlgorithmTag, | |
SymmetricKeyAlgorithmTag SymmetricKeyAlgorithmTag, | |
string identity, | |
char[] passPhrase, | |
bool armor) | |
{ | |
if( armor ) | |
{ | |
secretOut = new ArmoredOutputStream(secretOut); | |
} | |
PgpSecretKey secretKey = new PgpSecretKey( | |
PgpSignature.DefaultCertification, | |
PublicKeyAlgorithmTag, | |
publicKey, | |
privateKey, | |
DateTime.Now, | |
identity, | |
SymmetricKeyAlgorithmTag, | |
passPhrase, | |
null, | |
null, | |
new SecureRandom() | |
// ,"BC" | |
); | |
secretKey.Encode(secretOut); | |
secretOut.Close(); | |
if( armor ) | |
{ | |
publicOut = new ArmoredOutputStream(publicOut); | |
} | |
PgpPublicKey key = secretKey.PublicKey; | |
key.Encode(publicOut); | |
publicOut.Close(); | |
} | |
private static void ExportKeyPair( | |
Stream secretOut, | |
Stream publicOut, | |
AsymmetricCipherKeyPair dsaKp, | |
AsymmetricCipherKeyPair elgKp, | |
string identity, | |
char[] passPhrase, | |
bool armor) | |
{ | |
if( armor ) | |
{ | |
secretOut = new ArmoredOutputStream(secretOut); | |
} | |
PgpKeyPair dsaKeyPair = new PgpKeyPair(PublicKeyAlgorithmTag.Dsa, dsaKp, DateTime.UtcNow); | |
PgpKeyPair elgKeyPair = new PgpKeyPair(PublicKeyAlgorithmTag.ElGamalEncrypt, elgKp, DateTime.UtcNow); | |
PgpKeyRingGenerator keyRingGen = new PgpKeyRingGenerator(PgpSignature.PositiveCertification, | |
dsaKeyPair, | |
identity, | |
SymmetricKeyAlgorithmTag.Aes256, | |
passPhrase, | |
true, | |
null, | |
null, | |
new SecureRandom()); | |
keyRingGen.AddSubKey(elgKeyPair); | |
keyRingGen.GenerateSecretKeyRing().Encode(secretOut); | |
if( armor ) | |
{ | |
secretOut.Close(); | |
publicOut = new ArmoredOutputStream(publicOut); | |
} | |
keyRingGen.GeneratePublicKeyRing().Encode(publicOut); | |
if( armor ) | |
{ | |
publicOut.Close(); | |
} | |
} | |
// ================================== extra stuff | |
private const string GENERATED_PRIVATE_KEY_PATH_SUFFIX = "_PrivateKey.txt"; | |
private const string GENERATED_PUBLIC_KEY_PATH_SUFFIX = "_PublicKey.txt"; | |
public static string GetPublicKeyPath(string keyStoreUrl) | |
{ | |
return string.Format("{0}{1}", keyStoreUrl, GENERATED_PUBLIC_KEY_PATH_SUFFIX); | |
} | |
public static string GetPrivateKeyPath(string keyStoreUrl) | |
{ | |
return string.Format("{0}{1}", keyStoreUrl, GENERATED_PRIVATE_KEY_PATH_SUFFIX); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[TestClass] | |
public class PGPEncryptDecryptTests : UnitTestsBase | |
{ | |
[TestMethod] | |
public void Pgp_RoundTrip() | |
{ | |
var testname = System.Reflection.MethodBase.GetCurrentMethod().Name; | |
var passphrase = PASSWORD; | |
// make some keys just for this test | |
var keyPath = CreateAndConfirmKeys(testname); | |
// test file paths | |
var pubkeyFile = PGPEncryptDecrypt.GetPublicKeyPath(keyPath); | |
var privkeyFile = PGPEncryptDecrypt.GetPrivateKeyPath(keyPath); | |
var unencryptedSourceFile = getTestPath(testname, "unencrypted.txt"); | |
var encryptedSourceFile = getTestPath(testname, "encrypted.txt"); | |
var decryptedSourceFile = getTestPath(testname, "decrypted.txt"); | |
// some random content | |
var testRoot = getTestPath(testname); | |
if(!Directory.Exists(testRoot)) Directory.CreateDirectory(testRoot); | |
var content = Faker.TextFaker.Sentences(5); | |
File.WriteAllText(unencryptedSourceFile, content); | |
this.Dump("↓ Original Source Text at {0}:", unencryptedSourceFile); | |
this.Dump(content); | |
// encrypt | |
PGPEncryptDecrypt.Encrypt(unencryptedSourceFile, pubkeyFile, encryptedSourceFile); | |
Assert.IsTrue(File.Exists(encryptedSourceFile), "Did not make an encrypted file"); | |
// decrypt | |
PGPEncryptDecrypt.Decrypt(encryptedSourceFile, privkeyFile, passphrase, decryptedSourceFile); | |
Assert.IsTrue(File.Exists(decryptedSourceFile), "Did not make an decrypted file"); | |
// confirm | |
var decryptedContent = File.ReadAllText(decryptedSourceFile); | |
this.Dump(decryptedContent); | |
this.Dump("↑ Decrypted Source Text from {0}:", decryptedSourceFile); | |
Assert.AreEqual(content, decryptedContent, "Decrypted content doesn't match original"); | |
// cleanup | |
File.Delete(pubkeyFile); | |
File.Delete(privkeyFile); | |
File.Delete(unencryptedSourceFile); | |
File.Delete(encryptedSourceFile); | |
File.Delete(decryptedSourceFile); | |
} | |
private const string USERNAME = "theusername"; | |
private const string PASSWORD = "thepassword"; | |
private const string OUTPUT_FOLDER = @"..\..\files\"; | |
[TestMethod] | |
public void Pgp_GenerateKey() | |
{ | |
string subfolder = string.Empty; | |
var keyPath = CreateAndConfirmKeys(subfolder); | |
var pubkeyFile = PGPEncryptDecrypt.GetPublicKeyPath(keyPath); | |
var privkeyFile = PGPEncryptDecrypt.GetPrivateKeyPath(keyPath); | |
// cleanup | |
File.Delete(pubkeyFile); | |
File.Delete(privkeyFile); | |
} | |
/// <summary> | |
/// Where all the test files live | |
/// </summary> | |
/// <param name="path"></param> | |
/// <returns></returns> | |
private static string getTestPath(params string[] path) | |
{ | |
if(null == path || !path.Any()) throw new ArgumentNullException("path"); | |
var newlist = new string[path.Length + 1]; | |
newlist[0] = OUTPUT_FOLDER; | |
path.CopyTo(newlist, 1); | |
return Path.Combine(newlist); | |
} | |
/// <summary> | |
/// Create public/private keys in the given subfolder | |
/// </summary> | |
/// <param name="subfolder"></param> | |
/// <returns>returns the full path to where the keys are located (for use by <see cref="PGPEncryptDecrypt.GetPublicKeyPath"/> and <see cref="PGPEncryptDecrypt.GetPrivateKeyPath"/></returns> | |
private static string CreateAndConfirmKeys(string subfolder) | |
{ | |
var destPath = getTestPath(subfolder); | |
PGPEncryptDecrypt.GenerateKey(USERNAME, PASSWORD, destPath); | |
var expectedPath = PGPEncryptDecrypt.GetPublicKeyPath(destPath); | |
Assert.IsTrue(File.Exists(expectedPath), "Public key not created at {0}", expectedPath); | |
expectedPath = PGPEncryptDecrypt.GetPrivateKeyPath(destPath); | |
Assert.IsTrue(File.Exists(expectedPath), "Private key not created at {0}", expectedPath); | |
return destPath; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment