Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework to support usage of encryption subkeys #247

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 69 additions & 3 deletions PgpCore.Tests/UnitTests/UnitTestsSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Bcpg.OpenPgp;
using Xunit;
Expand Down Expand Up @@ -358,7 +356,6 @@ public void Decrypt300MbFile_DecryptEncryptedFileWithMemoryUsageLessThan50Mb(Key
// Arrange
long memoryCap = 50 * 1024 * 1024;
long startPeakWorkingSet = Process.GetCurrentProcess().PeakWorkingSet64;

TestFactory testFactory = new TestFactory();
testFactory.Arrange(keyType, fileType);
EncryptionKeys encryptionKeys = new EncryptionKeys(testFactory.PublicKeyFileInfo);
Expand Down Expand Up @@ -1727,6 +1724,42 @@ public void DecryptStream_DecryptEncryptedStream(KeyType keyType)
testFactory.Teardown();
}

[Fact]
public void DecryptStream_DecryptEncryptedStreamWithPreferredKey()
{
// Arrange
TestFactory testFactory = new TestFactory();
testFactory.Arrange(KeyType.Known, FileType.Known);
EncryptionKeys encryptionKeys = new EncryptionKeys(testFactory.PublicKeyStream);
EncryptionKeys decryptionKeys = new EncryptionKeys(testFactory.PrivateKeyStream, testFactory.Password);
PGP pgpEncrypt = new PGP(encryptionKeys);
PGP pgpDecrypt = new PGP(decryptionKeys);

long[] keyIdsInPublicKeyRing = encryptionKeys.PublicKeyRings.First().PgpPublicKeyRing.GetPublicKeys()
.Where(key => key.IsEncryptionKey).Select(key => key.KeyId).ToArray();
foreach (long keyId in keyIdsInPublicKeyRing)
{
// Act
encryptionKeys.UseEncrytionKey(keyId);
using (Stream inputFileStream = testFactory.ContentStream)
using (Stream outputFileStream = File.Create(testFactory.EncryptedContentFilePath))
pgpEncrypt.EncryptStream(inputFileStream, outputFileStream);

using (Stream inputFileStream = testFactory.EncryptedContentStream)
using (Stream outputFileStream = File.Create(testFactory.DecryptedContentFilePath))
pgpDecrypt.DecryptStream(inputFileStream, outputFileStream);

// Assert
Assert.True(testFactory.EncryptedContentFileInfo.Exists);
Assert.True(testFactory.DecryptedContentFileInfo.Exists);
Assert.Equal(testFactory.Content, testFactory.DecryptedContent.Trim());
}
Assert.True(keyIdsInPublicKeyRing.Length > 1);

// Teardown
testFactory.Teardown();
}

[Theory]
[InlineData(KeyType.Generated)]
[InlineData(KeyType.Known)]
Expand Down Expand Up @@ -1891,6 +1924,39 @@ public void Verify_VerifyEncryptedAndSignedStream(KeyType keyType)
// Teardown
testFactory.Teardown();
}

[Fact]
public void Verify_VerifyEncryptedAndSignedStreamForMultipleKeys()
{
// Arrange
TestFactory testFactory = new TestFactory();
testFactory.Arrange(KeyType.Known, FileType.Known);
EncryptionKeys encryptionKeys = new EncryptionKeys(testFactory.PublicKeyStream, testFactory.PrivateKeyStream, testFactory.Password);
PGP pgp = new PGP(encryptionKeys);

// Act
long[] keyIdsInPublicKeyRing = encryptionKeys.PublicKeyRings.First().PgpPublicKeyRing.GetPublicKeys()
.Where(key => key.IsEncryptionKey).Select(key => key.KeyId).ToArray();
foreach (long keyId in keyIdsInPublicKeyRing)
{
encryptionKeys.UseEncrytionKey(keyId);
using (Stream inputFileStream = testFactory.ContentStream)
using (Stream outputFileStream = File.Create(testFactory.EncryptedContentFilePath))
pgp.EncryptStreamAndSign(inputFileStream, outputFileStream);

bool verified = false;

using (Stream inputFileStream = testFactory.EncryptedContentStream)
verified = pgp.VerifyStream(inputFileStream);

// Assert
Assert.True(testFactory.EncryptedContentFileInfo.Exists);
Assert.True(verified);
}

// Teardown
testFactory.Teardown();
}

