diff --git a/MimeKit/Cryptography/CryptographyContext.cs b/MimeKit/Cryptography/CryptographyContext.cs index dfeea4d473..7ea4606f46 100644 --- a/MimeKit/Cryptography/CryptographyContext.cs +++ b/MimeKit/Cryptography/CryptographyContext.cs @@ -44,7 +44,7 @@ public abstract class CryptographyContext : IDisposable { const string SubclassAndRegisterFormat = "You need to subclass {0} and then register it with MimeKit.Cryptography.CryptographyContext.Register()."; static Func SecureMimeContextFactory; - static Func OpenPgpContextFactory; + static Func PgpContextFactory; static readonly object mutex = new object (); EncryptionAlgorithm[] encryptionAlgorithmRank; @@ -549,8 +549,8 @@ public static CryptographyContext Create (string protocol) case "application/pgp-encrypted": case "application/x-pgp-keys": case "application/pgp-keys": - if (OpenPgpContextFactory != null) - return OpenPgpContextFactory (); + if (PgpContextFactory != null) + return PgpContextFactory (); throw new NotSupportedException (string.Format (SubclassAndRegisterFormat, "MimeKit.Cryptography.OpenPgpContext or MimeKit.Cryptography.GnuPGContext")); default: @@ -596,9 +596,9 @@ public static void Register (Type type) lock (mutex) { SecureMimeContextFactory = () => (SecureMimeContext) ctor.Invoke (new object[0]); } - } else if (info.IsSubclassOf (typeof (OpenPgpContext))) { + } else if (info.IsSubclassOf (typeof (PgpContext))) { lock (mutex) { - OpenPgpContextFactory = () => (OpenPgpContext) ctor.Invoke (new object[0]); + PgpContextFactory = () => (PgpContext) ctor.Invoke (new object[0]); } } else { throw new ArgumentException ("The specified type must be a subclass of SecureMimeContext or OpenPgpContext.", nameof (type)); @@ -635,13 +635,13 @@ public static void Register (Func factory) /// /// is null. /// - public static void Register (Func factory) + public static void Register (Func factory) { if (factory == null) throw new ArgumentNullException(nameof (factory)); lock (mutex) { - OpenPgpContextFactory = factory; + PgpContextFactory = factory; } } } diff --git a/MimeKit/Cryptography/KeyRetrievalResults.cs b/MimeKit/Cryptography/KeyRetrievalResults.cs new file mode 100644 index 0000000000..073f6ad291 --- /dev/null +++ b/MimeKit/Cryptography/KeyRetrievalResults.cs @@ -0,0 +1,48 @@ +// +// OpenPgpContext.cs +// +// Author: Jeffrey Stedfast +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using Org.BouncyCastle.Bcpg.OpenPgp; + +namespace MimeKit.Cryptography +{ + public abstract partial class PgpContext + { + /// + /// Helper class to return both public keyring and public key associated with each other. + /// + public class KeyRetrievalResults + { + public readonly PgpPublicKeyRing KeyRing; + public readonly PgpPublicKey Key; + + public KeyRetrievalResults (PgpPublicKeyRing keyring, PgpPublicKey pubkey) + { + KeyRing = keyring; + Key = pubkey; + } + } + } +} diff --git a/MimeKit/Cryptography/OpenPgpContext.cs b/MimeKit/Cryptography/OpenPgpContext.cs index c6ead0ba53..c89a9ab1d9 100644 --- a/MimeKit/Cryptography/OpenPgpContext.cs +++ b/MimeKit/Cryptography/OpenPgpContext.cs @@ -1,4 +1,4 @@ -// +// // OpenPgpContext.cs // // Author: Jeffrey Stedfast @@ -26,10 +26,7 @@ using System; using System.IO; -using System.Net; using System.Linq; -using System.Text; -using System.Net.Http; using System.Threading; using System.Diagnostics; using System.Threading.Tasks; @@ -46,48 +43,21 @@ using MimeKit.IO; using MimeKit.Utils; -namespace MimeKit.Cryptography { +namespace MimeKit.Cryptography +{ + // NOTE: This class should really be called "GnuPGContext", since it's based upon the GnuPG way to handle keys. + // However, renaming it now is impossible, since that would break every single class currently inheriting form it :/ /// - /// An abstract OpenPGP cryptography context which can be used for PGP/MIME. + /// An abstract OpenPGP cryptography context which can be used for PGP/MIME that is based upon GnuPG + /// files to store PGP keys. /// /// /// Generally speaking, applications should not use a /// directly, but rather via higher level APIs such as /// and . /// - public abstract class OpenPgpContext : CryptographyContext + public abstract class OpenPgpContext : PgpContext { - static readonly string[] ProtocolSubtypes = { "pgp-signature", "pgp-encrypted", "pgp-keys", "x-pgp-signature", "x-pgp-encrypted", "x-pgp-keys" }; - const string BeginPublicKeyBlock = "-----BEGIN PGP PUBLIC KEY BLOCK-----"; - const string EndPublicKeyBlock = "-----END PGP PUBLIC KEY BLOCK-----"; - - internal static readonly EncryptionAlgorithm[] DefaultEncryptionAlgorithmRank = { - EncryptionAlgorithm.Idea, - EncryptionAlgorithm.TripleDes, - EncryptionAlgorithm.Cast5, - EncryptionAlgorithm.Blowfish, - EncryptionAlgorithm.Aes128, - EncryptionAlgorithm.Aes192, - EncryptionAlgorithm.Aes256, - EncryptionAlgorithm.Twofish, - EncryptionAlgorithm.Camellia128, - EncryptionAlgorithm.Camellia192, - EncryptionAlgorithm.Camellia256 - }; - - internal static readonly DigestAlgorithm[] DefaultDigestAlgorithmRank = { - DigestAlgorithm.Sha1, - DigestAlgorithm.RipeMD160, - DigestAlgorithm.Sha256, - DigestAlgorithm.Sha384, - DigestAlgorithm.Sha512, - DigestAlgorithm.Sha224 - }; - - EncryptionAlgorithm defaultAlgorithm; - HttpClient client; - Uri keyServer; - /// /// Initialize a new instance of the class. /// @@ -96,21 +66,8 @@ public abstract class OpenPgpContext : CryptographyContext /// , , and the /// properties themselves. /// - protected OpenPgpContext () - { - EncryptionAlgorithmRank = DefaultEncryptionAlgorithmRank; - DigestAlgorithmRank = DefaultDigestAlgorithmRank; - - foreach (var algorithm in EncryptionAlgorithmRank) - Enable (algorithm); - - foreach (var algorithm in DigestAlgorithmRank) - Enable (algorithm); - - defaultAlgorithm = EncryptionAlgorithm.Cast5; - - client = new HttpClient (); - } + protected OpenPgpContext () : base() // Base constructor sets all defaults. + { } /// /// Initialize a new instance of the class. @@ -159,71 +116,6 @@ protected OpenPgpContext (string pubring, string secring) : this () } } - /// - /// Get or set the default encryption algorithm. - /// - /// - /// Gets or sets the default encryption algorithm. - /// - /// The encryption algorithm. - /// - /// The specified encryption algorithm is not supported. - /// - public EncryptionAlgorithm DefaultEncryptionAlgorithm { - get { return defaultAlgorithm; } - set { - GetSymmetricKeyAlgorithm (value); - defaultAlgorithm = value; - } - } - - bool IsValidKeyServer { - get { - if (keyServer == null) - return false; - - switch (keyServer.Scheme.ToLowerInvariant ()) { - case "https": case "http": case "hkp": return true; - default: return false; - } - } - } - - /// - /// Get or set the key server to use when automatically retrieving keys. - /// - /// - /// Gets or sets the key server to use when verifying keys that are - /// not already in the public keychain. - /// Only HTTP and HKP protocols are supported. - /// - /// The key server. - /// - /// is not an absolute URI. - /// - public Uri KeyServer { - get { return keyServer; } - set { - if (value != null && !value.IsAbsoluteUri) - throw new ArgumentException ("The key server URI must be absolute.", nameof (value)); - - keyServer = value; - } - } - - /// - /// Get or set whether unknown PGP keys should automtically be retrieved. - /// - /// - /// Gets or sets whether or not the should automatically - /// fetch keys as needed from the keyserver when verifying signatures. - /// Requires a valid to be set. - /// - /// true if unknown PGP keys should automatically be retrieved; otherwise, false. - public bool AutoKeyRetrieve { - get; set; - } - /// /// Get the public keyring path. /// @@ -269,257 +161,13 @@ public PgpSecretKeyRingBundle SecretKeyRingBundle { } /// - /// Get the signature protocol. - /// - /// - /// The signature protocol is used by - /// in order to determine what the protocol parameter of the Content-Type - /// header should be. - /// - /// The signature protocol. - public override string SignatureProtocol { - get { return "application/pgp-signature"; } - } - - /// - /// Get the encryption protocol. - /// - /// - /// The encryption protocol is used by - /// in order to determine what the protocol parameter of the Content-Type - /// header should be. - /// - /// The encryption protocol. - public override string EncryptionProtocol { - get { return "application/pgp-encrypted"; } - } - - /// - /// Get the key exchange protocol. - /// - /// - /// Gets the key exchange protocol. - /// - /// The key exchange protocol. - public override string KeyExchangeProtocol { - get { return "application/pgp-keys"; } - } - - /// - /// Check whether or not the specified protocol is supported. - /// - /// - /// Used in order to make sure that the protocol parameter value specified in either a multipart/signed - /// or multipart/encrypted part is supported by the supplied cryptography context. - /// - /// true if the protocol is supported; otherwise false - /// The protocol. - /// - /// is null. - /// - public override bool Supports (string protocol) - { - if (protocol == null) - throw new ArgumentNullException (nameof (protocol)); - - if (!protocol.StartsWith ("application/", StringComparison.OrdinalIgnoreCase)) - return false; - - int startIndex = "application/".Length; - int subtypeLength = protocol.Length - startIndex; - - for (int i = 0; i < ProtocolSubtypes.Length; i++) { - if (subtypeLength != ProtocolSubtypes[i].Length) - continue; - - if (string.Compare (protocol, startIndex, ProtocolSubtypes[i], 0, subtypeLength, StringComparison.OrdinalIgnoreCase) == 0) - return true; - } - - return false; - } - - /// - /// Get the string name of the digest algorithm for use with the micalg parameter of a multipart/signed part. - /// - /// - /// Maps the to the appropriate string identifier - /// as used by the micalg parameter value of a multipart/signed Content-Type - /// header. For example: - /// - /// AlgorithmName - /// pgp-md5 - /// pgp-sha1 - /// pgp-ripemd160 - /// pgp-md2 - /// pgp-tiger192 - /// pgp-haval-5-160 - /// pgp-sha256 - /// pgp-sha384 - /// pgp-sha512 - /// pgp-sha224 - /// - /// - /// The micalg value. - /// The digest algorithm. - /// - /// is out of range. - /// - public override string GetDigestAlgorithmName (DigestAlgorithm micalg) - { - switch (micalg) { - case DigestAlgorithm.MD5: return "pgp-md5"; - case DigestAlgorithm.Sha1: return "pgp-sha1"; - case DigestAlgorithm.RipeMD160: return "pgp-ripemd160"; - case DigestAlgorithm.MD2: return "pgp-md2"; - case DigestAlgorithm.Tiger192: return "pgp-tiger192"; - case DigestAlgorithm.Haval5160: return "pgp-haval-5-160"; - case DigestAlgorithm.Sha256: return "pgp-sha256"; - case DigestAlgorithm.Sha384: return "pgp-sha384"; - case DigestAlgorithm.Sha512: return "pgp-sha512"; - case DigestAlgorithm.Sha224: return "pgp-sha224"; - case DigestAlgorithm.MD4: return "pgp-md4"; - default: throw new ArgumentOutOfRangeException (nameof (micalg)); - } - } - - /// - /// Get the digest algorithm from the micalg parameter value in a multipart/signed part. + /// Helper method to retrieve a public key, and its keyring, given a key's ID /// - /// - /// Maps the micalg parameter value string back to the appropriate . - /// - /// The digest algorithm. - /// The micalg parameter value. - /// - /// is null. - /// - public override DigestAlgorithm GetDigestAlgorithm (string micalg) - { - if (micalg == null) - throw new ArgumentNullException (nameof (micalg)); - - switch (micalg.ToLowerInvariant ()) { - case "pgp-md5": return DigestAlgorithm.MD5; - case "pgp-sha1": return DigestAlgorithm.Sha1; - case "pgp-ripemd160": return DigestAlgorithm.RipeMD160; - case "pgp-md2": return DigestAlgorithm.MD2; - case "pgp-tiger192": return DigestAlgorithm.Tiger192; - case "pgp-haval-5-160": return DigestAlgorithm.Haval5160; - case "pgp-sha256": return DigestAlgorithm.Sha256; - case "pgp-sha384": return DigestAlgorithm.Sha384; - case "pgp-sha512": return DigestAlgorithm.Sha512; - case "pgp-sha224": return DigestAlgorithm.Sha224; - case "pgp-md4": return DigestAlgorithm.MD4; - default: return DigestAlgorithm.None; - } - } - - static string HexEncode (byte[] data) - { - var fingerprint = new StringBuilder (); - - for (int i = 0; i < data.Length; i++) - fingerprint.Append (data[i].ToString ("x2")); - - return fingerprint.ToString (); - } - - static bool PgpPublicKeyMatches (PgpPublicKey key, MailboxAddress mailbox) - { - var secure = mailbox as SecureMailboxAddress; - - if (secure != null && !string.IsNullOrEmpty (secure.Fingerprint)) { - if (secure.Fingerprint.Length > 16) { - var fingerprint = HexEncode (key.GetFingerprint ()); - - return secure.Fingerprint.Equals (fingerprint, StringComparison.OrdinalIgnoreCase); - } - - var id = ((int) key.KeyId).ToString ("X2"); - - return secure.Fingerprint.EndsWith (id, StringComparison.OrdinalIgnoreCase); - } - - foreach (string userId in key.GetUserIds ()) { - MailboxAddress email; - - if (!MailboxAddress.TryParse (userId, out email)) - continue; - - if (mailbox.Address.Equals (email.Address, StringComparison.OrdinalIgnoreCase)) - return true; - } - - return false; - } - - async Task RetrievePublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken) - { - var scheme = keyServer.Scheme.ToLowerInvariant (); - var uri = new UriBuilder (); - - uri.Scheme = scheme == "hkp" ? "http" : scheme; - uri.Host = keyServer.Host; - - if (keyServer.IsDefaultPort) { - if (scheme == "hkp") - uri.Port = 11371; - } else { - uri.Port = keyServer.Port; - } - - uri.Path = "/pks/lookup"; - uri.Query = string.Format ("op=get&search=0x{0:X}", keyId); - - using (var stream = new MemoryBlockStream ()) { - using (var filtered = new FilteredStream (stream)) { - filtered.Add (new OpenPgpBlockFilter (BeginPublicKeyBlock, EndPublicKeyBlock)); - - if (doAsync) { - using (var response = await client.GetAsync (uri.ToString (), cancellationToken).ConfigureAwait (false)) - await response.Content.CopyToAsync (filtered).ConfigureAwait (false); - } else { -#if !NETSTANDARD1_3 && !NETSTANDARD1_6 - var request = (HttpWebRequest) WebRequest.Create (uri.ToString ()); - using (var response = request.GetResponse ()) { - var content = response.GetResponseStream (); - content.CopyTo (filtered, 4096); - } -#else - using (var response = client.GetAsync (uri.ToString (), cancellationToken).GetAwaiter ().GetResult ()) - response.Content.CopyToAsync (filtered).GetAwaiter ().GetResult (); -#endif - } - - filtered.Flush (); - } - - stream.Position = 0; - - using (var armored = new ArmoredInputStream (stream, true)) { - var bundle = new PgpPublicKeyRingBundle (armored); - - Import (bundle); - - return bundle.GetPublicKeyRing (keyId); - } - } - } - - class KeyRetrievalResults - { - public readonly PgpPublicKeyRing KeyRing; - public readonly PgpPublicKey Key; - - public KeyRetrievalResults (PgpPublicKeyRing keyring, PgpPublicKey pubkey) - { - KeyRing = keyring; - Key = pubkey; - } - } - - async Task GetPublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken) + /// + /// + /// + /// + public override async Task GetPublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken) { foreach (PgpPublicKeyRing ring in PublicKeyRingBundle.GetKeyRings ()) { foreach (PgpPublicKey key in ring.GetPublicKeys ()) { @@ -529,14 +177,8 @@ async Task GetPublicKeyRingAsync (long keyId, bool doAsync, } if (AutoKeyRetrieve && IsValidKeyServer) { - try { - var keyring = await RetrievePublicKeyRingAsync (keyId, doAsync, cancellationToken).ConfigureAwait (false); - - return new KeyRetrievalResults (keyring, keyring.GetPublicKey (keyId)); - } catch (OperationCanceledException) { - throw; - } catch { - } + var keyring = await RetrievePublicKeyRingAsync (keyId, doAsync, cancellationToken).ConfigureAwait (false); + return new KeyRetrievalResults (keyring, keyring.GetPublicKey (keyId)); } return new KeyRetrievalResults (null, null); @@ -696,19 +338,6 @@ public virtual IEnumerable EnumerateSecretKeys (MailboxAddress mai yield break; } - static bool IsExpired (PgpPublicKey pubkey) - { - long seconds = pubkey.GetValidSeconds (); - - if (seconds != 0) { - var expires = pubkey.CreationTime.AddSeconds ((double) seconds); - if (expires <= DateTime.Now) - return true; - } - - return false; - } - /// /// Get the public key associated with the mailbox address. /// @@ -749,7 +378,7 @@ protected virtual PgpPublicKey GetPublicKey (MailboxAddress mailbox) /// /// A public key for one or more of the could not be found. /// - internal protected virtual IList GetPublicKeys (IEnumerable mailboxes) + public override IList GetPublicKeys (IEnumerable mailboxes) { if (mailboxes == null) throw new ArgumentNullException (nameof (mailboxes)); @@ -787,6 +416,27 @@ static bool PgpSecretKeyMatches (PgpSecretKey key, MailboxAddress mailbox) return false; } + /// + /// Get the secret key for a specified key identifier. + /// + /// + /// Gets the secret key for a specified key identifier. + /// + /// The key identifier for the desired secret key. + /// The secret key. + /// + /// The secret key specified by the could not be found. + /// + public override PgpSecretKey GetSecretKey (long keyId) + { + foreach (var key in EnumerateSecretKeys ()) { + if (key.KeyId == keyId) + return key; + } + + throw new PrivateKeyNotFoundException (keyId, "The secret key could not be found."); + } + /// /// Get the signing key associated with the mailbox address. /// @@ -801,7 +451,7 @@ static bool PgpSecretKeyMatches (PgpSecretKey key, MailboxAddress mailbox) /// /// A private key for the specified could not be found. /// - internal protected virtual PgpSecretKey GetSigningKey (MailboxAddress mailbox) + public override PgpSecretKey GetSigningKey (MailboxAddress mailbox) { if (mailbox == null) throw new ArgumentNullException (nameof (mailbox)); @@ -823,87 +473,59 @@ internal protected virtual PgpSecretKey GetSigningKey (MailboxAddress mailbox) } /// - /// Gets the password for key. - /// - /// - /// Gets the password for key. - /// - /// The password for key. - /// The key. - /// - /// The user chose to cancel the password request. - /// - protected abstract string GetPasswordForKey (PgpSecretKey key); - - /// - /// Gets the private key from the specified secret key. + /// Check whether or not a particular mailbox address can be used for signing. /// /// - /// Gets the private key from the specified secret key. + /// Checks whether or not as particular mailbocx address can be used for signing. /// - /// The private key. - /// The secret key. + /// true if the mailbox address can be used for signing; otherwise, false. + /// The signer. /// - /// is null. - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. + /// is null. /// - protected PgpPrivateKey GetPrivateKey (PgpSecretKey key) + public override bool CanSign (MailboxAddress signer) { - int attempts = 0; - string password; - - if (key == null) - throw new ArgumentNullException (nameof (key)); - - do { - if ((password = GetPasswordForKey (key)) == null) - throw new OperationCanceledException (); - - try { - var privateKey = key.ExtractPrivateKey (password.ToCharArray ()); + if (signer == null) + throw new ArgumentNullException (nameof (signer)); - // Note: the private key will be null if the private key is empty. - if (privateKey == null) - break; + foreach (var key in EnumerateSecretKeys (signer)) { + if (!key.IsSigningKey) + continue; - return privateKey; - } catch (Exception ex) { -#if DEBUG - Debug.WriteLine (string.Format ("Failed to extract secret key: {0}", ex)); -#endif - } + var pubkey = key.PublicKey; + if (pubkey.IsRevoked () || IsExpired (pubkey)) + continue; - attempts++; - } while (attempts < 3); + return true; + } - throw new UnauthorizedAccessException (); + return false; } - /// - /// Get the secret key for a specified key identifier. + /// Check whether or not the cryptography context can encrypt to a particular recipient. /// /// - /// Gets the secret key for a specified key identifier. + /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. /// - /// The key identifier for the desired secret key. - /// The secret key. - /// - /// The secret key specified by the could not be found. + /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. + /// The recipient's mailbox address. + /// + /// is null. /// - public virtual PgpSecretKey GetSecretKey (long keyId) + public override bool CanEncrypt (MailboxAddress mailbox) { - foreach (var key in EnumerateSecretKeys ()) { - if (key.KeyId == keyId) - return key; + if (mailbox == null) + throw new ArgumentNullException (nameof (mailbox)); + + foreach (var key in EnumeratePublicKeys (mailbox)) { + if (!key.IsEncryptionKey || key.IsRevoked () || IsExpired (key)) + continue; + + return true; } - throw new PrivateKeyNotFoundException (keyId, "The secret key could not be found."); + return false; } #if false @@ -1131,1212 +753,21 @@ public void SignKey (PgpSecretKey secretKey, PgpPublicKey publicKey, DigestAlgor } /// - /// Gets the equivalent for the - /// specified . + /// Saves the public key-ring bundle. /// /// - /// Maps a to the equivalent . + /// Atomically saves the public key-ring bundle to the path specified by . + /// Called by if any public keys were successfully imported. /// - /// The hash algorithm. - /// The digest algorithm. - /// - /// is out of range. - /// - /// - /// is not a supported digest algorithm. + /// + /// An error occured while saving the public key-ring bundle. /// - public static HashAlgorithmTag GetHashAlgorithm (DigestAlgorithm digestAlgo) + protected void SavePublicKeyRingBundle () { - switch (digestAlgo) { - case DigestAlgorithm.MD5: return HashAlgorithmTag.MD5; - case DigestAlgorithm.Sha1: return HashAlgorithmTag.Sha1; - case DigestAlgorithm.RipeMD160: return HashAlgorithmTag.RipeMD160; - case DigestAlgorithm.DoubleSha: throw new NotSupportedException ("The Double SHA digest algorithm is not supported."); - case DigestAlgorithm.MD2: return HashAlgorithmTag.MD2; - case DigestAlgorithm.Tiger192: throw new NotSupportedException ("The Tiger-192 digest algorithm is not supported."); - case DigestAlgorithm.Haval5160: throw new NotSupportedException ("The HAVAL 5 160 digest algorithm is not supported."); - case DigestAlgorithm.Sha256: return HashAlgorithmTag.Sha256; - case DigestAlgorithm.Sha384: return HashAlgorithmTag.Sha384; - case DigestAlgorithm.Sha512: return HashAlgorithmTag.Sha512; - case DigestAlgorithm.Sha224: return HashAlgorithmTag.Sha224; - case DigestAlgorithm.MD4: throw new NotSupportedException ("The MD4 digest algorithm is not supported."); - default: throw new ArgumentOutOfRangeException (nameof (digestAlgo)); - } - } - - /// - /// Check whether or not a particular mailbox address can be used for signing. - /// - /// - /// Checks whether or not as particular mailbocx address can be used for signing. - /// - /// true if the mailbox address can be used for signing; otherwise, false. - /// The signer. - /// - /// is null. - /// - public override bool CanSign (MailboxAddress signer) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - foreach (var key in EnumerateSecretKeys (signer)) { - if (!key.IsSigningKey) - continue; - - var pubkey = key.PublicKey; - if (pubkey.IsRevoked () || IsExpired (pubkey)) - continue; - - return true; - } - - return false; - } - - /// - /// Check whether or not the cryptography context can encrypt to a particular recipient. - /// - /// - /// Checks whether or not the cryptography context can be used to encrypt to a particular recipient. - /// - /// true if the cryptography context can be used to encrypt to the designated recipient; otherwise, false. - /// The recipient's mailbox address. - /// - /// is null. - /// - public override bool CanEncrypt (MailboxAddress mailbox) - { - if (mailbox == null) - throw new ArgumentNullException (nameof (mailbox)); - - foreach (var key in EnumeratePublicKeys (mailbox)) { - if (!key.IsEncryptionKey || key.IsRevoked () || IsExpired (key)) - continue; - - return true; - } - - return false; - } - - /// - /// Cryptographically signs the content. - /// - /// - /// Cryptographically signs the content using the specified signer and digest algorithm. - /// - /// A new instance - /// containing the detached signature data. - /// The signer. - /// The digest algorithm to use for signing. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// is out of range. - /// - /// - /// The specified is not supported by this context. - /// - /// - /// A signing key could not be found for . - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public override MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var key = GetSigningKey (signer); - - return Sign (key, digestAlgo, content); - } - - /// - /// Cryptographically signs the content. - /// - /// - /// Cryptographically signs the content using the specified signer and digest algorithm. - /// - /// A new instance - /// containing the detached signature data. - /// The signer. - /// The digest algorithm to use for signing. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// cannot be used for signing. - /// - /// - /// The was out of range. - /// - /// - /// The is not supported. - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public ApplicationPgpSignature Sign (PgpSecretKey signer, DigestAlgorithm digestAlgo, Stream content) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - if (!signer.IsSigningKey) - throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var hashAlgorithm = GetHashAlgorithm (digestAlgo); - var memory = new MemoryBlockStream (); - - using (var armored = new ArmoredOutputStream (memory)) { - armored.SetHeader ("Version", null); - - var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); - using (var compressed = compresser.Open (armored)) { - var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); - var buf = new byte[4096]; - int nread; - - signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); - - while ((nread = content.Read (buf, 0, buf.Length)) > 0) - signatureGenerator.Update (buf, 0, nread); - - var signature = signatureGenerator.Generate (); - - signature.Encode (compressed); - compressed.Flush (); - } - - armored.Flush (); - } - - memory.Position = 0; - - return new ApplicationPgpSignature (memory); - } - - /// - /// Gets the equivalent for the specified - /// . - /// - /// - /// Gets the equivalent for the specified - /// . - /// - /// The digest algorithm. - /// The hash algorithm. - /// - /// is out of range. - /// - /// - /// does not have an equivalent value. - /// - public static DigestAlgorithm GetDigestAlgorithm (HashAlgorithmTag hashAlgorithm) - { - switch (hashAlgorithm) { - case HashAlgorithmTag.MD5: return DigestAlgorithm.MD5; - case HashAlgorithmTag.Sha1: return DigestAlgorithm.Sha1; - case HashAlgorithmTag.RipeMD160: return DigestAlgorithm.RipeMD160; - case HashAlgorithmTag.DoubleSha: return DigestAlgorithm.DoubleSha; - case HashAlgorithmTag.MD2: return DigestAlgorithm.MD2; - case HashAlgorithmTag.Tiger192: return DigestAlgorithm.Tiger192; - case HashAlgorithmTag.Haval5pass160: return DigestAlgorithm.Haval5160; - case HashAlgorithmTag.Sha256: return DigestAlgorithm.Sha256; - case HashAlgorithmTag.Sha384: return DigestAlgorithm.Sha384; - case HashAlgorithmTag.Sha512: return DigestAlgorithm.Sha512; - case HashAlgorithmTag.Sha224: return DigestAlgorithm.Sha224; - default: throw new ArgumentOutOfRangeException (nameof (hashAlgorithm)); - } - } - - /// - /// Gets the equivalent for the specified - /// . - /// - /// - /// Gets the equivalent for the specified - /// . - /// - /// The public-key algorithm. - /// The public-key algorithm. - /// - /// is out of range. - /// - /// - /// does not have an equivalent value. - /// - public static PublicKeyAlgorithm GetPublicKeyAlgorithm (PublicKeyAlgorithmTag algorithm) - { - switch (algorithm) { - case PublicKeyAlgorithmTag.RsaGeneral: return PublicKeyAlgorithm.RsaGeneral; - case PublicKeyAlgorithmTag.RsaEncrypt: return PublicKeyAlgorithm.RsaEncrypt; - case PublicKeyAlgorithmTag.RsaSign: return PublicKeyAlgorithm.RsaSign; - case PublicKeyAlgorithmTag.ElGamalGeneral: return PublicKeyAlgorithm.ElGamalGeneral; - case PublicKeyAlgorithmTag.ElGamalEncrypt: return PublicKeyAlgorithm.ElGamalEncrypt; - case PublicKeyAlgorithmTag.Dsa: return PublicKeyAlgorithm.Dsa; - case PublicKeyAlgorithmTag.ECDH: return PublicKeyAlgorithm.EllipticCurve; - case PublicKeyAlgorithmTag.ECDsa: return PublicKeyAlgorithm.EllipticCurveDsa; - case PublicKeyAlgorithmTag.DiffieHellman: return PublicKeyAlgorithm.DiffieHellman; - default: throw new ArgumentOutOfRangeException (nameof (algorithm)); - } - } - - async Task GetDigitalSignaturesAsync (PgpSignatureList signatureList, Stream content, bool doAsync, CancellationToken cancellationToken) - { - var signatures = new List (); - var buf = new byte[4096]; - int nread; - - for (int i = 0; i < signatureList.Count; i++) { - long keyId = signatureList[i].KeyId; - KeyRetrievalResults results; - - if (doAsync) - results = await GetPublicKeyRingAsync (keyId, doAsync, cancellationToken).ConfigureAwait (false); - else - results = GetPublicKeyRingAsync (keyId, doAsync, cancellationToken).GetAwaiter ().GetResult (); - - var signature = new OpenPgpDigitalSignature (results.KeyRing, results.Key, signatureList[i]) { - PublicKeyAlgorithm = GetPublicKeyAlgorithm (signatureList[i].KeyAlgorithm), - DigestAlgorithm = GetDigestAlgorithm (signatureList[i].HashAlgorithm), - CreationDate = signatureList[i].CreationTime, - }; - - if (results.Key != null) - signatureList[i].InitVerify (results.Key); - - signatures.Add (signature); - } - - while ((nread = content.Read (buf, 0, buf.Length)) > 0) { - for (int i = 0; i < signatures.Count; i++) { - if (signatures[i].SignerCertificate != null) { - var pgp = (OpenPgpDigitalSignature) signatures[i]; - pgp.Signature.Update (buf, 0, nread); - } - } - } - - return new DigitalSignatureCollection (signatures); - } - - Task VerifyAsync (Stream content, Stream signatureData, bool doAsync, CancellationToken cancellationToken) - { - if (content == null) - throw new ArgumentNullException (nameof (content)); - - if (signatureData == null) - throw new ArgumentNullException (nameof (signatureData)); - - using (var armored = new ArmoredInputStream (signatureData)) { - var factory = new PgpObjectFactory (armored); - var data = factory.NextPgpObject (); - PgpSignatureList signatureList; - - var compressed = data as PgpCompressedData; - if (compressed != null) { - factory = new PgpObjectFactory (compressed.GetDataStream ()); - data = factory.NextPgpObject (); - } - - if (data == null) - throw new FormatException ("Invalid PGP format."); - - signatureList = (PgpSignatureList) data; - - return GetDigitalSignaturesAsync (signatureList, content, doAsync, cancellationToken); - } - } - - /// - /// Verify the specified content using the detached signatureData. - /// - /// - /// Verifies the specified content using the detached signatureData. - /// If any of the signatures were made with an unrecognized key and is enabled, - /// an attempt will be made to retrieve said key(s). The can be used to cancel - /// key retrieval. - /// - /// A list of digital signatures. - /// The content. - /// The signature data. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// does not contain valid PGP signature data. - /// - public override DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) - { - return VerifyAsync (content, signatureData, false, cancellationToken).GetAwaiter ().GetResult (); - } - - /// - /// Asynchronously verify the specified content using the detached signatureData. - /// - /// - /// Verifies the specified content using the detached signatureData. - /// If any of the signatures were made with an unrecognized key and is enabled, - /// an attempt will be made to retrieve said key(s). The can be used to cancel - /// key retrieval. - /// - /// A list of digital signatures. - /// The content. - /// The signature data. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// does not contain valid PGP signature data. - /// - public override Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) - { - return VerifyAsync (content, signatureData, true, cancellationToken); - } - - static Stream Compress (Stream content, byte[] buf) - { - var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); - var memory = new MemoryBlockStream (); - - using (var compressed = compresser.Open (memory)) { - var literalGenerator = new PgpLiteralDataGenerator (); - - using (var literal = literalGenerator.Open (compressed, 't', "mime.txt", content.Length, DateTime.Now)) { - int nread; - - while ((nread = content.Read (buf, 0, buf.Length)) > 0) - literal.Write (buf, 0, nread); - - literal.Flush (); - } - - compressed.Flush (); - } - - memory.Position = 0; - - return memory; - } - - static Stream Encrypt (PgpEncryptedDataGenerator encrypter, Stream content) - { - var memory = new MemoryBlockStream (); - - using (var armored = new ArmoredOutputStream (memory)) { - var buf = new byte[4096]; - - armored.SetHeader ("Version", null); - - using (var compressed = Compress (content, buf)) { - using (var encrypted = encrypter.Open (armored, compressed.Length)) { - int nread; - - while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) - encrypted.Write (buf, 0, nread); - - encrypted.Flush (); - } - } - - armored.Flush (); - } - - memory.Position = 0; - - return memory; - } - - static SymmetricKeyAlgorithmTag GetSymmetricKeyAlgorithm (EncryptionAlgorithm algorithm) - { - switch (algorithm) { - case EncryptionAlgorithm.Aes128: return SymmetricKeyAlgorithmTag.Aes128; - case EncryptionAlgorithm.Aes192: return SymmetricKeyAlgorithmTag.Aes192; - case EncryptionAlgorithm.Aes256: return SymmetricKeyAlgorithmTag.Aes256; - case EncryptionAlgorithm.Camellia128: return SymmetricKeyAlgorithmTag.Camellia128; - case EncryptionAlgorithm.Camellia192: return SymmetricKeyAlgorithmTag.Camellia192; - case EncryptionAlgorithm.Camellia256: return SymmetricKeyAlgorithmTag.Camellia256; - case EncryptionAlgorithm.Cast5: return SymmetricKeyAlgorithmTag.Cast5; - case EncryptionAlgorithm.Des: return SymmetricKeyAlgorithmTag.Des; - case EncryptionAlgorithm.TripleDes: return SymmetricKeyAlgorithmTag.TripleDes; - case EncryptionAlgorithm.Idea: return SymmetricKeyAlgorithmTag.Idea; - case EncryptionAlgorithm.Blowfish: return SymmetricKeyAlgorithmTag.Blowfish; - case EncryptionAlgorithm.Twofish: return SymmetricKeyAlgorithmTag.Twofish; - default: throw new NotSupportedException (string.Format ("{0} is not supported.", algorithm)); - } - } - - /// - /// Encrypt the specified content for the specified recipients. - /// - /// - /// Encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// A public key could not be found for one or more of the . - /// - public override MimePart Encrypt (IEnumerable recipients, Stream content) - { - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - // TODO: document the exceptions that can be thrown by BouncyCastle - return Encrypt (GetPublicKeys (recipients), content); - } - - /// - /// Encrypt the specified content for the specified recipients. - /// - /// - /// Encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The encryption algorithm. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// A public key could not be found for one or more of the . - /// - /// - /// The specified encryption algorithm is not supported. - /// - public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) - { - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - // TODO: document the exceptions that can be thrown by BouncyCastle - return Encrypt (algorithm, GetPublicKeys (recipients), content); - } - - /// - /// Encrypt the specified content for the specified recipients. - /// - /// - /// Encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The encryption algorithm. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// The specified encryption algorithm is not supported. - /// - public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) - { - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (algorithm), true); - var unique = new HashSet (); - int count = 0; - - foreach (var recipient in recipients) { - if (!recipient.IsEncryptionKey) - throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); - - if (unique.Add (recipient.KeyId)) { - encrypter.AddMethod (recipient); - count++; - } - } - - if (count == 0) - throw new ArgumentException ("No recipients specified.", nameof (recipients)); - - var encrypted = Encrypt (encrypter, content); - - return new MimePart ("application", "octet-stream") { - ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), - Content = new MimeContent (encrypted), - }; - } - - /// - /// Encrypt the specified content for the specified recipients. - /// - /// - /// Encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - public MimePart Encrypt (IEnumerable recipients, Stream content) - { - return Encrypt (defaultAlgorithm, recipients, content); - } - - /// - /// Cryptographically sign and encrypt the specified content for the specified recipients. - /// - /// - /// Cryptographically signs and encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The signer. - /// The digest algorithm to use for signing. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// is out of range. - /// - /// - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// The specified is not supported by this context. - /// - /// - /// The private key could not be found for . - /// - /// - /// A public key could not be found for one or more of the . - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var key = GetSigningKey (signer); - - return SignAndEncrypt (key, digestAlgo, GetPublicKeys (recipients), content); - } - - /// - /// Cryptographically sign and encrypt the specified content for the specified recipients. - /// - /// - /// Cryptographically signs and encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The signer. - /// The digest algorithm to use for signing. - /// The encryption algorithm. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// cannot be used for signing. - /// -or- - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// The specified encryption algorithm is not supported. - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var key = GetSigningKey (signer); - - return SignAndEncrypt (key, digestAlgo, cipherAlgo, GetPublicKeys (recipients), content); - } - - /// - /// Cryptographically sign and encrypt the specified content for the specified recipients. - /// - /// - /// Cryptographically signs and encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The signer. - /// The digest algorithm to use for signing. - /// The encryption algorithm. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// cannot be used for signing. - /// -or- - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// The specified encryption algorithm is not supported. - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) - { - if (signer == null) - throw new ArgumentNullException (nameof (signer)); - - if (!signer.IsSigningKey) - throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); - - if (recipients == null) - throw new ArgumentNullException (nameof (recipients)); - - if (content == null) - throw new ArgumentNullException (nameof (content)); - - var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (cipherAlgo), true); - var hashAlgorithm = GetHashAlgorithm (digestAlgo); - var unique = new HashSet (); - var buf = new byte[4096]; - int nread, count = 0; - - foreach (var recipient in recipients) { - if (!recipient.IsEncryptionKey) - throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); - - if (unique.Add (recipient.KeyId)) { - encrypter.AddMethod (recipient); - count++; - } - } - - if (count == 0) - throw new ArgumentException ("No recipients specified.", nameof (recipients)); - - var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); - - using (var compressed = new MemoryBlockStream ()) { - using (var signed = compresser.Open (compressed)) { - var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); - signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); - var subpacket = new PgpSignatureSubpacketGenerator (); - - foreach (string userId in signer.PublicKey.GetUserIds ()) { - subpacket.SetSignerUserId (false, userId); - break; - } - - signatureGenerator.SetHashedSubpackets (subpacket.Generate ()); - - var onepass = signatureGenerator.GenerateOnePassVersion (false); - onepass.Encode (signed); - - var literalGenerator = new PgpLiteralDataGenerator (); - using (var literal = literalGenerator.Open (signed, 't', "mime.txt", content.Length, DateTime.Now)) { - while ((nread = content.Read (buf, 0, buf.Length)) > 0) { - signatureGenerator.Update (buf, 0, nread); - literal.Write (buf, 0, nread); - } - - literal.Flush (); - } - - var signature = signatureGenerator.Generate (); - signature.Encode (signed); - - signed.Flush (); - } - - compressed.Position = 0; - - var memory = new MemoryBlockStream (); - - using (var armored = new ArmoredOutputStream (memory)) { - armored.SetHeader ("Version", null); - - using (var encrypted = encrypter.Open (armored, compressed.Length)) { - while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) - encrypted.Write (buf, 0, nread); - - encrypted.Flush (); - } - - armored.Flush (); - } - - memory.Position = 0; - - return new MimePart ("application", "octet-stream") { - ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), - Content = new MimeContent (memory) - }; - } - } - - /// - /// Cryptographically sign and encrypt the specified content for the specified recipients. - /// - /// - /// Cryptographically signs and encrypts the specified content for the specified recipients. - /// - /// A new instance - /// containing the encrypted data. - /// The signer. - /// The digest algorithm to use for signing. - /// The recipients. - /// The content. - /// - /// is null. - /// -or- - /// is null. - /// -or- - /// is null. - /// - /// - /// cannot be used for signing. - /// -or- - /// One or more of the recipient keys cannot be used for encrypting. - /// -or- - /// No recipients were specified. - /// - /// - /// The user chose to cancel the password prompt. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) - { - return SignAndEncrypt (signer, digestAlgo, defaultAlgorithm, recipients, content); - } - - async Task DecryptToAsync (Stream encryptedData, Stream decryptedData, bool doAsync, CancellationToken cancellationToken) - { - if (encryptedData == null) - throw new ArgumentNullException (nameof (encryptedData)); - - if (decryptedData == null) - throw new ArgumentNullException (nameof (decryptedData)); - - using (var armored = new ArmoredInputStream (encryptedData)) { - var factory = new PgpObjectFactory (armored); - var obj = factory.NextPgpObject (); - var list = obj as PgpEncryptedDataList; - - if (list == null) { - // probably a PgpMarker... - obj = factory.NextPgpObject (); - - list = obj as PgpEncryptedDataList; - - if (list == null) - throw new PgpException ("Unexpected OpenPGP packet."); - } - - PgpPublicKeyEncryptedData encrypted = null; - PrivateKeyNotFoundException pkex = null; - bool hasEncryptedPackets = false; - PgpSecretKey secret = null; - - foreach (PgpEncryptedData data in list.GetEncryptedDataObjects ()) { - if ((encrypted = data as PgpPublicKeyEncryptedData) == null) - continue; - - hasEncryptedPackets = true; - - try { - secret = GetSecretKey (encrypted.KeyId); - break; - } catch (PrivateKeyNotFoundException ex) { - pkex = ex; - } - } - - if (!hasEncryptedPackets) - throw new PgpException ("No encrypted packets found."); - - if (secret == null) - throw pkex; - - factory = new PgpObjectFactory (encrypted.GetDataStream (GetPrivateKey (secret))); - List onepassList = null; - DigitalSignatureCollection signatures; - PgpSignatureList signatureList = null; - PgpCompressedData compressed = null; - var position = decryptedData.Position; - long nwritten = 0; - - obj = factory.NextPgpObject (); - while (obj != null) { - if (obj is PgpCompressedData) { - if (compressed != null) - throw new PgpException ("Recursive compression packets are not supported."); - - compressed = (PgpCompressedData) obj; - factory = new PgpObjectFactory (compressed.GetDataStream ()); - } else if (obj is PgpOnePassSignatureList) { - if (nwritten == 0) { - var onepasses = (PgpOnePassSignatureList) obj; - - onepassList = new List (); - - for (int i = 0; i < onepasses.Count; i++) { - var onepass = onepasses[i]; - - var results = await GetPublicKeyRingAsync (onepass.KeyId, doAsync, cancellationToken).ConfigureAwait (false); - - if (results.KeyRing == null) { - // too messy, pretend we never found a one-pass signature list - onepassList = null; - break; - } - - onepass.InitVerify (results.Key); - - var signature = new OpenPgpDigitalSignature (results.KeyRing, results.Key, onepass) { - PublicKeyAlgorithm = GetPublicKeyAlgorithm (onepass.KeyAlgorithm), - DigestAlgorithm = GetDigestAlgorithm (onepass.HashAlgorithm), - }; - - onepassList.Add (signature); - } - } - } else if (obj is PgpSignatureList) { - signatureList = (PgpSignatureList) obj; - } else if (obj is PgpLiteralData) { - var literal = (PgpLiteralData) obj; - - using (var stream = literal.GetDataStream ()) { - var buffer = new byte[4096]; - int nread; - - while ((nread = stream.Read (buffer, 0, buffer.Length)) > 0) { - if (onepassList != null) { - // update our one-pass signatures... - for (int index = 0; index < nread; index++) { - byte c = buffer[index]; - - for (int i = 0; i < onepassList.Count; i++) { - var pgp = (OpenPgpDigitalSignature) onepassList[i]; - pgp.OnePassSignature.Update (c); - } - } - } - - if (doAsync) - await decryptedData.WriteAsync (buffer, 0, nread, cancellationToken).ConfigureAwait (false); - else - decryptedData.Write (buffer, 0, nread); - - nwritten += nread; - } - } - } - - obj = factory.NextPgpObject (); - } - - if (signatureList != null) { - if (onepassList != null && signatureList.Count == onepassList.Count) { - for (int i = 0; i < onepassList.Count; i++) { - var pgp = (OpenPgpDigitalSignature) onepassList[i]; - pgp.CreationDate = signatureList[i].CreationTime; - pgp.Signature = signatureList[i]; - } - - signatures = new DigitalSignatureCollection (onepassList); - } else { - decryptedData.Position = position; - signatures = await GetDigitalSignaturesAsync (signatureList, decryptedData, doAsync, cancellationToken).ConfigureAwait (false); - decryptedData.Position = decryptedData.Length; - } - } else { - signatures = null; - } - - return signatures; - } - } - - /// - /// Decrypt an encrypted stream and extract the digital signers if the content was also signed. - /// - /// - /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. - /// If any of the signatures were made with an unrecognized key and is enabled, - /// an attempt will be made to retrieve said key(s). The can be used to cancel - /// key retrieval. - /// - /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. - /// The encrypted data. - /// The stream to write the decrypted data to. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The private key could not be found to decrypt the stream. - /// - /// - /// The user chose to cancel the password prompt. - /// -or- - /// The operation was cancelled via the cancellation token. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - /// - /// An OpenPGP error occurred. - /// - public DigitalSignatureCollection DecryptTo (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) - { - return DecryptToAsync (encryptedData, decryptedData, false, cancellationToken).GetAwaiter ().GetResult (); - } - - /// - /// Asynchronously decrypt an encrypted stream and extract the digital signers if the content was also signed. - /// - /// - /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. - /// If any of the signatures were made with an unrecognized key and is enabled, - /// an attempt will be made to retrieve said key(s). The can be used to cancel - /// key retrieval. - /// - /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. - /// The encrypted data. - /// The stream to write the decrypted data to. - /// The cancellation token. - /// - /// is null. - /// -or- - /// is null. - /// - /// - /// The private key could not be found to decrypt the stream. - /// - /// - /// The user chose to cancel the password prompt. - /// -or- - /// The operation was cancelled via the cancellation token. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - /// - /// An OpenPGP error occurred. - /// - public Task DecryptToAsync (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) - { - return DecryptToAsync (encryptedData, decryptedData, true, cancellationToken); - } - - /// - /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. - /// - /// - /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. - /// - /// The decrypted . - /// The encrypted data. - /// A list of digital signatures if the data was both signed and encrypted. - /// The cancellation token. - /// - /// is null. - /// - /// - /// The private key could not be found to decrypt the stream. - /// - /// - /// The user chose to cancel the password prompt. - /// -or- - /// The operation was cancelled via the cancellation token. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - /// - /// An OpenPGP error occurred. - /// - public MimeEntity Decrypt (Stream encryptedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) - { - using (var decryptedData = new MemoryBlockStream ()) { - signatures = DecryptTo (encryptedData, decryptedData, cancellationToken); - decryptedData.Position = 0; - - return MimeEntity.Load (decryptedData, cancellationToken); - } - } - - /// - /// Decrypts the specified encryptedData. - /// - /// - /// Decrypts the specified encryptedData. - /// - /// The decrypted . - /// The encrypted data. - /// The cancellation token. - /// - /// is null. - /// - /// - /// The private key could not be found to decrypt the stream. - /// - /// - /// The user chose to cancel the password prompt. - /// -or- - /// The operation was cancelled via the cancellation token. - /// - /// - /// 3 bad attempts were made to unlock the secret key. - /// - /// - /// An OpenPGP error occurred. - /// - public override MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)) - { - using (var decryptedData = new MemoryBlockStream ()) { - DecryptTo (encryptedData, decryptedData, cancellationToken); - decryptedData.Position = 0; - - return MimeEntity.Load (decryptedData, cancellationToken); - } - } - - /// - /// Saves the public key-ring bundle. - /// - /// - /// Atomically saves the public key-ring bundle to the path specified by . - /// Called by if any public keys were successfully imported. - /// - /// - /// An error occured while saving the public key-ring bundle. - /// - protected void SavePublicKeyRingBundle () - { - var filename = Path.GetFileName (PublicKeyRingPath) + "~"; - var dirname = Path.GetDirectoryName (PublicKeyRingPath); - var tmp = Path.Combine (dirname, "." + filename); - var bak = Path.Combine (dirname, filename); + var filename = Path.GetFileName (PublicKeyRingPath) + "~"; + var dirname = Path.GetDirectoryName (PublicKeyRingPath); + var tmp = Path.Combine (dirname, "." + filename); + var bak = Path.Combine (dirname, filename); Directory.CreateDirectory (dirname); @@ -2426,7 +857,7 @@ public virtual void Import (PgpPublicKeyRing keyring) /// /// is null. /// - public virtual void Import (PgpPublicKeyRingBundle bundle) + public override void Import (PgpPublicKeyRingBundle bundle) { if (bundle == null) throw new ArgumentNullException (nameof (bundle)); @@ -2729,22 +1160,5 @@ public virtual void Delete (PgpSecretKeyRing keyring) SecretKeyRingBundle = PgpSecretKeyRingBundle.RemoveSecretKeyRing (SecretKeyRingBundle, keyring); SaveSecretKeyRingBundle (); } - - /// - /// Releases all resources used by the object. - /// - /// Call when you are finished using the . The - /// method leaves the in an unusable state. After - /// calling , you must release all references to the so - /// the garbage collector can reclaim the memory that the was occupying. - protected override void Dispose (bool disposing) - { - if (disposing && client != null) { - client.Dispose (); - client = null; - } - - base.Dispose (disposing); - } } } diff --git a/MimeKit/Cryptography/PgpContext.cs b/MimeKit/Cryptography/PgpContext.cs new file mode 100644 index 0000000000..a7edd7cd1e --- /dev/null +++ b/MimeKit/Cryptography/PgpContext.cs @@ -0,0 +1,1747 @@ +// +// OpenPgpContext.cs +// +// Author: Jeffrey Stedfast and Thomas Hansen +// +// Copyright (c) 2013-2020 Xamarin Inc. (www.xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Net.Http; +using System.Threading; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Org.BouncyCastle.Bcpg; +using Org.BouncyCastle.Bcpg.OpenPgp; + +using MimeKit.IO; + +namespace MimeKit.Cryptography +{ + /// + /// An abstract OpenPGP cryptography context which can be used for PGP/MIME. This is a more + /// low level class than OpenPgpContext, since it allows you to persist your keys, any ways you + /// see fit, and is not dependent upon GnuPG in any ways. + /// + /// + /// Generally speaking, applications should not use a + /// directly, but rather via higher level APIs such as + /// and . + /// + public abstract partial class PgpContext : CryptographyContext + { + internal static readonly string[] ProtocolSubtypes = { "pgp-signature", "pgp-encrypted", "pgp-keys", "x-pgp-signature", "x-pgp-encrypted", "x-pgp-keys" }; + protected const string BeginPublicKeyBlock = "-----BEGIN PGP PUBLIC KEY BLOCK-----"; + protected const string EndPublicKeyBlock = "-----END PGP PUBLIC KEY BLOCK-----"; + + internal static readonly EncryptionAlgorithm[] DefaultEncryptionAlgorithmRank = { + EncryptionAlgorithm.Idea, + EncryptionAlgorithm.TripleDes, + EncryptionAlgorithm.Cast5, + EncryptionAlgorithm.Blowfish, + EncryptionAlgorithm.Aes128, + EncryptionAlgorithm.Aes192, + EncryptionAlgorithm.Aes256, + EncryptionAlgorithm.Twofish, + EncryptionAlgorithm.Camellia128, + EncryptionAlgorithm.Camellia192, + EncryptionAlgorithm.Camellia256 + }; + + internal static readonly DigestAlgorithm[] DefaultDigestAlgorithmRank = { + DigestAlgorithm.Sha1, + DigestAlgorithm.RipeMD160, + DigestAlgorithm.Sha256, + DigestAlgorithm.Sha384, + DigestAlgorithm.Sha512, + DigestAlgorithm.Sha224 + }; + + protected EncryptionAlgorithm defaultAlgorithm; + protected HttpClient client; + protected Uri keyServer; + + /// + /// Initialize a new instance of the class. + /// + /// + /// Subclasses choosing to use this constructor MUST set the , + /// , , and the + /// properties themselves. + /// + protected PgpContext () + { + EncryptionAlgorithmRank = DefaultEncryptionAlgorithmRank; + DigestAlgorithmRank = DefaultDigestAlgorithmRank; + + foreach (var algorithm in EncryptionAlgorithmRank) + Enable (algorithm); + + foreach (var algorithm in DigestAlgorithmRank) + Enable (algorithm); + + defaultAlgorithm = EncryptionAlgorithm.Cast5; + + client = new HttpClient (); + } + + /// + /// Imports the specified public key ring bundle. + /// + /// Bundle of keys to import. + public abstract void Import (PgpPublicKeyRingBundle bundle); + + /// + /// Gets the password for key. + /// + /// + /// Gets the password for key. + /// + /// The password for key. + /// The key. + /// + /// The user chose to cancel the password request. + /// + protected abstract string GetPasswordForKey (PgpSecretKey key); + + /// + /// Helper method to retrieve a public key, and its keyring, given a key's ID + /// + /// + /// + /// + /// + public abstract Task GetPublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken); + + /// + /// Get the secret key for a specified key identifier. + /// + /// + /// Gets the secret key for a specified key identifier. + /// + /// The key identifier for the desired secret key. + /// The secret key. + /// + /// The secret key specified by the could not be found. + /// + public abstract PgpSecretKey GetSecretKey (long keyId); + + /// + /// Returns public keys associated with enumerable of MailboxAddresses. + /// Notice, input can also be SecureMailboxAddress, at which point it might contain + /// a fingerprint, allowing you to do lookups according to fingerprints, instead of identity + /// of key. + /// + /// Address list to lookup key(s) for. + /// + public abstract IList GetPublicKeys (IEnumerable mailboxes); + + /// + /// Returns signing key for specified MailboxAddress. + /// Notice, input can also be SecureMailboxAddress, at which point it might contain + /// a fingerprint, allowing you to do lookups according to fingerprints, instead of identity + /// of key. + /// + /// Address to lookup key for. + /// + public abstract PgpSecretKey GetSigningKey (MailboxAddress mailbox); + + /// + /// Get or set the default encryption algorithm. + /// + /// + /// Gets or sets the default encryption algorithm. + /// + /// The encryption algorithm. + /// + /// The specified encryption algorithm is not supported. + /// + public EncryptionAlgorithm DefaultEncryptionAlgorithm { + get { return defaultAlgorithm; } + set { + GetSymmetricKeyAlgorithm (value); + defaultAlgorithm = value; + } + } + + internal protected bool IsValidKeyServer { + get { + if (keyServer == null) + return false; + + switch (keyServer.Scheme.ToLowerInvariant ()) { + case "https": case "http": case "hkp": return true; + default: return false; + } + } + } + + /// + /// Get or set the key server to use when automatically retrieving keys. + /// + /// + /// Gets or sets the key server to use when verifying keys that are + /// not already in the public keychain. + /// Only HTTP and HKP protocols are supported. + /// + /// The key server. + /// + /// is not an absolute URI. + /// + public Uri KeyServer { + get { return keyServer; } + set { + if (value != null && !value.IsAbsoluteUri) + throw new ArgumentException ("The key server URI must be absolute.", nameof (value)); + + keyServer = value; + } + } + + /// + /// Get or set whether unknown PGP keys should automtically be retrieved. + /// + /// + /// Gets or sets whether or not the should automatically + /// fetch keys as needed from the keyserver when verifying signatures. + /// Requires a valid to be set. + /// + /// true if unknown PGP keys should automatically be retrieved; otherwise, false. + public bool AutoKeyRetrieve { + get; set; + } + + /// + /// Get the signature protocol. + /// + /// + /// The signature protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The signature protocol. + public override string SignatureProtocol { + get { return "application/pgp-signature"; } + } + + /// + /// Get the encryption protocol. + /// + /// + /// The encryption protocol is used by + /// in order to determine what the protocol parameter of the Content-Type + /// header should be. + /// + /// The encryption protocol. + public override string EncryptionProtocol { + get { return "application/pgp-encrypted"; } + } + + /// + /// Get the key exchange protocol. + /// + /// + /// Gets the key exchange protocol. + /// + /// The key exchange protocol. + public override string KeyExchangeProtocol { + get { return "application/pgp-keys"; } + } + + /// + /// Check whether or not the specified protocol is supported. + /// + /// + /// Used in order to make sure that the protocol parameter value specified in either a multipart/signed + /// or multipart/encrypted part is supported by the supplied cryptography context. + /// + /// true if the protocol is supported; otherwise false + /// The protocol. + /// + /// is null. + /// + public override bool Supports (string protocol) + { + if (protocol == null) + throw new ArgumentNullException (nameof (protocol)); + + if (!protocol.StartsWith ("application/", StringComparison.OrdinalIgnoreCase)) + return false; + + int startIndex = "application/".Length; + int subtypeLength = protocol.Length - startIndex; + + for (int i = 0; i < ProtocolSubtypes.Length; i++) { + if (subtypeLength != ProtocolSubtypes[i].Length) + continue; + + if (string.Compare (protocol, startIndex, ProtocolSubtypes[i], 0, subtypeLength, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + + return false; + } + + /// + /// Get the string name of the digest algorithm for use with the micalg parameter of a multipart/signed part. + /// + /// + /// Maps the to the appropriate string identifier + /// as used by the micalg parameter value of a multipart/signed Content-Type + /// header. For example: + /// + /// AlgorithmName + /// pgp-md5 + /// pgp-sha1 + /// pgp-ripemd160 + /// pgp-md2 + /// pgp-tiger192 + /// pgp-haval-5-160 + /// pgp-sha256 + /// pgp-sha384 + /// pgp-sha512 + /// pgp-sha224 + /// + /// + /// The micalg value. + /// The digest algorithm. + /// + /// is out of range. + /// + public override string GetDigestAlgorithmName (DigestAlgorithm micalg) + { + switch (micalg) { + case DigestAlgorithm.MD5: return "pgp-md5"; + case DigestAlgorithm.Sha1: return "pgp-sha1"; + case DigestAlgorithm.RipeMD160: return "pgp-ripemd160"; + case DigestAlgorithm.MD2: return "pgp-md2"; + case DigestAlgorithm.Tiger192: return "pgp-tiger192"; + case DigestAlgorithm.Haval5160: return "pgp-haval-5-160"; + case DigestAlgorithm.Sha256: return "pgp-sha256"; + case DigestAlgorithm.Sha384: return "pgp-sha384"; + case DigestAlgorithm.Sha512: return "pgp-sha512"; + case DigestAlgorithm.Sha224: return "pgp-sha224"; + case DigestAlgorithm.MD4: return "pgp-md4"; + default: throw new ArgumentOutOfRangeException (nameof (micalg)); + } + } + + /// + /// Get the digest algorithm from the micalg parameter value in a multipart/signed part. + /// + /// + /// Maps the micalg parameter value string back to the appropriate . + /// + /// The digest algorithm. + /// The micalg parameter value. + /// + /// is null. + /// + public override DigestAlgorithm GetDigestAlgorithm (string micalg) + { + if (micalg == null) + throw new ArgumentNullException (nameof (micalg)); + + switch (micalg.ToLowerInvariant ()) { + case "pgp-md5": return DigestAlgorithm.MD5; + case "pgp-sha1": return DigestAlgorithm.Sha1; + case "pgp-ripemd160": return DigestAlgorithm.RipeMD160; + case "pgp-md2": return DigestAlgorithm.MD2; + case "pgp-tiger192": return DigestAlgorithm.Tiger192; + case "pgp-haval-5-160": return DigestAlgorithm.Haval5160; + case "pgp-sha256": return DigestAlgorithm.Sha256; + case "pgp-sha384": return DigestAlgorithm.Sha384; + case "pgp-sha512": return DigestAlgorithm.Sha512; + case "pgp-sha224": return DigestAlgorithm.Sha224; + case "pgp-md4": return DigestAlgorithm.MD4; + default: return DigestAlgorithm.None; + } + } + + /// + /// Helper method to turn a bunch of byte data, into a string. Useful for + /// among other things generating fingerprints. + /// + /// Byte array that should be turned into string representation. + /// Hexa encoded results from specified input data. + public static string HexEncode (byte[] data) + { + var fingerprint = new StringBuilder (); + + for (int i = 0; i < data.Length; i++) + fingerprint.Append (data[i].ToString ("x2")); + + return fingerprint.ToString (); + } + + /// + /// Returns true if the specified public key matches the specified MailboxAddress. + /// Might do a fingerprint lookup, if given address is of type SecureMailboxAddress. + /// + /// Key to check + /// Address to check + /// True if match. + public static bool PgpPublicKeyMatches (PgpPublicKey key, MailboxAddress mailbox) + { + var secure = mailbox as SecureMailboxAddress; + + if (secure != null && !string.IsNullOrEmpty (secure.Fingerprint)) { + if (secure.Fingerprint.Length > 16) { + var fingerprint = HexEncode (key.GetFingerprint ()); + + return secure.Fingerprint.Equals (fingerprint, StringComparison.OrdinalIgnoreCase); + } + + var id = ((int) key.KeyId).ToString ("X2"); + + return secure.Fingerprint.EndsWith (id, StringComparison.OrdinalIgnoreCase); + } + + foreach (string userId in key.GetUserIds ()) { + MailboxAddress email; + + if (!MailboxAddress.TryParse (userId, out email)) + continue; + + if (mailbox.Address.Equals (email.Address, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + /// + /// Returns true if the specified secret key is matching the specified MailboxAddres. + /// Might do a fingerprint lookup, if given address is of type SecureMailboxAddress. + /// + /// Key to check + /// Address to check + /// True if match. + public static bool PgpSecretKeyMatches (PgpSecretKey key, MailboxAddress mailbox) + { + if (mailbox is SecureMailboxAddress secure && !string.IsNullOrEmpty (secure.Fingerprint)) { + if (secure.Fingerprint.Length > 16) { + var fingerprint = HexEncode (key.PublicKey.GetFingerprint ()); + + return secure.Fingerprint.Equals (fingerprint, StringComparison.OrdinalIgnoreCase); + } + + var id = ((int) key.KeyId).ToString ("X2"); + + return secure.Fingerprint.EndsWith (id, StringComparison.OrdinalIgnoreCase); + } + + foreach (string userId in key.UserIds) { + if (!MailboxAddress.TryParse (userId, out var email)) + continue; + + if (mailbox.Address.Equals (email.Address, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + /// + /// Returns true if the specified public key is expired. + /// + /// Key to check. + /// True if key is expired. + public static bool IsExpired (PgpPublicKey pubkey) + { + long seconds = pubkey.GetValidSeconds (); + + if (seconds != 0) { + var expires = pubkey.CreationTime.AddSeconds ((double) seconds); + if (expires <= DateTime.Now) + return true; + } + + return false; + } + + /// + /// Retrieves the public keyring, using the preferred key server, automatically importing it afterwards. + /// + /// ID of key to retrieve + /// whether or not this invocation should be handled as an async invocation. + /// Cancellation token for operation. + /// + public async Task RetrievePublicKeyRingAsync (long keyId, bool doAsync, CancellationToken cancellationToken) + { + var scheme = keyServer.Scheme.ToLowerInvariant (); + var uri = new UriBuilder (); + + uri.Scheme = scheme == "hkp" ? "http" : scheme; + uri.Host = keyServer.Host; + + if (keyServer.IsDefaultPort) { + if (scheme == "hkp") + uri.Port = 11371; + } else { + uri.Port = keyServer.Port; + } + + uri.Path = "/pks/lookup"; + uri.Query = string.Format ("op=get&search=0x{0:X}", keyId); + + using (var stream = new MemoryBlockStream ()) { + using (var filtered = new FilteredStream (stream)) { + filtered.Add (new OpenPgpBlockFilter (BeginPublicKeyBlock, EndPublicKeyBlock)); + + if (doAsync) { + using (var response = await client.GetAsync (uri.ToString (), cancellationToken).ConfigureAwait (false)) + await response.Content.CopyToAsync (filtered).ConfigureAwait (false); + } else { +#if !NETSTANDARD1_3 && !NETSTANDARD1_6 + var request = (HttpWebRequest) WebRequest.Create (uri.ToString ()); + using (var response = request.GetResponse ()) { + var content = response.GetResponseStream (); + content.CopyTo (filtered, 4096); + } +#else + using (var response = client.GetAsync (uri.ToString (), cancellationToken).GetAwaiter ().GetResult ()) + response.Content.CopyToAsync (filtered).GetAwaiter ().GetResult (); +#endif + } + + filtered.Flush (); + } + + stream.Position = 0; + + using (var armored = new ArmoredInputStream (stream, true)) { + var bundle = new PgpPublicKeyRingBundle (armored); + + Import (bundle); + + return bundle.GetPublicKeyRing (keyId); + } + } + } + + /// + /// Gets the private key from the specified secret key. + /// + /// + /// Gets the private key from the specified secret key. + /// + /// The private key. + /// The secret key. + /// + /// is null. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + protected PgpPrivateKey GetPrivateKey (PgpSecretKey key) + { + int attempts = 0; + string password; + + if (key == null) + throw new ArgumentNullException (nameof (key)); + + do { + if ((password = GetPasswordForKey (key)) == null) + throw new OperationCanceledException (); + + try { + var privateKey = key.ExtractPrivateKey (password.ToCharArray ()); + + // Note: the private key will be null if the private key is empty. + if (privateKey == null) + break; + + return privateKey; + } catch (Exception ex) { +#if DEBUG + Debug.WriteLine (string.Format ("Failed to extract secret key: {0}", ex)); +#endif + } + + attempts++; + } while (attempts < 3); + + throw new UnauthorizedAccessException (); + } + + /// + /// Gets the equivalent for the + /// specified . + /// + /// + /// Maps a to the equivalent . + /// + /// The hash algorithm. + /// The digest algorithm. + /// + /// is out of range. + /// + /// + /// is not a supported digest algorithm. + /// + public static HashAlgorithmTag GetHashAlgorithm (DigestAlgorithm digestAlgo) + { + switch (digestAlgo) { + case DigestAlgorithm.MD5: return HashAlgorithmTag.MD5; + case DigestAlgorithm.Sha1: return HashAlgorithmTag.Sha1; + case DigestAlgorithm.RipeMD160: return HashAlgorithmTag.RipeMD160; + case DigestAlgorithm.DoubleSha: throw new NotSupportedException ("The Double SHA digest algorithm is not supported."); + case DigestAlgorithm.MD2: return HashAlgorithmTag.MD2; + case DigestAlgorithm.Tiger192: throw new NotSupportedException ("The Tiger-192 digest algorithm is not supported."); + case DigestAlgorithm.Haval5160: throw new NotSupportedException ("The HAVAL 5 160 digest algorithm is not supported."); + case DigestAlgorithm.Sha256: return HashAlgorithmTag.Sha256; + case DigestAlgorithm.Sha384: return HashAlgorithmTag.Sha384; + case DigestAlgorithm.Sha512: return HashAlgorithmTag.Sha512; + case DigestAlgorithm.Sha224: return HashAlgorithmTag.Sha224; + case DigestAlgorithm.MD4: throw new NotSupportedException ("The MD4 digest algorithm is not supported."); + default: throw new ArgumentOutOfRangeException (nameof (digestAlgo)); + } + } + + /// + /// Cryptographically signs the content. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// A signing key could not be found for . + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public override MimePart Sign (MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return Sign (key, digestAlgo, content); + } + + /// + /// Cryptographically signs the content. + /// + /// + /// Cryptographically signs the content using the specified signer and digest algorithm. + /// + /// A new instance + /// containing the detached signature data. + /// The signer. + /// The digest algorithm to use for signing. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// + /// + /// The was out of range. + /// + /// + /// The is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public ApplicationPgpSignature Sign (PgpSecretKey signer, DigestAlgorithm digestAlgo, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (!signer.IsSigningKey) + throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var hashAlgorithm = GetHashAlgorithm (digestAlgo); + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + armored.SetHeader ("Version", null); + + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + using (var compressed = compresser.Open (armored)) { + var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); + var buf = new byte[4096]; + int nread; + + signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) + signatureGenerator.Update (buf, 0, nread); + + var signature = signatureGenerator.Generate (); + + signature.Encode (compressed); + compressed.Flush (); + } + + armored.Flush (); + } + + memory.Position = 0; + + return new ApplicationPgpSignature (memory); + } + + /// + /// Gets the equivalent for the specified + /// . + /// + /// + /// Gets the equivalent for the specified + /// . + /// + /// The digest algorithm. + /// The hash algorithm. + /// + /// is out of range. + /// + /// + /// does not have an equivalent value. + /// + public static DigestAlgorithm GetDigestAlgorithm (HashAlgorithmTag hashAlgorithm) + { + switch (hashAlgorithm) { + case HashAlgorithmTag.MD5: return DigestAlgorithm.MD5; + case HashAlgorithmTag.Sha1: return DigestAlgorithm.Sha1; + case HashAlgorithmTag.RipeMD160: return DigestAlgorithm.RipeMD160; + case HashAlgorithmTag.DoubleSha: return DigestAlgorithm.DoubleSha; + case HashAlgorithmTag.MD2: return DigestAlgorithm.MD2; + case HashAlgorithmTag.Tiger192: return DigestAlgorithm.Tiger192; + case HashAlgorithmTag.Haval5pass160: return DigestAlgorithm.Haval5160; + case HashAlgorithmTag.Sha256: return DigestAlgorithm.Sha256; + case HashAlgorithmTag.Sha384: return DigestAlgorithm.Sha384; + case HashAlgorithmTag.Sha512: return DigestAlgorithm.Sha512; + case HashAlgorithmTag.Sha224: return DigestAlgorithm.Sha224; + default: throw new ArgumentOutOfRangeException (nameof (hashAlgorithm)); + } + } + + /// + /// Gets the equivalent for the specified + /// . + /// + /// + /// Gets the equivalent for the specified + /// . + /// + /// The public-key algorithm. + /// The public-key algorithm. + /// + /// is out of range. + /// + /// + /// does not have an equivalent value. + /// + public static PublicKeyAlgorithm GetPublicKeyAlgorithm (PublicKeyAlgorithmTag algorithm) + { + switch (algorithm) { + case PublicKeyAlgorithmTag.RsaGeneral: return PublicKeyAlgorithm.RsaGeneral; + case PublicKeyAlgorithmTag.RsaEncrypt: return PublicKeyAlgorithm.RsaEncrypt; + case PublicKeyAlgorithmTag.RsaSign: return PublicKeyAlgorithm.RsaSign; + case PublicKeyAlgorithmTag.ElGamalGeneral: return PublicKeyAlgorithm.ElGamalGeneral; + case PublicKeyAlgorithmTag.ElGamalEncrypt: return PublicKeyAlgorithm.ElGamalEncrypt; + case PublicKeyAlgorithmTag.Dsa: return PublicKeyAlgorithm.Dsa; + case PublicKeyAlgorithmTag.ECDH: return PublicKeyAlgorithm.EllipticCurve; + case PublicKeyAlgorithmTag.ECDsa: return PublicKeyAlgorithm.EllipticCurveDsa; + case PublicKeyAlgorithmTag.DiffieHellman: return PublicKeyAlgorithm.DiffieHellman; + default: throw new ArgumentOutOfRangeException (nameof (algorithm)); + } + } + + async Task GetDigitalSignaturesAsync (PgpSignatureList signatureList, Stream content, bool doAsync, CancellationToken cancellationToken) + { + var signatures = new List (); + var buf = new byte[4096]; + int nread; + + for (int i = 0; i < signatureList.Count; i++) { + long keyId = signatureList[i].KeyId; + KeyRetrievalResults results; + + if (doAsync) + results = await GetPublicKeyRingAsync (keyId, doAsync, cancellationToken).ConfigureAwait (false); + else + results = GetPublicKeyRingAsync (keyId, doAsync, cancellationToken).GetAwaiter ().GetResult (); + + var signature = new OpenPgpDigitalSignature (results.KeyRing, results.Key, signatureList[i]) { + PublicKeyAlgorithm = GetPublicKeyAlgorithm (signatureList[i].KeyAlgorithm), + DigestAlgorithm = GetDigestAlgorithm (signatureList[i].HashAlgorithm), + CreationDate = signatureList[i].CreationTime, + }; + + if (results.Key != null) + signatureList[i].InitVerify (results.Key); + + signatures.Add (signature); + } + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) { + for (int i = 0; i < signatures.Count; i++) { + if (signatures[i].SignerCertificate != null) { + var pgp = (OpenPgpDigitalSignature) signatures[i]; + pgp.Signature.Update (buf, 0, nread); + } + } + } + + return new DigitalSignatureCollection (signatures); + } + + Task VerifyAsync (Stream content, Stream signatureData, bool doAsync, CancellationToken cancellationToken) + { + if (content == null) + throw new ArgumentNullException (nameof (content)); + + if (signatureData == null) + throw new ArgumentNullException (nameof (signatureData)); + + using (var armored = new ArmoredInputStream (signatureData)) { + var factory = new PgpObjectFactory (armored); + var data = factory.NextPgpObject (); + PgpSignatureList signatureList; + + var compressed = data as PgpCompressedData; + if (compressed != null) { + factory = new PgpObjectFactory (compressed.GetDataStream ()); + data = factory.NextPgpObject (); + } + + if (data == null) + throw new FormatException ("Invalid PGP format."); + + signatureList = (PgpSignatureList) data; + + return GetDigitalSignaturesAsync (signatureList, content, doAsync, cancellationToken); + } + } + + /// + /// Verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain valid PGP signature data. + /// + public override DigitalSignatureCollection Verify (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (content, signatureData, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously verify the specified content using the detached signatureData. + /// + /// + /// Verifies the specified content using the detached signatureData. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// A list of digital signatures. + /// The content. + /// The signature data. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// does not contain valid PGP signature data. + /// + public override Task VerifyAsync (Stream content, Stream signatureData, CancellationToken cancellationToken = default (CancellationToken)) + { + return VerifyAsync (content, signatureData, true, cancellationToken); + } + + static Stream Compress (Stream content, byte[] buf) + { + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + var memory = new MemoryBlockStream (); + + using (var compressed = compresser.Open (memory)) { + var literalGenerator = new PgpLiteralDataGenerator (); + + using (var literal = literalGenerator.Open (compressed, 't', "mime.txt", content.Length, DateTime.Now)) { + int nread; + + while ((nread = content.Read (buf, 0, buf.Length)) > 0) + literal.Write (buf, 0, nread); + + literal.Flush (); + } + + compressed.Flush (); + } + + memory.Position = 0; + + return memory; + } + + static Stream Encrypt (PgpEncryptedDataGenerator encrypter, Stream content) + { + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + var buf = new byte[4096]; + + armored.SetHeader ("Version", null); + + using (var compressed = Compress (content, buf)) { + using (var encrypted = encrypter.Open (armored, compressed.Length)) { + int nread; + + while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) + encrypted.Write (buf, 0, nread); + + encrypted.Flush (); + } + } + + armored.Flush (); + } + + memory.Position = 0; + + return memory; + } + + internal static SymmetricKeyAlgorithmTag GetSymmetricKeyAlgorithm (EncryptionAlgorithm algorithm) + { + switch (algorithm) { + case EncryptionAlgorithm.Aes128: return SymmetricKeyAlgorithmTag.Aes128; + case EncryptionAlgorithm.Aes192: return SymmetricKeyAlgorithmTag.Aes192; + case EncryptionAlgorithm.Aes256: return SymmetricKeyAlgorithmTag.Aes256; + case EncryptionAlgorithm.Camellia128: return SymmetricKeyAlgorithmTag.Camellia128; + case EncryptionAlgorithm.Camellia192: return SymmetricKeyAlgorithmTag.Camellia192; + case EncryptionAlgorithm.Camellia256: return SymmetricKeyAlgorithmTag.Camellia256; + case EncryptionAlgorithm.Cast5: return SymmetricKeyAlgorithmTag.Cast5; + case EncryptionAlgorithm.Des: return SymmetricKeyAlgorithmTag.Des; + case EncryptionAlgorithm.TripleDes: return SymmetricKeyAlgorithmTag.TripleDes; + case EncryptionAlgorithm.Idea: return SymmetricKeyAlgorithmTag.Idea; + case EncryptionAlgorithm.Blowfish: return SymmetricKeyAlgorithmTag.Blowfish; + case EncryptionAlgorithm.Twofish: return SymmetricKeyAlgorithmTag.Twofish; + default: throw new NotSupportedException (string.Format ("{0} is not supported.", algorithm)); + } + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// A public key could not be found for one or more of the . + /// + public override MimePart Encrypt (IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + // TODO: document the exceptions that can be thrown by BouncyCastle + return Encrypt (GetPublicKeys (recipients), content); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// A public key could not be found for one or more of the . + /// + /// + /// The specified encryption algorithm is not supported. + /// + public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + // TODO: document the exceptions that can be thrown by BouncyCastle + return Encrypt (algorithm, GetPublicKeys (recipients), content); + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + public MimePart Encrypt (EncryptionAlgorithm algorithm, IEnumerable recipients, Stream content) + { + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (algorithm), true); + var unique = new HashSet (); + int count = 0; + + foreach (var recipient in recipients) { + if (!recipient.IsEncryptionKey) + throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); + + if (unique.Add (recipient.KeyId)) { + encrypter.AddMethod (recipient); + count++; + } + } + + if (count == 0) + throw new ArgumentException ("No recipients specified.", nameof (recipients)); + + var encrypted = Encrypt (encrypter, content); + + return new MimePart ("application", "octet-stream") { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), + Content = new MimeContent (encrypted), + }; + } + + /// + /// Encrypt the specified content for the specified recipients. + /// + /// + /// Encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + public MimePart Encrypt (IEnumerable recipients, Stream content) + { + return Encrypt (defaultAlgorithm, recipients, content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// is out of range. + /// + /// + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified is not supported by this context. + /// + /// + /// The private key could not be found for . + /// + /// + /// A public key could not be found for one or more of the . + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return SignAndEncrypt (key, digestAlgo, GetPublicKeys (recipients), content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (MailboxAddress signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var key = GetSigningKey (signer); + + return SignAndEncrypt (key, digestAlgo, cipherAlgo, GetPublicKeys (recipients), content); + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The encryption algorithm. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The specified encryption algorithm is not supported. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, EncryptionAlgorithm cipherAlgo, IEnumerable recipients, Stream content) + { + if (signer == null) + throw new ArgumentNullException (nameof (signer)); + + if (!signer.IsSigningKey) + throw new ArgumentException ("The specified secret key cannot be used for signing.", nameof (signer)); + + if (recipients == null) + throw new ArgumentNullException (nameof (recipients)); + + if (content == null) + throw new ArgumentNullException (nameof (content)); + + var encrypter = new PgpEncryptedDataGenerator (GetSymmetricKeyAlgorithm (cipherAlgo), true); + var hashAlgorithm = GetHashAlgorithm (digestAlgo); + var unique = new HashSet (); + var buf = new byte[4096]; + int nread, count = 0; + + foreach (var recipient in recipients) { + if (!recipient.IsEncryptionKey) + throw new ArgumentException ("One or more of the recipient keys cannot be used for encrypting.", nameof (recipients)); + + if (unique.Add (recipient.KeyId)) { + encrypter.AddMethod (recipient); + count++; + } + } + + if (count == 0) + throw new ArgumentException ("No recipients specified.", nameof (recipients)); + + var compresser = new PgpCompressedDataGenerator (CompressionAlgorithmTag.ZLib); + + using (var compressed = new MemoryBlockStream ()) { + using (var signed = compresser.Open (compressed)) { + var signatureGenerator = new PgpSignatureGenerator (signer.PublicKey.Algorithm, hashAlgorithm); + signatureGenerator.InitSign (PgpSignature.CanonicalTextDocument, GetPrivateKey (signer)); + var subpacket = new PgpSignatureSubpacketGenerator (); + + foreach (string userId in signer.PublicKey.GetUserIds ()) { + subpacket.SetSignerUserId (false, userId); + break; + } + + signatureGenerator.SetHashedSubpackets (subpacket.Generate ()); + + var onepass = signatureGenerator.GenerateOnePassVersion (false); + onepass.Encode (signed); + + var literalGenerator = new PgpLiteralDataGenerator (); + using (var literal = literalGenerator.Open (signed, 't', "mime.txt", content.Length, DateTime.Now)) { + while ((nread = content.Read (buf, 0, buf.Length)) > 0) { + signatureGenerator.Update (buf, 0, nread); + literal.Write (buf, 0, nread); + } + + literal.Flush (); + } + + var signature = signatureGenerator.Generate (); + signature.Encode (signed); + + signed.Flush (); + } + + compressed.Position = 0; + + var memory = new MemoryBlockStream (); + + using (var armored = new ArmoredOutputStream (memory)) { + armored.SetHeader ("Version", null); + + using (var encrypted = encrypter.Open (armored, compressed.Length)) { + while ((nread = compressed.Read (buf, 0, buf.Length)) > 0) + encrypted.Write (buf, 0, nread); + + encrypted.Flush (); + } + + armored.Flush (); + } + + memory.Position = 0; + + return new MimePart ("application", "octet-stream") { + ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), + Content = new MimeContent (memory) + }; + } + } + + /// + /// Cryptographically sign and encrypt the specified content for the specified recipients. + /// + /// + /// Cryptographically signs and encrypts the specified content for the specified recipients. + /// + /// A new instance + /// containing the encrypted data. + /// The signer. + /// The digest algorithm to use for signing. + /// The recipients. + /// The content. + /// + /// is null. + /// -or- + /// is null. + /// -or- + /// is null. + /// + /// + /// cannot be used for signing. + /// -or- + /// One or more of the recipient keys cannot be used for encrypting. + /// -or- + /// No recipients were specified. + /// + /// + /// The user chose to cancel the password prompt. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + public MimePart SignAndEncrypt (PgpSecretKey signer, DigestAlgorithm digestAlgo, IEnumerable recipients, Stream content) + { + return SignAndEncrypt (signer, digestAlgo, defaultAlgorithm, recipients, content); + } + + async Task DecryptToAsync (Stream encryptedData, Stream decryptedData, bool doAsync, CancellationToken cancellationToken) + { + if (encryptedData == null) + throw new ArgumentNullException (nameof (encryptedData)); + + if (decryptedData == null) + throw new ArgumentNullException (nameof (decryptedData)); + + using (var armored = new ArmoredInputStream (encryptedData)) { + var factory = new PgpObjectFactory (armored); + var obj = factory.NextPgpObject (); + var list = obj as PgpEncryptedDataList; + + if (list == null) { + // probably a PgpMarker... + obj = factory.NextPgpObject (); + + list = obj as PgpEncryptedDataList; + + if (list == null) + throw new PgpException ("Unexpected OpenPGP packet."); + } + + PgpPublicKeyEncryptedData encrypted = null; + PrivateKeyNotFoundException pkex = null; + bool hasEncryptedPackets = false; + PgpSecretKey secret = null; + + foreach (PgpEncryptedData data in list.GetEncryptedDataObjects ()) { + if ((encrypted = data as PgpPublicKeyEncryptedData) == null) + continue; + + hasEncryptedPackets = true; + + try { + secret = GetSecretKey (encrypted.KeyId); + break; + } catch (PrivateKeyNotFoundException ex) { + pkex = ex; + } + } + + if (!hasEncryptedPackets) + throw new PgpException ("No encrypted packets found."); + + if (secret == null) + throw pkex; + + factory = new PgpObjectFactory (encrypted.GetDataStream (GetPrivateKey (secret))); + List onepassList = null; + DigitalSignatureCollection signatures; + PgpSignatureList signatureList = null; + PgpCompressedData compressed = null; + var position = decryptedData.Position; + long nwritten = 0; + + obj = factory.NextPgpObject (); + while (obj != null) { + if (obj is PgpCompressedData) { + if (compressed != null) + throw new PgpException ("Recursive compression packets are not supported."); + + compressed = (PgpCompressedData) obj; + factory = new PgpObjectFactory (compressed.GetDataStream ()); + } else if (obj is PgpOnePassSignatureList) { + if (nwritten == 0) { + var onepasses = (PgpOnePassSignatureList) obj; + + onepassList = new List (); + + for (int i = 0; i < onepasses.Count; i++) { + var onepass = onepasses[i]; + + var results = await GetPublicKeyRingAsync (onepass.KeyId, doAsync, cancellationToken).ConfigureAwait (false); + + if (results.KeyRing == null) { + // too messy, pretend we never found a one-pass signature list + onepassList = null; + break; + } + + onepass.InitVerify (results.Key); + + var signature = new OpenPgpDigitalSignature (results.KeyRing, results.Key, onepass) { + PublicKeyAlgorithm = GetPublicKeyAlgorithm (onepass.KeyAlgorithm), + DigestAlgorithm = GetDigestAlgorithm (onepass.HashAlgorithm), + }; + + onepassList.Add (signature); + } + } + } else if (obj is PgpSignatureList) { + signatureList = (PgpSignatureList) obj; + } else if (obj is PgpLiteralData) { + var literal = (PgpLiteralData) obj; + + using (var stream = literal.GetDataStream ()) { + var buffer = new byte[4096]; + int nread; + + while ((nread = stream.Read (buffer, 0, buffer.Length)) > 0) { + if (onepassList != null) { + // update our one-pass signatures... + for (int index = 0; index < nread; index++) { + byte c = buffer[index]; + + for (int i = 0; i < onepassList.Count; i++) { + var pgp = (OpenPgpDigitalSignature) onepassList[i]; + pgp.OnePassSignature.Update (c); + } + } + } + + if (doAsync) + await decryptedData.WriteAsync (buffer, 0, nread, cancellationToken).ConfigureAwait (false); + else + decryptedData.Write (buffer, 0, nread); + + nwritten += nread; + } + } + } + + obj = factory.NextPgpObject (); + } + + if (signatureList != null) { + if (onepassList != null && signatureList.Count == onepassList.Count) { + for (int i = 0; i < onepassList.Count; i++) { + var pgp = (OpenPgpDigitalSignature) onepassList[i]; + pgp.CreationDate = signatureList[i].CreationTime; + pgp.Signature = signatureList[i]; + } + + signatures = new DigitalSignatureCollection (onepassList); + } else { + decryptedData.Position = position; + signatures = await GetDigitalSignaturesAsync (signatureList, decryptedData, doAsync, cancellationToken).ConfigureAwait (false); + decryptedData.Position = decryptedData.Length; + } + } else { + signatures = null; + } + + return signatures; + } + } + + /// + /// Decrypt an encrypted stream and extract the digital signers if the content was also signed. + /// + /// + /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. + /// The encrypted data. + /// The stream to write the decrypted data to. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public DigitalSignatureCollection DecryptTo (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + return DecryptToAsync (encryptedData, decryptedData, false, cancellationToken).GetAwaiter ().GetResult (); + } + + /// + /// Asynchronously decrypt an encrypted stream and extract the digital signers if the content was also signed. + /// + /// + /// Decrypts an encrypted stream and extracts the digital signers if the content was also signed. + /// If any of the signatures were made with an unrecognized key and is enabled, + /// an attempt will be made to retrieve said key(s). The can be used to cancel + /// key retrieval. + /// + /// The list of digital signatures if the data was both signed and encrypted; otherwise, null. + /// The encrypted data. + /// The stream to write the decrypted data to. + /// The cancellation token. + /// + /// is null. + /// -or- + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public Task DecryptToAsync (Stream encryptedData, Stream decryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + return DecryptToAsync (encryptedData, decryptedData, true, cancellationToken); + } + + /// + /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. + /// + /// + /// Decrypts the specified encryptedData and extracts the digital signers if the content was also signed. + /// + /// The decrypted . + /// The encrypted data. + /// A list of digital signatures if the data was both signed and encrypted. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public MimeEntity Decrypt (Stream encryptedData, out DigitalSignatureCollection signatures, CancellationToken cancellationToken = default (CancellationToken)) + { + using (var decryptedData = new MemoryBlockStream ()) { + signatures = DecryptTo (encryptedData, decryptedData, cancellationToken); + decryptedData.Position = 0; + + return MimeEntity.Load (decryptedData, cancellationToken); + } + } + + /// + /// Decrypts the specified encryptedData. + /// + /// + /// Decrypts the specified encryptedData. + /// + /// The decrypted . + /// The encrypted data. + /// The cancellation token. + /// + /// is null. + /// + /// + /// The private key could not be found to decrypt the stream. + /// + /// + /// The user chose to cancel the password prompt. + /// -or- + /// The operation was cancelled via the cancellation token. + /// + /// + /// 3 bad attempts were made to unlock the secret key. + /// + /// + /// An OpenPGP error occurred. + /// + public override MimeEntity Decrypt (Stream encryptedData, CancellationToken cancellationToken = default (CancellationToken)) + { + using (var decryptedData = new MemoryBlockStream ()) { + DecryptTo (encryptedData, decryptedData, cancellationToken); + decryptedData.Position = 0; + + return MimeEntity.Load (decryptedData, cancellationToken); + } + } + + /// + /// Releases all resources used by the object. + /// + /// Call when you are finished using the . The + /// method leaves the in an unusable state. After + /// calling , you must release all references to the so + /// the garbage collector can reclaim the memory that the was occupying. + protected override void Dispose (bool disposing) + { + if (disposing && client != null) { + client.Dispose (); + client = null; + } + + base.Dispose (disposing); + } + } +} diff --git a/MimeKit/MimeKit.Net45.csproj b/MimeKit/MimeKit.Net45.csproj index 683a7f73f9..46e63e9fe8 100644 --- a/MimeKit/MimeKit.Net45.csproj +++ b/MimeKit/MimeKit.Net45.csproj @@ -1,4 +1,4 @@ - + Debug @@ -98,9 +98,11 @@ + + @@ -293,4 +295,4 @@ - + \ No newline at end of file diff --git a/MimeKit/MimeKit.NetStandard.csproj b/MimeKit/MimeKit.NetStandard.csproj index 8fd56eb451..c10aa73bbc 100644 --- a/MimeKit/MimeKit.NetStandard.csproj +++ b/MimeKit/MimeKit.NetStandard.csproj @@ -109,9 +109,11 @@ + +