PageRenderTime 46ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/Security/GPG.cs

https://bitbucket.org/AdamMil/adammil.net
C# | 5775 lines | 4756 code | 581 blank | 438 comment | 1152 complexity | b51005ae4555811860c2929a6b59822a MD5 | raw file
Possible License(s): GPL-2.0
  1. /*
  2. AdamMil.Security is a .NET library providing OpenPGP-based security.
  3. http://www.adammil.net/
  4. Copyright (C) 2008-2013 Adam Milazzo
  5. This program is free software; you can redistribute it and/or
  6. modify it under the terms of the GNU General Public License
  7. as published by the Free Software Foundation; either version 2
  8. of the License, or (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program; if not, write to the Free Software
  15. Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  16. */
  17. using System;
  18. using System.Collections.Generic;
  19. using System.Diagnostics;
  20. using System.Globalization;
  21. using System.IO;
  22. using System.Text;
  23. using System.Text.RegularExpressions;
  24. using System.Threading;
  25. using AdamMil.Collections;
  26. using AdamMil.IO;
  27. using AdamMil.Security.PGP.GPG.StatusMessages;
  28. using AdamMil.Utilities;
  29. using Microsoft.Win32.SafeHandles;
  30. using SecureString=System.Security.SecureString;
  31. namespace AdamMil.Security.PGP.GPG
  32. {
  33. /// <summary>Processes text output from GPG.</summary>
  34. public delegate void TextLineHandler(string line);
  35. #region GPG
  36. /// <summary>A base class to aid in the implementation of interfaces to the GNU Privacy Guard (GPG).</summary>
  37. public abstract class GPG : PGPSystem
  38. {
  39. /// <summary>Parses an argument from a GPG status message into a cipher name, or null if the cipher type cannot be
  40. /// determined.
  41. /// </summary>
  42. public static string ParseCipher(string str)
  43. {
  44. switch((OpenPGPCipher)int.Parse(str, CultureInfo.InvariantCulture))
  45. {
  46. case OpenPGPCipher.AES: return SymmetricCipher.AES;
  47. case OpenPGPCipher.AES192: return SymmetricCipher.AES192;
  48. case OpenPGPCipher.AES256: return SymmetricCipher.AES256;
  49. case OpenPGPCipher.Blowfish: return SymmetricCipher.Blowfish;
  50. case OpenPGPCipher.CAST5: return SymmetricCipher.CAST5;
  51. case OpenPGPCipher.IDEA: return SymmetricCipher.IDEA;
  52. case OpenPGPCipher.TripleDES: return SymmetricCipher.TripleDES;
  53. case OpenPGPCipher.Twofish: return SymmetricCipher.Twofish;
  54. case OpenPGPCipher.DESSK: return "DESSK";
  55. case OpenPGPCipher.SAFER: return "SAFER";
  56. case OpenPGPCipher.Unencrypted: return "Unencrypted";
  57. default: return string.IsNullOrEmpty(str) ? null : str;
  58. }
  59. }
  60. /// <summary>Parses an argument from a GPG status message into a hash algorithm name, or null if the algorithm cannot
  61. /// be determined.
  62. /// </summary>
  63. public static string ParseHashAlgorithm(string str)
  64. {
  65. switch((OpenPGPHashAlgorithm)int.Parse(str, CultureInfo.InvariantCulture))
  66. {
  67. case OpenPGPHashAlgorithm.MD5: return HashAlgorithm.MD5;
  68. case OpenPGPHashAlgorithm.RIPEMD160: return HashAlgorithm.RIPEMD160;
  69. case OpenPGPHashAlgorithm.SHA1: return HashAlgorithm.SHA1;
  70. case OpenPGPHashAlgorithm.SHA224: return HashAlgorithm.SHA224;
  71. case OpenPGPHashAlgorithm.SHA256: return HashAlgorithm.SHA256;
  72. case OpenPGPHashAlgorithm.SHA384: return HashAlgorithm.SHA384;
  73. case OpenPGPHashAlgorithm.SHA512: return HashAlgorithm.SHA512;
  74. case OpenPGPHashAlgorithm.HAVAL: return "HAVAL-5-160";
  75. case OpenPGPHashAlgorithm.MD2: return "MD2";
  76. case OpenPGPHashAlgorithm.TIGER192: return "TIGER192";
  77. default: return string.IsNullOrEmpty(str) ? null : str;
  78. }
  79. }
  80. /// <summary>Parses an argument from a GPG status message into a key type name, or null if the key type cannot
  81. /// be determined.
  82. /// </summary>
  83. public static string ParseKeyType(string str)
  84. {
  85. switch((OpenPGPKeyType)int.Parse(str, CultureInfo.InvariantCulture))
  86. {
  87. case OpenPGPKeyType.DSA:
  88. return KeyType.DSA;
  89. case OpenPGPKeyType.ElGamal: case OpenPGPKeyType.ElGamalEncryptOnly:
  90. return KeyType.ElGamal;
  91. case OpenPGPKeyType.RSA: case OpenPGPKeyType.RSAEncryptOnly: case OpenPGPKeyType.RSASignOnly:
  92. return KeyType.RSA;
  93. default: return string.IsNullOrEmpty(str) ? null : str;
  94. }
  95. }
  96. /// <summary>Parses an argument from a GPG status message into a timestamp.</summary>
  97. public static DateTime ParseTimestamp(string str)
  98. {
  99. if(str.IndexOf('T') == -1) // the time is specified in seconds since Midnight, January 1, 1970
  100. {
  101. long seconds = long.Parse(str, CultureInfo.InvariantCulture);
  102. return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(seconds);
  103. }
  104. else // the date is in ISO8601 format. DateTime.Parse() can handle it.
  105. {
  106. return DateTime.Parse(str, CultureInfo.InvariantCulture);
  107. }
  108. }
  109. /// <summary>Parses an argument from a GPG status message into a timestamp, or null if there is no timestamp.</summary>
  110. public static DateTime? ParseNullableTimestamp(string str)
  111. {
  112. return string.IsNullOrEmpty(str) || str.Equals("0", StringComparison.Ordinal) ?
  113. (DateTime?)null : ParseTimestamp(str);
  114. }
  115. }
  116. #endregion
  117. #region ExeGPG
  118. /// <summary>This class implements a connection to the GNU Privacy Guard via piping input to and from its command-line
  119. /// executable.
  120. /// </summary>
  121. public class ExeGPG : GPG
  122. {
  123. /// <summary>Initializes a new <see cref="ExeGPG"/> with no reference to the GPG executable.</summary>
  124. public ExeGPG() { }
  125. /// <summary>Initializes a new <see cref="ExeGPG"/> with a full path to the GPG executable.</summary>
  126. public ExeGPG(string exePath)
  127. {
  128. Initialize(exePath);
  129. }
  130. /// <summary>Raised when a line of text is to be logged.</summary>
  131. public event TextLineHandler LineLogged;
  132. /// <summary>Gets or sets whether the GPG agent will be used. If enabled, GPG may use its own user interface to query
  133. /// for passwords, bypassing the support provided by this library. The default is false. However, this property has
  134. /// no effect when using GPG2, because GPG2 doesn't allow the agent to be disabled.
  135. /// </summary>
  136. public bool EnableGPGAgent
  137. {
  138. get { return enableAgent; }
  139. set { enableAgent = value; }
  140. }
  141. /// <summary>Gets the path to the GPG executable, or null if <see cref="Initialize"/> has not been called.</summary>
  142. public string ExecutablePath
  143. {
  144. get { return exePath; }
  145. }
  146. /// <summary>Gets or sets whether the <see cref="SignatureBase.KeyFingerprint">KeySignature.KeyFingerprint</see>
  147. /// field will be retrieved. According to the GPG documentation, GPG won't return fingerprints on key signatures
  148. /// unless signature verification is enabled and signature caching is disabled, due to "various technical reasons".
  149. /// Checking the signatures and disabling the cache causes a significant performance hit, however, so by default it
  150. /// is not done. If this property is set to true, the cache will be disabled and signature verification will be
  151. /// enabled on all signature retrievals, allowing GPG to return the key signature fingerprint. Note that even with
  152. /// this property set to true, the fingerprint still won't be set if the key signature failed verification.
  153. /// </summary>
  154. public bool RetrieveKeySignatureFingerprints
  155. {
  156. get { return retrieveKeySignatureFingerprints; }
  157. set { retrieveKeySignatureFingerprints = value; }
  158. }
  159. /// <summary>Gets the version of the GPG executable, encoded as an integer so that 1.4.9 becomes 10409 and 2.0.21 becomes
  160. /// 20021. Note that the version number is retrieved when this class is instantiated with an executable and whenever
  161. /// <see cref="Initialize"/> is called. If a newer version of GPG is installed in the mean time, the version reported by this
  162. /// property will not be updated until <see cref="Initialize"/> is called again.
  163. /// </summary>
  164. public int Version
  165. {
  166. get { return gpgVersion; }
  167. }
  168. #region Configuration
  169. /// <include file="documentation.xml" path="/Security/PGPSystem/GetDefaultPrimaryKeyType/node()"/>
  170. public override string GetDefaultPrimaryKeyType()
  171. {
  172. return KeyType.DSA;
  173. }
  174. /// <include file="documentation.xml" path="/Security/PGPSystem/GetDefaultSubkeyType/node()"/>
  175. public override string GetDefaultSubkeyType()
  176. {
  177. return KeyType.ElGamal;
  178. }
  179. /// <include file="documentation.xml" path="/Security/PGPSystem/GetMaximumKeyLength/node()"/>
  180. public override int GetMaximumKeyLength(string keyType)
  181. {
  182. if(!string.Equals(keyType, "RSA-E", StringComparison.OrdinalIgnoreCase) &&
  183. !string.Equals(keyType, "RSA-S", StringComparison.OrdinalIgnoreCase) &&
  184. !string.Equals(keyType, "ELG-E", StringComparison.OrdinalIgnoreCase) &&
  185. !string.Equals(keyType, "ELG", StringComparison.OrdinalIgnoreCase))
  186. {
  187. AssertSupported(keyType, keyTypes, "key type");
  188. }
  189. return string.Equals(keyType, "DSA", StringComparison.OrdinalIgnoreCase) ? 3072 : 4096;
  190. }
  191. /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedCiphers/node()"/>
  192. public override string[] GetSupportedCiphers()
  193. {
  194. AssertInitialized();
  195. return ciphers == null ? new string[0] : (string[])ciphers.Clone();
  196. }
  197. /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedCompressions/node()"/>
  198. public override string[] GetSupportedCompressions()
  199. {
  200. AssertInitialized();
  201. return compressions == null ? new string[0] : (string[])compressions.Clone();
  202. }
  203. /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedHashes/node()"/>
  204. public override string[] GetSupportedHashes()
  205. {
  206. AssertInitialized();
  207. return hashes == null ? new string[0] : (string[])hashes.Clone();
  208. }
  209. /// <include file="documentation.xml" path="/Security/PGPSystem/GetSupportedKeyTypes/node()"/>
  210. public override string[] GetSupportedKeyTypes()
  211. {
  212. AssertInitialized();
  213. return keyTypes == null ? new string[0] : (string[])keyTypes.Clone();
  214. }
  215. #endregion
  216. #region Encryption and signing
  217. /// <include file="documentation.xml" path="/Security/PGPSystem/SignAndEncrypt/node()"/>
  218. public override void SignAndEncrypt(Stream sourceData, Stream destination, SigningOptions signingOptions,
  219. EncryptionOptions encryptionOptions, OutputOptions outputOptions)
  220. {
  221. if(sourceData == null || destination == null || encryptionOptions == null && signingOptions == null)
  222. {
  223. throw new ArgumentNullException();
  224. }
  225. string args = GetOutputArgs(outputOptions);
  226. bool symmetric = false; // whether we're doing password-based encryption (possibly in addition to key-based)
  227. bool customAlgo = false; // whether a custom algorithm was specified
  228. // add the keyrings of all the recipient and signer keys to the command line
  229. List<PrimaryKey> keyringKeys = new List<PrimaryKey>();
  230. if(encryptionOptions != null) // if we'll be doing any encryption
  231. {
  232. // we can't do signing with detached signatures because GPG doesn't have a way to specify the two output files.
  233. // and encryption with
  234. if(signingOptions != null && signingOptions.Type != SignatureType.Embedded)
  235. {
  236. if(signingOptions.Type == SignatureType.ClearSignedText)
  237. {
  238. throw new ArgumentException("Combining encryption with clear-signing does not make sense, because the data "+
  239. "cannot be both encrypted and in the clear.");
  240. }
  241. else
  242. {
  243. throw new NotSupportedException("Simultaneous encryption and detached signing is not supported by GPG. "+
  244. "Perform the encryption and detached signing as two separate steps.");
  245. }
  246. }
  247. symmetric = encryptionOptions.Password != null && encryptionOptions.Password.Length != 0;
  248. // we need recipients if we're not doing password-based encryption
  249. if(!symmetric && encryptionOptions.Recipients.Count == 0 && encryptionOptions.HiddenRecipients.Count == 0)
  250. {
  251. throw new ArgumentException("No recipients were specified.");
  252. }
  253. keyringKeys.AddRange(encryptionOptions.Recipients);
  254. keyringKeys.AddRange(encryptionOptions.HiddenRecipients);
  255. // if there are recipients for key-based encryption, add them to the command line
  256. if(encryptionOptions.Recipients.Count != 0 || encryptionOptions.HiddenRecipients.Count != 0)
  257. {
  258. args += GetFingerprintArgs(encryptionOptions.Recipients, "-r") +
  259. GetFingerprintArgs(encryptionOptions.HiddenRecipients, "-R") + "-e "; // plus the encrypt command
  260. }
  261. if(!string.IsNullOrEmpty(encryptionOptions.Cipher))
  262. {
  263. AssertSupported(encryptionOptions.Cipher, ciphers, "cipher");
  264. args += "--cipher-algo " + EscapeArg(encryptionOptions.Cipher) + " ";
  265. customAlgo = true;
  266. }
  267. if(symmetric) args += "-c "; // add the password-based encryption command if necessary
  268. if(encryptionOptions.AlwaysTrustRecipients) args += "--trust-model always ";
  269. }
  270. if(signingOptions != null) // if we'll be doing any signing
  271. {
  272. if(signingOptions.Signers.Count == 0) throw new ArgumentException("No signers were specified.");
  273. // add the keyrings of the signers to the command prompt
  274. keyringKeys.AddRange(signingOptions.Signers);
  275. if(!string.IsNullOrEmpty(signingOptions.Hash))
  276. {
  277. AssertSupported(encryptionOptions.Cipher, hashes, "hash");
  278. args += "--digest-algo "+EscapeArg(signingOptions.Hash)+" ";
  279. customAlgo = true;
  280. }
  281. // add all of the signers to the command line, and the signing command
  282. args += GetFingerprintArgs(signingOptions.Signers, "-u") +
  283. (signingOptions.Type == SignatureType.Detached ? "-b " :
  284. signingOptions.Type == SignatureType.ClearSignedText ? "--clearsign " : "-s ");
  285. }
  286. args += GetKeyringArgs(keyringKeys, true); // add all the keyrings to the command line
  287. Command command = Execute(args, StatusMessages.ReadInBackground, false);
  288. CommandState commandState = new CommandState(command);
  289. if(customAlgo) commandState.FailureReasons |= FailureReason.UnsupportedAlgorithm; // using a custom algo can cause failure
  290. using(ManualResetEvent ready = new ManualResetEvent(false)) // create an event to signal when the data should be sent
  291. {
  292. ProcessCommand(command, commandState,
  293. delegate(Command cmd, CommandState state)
  294. {
  295. cmd.InputNeeded += delegate(string promptId)
  296. {
  297. if(string.Equals(promptId, "untrusted_key.override", StringComparison.Ordinal))
  298. { // this question indicates that a recipient key is not trusted
  299. bool alwaysTrust = encryptionOptions != null && encryptionOptions.AlwaysTrustRecipients;
  300. if(!alwaysTrust) state.FailureReasons |= FailureReason.UntrustedRecipient;
  301. cmd.SendLine(alwaysTrust ? "Y" : "N");
  302. }
  303. else if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) &&
  304. state.PasswordMessage != null && state.PasswordMessage.Type == StatusMessageType.NeedCipherPassphrase)
  305. {
  306. cmd.SendPassword(encryptionOptions.Password, false);
  307. }
  308. else if(!state.Canceled)
  309. {
  310. DefaultPromptHandler(promptId, state);
  311. if(state.Canceled) cmd.Kill(); // kill GPG if the user doesn't give the password, so it doesn't keep asking
  312. }
  313. };
  314. cmd.StatusMessageReceived += delegate(StatusMessage msg)
  315. {
  316. switch(msg.Type)
  317. {
  318. case StatusMessageType.BeginEncryption: case StatusMessageType.BeginSigning:
  319. ready.Set(); // all set. send the data!
  320. break;
  321. default: DefaultStatusMessageHandler(msg, state); break;
  322. }
  323. };
  324. },
  325. delegate(Command cmd, CommandState state)
  326. {
  327. // wait until it's time to write the data or the process aborted
  328. while(!ready.WaitOne(50, false) && !cmd.Process.HasExited) { }
  329. // if the process is still running and it didn't exit before we could copy the input data...
  330. if(!cmd.Process.HasExited) ReadAndWriteStreams(destination, sourceData, cmd.Process);
  331. });
  332. }
  333. if(!command.SuccessfulExit) // if the process wasn't successful, throw an exception
  334. {
  335. if(commandState.Canceled) throw new OperationCanceledException();
  336. else if(encryptionOptions != null) throw new EncryptionFailedException(commandState.FailureReasons);
  337. else throw new SigningFailedException(commandState.FailureReasons);
  338. }
  339. }
  340. /// <include file="documentation.xml" path="/Security/PGPSystem/Decrypt/node()"/>
  341. public override Signature[] Decrypt(Stream ciphertext, Stream destination, DecryptionOptions options)
  342. {
  343. if(ciphertext == null || destination == null) throw new ArgumentNullException();
  344. Command cmd = Execute(GetVerificationArgs(options, true) + "-d", StatusMessages.ReadInBackground, false);
  345. return DecryptVerifyCore(cmd, ciphertext, destination, options);
  346. }
  347. /// <include file="documentation.xml" path="/Security/PGPSystem/Verify2/node()"/>
  348. public override Signature[] Verify(Stream signedData, VerificationOptions options)
  349. {
  350. if(signedData == null) throw new ArgumentNullException();
  351. return VerifyCore(null, signedData, options);
  352. }
  353. /// <include file="documentation.xml" path="/Security/PGPSystem/Verify3/node()"/>
  354. /// <remarks>The signature data (from <paramref name="signature"/>) will be written into a temporary file for the
  355. /// duration of this method call.
  356. /// </remarks>
  357. public override Signature[] Verify(Stream signedData, Stream signature, VerificationOptions options)
  358. {
  359. if(signedData == null || signature == null) throw new ArgumentNullException();
  360. // copy the signature into a temporary file, because we can't pass both streams on standard input
  361. string sigFileName = Path.GetTempFileName();
  362. try
  363. {
  364. using(FileStream file = new FileStream(sigFileName, FileMode.Truncate, FileAccess.Write))
  365. {
  366. signature.CopyTo(file);
  367. }
  368. return VerifyCore(sigFileName, signedData, options);
  369. }
  370. finally { File.Delete(sigFileName); }
  371. }
  372. #endregion
  373. #region Key import and export
  374. /// <include file="documentation.xml" path="/Security/PGPSystem/ExportKeys/node()"/>
  375. public override void ExportKeys(PrimaryKey[] keys, Stream destination, ExportOptions exportOptions,
  376. OutputOptions outputOptions)
  377. {
  378. if(keys == null || destination == null) throw new ArgumentNullException();
  379. if((exportOptions & (ExportOptions.ExportPublicKeys|ExportOptions.ExportSecretKeys)) == 0)
  380. {
  381. throw new ArgumentException("At least one of ExportOptions.ExportPublicKeys or ExportOptions.ExportSecretKeys "+
  382. "must be specified.");
  383. }
  384. if(keys.Length == 0) return;
  385. if((exportOptions & ExportOptions.ExportPublicKeys) != 0)
  386. {
  387. string args = GetKeyringArgs(keys, false) + GetExportArgs(exportOptions, false, true) +
  388. GetOutputArgs(outputOptions) + GetFingerprintArgs(keys);
  389. ExportCore(args, destination);
  390. }
  391. if((exportOptions & ExportOptions.ExportSecretKeys) != 0)
  392. {
  393. string args = GetKeyringArgs(keys, true) + GetExportArgs(exportOptions, true, true) +
  394. GetOutputArgs(outputOptions) + GetFingerprintArgs(keys);
  395. ExportCore(args, destination);
  396. }
  397. }
  398. /// <include file="documentation.xml" path="/Security/PGPSystem/ExportKeys2/node()"/>
  399. public override void ExportKeys(Keyring[] keyrings, bool includeDefaultKeyring, Stream destination,
  400. ExportOptions exportOptions, OutputOptions outputOptions)
  401. {
  402. if(destination == null) throw new ArgumentNullException();
  403. if((exportOptions & (ExportOptions.ExportPublicKeys|ExportOptions.ExportSecretKeys)) == 0)
  404. {
  405. throw new ArgumentException("At least one of ExportOptions.ExportPublicKeys or ExportOptions.ExportSecretKeys "+
  406. "must be specified.");
  407. }
  408. if((exportOptions & ExportOptions.ExportPublicKeys) != 0)
  409. {
  410. string args = GetKeyringArgs(keyrings, !includeDefaultKeyring, false) +
  411. GetExportArgs(exportOptions, false, true) + GetOutputArgs(outputOptions);
  412. ExportCore(args, destination);
  413. }
  414. if((exportOptions & ExportOptions.ExportSecretKeys) != 0)
  415. {
  416. string args = GetKeyringArgs(keyrings, !includeDefaultKeyring, true) +
  417. GetExportArgs(exportOptions, true, true) + GetOutputArgs(outputOptions);
  418. ExportCore(args, destination);
  419. }
  420. }
  421. /// <include file="documentation.xml" path="/Security/PGPSystem/ImportKeys3/node()"/>
  422. public override ImportedKey[] ImportKeys(Stream source, Keyring keyring, ImportOptions options)
  423. {
  424. if(source == null) throw new ArgumentNullException();
  425. CommandState state;
  426. Command cmd = Execute(GetImportArgs(keyring, options) + "--import", StatusMessages.ReadInBackground, false);
  427. ImportedKey[] keys = ImportCore(cmd, source, out state);
  428. if(!cmd.SuccessfulExit) throw new ImportFailedException(state.FailureReasons);
  429. return keys;
  430. }
  431. #endregion
  432. #region Key revocation
  433. /// <include file="documentation.xml" path="/Security/PGPSystem/AddDesignatedRevoker/node()"/>
  434. public override void AddDesignatedRevoker(PrimaryKey key, PrimaryKey revokerKey)
  435. {
  436. if(key == null || revokerKey == null) throw new ArgumentNullException();
  437. if(string.IsNullOrEmpty(revokerKey.Fingerprint))
  438. {
  439. throw new ArgumentException("The revoker key has no fingerprint.");
  440. }
  441. if(string.Equals(key.Fingerprint, revokerKey.Fingerprint, StringComparison.Ordinal))
  442. {
  443. throw new ArgumentException("You can't add a key as its own designated revoker.");
  444. }
  445. DoEdit(key, GetKeyringArgs(new PrimaryKey[] { key, revokerKey }, true), false,
  446. new AddRevokerCommand(revokerKey.Fingerprint));
  447. }
  448. /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRevocationCertificate/node()"/>
  449. public override void GenerateRevocationCertificate(PrimaryKey key, Stream destination, KeyRevocationReason reason,
  450. OutputOptions outputOptions)
  451. {
  452. GenerateRevocationCertificateCore(key, null, destination, reason, outputOptions);
  453. }
  454. /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRevocationCertificateD/node()"/>
  455. public override void GenerateRevocationCertificate(PrimaryKey keyToRevoke, PrimaryKey designatedRevoker,
  456. Stream destination, KeyRevocationReason reason,
  457. OutputOptions outputOptions)
  458. {
  459. if(designatedRevoker == null) throw new ArgumentNullException();
  460. GenerateRevocationCertificateCore(keyToRevoke, designatedRevoker, destination, reason, outputOptions);
  461. }
  462. /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeKeys/node()"/>
  463. public override void RevokeKeys(KeyRevocationReason reason, params PrimaryKey[] keys)
  464. {
  465. RevokeKeysCore(null, reason, keys);
  466. }
  467. /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeKeysD/node()"/>
  468. public override void RevokeKeys(PrimaryKey designatedRevoker, KeyRevocationReason reason, params PrimaryKey[] keys)
  469. {
  470. if(designatedRevoker == null) throw new ArgumentNullException();
  471. RevokeKeysCore(designatedRevoker, reason, keys);
  472. }
  473. /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeSubkeys/node()"/>
  474. public override void RevokeSubkeys(KeyRevocationReason reason, params Subkey[] subkeys)
  475. {
  476. EditSubkeys(subkeys, delegate { return new RevokeSubkeysCommand(reason); });
  477. }
  478. #endregion
  479. #region Key server operations
  480. /// <include file="documentation.xml" path="/Security/PGPSystem/FindKeysOnServer/node()"/>
  481. public override void FindKeysOnServer(Uri keyServer, KeySearchHandler handler, params string[] searchKeywords)
  482. {
  483. if(keyServer == null || handler == null || searchKeywords == null) throw new ArgumentNullException();
  484. if(searchKeywords.Length == 0) throw new ArgumentException("No keywords were given.");
  485. string args = "--keyserver " + EscapeArg(keyServer.AbsoluteUri) + " --with-colons --fixed-list-mode --search-keys";
  486. foreach(string keyword in searchKeywords) args += " " + EscapeArg(keyword);
  487. Command command = Execute(args, StatusMessages.MixIntoStdout, true, true);
  488. CommandState commandState = ProcessCommand(command,
  489. delegate(Command cmd, CommandState state)
  490. {
  491. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  492. },
  493. delegate(Command cmd, CommandState state)
  494. {
  495. List<PrimaryKey> keysFound = new List<PrimaryKey>();
  496. List<UserId> userIds = new List<UserId>();
  497. while(true)
  498. {
  499. string line;
  500. cmd.ReadLine(out line);
  501. if(line != null) LogLine(line);
  502. gotLine:
  503. if(line == null && cmd.StatusMessage == null) break;
  504. if(line == null)
  505. {
  506. switch(cmd.StatusMessage.Type)
  507. {
  508. case StatusMessageType.GetLine:
  509. GetInputMessage m = (GetInputMessage)cmd.StatusMessage;
  510. if(string.Equals(m.PromptId, "keysearch.prompt", StringComparison.Ordinal))
  511. {
  512. // we're done with this chunk of the search, so we'll give the keys to the search handler.
  513. // we won't continue if we didn't find anything, even if the handler returns true
  514. bool shouldContinue = keysFound.Count != 0 && handler(keysFound.ToArray());
  515. cmd.SendLine(shouldContinue ? "N" : "Q");
  516. keysFound.Clear();
  517. break;
  518. }
  519. else goto default;
  520. default: DefaultStatusMessageHandler(cmd.StatusMessage, state); break;
  521. }
  522. }
  523. else if(line.StartsWith("pub:", StringComparison.Ordinal)) // a key description follows
  524. {
  525. string[] fields = line.Split(':');
  526. PrimaryKey key = new PrimaryKey();
  527. if(IsValidKeyId(fields[1])) key.KeyId = fields[1].ToUpperInvariant();
  528. else if(IsValidFingerprint(fields[1])) key.Fingerprint = fields[1].ToUpperInvariant();
  529. else // there's no valid ID, so skip any related records that follow
  530. {
  531. do
  532. {
  533. cmd.ReadLine(out line);
  534. if(line != null) LogLine(line);
  535. }
  536. while(line != null && !line.StartsWith("pub:", StringComparison.Ordinal));
  537. goto gotLine;
  538. }
  539. if(fields.Length > 2 && !string.IsNullOrEmpty(fields[2])) key.KeyType = ParseKeyType(fields[2]);
  540. if(fields.Length > 3 && !string.IsNullOrEmpty(fields[3])) key.Length = int.Parse(fields[3]);
  541. if(fields.Length > 4 && !string.IsNullOrEmpty(fields[4])) key.CreationTime = ParseTimestamp(fields[4]);
  542. if(fields.Length > 5 && !string.IsNullOrEmpty(fields[5])) key.ExpirationTime = ParseNullableTimestamp(fields[5]);
  543. if(fields.Length > 6 && !string.IsNullOrEmpty(fields[6]))
  544. {
  545. foreach(char c in fields[6])
  546. {
  547. switch(char.ToLowerInvariant(c))
  548. {
  549. case 'd': key.Disabled = true; break;
  550. case 'e': key.Expired = true; break;
  551. case 'r': key.Revoked = true; break;
  552. }
  553. }
  554. }
  555. // now parse the user IDs
  556. while(true)
  557. {
  558. cmd.ReadLine(out line);
  559. if(line == null) break; // if we hit a status message or EOF, break
  560. LogLine(line);
  561. if(line.StartsWith("pub:", StringComparison.Ordinal)) break;
  562. else if(!line.StartsWith("uid", StringComparison.Ordinal)) continue;
  563. fields = line.Split(':');
  564. if(string.IsNullOrEmpty(fields[1])) continue;
  565. UserId id = new UserId();
  566. id.PrimaryKey = key;
  567. id.Name = CUnescape(fields[1]);
  568. id.Signatures = NoSignatures;
  569. if(fields.Length > 2 && !string.IsNullOrEmpty(fields[2])) id.CreationTime = ParseTimestamp(fields[2]);
  570. id.MakeReadOnly();
  571. userIds.Add(id);
  572. }
  573. if(userIds.Count != 0)
  574. {
  575. key.Attributes = NoAttributes;
  576. key.DesignatedRevokers = NoRevokers;
  577. key.Signatures = NoSignatures;
  578. key.Subkeys = NoSubkeys;
  579. key.UserIds = new ReadOnlyListWrapper<UserId>(userIds.ToArray());
  580. key.MakeReadOnly();
  581. keysFound.Add(key);
  582. userIds.Clear();
  583. }
  584. goto gotLine;
  585. }
  586. }
  587. });
  588. if(!command.SuccessfulExit) throw new KeyServerFailedException("Key search failed.", commandState.FailureReasons);
  589. }
  590. /// <include file="documentation.xml" path="/Security/PGPSystem/ImportKeysFromServer/node()"/>
  591. public override ImportedKey[] ImportKeysFromServer(KeyDownloadOptions options, Keyring keyring,
  592. params string[] keyFingerprintsOrIds)
  593. {
  594. if(keyFingerprintsOrIds == null) throw new ArgumentNullException();
  595. if(keyFingerprintsOrIds.Length == 0) return new ImportedKey[0];
  596. string args = GetKeyServerArgs(options, true) + GetImportArgs(keyring, options.ImportOptions) + "--recv-keys";
  597. foreach(string id in keyFingerprintsOrIds)
  598. {
  599. if(string.IsNullOrEmpty(id)) throw new ArgumentException("A key ID was null or empty.");
  600. args += " " + id;
  601. }
  602. return KeyServerCore(args, "Key import", true, false);
  603. }
  604. /// <include file="documentation.xml" path="/Security/PGPSystem/RefreshKeyringFromServer/node()"/>
  605. public override ImportedKey[] RefreshKeysFromServer(KeyDownloadOptions options, Keyring keyring)
  606. {
  607. string args = GetImportArgs(keyring, options == null ? ImportOptions.Default : options.ImportOptions) +
  608. GetKeyServerArgs(options, false) + "--refresh-keys";
  609. return KeyServerCore(args, "Keyring refresh", true, false);
  610. }
  611. /// <include file="documentation.xml" path="/Security/PGPSystem/RefreshKeysFromServer/node()"/>
  612. public override ImportedKey[] RefreshKeysFromServer(KeyDownloadOptions options, params PrimaryKey[] keys)
  613. {
  614. if(keys == null) throw new ArgumentNullException();
  615. if(keys.Length == 0) return new ImportedKey[0];
  616. string args = GetKeyringArgs(keys, true) + GetKeyServerArgs(options, false) +
  617. GetImportArgs(null, options == null ? ImportOptions.Default : options.ImportOptions) +
  618. "--refresh-keys " + GetFingerprintArgs(keys);
  619. return KeyServerCore(args, "Key refresh", true, false);
  620. }
  621. /// <include file="documentation.xml" path="/Security/PGPSystem/UploadKeys/node()"/>
  622. public override void UploadKeys(KeyUploadOptions options, params PrimaryKey[] keys)
  623. {
  624. if(keys == null) throw new ArgumentNullException();
  625. if(keys.Length == 0) return;
  626. string args = GetKeyringArgs(keys, false) + GetKeyServerArgs(options, true) +
  627. GetExportArgs(options.ExportOptions, false, false) + "--send-keys " + GetFingerprintArgs(keys);
  628. KeyServerCore(args, "Key upload", false, true);
  629. }
  630. #endregion
  631. #region Key signing
  632. /// <include file="documentation.xml" path="/Security/PGPSystem/DeleteSignatures/node()"/>
  633. public override void DeleteSignatures(params KeySignature[] signatures)
  634. {
  635. EditSignatures(signatures, delegate(KeySignature[] sigs) { return new DeleteSigsCommand(sigs); });
  636. }
  637. /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeSignatures/node()"/>
  638. public override void RevokeSignatures(UserRevocationReason reason, params KeySignature[] signatures)
  639. {
  640. EditSignatures(signatures, delegate(KeySignature[] sigs) { return new RevokeSigsCommand(reason, sigs); });
  641. }
  642. /// <include file="documentation.xml" path="/Security/PGPSystem/SignAttributes/node()"/>
  643. public override void SignAttributes(UserAttribute[] attributes, PrimaryKey signingKey, KeySigningOptions options)
  644. {
  645. if(attributes == null || signingKey == null) throw new ArgumentNullException();
  646. foreach(List<UserAttribute> attrList in GroupAttributesByKey(attributes))
  647. {
  648. EditCommand[] commands = new EditCommand[attrList.Count+1];
  649. for(int i=0; i<attrList.Count; i++) commands[i] = new RawCommand("uid " + attrList[i].Id);
  650. commands[attrList.Count] = new SignKeyCommand(options, false);
  651. PrimaryKey keyToEdit = attrList[0].PrimaryKey;
  652. DoEdit(keyToEdit, "--ask-cert-level " + GetKeyringArgs(new PrimaryKey[] { keyToEdit, signingKey }, true) +
  653. "-u " + signingKey.Fingerprint, false, commands);
  654. }
  655. }
  656. /// <include file="documentation.xml" path="/Security/PGPSystem/SignKeys/node()"/>
  657. public override void SignKeys(PrimaryKey[] keysToSign, PrimaryKey signingKey, KeySigningOptions options)
  658. {
  659. if(keysToSign == null || signingKey == null) throw new ArgumentNullException();
  660. foreach(PrimaryKey key in keysToSign)
  661. {
  662. if(key == null) throw new ArgumentException("A key was null.");
  663. DoEdit(key, "--ask-cert-level " + GetKeyringArgs(new PrimaryKey[] { key, signingKey }, true) +
  664. "-u " + signingKey.Fingerprint, false, new SignKeyCommand(options, true));
  665. }
  666. }
  667. #endregion
  668. #region Keyring queries
  669. /// <include file="documentation.xml" path="/Security/PGPSystem/FindKey/node()"/>
  670. public override PrimaryKey FindKey(string keywordOrId, Keyring keyring, ListOptions options)
  671. {
  672. PrimaryKey[] keys = FindKeys(new string[] { keywordOrId },
  673. keyring == null ? null : new Keyring[] { keyring }, keyring == null, options);
  674. return keys[0];
  675. }
  676. /// <include file="documentation.xml" path="/Security/PGPSystem/FindKeys/node()"/>
  677. public override PrimaryKey[] FindKeys(string[] fingerprintsOrIds, Keyring[] keyrings,
  678. bool includeDefaultKeyring, ListOptions options)
  679. {
  680. if(fingerprintsOrIds == null) throw new ArgumentNullException();
  681. if(fingerprintsOrIds.Length == 0) return new PrimaryKey[0];
  682. // create search arguments containing all the key IDs
  683. string searchArgs = null;
  684. if(fingerprintsOrIds.Length > 1) // if there's more than one ID, we can't allow fancy matches like email addresses,
  685. { // so validate and normalize all IDs
  686. // clone the array so we don't modify the parameters
  687. fingerprintsOrIds = (string[])fingerprintsOrIds.Clone();
  688. for(int i=0; i<fingerprintsOrIds.Length; i++)
  689. {
  690. if(string.IsNullOrEmpty(fingerprintsOrIds[i]))
  691. {
  692. throw new ArgumentException("A fingerprint/ID was null or empty.");
  693. }
  694. fingerprintsOrIds[i] = NormalizeKeyId(fingerprintsOrIds[i]);
  695. }
  696. }
  697. // add all IDs to the command line
  698. foreach(string id in fingerprintsOrIds) searchArgs += EscapeArg(id) + " ";
  699. PrimaryKey[] keys = GetKeys(keyrings, includeDefaultKeyring, options, searchArgs);
  700. if(fingerprintsOrIds.Length == 1) // if there was only a single key returned, then that's the one
  701. {
  702. return keys.Length == 1 ? keys : new PrimaryKey[1];
  703. }
  704. else
  705. {
  706. // add each key found to a dictionary
  707. Dictionary<string, PrimaryKey> keyDict = new Dictionary<string, PrimaryKey>();
  708. foreach(PrimaryKey key in keys)
  709. {
  710. keyDict[key.Fingerprint] = key;
  711. keyDict[key.KeyId] = key;
  712. keyDict[key.ShortKeyId] = key;
  713. }
  714. // then create the return array and return the keys found
  715. if(keys.Length != fingerprintsOrIds.Length) keys = new PrimaryKey[fingerprintsOrIds.Length];
  716. for(int i=0; i<keys.Length; i++) keyDict.TryGetValue(fingerprintsOrIds[i], out keys[i]);
  717. return keys;
  718. }
  719. }
  720. /// <include file="documentation.xml" path="/Security/PGPSystem/GetKeys/node()"/>
  721. public override PrimaryKey[] GetKeys(Keyring[] keyrings, bool includeDefaultKeyring, ListOptions options)
  722. {
  723. return GetKeys(keyrings, includeDefaultKeyring, options, null);
  724. }
  725. #endregion
  726. #region Miscellaneous
  727. /// <include file="documentation.xml" path="/Security/PGPSystem/CreateTrustDatabase/node()"/>
  728. public override void CreateTrustDatabase(string path)
  729. {
  730. // the following creates a valid, empty version 3 trust database. (see gpg-src\doc\DETAILS)
  731. using(FileStream dbFile = File.Open(path, FileMode.Create, FileAccess.Write))
  732. {
  733. dbFile.SetLength(40); // the database is 40 bytes long, but only the first 16 bytes are non-zero
  734. byte[] headerStart = new byte[] { 1, 0x67, 0x70, 0x67, 3, 3, 1, 5, 1, 0, 0, 0 };
  735. dbFile.Write(headerStart, 0, headerStart.Length);
  736. // the next four bytes are the big-endian creation timestamp in seconds since epoch
  737. dbFile.WriteBE4((int)((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds));
  738. }
  739. }
  740. /// <include file="documentation.xml" path="/Security/PGPSystem/GenerateRandomData/node()"/>
  741. public override void GetRandomData(Randomness quality, byte[] buffer, int index, int count)
  742. {
  743. Utility.ValidateRange(buffer, index, count);
  744. if(count == 0) return;
  745. // "gpg --gen-random QUALITY COUNT" writes random COUNT bytes to standard output. QUALITY is a value from 0 to 2
  746. // representing the quality of the random number generator to use
  747. string qualityArg;
  748. if(quality == Randomness.Weak) qualityArg = "0";
  749. else if(quality == Randomness.TooStrong) qualityArg = "2";
  750. else qualityArg = "1"; // we'll default to the Strong level
  751. Command command = Execute("--gen-random " + qualityArg + " " + count.ToStringInvariant(), StatusMessages.Ignore, true, true);
  752. ProcessCommand(command, null,
  753. delegate(Command cmd, CommandState state) { count -= cmd.Process.StandardOutput.BaseStream.FullRead(buffer, 0, count); });
  754. if(count != 0) throw new PGPException("GPG didn't write enough random bytes.");
  755. command.CheckExitCode();
  756. }
  757. /// <include file="documentation.xml" path="/Security/PGPSystem/Hash/node()"/>
  758. public override byte[] Hash(Stream data, string hashAlgorithm)
  759. {
  760. if(data == null) throw new ArgumentNullException();
  761. bool customAlgorithm = false;
  762. if(hashAlgorithm == null || hashAlgorithm == HashAlgorithm.Default)
  763. {
  764. hashAlgorithm = HashAlgorithm.SHA1;
  765. }
  766. else if(hashAlgorithm.Length == 0)
  767. {
  768. throw new ArgumentException("Unspecified hash algorithm.");
  769. }
  770. else
  771. {
  772. AssertSupported(hashAlgorithm, hashes, "hash");
  773. customAlgorithm = true;
  774. }
  775. // "gpg --print-md ALGO" hashes data presented on standard input. if the algorithm is not supported, gpg exits
  776. // immediately with error code 2. otherwise, it consumes all available input, and then prints the hash in a
  777. // human-readable form, with hex digits nicely formatted into blocks and lines. we'll feed it all the input and
  778. // then read the output.
  779. List<byte> hash = new List<byte>();
  780. Command command = Execute("--print-md " + EscapeArg(hashAlgorithm), StatusMessages.Ignore, false, true);
  781. ProcessCommand(command, null,
  782. delegate(Command cmd, CommandState state)
  783. {
  784. if(WriteStreamToProcess(data, cmd.Process))
  785. {
  786. while(true)
  787. {
  788. string line = cmd.Process.StandardOutput.ReadLine();
  789. if(line == null) break;
  790. // on each line, there are some hex digits separated with whitespace. we'll read each character, but only
  791. // use characters that are valid hex digits
  792. int value = 0, chars = 0;
  793. foreach(char c in line.ToLowerInvariant())
  794. {
  795. if(IsHexDigit(c))
  796. {
  797. value = (value<<4) + GetHexValue(c);
  798. if(++chars == 2) // when two hex digits have accumulated, a byte is complete, so write it to the output
  799. {
  800. hash.Add((byte)value);
  801. chars = 0;
  802. }
  803. }
  804. }
  805. }
  806. }
  807. });
  808. if(!command.SuccessfulExit || hash.Count == 0)
  809. {
  810. throw new PGPException("Hash failed.",
  811. customAlgorithm ? FailureReason.UnsupportedAlgorithm : FailureReason.None);
  812. }
  813. return hash.ToArray();
  814. }
  815. #endregion
  816. #region Primary key management
  817. /// <include file="documentation.xml" path="/Security/PGPSystem/AddSubkey/node()"/>
  818. public override void AddSubkey(PrimaryKey key, string keyType, KeyCapabilities capabilities, int keyLength,
  819. DateTime? expiration)
  820. {
  821. // if a custom length is specified, it might be long enough to require a DSA2 key, so add the option just in case
  822. DoEdit(key, (keyLength == 0 ? null : "--enable-dsa2 ") + "--expert", true,
  823. new AddSubkeyCommand(this, keyType, capabilities, keyLength, expiration));
  824. }
  825. /// <include file="documentation.xml" path="/Security/PGPSystem/ChangeExpiration/node()"/>
  826. public override void ChangeExpiration(Key key, DateTime? expiration)
  827. {
  828. if(key == null) throw new ArgumentNullException();
  829. Subkey subkey = key as Subkey;
  830. if(subkey == null) // if it's not a subkey, we'll assume it's a primary key, and change that
  831. {
  832. DoEdit(key.GetPrimaryKey(), new ChangeExpirationCommand(expiration));
  833. }
  834. else // otherwise, first select the subkey
  835. {
  836. DoEdit(key.GetPrimaryKey(), new SelectSubkeyCommand(subkey.Fingerprint, true), new ChangeExpirationCommand(expiration));
  837. }
  838. }
  839. /// <include file="documentation.xml" path="/Security/PGPSystem/ChangePassword/node()"/>
  840. public override void ChangePassword(PrimaryKey key, SecureString password)
  841. {
  842. DoEdit(key, new ChangePasswordCommand(password));
  843. }
  844. /// <include file="documentation.xml" path="/Security/PGPSystem/CleanKeys/node()"/>
  845. public override void CleanKeys(params PrimaryKey[] keys)
  846. {
  847. RepeatedRawEditCommand(keys, "clean");
  848. }
  849. /// <include file="documentation.xml" path="/Security/PGPSystem/CreateKey/node()"/>
  850. /// <remarks>If <see cref="NewKeyOptions.Keyring"/> is set, the key will not be automatically trusted in the default
  851. /// trust database.
  852. /// </remarks>
  853. public override PrimaryKey CreateKey(NewKeyOptions options)
  854. {
  855. if(options == null) throw new ArgumentNullException();
  856. string email = Trim(options.Email), realName = Trim(options.RealName), comment = Trim(options.Comment);
  857. if(string.IsNullOrEmpty(email) && string.IsNullOrEmpty(realName))
  858. {
  859. throw new ArgumentException("At least one of NewKeyOptions.Email or NewKeyOptions.RealName must be set.");
  860. }
  861. if(ContainsControlCharacters(options.Comment + options.Email + options.RealName + options.Password))
  862. {
  863. throw new ArgumentException("The comment, email, real name, and/or password contains control characters. "+
  864. "Remove them.");
  865. }
  866. bool primaryIsDSA = string.IsNullOrEmpty(options.KeyType) || // DSA is the default primary key type
  867. string.Equals(options.KeyType, KeyType.DSA, StringComparison.OrdinalIgnoreCase);
  868. bool primaryIsRSA = string.Equals(options.KeyType, KeyType.RSA, StringComparison.OrdinalIgnoreCase);
  869. if(!primaryIsDSA && !primaryIsRSA)
  870. {
  871. throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm,
  872. "Primary key type "+options.KeyType+" is not supported.");
  873. }
  874. // GPG supports key sizes from 1024 to 3072 (for DSA keys) or 4096 (for other keys)
  875. int maxKeyLength = primaryIsDSA ? 3072 : 4096;
  876. if(options.KeyLength != 0 && (options.KeyLength < 1024 || options.KeyLength > maxKeyLength))
  877. {
  878. throw new KeyCreationFailedException(FailureReason.None, "Key length " +
  879. options.KeyLength.ToStringInvariant() + " is not supported.");
  880. }
  881. bool subIsDSA = string.Equals(options.SubkeyType, KeyType.DSA, StringComparison.OrdinalIgnoreCase);
  882. bool subIsELG = string.IsNullOrEmpty(options.SubkeyType) || // ElGamal is the default subkey type
  883. string.Equals(options.SubkeyType, KeyType.ElGamal, StringComparison.OrdinalIgnoreCase) ||
  884. string.Equals(options.SubkeyType, "ELG-E", StringComparison.OrdinalIgnoreCase);
  885. bool subIsRSA = string.Equals(options.SubkeyType, KeyType.RSA, StringComparison.OrdinalIgnoreCase) ||
  886. string.Equals(options.SubkeyType, "RSA-E", StringComparison.OrdinalIgnoreCase) ||
  887. string.Equals(options.SubkeyType, "RSA-S", StringComparison.OrdinalIgnoreCase);
  888. bool subIsNone = string.Equals(options.SubkeyType, KeyType.None, StringComparison.OrdinalIgnoreCase);
  889. if(!subIsNone && !subIsDSA && !subIsELG && !subIsRSA)
  890. {
  891. throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm,
  892. "Subkey type "+options.SubkeyType+" is not supported.");
  893. }
  894. KeyCapabilities primaryCapabilities = options.KeyCapabilities, subCapabilities = options.SubkeyCapabilities;
  895. if(primaryCapabilities == KeyCapabilities.Default)
  896. {
  897. primaryCapabilities = KeyCapabilities.Certify | KeyCapabilities.Sign | KeyCapabilities.Authenticate;
  898. }
  899. if(!subIsNone) // if a subkey will be created
  900. {
  901. // GPG supports key sizes from 1024 to 3072 (for DSA keys) or 4096 (for other keys)
  902. maxKeyLength = subIsDSA ? 3072 : 4096;
  903. if(options.SubkeyLength != 0 && (options.SubkeyLength < 1024 || options.SubkeyLength > maxKeyLength))
  904. {
  905. throw new KeyCreationFailedException(FailureReason.None, "Key length "+
  906. options.SubkeyLength.ToStringInvariant() + " is not supported.");
  907. }
  908. if(subCapabilities == KeyCapabilities.Default)
  909. {
  910. subCapabilities = subIsDSA ? KeyCapabilities.Sign : KeyCapabilities.Encrypt;
  911. }
  912. }
  913. int keyExpirationDays = GetExpirationDays(options.KeyExpiration);
  914. int subkeyExpirationDays = GetExpirationDays(options.SubkeyExpiration);
  915. // the options look good, so lets make the key
  916. string keyFingerprint = null, args = GetKeyringArgs(options.Keyring, true);
  917. // if we're using DSA keys greater than 1024 bits, we need to enable DSA2 support
  918. if(primaryIsDSA && options.KeyLength > 1024 || subIsDSA && options.SubkeyLength > 1024) args += "--enable-dsa2 ";
  919. Command command = Execute(args + "--batch --gen-key", StatusMessages.ReadInBackground, false);
  920. CommandState commandState = ProcessCommand(command,
  921. delegate(Command cmd, CommandState state)
  922. {
  923. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  924. cmd.StatusMessageReceived += delegate(StatusMessage msg)
  925. {
  926. if(msg.Type == StatusMessageType.KeyCreated) // when the key is created, grab its fingerprint
  927. {
  928. KeyCreatedMessage m = (KeyCreatedMessage)msg;
  929. if(m.PrimaryKeyCreated) keyFingerprint = m.Fingerprint;
  930. }
  931. else DefaultStatusMessageHandler(msg, state);
  932. };
  933. },
  934. delegate(Command cmd, CommandState state)
  935. {
  936. cmd.Process.StandardInput.WriteLine("Key-Type: " + (primaryIsDSA ? "DSA" : "RSA"));
  937. int keyLength = options.KeyLength != 0 ? options.KeyLength : primaryIsDSA ? 1024 : 2048;
  938. cmd.Process.StandardInput.WriteLine("Key-Length: " + keyLength.ToStringInvariant());
  939. cmd.Process.StandardInput.WriteLine("Key-Usage: " + GetKeyUsageString(primaryCapabilities));
  940. if(!subIsNone)
  941. {
  942. cmd.Process.StandardInput.WriteLine("Subkey-Type: " + (subIsDSA ? "DSA" : subIsELG ? "ELG-E" : "RSA"));
  943. cmd.Process.StandardInput.WriteLine("Subkey-Usage: " + GetKeyUsageString(subCapabilities));
  944. keyLength = options.SubkeyLength != 0 ? options.SubkeyLength : subIsDSA ? 1024 : 2048;
  945. cmd.Process.StandardInput.WriteLine("Subkey-Length: " + keyLength.ToStringInvariant());
  946. }
  947. if(!string.IsNullOrEmpty(realName)) cmd.Process.StandardInput.WriteLine("Name-Real: " + realName);
  948. if(!string.IsNullOrEmpty(email)) cmd.Process.StandardInput.WriteLine("Name-Email: " + email);
  949. if(!string.IsNullOrEmpty(comment)) cmd.Process.StandardInput.WriteLine("Name-Comment: " + comment);
  950. if(options.Password != null && options.Password.Length != 0)
  951. {
  952. cmd.Process.StandardInput.Write("Passphrase: ");
  953. options.Password.Process(delegate(char[] chars) { cmd.Process.StandardInput.WriteLine(chars); });
  954. }
  955. // GPG doesn't allow separate expiration dates for the primary key and subkey during key creation, so we'll
  956. // just use the primary key's expiration date and set the subkey's date later
  957. if(options.KeyExpiration.HasValue)
  958. {
  959. cmd.Process.StandardInput.WriteLine("Expire-Date: " +
  960. keyExpirationDays.ToStringInvariant() + "d");
  961. }
  962. cmd.Process.StandardInput.Close(); // close STDIN so GPG can start generating the key
  963. });
  964. PrimaryKey newKey = !command.SuccessfulExit || keyFingerprint == null ?
  965. null : FindKey(keyFingerprint, options.Keyring, ListOptions.Default);
  966. if(newKey == null) throw new KeyCreationFailedException(commandState.FailureReasons);
  967. if(!subIsNone && keyExpirationDays != subkeyExpirationDays)
  968. {
  969. try
  970. {
  971. DoEdit(newKey, new SetDefaultPasswordCommand(options.Password),
  972. new SelectSubkeyCommand(newKey.Subkeys[0].Fingerprint, true),
  973. new ChangeExpirationCommand(options.SubkeyExpiration));
  974. newKey = RefreshKey(newKey);
  975. if(newKey == null) throw new KeyCreationFailedException("The key was created, but then disappeared suddenly.");
  976. }
  977. catch(Exception ex)
  978. {
  979. throw new KeyCreationFailedException("The key was created, but the subkey expiration date could not be set.",
  980. ex);
  981. }
  982. }
  983. return newKey;
  984. }
  985. /// <include file="documentation.xml" path="/Security/PGPSystem/DeleteKeys/node()"/>
  986. public override void DeleteKeys(PrimaryKey[] keys, KeyDeletion deletion)
  987. {
  988. if(keys == null) throw new ArgumentNullException();
  989. string args = GetKeyringArgs(keys, deletion == KeyDeletion.PublicAndSecret);
  990. args += (deletion == KeyDeletion.Secret ? "--delete-secret-key " : "--delete-secret-and-public-key ") +
  991. GetFingerprintArgs(keys);
  992. Command command = Execute(args, StatusMessages.ReadInBackground, true);
  993. CommandState commandState = ProcessCommand(command,
  994. delegate(Command cmd, CommandState state)
  995. {
  996. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  997. cmd.StatusMessageReceived += delegate(StatusMessage msg) { DefaultStatusMessageHandler(msg, state); };
  998. cmd.InputNeeded += delegate(string promptId)
  999. {
  1000. if(string.Equals(promptId, "delete_key.okay", StringComparison.Ordinal) ||
  1001. string.Equals(promptId, "delete_key.secret.okay", StringComparison.Ordinal))
  1002. {
  1003. cmd.SendLine("Y");
  1004. }
  1005. else DefaultPromptHandler(promptId, state);
  1006. };
  1007. });
  1008. if(!command.SuccessfulExit) throw new KeyEditFailedException("Deleting keys failed.", commandState.FailureReasons);
  1009. }
  1010. /// <include file="documentation.xml" path="/Security/PGPSystem/DeleteSubkeys/node()"/>
  1011. public override void DeleteSubkeys(params Subkey[] subkeys)
  1012. {
  1013. EditSubkeys(subkeys, delegate { return new DeleteSubkeysCommand(); });
  1014. }
  1015. /// <include file="documentation.xml" path="/Security/PGPSystem/DisableKeys/node()"/>
  1016. public override void DisableKeys(params PrimaryKey[] keys)
  1017. {
  1018. RepeatedRawEditCommand(keys, "disable");
  1019. }
  1020. /// <include file="documentation.xml" path="/Security/PGPSystem/EnableKeys/node()"/>
  1021. public override void EnableKeys(params PrimaryKey[] keys)
  1022. {
  1023. RepeatedRawEditCommand(keys, "enable");
  1024. }
  1025. /// <include file="documentation.xml" path="/Security/PGPSystem/MinimizeKeys/node()"/>
  1026. public override void MinimizeKeys(params PrimaryKey[] keys)
  1027. {
  1028. RepeatedRawEditCommand(keys, "minimize");
  1029. }
  1030. /// <include file="documentation.xml" path="/Security/PGPSystem/SetOwnerTrust/node()"/>
  1031. public override void SetOwnerTrust(TrustLevel trust, params PrimaryKey[] keys)
  1032. {
  1033. EditKeys(keys, delegate { return new SetTrustCommand(trust); });
  1034. }
  1035. #endregion
  1036. #region User ID management
  1037. /// <include file="documentation.xml" path="/Security/PGPSystem/AddPhoto4/node()"/>
  1038. public override void AddPhoto(PrimaryKey key, Stream image, OpenPGPImageType imageFormat,
  1039. UserPreferences preferences)
  1040. {
  1041. if(key == null || image == null) throw new ArgumentNullException();
  1042. if(imageFormat != OpenPGPImageType.Jpeg)
  1043. {
  1044. throw new NotImplementedException("Only JPEG photos are currently supported.");
  1045. }
  1046. // GPG requires an image filename, so save the image to a temporary file first
  1047. string filename = Path.GetTempFileName();
  1048. try
  1049. {
  1050. using(FileStream file = new FileStream(filename, FileMode.Open, FileAccess.Write))
  1051. {
  1052. image.CopyTo(file);
  1053. }
  1054. DoEdit(key, new AddPhotoCommand(filename, preferences));
  1055. }
  1056. finally { File.Delete(filename); }
  1057. }
  1058. /// <include file="documentation.xml" path="/Security/PGPSystem/AddUserId/node()"/>
  1059. public override void AddUserId(PrimaryKey key, string realName, string email, string comment,
  1060. UserPreferences preferences)
  1061. {
  1062. realName = Trim(realName);
  1063. email = Trim(email);
  1064. comment = Trim(comment);
  1065. if(string.IsNullOrEmpty(realName) && string.IsNullOrEmpty(email))
  1066. {
  1067. throw new ArgumentException("At least one of the real name or email must be set.");
  1068. }
  1069. // GPG normally imposes strict requirements for the user ID, but we want to be free! freeform, that is.
  1070. DoEdit(key, "--allow-freeform-uid", true, new AddUidCommand(realName, email, comment, preferences));
  1071. }
  1072. /// <include file="documentation.xml" path="/Security/PGPSystem/DeleteAttributes/node()"/>
  1073. public override void DeleteAttributes(params UserAttribute[] attributes)
  1074. {
  1075. EditAttributes(attributes, delegate { return new DeleteUidCommand(); });
  1076. }
  1077. /// <include file="documentation.xml" path="/Security/PGPSystem/GetPreferences/node()"/>
  1078. public override UserPreferences GetPreferences(UserAttribute user)
  1079. {
  1080. if(user == null) throw new ArgumentNullException();
  1081. if(user.PrimaryKey == null) throw new ArgumentException("The user attribute must be associated with a key.");
  1082. // TODO: currently, this fails to retrieve the user's preferred keyserver, because GPG writes it to the TTY where
  1083. // it can't be captured...
  1084. UserPreferences preferences = new UserPreferences(); // this will be filled out by the GetPrefs class
  1085. DoEdit(user.PrimaryKey, new RawCommand("uid " + user.Id), new GetPrefsCommand(preferences));
  1086. return preferences;
  1087. }
  1088. /// <include file="documentation.xml" path="/Security/PGPSystem/RevokeAttributes/node()"/>
  1089. public override void RevokeAttributes(UserRevocationReason reason, params UserAttribute[] attributes)
  1090. {
  1091. EditAttributes(attributes, delegate { return new RevokeUidCommand(reason); });
  1092. }
  1093. /// <include file="documentation.xml" path="/Security/PGPSystem/SetPreferences/node()"/>
  1094. public override void SetPreferences(UserAttribute user, UserPreferences preferences)
  1095. {
  1096. if(user == null || preferences == null) throw new ArgumentNullException();
  1097. if(user.PrimaryKey == null) throw new ArgumentException("The user attribute must be associated with a key.");
  1098. DoEdit(user.PrimaryKey, new RawCommand("uid " + user.Id), new SetPrefsCommand(preferences));
  1099. }
  1100. #endregion
  1101. /// <summary>Initializes a new <see cref="ExeGPG"/> object with path the path to the GPG executable. It is assumed
  1102. /// that the executable file will not be altered during the lifetime of this object.
  1103. /// </summary>
  1104. public void Initialize(string exePath)
  1105. {
  1106. // do some basic checks on the executable path
  1107. FileInfo info;
  1108. try { info = new FileInfo(exePath); }
  1109. catch(Exception ex) { throw new ArgumentException("The executable path is not valid.", ex); }
  1110. if(!info.Exists) throw new FileNotFoundException();
  1111. // it exists, so try to execute it and check the version information
  1112. Process process;
  1113. try { process = Execute(exePath, "--version"); }
  1114. catch(Exception ex) { throw new ArgumentException("The file could not be executed.", ex); }
  1115. process.StandardInput.Close(); // GPG should not expect any input
  1116. // reset exePath here so we don't end up in a state where the supported algorithms have changed but not the exePath
  1117. this.exePath = null;
  1118. ciphers = hashes = keyTypes = compressions = null;
  1119. while(true) // read the lists of supported algorithms
  1120. {
  1121. string line = process.StandardOutput.ReadLine();
  1122. if(line == null) break;
  1123. Match match = supportLineRe.Match(line);
  1124. if(match.Success)
  1125. {
  1126. string key = match.Groups[1].Value.ToLowerInvariant();
  1127. string[] list = commaSepRe.Split(match.Groups[2].Value.ToUpperInvariant());
  1128. if(string.Equals(key, "pubkey", StringComparison.Ordinal))
  1129. {
  1130. // trim the key types, changing RSA-S and RSA-E into RSA, and ELG-E into ELG
  1131. List<string> trimmedKeyTypes = new List<string>();
  1132. for(int i=0; i<list.Length; i++)
  1133. {
  1134. if(string.Equals(list[i], "ELG-E", StringComparison.OrdinalIgnoreCase))
  1135. {
  1136. if(!trimmedKeyTypes.Contains("ELG")) trimmedKeyTypes.Add("ELG");
  1137. }
  1138. else if(string.Equals(list[i], "RSA-E", StringComparison.OrdinalIgnoreCase))
  1139. {
  1140. if(!trimmedKeyTypes.Contains("RSA")) trimmedKeyTypes.Add("RSA");
  1141. }
  1142. else if(string.Equals(list[i], "RSA-S", StringComparison.OrdinalIgnoreCase))
  1143. {
  1144. if(!trimmedKeyTypes.Contains("RSA")) trimmedKeyTypes.Add("RSA");
  1145. }
  1146. else trimmedKeyTypes.Add(list[i]);
  1147. }
  1148. keyTypes = trimmedKeyTypes.ToArray();
  1149. }
  1150. else if(string.Equals(key, "cipher", StringComparison.Ordinal)) ciphers = list;
  1151. else if(string.Equals(key, "hash", StringComparison.Ordinal)) hashes = list;
  1152. else if(string.Equals(key, "compression", StringComparison.Ordinal)) compressions = list;
  1153. }
  1154. else
  1155. {
  1156. match = versionLineRe.Match(line);
  1157. if(match.Success)
  1158. {
  1159. string[] bits = match.Groups[1].Value.Split('.');
  1160. gpgVersion = int.Parse(bits[0]) * 10000; // 1.4.9 becomes 10409, and 2.0.21 becomes 20021
  1161. if(bits.Length > 1) gpgVersion += int.Parse(bits[1]) * 100;
  1162. if(bits.Length > 2) gpgVersion += int.Parse(bits[2]);
  1163. }
  1164. }
  1165. }
  1166. if(Exit(process) != 0) throw new PGPException("GPG returned an error while running --version.");
  1167. this.exePath = info.FullName; // everything seems okay, so set the full exePath
  1168. }
  1169. /// <summary>Processes a GPG command.</summary>
  1170. delegate void CommandProcessor(Command cmd, CommandState state);
  1171. /// <summary>Processes status messages from GPG.</summary>
  1172. delegate void StatusMessageHandler(StatusMessage message);
  1173. /// <summary>Creates an edit command on demand.</summary>
  1174. delegate EditCommand EditCommandCreator();
  1175. /// <summary>Creates an edit command on demand to operate on the given key signatures.</summary>
  1176. delegate EditCommand KeySignatureEditCommandCreator(KeySignature[] sigs);
  1177. enum StatusMessages
  1178. {
  1179. Ignore, MixIntoStdout, ReadInBackground
  1180. }
  1181. #region CommandState
  1182. /// <summary>Holds variables set by the default STDERR and status message handlers.</summary>
  1183. sealed class CommandState
  1184. {
  1185. public CommandState(Command command)
  1186. {
  1187. if(command == null) throw new ArgumentNullException();
  1188. this.Command = command;
  1189. }
  1190. /// <summary>Gets whether the operation may have been canceled by the user. (The user clicked a cancel button or
  1191. /// something, but that's not necessarily the cause for failure, and the command may still have succeeded.)
  1192. /// </summary>
  1193. public bool Canceled
  1194. {
  1195. get { return (FailureReasons & FailureReason.OperationCanceled) != 0; }
  1196. }
  1197. /// <summary>The command being executed.</summary>
  1198. public readonly Command Command;
  1199. /// <summary>If not null, this password will be sent when a key password is requested.</summary>
  1200. public SecureString DefaultPassword;
  1201. /// <summary>The status message that informed us of the most recent password request.</summary>
  1202. public StatusMessage PasswordMessage;
  1203. /// <summary>The hint for the next password to be requested.</summary>
  1204. public string PasswordHint;
  1205. /// <summary>Some potential causes of a failure.</summary>
  1206. public FailureReason FailureReasons;
  1207. }
  1208. #endregion
  1209. #region Command
  1210. /// <summary>Represents a GPG command.</summary>
  1211. sealed class Command : System.Runtime.ConstrainedExecution.CriticalFinalizerObject, IDisposable
  1212. {
  1213. public Command(ExeGPG gpg, ProcessStartInfo psi, InheritablePipe commandPipe,
  1214. StatusMessages statusHandling, bool closeStdInput, bool canKillOnAbort)
  1215. {
  1216. if(gpg == null || psi == null) throw new ArgumentNullException();
  1217. this.gpg = gpg;
  1218. this.psi = psi;
  1219. this.commandPipe = commandPipe;
  1220. this.statusHandling = statusHandling;
  1221. this.closeStdInput = closeStdInput;
  1222. KillProcessOnAbort = canKillOnAbort;
  1223. }
  1224. ~Command() { Dispose(false); }
  1225. /// <summary>Called for each line of text from STDERR when using <see cref="StreamHandling.ProcessText"/>.</summary>
  1226. public event TextLineHandler StandardErrorLine;
  1227. /// <summary>Called for each input prompt, with the prompt ID.</summary>
  1228. public event TextLineHandler InputNeeded;
  1229. /// <summary>Called for each status message sent on the status pipe.</summary>
  1230. public event StatusMessageHandler StatusMessageReceived;
  1231. /// <summary>Gets the exit code of the process, or throws an exception if the process has not yet exited.</summary>
  1232. public int ExitCode
  1233. {
  1234. get
  1235. {
  1236. if(process == null || !process.HasExited)
  1237. {
  1238. throw new InvalidOperationException("The process has not yet exited.");
  1239. }
  1240. return process.ExitCode;
  1241. }
  1242. }
  1243. /// <summary>Gets the <see cref="ExeGPG"/> object that created this command.</summary>
  1244. public ExeGPG GPG
  1245. {
  1246. get { return gpg; }
  1247. }
  1248. /// <summary>Returns true if the process has exited and the remaining data has been read from all streams.</summary>
  1249. public bool IsDone
  1250. {
  1251. get { return statusDone && errorDone && process.HasExited; }
  1252. }
  1253. /// <summary>Gets or sets whether the process should be killed if the command is aborted.</summary>
  1254. public bool KillProcessOnAbort { get; set; }
  1255. /// <summary>Gets the GPG process, or throws an exception if it has not been started yet.</summary>
  1256. public Process Process
  1257. {
  1258. get
  1259. {
  1260. if(process == null) throw new InvalidOperationException("The process has not started yet.");
  1261. return process;
  1262. }
  1263. }
  1264. /// <summary>Gets the status message from the most recent line, or null if the most recent line didn't contain a
  1265. /// status message.
  1266. /// </summary>
  1267. public StatusMessage StatusMessage
  1268. {
  1269. get { return statusMessage; }
  1270. }
  1271. /// <summary>Returns true if GPG exited successfully (with a return code of 0 [success] or 1 [warning]).</summary>
  1272. public bool SuccessfulExit
  1273. {
  1274. get { return ExitCode == 0 || ExitCode == 1; }
  1275. }
  1276. /// <summary>Throws an exception if <see cref="SuccessfulExit"/> is false.</summary>
  1277. public void CheckExitCode()
  1278. {
  1279. if(!SuccessfulExit)
  1280. {
  1281. throw new PGPException("GPG returned failure code "+ExitCode.ToStringInvariant());
  1282. }
  1283. }
  1284. /// <summary>Exits the process if it's running and frees system resources used by the <see cref="Command"/> object.</summary>
  1285. public void Dispose()
  1286. {
  1287. Dispose(true);
  1288. GC.SuppressFinalize(this);
  1289. }
  1290. /// <summary>Kills the process if it's running.</summary>
  1291. public void Kill()
  1292. {
  1293. if(process != null && !process.HasExited)
  1294. {
  1295. try { process.Kill(); }
  1296. catch(InvalidOperationException) { } // if it exited before the Kill(), don't worry about it
  1297. }
  1298. }
  1299. /// <summary>Reads a line from STDOUT. The method will return true if a line was processed, but the line will be
  1300. /// null if it was a status message. (The status message can be handled using <see cref="StatusMessageReceived"/>
  1301. /// or by retrieving it from the <see cref="StatusMessage"/> property.) False will be returned if the end of input
  1302. /// is reached.
  1303. /// </summary>
  1304. public bool ReadLine(out string line)
  1305. {
  1306. // if we don't have status messages mixed in, we can just let the StreamReader handle it
  1307. if(statusHandling != StatusMessages.MixIntoStdout)
  1308. {
  1309. line = process.StandardOutput.ReadLine();
  1310. return line != null;
  1311. }
  1312. bool tryAgain;
  1313. do
  1314. {
  1315. statusMessage = null; // assume this line isn't a status message
  1316. tryAgain = false; // and that it'll be something we want to return
  1317. // if we don't know where the next EOL is, we need to read some more data
  1318. while(nextOutEOL == -1)
  1319. {
  1320. if(outBytes == outBuffer.Length) // if we need to make more room in the buffer...
  1321. {
  1322. if(outStart != 0) // first try shifting the data over if we can
  1323. {
  1324. Array.Copy(outBuffer, outStart, outBuffer, 0, outBytes - outStart);
  1325. outBytes -= outStart;
  1326. outStart = 0;
  1327. }
  1328. else // otherwise, enlarge the buffer
  1329. {
  1330. byte[] newBuffer = new byte[outBytes*2];
  1331. Array.Copy(outBuffer, newBuffer, outBytes);
  1332. outBuffer = newBuffer;
  1333. }
  1334. }
  1335. // now try reading some more data
  1336. int bytesRead = process.StandardOutput.BaseStream.Read(outBuffer, outBytes, outBuffer.Length - outBytes);
  1337. if(bytesRead == 0) // we hit the EOF
  1338. {
  1339. if(outBytes - outStart != 0) // there's a bit of data left in the buffer, so use it as the last line
  1340. {
  1341. nextOutEOL = outBytes;
  1342. }
  1343. else if(partialLineLength != 0) // the buffer is empty, but there's a partial line left over. use that.
  1344. {
  1345. line = Encoding.UTF8.GetString(partialLine, 0, partialLineLength);
  1346. partialLineLength = 0;
  1347. return true;
  1348. }
  1349. else // there's no data left anywhere, so return null
  1350. {
  1351. line = null;
  1352. return false;
  1353. }
  1354. }
  1355. else // we got new data in the buffer, so see if there's an EOL character in it
  1356. {
  1357. nextOutEOL = Array.IndexOf(outBuffer, (byte)'\n', outBytes, bytesRead);
  1358. outBytes += bytesRead;
  1359. }
  1360. }
  1361. // at this point, we've found the EOL. we'll peek behind it and see if it's a CRLF combination.
  1362. // we'll actually use a loop because sometimes GPG sends CR CR LF...
  1363. int lineEnd = nextOutEOL;
  1364. while(lineEnd != 0 && outBuffer[lineEnd-1] == '\r') lineEnd--;
  1365. int lineLength = lineEnd - outStart;
  1366. // now we know what data is in the next line. we need to search it for a "[GNUPG:] " token
  1367. int statusMsgIndex = lineLength < 9 ? -1 : Array.IndexOf(outBuffer, (byte)'[', outStart, lineLength - 8);
  1368. if(statusMsgIndex != -1)
  1369. {
  1370. if(outBuffer[statusMsgIndex+1] != (byte)'G' || outBuffer[statusMsgIndex+2] != (byte)'N' ||
  1371. outBuffer[statusMsgIndex+3] != (byte)'U' || outBuffer[statusMsgIndex+4] != (byte)'P' ||
  1372. outBuffer[statusMsgIndex+5] != (byte)'G' || outBuffer[statusMsgIndex+6] != (byte)':' ||
  1373. outBuffer[statusMsgIndex+7] != (byte)']' || outBuffer[statusMsgIndex+8] != (byte)' ')
  1374. {
  1375. statusMsgIndex = -1;
  1376. }
  1377. }
  1378. // if there's no status message in this line, then return the line as-is
  1379. if(statusMsgIndex == -1)
  1380. {
  1381. if(partialLineLength == 0) // there's no partial line from a previous call
  1382. {
  1383. line = Encoding.UTF8.GetString(outBuffer, outStart, lineLength);
  1384. }
  1385. else // if there's a partial line from a previous call, prepend it to the new line
  1386. {
  1387. byte[] newLine = new byte[lineLength + partialLineLength];
  1388. if(partialLineLength != 0) Array.Copy(partialLine, newLine, partialLineLength);
  1389. Array.Copy(outBuffer, outStart, newLine, partialLineLength, lineLength);
  1390. line = Encoding.UTF8.GetString(newLine);
  1391. partialLineLength = 0;
  1392. }
  1393. }
  1394. else // there's a status message in the line somewhere
  1395. {
  1396. if(statusMsgIndex != outStart) // it's embedded in the line
  1397. {
  1398. // append the non-status portion to the partial line
  1399. if(partialLine == null || partialLine.Length < partialLineLength + lineLength)
  1400. {
  1401. byte[] newPartialLine = new byte[partialLineLength + lineLength + 64];
  1402. if(partialLineLength != 0) Array.Copy(partialLine, newPartialLine, partialLineLength);
  1403. partialLine = newPartialLine;
  1404. }
  1405. Array.Copy(outBuffer, outStart, partialLine, partialLineLength, statusMsgIndex - outStart);
  1406. partialLineLength += statusMsgIndex - outStart;
  1407. }
  1408. lineLength = Decode(outBuffer, statusMsgIndex, lineEnd - statusMsgIndex);
  1409. // GPG sends status lines in UTF-8 which has been further encoded so that certain characters become %XX.
  1410. // decode the line and split it into arguments
  1411. string type;
  1412. string[] arguments;
  1413. SplitDecodedLine(outBuffer, statusMsgIndex, lineLength, out type, out arguments);
  1414. if(type != null) // if the line decoded properly, parse and handle the message
  1415. {
  1416. statusMessage = ParseStatusMessage(type, arguments);
  1417. if(statusMessage != null) OnStatusMessage(statusMessage);
  1418. }
  1419. tryAgain = statusMessage == null; // if the status message was not used, go to the next line
  1420. line = null; // no line is returned if a status message was handled
  1421. }
  1422. // search for the next EOL so we're ready for the next call
  1423. outStart = nextOutEOL+1;
  1424. nextOutEOL = Array.IndexOf(outBuffer, (byte)'\n', outStart, outBytes - outStart);
  1425. } while(tryAgain);
  1426. return true;
  1427. }
  1428. /// <summary>Sends a blank line on the command stream.</summary>
  1429. public void SendLine()
  1430. {
  1431. SendLine(null);
  1432. }
  1433. /// <summary>Sends the given line on the command stream. The line should not include any end-of-line characters.</summary>
  1434. public void SendLine(string line)
  1435. {
  1436. if(commandStream == null) throw new InvalidOperationException("The command stream is not open.");
  1437. if(gpg.LoggingEnabled) gpg.LogLine(">> " + line);
  1438. if(!string.IsNullOrEmpty(line))
  1439. {
  1440. byte[] bytes = Encoding.UTF8.GetBytes(line);
  1441. commandStream.Write(bytes, 0, bytes.Length);
  1442. }
  1443. commandStream.WriteByte((byte)'\n');
  1444. commandStream.Flush();
  1445. }
  1446. /// <summary>Sends the given password on the command stream. If <paramref name="ownsPassword"/> is true, the
  1447. /// password will be disposed.
  1448. /// </summary>
  1449. public void SendPassword(SecureString password, bool ownsPassword)
  1450. {
  1451. if(password == null)
  1452. {
  1453. SendLine();
  1454. }
  1455. else
  1456. {
  1457. password.Process(delegate(byte[] bytes) { commandStream.Write(bytes, 0, bytes.Length); });
  1458. commandStream.WriteByte((byte)'\n'); // the password must be EOL-terminated for GPG to accept it
  1459. commandStream.Flush();
  1460. }
  1461. }
  1462. /// <summary>Starts executing the command.</summary>
  1463. public void Start()
  1464. {
  1465. if(process != null) throw new InvalidOperationException("The process has already been started.");
  1466. if(gpg.LoggingEnabled) gpg.LogLine(EscapeArg(psi.FileName) + " " + psi.Arguments);
  1467. process = Process.Start(psi);
  1468. if(closeStdInput) process.StandardInput.Close();
  1469. // if we have a command pipe, set up a stream for it
  1470. if(commandPipe != null)
  1471. {
  1472. commandStream = new FileStream(new SafeFileHandle(commandPipe.ServerHandle, false),
  1473. statusHandling == StatusMessages.ReadInBackground ? FileAccess.ReadWrite : FileAccess.Write);
  1474. }
  1475. outBuffer = new byte[4096];
  1476. if(statusHandling == StatusMessages.ReadInBackground) OnStatusRead(null);
  1477. else statusDone = true;
  1478. errorBuffer = new byte[4096];
  1479. OnStdErrorRead(null); // start reading STDERR on a background thread
  1480. }
  1481. /// <summary>Waits for the process to exit and all data to be read.</summary>
  1482. public void WaitForExit()
  1483. {
  1484. try
  1485. {
  1486. Process.WaitForExit(); // first wait for the process to finish
  1487. bool closedPipe = commandPipe == null;
  1488. while(!IsDone) // then wait for all of the streams to finish being read
  1489. {
  1490. if(!closedPipe) // if GPG didn't close its end of the pipe, we may have to do it
  1491. {
  1492. commandPipe.CloseClient();
  1493. closedPipe = true; // but don't close it on every iteration
  1494. }
  1495. System.Threading.Thread.Sleep(0); // give other threads (ie, the stream reading threads) a chance to finish
  1496. }
  1497. }
  1498. catch(ThreadAbortException)
  1499. {
  1500. if(KillProcessOnAbort) Kill();
  1501. }
  1502. }
  1503. delegate void LineProcessor(string line);
  1504. enum StreamHandling
  1505. {
  1506. ProcessText, ProcessStatus, DumpBinary
  1507. }
  1508. void Dispose(bool manualDispose)
  1509. {
  1510. if(!disposed)
  1511. {
  1512. Utility.Dispose(ref commandStream);
  1513. Utility.Dispose(ref commandPipe);
  1514. if(process != null) Exit(process);
  1515. // wipe the read buffers. it's unlikely that they contains sensitive data, but just in case...
  1516. SecurityUtility.ZeroBuffer(outBuffer);
  1517. SecurityUtility.ZeroBuffer(errorBuffer);
  1518. SecurityUtility.ZeroBuffer(partialLine);
  1519. statusMessage = null;
  1520. statusDone = errorDone = disposed = true; // mark reads complete so IsDone will return true
  1521. }
  1522. }
  1523. /// <summary>Handles an asynchronous read completion on a stream.</summary>
  1524. void HandleStream(StreamHandling handling, Stream stream, IAsyncResult result, ref byte[] buffer,
  1525. ref int bufferBytes, ref bool bufferDone, LineProcessor processor, AsyncCallback callback)
  1526. {
  1527. if(stream == null) // if the stream was destroyed already, then just mark that the stream is done
  1528. {
  1529. bufferDone = true;
  1530. }
  1531. else // otherwise, the stream is still going, so we can look at the data that was read
  1532. {
  1533. if(result != null) // if there was any data read, process it according to the stream handling
  1534. {
  1535. if(handling == StreamHandling.ProcessText)
  1536. {
  1537. foreach(string line in ProcessUnicodeStream(result, stream, ref buffer, ref bufferBytes, ref bufferDone))
  1538. {
  1539. processor(line);
  1540. }
  1541. }
  1542. else if(handling == StreamHandling.ProcessStatus)
  1543. {
  1544. ProcessStatusStream(result, stream, handling, ref buffer, ref bufferBytes, ref bufferDone);
  1545. }
  1546. else
  1547. {
  1548. DumpBinaryStream(result, stream, ref bufferDone);
  1549. }
  1550. }
  1551. // a possible race condition with WaitForExit() (or Dispose()) may have disposed the stream, so check CanRead
  1552. // to prevent an ObjectDisposedException if possible
  1553. // TODO: eliminate this race condition if possible
  1554. if(!stream.CanRead)
  1555. {
  1556. bufferDone = true;
  1557. }
  1558. else // if we can still read from the stream, start another asynchronous read
  1559. {
  1560. try { stream.BeginRead(buffer, bufferBytes, buffer.Length - bufferBytes, callback, null); }
  1561. catch(ObjectDisposedException) { bufferDone = true; } // if the stream was disposed, mark it as done
  1562. }
  1563. }
  1564. }
  1565. /// <summary>Handles a line of text from STDOUT.</summary>
  1566. void OnStdOutLine(string line)
  1567. {
  1568. if(gpg.LoggingEnabled) gpg.LogLine("OUT: " + line);
  1569. }
  1570. /// <summary>Handles a line of text from STDERR.</summary>
  1571. void OnStdErrorLine(string line)
  1572. {
  1573. if(gpg.LoggingEnabled) gpg.LogLine("ERR: " + line);
  1574. if(StandardErrorLine != null) StandardErrorLine(line);
  1575. }
  1576. /// <summary>Handles a status message.</summary>
  1577. void OnStatusMessage(StatusMessage message)
  1578. {
  1579. GetInputMessage inputMsg = message as GetInputMessage;
  1580. if(inputMsg != null && InputNeeded != null)
  1581. {
  1582. InputNeeded(inputMsg.PromptId);
  1583. return; // input messages are not given to the status message handler unless there's no prompt handler
  1584. }
  1585. if(StatusMessageReceived != null) StatusMessageReceived(message);
  1586. }
  1587. // warning 0420 is "reference to a volatile field will not be treated as volatile". we aren't worried about this
  1588. // because the field is only written to by the callee, not read.
  1589. #pragma warning disable 420
  1590. void OnStdErrorRead(IAsyncResult result)
  1591. {
  1592. HandleStream(StreamHandling.ProcessText, process.StandardError.BaseStream, result, ref errorBuffer, ref errorBytes,
  1593. ref errorDone, OnStdErrorLine, OnStdErrorRead);
  1594. }
  1595. void OnStatusRead(IAsyncResult result)
  1596. {
  1597. HandleStream(StreamHandling.ProcessStatus, commandStream, result, ref outBuffer, ref outBytes,
  1598. ref statusDone, null, OnStatusRead);
  1599. }
  1600. #pragma warning restore 420
  1601. /// <summary>Parses a status message with the given type and arguments, and returns the corresponding
  1602. /// <see cref="StatusMessage"/>, or null if the message could not be parsed or was ignored.
  1603. /// </summary>
  1604. StatusMessage ParseStatusMessage(string type, string[] arguments)
  1605. {
  1606. StatusMessage message;
  1607. switch(type)
  1608. {
  1609. case "NEWSIG": message = new GenericMessage(StatusMessageType.NewSig); break;
  1610. case "GOODSIG": message = new GoodSigMessage(arguments); break;
  1611. case "EXPSIG": message = new ExpiredSigMessage(arguments); break;
  1612. case "EXPKEYSIG": message = new ExpiredKeySigMessage(arguments); break;
  1613. case "REVKEYSIG": message = new RevokedKeySigMessage(arguments); break;
  1614. case "BADSIG": message = new BadSigMessage(arguments); break;
  1615. case "ERRSIG": message = new ErrorSigMessage(arguments); break;
  1616. case "VALIDSIG": message = new ValidSigMessage(arguments); break;
  1617. case "IMPORTED": message = new KeySigImportedMessage(arguments); break;
  1618. case "IMPORT_OK": message = new KeyImportOkayMessage(arguments); break;
  1619. case "IMPORT_PROBLEM": message = new KeyImportFailedMessage(arguments); break;
  1620. case "IMPORT_RES": message = new KeyImportResultsMessage(arguments); break;
  1621. case "USERID_HINT": message = new UserIdHintMessage(arguments); break;
  1622. case "NEED_PASSPHRASE": message = new NeedKeyPassphraseMessage(arguments); break;
  1623. case "GOOD_PASSPHRASE": message = new GenericMessage(StatusMessageType.GoodPassphrase); break;
  1624. case "MISSING_PASSPHRASE": message = new GenericMessage(StatusMessageType.MissingPassphrase); break;
  1625. case "BAD_PASSPHRASE": message = new BadPassphraseMessage(arguments); break;
  1626. case "NEED_PASSPHRASE_SYM": message = new GenericMessage(StatusMessageType.NeedCipherPassphrase); break;
  1627. case "BEGIN_SIGNING": message = new GenericMessage(StatusMessageType.BeginSigning); break;
  1628. case "SIG_CREATED": message = new GenericMessage(StatusMessageType.SigCreated); break;
  1629. case "BEGIN_DECRYPTION": message = new GenericMessage(StatusMessageType.BeginDecryption); break;
  1630. case "END_DECRYPTION": message = new GenericMessage(StatusMessageType.EndDecryption); break;
  1631. case "ENC_TO": message = new GenericKeyIdMessage(StatusMessageType.EncTo, arguments); break;
  1632. case "DECRYPTION_OKAY": message = new GenericMessage(StatusMessageType.DecryptionOkay); break;
  1633. case "DECRYPTION_FAILED": message = new GenericMessage(StatusMessageType.DecryptionFailed); break;
  1634. case "BEGIN_ENCRYPTION": message = new GenericMessage(StatusMessageType.BeginEncryption); break;
  1635. case "END_ENCRYPTION": message = new GenericMessage(StatusMessageType.EndEncryption); break;
  1636. case "INV_RECP": message = new InvalidRecipientMessage(arguments); break;
  1637. case "NODATA": message = new GenericMessage(StatusMessageType.NoData); break;
  1638. case "NO_PUBKEY": message = new GenericKeyIdMessage(StatusMessageType.NoPublicKey, arguments); break;
  1639. case "NO_SECKEY": message = new GenericKeyIdMessage(StatusMessageType.NoSecretKey, arguments); break;
  1640. case "UNEXPECTED": message = new GenericMessage(StatusMessageType.UnexpectedData); break;
  1641. case "TRUST_UNDEFINED": message = new TrustLevelMessage(StatusMessageType.TrustUndefined); break;
  1642. case "TRUST_NEVER": message = new TrustLevelMessage(StatusMessageType.TrustNever); break;
  1643. case "TRUST_MARGINAL": message = new TrustLevelMessage(StatusMessageType.TrustMarginal); break;
  1644. case "TRUST_FULLY": message = new TrustLevelMessage(StatusMessageType.TrustFully); break;
  1645. case "TRUST_ULTIMATE": message = new TrustLevelMessage(StatusMessageType.TrustUltimate); break;
  1646. case "ATTRIBUTE": message = new AttributeMessage(arguments); break;
  1647. case "GET_HIDDEN": message = new GetInputMessage(StatusMessageType.GetHidden, arguments); break;
  1648. case "GET_BOOL": message = new GetInputMessage(StatusMessageType.GetBool, arguments); break;
  1649. case "GET_LINE": message = new GetInputMessage(StatusMessageType.GetLine, arguments); break;
  1650. case "DELETE_PROBLEM": message = new DeleteFailedMessage(arguments); break;
  1651. case "KEY_CREATED": message = new KeyCreatedMessage(arguments); break;
  1652. case "KEY_NOT_CREATED": message = new GenericMessage(StatusMessageType.KeyNotCreated); break;
  1653. // ignore these messages
  1654. case "PROGRESS": case "PLAINTEXT": case "PLAINTEXT_LENGTH": case "SIG_ID": case "GOT_IT": case "GOODMDC":
  1655. case "KEYEXPIRED": case "SIGEXPIRED":
  1656. message = null;
  1657. break;
  1658. default:
  1659. if(gpg.LoggingEnabled) gpg.LogLine("Unprocessed status message: "+type);
  1660. message = null;
  1661. break;
  1662. }
  1663. return message;
  1664. }
  1665. void ProcessStatusStream(IAsyncResult result, Stream stream, StreamHandling handling,
  1666. ref byte[] buffer, ref int bufferBytes, ref bool bufferDone)
  1667. {
  1668. foreach(byte[] binaryLine in ProcessAsciiStream(result, stream, ref buffer, ref bufferBytes, ref bufferDone))
  1669. {
  1670. // GPG sends lines in UTF-8, which has been further encoded, so that certain characters become %XX. decode the
  1671. // line and split it into arguments
  1672. string type;
  1673. string[] arguments;
  1674. SplitDecodedLine(binaryLine, 0, Decode(binaryLine, 0, binaryLine.Length), out type, out arguments);
  1675. if(type != null) // if the line decoded properly and has a message type, parse and handle the message
  1676. {
  1677. StatusMessage message = ParseStatusMessage(type, arguments);
  1678. if(message != null) OnStatusMessage(message);
  1679. }
  1680. }
  1681. }
  1682. /// <summary>Splits a decoded ASCII line representing a status message into a message type and message arguments.</summary>
  1683. void SplitDecodedLine(byte[] line, int index, int count, out string type, out string[] arguments)
  1684. {
  1685. if(gpg.LoggingEnabled) gpg.LogLine(Encoding.ASCII.GetString(line, index, count));
  1686. List<string> chunks = new List<string>();
  1687. type = null;
  1688. arguments = null;
  1689. // the chunks are whitespace-separated
  1690. for(int end=index+count; ;)
  1691. {
  1692. while(index < end && line[index] == (byte)' ') index++; // find the next non-whitespace character
  1693. int start = index;
  1694. while(index < end && line[index] != (byte)' ') index++; // find the next whitespace character after that
  1695. if(start == end) break; // if we're at the end of the line, we're done
  1696. chunks.Add(Encoding.UTF8.GetString(line, start, index-start)); // grab the text between the two
  1697. }
  1698. if(chunks.Count >= 2) // if there are enough chunks to make up a status line
  1699. {
  1700. type = chunks[1]; // skip the first chunk, which is assumed to be "[GNUPG:]". the second becomes the type
  1701. arguments = new string[chunks.Count-2]; // grab the rest as the arguments
  1702. chunks.CopyTo(2, arguments, 0, arguments.Length);
  1703. }
  1704. }
  1705. Process process;
  1706. InheritablePipe commandPipe;
  1707. FileStream commandStream;
  1708. byte[] errorBuffer, outBuffer, partialLine;
  1709. ProcessStartInfo psi;
  1710. ExeGPG gpg;
  1711. StatusMessage statusMessage;
  1712. int errorBytes, outStart, outBytes, partialLineLength, nextOutEOL = -1;
  1713. StatusMessages statusHandling;
  1714. volatile bool errorDone, statusDone;
  1715. bool closeStdInput, disposed;
  1716. /// <summary>Decodes %XX-encoded values in ASCII text (represented as a byte array).</summary>
  1717. /// <returns>Returns the new length of the text (the text is decoded in place, and can get shorter).</returns>
  1718. static int Decode(byte[] encoded, int index, int count)
  1719. {
  1720. index = Array.IndexOf(encoded, (byte)'%', index, count);
  1721. if(index != -1)
  1722. {
  1723. for(int offset=0; index < count; index++)
  1724. {
  1725. byte c = encoded[index + offset];
  1726. if(c == (byte)'%' && index < count-2)
  1727. {
  1728. char high = (char)encoded[index + offset+1], low = (char)encoded[index + offset+2];
  1729. if(IsHexDigit(high) && IsHexDigit(low))
  1730. {
  1731. encoded[index] = (byte)GetHexValue(high, low); // convert the hex value to the new byte value
  1732. offset += 2;
  1733. count -= 2;
  1734. }
  1735. }
  1736. else encoded[index] = encoded[index + offset];
  1737. }
  1738. }
  1739. return count;
  1740. }
  1741. /// <summary>Handles an asynchronous read completion by throwing away the data that was read.</summary>
  1742. static void DumpBinaryStream(IAsyncResult result, Stream stream, ref bool bufferDone)
  1743. {
  1744. int bytesRead = 0;
  1745. if(stream != null)
  1746. {
  1747. try { bytesRead = stream.EndRead(result); }
  1748. catch(ObjectDisposedException) { }
  1749. }
  1750. // if the stream was null, EndRead() returned zero, or ObjectDisposedException was thrown, the stream is done
  1751. if(bytesRead == 0) bufferDone = true;
  1752. }
  1753. /// <summary>Processes data read in an ASCII stream and returns completed lines as arrays of bytes.</summary>
  1754. static IEnumerable<byte[]> ProcessAsciiStream(IAsyncResult result, Stream stream,
  1755. ref byte[] buffer, ref int bufferBytes, ref bool bufferDone)
  1756. {
  1757. List<byte[]> lines = new List<byte[]>();
  1758. if(result != null)
  1759. {
  1760. int bytesRead = 0;
  1761. if(stream != null)
  1762. {
  1763. try { bytesRead = stream.EndRead(result); }
  1764. catch(ObjectDisposedException) { }
  1765. }
  1766. if(bytesRead == 0) // if the stream was null, or EndRead() returned zero, or ObjectDisposedException was
  1767. { // thrown, the stream is done
  1768. bufferDone = true;
  1769. if(bufferBytes != 0) // if data is still in the buffer, return it as the final line
  1770. {
  1771. byte[] line = new byte[bufferBytes];
  1772. Array.Copy(buffer, line, bufferBytes);
  1773. lines.Add(line);
  1774. bufferBytes = 0;
  1775. }
  1776. }
  1777. else // otherwise, data was read, so scan the new data for line endings
  1778. {
  1779. int index, searchStart = bufferBytes, newBufferStart = 0;
  1780. bufferBytes += bytesRead;
  1781. do
  1782. {
  1783. index = Array.IndexOf(buffer, (byte)'\n', searchStart, bufferBytes-searchStart);
  1784. if(index == -1) break;
  1785. // we found a line ending in the new data. we won't return the line ending, so we'll skip either 1 or 2
  1786. // bytes depending on whether the ending is LF or CRLF
  1787. int eolLength = 1;
  1788. if(index != 0 && buffer[index-1] == (byte)'\r')
  1789. {
  1790. index--;
  1791. eolLength++;
  1792. }
  1793. // grab the portion of the buffer corresponding to the line
  1794. byte[] line = new byte[index-newBufferStart];
  1795. Array.Copy(buffer, newBufferStart, line, 0, line.Length);
  1796. lines.Add(line);
  1797. // mark the returned portion of the buffer as unused
  1798. newBufferStart = searchStart = index+eolLength;
  1799. } while(bufferBytes != searchStart);
  1800. if(newBufferStart != 0) // if any portion of the buffer became unused, shift the remaining data to the front
  1801. {
  1802. bufferBytes -= newBufferStart;
  1803. if(bufferBytes != 0) Array.Copy(buffer, newBufferStart, buffer, 0, bufferBytes);
  1804. }
  1805. }
  1806. }
  1807. if(bufferBytes == buffer.Length) // if the buffer is full, enlarge it so we can read more data
  1808. {
  1809. byte[] newBuffer = new byte[buffer.Length*2];
  1810. Array.Copy(buffer, newBuffer, bufferBytes);
  1811. buffer = newBuffer;
  1812. }
  1813. return lines;
  1814. }
  1815. /// <summary>Processes data read in a UTF-8 stream.</summary>
  1816. static IEnumerable<string> ProcessUnicodeStream(IAsyncResult result, Stream stream,
  1817. ref byte[] buffer, ref int bufferBytes, ref bool bufferDone)
  1818. {
  1819. List<string> lines = new List<string>();
  1820. foreach(byte[] binaryLine in ProcessAsciiStream(result, stream, ref buffer, ref bufferBytes, ref bufferDone))
  1821. {
  1822. lines.Add(Encoding.UTF8.GetString(binaryLine));
  1823. }
  1824. return lines;
  1825. }
  1826. }
  1827. #endregion
  1828. #region DummyAttribute
  1829. /// <summary>A placeholder for a user attribute that has not been fully retrieved in the key listing.</summary>
  1830. sealed class DummyAttribute : UserAttribute
  1831. {
  1832. public DummyAttribute(AttributeMessage msg)
  1833. {
  1834. Message = msg;
  1835. }
  1836. public readonly AttributeMessage Message;
  1837. }
  1838. #endregion
  1839. #region Edit commands and objects
  1840. #region EditCommandResult
  1841. /// <summary>Determines the result of processing during edit mode.</summary>
  1842. enum EditCommandResult
  1843. {
  1844. /// <summary>The event was processed, and this command is finished.</summary>
  1845. Done,
  1846. /// <summary>The event was processed, but this command has more work to do.</summary>
  1847. Continue,
  1848. /// <summary>The event was not processed, and this command is finished.</summary>
  1849. Next
  1850. }
  1851. #endregion
  1852. #region EditUserId
  1853. /// <summary>Represents a user ID or attribute parsed from an edit key listing.</summary>
  1854. sealed class EditUserId
  1855. {
  1856. /// <summary>Determines whether this object is identical to the given <see cref="EditUserId"/>. This doesn't
  1857. /// guarantee that they reference the same user ID, but it's the best we've got.
  1858. /// </summary>
  1859. public bool Matches(EditUserId id)
  1860. {
  1861. return IsAttribute == id.IsAttribute && string.Equals(Name, id.Name, StringComparison.Ordinal) &&
  1862. string.Equals(Prefs, id.Prefs, StringComparison.Ordinal);
  1863. }
  1864. public override string ToString()
  1865. {
  1866. return (IsAttribute ? "Attribute " : null) + Name + " - " + Prefs;
  1867. }
  1868. public string Name, Prefs;
  1869. public bool IsAttribute, Primary, Selected;
  1870. }
  1871. #endregion
  1872. #region EditKey
  1873. /// <summary>Represents the current state of a key being edited.</summary>
  1874. sealed class EditKey
  1875. {
  1876. /// <summary>Returns the first <see cref="EditUserId"/> that is an attribute and is primary, or null if there is
  1877. /// none.
  1878. /// </summary>
  1879. public EditUserId PrimaryAttribute
  1880. {
  1881. get
  1882. {
  1883. foreach(EditUserId userId in UserIds)
  1884. {
  1885. if(userId.IsAttribute && userId.Primary) return userId;
  1886. }
  1887. return null;
  1888. }
  1889. }
  1890. /// <summary>Returns the first <see cref="EditUserId"/> that is a user ID and is primary, or null if there is none.</summary>
  1891. public EditUserId PrimaryUserId
  1892. {
  1893. get
  1894. {
  1895. foreach(EditUserId userId in UserIds)
  1896. {
  1897. if(!userId.IsAttribute && userId.Primary) return userId;
  1898. }
  1899. return null;
  1900. }
  1901. }
  1902. /// <summary>Returns the first <see cref="EditUserId"/> that is selected, or null if there is none.</summary>
  1903. public EditUserId SelectedUserId
  1904. {
  1905. get
  1906. {
  1907. foreach(EditUserId userId in UserIds)
  1908. {
  1909. if(userId.Selected) return userId;
  1910. }
  1911. return null;
  1912. }
  1913. }
  1914. /// <summary>A list of <see cref="EditUserId"/>, in the order in which they were listed.</summary>
  1915. public readonly List<EditUserId> UserIds = new List<EditUserId>();
  1916. /// <summary>A list of the fingerprints of subkeys, in the order in which they were listed.</summary>
  1917. public readonly List<string> Subkeys = new List<string>();
  1918. }
  1919. #endregion
  1920. #region EditCommand
  1921. /// <summary>Represents a command that operates in edit mode.</summary>
  1922. abstract class EditCommand
  1923. {
  1924. /// <summary>Responds to a request for input.</summary>
  1925. public virtual EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  1926. CommandState state, string promptId)
  1927. {
  1928. if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) && state.PasswordMessage != null &&
  1929. state.PasswordMessage.Type == StatusMessageType.NeedKeyPassphrase)
  1930. {
  1931. if(state.DefaultPassword != null)
  1932. {
  1933. state.Command.SendPassword(state.DefaultPassword, false);
  1934. }
  1935. else if(!state.Command.GPG.SendKeyPassword(state.Command, state.PasswordHint,
  1936. (NeedKeyPassphraseMessage)state.PasswordMessage))
  1937. {
  1938. throw new OperationCanceledException(); // abort if the password was not provided
  1939. }
  1940. return EditCommandResult.Continue;
  1941. }
  1942. else throw new NotImplementedException("Unhandled input request: " + promptId);
  1943. }
  1944. /// <summary>Processes a line of text received from GPG.</summary>
  1945. public virtual EditCommandResult Process(string line)
  1946. {
  1947. return EditCommandResult.Continue;
  1948. }
  1949. /// <summary>Gets or sets whether this command expects a relist before the next prompt. If true, and GPG doesn't
  1950. /// issue a relist, one will be manually requested.
  1951. /// </summary>
  1952. public bool ExpectRelist;
  1953. /// <summary>Returns an exception that represents an unexpected condition.</summary>
  1954. protected static KeyEditFailedException UnexpectedError(string problem)
  1955. {
  1956. return new KeyEditFailedException("Key edit failed: " + problem + ".");
  1957. }
  1958. }
  1959. #endregion
  1960. #region AddUidBase
  1961. /// <summary>A base class for edit commands that add user IDs.</summary>
  1962. abstract class AddUidBase : EditCommand
  1963. {
  1964. /// <param name="preferences">The <see cref="UserPreferences"/> to use, or null to use the defaults.</param>
  1965. /// <param name="addAttribute">True if an attribute is being added, and false if a user ID is being added.</param>
  1966. public AddUidBase(UserPreferences preferences, bool addAttribute)
  1967. {
  1968. this.preferences = preferences;
  1969. this.addAttribute = addAttribute;
  1970. }
  1971. /// <summary>Enqueues additional commands to set the preferences of the new user ID, which is assumed to be the
  1972. /// last ID in the key.
  1973. /// </summary>
  1974. protected void AddPreferenceCommands(Queue<EditCommand> commands, EditKey originalKey)
  1975. {
  1976. if(preferences != null)
  1977. {
  1978. if(!preferences.Primary)
  1979. {
  1980. EditUserId id = addAttribute ? originalKey.PrimaryAttribute : originalKey.PrimaryUserId;
  1981. if(id != null)
  1982. {
  1983. commands.Enqueue(new SelectUidCommand(id));
  1984. commands.Enqueue(new SetPrimaryCommand());
  1985. }
  1986. }
  1987. commands.Enqueue(new SelectLastUidCommand());
  1988. commands.Enqueue(new SetPrefsCommand(preferences));
  1989. }
  1990. }
  1991. readonly UserPreferences preferences;
  1992. readonly bool addAttribute;
  1993. }
  1994. #endregion
  1995. #region AddPhotoCommand
  1996. /// <summary>An edit command that adds a photo id to a key.</summary>
  1997. sealed class AddPhotoCommand : AddUidBase
  1998. {
  1999. public AddPhotoCommand(string filename, UserPreferences preferences) : base(preferences, true)
  2000. {
  2001. if(string.IsNullOrEmpty(filename)) throw new ArgumentException();
  2002. this.filename = filename;
  2003. }
  2004. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2005. CommandState state, string promptId)
  2006. {
  2007. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2008. {
  2009. if(!sentCommand)
  2010. {
  2011. state.Command.SendLine("addphoto " + filename);
  2012. sentCommand = true;
  2013. }
  2014. else
  2015. {
  2016. AddPreferenceCommands(commands, originalKey);
  2017. return EditCommandResult.Next;
  2018. }
  2019. }
  2020. else if(string.Equals(promptId, "photoid.jpeg.size", StringComparison.Ordinal))
  2021. {
  2022. state.Command.SendLine("Y"); // yes, it's okay if the photo is large
  2023. }
  2024. else if(string.Equals(promptId, "photoid.jpeg.add", StringComparison.Ordinal))
  2025. {
  2026. // if GPG asks us for the filename, that means it rejected the file we gave originally
  2027. throw UnexpectedError("The image was rejected. Perhaps it's not a valid JPEG?");
  2028. }
  2029. else return base.Process(commands, originalKey, key, state, promptId);
  2030. return EditCommandResult.Continue;
  2031. }
  2032. readonly string filename;
  2033. bool sentCommand;
  2034. }
  2035. #endregion
  2036. #region AddRevokerCommand
  2037. /// <summary>An edit command that adds a designated revoker to a key.</summary>
  2038. sealed class AddRevokerCommand : EditCommand
  2039. {
  2040. /// <param name="fingerprint">The fingerprint of the designated revoker.</param>
  2041. public AddRevokerCommand(string fingerprint)
  2042. {
  2043. this.fingerprint = fingerprint;
  2044. }
  2045. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2046. CommandState state, string promptId)
  2047. {
  2048. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2049. {
  2050. if(!sentCommand)
  2051. {
  2052. state.Command.SendLine("addrevoker");
  2053. sentCommand = true;
  2054. }
  2055. else return EditCommandResult.Next;
  2056. }
  2057. else if(string.Equals(promptId, "keyedit.add_revoker", StringComparison.Ordinal))
  2058. {
  2059. if(!sentFingerprint)
  2060. {
  2061. state.Command.SendLine(fingerprint);
  2062. sentFingerprint = true;
  2063. }
  2064. else // if it asks us again, that means it rejected the first fingerprint
  2065. {
  2066. throw UnexpectedError("Adding the designated revoker failed.");
  2067. }
  2068. }
  2069. else if(string.Equals(promptId, "keyedit.add_revoker.okay", StringComparison.Ordinal))
  2070. {
  2071. state.Command.SendLine("Y");
  2072. }
  2073. else return base.Process(commands, originalKey, key, state, promptId);
  2074. return EditCommandResult.Continue;
  2075. }
  2076. readonly string fingerprint;
  2077. bool sentCommand, sentFingerprint;
  2078. }
  2079. #endregion
  2080. #region AddSubkeyCommand
  2081. /// <summary>An edit command that adds a subkey to a primary key.</summary>
  2082. sealed class AddSubkeyCommand : EditCommand
  2083. {
  2084. /// <param name="gpg">A reference to the ExeGPG class.</param>
  2085. /// <param name="type">The subkey type.</param>
  2086. /// <param name="capabilities">The desired capabilities of the subkey.</param>
  2087. /// <param name="length">The subkey length.</param>
  2088. /// <param name="expiration">The subkey expiration, or null if it does not expire.</param>
  2089. public AddSubkeyCommand(ExeGPG gpg, string type, KeyCapabilities capabilities, int length, DateTime? expiration)
  2090. {
  2091. this.gpgVersion = gpg.gpgVersion;
  2092. this.type = type;
  2093. this.expiration = expiration;
  2094. this.expirationDays = GetExpirationDays(expiration);
  2095. if((capabilities & KeyCapabilities.Certify) != 0)
  2096. {
  2097. throw new ArgumentException("The Certify capability is only allowed on the primary key.");
  2098. }
  2099. this.isDSA = string.Equals(type, KeyType.DSA, StringComparison.OrdinalIgnoreCase);
  2100. this.isELG = type == null || string.Equals(type, KeyType.ElGamal, StringComparison.OrdinalIgnoreCase);
  2101. this.isRSA = string.Equals(type, KeyType.RSA, StringComparison.OrdinalIgnoreCase);
  2102. if(!isDSA && !isELG && !isRSA)
  2103. {
  2104. throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm, "Unsupported subkey type: " + type);
  2105. }
  2106. if(capabilities == KeyCapabilities.Default)
  2107. {
  2108. capabilities = isDSA ? KeyCapabilities.Sign
  2109. : isELG ? KeyCapabilities.Encrypt : KeyCapabilities.Sign | KeyCapabilities.Encrypt;
  2110. }
  2111. else if(isDSA && (capabilities & KeyCapabilities.Encrypt) != 0)
  2112. {
  2113. throw new KeyCreationFailedException(FailureReason.None, "DSA keys cannot be used for encryption.");
  2114. }
  2115. else if(isELG && capabilities != KeyCapabilities.Encrypt)
  2116. {
  2117. throw new KeyCreationFailedException(FailureReason.None, "GPG only supports encryption-only ElGamal keys.");
  2118. }
  2119. if(length == 0)
  2120. {
  2121. length = isDSA ? 1024 : 2048;
  2122. }
  2123. else
  2124. {
  2125. int maxLength = isDSA ? 3072 : 4096;
  2126. if((uint)length > (uint)maxLength)
  2127. {
  2128. throw new KeyCreationFailedException(FailureReason.None, "Key length " +
  2129. length.ToStringInvariant() + " is not supported.");
  2130. }
  2131. }
  2132. this.length = length;
  2133. this.capabilities = capabilities;
  2134. this.flagsToToggle = isELG ? 0 : (isDSA ? DefaultDSACaps : DefaultRSACaps) ^ capabilities;
  2135. }
  2136. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2137. CommandState state, string promptId)
  2138. {
  2139. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2140. {
  2141. if(!sentCommand)
  2142. {
  2143. state.Command.SendLine("addkey");
  2144. sentCommand = true;
  2145. }
  2146. else return EditCommandResult.Next;
  2147. }
  2148. else if(string.Equals(promptId, "keygen.algo", StringComparison.Ordinal))
  2149. {
  2150. if(!sentAlgo)
  2151. {
  2152. string selection;
  2153. if(gpgVersion < 20000) // if this is GPG 1.x...
  2154. {
  2155. if(isDSA) selection = capabilities == KeyCapabilities.Sign ? "2" : "3";
  2156. else if(isELG) selection = "4";
  2157. else selection = capabilities == KeyCapabilities.Sign ? "5" : capabilities == KeyCapabilities.Encrypt ? "6" : "7";
  2158. }
  2159. else // GPG 2+ uses a different set of options
  2160. {
  2161. if(isDSA) selection = capabilities == KeyCapabilities.Sign ? "3" : "7";
  2162. else if(isELG) selection = "5";
  2163. else selection = capabilities == KeyCapabilities.Sign ? "4" : capabilities == KeyCapabilities.Encrypt ? "6" : "8";
  2164. }
  2165. state.Command.SendLine(selection);
  2166. sentAlgo = true;
  2167. }
  2168. else // if GPG asks a second time, then it rejected the algorithm choice
  2169. {
  2170. throw new KeyCreationFailedException(FailureReason.UnsupportedAlgorithm, "Unsupported subkey type: " + type);
  2171. }
  2172. }
  2173. else if(string.Equals(promptId, "keygen.size", StringComparison.Ordinal))
  2174. {
  2175. if(!sentLength)
  2176. {
  2177. state.Command.SendLine(length.ToStringInvariant());
  2178. sentLength = true;
  2179. }
  2180. else // if GPG asks a second time, then it rejected the key length
  2181. {
  2182. throw new KeyCreationFailedException(FailureReason.None, "Key length " +
  2183. length.ToStringInvariant() + " is not supported.");
  2184. }
  2185. }
  2186. else if(string.Equals(promptId, "keygen.valid", StringComparison.Ordinal))
  2187. {
  2188. if(!sentExpiration)
  2189. {
  2190. state.Command.SendLine(expirationDays.ToStringInvariant());
  2191. sentExpiration = true;
  2192. }
  2193. else // if GPG asks a second time, then it rejected the expiration date
  2194. {
  2195. throw new KeyCreationFailedException(FailureReason.None, "Expiration date " + Convert.ToString(expiration) +
  2196. " is not supported.");
  2197. }
  2198. }
  2199. else if(string.Equals(promptId, "keygen.flags", StringComparison.Ordinal))
  2200. {
  2201. if((flagsToToggle & KeyCapabilities.Authenticate) != 0)
  2202. {
  2203. state.Command.SendLine("A");
  2204. flagsToToggle &= ~KeyCapabilities.Authenticate;
  2205. }
  2206. else if((flagsToToggle & KeyCapabilities.Encrypt) != 0)
  2207. {
  2208. state.Command.SendLine("E");
  2209. flagsToToggle &= ~KeyCapabilities.Encrypt;
  2210. }
  2211. else if((flagsToToggle & KeyCapabilities.Sign) != 0)
  2212. {
  2213. state.Command.SendLine("S");
  2214. flagsToToggle &= ~KeyCapabilities.Sign;
  2215. }
  2216. else
  2217. {
  2218. state.Command.SendLine("Q");
  2219. }
  2220. }
  2221. else return base.Process(commands, originalKey, key, state, promptId);
  2222. return EditCommandResult.Continue;
  2223. }
  2224. const KeyCapabilities DefaultRSACaps = KeyCapabilities.Sign | KeyCapabilities.Encrypt;
  2225. const KeyCapabilities DefaultDSACaps = KeyCapabilities.Sign;
  2226. readonly string type;
  2227. readonly DateTime? expiration;
  2228. readonly int length, expirationDays, gpgVersion;
  2229. readonly bool isDSA, isELG, isRSA;
  2230. KeyCapabilities capabilities, flagsToToggle;
  2231. bool sentCommand, sentAlgo, sentLength, sentExpiration;
  2232. }
  2233. #endregion
  2234. #region AddUidCommand
  2235. /// <summary>An edit command that adds a new user ID to a key.</summary>
  2236. sealed class AddUidCommand : AddUidBase
  2237. {
  2238. public AddUidCommand(string realName, string email, string comment, UserPreferences preferences) : base(preferences, false)
  2239. {
  2240. if(ContainsControlCharacters(realName + email + comment))
  2241. {
  2242. throw new ArgumentException("The name, email, and/or comment contains control characters. Remove them.");
  2243. }
  2244. this.realName = realName;
  2245. this.email = email;
  2246. this.comment = comment;
  2247. }
  2248. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2249. CommandState state, string promptId)
  2250. {
  2251. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2252. {
  2253. if(!startedUid)
  2254. {
  2255. state.Command.SendLine("adduid");
  2256. startedUid = true;
  2257. }
  2258. else // if we didn't get to the "comment" prompt, then it probably failed
  2259. {
  2260. throw UnexpectedError("Adding a new user ID seemed to fail.");
  2261. }
  2262. }
  2263. else if(string.Equals(promptId, "keygen.name", StringComparison.Ordinal)) state.Command.SendLine(realName);
  2264. else if(string.Equals(promptId, "keygen.email", StringComparison.Ordinal)) state.Command.SendLine(email);
  2265. else if(string.Equals(promptId, "keygen.comment", StringComparison.Ordinal))
  2266. {
  2267. state.Command.SendLine(comment);
  2268. AddPreferenceCommands(commands, originalKey);
  2269. return EditCommandResult.Done;
  2270. }
  2271. else return base.Process(commands, originalKey, key, state, promptId);
  2272. return EditCommandResult.Continue;
  2273. }
  2274. readonly string realName, email, comment;
  2275. bool startedUid;
  2276. }
  2277. #endregion
  2278. #region ChangeExpirationCommand
  2279. /// <summary>An edit command that changes the expiration date of the primary key or selected subkey.</summary>
  2280. sealed class ChangeExpirationCommand : EditCommand
  2281. {
  2282. public ChangeExpirationCommand(DateTime? expiration)
  2283. {
  2284. this.expiration = expiration;
  2285. this.expirationDays = GetExpirationDays(expiration);
  2286. }
  2287. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2288. CommandState state, string promptId)
  2289. {
  2290. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2291. {
  2292. if(!sentCommand)
  2293. {
  2294. state.Command.SendLine("expire");
  2295. sentCommand = true;
  2296. }
  2297. else return EditCommandResult.Next;
  2298. }
  2299. else if(string.Equals(promptId, "keygen.valid", StringComparison.Ordinal))
  2300. {
  2301. if(!sentExpiration)
  2302. {
  2303. state.Command.SendLine(expirationDays.ToStringInvariant());
  2304. sentExpiration = true;
  2305. }
  2306. else // if GPG asked us twice, that means it rejected the expiration date
  2307. {
  2308. throw UnexpectedError("Changing expiration date to " + Convert.ToString(expiration) + " failed.");
  2309. }
  2310. }
  2311. else return base.Process(commands, originalKey, key, state, promptId);
  2312. return EditCommandResult.Continue;
  2313. }
  2314. readonly DateTime? expiration;
  2315. readonly int expirationDays;
  2316. bool sentCommand, sentExpiration;
  2317. }
  2318. #endregion
  2319. #region ChangePasswordCommand
  2320. /// <summary>An edit command that changes the password on a secret key.</summary>
  2321. sealed class ChangePasswordCommand : EditCommand
  2322. {
  2323. public ChangePasswordCommand(SecureString password)
  2324. {
  2325. this.password = password;
  2326. }
  2327. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2328. CommandState state, string promptId)
  2329. {
  2330. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2331. {
  2332. if(!sentCommand)
  2333. {
  2334. state.Command.SendLine("passwd");
  2335. sentCommand = true;
  2336. }
  2337. else return EditCommandResult.Next;
  2338. }
  2339. else if(string.Equals(promptId, "change_passwd.empty.okay", StringComparison.Ordinal))
  2340. {
  2341. state.Command.SendLine("Y"); // yes, an empty password is okay
  2342. }
  2343. else if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) && state.PasswordMessage != null &&
  2344. state.PasswordMessage.Type == StatusMessageType.NeedCipherPassphrase)
  2345. {
  2346. state.Command.SendPassword(password, false);
  2347. }
  2348. else return base.Process(commands, originalKey, key, state, promptId);
  2349. return EditCommandResult.Continue;
  2350. }
  2351. public override EditCommandResult Process(string line)
  2352. {
  2353. if(string.Equals(line, "Need the secret key to do this.", StringComparison.Ordinal))
  2354. {
  2355. throw new KeyEditFailedException("Changing password failed.", FailureReason.MissingSecretKey);
  2356. }
  2357. else return EditCommandResult.Continue;
  2358. }
  2359. readonly SecureString password;
  2360. bool sentCommand;
  2361. }
  2362. #endregion
  2363. #region DeleteSigsCommand
  2364. /// <summary>An edit command that deletes key signatures on user IDs.</summary>
  2365. sealed class DeleteSigsCommand : EditSigsBase
  2366. {
  2367. public DeleteSigsCommand(KeySignature[] sigs) : base(sigs) { }
  2368. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2369. CommandState state, string promptId)
  2370. {
  2371. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2372. {
  2373. if(!sentCommand)
  2374. {
  2375. if(key.SelectedUserId == null)
  2376. {
  2377. throw UnexpectedError("Can't delete signatures because no user ID is selected. "+
  2378. "Perhaps it no longer exists?");
  2379. }
  2380. state.Command.SendLine("delsig");
  2381. sentCommand = true;
  2382. ExpectRelist = false;
  2383. }
  2384. else return EditCommandResult.Next;
  2385. }
  2386. else if(string.Equals(promptId, "keyedit.delsig.valid", StringComparison.Ordinal))
  2387. {
  2388. // the previous line should have contained a sig: line that was parsed into the various sig* member variables.
  2389. // we'll answer yes if the parsed signature appears to match any of the KeySignature objects we have
  2390. state.Command.SendLine(CurrentSigMatches ? "Y" : "N"); // do we want to delete this particular signature?
  2391. }
  2392. else if(string.Equals(promptId, "keyedit.delsig.selfsig", StringComparison.Ordinal))
  2393. {
  2394. state.Command.SendLine("Y"); // yes, it's okay to delete a self-signature
  2395. }
  2396. else return base.Process(commands, originalKey, key, state, promptId);
  2397. return EditCommandResult.Continue;
  2398. }
  2399. bool sentCommand;
  2400. }
  2401. #endregion
  2402. #region DeleteSubkeysCommand
  2403. /// <summary>An edit command that deletes subkeys.</summary>
  2404. sealed class DeleteSubkeysCommand : EditCommand
  2405. {
  2406. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2407. CommandState state, string promptId)
  2408. {
  2409. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2410. {
  2411. if(!sentCommand)
  2412. {
  2413. state.Command.SendLine("delkey");
  2414. sentCommand = true;
  2415. }
  2416. else return EditCommandResult.Next;
  2417. }
  2418. else if(string.Equals(promptId, "keyedit.remove.subkey.okay", StringComparison.Ordinal))
  2419. {
  2420. state.Command.SendLine("Y"); // yes, it's okay to delete a subkey
  2421. }
  2422. else return base.Process(commands, originalKey, key, state, promptId);
  2423. return EditCommandResult.Continue;
  2424. }
  2425. bool sentCommand;
  2426. }
  2427. #endregion
  2428. #region DeleteUidCommand
  2429. /// <summary>An edit command that deletes a user ID or attribute.</summary>
  2430. sealed class DeleteUidCommand : EditCommand
  2431. {
  2432. public DeleteUidCommand()
  2433. {
  2434. ExpectRelist = true;
  2435. }
  2436. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2437. CommandState state, string promptId)
  2438. {
  2439. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2440. {
  2441. if(!sentCommand)
  2442. {
  2443. // find the first user ID (ignoring attributes) that is NOT selected
  2444. int i;
  2445. for(i=0; i<key.UserIds.Count; i++)
  2446. {
  2447. if(!key.UserIds[i].IsAttribute && !key.UserIds[i].Selected) break;
  2448. }
  2449. // if they're all selected, then that's a problem
  2450. if(i == key.UserIds.Count) throw UnexpectedError("Can't delete the last user ID!");
  2451. state.Command.SendLine("deluid");
  2452. sentCommand = true;
  2453. ExpectRelist = false;
  2454. }
  2455. else return EditCommandResult.Next;
  2456. }
  2457. else if(string.Equals(promptId, "keyedit.remove.uid.okay", StringComparison.Ordinal))
  2458. {
  2459. state.Command.SendLine("Y"); // yes, it's okay to delete a user id
  2460. }
  2461. else return base.Process(commands, originalKey, key, state, promptId);
  2462. return EditCommandResult.Continue;
  2463. }
  2464. bool sentCommand;
  2465. }
  2466. #endregion
  2467. #region EditSigsBase
  2468. /// <summary>A base class for commands that edit signatures.</summary>
  2469. abstract class EditSigsBase : RevokeBase
  2470. {
  2471. protected EditSigsBase(KeySignature[] sigs) : this(null, sigs)
  2472. {
  2473. ExpectRelist = true;
  2474. }
  2475. protected EditSigsBase(UserRevocationReason reason, KeySignature[] sigs) : base(reason)
  2476. {
  2477. if(sigs == null || sigs.Length == 0) throw new ArgumentException();
  2478. this.sigs = sigs;
  2479. }
  2480. /// <summary>Determines whether the current signature matches any of the signatures given to the constructor.</summary>
  2481. protected bool CurrentSigMatches
  2482. {
  2483. get
  2484. {
  2485. bool matches = false;
  2486. if(sigs != null)
  2487. {
  2488. foreach(KeySignature sig in sigs)
  2489. {
  2490. if(sig.Exportable == sigExportable && sig.Type == sigType &&
  2491. string.Equals(sig.KeyId, sigKeyId, StringComparison.Ordinal) && sig.CreationTime == sigCreation)
  2492. {
  2493. matches = true;
  2494. break;
  2495. }
  2496. }
  2497. }
  2498. return matches;
  2499. }
  2500. }
  2501. public override EditCommandResult Process(string line)
  2502. {
  2503. // GPG spits out sig: lines and then asks us questions about them. we parse the sig: line so we know what
  2504. // signature GPG is talking about
  2505. if(line.StartsWith("sig:", StringComparison.OrdinalIgnoreCase))
  2506. {
  2507. string[] fields = line.Split(':');
  2508. sigKeyId = fields[4].ToUpperInvariant();
  2509. sigCreation = GPG.ParseTimestamp(fields[5]);
  2510. string sigTypeStr = fields[10];
  2511. sigType = (OpenPGPSignatureType)GetHexValue(sigTypeStr[0], sigTypeStr[1]);
  2512. sigExportable = sigTypeStr[2] == 'x';
  2513. return EditCommandResult.Continue;
  2514. }
  2515. else return base.Process(line);
  2516. }
  2517. readonly KeySignature[] sigs;
  2518. string sigKeyId;
  2519. DateTime sigCreation;
  2520. OpenPGPSignatureType sigType;
  2521. bool sigExportable;
  2522. }
  2523. #endregion
  2524. #region GetPrefsCommand
  2525. /// <summary>An edit command that retrieves user preferences.</summary>
  2526. sealed class GetPrefsCommand : EditCommand
  2527. {
  2528. public GetPrefsCommand()
  2529. {
  2530. ExpectRelist = true;
  2531. }
  2532. /// <param name="preferences">A <see cref="UserPreferences"/> object that will be filled with the user preferences.</param>
  2533. public GetPrefsCommand(UserPreferences preferences)
  2534. {
  2535. if(preferences == null) throw new ArgumentNullException();
  2536. preferences.PreferredCiphers.Clear();
  2537. preferences.PreferredCompressions.Clear();
  2538. preferences.PreferredHashes.Clear();
  2539. this.preferences = preferences;
  2540. }
  2541. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2542. CommandState state, string promptId)
  2543. {
  2544. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2545. {
  2546. if(!sentCommand)
  2547. {
  2548. EditUserId selectedId = key.SelectedUserId;
  2549. if(selectedId == null) throw UnexpectedError("No user ID is selected. Perhaps the user ID doesn't exist?");
  2550. // parse the user preferences from the string given in the key listing. this doesn't include the key server,
  2551. // but we can retrieve it using the "showpref" command
  2552. foreach(string pref in selectedId.Prefs.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
  2553. {
  2554. int id = int.Parse(pref.Substring(1));
  2555. if(pref[0] == 'S') preferences.PreferredCiphers.Add((OpenPGPCipher)id);
  2556. else if(pref[0] == 'H') preferences.PreferredHashes.Add((OpenPGPHashAlgorithm)id);
  2557. else if(pref[0] == 'Z') preferences.PreferredCompressions.Add((OpenPGPCompression)id);
  2558. }
  2559. preferences.Primary = selectedId.Primary;
  2560. state.Command.SendLine("showpref"); // this will cause GPG to print the preferences in a
  2561. sentCommand = true; // text format that we can parse below
  2562. ExpectRelist = false;
  2563. return EditCommandResult.Continue;
  2564. }
  2565. else return EditCommandResult.Next;
  2566. }
  2567. else return base.Process(commands, originalKey, key, state, promptId);
  2568. }
  2569. public override EditCommandResult Process(string line)
  2570. {
  2571. if(sentCommand) // if we sent the showpref command, then we can look for the preferred keyserver line
  2572. {
  2573. line = line.Trim();
  2574. if(line.StartsWith("Preferred keyserver: ", StringComparison.Ordinal))
  2575. {
  2576. preferences.Keyserver = new Uri(line.Substring(21)); // 21 is the length of "Preferred keyserver: "
  2577. return EditCommandResult.Done;
  2578. }
  2579. }
  2580. return EditCommandResult.Continue;
  2581. }
  2582. readonly UserPreferences preferences;
  2583. bool sentCommand;
  2584. }
  2585. #endregion
  2586. #region QuitCommand
  2587. /// <summary>A command that quits the edit session, optionally saving first.</summary>
  2588. sealed class QuitCommand : EditCommand
  2589. {
  2590. public QuitCommand(bool save)
  2591. {
  2592. this.save = save;
  2593. }
  2594. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2595. CommandState state, string promptId)
  2596. {
  2597. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2598. {
  2599. if(!sentCommand)
  2600. {
  2601. state.Command.SendLine(save ? "save" : "quit");
  2602. sentCommand = true;
  2603. }
  2604. else // if GPG didn't quit, then something's wrong...
  2605. {
  2606. throw new KeyEditFailedException("An error occurred while " + (save ? "saving" : "quitting") +
  2607. ". Changes may not have been applied.");
  2608. }
  2609. }
  2610. else if(string.Equals(promptId, "keyedit.save.okay", StringComparison.Ordinal))
  2611. {
  2612. state.Command.SendLine(save ? "Y" : "N");
  2613. }
  2614. else return base.Process(commands, originalKey, key, state, promptId);
  2615. return EditCommandResult.Continue;
  2616. }
  2617. readonly bool save;
  2618. bool sentCommand;
  2619. }
  2620. #endregion
  2621. #region RawCommand
  2622. /// <summary>An edit command that sends a single command to GPG.</summary>
  2623. sealed class RawCommand : EditCommand
  2624. {
  2625. public RawCommand(string command)
  2626. {
  2627. this.command = command;
  2628. }
  2629. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2630. CommandState state, string promptId)
  2631. {
  2632. state.Command.SendLine(command);
  2633. return EditCommandResult.Done;
  2634. }
  2635. readonly string command;
  2636. }
  2637. #endregion
  2638. #region RevokeBase
  2639. /// <summary>A base class for edit commands that revoke stuff.</summary>
  2640. abstract class RevokeBase : EditCommand
  2641. {
  2642. public RevokeBase(UserRevocationReason reason)
  2643. {
  2644. userReason = reason;
  2645. }
  2646. public RevokeBase(KeyRevocationReason reason)
  2647. {
  2648. keyReason = reason;
  2649. }
  2650. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2651. CommandState state, string promptId)
  2652. {
  2653. if(HandleRevokePrompt(state.Command, promptId, keyReason, userReason, ref lines, ref lineIndex))
  2654. {
  2655. return EditCommandResult.Continue;
  2656. }
  2657. else return base.Process(commands, originalKey, key, state, promptId);
  2658. }
  2659. readonly UserRevocationReason userReason;
  2660. readonly KeyRevocationReason keyReason;
  2661. string[] lines;
  2662. int lineIndex;
  2663. }
  2664. #endregion
  2665. #region RevokeSigsCommand
  2666. /// <summary>An edit command that revokes key signatures.</summary>
  2667. sealed class RevokeSigsCommand : EditSigsBase
  2668. {
  2669. public RevokeSigsCommand(UserRevocationReason reason, KeySignature[] sigs) : base(reason, sigs) { }
  2670. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2671. CommandState state, string promptId)
  2672. {
  2673. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2674. {
  2675. if(!sentCommand)
  2676. {
  2677. if(key.SelectedUserId == null)
  2678. {
  2679. throw UnexpectedError("Can't revoke signatures because no user ID is selected. "+
  2680. "Perhaps the user ID no longer exists?");
  2681. }
  2682. state.Command.SendLine("revsig");
  2683. sentCommand = true;
  2684. ExpectRelist = false;
  2685. }
  2686. else return EditCommandResult.Next;
  2687. }
  2688. else if(string.Equals(promptId, "ask_revoke_sig.one", StringComparison.Ordinal))
  2689. {
  2690. // the previous line should have contained a sig: line that was parsed into the various sig* member variables.
  2691. // we'll answer yes if the parsed signature appears to match any of the KeySignature objects we have
  2692. state.Command.SendLine(CurrentSigMatches ? "Y" : "N"); // do we want to revoke this particular signature?
  2693. }
  2694. else if(string.Equals(promptId, "ask_revoke_sig.okay", StringComparison.Ordinal))
  2695. {
  2696. state.Command.SendLine("Y");
  2697. }
  2698. else return base.Process(commands, originalKey, key, state, promptId);
  2699. return EditCommandResult.Continue;
  2700. }
  2701. bool sentCommand;
  2702. }
  2703. #endregion
  2704. #region RevokeSubkeysCommand
  2705. /// <summary>An edit command that revokes subkeys.</summary>
  2706. sealed class RevokeSubkeysCommand : RevokeBase
  2707. {
  2708. public RevokeSubkeysCommand(KeyRevocationReason reason) : base(reason) { }
  2709. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2710. CommandState state, string promptId)
  2711. {
  2712. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2713. {
  2714. if(!sentCommand)
  2715. {
  2716. state.Command.SendLine("revkey");
  2717. sentCommand = true;
  2718. }
  2719. else if(!sentConfirmation) // if GPG never asked us if we were sure, then that means it failed
  2720. {
  2721. throw UnexpectedError("Unable to delete subkeys. Perhaps the subkey no longer exists?");
  2722. }
  2723. else return EditCommandResult.Next;
  2724. }
  2725. else if(string.Equals(promptId, "keyedit.revoke.subkey.okay", StringComparison.Ordinal))
  2726. {
  2727. state.Command.SendLine("Y");
  2728. sentConfirmation = true;
  2729. }
  2730. else return base.Process(commands, originalKey, key, state, promptId);
  2731. return EditCommandResult.Continue;
  2732. }
  2733. bool sentCommand, sentConfirmation;
  2734. }
  2735. #endregion
  2736. #region RevokeUidCommand
  2737. /// <summary>An edit command that revokes user IDs and attributes.</summary>
  2738. sealed class RevokeUidCommand : RevokeBase
  2739. {
  2740. public RevokeUidCommand(UserRevocationReason reason) : base(reason)
  2741. {
  2742. ExpectRelist = true;
  2743. }
  2744. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2745. CommandState state, string promptId)
  2746. {
  2747. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2748. {
  2749. if(!sentCommand)
  2750. {
  2751. if(!sentCommand)
  2752. {
  2753. if(key.SelectedUserId == null)
  2754. {
  2755. throw UnexpectedError("Can't revoke user IDs because none are selected. Perhaps they no longer exist?");
  2756. }
  2757. state.Command.SendLine("revuid");
  2758. sentCommand = true;
  2759. ExpectRelist = false;
  2760. }
  2761. else if(!sentConfirmation) // if GPG never asked us if we were sure, then that means it failed
  2762. {
  2763. throw UnexpectedError("Unable to revoke user IDs.");
  2764. }
  2765. }
  2766. else return EditCommandResult.Next;
  2767. }
  2768. else if(string.Equals(promptId, "keyedit.revoke.uid.okay", StringComparison.Ordinal))
  2769. {
  2770. state.Command.SendLine("Y");
  2771. sentConfirmation = true;
  2772. }
  2773. else return base.Process(commands, originalKey, key, state, promptId);
  2774. return EditCommandResult.Continue;
  2775. }
  2776. bool sentCommand, sentConfirmation;
  2777. }
  2778. #endregion
  2779. #region SelectLastUidCommand
  2780. /// <summary>An edit command that selects the last user ID or attribute in the list.</summary>
  2781. sealed class SelectLastUidCommand : EditCommand
  2782. {
  2783. public SelectLastUidCommand()
  2784. {
  2785. ExpectRelist = true;
  2786. }
  2787. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2788. CommandState state, string promptId)
  2789. {
  2790. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2791. {
  2792. if(key.SelectedUserId != null) // clear the existing selection first
  2793. {
  2794. state.Command.SendLine("uid -");
  2795. return EditCommandResult.Continue;
  2796. }
  2797. if(!key.UserIds[key.UserIds.Count-1].Selected) // then, if the UID is not already selected, select it
  2798. {
  2799. state.Command.SendLine("uid " + key.UserIds.Count.ToStringInvariant());
  2800. return EditCommandResult.Done;
  2801. }
  2802. return EditCommandResult.Next;
  2803. }
  2804. else return base.Process(commands, originalKey, key, state, promptId);
  2805. }
  2806. }
  2807. #endregion
  2808. #region SelectSubkeyCommand
  2809. /// <summary>An edit command that selects a subkey by fingerprint.</summary>
  2810. sealed class SelectSubkeyCommand : EditCommand
  2811. {
  2812. /// <param name="fingerprint">The fingerprint of the subkey to select.</param>
  2813. /// <param name="deselectFirst">True to deselect other subkeys first, and false to not.</param>
  2814. public SelectSubkeyCommand(string fingerprint, bool deselectFirst)
  2815. {
  2816. if(string.IsNullOrEmpty(fingerprint)) throw new ArgumentException("Fingerprint was null or empty.");
  2817. this.fingerprint = fingerprint;
  2818. this.deselectFirst = deselectFirst;
  2819. }
  2820. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2821. CommandState state, string promptId)
  2822. {
  2823. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2824. {
  2825. if(deselectFirst && !clearedSelection)
  2826. {
  2827. state.Command.SendLine("key -"); // GPG doesn't let us know which keys are currently selected, so we'll assume the
  2828. clearedSelection = true; // worst and deselect all keys
  2829. return EditCommandResult.Continue;
  2830. }
  2831. else
  2832. {
  2833. // find the subkey with the given fingerprint
  2834. int index;
  2835. for(index=0; index < key.Subkeys.Count; index++)
  2836. {
  2837. if(string.Equals(fingerprint, key.Subkeys[index], StringComparison.Ordinal)) break;
  2838. }
  2839. if(index == key.Subkeys.Count) throw UnexpectedError("No subkey found with fingerprint " + fingerprint);
  2840. // then select it
  2841. state.Command.SendLine("key " + (index+1).ToStringInvariant());
  2842. return EditCommandResult.Done;
  2843. }
  2844. }
  2845. else return base.Process(commands, originalKey, key, state, promptId);
  2846. }
  2847. readonly string fingerprint;
  2848. readonly bool deselectFirst;
  2849. bool clearedSelection;
  2850. }
  2851. #endregion
  2852. #region SelectUidCommand
  2853. /// <summary>An edit command that selects a given user ID.</summary>
  2854. sealed class SelectUidCommand : EditCommand
  2855. {
  2856. public SelectUidCommand(EditUserId id)
  2857. {
  2858. if(id == null) throw new ArgumentNullException();
  2859. this.id = id;
  2860. ExpectRelist = true;
  2861. }
  2862. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2863. CommandState state, string promptId)
  2864. {
  2865. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2866. {
  2867. // find the UID that matches the given ID
  2868. int index;
  2869. for(index=0; index < key.UserIds.Count; index++)
  2870. {
  2871. if(id.Matches(key.UserIds[index])) // we found it
  2872. {
  2873. // make sure no other UIDs match it
  2874. for(int i=index+1; i < key.UserIds.Count; i++)
  2875. {
  2876. if(id.Matches(key.UserIds[i])) throw UnexpectedError("Multiple user IDs matched " + id.ToString());
  2877. }
  2878. break;
  2879. }
  2880. }
  2881. if(index == key.UserIds.Count) throw UnexpectedError("No user ID matched " + id.ToString());
  2882. // if any UIDs besides the one we want are selected, deselect them
  2883. for(int i=0; i < key.UserIds.Count; i++)
  2884. {
  2885. if(i != index && key.UserIds[i].Selected)
  2886. {
  2887. state.Command.SendLine("uid -");
  2888. return EditCommandResult.Continue;
  2889. }
  2890. }
  2891. // if the one we want is not currently selected, select it
  2892. if(!key.UserIds[index].Selected)
  2893. {
  2894. state.Command.SendLine("uid " + (index+1).ToStringInvariant());
  2895. return EditCommandResult.Done;
  2896. }
  2897. return EditCommandResult.Next;
  2898. }
  2899. else return base.Process(commands, originalKey, key, state, promptId);
  2900. }
  2901. readonly EditUserId id;
  2902. }
  2903. #endregion
  2904. #region SetAlgoPrefsCommand
  2905. /// <summary>An edit command that sets user algorithm preferences.</summary>
  2906. sealed class SetAlgoPrefsCommand : EditCommand
  2907. {
  2908. public SetAlgoPrefsCommand(UserPreferences preferences)
  2909. {
  2910. // create the preference string from the given preferences object
  2911. StringBuilder prefString = new StringBuilder();
  2912. foreach(OpenPGPCipher cipher in preferences.PreferredCiphers)
  2913. {
  2914. prefString.Append(" S").Append(((int)cipher).ToStringInvariant());
  2915. }
  2916. foreach(OpenPGPHashAlgorithm hash in preferences.PreferredHashes)
  2917. {
  2918. prefString.Append(" H").Append(((int)hash).ToStringInvariant());
  2919. }
  2920. foreach(OpenPGPCompression compression in preferences.PreferredCompressions)
  2921. {
  2922. prefString.Append(" Z").Append(((int)compression).ToStringInvariant());
  2923. }
  2924. this.prefString = prefString.ToString();
  2925. }
  2926. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2927. CommandState state, string promptId)
  2928. {
  2929. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2930. {
  2931. if(!sentPrefs)
  2932. {
  2933. state.Command.SendLine("setpref " + prefString);
  2934. sentPrefs = true;
  2935. }
  2936. else return EditCommandResult.Next;
  2937. }
  2938. else if(string.Equals(promptId, "keyedit.setpref.okay", StringComparison.Ordinal))
  2939. {
  2940. state.Command.SendLine("Y");
  2941. }
  2942. else return base.Process(commands, originalKey, key, state, promptId);
  2943. return EditCommandResult.Continue;
  2944. }
  2945. string prefString;
  2946. bool sentPrefs;
  2947. }
  2948. #endregion
  2949. #region SetDefaultPasswordCommand
  2950. sealed class SetDefaultPasswordCommand : EditCommand
  2951. {
  2952. public SetDefaultPasswordCommand(SecureString password)
  2953. {
  2954. this.password = password;
  2955. }
  2956. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2957. CommandState state, string promptId)
  2958. {
  2959. state.DefaultPassword = password;
  2960. return EditCommandResult.Next;
  2961. }
  2962. readonly SecureString password;
  2963. }
  2964. #endregion
  2965. #region SetPrefsCommand
  2966. /// <summary>An edit command that enqueues other commands to set the selected user's preferences.</summary>
  2967. sealed class SetPrefsCommand : EditCommand
  2968. {
  2969. public SetPrefsCommand(UserPreferences preferences)
  2970. {
  2971. this.preferences = preferences;
  2972. }
  2973. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  2974. CommandState state, string promptId)
  2975. {
  2976. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  2977. {
  2978. if(preferences.Primary) commands.Enqueue(new SetPrimaryCommand());
  2979. if(preferences.Keyserver != null)
  2980. {
  2981. commands.Enqueue(new RawCommand("keyserver " + preferences.Keyserver.AbsoluteUri));
  2982. }
  2983. if(preferences.PreferredCiphers.Count != 0 || preferences.PreferredCompressions.Count != 0 ||
  2984. preferences.PreferredHashes.Count != 0)
  2985. {
  2986. commands.Enqueue(new SetAlgoPrefsCommand(preferences));
  2987. }
  2988. return EditCommandResult.Next;
  2989. }
  2990. else return base.Process(commands, originalKey, key, state, promptId);
  2991. }
  2992. readonly UserPreferences preferences;
  2993. }
  2994. #endregion
  2995. #region SetPrimaryCommand
  2996. /// <summary>An edit command that sets the currently-selected user ID or attribute as primary.</summary>
  2997. sealed class SetPrimaryCommand : EditCommand
  2998. {
  2999. public SetPrimaryCommand()
  3000. {
  3001. ExpectRelist = true;
  3002. }
  3003. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  3004. CommandState state, string promptId)
  3005. {
  3006. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  3007. {
  3008. if(key.SelectedUserId == null)
  3009. {
  3010. throw UnexpectedError("Can't set primary user ID because no user ID is selected.");
  3011. }
  3012. if(!key.SelectedUserId.Primary) // if it's not already primary, make it so
  3013. {
  3014. state.Command.SendLine("primary");
  3015. return EditCommandResult.Done;
  3016. }
  3017. else return EditCommandResult.Next; // otherwise, just go to the next command
  3018. }
  3019. else return base.Process(commands, originalKey, key, state, promptId);
  3020. }
  3021. }
  3022. #endregion
  3023. #region SetTrustCommand
  3024. /// <summary>An edit command that sets the owner trust of the primary key.</summary>
  3025. sealed class SetTrustCommand : EditCommand
  3026. {
  3027. public SetTrustCommand(TrustLevel level)
  3028. {
  3029. this.level = level;
  3030. }
  3031. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  3032. CommandState state, string promptId)
  3033. {
  3034. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  3035. {
  3036. if(!sentCommand)
  3037. {
  3038. state.Command.SendLine("trust");
  3039. sentCommand = true;
  3040. }
  3041. else return EditCommandResult.Next;
  3042. }
  3043. else if(string.Equals(promptId, "edit_ownertrust.value", StringComparison.Ordinal))
  3044. {
  3045. switch(level)
  3046. {
  3047. case TrustLevel.Never: state.Command.SendLine("2"); break;
  3048. case TrustLevel.Marginal: state.Command.SendLine("3"); break;
  3049. case TrustLevel.Full: state.Command.SendLine("4"); break;
  3050. case TrustLevel.Ultimate: state.Command.SendLine("5"); break;
  3051. default: state.Command.SendLine("1"); break;
  3052. }
  3053. }
  3054. else if(string.Equals(promptId, "edit_ownertrust.set_ultimate.okay", StringComparison.Ordinal))
  3055. {
  3056. state.Command.SendLine("Y"); // yes, it's okay to set ultimate trust
  3057. }
  3058. else return base.Process(commands, originalKey, key, state, promptId);
  3059. return EditCommandResult.Continue;
  3060. }
  3061. readonly TrustLevel level;
  3062. bool sentCommand;
  3063. }
  3064. #endregion
  3065. #region SignKeyCommand
  3066. /// <summary>An edit command that signs a primary key or the currently-selected user IDs.</summary>
  3067. sealed class SignKeyCommand : EditCommand
  3068. {
  3069. /// <param name="options">Options that control the signing.</param>
  3070. /// <param name="signWholeKey">If true, the entire key should be signed. If false, only the selected user IDs
  3071. /// should be signed.
  3072. /// </param>
  3073. public SignKeyCommand(KeySigningOptions options, bool signWholeKey)
  3074. {
  3075. this.options = options;
  3076. this.signWholeKey = signWholeKey;
  3077. if(options != null && ContainsControlCharacters(options.TrustDomain))
  3078. {
  3079. throw new ArgumentException("The trust domain contains control characters. Remove them.");
  3080. }
  3081. }
  3082. public override EditCommandResult Process(Queue<EditCommand> commands, EditKey originalKey, EditKey key,
  3083. CommandState state, string promptId)
  3084. {
  3085. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal))
  3086. {
  3087. if(!sentCommand)
  3088. {
  3089. // build the command name based on the options
  3090. string prefix = null;
  3091. if(options == null || !options.Exportable) prefix += "l";
  3092. if(options != null)
  3093. {
  3094. if(options.TrustLevel != TrustLevel.Unknown) prefix += "t";
  3095. if(options.Irrevocable) prefix += "nr";
  3096. }
  3097. state.Command.SendLine(prefix + "sign");
  3098. sentCommand = true;
  3099. }
  3100. else return EditCommandResult.Next;
  3101. }
  3102. else if(string.Equals(promptId, "sign_uid.class", StringComparison.Ordinal))
  3103. {
  3104. if(options == null || options.CertificationLevel == CertificationLevel.Undisclosed)
  3105. {
  3106. state.Command.SendLine("0");
  3107. }
  3108. else if(options.CertificationLevel == CertificationLevel.None) state.Command.SendLine("1");
  3109. else if(options.CertificationLevel == CertificationLevel.Casual) state.Command.SendLine("2");
  3110. else if(options.CertificationLevel == CertificationLevel.Rigorous) state.Command.SendLine("3");
  3111. else throw new NotSupportedException("Certification level " + options.CertificationLevel.ToString() +
  3112. " is not supported.");
  3113. }
  3114. else if(string.Equals(promptId, "sign_uid.okay", StringComparison.Ordinal))
  3115. {
  3116. state.Command.SendLine("Y"); // yes, it's okay to sign a UID
  3117. }
  3118. else if(string.Equals(promptId, "keyedit.sign_all.okay", StringComparison.Ordinal))
  3119. {
  3120. // GPG is saying that no UID is selected, and asking if we want to sign the whole key
  3121. if(signWholeKey)
  3122. {
  3123. state.Command.SendLine("Y");
  3124. }
  3125. else // if that wasn't what the user asked for, then bail out
  3126. {
  3127. throw UnexpectedError("No user ID was selected, and you didn't request to sign the entire key. "+
  3128. "Perhaps the user ID to sign no longer exists?");
  3129. }
  3130. }
  3131. else if(string.Equals(promptId, "trustsig_prompt.trust_value", StringComparison.Ordinal))
  3132. {
  3133. if(options == null || options.TrustLevel == TrustLevel.Unknown)
  3134. {
  3135. throw UnexpectedError("GPG asked about trust levels for a non-trust signature.");
  3136. }
  3137. else if(options.TrustLevel == TrustLevel.Marginal) state.Command.SendLine("1");
  3138. else if(options.TrustLevel == TrustLevel.Full) state.Command.SendLine("2");
  3139. else throw new NotSupportedException("Trust level " + options.TrustLevel.ToString() + " is not supported.");
  3140. }
  3141. else if(string.Equals(promptId, "trustsig_prompt.trust_depth", StringComparison.Ordinal))
  3142. {
  3143. state.Command.SendLine(options.TrustDepth.ToStringInvariant());
  3144. }
  3145. else if(string.Equals(promptId, "trustsig_prompt.trust_regexp", StringComparison.Ordinal))
  3146. {
  3147. state.Command.SendLine(options.TrustDomain);
  3148. }
  3149. else return base.Process(commands, originalKey, key, state, promptId);
  3150. return EditCommandResult.Continue;
  3151. }
  3152. readonly KeySigningOptions options;
  3153. readonly bool signWholeKey;
  3154. bool sentCommand;
  3155. }
  3156. #endregion
  3157. #endregion
  3158. /// <summary>Gets whether logging is enabled.</summary>
  3159. bool LoggingEnabled
  3160. {
  3161. get { return LineLogged != null; }
  3162. }
  3163. /// <summary>Throws an exception if <see cref="Initialize"/> has not yet been called.</summary>
  3164. void AssertInitialized()
  3165. {
  3166. if(ExecutablePath == null) throw new InvalidOperationException("Initialize() has not been called.");
  3167. }
  3168. /// <summary>Throws an exception if the given type is not within the given array of supported types, with
  3169. /// case-insensitive matching.
  3170. /// </summary>
  3171. void AssertSupported(string type, string[] supportedTypes, string name)
  3172. {
  3173. foreach(string supportedType in supportedTypes)
  3174. {
  3175. if(string.Equals(type, supportedType, StringComparison.OrdinalIgnoreCase)) return;
  3176. }
  3177. throw new ArgumentException(type + " is not a supported " + name + ".");
  3178. }
  3179. /// <summary>Performs the main work of both decryption and verification.</summary>
  3180. Signature[] DecryptVerifyCore(Command command, Stream signedData, Stream destination, DecryptionOptions options)
  3181. {
  3182. List<Signature> signatures = new List<Signature>(); // this holds the completed signatures
  3183. Signature sig = new Signature(); // keep track of the current signature
  3184. bool sigFilled = false;
  3185. command.KillProcessOnAbort = true;
  3186. CommandState commandState = ProcessCommand(command,
  3187. delegate(Command cmd, CommandState state)
  3188. {
  3189. bool triedPasswordInOptions = false;
  3190. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  3191. cmd.InputNeeded += delegate(string promptId)
  3192. {
  3193. if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) && state.PasswordMessage != null &&
  3194. state.PasswordMessage.Type == StatusMessageType.NeedCipherPassphrase)
  3195. {
  3196. // we'll first try sending the password from the options if we have it, but only once.
  3197. if(!triedPasswordInOptions &&
  3198. options != null && options.Password != null && options.Password.Length != 0)
  3199. {
  3200. triedPasswordInOptions = true;
  3201. cmd.SendPassword(options.Password, false);
  3202. }
  3203. else // we either don't have a password in the options, or we already sent it (and it probably failed),
  3204. { // so ask the user
  3205. SecureString password = GetDecryptionPassword();
  3206. if(password != null) cmd.SendPassword(password, true);
  3207. else cmd.SendLine();
  3208. }
  3209. }
  3210. else DefaultPromptHandler(promptId, state);
  3211. };
  3212. cmd.StatusMessageReceived += delegate(StatusMessage msg)
  3213. {
  3214. if(msg is TrustLevelMessage)
  3215. {
  3216. sig.TrustLevel = ((TrustLevelMessage)msg).Level;
  3217. }
  3218. else
  3219. {
  3220. // if the message begins a new signature, add the previous one (if it's complete enough to add)
  3221. if(msg.Type == StatusMessageType.NewSig || msg.Type == StatusMessageType.BadSig ||
  3222. msg.Type == StatusMessageType.GoodSig || msg.Type == StatusMessageType.ErrorSig)
  3223. {
  3224. if(sigFilled) signatures.Add(sig);
  3225. sig = new Signature();
  3226. sigFilled = false; // the new signature is not complete enough to add
  3227. }
  3228. switch(msg.Type)
  3229. {
  3230. case StatusMessageType.BadSig:
  3231. {
  3232. BadSigMessage bad = (BadSigMessage)msg;
  3233. sig.KeyId = bad.KeyId;
  3234. sig.SignerName = bad.UserName;
  3235. sig.Status = SignatureStatus.Invalid;
  3236. sigFilled = true;
  3237. break;
  3238. }
  3239. case StatusMessageType.ErrorSig:
  3240. {
  3241. ErrorSigMessage error = (ErrorSigMessage)msg;
  3242. sig.HashAlgorithm = error.HashAlgorithm;
  3243. sig.KeyId = error.KeyId;
  3244. sig.KeyType = error.KeyType;
  3245. sig.CreationTime = error.Timestamp;
  3246. sig.Status = SignatureStatus.Error | (error.MissingKey ? SignatureStatus.MissingKey : 0) |
  3247. (error.UnsupportedAlgorithm ? SignatureStatus.UnsupportedAlgorithm : 0);
  3248. sigFilled = true;
  3249. break;
  3250. }
  3251. case StatusMessageType.ExpiredKeySig:
  3252. {
  3253. ExpiredKeySigMessage em = (ExpiredKeySigMessage)msg;
  3254. sig.KeyId = em.KeyId;
  3255. sig.SignerName = em.UserName;
  3256. sig.Status |= SignatureStatus.ExpiredKey;
  3257. break;
  3258. }
  3259. case StatusMessageType.ExpiredSig:
  3260. {
  3261. ExpiredSigMessage em = (ExpiredSigMessage)msg;
  3262. sig.KeyId = em.KeyId;
  3263. sig.SignerName = em.UserName;
  3264. sig.Status |= SignatureStatus.ExpiredSignature;
  3265. break;
  3266. }
  3267. case StatusMessageType.GoodSig:
  3268. {
  3269. GoodSigMessage good = (GoodSigMessage)msg;
  3270. sig.KeyId = good.KeyId;
  3271. sig.SignerName = good.UserName;
  3272. sig.Status = SignatureStatus.Valid | (sig.Status & SignatureStatus.ValidFlagMask);
  3273. sigFilled = true;
  3274. break;
  3275. }
  3276. case StatusMessageType.RevokedKeySig:
  3277. {
  3278. RevokedKeySigMessage em = (RevokedKeySigMessage)msg;
  3279. sig.KeyId = em.KeyId;
  3280. sig.SignerName = em.UserName;
  3281. sig.Status |= SignatureStatus.RevokedKey;
  3282. break;
  3283. }
  3284. case StatusMessageType.ValidSig:
  3285. {
  3286. ValidSigMessage valid = (ValidSigMessage)msg;
  3287. sig.HashAlgorithm = valid.HashAlgorithm;
  3288. sig.KeyType = valid.KeyType;
  3289. sig.PrimaryKeyFingerprint = valid.PrimaryKeyFingerprint;
  3290. sig.Expiration = valid.SignatureExpiration;
  3291. sig.KeyFingerprint = valid.SignatureKeyFingerprint;
  3292. sig.CreationTime = valid.SignatureTime;
  3293. break;
  3294. }
  3295. default: DefaultStatusMessageHandler(msg, state); break;
  3296. }
  3297. }
  3298. };
  3299. },
  3300. delegate(Command cmd, CommandState state)
  3301. {
  3302. // write the signed and/or encrypted data to STDIN and read the plaintext from STDOUT
  3303. if(destination == null) WriteStreamToProcess(signedData, cmd.Process);
  3304. else ReadAndWriteStreams(destination, signedData, cmd.Process);
  3305. });
  3306. if(!command.SuccessfulExit) throw new DecryptionFailedException(commandState.FailureReasons);
  3307. if(sigFilled) signatures.Add(sig); // add the final signature if it's filled out
  3308. // make all the signature objects read only and return them
  3309. foreach(Signature signature in signatures) signature.MakeReadOnly();
  3310. return signatures.ToArray();
  3311. }
  3312. /// <summary>Provides default handling for input prompts.</summary>
  3313. void DefaultPromptHandler(string promptId, CommandState state)
  3314. {
  3315. if(string.Equals(promptId, "passphrase.enter", StringComparison.Ordinal) && state.PasswordMessage != null &&
  3316. state.PasswordMessage.Type == StatusMessageType.NeedKeyPassphrase)
  3317. {
  3318. if(state.DefaultPassword != null)
  3319. {
  3320. state.Command.SendPassword(state.DefaultPassword, false);
  3321. }
  3322. else if(!SendKeyPassword(state.Command, state.PasswordHint, (NeedKeyPassphraseMessage)state.PasswordMessage))
  3323. {
  3324. state.FailureReasons |= FailureReason.OperationCanceled;
  3325. }
  3326. }
  3327. else throw new NotImplementedException("Unhandled input request: " + promptId);
  3328. }
  3329. /// <summary>Provides default handling for status messages.</summary>
  3330. void DefaultStatusMessageHandler(StatusMessage msg, CommandState state)
  3331. {
  3332. switch(msg.Type)
  3333. {
  3334. case StatusMessageType.UserIdHint: state.PasswordHint = ((UserIdHintMessage)msg).Hint; break;
  3335. case StatusMessageType.InvalidRecipient:
  3336. {
  3337. state.FailureReasons |= FailureReason.InvalidRecipients;
  3338. InvalidRecipientMessage m = (InvalidRecipientMessage)msg;
  3339. throw new EncryptionFailedException(state.FailureReasons, "Invalid recipient "+m.Recipient+". "+m.ReasonText);
  3340. }
  3341. case StatusMessageType.BadPassphrase:
  3342. if(state.DefaultPassword != null)
  3343. {
  3344. state.DefaultPassword = null;
  3345. }
  3346. else if(!state.Canceled) // don't send the OnInvalidPassword message if a user has simply hit Cancel
  3347. {
  3348. OnInvalidPassword(((BadPassphraseMessage)msg).KeyId);
  3349. state.FailureReasons |= FailureReason.BadPassword;
  3350. }
  3351. break;
  3352. case StatusMessageType.NoPublicKey: state.FailureReasons |= FailureReason.MissingPublicKey; break;
  3353. case StatusMessageType.NoSecretKey: state.FailureReasons |= FailureReason.MissingSecretKey; break;
  3354. case StatusMessageType.NeedKeyPassphrase:
  3355. case StatusMessageType.NeedCipherPassphrase:
  3356. case StatusMessageType.NeedPin:
  3357. state.PasswordMessage = msg; // keep track of the password request message so it can be handled if we get a
  3358. break; // password request prompt
  3359. case StatusMessageType.UnexpectedData: case StatusMessageType.NoData:
  3360. state.FailureReasons |= FailureReason.BadData;
  3361. break;
  3362. case StatusMessageType.DeleteFailed:
  3363. {
  3364. DeleteFailedMessage m = (DeleteFailedMessage)msg;
  3365. if(m.Reason == DeleteFailureReason.NoSuchKey) state.FailureReasons |= FailureReason.KeyNotFound;
  3366. break;
  3367. }
  3368. case StatusMessageType.GetBool: case StatusMessageType.GetHidden: case StatusMessageType.GetLine:
  3369. throw new NotImplementedException("Unhandled input request: " + ((GetInputMessage)msg).PromptId);
  3370. }
  3371. }
  3372. void DoEdit(PrimaryKey key, params EditCommand[] initialCommands)
  3373. {
  3374. DoEdit(key, null, true, initialCommands);
  3375. }
  3376. void DoEdit(PrimaryKey key, string args, bool addKeyring, params EditCommand[] initialCommands)
  3377. {
  3378. if(key == null) throw new ArgumentNullException();
  3379. if(string.IsNullOrEmpty(key.Fingerprint)) throw new ArgumentException("The key to edit has no fingerprint.");
  3380. EditKey originalKey = null, editKey = null;
  3381. Queue<EditCommand> commands = new Queue<EditCommand>(initialCommands);
  3382. args += " --with-colons --fixed-list-mode --edit-key " + key.EffectiveId;
  3383. if(addKeyring) args = GetKeyringArgs(key.Keyring, true) + args;
  3384. Command command = Execute(args, StatusMessages.MixIntoStdout, true, true);
  3385. CommandState commandState = ProcessCommand(command,
  3386. delegate(Command cmd, CommandState state)
  3387. {
  3388. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  3389. },
  3390. delegate(Command cmd, CommandState state)
  3391. {
  3392. bool gotFreshList = false;
  3393. // the ExecuteForEdit() command coallesced the status lines into STDOUT, so we need to parse out the status
  3394. // messages ourselves
  3395. while(true)
  3396. {
  3397. string line;
  3398. cmd.ReadLine(out line);
  3399. if(line != null) LogLine(line);
  3400. gotLine:
  3401. if(line == null && cmd.StatusMessage == null) break;
  3402. if(line == null) // it's a status message
  3403. {
  3404. switch(cmd.StatusMessage.Type)
  3405. {
  3406. case StatusMessageType.GetLine: case StatusMessageType.GetHidden: case StatusMessageType.GetBool:
  3407. {
  3408. string promptId = ((GetInputMessage)cmd.StatusMessage).PromptId;
  3409. while(true) // input is needed, so process it
  3410. {
  3411. // if the queue is empty, add a quit command
  3412. if(commands.Count == 0) commands.Enqueue(new QuitCommand(true));
  3413. if(string.Equals(promptId, "keyedit.prompt", StringComparison.Ordinal) &&
  3414. !gotFreshList && commands.Peek().ExpectRelist)
  3415. {
  3416. cmd.SendLine("list");
  3417. break;
  3418. }
  3419. EditCommandResult result = commands.Peek().Process(commands, originalKey, editKey, state, promptId);
  3420. gotFreshList = false;
  3421. if(result == EditCommandResult.Next || result == EditCommandResult.Done) commands.Dequeue();
  3422. if(result == EditCommandResult.Continue || result == EditCommandResult.Done) break;
  3423. }
  3424. break;
  3425. }
  3426. default:
  3427. DefaultStatusMessageHandler(cmd.StatusMessage, state);
  3428. if(state.Canceled) throw new OperationCanceledException();
  3429. break;
  3430. }
  3431. }
  3432. else if(line.StartsWith("pub:", StringComparison.Ordinal)) // a key listing is beginning
  3433. {
  3434. // GPG outputs a key listing when edit mode is first started, and after commands that might change the key
  3435. editKey = new EditKey();
  3436. bool gotSubkey = false;
  3437. do // parse the key listing
  3438. {
  3439. string[] fields = line.Split(':');
  3440. switch(fields[0])
  3441. {
  3442. case "sub": gotSubkey = true; break;
  3443. case "fpr":
  3444. if(gotSubkey) editKey.Subkeys.Add(fields[9].ToUpperInvariant());
  3445. break;
  3446. case "uid": case "uat":
  3447. {
  3448. EditUserId uid = new EditUserId();
  3449. uid.IsAttribute = fields[0][1] == 'a'; // it's an attribute if fields[0] == "uat"
  3450. uid.Name = fields[9];
  3451. uid.Prefs = fields[12].Split(',')[0];
  3452. string[] bits = fields[13].Split(',');
  3453. if(bits.Length > 1)
  3454. {
  3455. foreach(char c in bits[1])
  3456. {
  3457. if(c == 'p') uid.Primary = true;
  3458. else if(c == 's') uid.Selected = true;
  3459. }
  3460. }
  3461. editKey.UserIds.Add(uid);
  3462. break;
  3463. }
  3464. }
  3465. cmd.ReadLine(out line);
  3466. if(line != null) LogLine(line);
  3467. } while(!string.IsNullOrEmpty(line)); // break out if the line is empty or a status message
  3468. gotFreshList = true;
  3469. // keep a copy of the original key state. this is useful to tell which user ID was initially primary, etc.
  3470. if(originalKey == null) originalKey = editKey;
  3471. // at this point, we've got a valid line, so jump to the part where we inspect it
  3472. goto gotLine;
  3473. }
  3474. else // a line other than a key listing or a status line was received
  3475. {
  3476. while(true) // let the edit commands handle it
  3477. {
  3478. if(commands.Count == 0) break;
  3479. EditCommandResult result = commands.Peek().Process(line);
  3480. if(result == EditCommandResult.Next || result == EditCommandResult.Done) commands.Dequeue();
  3481. if(result == EditCommandResult.Continue || result == EditCommandResult.Done) break;
  3482. }
  3483. }
  3484. }
  3485. });
  3486. if(!command.SuccessfulExit) throw new KeyEditFailedException("Key edit failed.", commandState.FailureReasons);
  3487. }
  3488. /// <summary>Performs an edit command on groups of user attributes.</summary>
  3489. void EditAttributes(UserAttribute[] attributes, EditCommandCreator cmdCreator)
  3490. {
  3491. foreach(List<UserAttribute> list in GroupAttributesByKey(attributes))
  3492. {
  3493. EditCommand[] commands = new EditCommand[list.Count+1];
  3494. for(int i=0; i<list.Count; i++) commands[i] = new RawCommand("uid " + list[i].Id);
  3495. commands[commands.Length-1] = cmdCreator();
  3496. DoEdit(list[0].PrimaryKey, commands);
  3497. }
  3498. }
  3499. /// <summary>Performs an edit command on a list of primary keys.</summary>
  3500. void EditKeys(PrimaryKey[] keys, EditCommandCreator cmdCreator)
  3501. {
  3502. if(keys == null) throw new ArgumentNullException();
  3503. foreach(PrimaryKey key in keys)
  3504. {
  3505. if(key == null) throw new ArgumentNullException("A key was null.");
  3506. DoEdit(key, cmdCreator());
  3507. }
  3508. }
  3509. /// <summary>Performs an edit command on groups of subkeys.</summary>
  3510. void EditSubkeys(Subkey[] subkeys, EditCommandCreator cmdCreator)
  3511. {
  3512. foreach(List<Subkey> keyList in GroupSubkeysByKey(subkeys))
  3513. {
  3514. EditCommand[] commands = new EditCommand[keyList.Count+1];
  3515. for(int i=0; i<keyList.Count; i++) commands[i] = new SelectSubkeyCommand(keyList[i].Fingerprint, false);
  3516. commands[keyList.Count] = cmdCreator();
  3517. DoEdit(keyList[0].PrimaryKey, commands);
  3518. }
  3519. }
  3520. /// <summary>Performs an edit command on groups of key signatures.</summary>
  3521. void EditSignatures(KeySignature[] signatures, KeySignatureEditCommandCreator cmdCreator)
  3522. {
  3523. // first group the signatures by the owning key and the signed object
  3524. Dictionary<string, List<UserAttribute>> uidMap;
  3525. Dictionary<string, List<KeySignature>> sigMap;
  3526. GroupSignaturesByKeyAndObject(signatures, out uidMap, out sigMap);
  3527. List<EditCommand> commands = new List<EditCommand>();
  3528. foreach(KeyValuePair<string, List<UserAttribute>> pair in uidMap) // then, for each key to be edited...
  3529. {
  3530. bool firstUid = true;
  3531. foreach(UserAttribute uid in pair.Value) // for each affected UID in the key
  3532. {
  3533. // select the UID
  3534. if(!firstUid) commands.Add(new RawCommand("uid -"));
  3535. commands.Add(new RawCommand("uid " + uid.Id));
  3536. // then perform the command
  3537. commands.Add(cmdCreator(sigMap[uid.Id].ToArray()));
  3538. firstUid = false;
  3539. }
  3540. DoEdit(pair.Value[0].PrimaryKey, commands.ToArray());
  3541. commands.Clear();
  3542. }
  3543. }
  3544. Command Execute(string args, StatusMessages statusMessageHandling, bool closeStdInput)
  3545. {
  3546. return Execute(args, statusMessageHandling, closeStdInput, false);
  3547. }
  3548. /// <summary>Creates a new <see cref="Command"/> object and returns it.</summary>
  3549. /// <param name="args">Command-line arguments to pass to GPG.</param>
  3550. /// <param name="statusMessageHandling">How status messages will be handled.</param>
  3551. /// <param name="closeStdInput">If true, STDIN will be closed immediately after starting the process so that
  3552. /// GPG will not block waiting for input from it.
  3553. /// </param>
  3554. /// <param name="canKill">If true, the process may be automatically killed if the thread is aborted.</param>
  3555. Command Execute(string args, StatusMessages statusMessageHandling, bool closeStdInput, bool canKill)
  3556. {
  3557. InheritablePipe commandPipe = null;
  3558. if(statusMessageHandling != StatusMessages.Ignore) // if the status stream is requested...
  3559. {
  3560. commandPipe = new InheritablePipe(); // create a pipe for the command-fd
  3561. string cmdFd = commandPipe.ClientHandle.ToInt64().ToStringInvariant();
  3562. string statusFd = statusMessageHandling == StatusMessages.MixIntoStdout ? "1" : cmdFd;
  3563. args = "--exit-on-status-write-error --status-fd " + statusFd + " --command-fd " + cmdFd + " " + args;
  3564. }
  3565. return new Command(this, GetProcessStartInfo(ExecutablePath, args), commandPipe,
  3566. statusMessageHandling, closeStdInput, canKill);
  3567. }
  3568. /// <summary>Executes the given GPG executable with the given arguments.</summary>
  3569. Process Execute(string exePath, string args)
  3570. {
  3571. return Process.Start(GetProcessStartInfo(exePath, args));
  3572. }
  3573. /// <summary>Performs the main work of exporting keys.</summary>
  3574. void ExportCore(string args, Stream destination)
  3575. {
  3576. Command command = Execute(args, StatusMessages.ReadInBackground, true, true);
  3577. CommandState commandState = ProcessCommand(command,
  3578. delegate(Command cmd, CommandState state)
  3579. {
  3580. cmd.StatusMessageReceived += delegate(StatusMessage msg) { DefaultStatusMessageHandler(msg, state); };
  3581. },
  3582. delegate(Command cmd, CommandState state)
  3583. {
  3584. cmd.Process.StandardOutput.BaseStream.CopyTo(destination);
  3585. });
  3586. if(!command.SuccessfulExit) throw new ExportFailedException(commandState.FailureReasons);
  3587. }
  3588. /// <summary>Generates a revocation certificate, either directly or via a designated revoker.</summary>
  3589. void GenerateRevocationCertificateCore(PrimaryKey key, PrimaryKey designatedRevoker, Stream destination,
  3590. KeyRevocationReason reason, OutputOptions outputOptions)
  3591. {
  3592. if(key == null || destination == null) throw new ArgumentNullException();
  3593. string args = GetOutputArgs(outputOptions);
  3594. if(designatedRevoker == null) // direct revocation certificate
  3595. {
  3596. args += GetKeyringArgs(key.Keyring, true) + "--gen-revoke ";
  3597. }
  3598. else // delegated revocation certificate
  3599. {
  3600. args += GetKeyringArgs(new PrimaryKey[] { key, designatedRevoker }, true) +
  3601. "-u " + designatedRevoker.Fingerprint + " --desig-revoke ";
  3602. }
  3603. args += key.Fingerprint;
  3604. Command command = Execute(args, StatusMessages.ReadInBackground, true, true);
  3605. CommandState commandState = ProcessCommand(command,
  3606. delegate(Command cmd, CommandState state)
  3607. {
  3608. string[] lines = null; // needed for the revocation prompt handler
  3609. int lineIndex = 0;
  3610. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  3611. cmd.StatusMessageReceived += delegate(StatusMessage msg) { DefaultStatusMessageHandler(msg, state); };
  3612. cmd.InputNeeded += delegate(string promptId)
  3613. {
  3614. if(string.Equals(promptId, "gen_revoke.okay", StringComparison.Ordinal) ||
  3615. string.Equals(promptId, "gen_desig_revoke.okay", StringComparison.Ordinal))
  3616. {
  3617. cmd.SendLine("Y");
  3618. }
  3619. else if(!HandleRevokePrompt(cmd, promptId, reason, null, ref lines, ref lineIndex))
  3620. {
  3621. if(!state.Canceled)
  3622. {
  3623. DefaultPromptHandler(promptId, state);
  3624. if(state.Canceled) cmd.Kill(); // kill GPG if the user doesn't give the password, so it doesn't keep asking
  3625. }
  3626. }
  3627. };
  3628. },
  3629. delegate(Command cmd, CommandState state) { cmd.Process.StandardOutput.BaseStream.CopyTo(destination); });
  3630. if(!command.SuccessfulExit)
  3631. {
  3632. if(commandState.Canceled) // if the user canceled
  3633. {
  3634. throw new OperationCanceledException();
  3635. }
  3636. else
  3637. {
  3638. throw new PGPException("Unable to generate revocation certificate for key " + key.ToString(),
  3639. commandState.FailureReasons);
  3640. }
  3641. }
  3642. }
  3643. /// <summary>Does the work of retrieving and searching for keys.</summary>
  3644. PrimaryKey[] GetKeys(Keyring[] keyrings, bool includeDefaultKeyring, ListOptions options, string searchArgs)
  3645. {
  3646. string args = "--with-fingerprint --with-fingerprint --with-colons --fixed-list-mode "; // machine-readable output
  3647. // although GPG has a "show-keyring" option, it doesn't work with --with-colons, so we need to query each keyring
  3648. // individually, so we can tell which keyring a key came from. this may cause problems with signature verification
  3649. // if a key on one ring signs a key on another ring...
  3650. List<PrimaryKey> keys = new List<PrimaryKey>(64);
  3651. if(includeDefaultKeyring) GetKeys(keys, options, args, null, searchArgs);
  3652. if(keyrings != null)
  3653. {
  3654. foreach(Keyring keyring in keyrings)
  3655. {
  3656. if(keyring == null) throw new ArgumentException("A keyring was null.");
  3657. GetKeys(keys, options, args, keyring, searchArgs);
  3658. }
  3659. }
  3660. // make the keys read only, which we couldn't do when they were first added to the list
  3661. foreach(PrimaryKey key in keys) key.MakeReadOnly();
  3662. return keys.ToArray();
  3663. }
  3664. /// <summary>Does the work of retrieving and searching for keys on a single keyring.</summary>
  3665. void GetKeys(List<PrimaryKey> keys, ListOptions options, string args, Keyring keyring, string searchArgs)
  3666. {
  3667. ListOptions secretKeyHandling = options & ListOptions.SecretKeyMask;
  3668. if(secretKeyHandling == ListOptions.IgnoreSecretKeys)
  3669. {
  3670. GetKeys(keys, options, args, keyring, searchArgs, false);
  3671. }
  3672. else
  3673. {
  3674. Dictionary<string,object> secretKeys = new Dictionary<string,object>(StringComparer.Ordinal);
  3675. List<PrimaryKey> tempKeys = new List<PrimaryKey>(64);
  3676. // get the list of secret keys and store their IDs in a dictionary
  3677. GetKeys(tempKeys, options & ~(ListOptions.SignatureMask|ListOptions.RetrieveAttributes),
  3678. args, keyring, searchArgs, true);
  3679. foreach(PrimaryKey key in tempKeys) secretKeys[key.EffectiveId] = null;
  3680. tempKeys.Clear();
  3681. // then get the public keys and match them up to the secret ones
  3682. GetKeys(tempKeys, options & ~ListOptions.SecretKeyMask, args, keyring, searchArgs, false);
  3683. if(secretKeyHandling == ListOptions.RetrieveSecretKeys)
  3684. {
  3685. foreach(PrimaryKey key in tempKeys) key.HasSecretKey = secretKeys.ContainsKey(key.EffectiveId);
  3686. keys.AddRange(tempKeys);
  3687. }
  3688. else
  3689. {
  3690. foreach(PrimaryKey key in tempKeys)
  3691. {
  3692. if(secretKeys.ContainsKey(key.EffectiveId))
  3693. {
  3694. key.HasSecretKey = true;
  3695. keys.Add(key);
  3696. }
  3697. }
  3698. }
  3699. }
  3700. }
  3701. /// <summary>Does the work of retrieving and searching for keys on a single keyring.</summary>
  3702. void GetKeys(List<PrimaryKey> keys, ListOptions options, string args, Keyring keyring,
  3703. string searchArgs, bool secretKeys)
  3704. {
  3705. ListOptions signatures = options & ListOptions.SignatureMask;
  3706. if(secretKeys) args += "--list-secret-keys ";
  3707. else if(signatures != 0 && RetrieveKeySignatureFingerprints) args += "--check-sigs --no-sig-cache ";
  3708. else if(signatures == ListOptions.RetrieveSignatures) args += "--list-sigs ";
  3709. else if(signatures == ListOptions.VerifySignatures) args += "--check-sigs ";
  3710. else args += "--list-keys ";
  3711. args += GetKeyringArgs(keyring, secretKeys);
  3712. // keep track of the initial key count so we know which ones were added by this call
  3713. int initialKeyCount = keys.Count;
  3714. // if we're searching, but GPG finds no keys, it will give an error. (it doesn't give an error if it found at least
  3715. // one item searched for.) we'll keep track of this case and ignore the error if we happen to be searching.
  3716. bool retrieveAttributes = (options & ListOptions.RetrieveAttributes) != 0;
  3717. // annoyingly, GPG doesn't flush the attribute stream after writing an attribute, so we can't reliably read
  3718. // attribute data in response to an ATTRIBUTE status message, because it may block waiting for data that's stuck in
  3719. // GPG's output buffer. so we'll write the attribute data to a temp file, and create dummy attributes. then at the
  3720. // end, we'll replace the dummy attributes with the real thing.
  3721. // if attributes are being retrieved, create a new pipe and some syncronization primitives to help with the task
  3722. InheritablePipe attrPipe;
  3723. FileStream attrStream, attrTempStream;
  3724. string attrTempFile;
  3725. AttributeMessage attrMsg = null;
  3726. AsyncCallback attrCallback = null;
  3727. ManualResetEvent attrDone; // signaled when all attribute data has been read
  3728. byte[] attrBuffer;
  3729. if(retrieveAttributes)
  3730. {
  3731. attrPipe = new InheritablePipe();
  3732. attrStream = new FileStream(new SafeFileHandle(attrPipe.ServerHandle, false), FileAccess.Read);
  3733. attrDone = new ManualResetEvent(false);
  3734. attrTempFile = Path.GetTempFileName();
  3735. attrTempStream = new FileStream(attrTempFile, FileMode.Open, FileAccess.ReadWrite);
  3736. attrBuffer = new byte[4096];
  3737. args += "--attribute-fd " + attrPipe.ClientHandle.ToInt64().ToStringInvariant() + " ";
  3738. }
  3739. else // otherwise, attributes are not being retrieved, so we don't need them
  3740. {
  3741. attrPipe = null;
  3742. attrStream = attrTempStream = null;
  3743. attrDone = null;
  3744. attrTempFile = null;
  3745. attrBuffer = null;
  3746. }
  3747. Command command = Execute(args + searchArgs, retrieveAttributes ? StatusMessages.MixIntoStdout : StatusMessages.Ignore,
  3748. true, true);
  3749. CommandState commandState = new CommandState(command);
  3750. try
  3751. {
  3752. List<UserAttribute> attributes = new List<UserAttribute>(); // holds user attributes on the key
  3753. ProcessCommand(command, commandState,
  3754. delegate(Command cmd, CommandState state)
  3755. {
  3756. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  3757. if(retrieveAttributes)
  3758. {
  3759. cmd.StatusMessageReceived += delegate(StatusMessage msg)
  3760. {
  3761. if(msg.Type == StatusMessageType.Attribute) attrMsg = (AttributeMessage)msg;
  3762. };
  3763. // create the callback for reading the attribute stream. we have to do this in the background because it could
  3764. // block if we try to read the attribute stream in the main loop -- even if we use asynchronous IO
  3765. attrCallback = delegate(IAsyncResult result)
  3766. {
  3767. int bytesRead = attrStream.EndRead(result);
  3768. if(bytesRead == 0) // if we're at EOF, then we're done
  3769. {
  3770. attrDone.Set();
  3771. }
  3772. else // otherwise, some data was read
  3773. {
  3774. attrTempStream.Write(attrBuffer, 0, bytesRead); // so write it to the temporary output stream
  3775. attrStream.BeginRead(attrBuffer, 0, attrBuffer.Length, attrCallback, null);
  3776. }
  3777. };
  3778. }
  3779. },
  3780. delegate(Command cmd, CommandState state)
  3781. {
  3782. List<Subkey> subkeys = new List<Subkey>(); // holds the subkeys in the current primary key
  3783. List<KeySignature> sigs = new List<KeySignature>(32); // holds the signatures on the last key or user id
  3784. List<string> revokers = new List<string>(); // holds designated revokers on the key
  3785. PrimaryKey currentPrimary = null;
  3786. Subkey currentSubkey = null;
  3787. UserAttribute currentAttribute = null;
  3788. // if we're retrieving attributes, start reading the data in the background
  3789. if(retrieveAttributes) attrStream.BeginRead(attrBuffer, 0, attrBuffer.Length, attrCallback, null);
  3790. while(true)
  3791. {
  3792. string line;
  3793. cmd.ReadLine(out line);
  3794. if(line == null)
  3795. {
  3796. if(cmd.StatusMessage == null) break;
  3797. else continue;
  3798. }
  3799. // each line is a bunch of stuff separated by colons. this is documented in gpg-src\doc\DETAILS
  3800. string[] fields = line.Split(':');
  3801. switch(fields[0])
  3802. {
  3803. case "sig": case "rev": // a signature or revocation signature
  3804. KeySignature sig = new KeySignature();
  3805. if(!string.IsNullOrEmpty(fields[1]))
  3806. {
  3807. switch(fields[1][0])
  3808. {
  3809. case '!': sig.Status = SignatureStatus.Valid; break;
  3810. case '-': sig.Status = SignatureStatus.Invalid; break;
  3811. case '%': sig.Status = SignatureStatus.Error; break;
  3812. case '?': sig.Status = SignatureStatus.Unverified; break;
  3813. }
  3814. }
  3815. if(!string.IsNullOrEmpty(fields[4])) sig.KeyId = fields[4].ToUpperInvariant();
  3816. if(!string.IsNullOrEmpty(fields[5])) sig.CreationTime = GPG.ParseTimestamp(fields[5]);
  3817. if(!string.IsNullOrEmpty(fields[9])) sig.SignerName = CUnescape(fields[9]);
  3818. if(fields[10] != null && fields[10].Length >= 2)
  3819. {
  3820. string type = fields[10];
  3821. sig.Type = (OpenPGPSignatureType)GetHexValue(type[0], type[1]);
  3822. sig.Exportable = type.Length >= 3 && type[2] == 'x';
  3823. }
  3824. if(fields.Length > 12 && !string.IsNullOrEmpty(fields[12]))
  3825. {
  3826. sig.KeyFingerprint = fields[12].ToUpperInvariant();
  3827. }
  3828. sigs.Add(sig);
  3829. break;
  3830. case "uid": // user id
  3831. {
  3832. FinishAttribute(attributes, sigs, currentPrimary, currentSubkey, ref currentAttribute);
  3833. UserId userId = new UserId();
  3834. if(!string.IsNullOrEmpty(fields[1]))
  3835. {
  3836. char c = fields[1][0];
  3837. userId.CalculatedTrust = ParseTrustLevel(c);
  3838. userId.Revoked = c == 'r';
  3839. }
  3840. if(!string.IsNullOrEmpty(fields[5])) userId.CreationTime = ParseTimestamp(fields[5]);
  3841. if(!string.IsNullOrEmpty(fields[7])) userId.Id = fields[7].ToUpperInvariant();
  3842. if(!string.IsNullOrEmpty(fields[9])) userId.Name = CUnescape(fields[9]);
  3843. currentAttribute = userId;
  3844. break;
  3845. }
  3846. case "pub": case "sec": // public and secret primary keys
  3847. FinishPrimaryKey(keys, subkeys, attributes, sigs, revokers,
  3848. ref currentPrimary, ref currentSubkey, ref currentAttribute, options);
  3849. currentPrimary = new PrimaryKey();
  3850. currentPrimary.Keyring = keyring;
  3851. ReadKeyData(currentPrimary, fields);
  3852. currentPrimary.HasSecretKey = fields[0][0] == 's'; // it's secret if the field was "sec"
  3853. break;
  3854. case "sub": case "ssb": // public and secret subkeys
  3855. FinishSubkey(subkeys, sigs, currentPrimary, ref currentSubkey, currentAttribute);
  3856. currentSubkey = new Subkey();
  3857. ReadKeyData(currentSubkey, fields);
  3858. break;
  3859. case "fpr": // key fingerprint
  3860. if(currentSubkey != null) currentSubkey.Fingerprint = fields[9].ToUpperInvariant();
  3861. else if(currentPrimary != null) currentPrimary.Fingerprint = fields[9].ToUpperInvariant();
  3862. break;
  3863. case "uat": // user attribute
  3864. {
  3865. FinishAttribute(attributes, sigs, currentPrimary, currentSubkey, ref currentAttribute);
  3866. if(retrieveAttributes && attrMsg != null)
  3867. {
  3868. currentAttribute = new DummyAttribute(attrMsg);
  3869. currentAttribute.Primary = attrMsg.IsPrimary;
  3870. currentAttribute.Revoked = attrMsg.IsRevoked;
  3871. if(!string.IsNullOrEmpty(fields[1])) currentAttribute.CalculatedTrust = ParseTrustLevel(fields[1][0]);
  3872. if(!string.IsNullOrEmpty(fields[5])) currentAttribute.CreationTime = ParseTimestamp(fields[5]);
  3873. if(!string.IsNullOrEmpty(fields[7])) currentAttribute.Id = fields[7].ToUpperInvariant();
  3874. attrMsg = null;
  3875. }
  3876. break;
  3877. }
  3878. case "rvk": // a designated revoker
  3879. revokers.Add(fields[9].ToUpperInvariant());
  3880. break;
  3881. case "crt": case "crs": // X.509 certificates (we just treat them as an end to the current key)
  3882. FinishPrimaryKey(keys, subkeys, attributes, sigs, revokers,
  3883. ref currentPrimary, ref currentSubkey, ref currentAttribute, options);
  3884. break;
  3885. }
  3886. }
  3887. FinishPrimaryKey(keys, subkeys, attributes, sigs, revokers,
  3888. ref currentPrimary, ref currentSubkey, ref currentAttribute, options);
  3889. });
  3890. if(retrieveAttributes)
  3891. {
  3892. attrPipe.CloseClient(); // GPG should be done writing data now, so close its end of the pipe
  3893. attrDone.WaitOne(); // wait for us to read all the data
  3894. // the attribute data is done being read, so now we can go back and replace all the dummy attributes
  3895. attrTempStream.Position = 0;
  3896. for(int i=initialKeyCount; i<keys.Count; i++)
  3897. {
  3898. PrimaryKey key = keys[i];
  3899. if(key.Attributes.Count != 0)
  3900. {
  3901. for(int j=0; j<key.Attributes.Count; j++)
  3902. {
  3903. DummyAttribute dummy = (DummyAttribute)key.Attributes[j];
  3904. byte[] attrData = new byte[dummy.Message.Length];
  3905. if(attrTempStream.FullRead(attrData, 0, attrData.Length) != attrData.Length)
  3906. {
  3907. LogLine("Ignoring truncated attribute.");
  3908. }
  3909. else
  3910. {
  3911. UserAttribute real = UserAttribute.Create(dummy.Message.AttributeType, attrData);
  3912. real.CalculatedTrust = dummy.CalculatedTrust;
  3913. real.CreationTime = dummy.CreationTime;
  3914. real.Id = dummy.Id;
  3915. real.PrimaryKey = dummy.PrimaryKey;
  3916. real.Primary = dummy.Primary;
  3917. real.Revoked = dummy.Revoked;
  3918. real.Signatures = dummy.Signatures;
  3919. real.MakeReadOnly();
  3920. attributes.Add(real);
  3921. }
  3922. }
  3923. key.Attributes = attributes.Count == 0 ?
  3924. NoAttributes : new ReadOnlyListWrapper<UserAttribute>(attributes.ToArray());
  3925. attributes.Clear();
  3926. }
  3927. }
  3928. }
  3929. }
  3930. finally
  3931. {
  3932. if(retrieveAttributes)
  3933. {
  3934. attrDone.Close();
  3935. attrStream.Dispose();
  3936. attrPipe.Dispose();
  3937. attrTempStream.Close();
  3938. File.Delete(attrTempFile);
  3939. }
  3940. }
  3941. if(!command.SuccessfulExit)
  3942. {
  3943. // if we're searching, we don't want to throw an exception just because GPG didn't find what we were searching
  3944. // for. so only throw if we're not searching or there's a failure reason besides a missing key
  3945. if(string.IsNullOrEmpty(searchArgs) ||
  3946. (commandState.FailureReasons & ~(FailureReason.KeyNotFound | FailureReason.MissingSecretKey |
  3947. FailureReason.MissingPublicKey)) != 0)
  3948. {
  3949. throw new KeyRetrievalFailedException(commandState.FailureReasons);
  3950. }
  3951. }
  3952. }
  3953. /// <summary>Creates and returns a new <see cref="ProcessStartInfo"/> for the given GPG executable and arguments.</summary>
  3954. ProcessStartInfo GetProcessStartInfo(string exePath, string args)
  3955. {
  3956. ProcessStartInfo psi = new ProcessStartInfo();
  3957. // enable or disable the GPG agent on GPG 1.x, but on GPG 2.x, the agent is always enabled...
  3958. psi.Arguments = (gpgVersion >= 20000 ? null : EnableGPGAgent ? "--use-agent " : "--no-use-agent ") +
  3959. "--no-tty --no-options --display-charset utf-8 " + args;
  3960. psi.CreateNoWindow = true;
  3961. psi.ErrorDialog = false;
  3962. psi.FileName = exePath;
  3963. psi.RedirectStandardError = true;
  3964. psi.RedirectStandardInput = true;
  3965. psi.RedirectStandardOutput = true;
  3966. psi.StandardErrorEncoding = Encoding.UTF8;
  3967. psi.StandardOutputEncoding = Encoding.UTF8;
  3968. psi.UseShellExecute = false;
  3969. return psi;
  3970. }
  3971. /// <summary>Executes a command and collects import-related information.</summary>
  3972. ImportedKey[] ImportCore(Command command, Stream source, out CommandState commandState)
  3973. {
  3974. // GPG sometimes sends multiple messages for a single key, for instance when the key has several subkeys or a
  3975. // secret portion. so we'll keep track of how fingerprints map to ImportedKey objects, so we'll know whether to
  3976. // modify the existing object or create a new one
  3977. Dictionary<string, ImportedKey> keysByFingerprint = new Dictionary<string, ImportedKey>();
  3978. // we want to return keys in the order they were processed, so we'll keep this ordered list of fingerprints
  3979. List<string> fingerprintsSeen = new List<string>();
  3980. commandState = ProcessCommand(command,
  3981. delegate(Command cmd, CommandState state)
  3982. {
  3983. cmd.StandardErrorLine += delegate(string line) { DefaultStandardErrorHandler(line, state); };
  3984. cmd.StatusMessageReceived += delegate(StatusMessage msg)
  3985. {
  3986. if(msg.Type == StatusMessageType.ImportOkay)
  3987. {
  3988. KeyImportOkayMessage m = (KeyImportOkayMessage)msg;
  3989. ImportedKey key;
  3990. if(!keysByFingerprint.TryGetValue(m.Fingerprint, out key))
  3991. {
  3992. key = new ImportedKey();
  3993. key.Fingerprint = m.Fingerprint;
  3994. key.Successful = true;
  3995. keysByFingerprint[key.Fingerprint] = key;
  3996. fingerprintsSeen.Add(key.Fingerprint);
  3997. }
  3998. if((m.Reason & KeyImportReason.ContainsSecretKey) != 0) key.Secret = true;
  3999. }
  4000. else if(msg.Type == StatusMessageType.ImportProblem)
  4001. {
  4002. KeyImportFailedMessage m = (KeyImportFailedMessage)msg;
  4003. ImportedKey key;
  4004. if(!keysByFingerprint.TryGetValue(m.Fingerprint, out key))
  4005. {
  4006. key = new ImportedKey();
  4007. key.Fingerprint = m.Fingerprint;
  4008. keysByFingerprint[key.Fingerprint] = key;
  4009. fingerprintsSeen.Add(key.Fingerprint);
  4010. }
  4011. key.Successful = false;
  4012. }
  4013. else { DefaultStatusMessageHandler(msg, state); }
  4014. };
  4015. },
  4016. delegate(Command cmd, CommandState state)
  4017. {
  4018. if(source != null) WriteStreamToProcess(source, cmd.Process);
  4019. });
  4020. ImportedKey[] keysProcessed = new ImportedKey[fingerprintsSeen.Count];
  4021. for(int i=0; i<keysProcessed.Length; i++)
  4022. {
  4023. keysProcessed[i] = keysByFingerprint[fingerprintsSeen[i]];
  4024. keysProcessed[i].MakeReadOnly();
  4025. }
  4026. return keysProcessed;
  4027. }
  4028. /// <summary>Performs the main work for key server operations.</summary>
  4029. ImportedKey[] KeyServerCore(string args, string name, bool isImport, bool canKill)
  4030. {
  4031. CommandState state;
  4032. Command cmd = Execute(args, StatusMessages.ReadInBackground, true, canKill);
  4033. ImportedKey[] keys = ImportCore(cmd, null, out state);
  4034. // during a keyring refresh, it's very likely that one of the keys won't be found on a keyserver, but we don't want
  4035. // to throw an exception unless no keys were refreshed or we got a failure reason other than BadData or KeyNotFound
  4036. if(!cmd.SuccessfulExit &&
  4037. (!string.Equals(name, "Keyring refresh", StringComparison.Ordinal) || keys.Length == 0 ||
  4038. (state.FailureReasons & ~(FailureReason.KeyNotFound | FailureReason.BadData)) != 0))
  4039. {
  4040. throw new KeyServerFailedException(name + " failed.", state.FailureReasons);
  4041. }
  4042. return keys;
  4043. }
  4044. /// <summary>Sends a line to the log if logging is enabled.</summary>
  4045. void LogLine(string line)
  4046. {
  4047. if(LineLogged != null) LineLogged(line);
  4048. }
  4049. CommandState ProcessCommand(Command cmd, CommandProcessor initCommand)
  4050. {
  4051. return ProcessCommand(cmd, initCommand, null);
  4052. }
  4053. CommandState ProcessCommand(Command cmd, CommandProcessor initCommand, CommandProcessor onStarted)
  4054. {
  4055. CommandState state = new CommandState(cmd);
  4056. ProcessCommand(cmd, state, initCommand, onStarted);
  4057. return state;
  4058. }
  4059. void ProcessCommand(Command cmd, CommandState state, CommandProcessor initCommand, CommandProcessor onStarted)
  4060. {
  4061. if(cmd == null || state == null) throw new ArgumentNullException();
  4062. try
  4063. {
  4064. if(initCommand != null) initCommand(cmd, state);
  4065. cmd.Start();
  4066. if(onStarted != null) onStarted(cmd, state);
  4067. cmd.WaitForExit();
  4068. }
  4069. catch(Exception ex)
  4070. {
  4071. if(cmd.KillProcessOnAbort) cmd.Kill();
  4072. if(!(ex is ThreadAbortException)) throw;
  4073. }
  4074. finally
  4075. {
  4076. cmd.Dispose();
  4077. }
  4078. }
  4079. /// <summary>Edits each key given with a single edit command.</summary>
  4080. void RepeatedRawEditCommand(PrimaryKey[] keys, string command)
  4081. {
  4082. EditKeys(keys, delegate { return new RawCommand(command); });
  4083. }
  4084. /// <summary>Does the work of revoking keys, either directly or via a designated revoker.</summary>
  4085. void RevokeKeysCore(PrimaryKey designatedRevoker, KeyRevocationReason reason, PrimaryKey[] keysToRevoke)
  4086. {
  4087. if(keysToRevoke == null) throw new ArgumentNullException();
  4088. foreach(PrimaryKey key in keysToRevoke)
  4089. {
  4090. if(key == null || string.IsNullOrEmpty(key.Fingerprint))
  4091. {
  4092. throw new ArgumentException("A key was null or had no fingerprint.");
  4093. }
  4094. }
  4095. MemoryStream ms = new MemoryStream();
  4096. foreach(PrimaryKey key in keysToRevoke)
  4097. {
  4098. if(!key.Revoked)
  4099. {
  4100. if(designatedRevoker == null) GenerateRevocationCertificate(key, ms, reason, null);
  4101. else GenerateRevocationCertificate(key, designatedRevoker, ms, reason, null);
  4102. ms.Position = 0;
  4103. ImportKeys(ms, key.Keyring);
  4104. ms.Position = 0;
  4105. ms.SetLength(0);
  4106. }
  4107. }
  4108. }
  4109. /// <summary>Gets a key password from the user and sends it to the command stream. Returns true if a password was
  4110. /// given and false if not, although if 'passwordRequired' is true, an exception will be throw if a password is not
  4111. /// given.
  4112. /// </summary>
  4113. bool SendKeyPassword(Command command, string passwordHint, NeedKeyPassphraseMessage msg)
  4114. {
  4115. string userIdHint = passwordHint + " (0x" + msg.KeyId;
  4116. if(!string.Equals(msg.KeyId, msg.PrimaryKeyId, StringComparison.Ordinal))
  4117. {
  4118. userIdHint += " on primary key 0x" + msg.PrimaryKeyId;
  4119. }
  4120. userIdHint += ")";
  4121. SecureString password = GetKeyPassword(msg.KeyId, userIdHint);
  4122. if(password == null)
  4123. {
  4124. command.SendLine();
  4125. return false;
  4126. }
  4127. else
  4128. {
  4129. command.SendPassword(password, true);
  4130. return true;
  4131. }
  4132. }
  4133. /// <summary>Performs the work of verifying either a detached or embedded signature.</summary>
  4134. Signature[] VerifyCore(string signatureFile, Stream signedData, VerificationOptions options)
  4135. {
  4136. string args = GetVerificationArgs(options, false);
  4137. // --verify takes either one or two arguments. we want the signed data to be sent on STDIN
  4138. args += "--verify " + (signatureFile == null ? "-" : EscapeArg(signatureFile) + " -");
  4139. Command cmd = Execute(args, StatusMessages.ReadInBackground, false);
  4140. return DecryptVerifyCore(cmd, signedData, FileStream.Null, null);
  4141. }
  4142. /// <summary>Determines whether the string contains control characters.</summary>
  4143. static bool ContainsControlCharacters(string str)
  4144. {
  4145. if(str != null)
  4146. {
  4147. foreach(char c in str)
  4148. {
  4149. if(c < ' ' || char.IsControl(c)) return true;
  4150. }
  4151. }
  4152. return false;
  4153. }
  4154. /// <summary>Performs C-unescaping on the given string, which has special characters encoded as <c>\xHH</c>, where
  4155. /// <c>HH</c> are the hex digits of the character.
  4156. /// </summary>
  4157. static string CUnescape(string str)
  4158. {
  4159. return cEscapeRe.Replace(str, delegate(Match m)
  4160. {
  4161. return new string((char)GetHexValue(m.Value[2], m.Value[3]), 1);
  4162. });
  4163. }
  4164. /// <summary>Performs default handling for lines of text read from STDERR.</summary>
  4165. static void DefaultStandardErrorHandler(string line, CommandState state)
  4166. {
  4167. // this is such a messy way to detect errors, but what else can we do?
  4168. if((state.FailureReasons & FailureReason.KeyringLocked) == 0 &&
  4169. (line.IndexOf(" file write error", StringComparison.Ordinal) != -1 ||
  4170. line.IndexOf(" file rename error", StringComparison.Ordinal) != -1))
  4171. {
  4172. state.FailureReasons |= FailureReason.KeyringLocked;
  4173. }
  4174. else if((state.FailureReasons & FailureReason.KeyNotFound) == 0 &&
  4175. line.IndexOf(" not found on keyserver", StringComparison.Ordinal) != -1)
  4176. {
  4177. state.FailureReasons |= FailureReason.KeyNotFound;
  4178. }
  4179. else if((state.FailureReasons & (FailureReason.KeyNotFound | FailureReason.MissingPublicKey)) !=
  4180. (FailureReason.KeyNotFound | FailureReason.MissingPublicKey) &&
  4181. line.IndexOf(" public key not found", StringComparison.Ordinal) != -1)
  4182. {
  4183. state.FailureReasons |= FailureReason.KeyNotFound | FailureReason.MissingPublicKey;
  4184. }
  4185. else if((state.FailureReasons & FailureReason.SecretKeyAlreadyExists) == 0 &&
  4186. line.IndexOf(" already in secret keyring", StringComparison.Ordinal) != -1)
  4187. {
  4188. state.FailureReasons |= FailureReason.SecretKeyAlreadyExists;
  4189. }
  4190. else if((state.FailureReasons & FailureReason.MissingSecretKey) == 0 &&
  4191. line.Equals("Need the secret key to do this.", StringComparison.Ordinal))
  4192. {
  4193. state.FailureReasons |= FailureReason.MissingSecretKey;
  4194. }
  4195. else if((state.FailureReasons & FailureReason.NoKeyServer) == 0 &&
  4196. line.IndexOf(" no keyserver known", StringComparison.Ordinal) != -1)
  4197. {
  4198. state.FailureReasons |= FailureReason.NoKeyServer;
  4199. }
  4200. else if((state.FailureReasons & FailureReason.BadKeyServerUri) == 0 &&
  4201. line.IndexOf(" bad URI", StringComparison.Ordinal) != -1)
  4202. {
  4203. state.FailureReasons |= FailureReason.BadKeyServerUri;
  4204. }
  4205. else if((state.FailureReasons & FailureReason.MissingSecretKey) == 0) // handle: secret key "Foo" not found
  4206. {
  4207. int index = line.IndexOf("secret key \"", StringComparison.Ordinal);
  4208. if(index != -1 && index < line.IndexOf("\" not found")) state.FailureReasons |= FailureReason.MissingSecretKey;
  4209. }
  4210. }
  4211. /// <summary>Exits a process by closing STDIN, STDOUT, and STDERR, and waiting for it to exit. If it doesn't exit
  4212. /// within a short period, it will be killed. Returns the process' exit code.
  4213. /// </summary>
  4214. static int Exit(Process process)
  4215. {
  4216. process.StandardInput.Close();
  4217. process.StandardOutput.Close();
  4218. process.StandardError.Close();
  4219. if(!process.WaitForExit(500))
  4220. {
  4221. process.Kill();
  4222. process.WaitForExit(100);
  4223. }
  4224. return process.ExitCode;
  4225. }
  4226. /// <summary>Escapes a command-line argument or throws an exception if it cannot be escaped.</summary>
  4227. static string EscapeArg(string arg)
  4228. {
  4229. if(arg == null) throw new ArgumentNullException();
  4230. if(ContainsControlCharacters(arg))
  4231. {
  4232. throw new ArgumentException("Argument '"+arg+"' contains illegal control characters.");
  4233. }
  4234. // this is almost the ugliest escaping algorithm ever, beaten only by the algorithm needed to escape cmd.exe
  4235. // commands. and it's not even guaranteed to work because it's actually IMPOSSIBLE to properly escape command-line
  4236. // arguments on Windows.
  4237. //
  4238. // first we need to escape double quotes with backslashes. but if those quotes have backslashes before them, then
  4239. // we need to escape all of those backslashes too. (ie, if a run of backslashes is followed by a quote, all the
  4240. // backslashes and the quote must be escaped, but if a run of backslashes isn't followed by a quote, the
  4241. // backslashes must NOT be escaped. this means we need an arbitrary amount of look-ahead.) finally, if it contains
  4242. // a space or tab, or is zero-length, it must be wrapped with double quotes. and after all that, it's not
  4243. // guaranteed to work because it relies on the assumption that the target program will use CommandLineToArgvW to
  4244. // parse its command line, which not all programs do. but GPG does, at least for the moment, so we should be okay.
  4245. //
  4246. // thank goodness we're not invoking cmd.exe. if we were, the task would be an order of magnitude more arcane.
  4247. //
  4248. // this all stems from the original, stupid decision in Windows to make programs parse their own command line. the
  4249. // command line is from the USER INTERFACE level (ie, from the shell). it's what the user types in. but what
  4250. // programs want to have is a list of arguments, not the raw text that the user typed! the shell receives the
  4251. // command line, so the shell should parse it! the fact that the .NET standard library copied this insanity is what
  4252. // really annoys me. ProcessStartInfo.Arguments should be a string array of unescaped arguments. c'mon!!
  4253. // we have to wrap the argument with double quotes if it contains a space or a tab or is zero-length
  4254. bool mustWrapWithQuotes = arg.IndexOf(' ') != -1 || arg.IndexOf('\t') != -1 || arg.Length == 0;
  4255. // if we don't have to wrap it with quotes, and it also doesn't contain any quotes, then we don't need to escape it
  4256. if(!mustWrapWithQuotes && arg.IndexOf('"') == -1) return arg;
  4257. StringBuilder escaped = new StringBuilder(arg.Length + 10);
  4258. if(mustWrapWithQuotes) escaped.Append('"');
  4259. int start = 0;
  4260. while(start < arg.Length)
  4261. {
  4262. // if there's a run of backslashes starting here, we need to find the end of it
  4263. int bsEnd = start;
  4264. while(bsEnd < arg.Length && arg[bsEnd] == '\\') bsEnd++;
  4265. if(bsEnd == start) // there was no run of backslashes
  4266. {
  4267. if(arg[start] == '"') escaped.Append("\\\""); // but there was a double quote, so we need to escape it
  4268. else escaped.Append(arg[start]);
  4269. start++;
  4270. }
  4271. else // there was one or more backslashes. we need to see if it ends (or will end) with a quote
  4272. {
  4273. // it ends with a quote, or will end with one after we wrap the argument with quotes
  4274. int bsCount = bsEnd - start;
  4275. if(bsEnd == arg.Length && mustWrapWithQuotes || arg[bsEnd] == '"')
  4276. {
  4277. bsCount *= 2; // double the number of backslashes
  4278. }
  4279. escaped.Append('\\', bsCount);
  4280. start = bsEnd; // continue with the character after the backslashes
  4281. }
  4282. }
  4283. if(mustWrapWithQuotes) escaped.Append('"');
  4284. return escaped.ToString();
  4285. }
  4286. /// <summary>A helper for reading key listings, that finishes the current primary key.</summary>
  4287. static void FinishPrimaryKey(List<PrimaryKey> keys, List<Subkey> subkeys, List<UserAttribute> attributes,
  4288. List<KeySignature> sigs, List<string> revokers,ref PrimaryKey currentPrimary,
  4289. ref Subkey currentSubkey, ref UserAttribute currentAttribute, ListOptions options)
  4290. {
  4291. // finishing a primary key finishes all signatures, subkeys, and user IDs on it
  4292. FinishSignatures(sigs, currentPrimary, currentSubkey, currentAttribute);
  4293. FinishSubkey(subkeys, sigs, currentPrimary, ref currentSubkey, currentAttribute);
  4294. FinishAttribute(attributes, sigs, currentPrimary, currentSubkey, ref currentAttribute);
  4295. if(currentPrimary != null)
  4296. {
  4297. currentPrimary.Subkeys = new ReadOnlyListWrapper<Subkey>(subkeys.ToArray());
  4298. // the attributes will be split into UserIds and other attributes
  4299. List<UserId> userIds = new List<UserId>(attributes.Count);
  4300. List<UserAttribute> userAttributes = new List<UserAttribute>();
  4301. foreach(UserAttribute attr in attributes)
  4302. {
  4303. UserId userId = attr as UserId;
  4304. if(userId != null) userIds.Add(userId);
  4305. else userAttributes.Add(attr);
  4306. }
  4307. currentPrimary.UserIds = new ReadOnlyListWrapper<UserId>(userIds.ToArray());
  4308. currentPrimary.Attributes = userAttributes.Count == 0 ?
  4309. NoAttributes : new ReadOnlyListWrapper<UserAttribute>(userAttributes.ToArray());
  4310. currentPrimary.DesignatedRevokers = revokers.Count == 0 ?
  4311. NoRevokers : new ReadOnlyListWrapper<string>(revokers.ToArray());
  4312. if(currentPrimary.Signatures == null) currentPrimary.Signatures = NoSignatures;
  4313. // we don't make the primary key read only here because a bug in GPG causes us to not know the real attributes
  4314. // until the process exits. so we'll make the key read only later. also, we may need to set .HasSecretKey later.
  4315. // GetKeys() will do it for us.
  4316. // filter out unusable keys if that was asked of us. we can't use PrimaryKey.Usable because whether we care about
  4317. // TotalCapabilities depends on the options
  4318. if((options & ListOptions.IgnoreUnusableKeys) == 0 ||
  4319. !currentPrimary.Disabled && !currentPrimary.Expired && !currentPrimary.Revoked &&
  4320. (currentPrimary.TotalCapabilities != KeyCapabilities.None || (options & ListOptions.SecretKeyMask) != 0))
  4321. {
  4322. keys.Add(currentPrimary);
  4323. }
  4324. currentPrimary = null;
  4325. }
  4326. subkeys.Clear();
  4327. attributes.Clear();
  4328. revokers.Clear();
  4329. }
  4330. /// <summary>A helper for reading key listings, that finishes the current key signatures.</summary>
  4331. static void FinishSignatures(List<KeySignature> sigs, PrimaryKey currentPrimary, Subkey currentSubkey,
  4332. UserAttribute currentAttribute)
  4333. {
  4334. ReadOnlyListWrapper<KeySignature> list = new ReadOnlyListWrapper<KeySignature>(sigs.ToArray());
  4335. // add the signatures to the most recent object in the key listing
  4336. ISignableObject signedObject = null;
  4337. if(currentAttribute != null)
  4338. {
  4339. if(currentAttribute.Signatures == null) // only set the signatures if they're not set already
  4340. {
  4341. currentAttribute.Signatures = list;
  4342. signedObject = currentAttribute;
  4343. }
  4344. }
  4345. else if(currentSubkey != null)
  4346. {
  4347. if(currentSubkey.Signatures == null) // only set the signatures if they're not set already
  4348. {
  4349. currentSubkey.Signatures = list;
  4350. signedObject = currentSubkey;
  4351. }
  4352. }
  4353. else if(currentPrimary != null)
  4354. {
  4355. if(currentPrimary.Signatures == null) // only set the signatures if they're not set already
  4356. {
  4357. currentPrimary.Signatures = list;
  4358. signedObject = currentPrimary;
  4359. }
  4360. }
  4361. if(signedObject != null)
  4362. {
  4363. foreach(KeySignature sig in list)
  4364. {
  4365. sig.Object = signedObject;
  4366. sig.MakeReadOnly();
  4367. }
  4368. }
  4369. sigs.Clear();
  4370. }
  4371. /// <summary>A helper for reading key listings, that finishes the current subkey.</summary>
  4372. static void FinishSubkey(List<Subkey> subkeys, List<KeySignature> sigs,
  4373. PrimaryKey currentPrimary, ref Subkey currentSubkey, UserAttribute currentAttribute)
  4374. {
  4375. FinishSignatures(sigs, currentPrimary, currentSubkey, currentAttribute);
  4376. if(currentSubkey != null && currentPrimary != null)
  4377. {
  4378. currentSubkey.PrimaryKey = currentPrimary;
  4379. if(currentSubkey.Signatures == null) currentSubkey.Signatures = NoSignatures;
  4380. currentSubkey.MakeReadOnly();
  4381. subkeys.Add(currentSubkey);
  4382. currentSubkey = null;
  4383. }
  4384. }
  4385. /// <summary>A helper for reading key listings, that finishes the current user attribute.</summary>
  4386. static void FinishAttribute(List<UserAttribute> attributes, List<KeySignature> sigs,
  4387. PrimaryKey currentPrimary, Subkey currentSubkey, ref UserAttribute currentAttribute)
  4388. {
  4389. FinishSignatures(sigs, currentPrimary, currentSubkey, currentAttribute);
  4390. if(currentAttribute != null && currentPrimary != null)
  4391. {
  4392. currentAttribute.PrimaryKey = currentPrimary;
  4393. if(currentAttribute is UserId) // the primary user ID is the first one listed
  4394. {
  4395. foreach(UserAttribute attr in attributes)
  4396. {
  4397. if(attr is UserId) goto notPrimary;
  4398. }
  4399. currentAttribute.Primary = true;
  4400. notPrimary:;
  4401. }
  4402. if(currentAttribute.Signatures == null) currentAttribute.Signatures = NoSignatures;
  4403. currentAttribute.MakeReadOnly();
  4404. attributes.Add(currentAttribute);
  4405. currentAttribute = null;
  4406. }
  4407. }
  4408. /// <summary>Given a list of fingerprints, returns a string containing the fingerprints of each, separated by spaces.</summary>
  4409. static string GetFingerprintArgs(IEnumerable<PrimaryKey> keys)
  4410. {
  4411. return GetFingerprintArgs(keys, null);
  4412. }
  4413. static string GetFingerprintArgs(IEnumerable<PrimaryKey> keys, string prefix)
  4414. {
  4415. if(!string.IsNullOrEmpty(prefix)) prefix += " ";
  4416. string args = null;
  4417. foreach(PrimaryKey key in keys)
  4418. {
  4419. if(key == null) throw new ArgumentException("A key was null.");
  4420. if(string.IsNullOrEmpty(key.Fingerprint))
  4421. {
  4422. throw new ArgumentException("The key " + key.ToString() + " had no fingerprint.");
  4423. }
  4424. args += prefix + key.Fingerprint + " ";
  4425. }
  4426. return args;
  4427. }
  4428. /// <summary>Converts a hex digit into its integer value.</summary>
  4429. static int GetHexValue(char c)
  4430. {
  4431. if(c >= '0' && c <= '9') return c-'0';
  4432. else
  4433. {
  4434. c = char.ToLowerInvariant(c);
  4435. if(c >= 'a' && c <= 'f') return c-'a'+10;
  4436. }
  4437. throw new ArgumentException("'"+c.ToString()+"' is not a hex digit.");
  4438. }
  4439. /// <summary>Converts two hex digits into their combined integer value.</summary>
  4440. static int GetHexValue(char high, char low)
  4441. {
  4442. return (GetHexValue(high)<<4) + GetHexValue(low);
  4443. }
  4444. /// <summary>Gets an expiration date in days from now, or zero if the key should not expire.</summary>
  4445. static int GetExpirationDays(DateTime? expiration)
  4446. {
  4447. int expirationDays = 0;
  4448. if(expiration.HasValue)
  4449. {
  4450. DateTime utcExpiration = expiration.Value.ToUniversalTime(); // the date should be in UTC
  4451. // give us 30 seconds of fudge time so the key doesn't expire between now and when we run GPG
  4452. if(utcExpiration <= DateTime.UtcNow.AddSeconds(30))
  4453. {
  4454. throw new ArgumentException("The key expiration date must be in the future.");
  4455. }
  4456. // GPG supports expiration dates in two formats: absolute dates and times relative to the current time.
  4457. // but it only supports absolute dates up to 2038, so we have to use a relative time format (days from now)
  4458. expirationDays = (int)Math.Ceiling((utcExpiration - DateTime.UtcNow.Date).TotalDays);
  4459. }
  4460. return expirationDays;
  4461. }
  4462. /// <summary>Creates GPG arguments to represent the given <see cref="ExportOptions"/>.</summary>
  4463. static string GetExportArgs(ExportOptions options, bool exportSecretKeys, bool addExportCommand)
  4464. {
  4465. string args = null;
  4466. if(options != ExportOptions.Default)
  4467. {
  4468. args += "--export-options \"";
  4469. if((options & ExportOptions.CleanKeys) != 0) args += "export-clean ";
  4470. if((options & ExportOptions.ExcludeAttributes) != 0) args += "no-export-attributes ";
  4471. if((options & ExportOptions.ExportLocalSignatures) != 0) args += "export-local-sigs ";
  4472. if((options & ExportOptions.ExportSensitiveRevokerInfo) != 0) args += "export-sensitive-revkeys ";
  4473. if((options & ExportOptions.MinimizeKeys) != 0) args += "export-minimize ";
  4474. if((options & ExportOptions.ResetSubkeyPassword) != 0) args += "export-reset-subkey-passwd ";
  4475. args += "\" ";
  4476. }
  4477. if(addExportCommand)
  4478. {
  4479. if(exportSecretKeys)
  4480. {
  4481. args += (options & ExportOptions.ClobberPrimarySecretKey) != 0 ?
  4482. "--export-secret-subkeys " : "--export-secret-keys ";
  4483. }
  4484. else args += "--export "; // exporting public keys
  4485. }
  4486. return args;
  4487. }
  4488. /// <summary>Creates GPG arguments to represent the given keyring.</summary>
  4489. static string GetKeyringArgs(Keyring keyring, bool secretKeyringFile)
  4490. {
  4491. string args = null;
  4492. if(keyring != null)
  4493. {
  4494. args += "--no-default-keyring --keyring " + EscapeArg(NormalizeKeyringFile(keyring.PublicFile)) + " ";
  4495. if(secretKeyringFile && keyring.SecretFile != null)
  4496. {
  4497. args += "--secret-keyring " + EscapeArg(NormalizeKeyringFile(keyring.SecretFile)) + " ";
  4498. }
  4499. if(keyring.TrustDbFile != null)
  4500. {
  4501. args += "--trustdb-name " + EscapeArg(NormalizeKeyringFile(keyring.TrustDbFile)) + " ";
  4502. }
  4503. }
  4504. return args;
  4505. }
  4506. /// <summary>Creates GPG arguments to represent the given <see cref="ExportOptions"/>.</summary>
  4507. static string GetImportArgs(Keyring keyring, ImportOptions options)
  4508. {
  4509. string args = GetKeyringArgs(keyring, true);
  4510. if(keyring != null)
  4511. {
  4512. // add the --primary-keyring option so that GPG will import into the keyrings we've given it
  4513. args += "--primary-keyring " + EscapeArg(NormalizeKeyringFile(keyring.PublicFile)) + " ";
  4514. }
  4515. if(options != ImportOptions.Default)
  4516. {
  4517. args += "--import-options \"";
  4518. if((options & ImportOptions.CleanKeys) != 0) args += "import-clean ";
  4519. if((options & ImportOptions.ImportLocalSignatures) != 0) args += "import-local-sigs ";
  4520. if((options & ImportOptions.MergeOnly) != 0) args += "merge-only ";
  4521. if((options & ImportOptions.MinimizeKeys) != 0) args += "import-minimize ";
  4522. args += "\" ";
  4523. }
  4524. return args;
  4525. }
  4526. /// <summary>Given a set of key capabilities, returns the key usage string expected by GPG during key creation.</summary>
  4527. static string GetKeyUsageString(KeyCapabilities caps)
  4528. {
  4529. // don't check for the Certify flag because GPG doesn't allow us to use it anyway
  4530. List<string> capList = new List<string>();
  4531. if((caps & KeyCapabilities.Authenticate) != 0) capList.Add("auth");
  4532. if((caps & KeyCapabilities.Encrypt) != 0) capList.Add("encrypt");
  4533. if((caps & KeyCapabilities.Sign) != 0) capList.Add("sign");
  4534. return string.Join(" ", capList.ToArray());
  4535. }
  4536. /// <summary>Creates GPG arguments to represent the given keyrings.</summary>
  4537. static string GetKeyringArgs(IEnumerable<Keyring> keyrings, bool ignoreDefaultKeyring, bool wantSecretKeyrings)
  4538. {
  4539. string args = null, trustDb = null;
  4540. bool trustDbSet = false;
  4541. if(keyrings != null)
  4542. {
  4543. foreach(Keyring keyring in keyrings)
  4544. {
  4545. string thisTrustDb = keyring == null ? null : NormalizeKeyringFile(keyring.TrustDbFile);
  4546. if(!trustDbSet)
  4547. {
  4548. trustDb = thisTrustDb;
  4549. trustDbSet = true;
  4550. }
  4551. else if(!string.Equals(trustDb, thisTrustDb, StringComparison.Ordinal))
  4552. {
  4553. throw new ArgumentException("Trust databases cannot be mixed in the same command. The two databases were "+
  4554. trustDb + " and " + thisTrustDb);
  4555. }
  4556. if(keyring != null)
  4557. {
  4558. args += "--keyring " + EscapeArg(NormalizeKeyringFile(keyring.PublicFile)) + " ";
  4559. if(wantSecretKeyrings && keyring.SecretFile != null)
  4560. {
  4561. args += "--secret-keyring " + EscapeArg(NormalizeKeyringFile(keyring.SecretFile)) + " ";
  4562. }
  4563. }
  4564. }
  4565. }
  4566. if(ignoreDefaultKeyring)
  4567. {
  4568. if(args == null)
  4569. {
  4570. throw new ArgumentException("The default keyring is being ignored, but no valid keyrings were given.");
  4571. }
  4572. args += "--no-default-keyring ";
  4573. }
  4574. if(trustDb != null) args += "--trustdb-name " + EscapeArg(trustDb) + " ";
  4575. return args;
  4576. }
  4577. /// <summary>Returns keyring arguments for all of the given keys.</summary>
  4578. static string GetKeyringArgs(IEnumerable<PrimaryKey> keys, bool secretKeyrings)
  4579. {
  4580. string args = null, trustDb = null;
  4581. bool trustDbSet = false, overrideDefaultKeyring = true;
  4582. if(keys != null)
  4583. {
  4584. // keep track of which public and secret keyring files have been seen so we don't add them twice
  4585. Dictionary<string, object> publicFiles = new Dictionary<string, object>(StringComparer.Ordinal);
  4586. Dictionary<string, object> secretFiles = new Dictionary<string, object>(StringComparer.Ordinal);
  4587. foreach(Key key in keys)
  4588. {
  4589. if(key == null) throw new ArgumentException("A key was null.");
  4590. string thisTrustDb = key.Keyring == null ? null : NormalizeKeyringFile(key.Keyring.TrustDbFile);
  4591. if(!trustDbSet)
  4592. {
  4593. trustDb = thisTrustDb;
  4594. trustDbSet = true;
  4595. }
  4596. else if(!string.Equals(trustDb, thisTrustDb, StringComparison.Ordinal))
  4597. {
  4598. throw new ArgumentException("Trust databases cannot be mixed in the same command. The two databases were "+
  4599. trustDb + " and " + thisTrustDb);
  4600. }
  4601. if(key.Keyring == null)
  4602. {
  4603. overrideDefaultKeyring = false;
  4604. }
  4605. else if(secretKeyrings && key.Keyring.SecretFile == null)
  4606. {
  4607. throw new ArgumentException("Keyring " + key.Keyring.ToString() + " on key " + key.ToString() +
  4608. " has no secret portion.");
  4609. }
  4610. else
  4611. {
  4612. string publicFile = NormalizeKeyringFile(key.Keyring.PublicFile);
  4613. string secretFile = key.Keyring.SecretFile == null ? null : NormalizeKeyringFile(key.Keyring.SecretFile);
  4614. if(!publicFiles.ContainsKey(publicFile))
  4615. {
  4616. publicFiles[publicFile] = null;
  4617. args += "--keyring " + publicFile + " ";
  4618. }
  4619. if(secretKeyrings && !secretFiles.ContainsKey(secretFile))
  4620. {
  4621. secretFiles[secretFile] = null;
  4622. args += "--secret-keyring " + secretFile + " ";
  4623. }
  4624. }
  4625. }
  4626. // if we added any keys, args will be non-null
  4627. if(overrideDefaultKeyring && args != null) args += "--no-default-keyring ";
  4628. // if we're using a non-default trust database, reference it
  4629. if(trustDb != null) args += "--trustdb-name " + EscapeArg(trustDb) + " ";
  4630. }
  4631. return args;
  4632. }
  4633. /// <summary>Creates GPG arguments to represent the given <see cref="KeyServerOptions"/>.</summary>
  4634. static string GetKeyServerArgs(KeyServerOptions options, bool requireKeyServer)
  4635. {
  4636. if(requireKeyServer)
  4637. {
  4638. if(options == null) throw new ArgumentNullException();
  4639. if(options.KeyServer == null) throw new ArgumentException("No key server was specified.");
  4640. }
  4641. string args = null;
  4642. if(options != null)
  4643. {
  4644. if(options.KeyServer != null) args += "--keyserver " + EscapeArg(options.KeyServer.AbsoluteUri) + " ";
  4645. if(options.HttpProxy != null || options.Timeout != 0)
  4646. {
  4647. string optStr = null;
  4648. if(options.HttpProxy != null) optStr += "http-proxy=" + options.HttpProxy.AbsoluteUri + " ";
  4649. if(options.Timeout != 0) optStr += "timeout=" + options.Timeout.ToStringInvariant() + " ";
  4650. args += "--keyserver-options " + EscapeArg(optStr) + " ";
  4651. }
  4652. }
  4653. return args;
  4654. }
  4655. /// <summary>Creates GPG arguments to represent the given <see cref="OutputOptions"/>.</summary>
  4656. static string GetOutputArgs(OutputOptions options)
  4657. {
  4658. string args = null;
  4659. if(options != null)
  4660. {
  4661. if(options.Format == OutputFormat.ASCII) args += "-a ";
  4662. foreach(string comment in options.Comments)
  4663. {
  4664. if(!string.IsNullOrEmpty(comment)) args += "--comment " + EscapeArg(comment) + " ";
  4665. }
  4666. }
  4667. return args;
  4668. }
  4669. /// <summary>Creates GPG arguments to represent the given <see cref="VerificationOptions"/>.</summary>
  4670. static string GetVerificationArgs(VerificationOptions options, bool wantSecretKeyrings)
  4671. {
  4672. string args = null;
  4673. if(options != null)
  4674. {
  4675. args += GetKeyringArgs(options.AdditionalKeyrings, options.IgnoreDefaultKeyring, wantSecretKeyrings);
  4676. if(options.AutoFetchKeys)
  4677. {
  4678. args += "--auto-key-locate ";
  4679. if(options.KeyServer != null) args += "keyserver ";
  4680. args += "ldap pka cert ";
  4681. }
  4682. if(options.KeyServer != null)
  4683. {
  4684. args += "--keyserver " + EscapeArg(options.KeyServer.AbsoluteUri) + " --keyserver-options auto-key-retrieve ";
  4685. if(!options.AutoFetchKeys) args += "--auto-key-locate keyserver ";
  4686. }
  4687. if(options.AssumeBinaryInput) args += "--no-armor ";
  4688. }
  4689. return args;
  4690. }
  4691. /// <summary>Given an array of user attributes, returns a collection of user attribute lists, where the attributes in
  4692. /// each list are grouped by key.
  4693. /// </summary>
  4694. static IEnumerable<List<UserAttribute>> GroupAttributesByKey(UserAttribute[] attributes)
  4695. {
  4696. if(attributes == null) throw new ArgumentNullException();
  4697. Dictionary<string, List<UserAttribute>> keyMap = new Dictionary<string, List<UserAttribute>>();
  4698. foreach(UserAttribute attribute in attributes)
  4699. {
  4700. if(attribute == null) throw new ArgumentException("An attribute was null.");
  4701. if(attribute.PrimaryKey == null || string.IsNullOrEmpty(attribute.PrimaryKey.Fingerprint))
  4702. {
  4703. throw new ArgumentException("An attribute did not have a key with a fingerprint.");
  4704. }
  4705. List<UserAttribute> list;
  4706. if(!keyMap.TryGetValue(attribute.PrimaryKey.Fingerprint, out list))
  4707. {
  4708. keyMap[attribute.PrimaryKey.Fingerprint] = list = new List<UserAttribute>();
  4709. }
  4710. int i;
  4711. for(i=0; i<list.Count; i++)
  4712. {
  4713. if(string.Equals(list[i].Id, attribute.Id, StringComparison.Ordinal)) break;
  4714. }
  4715. if(i == list.Count) list.Add(attribute);
  4716. }
  4717. return keyMap.Values;
  4718. }
  4719. /// <summary>Groups a list of signatures by their owning attributes, and groups the owning attributes by their
  4720. /// owning keys.
  4721. /// </summary>
  4722. static void GroupSignaturesByKeyAndObject(KeySignature[] signatures,
  4723. out Dictionary<string, List<UserAttribute>> uidMap,
  4724. out Dictionary<string, List<KeySignature>> sigMap)
  4725. {
  4726. if(signatures == null) throw new ArgumentNullException();
  4727. // we need to group the signed objects by their owning key and the signatures by the signed object
  4728. uidMap = new Dictionary<string, List<UserAttribute>>();
  4729. sigMap = new Dictionary<string, List<KeySignature>>();
  4730. foreach(KeySignature sig in signatures)
  4731. {
  4732. if(sig == null) throw new ArgumentException("A signature was null.");
  4733. UserAttribute signedObject = sig.Object as UserAttribute;
  4734. if(signedObject == null) throw new NotSupportedException("Only editing signatures on attributes is supported.");
  4735. if(signedObject.PrimaryKey == null || string.IsNullOrEmpty(signedObject.PrimaryKey.Fingerprint))
  4736. {
  4737. throw new ArgumentException("A signed object did not have a key with a fingerprint.");
  4738. }
  4739. List<UserAttribute> uidList;
  4740. if(!uidMap.TryGetValue(signedObject.PrimaryKey.Fingerprint, out uidList))
  4741. {
  4742. uidMap[signedObject.PrimaryKey.Fingerprint] = uidList = new List<UserAttribute>();
  4743. }
  4744. int i;
  4745. for(i=0; i<uidList.Count; i++)
  4746. {
  4747. if(string.Equals(signedObject.Id, uidList[i].Id, StringComparison.Ordinal)) break;
  4748. }
  4749. if(i == uidList.Count) uidList.Add(signedObject);
  4750. List<KeySignature> sigList;
  4751. if(!sigMap.TryGetValue(signedObject.Id, out sigList))
  4752. {
  4753. sigMap[signedObject.Id] = sigList = new List<KeySignature>();
  4754. }
  4755. sigList.Add(sig);
  4756. }
  4757. }
  4758. /// <summary>Given an array of subkeys, returns a collection of subkey lists, where the subkeys in each list are
  4759. /// grouped by key.
  4760. /// </summary>
  4761. static IEnumerable<List<Subkey>> GroupSubkeysByKey(Subkey[] subkeys)
  4762. {
  4763. if(subkeys == null) throw new ArgumentNullException();
  4764. // the subkeys need to be grouped by primary key
  4765. Dictionary<string, List<Subkey>> keyMap = new Dictionary<string, List<Subkey>>();
  4766. foreach(Subkey subkey in subkeys)
  4767. {
  4768. if(subkey == null) throw new ArgumentException("A subkey was null.");
  4769. if(subkey.PrimaryKey == null || string.IsNullOrEmpty(subkey.PrimaryKey.Fingerprint))
  4770. {
  4771. throw new ArgumentException("A subkey did not have a primary key with a fingerprint.");
  4772. }
  4773. List<Subkey> keyList;
  4774. if(!keyMap.TryGetValue(subkey.PrimaryKey.Fingerprint, out keyList))
  4775. {
  4776. keyMap[subkey.PrimaryKey.Fingerprint] = keyList = new List<Subkey>();
  4777. }
  4778. keyList.Add(subkey);
  4779. }
  4780. return keyMap.Values;
  4781. }
  4782. /// <summary>Handles a revocation prompt, supplying the reason, explanation, and confirmation.</summary>
  4783. static bool HandleRevokePrompt(Command cmd, string promptId, KeyRevocationReason keyReason,
  4784. UserRevocationReason userReason, ref string[] lines, ref int lineIndex)
  4785. {
  4786. if(string.Equals(promptId, "ask_revocation_reason.text", StringComparison.Ordinal))
  4787. {
  4788. if(lines == null) // parse the explanation text into lines, where no line is blank
  4789. {
  4790. string text = userReason != null ? userReason.Explanation :
  4791. keyReason != null ? keyReason.Explanation : null;
  4792. lines = text == null ? // remove empty lines of text
  4793. new string[0] : text.Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
  4794. }
  4795. if(lineIndex < lines.Length) // send the next line if there are lines left to send
  4796. {
  4797. cmd.SendLine(lines[lineIndex++]);
  4798. }
  4799. else // otherwise, send a blank line, which signifies an the end to the explanation
  4800. {
  4801. cmd.SendLine();
  4802. lineIndex = 0;
  4803. }
  4804. }
  4805. else if(string.Equals(promptId, "ask_revocation_reason.code", StringComparison.Ordinal))
  4806. {
  4807. if(userReason != null && userReason.Reason == UserRevocationCode.IdNoLongerValid)
  4808. {
  4809. cmd.SendLine("4");
  4810. }
  4811. else if(keyReason != null)
  4812. {
  4813. if(keyReason.Reason == KeyRevocationCode.KeyCompromised) cmd.SendLine("1");
  4814. else if(keyReason.Reason == KeyRevocationCode.KeyRetired) cmd.SendLine("3");
  4815. else if(keyReason.Reason == KeyRevocationCode.KeySuperceded) cmd.SendLine("2");
  4816. else cmd.SendLine("0");
  4817. }
  4818. else cmd.SendLine("0");
  4819. }
  4820. else if(string.Equals(promptId, "ask_revocation_reason.okay", StringComparison.Ordinal))
  4821. {
  4822. cmd.SendLine("Y");
  4823. }
  4824. else return false;
  4825. return true;
  4826. }
  4827. /// <summary>Returns true if the given character is a valid hex digit.</summary>
  4828. static bool IsHexDigit(char c)
  4829. {
  4830. if(c >= '0' && c <= '9') return true;
  4831. else
  4832. {
  4833. c = char.ToLowerInvariant(c);
  4834. return c >= 'a' && c <= 'f';
  4835. }
  4836. }
  4837. /// <summary>Determines whether the given string is a valid key fingerprint.</summary>
  4838. static bool IsValidKeyId(string str)
  4839. {
  4840. if(!string.IsNullOrEmpty(str) && (str.Length == 8 || str.Length == 16))
  4841. {
  4842. foreach(char c in str)
  4843. {
  4844. if(!IsHexDigit(c)) return false;
  4845. }
  4846. return true;
  4847. }
  4848. return false;
  4849. }
  4850. /// <summary>Determines whether the given string is a valid key fingerprint.</summary>
  4851. static bool IsValidFingerprint(string str)
  4852. {
  4853. if(!string.IsNullOrEmpty(str) && (str.Length == 32 || str.Length == 40))
  4854. {
  4855. foreach(char c in str)
  4856. {
  4857. if(!IsHexDigit(c)) return false;
  4858. }
  4859. return true;
  4860. }
  4861. return false;
  4862. }
  4863. /// <summary>A helper for reading key listings, that reads the data for a primary key or subkey.</summary>
  4864. static void ReadKeyData(Key key, string[] data)
  4865. {
  4866. PrimaryKey primaryKey = key as PrimaryKey;
  4867. if(!string.IsNullOrEmpty(data[1])) // read various key flags
  4868. {
  4869. char c = data[1][0];
  4870. switch(c)
  4871. {
  4872. case 'i': key.Invalid = true; break;
  4873. case 'd': if(key is PrimaryKey) ((PrimaryKey)key).Disabled = true; break;
  4874. case 'r': key.Revoked = true; break;
  4875. case 'e': key.Expired = true; break;
  4876. case '-': case 'q': case 'n': case 'm': case 'f': case 'u':
  4877. key.CalculatedTrust = ParseTrustLevel(c);
  4878. break;
  4879. }
  4880. }
  4881. if(!string.IsNullOrEmpty(data[2])) key.Length = int.Parse(data[2], CultureInfo.InvariantCulture);
  4882. if(!string.IsNullOrEmpty(data[3])) key.KeyType = ParseKeyType(data[3]);
  4883. if(!string.IsNullOrEmpty(data[4])) key.KeyId = data[4].ToUpperInvariant();
  4884. if(!string.IsNullOrEmpty(data[5])) key.CreationTime = ParseTimestamp(data[5]);
  4885. if(!string.IsNullOrEmpty(data[6])) key.ExpirationTime = ParseNullableTimestamp(data[6]);
  4886. if(!string.IsNullOrEmpty(data[8]) && primaryKey != null) primaryKey.OwnerTrust = ParseTrustLevel(data[8][0]);
  4887. if(!string.IsNullOrEmpty(data[11]))
  4888. {
  4889. KeyCapabilities totalCapabilities = 0;
  4890. foreach(char c in data[11])
  4891. {
  4892. switch(c)
  4893. {
  4894. case 'e': key.Capabilities |= KeyCapabilities.Encrypt; break;
  4895. case 's': key.Capabilities |= KeyCapabilities.Sign; break;
  4896. case 'c': key.Capabilities |= KeyCapabilities.Certify; break;
  4897. case 'a': key.Capabilities |= KeyCapabilities.Authenticate; break;
  4898. case 'E': totalCapabilities |= KeyCapabilities.Encrypt; break;
  4899. case 'S': totalCapabilities |= KeyCapabilities.Sign; break;
  4900. case 'C': totalCapabilities |= KeyCapabilities.Certify; break;
  4901. case 'A': totalCapabilities |= KeyCapabilities.Authenticate; break;
  4902. case 'D': if(key is PrimaryKey) ((PrimaryKey)key).Disabled = true; break;
  4903. }
  4904. }
  4905. if(primaryKey != null) primaryKey.TotalCapabilities = totalCapabilities;
  4906. }
  4907. }
  4908. /// <summary>Validates and normalize a key ID.</summary>
  4909. static string NormalizeKeyId(string id)
  4910. {
  4911. string newId = id;
  4912. // strip off any 0x prefix
  4913. if(newId != null)
  4914. {
  4915. newId = newId.ToUpperInvariant();
  4916. if(newId.StartsWith("0X", StringComparison.Ordinal)) newId = newId.Substring(2);
  4917. newId = newId.Replace(":", ""); // some fingerprints have the octets separated by colons
  4918. }
  4919. if(string.IsNullOrEmpty(newId)) throw new ArgumentException("The key ID was null or empty.");
  4920. // some key ids have a leading zero for no obvious reason...
  4921. if(newId[0] == '0' && (newId.Length == 9 || newId.Length == 17 || newId.Length == 33 || newId.Length == 41))
  4922. {
  4923. newId = newId.Substring(1);
  4924. }
  4925. bool invalid = newId.Length != 8 && newId.Length != 16 && newId.Length != 32 && newId.Length != 40;
  4926. if(!invalid)
  4927. {
  4928. foreach(char c in newId)
  4929. {
  4930. if(!IsHexDigit(c))
  4931. {
  4932. invalid = true;
  4933. break;
  4934. }
  4935. }
  4936. }
  4937. if(invalid) throw new ArithmeticException("Invalid key ID: " + id);
  4938. return newId;
  4939. }
  4940. /// <summary>Normalizes a keyring filename to something that is acceptable to GPG, and that allows two normalized
  4941. /// filenames to be compared with an ordinal comparison.
  4942. /// </summary>
  4943. static string NormalizeKeyringFile(string filename)
  4944. {
  4945. if(filename != null)
  4946. {
  4947. // GPG treats relative keyring and trustdb paths as being relative to the user's home directory, so we'll get the
  4948. // full path. and it detects relative paths by searching for only one directory separator char (backslash on
  4949. // windows), so we'll normalize those as well
  4950. filename = Path.GetFullPath(filename).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
  4951. // use case insensitive filenames on operating systems besides *nix
  4952. if(Environment.OSVersion.Platform != PlatformID.Unix) filename = filename.ToLowerInvariant();
  4953. }
  4954. return filename;
  4955. }
  4956. /// <summary>Converts a character representing a trust level into the corresponding <see cref="TrustLevel"/> value.</summary>
  4957. static TrustLevel ParseTrustLevel(char c)
  4958. {
  4959. switch(c)
  4960. {
  4961. case 'n': return TrustLevel.Never;
  4962. case 'm': return TrustLevel.Marginal;
  4963. case 'f': return TrustLevel.Full;
  4964. case 'u': return TrustLevel.Ultimate;
  4965. default: return TrustLevel.Unknown;
  4966. }
  4967. }
  4968. /// <summary>Trims a string, or returns null if the string is null.</summary>
  4969. static string Trim(string str)
  4970. {
  4971. return str == null ? null : str.Trim();
  4972. }
  4973. /// <summary>Copys all data from the write stream to the standard input of the process, and copies the data from
  4974. /// the standard output into the read stream.
  4975. /// </summary>
  4976. /// <returns>Returns true if all data was written to the standard input before the process terminated, and all the
  4977. /// data was read from standard output.
  4978. /// </returns>
  4979. static bool ReadAndWriteStreams(Stream read, Stream write, Process process)
  4980. {
  4981. ManualResetEvent readComplete = null;
  4982. byte[] writeBuffer = new byte[4096], readBuffer = read == null ? null : new byte[4096];
  4983. bool allDataWritten = false, allDataRead = read == null;
  4984. if(read != null)
  4985. {
  4986. readComplete = new ManualResetEvent(false);
  4987. AsyncCallback callback = null;
  4988. callback = delegate(IAsyncResult result)
  4989. {
  4990. Stream stream = process.StandardOutput.BaseStream;
  4991. int bytes = stream == null ? 0 : stream.EndRead(result);
  4992. if(bytes == 0)
  4993. {
  4994. if(stream != null) allDataRead = true;
  4995. readComplete.Set();
  4996. }
  4997. else
  4998. {
  4999. read.Write(readBuffer, 0, bytes);
  5000. if(stream.CanRead)
  5001. {
  5002. try { stream.BeginRead(readBuffer, 0, readBuffer.Length, callback, null); }
  5003. catch(ObjectDisposedException) { }
  5004. }
  5005. }
  5006. };
  5007. Stream stdOut = process.StandardOutput.BaseStream;
  5008. if(stdOut != null && stdOut.CanRead)
  5009. {
  5010. try { stdOut.BeginRead(readBuffer, 0, readBuffer.Length, callback, null); }
  5011. catch(ObjectDisposedException) { }
  5012. }
  5013. }
  5014. while(!process.HasExited)
  5015. {
  5016. int bytes;
  5017. // copy a chunk of bytes from the write stream to STDIN
  5018. bytes = write.Read(writeBuffer, 0, writeBuffer.Length);
  5019. if(bytes == 0)
  5020. {
  5021. allDataWritten = true;
  5022. break;
  5023. }
  5024. Stream stdIn = process.StandardInput.BaseStream;
  5025. if(stdIn == null || !stdIn.CanWrite) break;
  5026. try { stdIn.Write(writeBuffer, 0, bytes); }
  5027. catch(ObjectDisposedException) { break; }
  5028. }
  5029. process.StandardInput.Close(); // close STDIN so that GPG will finish up
  5030. if(read != null)
  5031. {
  5032. readComplete.WaitOne();
  5033. readComplete.Close();
  5034. }
  5035. return allDataWritten && allDataRead;
  5036. }
  5037. /// <summary>Writes all data from the stream to the standard input of the process,
  5038. /// and then closes the standard input.
  5039. /// </summary>
  5040. /// <returns>Returns true if all data was written to the stream before the process terminated.</returns>
  5041. static bool WriteStreamToProcess(Stream data, Process process)
  5042. {
  5043. return ReadAndWriteStreams(null, data, process);
  5044. }
  5045. string[] ciphers, hashes, keyTypes, compressions;
  5046. string exePath;
  5047. /// <summary>The GPG version, encoded so that 1.4.9 becomes 10409 and 2.0.21 becomes 20021</summary>
  5048. int gpgVersion;
  5049. bool enableAgent, retrieveKeySignatureFingerprints;
  5050. static readonly ReadOnlyListWrapper<UserAttribute> NoAttributes =
  5051. new ReadOnlyListWrapper<UserAttribute>(new UserAttribute[0]);
  5052. static readonly ReadOnlyListWrapper<string> NoRevokers = new ReadOnlyListWrapper<string>(new string[0]);
  5053. static readonly ReadOnlyListWrapper<KeySignature> NoSignatures =
  5054. new ReadOnlyListWrapper<KeySignature>(new KeySignature[0]);
  5055. static readonly ReadOnlyListWrapper<Subkey> NoSubkeys = new ReadOnlyListWrapper<Subkey>(new Subkey[0]);
  5056. static readonly Regex versionLineRe = new Regex(@"gpg .*?(\d+(\.\d+)+)", RegexOptions.Singleline);
  5057. static readonly Regex supportLineRe = new Regex(@"^(\w+):\s*(.+)", RegexOptions.Singleline);
  5058. static readonly Regex commaSepRe = new Regex(@",\s*", RegexOptions.Singleline);
  5059. static readonly Regex cEscapeRe = new Regex(@"\\x[0-9a-f]{2}",
  5060. RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
  5061. }
  5062. #endregion
  5063. } // namespace AdamMil.Security.PGP