2004-12-22 Sebastien Pouliot <sebastien@ximian.com>
[mono.git] / mcs / class / corlib / Mono.Security.Cryptography / KeyPairPersistence.cs
1 //
2 // KeyPairPersistence.cs: Keypair persistence
3 //
4 // Author:
5 //      Sebastien Pouliot <sebastien@ximian.com>
6 //
7 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28
29 using System;
30 using System.Globalization;
31 using System.IO;
32 using System.Runtime.CompilerServices;
33 using System.Runtime.InteropServices;
34 using System.Security;
35 using System.Security.Cryptography;
36 using System.Security.Permissions;
37 using System.Text;
38
39 using Mono.Xml;
40
41 namespace Mono.Security.Cryptography {
42
43         /* File name
44          * [type][unique name][key number].xml
45          * 
46          * where
47          *      type            CspParameters.ProviderType
48          *      unique name     A unique name for the keypair, which is
49          *                      a. default (for a provider default keypair)
50          *                      b. a GUID derived from
51          *                              i. random if no container name was
52          *                              specified at generation time
53          *                              ii. the MD5 hash of the container
54          *                              name (CspParameters.KeyContainerName)
55          *      key number      CspParameters.KeyNumber
56          * 
57          * File format
58          * <KeyPair>
59          *      <Properties>
60          *              <Provider Name="" Type=""/>
61          *              <Container Name=""/>
62          *      </Properties>
63          *      <KeyValue Id="">
64          *              RSAKeyValue, DSAKeyValue ...
65          *      </KeyValue>
66          * </KeyPair>
67          */
68
69         /* NOTES
70          * 
71          * - There's NO confidentiality / integrity built in this
72          * persistance mechanism. The container directories (both user and
73          * machine) are created with restrited ACL. The ACL is also checked
74          * when a key is accessed (so totally public keys won't be used).
75          * see /mono/mono/metadata/security.c for implementation
76          * 
77          * - As we do not use CSP we limit ourselves to provider types (not 
78          * names). This means that for a same type and container type, but 
79          * two different provider names) will return the same keypair. This
80          * should work as CspParameters always requires a csp type in its
81          * constructors.
82          * 
83          * - Assert (CAS) are used so only the OS permission will limit access
84          * to the keypair files. I.e. this will work even in high-security 
85          * scenarios where users do not have access to file system (e.g. web 
86          * application). We can allow this because the filename used is 
87          * TOTALLY under our control (no direct user input is used).
88          * 
89          * - You CAN'T changes properties of the keypair once it's been
90          * created (saved). You must remove the container than save it 
91          * back. This is the same behaviour as CSP under Windows.
92          */
93
94 #if INSIDE_CORLIB
95         internal
96 #else
97         public 
98 #endif
99         class KeyPairPersistence {
100         
101                 private static bool _userPathExists = false; // check at 1st use
102                 private static string _userPath;
103                 
104                 private static bool _machinePathExists = false; // check at 1st use
105                 private static string _machinePath;
106
107                 private CspParameters _params;
108                 private string _keyvalue;
109                 private string _filename;
110                 private string _container;
111
112                 // constructors
113
114                 public KeyPairPersistence (CspParameters parameters) 
115                         : this (parameters, null)
116                 {
117                 }
118
119                 public KeyPairPersistence (CspParameters parameters, string keyPair) 
120                 {
121                         if (parameters == null)
122                                 throw new ArgumentNullException ("parameters");
123
124                         _params = Copy (parameters);
125                         _keyvalue = keyPair;
126                 }
127
128                 // properties
129
130                 public string Filename {
131                         get { 
132                                 if (_filename == null) {
133                                         _filename = String.Format (CultureInfo.InvariantCulture,
134                                                 "[{0}][{1}][{2}].xml", 
135                                                 _params.ProviderType, 
136                                                 this.ContainerName, 
137                                                 _params.KeyNumber);
138                                         if (UseMachineKeyStore)
139                                                 _filename = Path.Combine (MachinePath, _filename);
140                                         else
141                                                 _filename = Path.Combine (UserPath, _filename);
142                                 }
143                                 return _filename; 
144                         }
145                 }
146
147                 public string KeyValue {
148                         get { return _keyvalue; }
149                         set { 
150                                 if (this.CanChange)
151                                         _keyvalue = value; 
152                         }
153                 }
154
155                 // return a (read-only) copy
156                 public CspParameters Parameters {
157                         get { return Copy (_params); }
158                 }
159
160                 // methods
161
162                 public bool Load () 
163                 {
164                         // see NOTES
165 // FIXME                new FileIOPermission (FileIOPermissionAccess.Read, this.Filename).Assert ();
166
167                         bool result = File.Exists (this.Filename);
168                         if (result) {
169                                 using (StreamReader sr = File.OpenText (this.Filename)) {
170                                         FromXml (sr.ReadToEnd ());
171                                 }
172                         }
173                         return result;
174                 }
175
176                 public void Save () 
177                 {
178                         // see NOTES
179 // FIXME                new FileIOPermission (FileIOPermissionAccess.Write, this.Filename).Assert ();
180
181                         using (FileStream fs = File.Open (this.Filename, FileMode.Create)) {
182                                 StreamWriter sw = new StreamWriter (fs, Encoding.UTF8);
183                                 sw.Write (this.ToXml ());
184                                 sw.Close ();
185                         }
186                         // apply protection to newly created files
187                         if (UseMachineKeyStore)
188                                 ProtectMachine (Filename);
189                         else
190                                 ProtectUser (Filename);
191                 }
192
193                 public void Remove () 
194                 {
195                         // see NOTES
196 // FIXME                new FileIOPermission (FileIOPermissionAccess.Write, this.Filename).Assert ();
197
198                         File.Delete (this.Filename);
199                         // it's now possible to change the keypair un the container
200                 }
201
202                 // private static stuff
203
204                 private static string UserPath {
205                         get {
206                                 if ((_userPath == null) || (!_userPathExists)) {
207                                         lock (typeof (KeyPairPersistence)) {
208                                                 _userPath = Path.Combine (
209                                                         Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
210                                                         ".mono");
211                                                 _userPath = Path.Combine (_userPath, "keypairs");
212
213                                                 _userPathExists = Directory.Exists (_userPath);
214                                                 if (!_userPathExists) {
215                                                         try {
216                                                                 Directory.CreateDirectory (_userPath);
217                                                                 ProtectUser (_userPath);
218                                                                 _userPathExists = true;
219                                                         }
220                                                         catch (Exception e) {
221                                                                 string msg = Locale.GetText ("Could not create user key store '{0}'.");
222                                                                 throw new CryptographicException (String.Format (msg, _userPath), e);
223                                                         }
224                                                 }
225                                         }
226                                 }
227                                 // is it properly protected ?
228                                 if (!IsUserProtected (_userPath)) {
229                                         string msg = Locale.GetText ("Improperly protected user's key pairs in '{0}'.");
230                                         throw new CryptographicException (String.Format (msg, _userPath));
231                                 }
232                                 return _userPath;
233                         }
234                 }
235
236                 private static string MachinePath {
237                         get {
238                                 if ((_machinePath == null) || (!_machinePathExists)) {
239                                         lock (typeof (KeyPairPersistence)) {
240                                                 _machinePath = Path.Combine (
241                                                         Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData),
242                                                         ".mono");
243                                                 _machinePath = Path.Combine (_machinePath, "keypairs");
244
245                                                 _machinePathExists = Directory.Exists (_machinePath);
246                                                 if (!_machinePathExists) {
247                                                         try {
248                                                                 Directory.CreateDirectory (_machinePath);
249                                                                 ProtectMachine (_machinePath);
250                                                                 _machinePathExists = true;
251                                                         }
252                                                         catch (Exception e) {
253                                                                 string msg = Locale.GetText ("Could not create machine key store '{0}'.");
254                                                                 throw new CryptographicException (String.Format (msg, _machinePath), e);
255                                                         }
256                                                 }
257                                         }
258                                 }
259                                 // is it properly protected ?
260                                 if (!IsMachineProtected (_machinePath)) {
261                                         string msg = Locale.GetText ("Improperly protected machine's key pairs in '{0}'.");
262                                         throw new CryptographicException (String.Format (msg, _machinePath));
263                                 }
264                                 return _machinePath;
265                         }
266                 }
267
268 #if INSIDE_CORLIB
269                 [MethodImplAttribute (MethodImplOptions.InternalCall)]
270                 internal static extern bool _CanSecure (string root);
271
272                 [MethodImplAttribute (MethodImplOptions.InternalCall)]
273                 internal static extern bool _ProtectUser (string path);
274
275                 [MethodImplAttribute (MethodImplOptions.InternalCall)]
276                 internal static extern bool _ProtectMachine (string path);
277
278                 [MethodImplAttribute (MethodImplOptions.InternalCall)]
279                 internal static extern bool _IsUserProtected (string path);
280
281                 [MethodImplAttribute (MethodImplOptions.InternalCall)]
282                 internal static extern bool _IsMachineProtected (string path);
283 #else
284                 // Mono.Security.dll assembly can't use the internal 
285                 // call (and still run with other runtimes)
286
287                 // Note: Class is only available in Mono.Security.dll as
288                 // a management helper (e.g. build a GUI app)
289
290                 internal static bool _CanSecure (string root) 
291                 {
292                         return true;
293                 }
294
295                 internal static bool _ProtectUser (string path)
296                 {
297                         return true;
298                 }
299
300                 internal static bool _ProtectMachine (string path)
301                 {
302                         return true;
303                 }
304
305                 internal static bool _IsUserProtected (string path)
306                 {
307                         return true;
308                 }
309
310                 internal static bool _IsMachineProtected (string path)
311                 {
312                         return true;
313                 }
314 #endif
315                 // private stuff
316
317                 private static bool CanSecure (string path) 
318                 {
319                         // we assume POSIX filesystems can always be secured
320                         if ((int) Environment.OSVersion.Platform == 128)
321                                 return true;
322                         // while we ask the runtime for Windows OS
323                         return _CanSecure (Path.GetPathRoot (path));
324                 }
325
326                 private static bool ProtectUser (string path)
327                 {
328                         // we cannot protect on some filsystem (like FAT)
329                         if (CanSecure (path)) {
330                                 return _ProtectUser (path);
331                         }
332                         // but Mono still needs to run on them :(
333                         return true;
334                 }
335
336                 private static bool ProtectMachine (string path)
337                 {
338                         // we cannot protect on some filsystem (like FAT)
339                         if (CanSecure (path)) {
340                                 return _ProtectMachine (path);
341                         }
342                         // but Mono still needs to run on them :(
343                         return true;
344                 }
345
346                 private static bool IsUserProtected (string path)
347                 {
348                         // we cannot protect on some filsystem (like FAT)
349                         if (CanSecure (path)) {
350                                 return _IsUserProtected (path);
351                         }
352                         // but Mono still needs to run on them :(
353                         return true;
354                 }
355
356                 private static bool IsMachineProtected (string path)
357                 {
358                         // we cannot protect on some filsystem (like FAT)
359                         if (CanSecure (path)) {
360                                 return _IsMachineProtected (path);
361                         }
362                         // but Mono still needs to run on them :(
363                         return true;
364                 }
365                 
366                 private bool CanChange {
367                         get { return (_keyvalue == null); }
368                 }
369
370                 private bool UseDefaultKeyContainer {
371                         get { return ((_params.Flags & CspProviderFlags.UseDefaultKeyContainer) == CspProviderFlags.UseDefaultKeyContainer); }
372                 }
373
374                 private bool UseMachineKeyStore {
375                         get { return ((_params.Flags & CspProviderFlags.UseMachineKeyStore) == CspProviderFlags.UseMachineKeyStore); }
376                 }
377
378                 private string ContainerName {
379                         get {
380                                 if (_container == null) {
381                                         if (UseDefaultKeyContainer) {
382                                                 // easy to spot
383                                                 _container = "default";
384                                         }
385                                         else if ((_params.KeyContainerName == null) || (_params.KeyContainerName.Length == 0)) {
386                                                 _container = Guid.NewGuid ().ToString ();
387                                         }
388                                         else {
389                                                 // we don't want to trust the key container name as we don't control it
390                                                 // anyway some characters may not be compatible with the file system
391                                                 byte[] data = Encoding.UTF8.GetBytes (_params.KeyContainerName);
392                                                 // Note: We use MD5 as it is faster than SHA1 and has the same length 
393                                                 // as a GUID. Recent problems found in MD5 (like collisions) aren't a
394                                                 // problem in this case.
395                                                 MD5 hash = MD5.Create ();
396                                                 byte[] result = hash.ComputeHash (data);
397                                                 _container = new Guid (result).ToString ();
398                                         }
399                                 }
400                                 return _container;
401                         }
402                 }
403
404                 // we do not want any changes after receiving the csp informations
405                 private CspParameters Copy (CspParameters p) 
406                 {
407                         CspParameters copy = new CspParameters (p.ProviderType, p.ProviderName, p.KeyContainerName);
408                         copy.KeyNumber = p.KeyNumber;
409                         copy.Flags = p.Flags;
410                         return copy;
411                 }
412
413                 private void FromXml (string xml) 
414                 {
415                         SecurityParser sp = new SecurityParser ();
416                         sp.LoadXml (xml);
417
418                         SecurityElement root = sp.ToXml ();
419                         if (root.Tag == "KeyPair") {
420                                 //SecurityElement prop = root.SearchForChildByTag ("Properties");
421                                 SecurityElement keyv = root.SearchForChildByTag ("KeyValue");
422                                 if (keyv.Children.Count > 0)
423                                         _keyvalue = keyv.Children [0].ToString ();
424                                 // Note: we do not read other stuff because 
425                                 // it can't be changed after key creation
426                         }
427                 }
428
429                 private string ToXml () 
430                 {
431                         // note: we do not use SecurityElement here because the
432                         // keypair is a XML string (requiring parsing)
433                         StringBuilder xml = new StringBuilder ();
434                         xml.AppendFormat ("<KeyPair>{0}\t<Properties>{0}\t\t<Provider ", Environment.NewLine);
435                         if ((_params.ProviderName != null) && (_params.ProviderName.Length != 0)) {
436                                 xml.AppendFormat ("Name=\"{0}\" ", _params.ProviderName);
437                         }
438                         xml.AppendFormat ("Type=\"{0}\" />{1}\t\t<Container ", _params.ProviderType, Environment.NewLine);
439                         xml.AppendFormat ("Name=\"{0}\" />{1}\t</Properties>{1}\t<KeyValue", this.ContainerName, Environment.NewLine);
440                         if (_params.KeyNumber != -1) {
441                                 xml.AppendFormat (" Id=\"{0}\" ", _params.KeyNumber);
442                         }
443                         xml.AppendFormat (">{1}\t\t{0}{1}\t</KeyValue>{1}</KeyPair>{1}", this.KeyValue, Environment.NewLine);
444                         return xml.ToString ();
445                 }
446         }
447 }