[Theory]
[InlineData(KeyType.Generated)]
Expand Down
17 changes: 17 additions & 0 deletions PgpCore/EncryptionKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class EncryptionKeys : IEncryptionKeys
{
#region Instance Members (Public)

public IEnumerable<PgpPublicKeyRingWithPreferredKey> PublicKeyRings => _publicKeyRingsWithPreferredKey.Value;
public IEnumerable<PgpPublicKey> EncryptKeys => _encryptKeys.Value;
public IEnumerable<PgpPublicKey> VerificationKeys => _verificationKeys.Value;
public PgpPrivateKey SigningPrivateKey => _signingPrivateKey.Value;
Expand All @@ -33,6 +34,7 @@ public class EncryptionKeys : IEncryptionKeys
private Lazy<PgpPrivateKey> _signingPrivateKey;
private Lazy<PgpSecretKey> _signingSecretKey;
private Lazy<PgpSecretKeyRingBundle> _secretKeys;
private Lazy<IEnumerable<PgpPublicKeyRingWithPreferredKey>> _publicKeyRingsWithPreferredKey;

#endregion Instance Members (Private)

Expand Down Expand Up @@ -362,6 +364,19 @@ public PgpPrivateKey FindSecretKey(long keyId)
return pgpSecKey.ExtractPrivateKey(_passPhrase.ToCharArray());
}

/// <summary>
/// This method will try to find the key with the given keyId in a key ring and set it as the preferred key.
/// If it cannot find the key, it will not change the preferred key.
/// </summary>
/// <param name="keyId">The keyId to find.</param>
public void UseEncrytionKey(long keyId)
{
foreach (PgpPublicKeyRingWithPreferredKey publicKeyRing in PublicKeyRings)
{
publicKeyRing.UsePreferredEncryptionKey(keyId);
}
}

#endregion Public Methods

