/Security/GPG.cs
C# | 5775 lines | 4756 code | 581 blank | 438 comment | 1152 complexity | b51005ae4555811860c2929a6b59822a MD5 | raw file
Possible License(s): GPL-2.0
Large files files are truncated, but you can click here to view the full file
- /*
- AdamMil.Security is a .NET library providing OpenPGP-based security.
- http://www.adammil.net/
- Copyright (C) 2008-2013 Adam Milazzo
-
- This program is free software; you can redistribute it and/or
- modify it under the terms of the GNU General Public License
- as published by the Free Software Foundation; either version 2
- of the License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- */
-
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Globalization;
- using System.IO;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Threading;
- using AdamMil.Collections;
- using AdamMil.IO;
- using AdamMil.Security.PGP.GPG.StatusMessages;
- using AdamMil.Utilities;
- using Microsoft.Win32.SafeHandles;
- using SecureString=System.Security.SecureString;
-
- namespace AdamMil.Security.PGP.GPG
- {
-
- /// <summary>Processes text output from GPG.</summary>
- public delegate void TextLineHandler(string line);
-
- #region GPG
- /// <summary>A base class to aid in the implementation of interfaces to the GNU Privacy Guard (GPG).</summary>
- public abstract class GPG : PGPSystem
- {
- /// <summary>Parses an argument from a GPG status message into a cipher name, or null if the cipher type cannot be
- /// determined.
- /// </summary>
- public static string ParseCipher(string str)
- {
- switch((OpenPGPCipher)int.Parse(str, CultureInfo.InvariantCulture))
- {
- case OpenPGPCipher.AES: return SymmetricCipher.AES;
- case OpenPGPCipher.AES192: return SymmetricCipher.AES192;
- case OpenPGPCipher.AES256: return SymmetricCipher.AES256;
- case OpenPGPCipher.Blowfish: return SymmetricCipher.Blowfish;
- case OpenPGPCipher.CAST5: return SymmetricCipher.CAST5;
- case OpenPGPCipher.IDEA: return SymmetricCipher.IDEA;
- case OpenPGPCipher.TripleDES: return SymmetricCipher.TripleDES;
- case OpenPGPCipher.Twofish: return SymmetricCipher.Twofish;
- case OpenPGPCipher.DESSK: return "DESSK";
- case OpenPGPCipher.SAFER: return "SAFER";
- case OpenPGPCipher.Unencrypted: return "Unencrypted";
- default: return string.IsNullOrEmpty(str) ? null : str;
- }
- }
-
- /// <summary>Parses an argument from a GPG status message into a hash algorithm name, or null if the algorithm cannot
- /// be determined.
- /// </summary>
- public static string ParseHashAlgorithm(string str)
- {
- switch((OpenPGPHashAlgorithm)int.Parse(str, CultureInfo.InvariantCulture))
- {
- case OpenPGPHashAlgorithm.MD5: return HashAlgorithm.MD5;
- case OpenPGPHashAlgorithm.RIPEMD160: return HashAlgorithm.RIPEMD160;
- case OpenPGPHashAlgorithm.SHA1: return HashAlgorithm.SHA1;
- case OpenPGPHashAlgorithm.SHA224: return HashAlgorithm.SHA224;
- case OpenPGPHashAlgorithm.SHA256: return HashAlgorithm.SHA256;
- case OpenPGPHashAlgorithm.SHA384: return HashAlgorithm.SHA384;
- case OpenPGPHashAlgorithm.SHA512: return HashAlgorithm.SHA512;
- case OpenPGPHashAlgorithm.HAVAL: return "HAVAL-5-160";
- case OpenPGPHashAlgorithm.MD2: return "MD2";
- case OpenPGPHashAlgorithm.TIGER192: return "TIGER192";
- default: return string.IsNullOrEmpty(str) ? null : str;
- }
- }
-
- /// <summary>Parses an argument from a GPG status message into a key type name, or null if the key type cannot
- /// be determined.
- /// </summary>
- public static string ParseKeyType(string str)
- {
- switch((OpenPGPKeyType)int.Parse(str, CultureInfo.InvariantCulture))
- {
- case OpenPGPKeyType.DSA:
- return KeyType.DSA;
- case OpenPGPKeyType.ElGamal: case OpenPGPKeyType.ElGamalEncryptOnly:
- return KeyType.ElGamal;
- case OpenPGPKeyType.RSA: case OpenPGPKeyType.RSAEncryptOnly: case OpenPGPKeyType.RSASignOnly:
- return KeyType.RSA;
- default: return string.IsNullOrEmpty(str) ? null : str;
- }
- }
-
- /// <summary>Parses an argument from a GPG status message into a timestamp.</summary>
- public static DateTime ParseTimestamp(string str)
- {
- if(str.IndexOf('T') == -1) // the time is specified in seconds since Midnight, January 1, 1970
- {
- long seconds = long.Parse(str, CultureInfo.InvariantCulture);
- return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds);
- }
- else // the date is in ISO8601 format. DateTime.Parse() can handle it.
- {
- return DateTime.Parse(str, CultureInfo.InvariantCulture);
- }
- }
-
- /// <summary>Parses an argument from a GPG status message into a timestamp, or null if there is no timestamp.</summary>
- public static DateTime? ParseNullableTimestamp(string str)
- {
- return string.IsNullOrEmpty(str) || str.Equals("0", StringComparison.Ordinal) ?
- (DateTime?)null : ParseTimestamp(str);
- }
- }
- #endregion
-
- #region ExeGPG
- /// <summary>This class implements a connection to the GNU Privacy Guard via piping input to and from its command-line
- /// executable.
- /// </summary>
- public class ExeGPG : GPG
- {
- /// <summary>Initializes a new <see cref="ExeGPG"/> with no reference to the GPG executable.</summary>
- public ExeGPG() { }
-
- /// <summary>Initializes a new <see cref="ExeGPG"/> with a full path to the GPG executable.</summary>
- public ExeGPG(string exePath)
- {
- Initialize(exePath);
- }
-
- /// <summary>Raised when a line of text is to be logged.</summary>
- public event TextLineHandler LineLogged;
-
- /// <summary>Gets or sets whether the GPG agent will be used. If enabled, GPG may use its own user interface to query
- /// for passwords, bypassing the support provided by this library. The default is false. However, this property has
- /// no effect when using GPG2, because GPG2 doesn't allow the agent to be disabled.
- /// </summary>
- public bool EnableGPGAgent
- {
- get { return enableAgent; }
- set { enableAgent = value; }
- }
-
- /// <summary>Gets the path to the GPG executable, or null if <see cref="Initialize"/> has not been called.</summary>
- public string ExecutablePath
- {
- get { return exePath; }
- }
-
- /// <summary>Gets or sets whether the <see cref="SignatureBase.KeyFingerprint">KeySignature.KeyFingerprint</see>
- /// field will be retrieved. According to the GPG documentation, GPG won't return fingerprints on key signatures
- /// unless signature verification is enabled and signature caching is disabled, due to "various technical reasons".
- /// Checking the signatures and disabling the cache causes a significant performance hit, however, so by default it
- /// is not done. If this property is set to true, the cache will be disabled and signature verification will be
- /// enabled on all signature retrievals, allowing GPG to return the key signature fingerprint. Note that even with
- /// this property set to true, the fingerprint still won't be set if the key signature failed verification.
- /// </summary>
- public bool RetrieveKeySignatureFingerprints
- {
- get { return retrieveKeySignatureFingerprints; }
- set { retrieveKeySignatureFingerprints = value; }
- }
-
- /// <summary>Gets the version of the GPG executable, encoded as an integer so that 1.4.9 becomes 10409 and 2.0.21 becomes
- /// 20021. Note that the version number is retrieved when this class is instantiated with an executable and whenever
- /// <see cref="Initialize"/> is called. If a newer version of GPG is installed in the mean time, the version reported by this
- /// property will not be updated until <see cref="Initialize"/> is called again.
- /// </summary>
- public int Version
- {
- get { return gpgVersion; }
- }
-
- #region Configuration
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetDefaultPrimaryKeyType/node()"/>
- public override string GetDefaultPrimaryKeyType()
- {
- return KeyType.DSA;
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetDefaultSubkeyType/node()"/>
- public override string GetDefaultSubkeyType()
- {
- return KeyType.ElGamal;
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetMaximumKeyLength/node()"/>
- public override int GetMaximumKeyLength(string keyType)
- {
- if(!string.Equals(keyType, "RSA-E", StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(keyType, "RSA-S", StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(keyType, "ELG-E", StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(keyType, "ELG", StringComparison.OrdinalIgnoreCase))
- {
- AssertSupported(keyType, keyTypes, "key type");
- }
-
- return string.Equals(keyType, "DSA", StringComparison.OrdinalIgnoreCase) ? 3072 : 4096;
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedCiphers/node()"/>
- public override string[] GetSupportedCiphers()
- {
- AssertInitialized();
- return ciphers == null ? new string[0] : (string[])ciphers.Clone();
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedCompressions/node()"/>
- public override string[] GetSupportedCompressions()
- {
- AssertInitialized();
- return compressions == null ? new string[0] : (string[])compressions.Clone();
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedHashes/node()"/>
- public override string[] GetSupportedHashes()
- {
- AssertInitialized();
- return hashes == null ? new string[0] : (string[])hashes.Clone();
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedKeyTypes/node()"/>
- public override string[] GetSupportedKeyTypes()
- {
- AssertInitialized();
- return keyTypes == null ? new string[0] : (string[])keyTypes.Clone();
- }
- #endregion
-
- #region Encryption and signing
- /// <include file="documentation.xml" path="/Security/PGPSystem/SignAndEncrypt/node()"/>
- public override void SignAndEncrypt(Stream sourceData, Stream destination, SigningOptions signingOptions,
- EncryptionOptions encryptionOptions, OutputOptions outputOptions)
- {
- if(sourceData == null || destination == null || encryptionOptions == null && signingOptions == null)
- {
- throw new ArgumentNullException();
- }
-
- string args = GetOutputArgs(outputOptions);
- bool symmetric = false; // whether we're doing password-based encryption (possibly in addition to key-based)
- bool customAlgo = false; // whether a custom algorithm was specified
-
- // add the keyrings of all the recipient and signer keys to the command line
- List<PrimaryKey> keyringKeys = new List<PrimaryKey>();
-
- if(encryptionOptions != null) // if we'll be doing any encryption
- {
- // we can't do signing with detached signatures because GPG doesn't have a way to specify the two output files.
- // and encryption with
- if(signingOptions != null && signingOptions.Type != SignatureType.Embedded)
- {
- if(signingOptions.Type == SignatureType.ClearSignedText)
- {
- throw new ArgumentException("Combining encryption with clear-signing does not make sense, because the data "+
- "cannot be both encrypted and in the clear.");
- }
- else
- {
- throw new NotSupportedException("Simultaneous encryption and detached signing is not supported by GPG. "+
- "Perform the encryption and detached signing as two separate steps.");
- }
- }
-
- symmetric = encryptionOptions.Password != null && encryptionOptions.Password.Length != 0;
-
- // we need recipients if we're not doing password-based encryption
- if(!symmetric && encryptionOptions.Recipients.Count == 0 && encryptionOptions.HiddenRecipients.Count == 0)
- {
- throw new ArgumentException("No recipients were specified.");
- }
-
- keyringKeys.AddRange(encryptionOptions.Recipients);
- keyringKeys.AddRange(encryptionOptions.HiddenRecipients);
-
- // if there are recipients for key-based encryption, add them to the command line
- if(encryptionOptions.Recipients.Count != 0 || encryptionOptions.HiddenRecipients.Count != 0)
- {
- args += GetFingerprintArgs(encryptionOptions.Recipients, "-r") +
- GetFingerprintArgs(encryptionOptions.HiddenRecipients, "-R") + "-e "; // plus the encrypt command
- }
-
- if(!string.IsNullOrEmpty(encryptionOptions.Cipher))
- {
- AssertSupported(encryptionOptions.Cipher, ciphers, "cipher");
- args += "--cipher-algo " + EscapeArg(encryptionOptions.Cipher) + " ";
- customAlgo = true;
- }
-
- if(symmetric) args += "-c "; // add the password-based encryption command if necessary
-
- if(encryptionOptions.AlwaysTrustRecipients) args += "--trust-model always ";
- }
-
- if(signingOptions != null) // if we'll be doing any signing
- {
- if(signingOptions.Signers.Count == 0) throw new ArgumentException("No signers were specified.");
-
- // add the keyrings of the signers to the command prompt
- keyringKeys.AddRange(signingOptions.Signers);
-
- if(!string.IsNullOrEmpty(signingOptions.Hash))
- {
- AssertSupported(encryptionOptions.Cipher, hashes, "hash");
- args += "--digest-algo "+EscapeArg(signingOptions.Hash)+" ";
- customAlgo = true;
- }
-
- // add all of the signers to the command line, and the signing command
- args += GetFingerprintArgs(signingOptions.Signers, "-u") +
- (signingOptions.Type == SignatureType.Detached ? "-b " :
- signingOptions.Type == SignatureType.ClearSignedText ? "--clearsign " : "-s ");
- }
-
- args += GetKeyringArgs(keyringKeys, true); // add all the keyrings to the command line
-
- Command command = Execute(args, StatusMessages.ReadInBackground, false);
- CommandState commandState = new CommandState(command);
- if(customAlgo) commandState.FailureReasons |= FailureReason.UnsupportedAlgorithm; // using a custom algo can cause failure
-
- using(ManualResetEvent ready = new ManualResetEvent(false)) // create an event to signal when the data should be sent
- {
- ProcessCommand(command, commandState,
- delegate(Command cmd, CommandState state)
- {
- cmd.InputNeeded += delegate(string promptId)
- {
- if(string.Equals(promptId, "untrusted_key.override", StringComparison.Ordinal))
- { // this question indicates that a recipient key is not trusted
- bool alwaysTrust = encryptionOptions != null && encryptionOptions.AlwaysTrustRecipients;
- if(!alwaysTrust) state.FailureReasons |= FailureReason.UntrustedRecipient;
- cmd.SendLine(alwaysTrust ? "Y" : "N");
- }
- else if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) &&
- state.PasswordMessage != null && state.PasswordMessage.Type == StatusMessageType.NeedCipherPassphrase)
- {
- cmd.SendPassword(encryptionOptions.Password, false);
- }
- else if(!state.Canceled)
- {
- DefaultPromptHandler(promptId, state);
- if(state.Canceled) cmd.Kill(); // kill GPG if the user doesn't give the password, so it doesn't keep asking
- }
- };
-
- cmd.StatusMessageReceived += delegate(StatusMessage msg)
- {
- switch(msg.Type)
- {
- case StatusMessageType.BeginEncryption: case StatusMessageType.BeginSigning:
- ready.Set(); // all set. send the data!
- break;
-
- default: DefaultStatusMessageHandler(msg, state); break;
- }
- };
- },
-
- delegate(Command cmd, CommandState state)
- {
- // wait until it's time to write the data or the process aborted
- while(!ready.WaitOne(50, false) && !cmd.Process.HasExited) { }
-
- // if the process is still running and it didn't exit before we could copy the input data...
- if(!cmd.Process.HasExited) ReadAndWriteStreams(destination, sourceData, cmd.Process);
- });
- }
-
- if(!command.SuccessfulExit) // if the process wasn't successful, throw an exception
- {
- if(commandState.Canceled) throw new OperationCanceledException();
- else if(encryptionOptions != null) throw new EncryptionFailedException(commandState.FailureReasons);
- else throw new SigningFailedException(commandState.FailureReasons);
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/Decrypt/node()"/>
- public override Signature[] Decrypt(Stream ciphertext, Stream destination, DecryptionOptions options)
- {
- if(ciphertext == null || destination == null) throw new ArgumentNullException();
-
- Command cmd = Execute(GetVerificationArgs(options, true) + "-d", StatusMessages.ReadInBackground, false);
- return DecryptVerifyCore(cmd, ciphertext, destination, options);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/Verify2/node()"/>
- public override Signature[] Verify(Stream signedData, VerificationOptions options)
- {
- if(signedData == null) throw new ArgumentNullException();
- return VerifyCore(null, signedData, options);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/Verify3/node()"/>
- /// <remarks>The signature data (from <paramref name="signature"/>) will be written into a temporary file for the
- /// duration of this method call.
- /// </remarks>
- public override Signature[] Verify(Stream signedData, Stream signature, VerificationOptions options)
- {
- if(signedData == null || signature == null) throw new ArgumentNullException();
-
- // copy the signature into a temporary file, because we can't pass both streams on standard input
- string sigFileName = Path.GetTempFileName();
- try
- {
- using(FileStream file = new FileStream(sigFileName, FileMode.Truncate, FileAccess.Write))
- {
- signature.CopyTo(file);
- }
-
- return VerifyCore(sigFileName, signedData, options);
- }
- finally { File.Delete(sigFileName); }
- }
- #endregion
-
- #region Key import and export
- /// <include file="documentation.xml" path="/Security/PGPSystem/ExportKeys/node()"/>
- public override void ExportKeys(PrimaryKey[] keys, Stream destination, ExportOptions exportOptions,
- OutputOptions outputOptions)
- {
- if(keys == null || destination == null) throw new ArgumentNullException();
-
- if((exportOptions & (ExportOptions.ExportPublicKeys|ExportOptions.ExportSecretKeys)) == 0)
- {
- throw new ArgumentException("At least one of ExportOptions.ExportPublicKeys or ExportOptions.ExportSecretKeys "+
- "must be specified.");
- }
-
- if(keys.Length == 0) return;
-
- if((exportOptions & ExportOptions.ExportPublicKeys) != 0)
- {
- string args = GetKeyringArgs(keys, false) + GetExportArgs(exportOptions, false, true) +
- GetOutputArgs(outputOptions) + GetFingerprintArgs(keys);
- ExportCore(args, destination);
- }
-
- if((exportOptions & ExportOptions.ExportSecretKeys) != 0)
- {
- string args = GetKeyringArgs(keys, true) + GetExportArgs(exportOptions, true, true) +
- GetOutputArgs(outputOptions) + GetFingerprintArgs(keys);
- ExportCore(args, destination);
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/ExportKeys2/node()"/>
- public override void ExportKeys(Keyring[] keyrings, bool includeDefaultKeyring, Stream destination,
- ExportOptions exportOptions, OutputOptions outputOptions)
- {
- if(destination == null) throw new ArgumentNullException();
-
- if((exportOptions & (ExportOptions.ExportPublicKeys|ExportOptions.ExportSecretKeys)) == 0)
- {
- throw new ArgumentException("At least one of ExportOptions.ExportPublicKeys or ExportOptions.ExportSecretKeys "+
- "must be specified.");
- }
-
- if((exportOptions & ExportOptions.ExportPublicKeys) != 0)
- {
- string args = GetKeyringArgs(keyrings, !includeDefaultKeyring, false) +
- GetExportArgs(exportOptions, false, true) + GetOutputArgs(outputOptions);
- ExportCore(args, destination);
- }
-
- if((exportOptions & ExportOptions.ExportSecretKeys) != 0)
- {
- string args = GetKeyringArgs(keyrings, !includeDefaultKeyring, true) +
- GetExportArgs(exportOptions, true, true) + GetOutputArgs(outputOptions);
- ExportCore(args, destination);
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/ImportKeys3/node()"/>
- public override ImportedKey[] ImportKeys(Stream source, Keyring keyring, ImportOptions options)
- {
- if(source == null) throw new ArgumentNullException();
-
- CommandState state;
- Command cmd = Execute(GetImportArgs(keyring, options) + "--import", StatusMessages.ReadInBackground, false);
- ImportedKey[] keys = ImportCore(cmd, source, out state);
- if(!cmd.SuccessfulExit) throw new ImportFailedException(state.FailureReasons);
- return keys;
- }
- #endregion
-
- #region Key revocation
- /// <include file="documentation.xml" path="/Security/PGPSystem/AddDesignatedRevoker/node()"/>
- public override void AddDesignatedRevoker(PrimaryKey key, PrimaryKey revokerKey)
- {
- if(key == null || revokerKey == null) throw new ArgumentNullException();
-
- if(string.IsNullOrEmpty(revokerKey.Fingerprint))
- {
- throw new ArgumentException("The revoker key has no fingerprint.");
- }
-
- if(string.Equals(key.Fingerprint, revokerKey.Fingerprint, StringComparison.Ordinal))
- {
- throw new ArgumentException("You can't add a key as its own designated revoker.");
- }
-
- DoEdit(key, GetKeyringArgs(new PrimaryKey[] { key, revokerKey }, true), false,
- new AddRevokerCommand(revokerKey.Fingerprint));
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRevocationCertificate/node()"/>
- public override void GenerateRevocationCertificate(PrimaryKey key, Stream destination, KeyRevocationReason reason,
- OutputOptions outputOptions)
- {
- GenerateRevocationCertificateCore(key, null, destination, reason, outputOptions);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRevocationCertificateD/node()"/>
- public override void GenerateRevocationCertificate(PrimaryKey keyToRevoke, PrimaryKey designatedRevoker,
- Stream destination, KeyRevocationReason reason,
- OutputOptions outputOptions)
- {
- if(designatedRevoker == null) throw new ArgumentNullException();
- GenerateRevocationCertificateCore(keyToRevoke, designatedRevoker, destination, reason, outputOptions);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeKeys/node()"/>
- public override void RevokeKeys(KeyRevocationReason reason, params PrimaryKey[] keys)
- {
- RevokeKeysCore(null, reason, keys);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeKeysD/node()"/>
- public override void RevokeKeys(PrimaryKey designatedRevoker, KeyRevocationReason reason, params PrimaryKey[] keys)
- {
- if(designatedRevoker == null) throw new ArgumentNullException();
- RevokeKeysCore(designatedRevoker, reason, keys);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeSubkeys/node()"/>
- public override void RevokeSubkeys(KeyRevocationReason reason, params Subkey[] subkeys)
- {
- EditSubkeys(subkeys, delegate { return new RevokeSubkeysCommand(reason); });
- }
- #endregion
-
- #region Key server operations
- /// <include file="documentation.xml" path="/Security/PGPSystem/FindKeysOnServer/node()"/>
- public override void FindKeysOnServer(Uri keyServer, KeySearchHandler handler, params string[] searchKeywords)
- {
- if(keyServer == null || handler == null || searchKeywords == null) throw new ArgumentNullException();
- if(searchKeywords.Length == 0) throw new ArgumentException("No keywords were given.");
-
- string args = "--keyserver " + EscapeArg(keyServer.AbsoluteUri) + " --with-colons --fixed-list-mode --search-keys";
- foreach(string keyword in searchKeywords) args += " " + EscapeArg(keyword);
-
- Command command = Execute(args, StatusMessages.MixIntoStdout, true, true);
- CommandState commandState = ProcessCommand(command,
- delegate(Command cmd, CommandState state)
- {
- cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
- },
-
- delegate(Command cmd, CommandState state)
- {
- List<PrimaryKey> keysFound = new List<PrimaryKey>();
- List<UserId> userIds = new List<UserId>();
- while(true)
- {
- string line;
- cmd.ReadLine(out line);
- if(line != null) LogLine(line);
-
- gotLine:
- if(line == null && cmd.StatusMessage == null) break;
-
- if(line == null)
- {
- switch(cmd.StatusMessage.Type)
- {
- case StatusMessageType.GetLine:
- GetInputMessage m = (GetInputMessage)cmd.StatusMessage;
- if(string.Equals(m.PromptId, "keysearch.prompt", StringComparison.Ordinal))
- {
- // we're done with this chunk of the search, so we'll give the keys to the search handler.
- // we won't continue if we didn't find anything, even if the handler returns true
- bool shouldContinue = keysFound.Count != 0 && handler(keysFound.ToArray());
- cmd.SendLine(shouldContinue ? "N" : "Q");
- keysFound.Clear();
- break;
- }
- else goto default;
-
- default: DefaultStatusMessageHandler(cmd.StatusMessage, state); break;
- }
- }
- else if(line.StartsWith("pub:", StringComparison.Ordinal)) // a key description follows
- {
- string[] fields = line.Split(':');
-
- PrimaryKey key = new PrimaryKey();
-
- if(IsValidKeyId(fields[1])) key.KeyId = fields[1].ToUpperInvariant();
- else if(IsValidFingerprint(fields[1])) key.Fingerprint = fields[1].ToUpperInvariant();
- else // there's no valid ID, so skip any related records that follow
- {
- do
- {
- cmd.ReadLine(out line);
- if(line != null) LogLine(line);
- }
- while(line != null && !line.StartsWith("pub:", StringComparison.Ordinal));
- goto gotLine;
- }
-
- if(fields.Length > 2 && !string.IsNullOrEmpty(fields[2])) key.KeyType = ParseKeyType(fields[2]);
- if(fields.Length > 3 && !string.IsNullOrEmpty(fields[3])) key.Length = int.Parse(fields[3]);
- if(fields.Length > 4 && !string.IsNullOrEmpty(fields[4])) key.CreationTime = ParseTimestamp(fields[4]);
- if(fields.Length > 5 && !string.IsNullOrEmpty(fields[5])) key.ExpirationTime = ParseNullableTimestamp(fields[5]);
-
- if(fields.Length > 6 && !string.IsNullOrEmpty(fields[6]))
- {
- foreach(char c in fields[6])
- {
- switch(char.ToLowerInvariant(c))
- {
- case 'd': key.Disabled = true; break;
- case 'e': key.Expired = true; break;
- case 'r': key.Revoked = true; break;
- }
- }
- }
-
- // now parse the user IDs
- while(true)
- {
- cmd.ReadLine(out line);
- if(line == null) break; // if we hit a status message or EOF, break
-
- LogLine(line);
- if(line.StartsWith("pub:", StringComparison.Ordinal)) break;
- else if(!line.StartsWith("uid", StringComparison.Ordinal)) continue;
-
- fields = line.Split(':');
- if(string.IsNullOrEmpty(fields[1])) continue;
-
- UserId id = new UserId();
- id.PrimaryKey = key;
- id.Name = CUnescape(fields[1]);
- id.Signatures = NoSignatures;
- if(fields.Length > 2 && !string.IsNullOrEmpty(fields[2])) id.CreationTime = ParseTimestamp(fields[2]);
- id.MakeReadOnly();
- userIds.Add(id);
- }
-
- if(userIds.Count != 0)
- {
- key.Attributes = NoAttributes;
- key.DesignatedRevokers = NoRevokers;
- key.Signatures = NoSignatures;
- key.Subkeys = NoSubkeys;
- key.UserIds = new ReadOnlyListWrapper<UserId>(userIds.ToArray());
- key.MakeReadOnly();
- keysFound.Add(key);
-
- userIds.Clear();
- }
-
- goto gotLine;
- }
- }
- });
-
- if(!command.SuccessfulExit) throw new KeyServerFailedException("Key search failed.", commandState.FailureReasons);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/ImportKeysFromServer/node()"/>
- public override ImportedKey[] ImportKeysFromServer(KeyDownloadOptions options, Keyring keyring,
- params string[] keyFingerprintsOrIds)
- {
- if(keyFingerprintsOrIds == null) throw new ArgumentNullException();
- if(keyFingerprintsOrIds.Length == 0) return new ImportedKey[0];
-
- string args = GetKeyServerArgs(options, true) + GetImportArgs(keyring, options.ImportOptions) + "--recv-keys";
- foreach(string id in keyFingerprintsOrIds)
- {
- if(string.IsNullOrEmpty(id)) throw new ArgumentException("A key ID was null or empty.");
- args += " " + id;
- }
- return KeyServerCore(args, "Key import", true, false);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RefreshKeyringFromServer/node()"/>
- public override ImportedKey[] RefreshKeysFromServer(KeyDownloadOptions options, Keyring keyring)
- {
- string args = GetImportArgs(keyring, options == null ? ImportOptions.Default : options.ImportOptions) +
- GetKeyServerArgs(options, false) + "--refresh-keys";
- return KeyServerCore(args, "Keyring refresh", true, false);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RefreshKeysFromServer/node()"/>
- public override ImportedKey[] RefreshKeysFromServer(KeyDownloadOptions options, params PrimaryKey[] keys)
- {
- if(keys == null) throw new ArgumentNullException();
- if(keys.Length == 0) return new ImportedKey[0];
-
- string args = GetKeyringArgs(keys, true) + GetKeyServerArgs(options, false) +
- GetImportArgs(null, options == null ? ImportOptions.Default : options.ImportOptions) +
- "--refresh-keys " + GetFingerprintArgs(keys);
- return KeyServerCore(args, "Key refresh", true, false);
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/UploadKeys/node()"/>
- public override void UploadKeys(KeyUploadOptions options, params PrimaryKey[] keys)
- {
- if(keys == null) throw new ArgumentNullException();
- if(keys.Length == 0) return;
-
- string args = GetKeyringArgs(keys, false) + GetKeyServerArgs(options, true) +
- GetExportArgs(options.ExportOptions, false, false) + "--send-keys " + GetFingerprintArgs(keys);
- KeyServerCore(args, "Key upload", false, true);
- }
- #endregion
-
- #region Key signing
- /// <include file="documentation.xml" path="/Security/PGPSystem/DeleteSignatures/node()"/>
- public override void DeleteSignatures(params KeySignature[] signatures)
- {
- EditSignatures(signatures, delegate(KeySignature[] sigs) { return new DeleteSigsCommand(sigs); });
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeSignatures/node()"/>
- public override void RevokeSignatures(UserRevocationReason reason, params KeySignature[] signatures)
- {
- EditSignatures(signatures, delegate(KeySignature[] sigs) { return new RevokeSigsCommand(reason, sigs); });
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/SignAttributes/node()"/>
- public override void SignAttributes(UserAttribute[] attributes, PrimaryKey signingKey, KeySigningOptions options)
- {
- if(attributes == null || signingKey == null) throw new ArgumentNullException();
-
- foreach(List<UserAttribute> attrList in GroupAttributesByKey(attributes))
- {
- EditCommand[] commands = new EditCommand[attrList.Count+1];
- for(int i=0; i<attrList.Count; i++) commands[i] = new RawCommand("uid " + attrList[i].Id);
- commands[attrList.Count] = new SignKeyCommand(options, false);
-
- PrimaryKey keyToEdit = attrList[0].PrimaryKey;
- DoEdit(keyToEdit, "--ask-cert-level " + GetKeyringArgs(new PrimaryKey[] { keyToEdit, signingKey }, true) +
- "-u " + signingKey.Fingerprint, false, commands);
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/SignKeys/node()"/>
- public override void SignKeys(PrimaryKey[] keysToSign, PrimaryKey signingKey, KeySigningOptions options)
- {
- if(keysToSign == null || signingKey == null) throw new ArgumentNullException();
-
- foreach(PrimaryKey key in keysToSign)
- {
- if(key == null) throw new ArgumentException("A key was null.");
- DoEdit(key, "--ask-cert-level " + GetKeyringArgs(new PrimaryKey[] { key, signingKey }, true) +
- "-u " + signingKey.Fingerprint, false, new SignKeyCommand(options, true));
- }
- }
- #endregion
-
- #region Keyring queries
- /// <include file="documentation.xml" path="/Security/PGPSystem/FindKey/node()"/>
- public override PrimaryKey FindKey(string keywordOrId, Keyring keyring, ListOptions options)
- {
- PrimaryKey[] keys = FindKeys(new string[] { keywordOrId },
- keyring == null ? null : new Keyring[] { keyring }, keyring == null, options);
- return keys[0];
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/FindKeys/node()"/>
- public override PrimaryKey[] FindKeys(string[] fingerprintsOrIds, Keyring[] keyrings,
- bool includeDefaultKeyring, ListOptions options)
- {
- if(fingerprintsOrIds == null) throw new ArgumentNullException();
- if(fingerprintsOrIds.Length == 0) return new PrimaryKey[0];
-
- // create search arguments containing all the key IDs
- string searchArgs = null;
-
- if(fingerprintsOrIds.Length > 1) // if there's more than one ID, we can't allow fancy matches like email addresses,
- { // so validate and normalize all IDs
- // clone the array so we don't modify the parameters
- fingerprintsOrIds = (string[])fingerprintsOrIds.Clone();
- for(int i=0; i<fingerprintsOrIds.Length; i++)
- {
- if(string.IsNullOrEmpty(fingerprintsOrIds[i]))
- {
- throw new ArgumentException("A fingerprint/ID was null or empty.");
- }
- fingerprintsOrIds[i] = NormalizeKeyId(fingerprintsOrIds[i]);
- }
- }
-
- // add all IDs to the command line
- foreach(string id in fingerprintsOrIds) searchArgs += EscapeArg(id) + " ";
- PrimaryKey[] keys = GetKeys(keyrings, includeDefaultKeyring, options, searchArgs);
-
- if(fingerprintsOrIds.Length == 1) // if there was only a single key returned, then that's the one
- {
- return keys.Length == 1 ? keys : new PrimaryKey[1];
- }
- else
- {
- // add each key found to a dictionary
- Dictionary<string, PrimaryKey> keyDict = new Dictionary<string, PrimaryKey>();
- foreach(PrimaryKey key in keys)
- {
- keyDict[key.Fingerprint] = key;
- keyDict[key.KeyId] = key;
- keyDict[key.ShortKeyId] = key;
- }
-
- // then create the return array and return the keys found
- if(keys.Length != fingerprintsOrIds.Length) keys = new PrimaryKey[fingerprintsOrIds.Length];
- for(int i=0; i<keys.Length; i++) keyDict.TryGetValue(fingerprintsOrIds[i], out keys[i]);
- return keys;
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GetKeys/node()"/>
- public override PrimaryKey[] GetKeys(Keyring[] keyrings, bool includeDefaultKeyring, ListOptions options)
- {
- return GetKeys(keyrings, includeDefaultKeyring, options, null);
- }
- #endregion
-
- #region Miscellaneous
- /// <include file="documentation.xml" path="/Security/PGPSystem/CreateTrustDatabase/node()"/>
- public override void CreateTrustDatabase(string path)
- {
- // the following creates a valid, empty version 3 trust database. (see gpg-src\doc\DETAILS)
- using(FileStream dbFile = File.Open(path, FileMode.Create, FileAccess.Write))
- {
- dbFile.SetLength(40); // the database is 40 bytes long, but only the first 16 bytes are non-zero
-
- byte[] headerStart = new byte[] { 1, 0x67, 0x70, 0x67, 3, 3, 1, 5, 1, 0, 0, 0 };
- dbFile.Write(headerStart, 0, headerStart.Length);
-
- // the next four bytes are the big-endian creation timestamp in seconds since epoch
- dbFile.WriteBE4((int)((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds));
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRandomData/node()"/>
- public override void GetRandomData(Randomness quality, byte[] buffer, int index, int count)
- {
- Utility.ValidateRange(buffer, index, count);
- if(count == 0) return;
-
- // "gpg --gen-random QUALITY COUNT" writes random COUNT bytes to standard output. QUALITY is a value from 0 to 2
- // representing the quality of the random number generator to use
- string qualityArg;
- if(quality == Randomness.Weak) qualityArg = "0";
- else if(quality == Randomness.TooStrong) qualityArg = "2";
- else qualityArg = "1"; // we'll default to the Strong level
-
- Command command = Execute("--gen-random " + qualityArg + " " + count.ToStringInvariant(), StatusMessages.Ignore, true, true);
- ProcessCommand(command, null,
- delegate(Command cmd, CommandState state) { count -= cmd.Process.StandardOutput.BaseStream.FullRead(buffer, 0, count); });
-
- if(count != 0) throw new PGPException("GPG didn't write enough random bytes.");
- command.CheckExitCode();
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/Hash/node()"/>
- public override byte[] Hash(Stream data, string hashAlgorithm)
- {
- if(data == null) throw new ArgumentNullException();
-
- bool customAlgorithm = false;
- if(hashAlgorithm == null || hashAlgorithm == HashAlgorithm.Default)
- {
- hashAlgorithm = HashAlgorithm.SHA1;
- }
- else if(hashAlgorithm.Length == 0)
- {
- throw new ArgumentException("Unspecified hash algorithm.");
- }
- else
- {
- AssertSupported(hashAlgorithm, hashes, "hash");
- customAlgorithm = true;
- }
-
- // "gpg --print-md ALGO" hashes data presented on standard input. if the algorithm is not supported, gpg exits
- // immediately with error code 2. otherwise, it consumes all available input, and then prints the hash in a
- // human-readable form, with hex digits nicely formatted into blocks and lines. we'll feed it all the input and
- // then read the output.
- List<byte> hash = new List<byte>();
- Command command = Execute("--print-md " + EscapeArg(hashAlgorithm), StatusMessages.Ignore, false, true);
- ProcessCommand(command, null,
- delegate(Command cmd, CommandState state)
- {
- if(WriteStreamToProcess(data, cmd.Process))
- {
- while(true)
- {
- string line = cmd.Process.StandardOutput.ReadLine();
- if(line == null) break;
-
- // on each line, there are some hex digits separated with whitespace. we'll read each character, but only
- // use characters that are valid hex digits
- int value = 0, chars = 0;
- foreach(char c in line.ToLowerInvariant())
- {
- if(IsHexDigit(c))
- {
- value = (value<<4) + GetHexValue(c);
- if(++chars == 2) // when two hex digits have accumulated, a byte is complete, so write it to the output
- {
- hash.Add((byte)value);
- chars = 0;
- }
- }
- }
- }
- }
- });
-
- if(!command.SuccessfulExit || hash.Count == 0)
- {
- throw new PGPException("Hash failed.",
- customAlgorithm ? FailureReason.UnsupportedAlgorithm : FailureReason.None);
- }
-
- return hash.ToArray();
- }
- #endregion
-
- #region Primary key management
- /// <include file="documentation.xml" path="/Security/PGPSystem/AddSubkey/node()"/>
- public override void AddSubkey(PrimaryKey key, string keyType, KeyCapabilities capabilities, int keyLength,
- DateTime? expiration)
- {
- // if a custom length is specified, it might be long enough to require a DSA2 key, so add the option just in case
- DoEdit(key, (keyLength == 0 ? null : "--enable-dsa2 ") + "--expert", true,
- new AddSubkeyCommand(this, keyType, capabilities, keyLength, expiration));
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/ChangeExpiration/node()"/>
- public override void ChangeExpiration(Key key, DateTime? expiration)
- {
- if(key == null) throw new ArgumentNullException();
-
- Subkey subkey = key as Subkey;
- if(subkey == null) // if it's not a subkey, we'll assume it's a primary key, and change that
- {
- DoEdit(key.GetPrimaryKey(), new ChangeExpirationCommand(expiration));
- }
- else // otherwise, first select the subkey
- {
- DoEdit(key.GetPrimaryKey(), new SelectSubkeyCommand(subkey.Fingerprint, true), new ChangeExpirationCommand(expiration));
- }
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/ChangePassword/node()"/>
- public override void ChangePassword(PrimaryKey key, SecureString password)
- {
- DoEdit(key, new ChangePasswordCommand(password));
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/CleanKeys/node()"/>
- public override void CleanKeys(params PrimaryKey[] keys)
- {
- RepeatedRawEditCommand(keys, "clean");
- }
-
- /// <include file="documentation.xml" path="/Security/PGPSystem/CreateKey/node()"/>
- /// <remarks>If <see cref="NewKeyOptions.Keyring"/> is set, the key will not be automatically trusted in the default
- /// trust database.
- /// </remarks>
- public override PrimaryKey CreateKey(NewKeyOptions options)
- {
- if(options == null) throw new ArgumentNullException();
-
- string email = Trim(options.Email), realName = Trim(options.RealName), comment = Trim(options.Comment);
- if(string.IsNullOrEmpty(email) && string.IsNullOrEmpty(realName))
- {
- throw new ArgumentException("At least one of NewKeyOptions.Email or NewKeyOptions.RealName must be set.");
- }
-
- if(ContainsControlCharacters(options.Comment + options.Email + options.RealName + options.Password))
- {
- throw new ArgumentException("The comment, email, real name, and/or password contains control characters. "+
- "Remove them.");
- }
-
- bool primaryIsDSA = string.IsNullOrEmpty(options.KeyType) || // DSA is the default primary key type
- string.Equals(options.KeyType, KeyType.DSA, StringComparison.OrdinalIgnoreCase);
- bool primaryIsRSA = string.Equals(options.KeyType, KeyType.RSA, StringComparison.OrdinalIgnoreCase);
-
- if(!primaryIsDSA && !primaryIsRSA)
- {
- throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm,
- "Primary key type "+options.KeyType+" is not supported.");
- }
-
- // GPG supports key sizes from 1024 to 3072 (for DSA keys) or 4096 (for other keys)
- int maxKeyLength = primaryIsDSA ? 3072 : 4096;
- if(options.KeyLength != 0 && (options.KeyLength < 1024 || options.KeyLength > maxKeyLength))
- {
- throw new KeyCreationFailedException(FailureReason.None, "Key length " +
- options.KeyLength.ToStringInvariant() + " is not supported.");
- }
-
- bool subIsDSA = string.Equals(options.SubkeyType, KeyType.DSA, StringComparison.OrdinalIgnoreCase);
- bool subIsELG = string.IsNullOrEmpty(options.SubkeyType) || // ElGamal is the default subkey type
- string.Equals(options.SubkeyType, KeyType.ElGamal, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(options.SubkeyType, "ELG-E", StringComparison.OrdinalIgnoreCase);
- bool subIsRSA = string.Equals(options.SubkeyType, KeyType.RSA, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(options.SubkeyType, "RSA-E", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(options.SubkeyType, "RSA-S", StringComparison.OrdinalIgnoreCase);
- bool subIsNone = string.Equals(options.SubkeyType, KeyType.None, StringComparison.OrdinalIgnoreCase);
-
- if(!subIsNone && !subIsDSA && !subIsELG && !subIsRSA)
- {
- throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm,
- "Subkey type "+options.SubkeyType+" is not supported.");
- }
-
- KeyCapabilities primaryCapabilities = options.KeyCapabilities, subCapabilities = options.SubkeyCapabilities;
-
- if(primaryCapabilities == KeyCapabilities.Default)
- {
- primaryCapabilities = KeyCapabilities.Certify | KeyCapabilities.Sign | KeyCapabilities.Authenticate;
- }
-
- if(!subIsNone) // if a subkey will be created
- {
- // GPG supports key sizes from 1024 to 3072 (for DSA keys) or 4096 (for other keys)
- maxKeyLength = subIsDSA ? 3072 : 4096;
- if(options.SubkeyLength != 0 && (options.SubkeyLength < 1024 || options.SubkeyLength > maxKeyLength))
- {
- throw new KeyCreationFailedException(FailureReason.None, "Key length "+
- options.SubkeyLength.ToStringInvariant() + " is not supported.");
- }
-
- if(subCapabilities == KeyCapabilities.Default)
- {
- subCapabilities = subIsDSA ? KeyCapabilities.Sign : KeyCapabilities.Encrypt;
- }
- }
-
- int keyExpirationDays = GetExpirationDays(options.KeyExpiration);
- int subkeyExpirationDays = GetExpirationDays(options.SubkeyExpiration);
-
- // the options look good, so lets make the key
- string keyFingerprint = null, args = GetKeyringArgs(options.Keyring, true);
-
- // if we're using DSA keys greater than 1024 bits, we need to enable DSA2 support
- if(primaryIsDSA && options.KeyLength > 1024 || subIsDSA && options.SubkeyLength > 1024) args += "--enable-dsa2 ";
-
- Command command = Execute(args + "--batch --gen-key", StatusMessages.ReadInBackground, false);
- CommandState commandState = ProcessCommand(command,
- delegate(Command cmd, CommandState state)
- {
- cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
-
- cmd.StatusMessageReceived += delegate(StatusMessage msg)
- {
- if(msg.Type == StatusMessageType.KeyCreated) // when the key is created, grab its fingerprint
- {
- KeyCreatedMessage m = (KeyCreatedMessage)msg;
- if(m.PrimaryKeyCreated) keyFingerprint = m.Fingerprint;
- }
- else DefaultStatusMessageHandler(msg, state);
- };
- },
-
- delegate(Command cmd, CommandState state)
- {
- cmd.Process.StandardInput.WriteLine("Key-Type: " + (primaryIsDSA ? "DSA" : "RSA"));
-
- int keyLength = options.KeyLength != 0 ? options.KeyLength : primaryIsDSA ? 1024 : 2048;
- cmd.Process.StandardInput.WriteLine("Key-Length: " + keyLength.ToStringInvariant());
-
- cmd.Process.StandardInput.WriteLine("Key-Usage: " + GetKeyUsageString(primaryCapabilities));
-
- if(!subIsNone)
- {
- cmd.Process.StandardInput.WriteLine("Subkey-Type: " + (subIsDSA ? "DSA" : subIsELG ? "ELG-E" : "RSA"));
- cmd.Process.StandardInput.WriteLine("Subkey-Usage: " + GetKeyUsageString(subCapabilities));
-
- keyLength = options.SubkeyLength != 0 ? options.SubkeyLength : subIsDSA ? 1024 : 2048;
- cmd.Process.StandardInput.WriteLine("Subkey-Length: " + keyLength.ToStringInvariant());
- }
-
- if(!string.IsNullOrEmpty(realName)) cmd.Process.StandardInput.WriteLine("Name-Real: " + realName);
- if(!string.IsNullOrEmpty(email)) cmd.Process.StandardInput.WriteLine("Name-Email: " + email);
- if(!string.IsNullOrEmpty(comment)) cmd.Process.StandardInput.WriteLine("Name-Comment: " + comment);
-
- if(options.Password != null && options.Password.Length != 0)
- {
- cmd.Process.StandardInput.Write("Passphrase: ");
- …
Large files files are truncated, but you can click here to view the full file