#region Private Key
Expand Down Expand Up @@ -389,10 +404,12 @@ private void
_masterKey = new Lazy<PgpPublicKey>(() => null);
_encryptKeys = new Lazy<IEnumerable<PgpPublicKey>>(() => null);
_verificationKeys = new Lazy<IEnumerable<PgpPublicKey>>(() => null);
_publicKeyRingsWithPreferredKey = new Lazy<IEnumerable<PgpPublicKeyRingWithPreferredKey>>(() => null);
}
else
{
// Need to consume the stream into a list before it is closed (can happen because of lazy instantiation).
_publicKeyRingsWithPreferredKey = new Lazy<IEnumerable<PgpPublicKeyRingWithPreferredKey>>(() => publicKeyRings.Select(keyRing => new PgpPublicKeyRingWithPreferredKey(keyRing)).ToArray());
_masterKey = new Lazy<PgpPublicKey>(() =>
Utilities.FindMasterKey(publicKeyRings.First()));
_encryptKeys = new Lazy<IEnumerable<PgpPublicKey>>(() =>
Expand Down
1 change: 1 addition & 0 deletions PgpCore/IEncryptionKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace PgpCore
/// </summary>
public interface IEncryptionKeys
{
IEnumerable<PgpPublicKeyRingWithPreferredKey> PublicKeyRings { get; }
IEnumerable<PgpPublicKey> EncryptKeys { get; }
IEnumerable<PgpPublicKey> VerificationKeys { get; }
PgpPrivateKey SigningPrivateKey { get; }
Expand Down
16 changes: 10 additions & 6 deletions PgpCore/PGP.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,9 @@

PgpEncryptedDataGenerator pk =
new PgpEncryptedDataGenerator(SymmetricKeyAlgorithm, withIntegrityCheck, new SecureRandom());
foreach (PgpPublicKey publicKey in EncryptionKeys.EncryptKeys)
foreach (PgpPublicKeyRingWithPreferredKey publicKeyRing in EncryptionKeys.PublicKeyRings)
{
PgpPublicKey publicKey = publicKeyRing.PreferredEncryptionKey ?? publicKeyRing.DefaultEncryptionKey;
pk.AddMethod(publicKey);
}

Expand All @@ -563,7 +564,7 @@
{
PgpCompressedDataGenerator comData = new PgpCompressedDataGenerator(CompressionAlgorithm);
await Utilities.WriteStreamToLiteralDataAsync(comData.Open(@out), FileTypeToChar(), inputStream, name);
comData.Close();

Check warning on line 567 in PgpCore/PGP.cs

View workflow job for this annotation

GitHub Actions / build

'PgpCompressedDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}
else
await Utilities.WriteStreamToLiteralDataAsync(@out, FileTypeToChar(), inputStream, name);
Expand Down Expand Up @@ -670,8 +671,9 @@
PgpEncryptedDataGenerator pk =
new PgpEncryptedDataGenerator(SymmetricKeyAlgorithm, withIntegrityCheck, new SecureRandom());

foreach (PgpPublicKey publicKey in EncryptionKeys.EncryptKeys)
foreach (PgpPublicKeyRingWithPreferredKey publicKeyRing in EncryptionKeys.PublicKeyRings)
{
PgpPublicKey publicKey = publicKeyRing.PreferredEncryptionKey ?? publicKeyRing.DefaultEncryptionKey;
pk.AddMethod(publicKey);
}

Expand All @@ -681,7 +683,7 @@
{
PgpCompressedDataGenerator comData = new PgpCompressedDataGenerator(CompressionAlgorithm);
Utilities.WriteStreamToLiteralData(comData.Open(@out), FileTypeToChar(), inputStream, name);
comData.Close();

Check warning on line 686 in PgpCore/PGP.cs

View workflow job for this annotation

GitHub Actions / build

'PgpCompressedDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}
else
Utilities.WriteStreamToLiteralData(@out, FileTypeToChar(), inputStream, name);
Expand Down Expand Up @@ -5688,8 +5690,8 @@
PgpPublicKeyEncryptedData publicKeyEncryptedData = Utilities.ExtractPublicKey(dataList);
var keyIdToVerify = publicKeyEncryptedData.KeyId;
// If we encounter an encrypted packet, verify with the encryption keys used instead
verified = Utilities.FindPublicKey(keyIdToVerify, EncryptionKeys.EncryptKeys, out PgpPublicKey _);

// TODO does this even make sense? maybe throw exception instead, or try to decrypt first
verified = Utilities.FindPublicKeyInKeyRings(keyIdToVerify, EncryptionKeys.PublicKeyRings.Select(keyRing => keyRing.PgpPublicKeyRing), out PgpPublicKey _);
}
else if (pgpObject is PgpOnePassSignatureList onePassSignatureList)
{
Expand Down Expand Up @@ -5835,7 +5837,8 @@
// Verify against public key ID and that of any sub keys

// If we encounter an encrypted packet, verify the encryption key used instead
if (Utilities.FindPublicKey(keyIdToVerify, EncryptionKeys.EncryptKeys, out PgpPublicKey _))
// TODO does this even make sense? maybe throw exception instead, or try to decrypt first
if (Utilities.FindPublicKeyInKeyRings(keyIdToVerify, EncryptionKeys.PublicKeyRings.Select(keyRing => keyRing.PgpPublicKeyRing), out PgpPublicKey _))
{
verified = true;
}
Expand Down Expand Up @@ -6148,8 +6151,9 @@
var encryptedDataGenerator =
new PgpEncryptedDataGenerator(SymmetricKeyAlgorithm, withIntegrityCheck, new SecureRandom());

foreach (PgpPublicKey publicKey in EncryptionKeys.EncryptKeys)
foreach (PgpPublicKeyRingWithPreferredKey publicKeyRing in EncryptionKeys.PublicKeyRings)
{
PgpPublicKey publicKey = publicKeyRing.PreferredEncryptionKey ?? publicKeyRing.DefaultEncryptionKey;
encryptedDataGenerator.AddMethod(publicKey);
}

Expand Down
45 changes: 45 additions & 0 deletions PgpCore/PgpPublicKeyRingWithPreferredKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Org.BouncyCastle.Bcpg.OpenPgp;

namespace PgpCore
{
/// <summary>
/// A wrapper class for <see cref="PgpPublicKeyRing"/> that also keeps track of a preferred <see cref="PgpPublicKey"/> to be used for encryption.
/// </summary>
public class PgpPublicKeyRingWithPreferredKey
{
public PgpPublicKeyRing PgpPublicKeyRing { get; set; }
public PgpPublicKey PreferredEncryptionKey { get; private set; } = null;
public PgpPublicKey DefaultEncryptionKey => _defaultEncryptionKey.Value;

private Lazy<PgpPublicKey> _defaultEncryptionKey;
private Lazy<IEnumerable<PgpPublicKey>> _encryptionKeys;

public PgpPublicKeyRingWithPreferredKey(PgpPublicKeyRing publicKeyRing)
{
PgpPublicKeyRing = publicKeyRing;
_defaultEncryptionKey = new Lazy<PgpPublicKey>(() => Utilities.FindBestEncryptionKey(PgpPublicKeyRing));
_encryptionKeys = new Lazy<IEnumerable<PgpPublicKey>>(() => PgpPublicKeyRing.GetPublicKeys().Where(key => key.IsEncryptionKey));
}

/// <summary>
/// Try to find the key with the given keyId and set it as the preferred encryption key.
/// If no key is found, the preferred key is not changed.
/// </summary>
/// <param name="keyId">The keyId to find.</param>
public void UsePreferredEncryptionKey(long? keyId)
{
PreferredEncryptionKey = _encryptionKeys.Value.FirstOrDefault(key => key.KeyId == keyId) ?? PreferredEncryptionKey;
}

/// <summary>
/// Clear the preferred encryption key.
/// </summary>
public void ClearPreferredEncryptionKey()
{
PreferredEncryptionKey = null;
}
}
}
15 changes: 15 additions & 0 deletions PgpCore/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, file.Name, file.Length, file.LastWriteTime);
await PipeFileContentsAsync(file, pOut, 4096);
lData.Close();

Check warning on line 323 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

/// <summary>Write out the passed in file as a literal data packet.</summary>
Expand All @@ -332,7 +332,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, file.Name, file.Length, file.LastWriteTime);
PipeFileContents(file, pOut, 4096);
lData.Close();

Check warning on line 335 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

/// <summary>Write out the passed in file as a literal data packet in partial packet format.</summary>
Expand All @@ -345,7 +345,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, file.Name, file.LastWriteTime, buffer);
await PipeFileContentsAsync(file, pOut, buffer.Length);
lData.Close();

Check warning on line 348 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

/// <summary>Write out the passed in file as a literal data packet in partial packet format.</summary>
Expand All @@ -358,7 +358,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, file.Name, file.LastWriteTime, buffer);
PipeFileContents(file, pOut, buffer.Length);
lData.Close();

Check warning on line 361 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

public static async Task WriteStreamToLiteralDataAsync(
Expand All @@ -370,7 +370,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, name, input.Length, DateTime.Now);
await PipeStreamContentsAsync(input, pOut, 4096);
lData.Close();

Check warning on line 373 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

public static void WriteStreamToLiteralData(
Expand All @@ -382,7 +382,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, name, input.Length, DateTime.Now);
PipeStreamContents(input, pOut, 4096);
lData.Close();

Check warning on line 385 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

public static async Task WriteStreamToLiteralDataAsync(
Expand All @@ -395,7 +395,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, name, DateTime.Now, buffer);
await PipeStreamContentsAsync(input, pOut, buffer.Length);
lData.Close();

Check warning on line 398 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

public static void WriteStreamToLiteralData(
Expand All @@ -408,7 +408,7 @@
PgpLiteralDataGenerator lData = new PgpLiteralDataGenerator();
Stream pOut = lData.Open(output, fileType, name, DateTime.Now, buffer);
PipeStreamContents(input, pOut, buffer.Length);
lData.Close();

Check warning on line 411 in PgpCore/Utilities.cs

View workflow job for this annotation

GitHub Actions / build

'PgpLiteralDataGenerator.Close()' is obsolete: 'Dispose any opened Stream directly'
}

/// <summary>
Expand Down Expand Up @@ -640,6 +640,21 @@
return foundKeys.Any();
}

public static bool FindPublicKeyInKeyRings(long keyId, IEnumerable<PgpPublicKeyRing> publicKeyRings,
out PgpPublicKey verificationKey)
{
verificationKey = null;

foreach (PgpPublicKeyRing publicKeyRing in publicKeyRings)
{
var verificationKeys = publicKeyRing.GetPublicKeys();
if (FindPublicKey(keyId, verificationKeys, out verificationKey))
return true;
}

return false;
}

private static async Task PipeFileContentsAsync(FileInfo file, Stream pOut, int bufSize)
{
using (FileStream inputStream = file.OpenRead())
Expand Down
Loading