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; // check at 1st use
102 		private static string _userPath;
103 
104 		private static bool _machinePathExists; // 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 
KeyPairPersistence(CspParameters parameters)114 		public KeyPairPersistence (CspParameters parameters)
115 			: this (parameters, null)
116 		{
117 		}
118 
KeyPairPersistence(CspParameters parameters, string keyPair)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 
Load()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 
Save()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 
Remove()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 		static object lockobj = new object ();
205 
206 		private static string UserPath {
207 			get {
208 				lock (lockobj) {
209 					if ((_userPath == null) || (!_userPathExists)) {
210 						_userPath = Path.Combine (
211 							Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
212 							".mono");
213 						_userPath = Path.Combine (_userPath, "keypairs");
214 
215 						_userPathExists = Directory.Exists (_userPath);
216 						if (!_userPathExists) {
217 							try {
218 								Directory.CreateDirectory (_userPath);
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 							_userPathExists = true;
225 						}
226 					}
227 					if (!IsUserProtected (_userPath) && !ProtectUser (_userPath)) {
228 						string msg = Locale.GetText ("Could not secure user key store '{0}'.");
229 						throw new IOException (String.Format (msg, _userPath));
230 					}
231 				}
232 				// is it properly protected ?
233 				if (!IsUserProtected (_userPath)) {
234 					string msg = Locale.GetText ("Improperly protected user's key pairs in '{0}'.");
235 					throw new CryptographicException (String.Format (msg, _userPath));
236 				}
237 				return _userPath;
238 			}
239 		}
240 
241 		private static string MachinePath {
242 			get {
243 				lock (lockobj) {
244 					if ((_machinePath == null) || (!_machinePathExists)) {
245 						_machinePath = Path.Combine (
246 							Environment.GetFolderPath (Environment.SpecialFolder.CommonApplicationData),
247 							"mono");
248 						_machinePath = Path.Combine (_machinePath, "keypairs");
249 
250 						_machinePathExists = Directory.Exists (_machinePath);
251 						if (!_machinePathExists) {
252 							try {
253 								Directory.CreateDirectory (_machinePath);
254 							}
255 							catch (Exception e) {
256 								string msg = Locale.GetText ("Could not create machine key store '{0}'.");
257 								throw new CryptographicException (String.Format (msg, _machinePath), e);
258 							}
259 							_machinePathExists = true;
260 						}
261 					}
262 					if (!IsMachineProtected (_machinePath) && !ProtectMachine (_machinePath)) {
263 						string msg = Locale.GetText ("Could not secure machine key store '{0}'.");
264 						throw new IOException (String.Format (msg, _machinePath));
265 					}
266 				}
267 				// is it properly protected ?
268 				if (!IsMachineProtected (_machinePath)) {
269 					string msg = Locale.GetText ("Improperly protected machine's key pairs in '{0}'.");
270 					throw new CryptographicException (String.Format (msg, _machinePath));
271 				}
272 				return _machinePath;
273 			}
274 		}
275 
276 #if INSIDE_CORLIB
277 		[MethodImplAttribute (MethodImplOptions.InternalCall)]
_CanSecure(string root)278 		internal static extern bool _CanSecure (string root);
279 
280 		[MethodImplAttribute (MethodImplOptions.InternalCall)]
_ProtectUser(string path)281 		internal static extern bool _ProtectUser (string path);
282 
283 		[MethodImplAttribute (MethodImplOptions.InternalCall)]
_ProtectMachine(string path)284 		internal static extern bool _ProtectMachine (string path);
285 
286 		[MethodImplAttribute (MethodImplOptions.InternalCall)]
_IsUserProtected(string path)287 		internal static extern bool _IsUserProtected (string path);
288 
289 		[MethodImplAttribute (MethodImplOptions.InternalCall)]
_IsMachineProtected(string path)290 		internal static extern bool _IsMachineProtected (string path);
291 #else
292 		// Mono.Security.dll assembly can't use the internal
293 		// call (and still run with other runtimes)
294 
295 		// Note: Class is only available in Mono.Security.dll as
296 		// a management helper (e.g. build a GUI app)
297 
_CanSecure(string root)298 		internal static bool _CanSecure (string root)
299 		{
300 			return true;
301 		}
302 
_ProtectUser(string path)303 		internal static bool _ProtectUser (string path)
304 		{
305 			return true;
306 		}
307 
_ProtectMachine(string path)308 		internal static bool _ProtectMachine (string path)
309 		{
310 			return true;
311 		}
312 
_IsUserProtected(string path)313 		internal static bool _IsUserProtected (string path)
314 		{
315 			return true;
316 		}
317 
_IsMachineProtected(string path)318 		internal static bool _IsMachineProtected (string path)
319 		{
320 			return true;
321 		}
322 #endif
323 		// private stuff
324 
CanSecure(string path)325 		private static bool CanSecure (string path)
326 		{
327 			// we assume POSIX filesystems can always be secured
328 
329 			// check for Unix platforms - see FAQ for more details
330 			// http://www.mono-project.com/FAQ:_Technical#How_to_detect_the_execution_platform_.3F
331 			int platform = (int) Environment.OSVersion.Platform;
332 			if ((platform == 4) || (platform == 128) || (platform == 6))
333 				return true;
334 
335 			// while we ask the runtime for Windows OS
336 			return _CanSecure (Path.GetPathRoot (path));
337 		}
338 
ProtectUser(string path)339 		private static bool ProtectUser (string path)
340 		{
341 			// we cannot protect on some filsystem (like FAT)
342 			if (CanSecure (path)) {
343 				return _ProtectUser (path);
344 			}
345 			// but Mono still needs to run on them :(
346 			return true;
347 		}
348 
ProtectMachine(string path)349 		private static bool ProtectMachine (string path)
350 		{
351 			// we cannot protect on some filsystem (like FAT)
352 			if (CanSecure (path)) {
353 				return _ProtectMachine (path);
354 			}
355 			// but Mono still needs to run on them :(
356 			return true;
357 		}
358 
IsUserProtected(string path)359 		private static bool IsUserProtected (string path)
360 		{
361 			// we cannot protect on some filsystem (like FAT)
362 			if (CanSecure (path)) {
363 				return _IsUserProtected (path);
364 			}
365 			// but Mono still needs to run on them :(
366 			return true;
367 		}
368 
IsMachineProtected(string path)369 		private static bool IsMachineProtected (string path)
370 		{
371 			// we cannot protect on some filsystem (like FAT)
372 			if (CanSecure (path)) {
373 				return _IsMachineProtected (path);
374 			}
375 			// but Mono still needs to run on them :(
376 			return true;
377 		}
378 
379 		private bool CanChange {
380 			get { return (_keyvalue == null); }
381 		}
382 
383 		private bool UseDefaultKeyContainer {
384 			get { return ((_params.Flags & CspProviderFlags.UseDefaultKeyContainer) == CspProviderFlags.UseDefaultKeyContainer); }
385 		}
386 
387 		private bool UseMachineKeyStore {
388 			get { return ((_params.Flags & CspProviderFlags.UseMachineKeyStore) == CspProviderFlags.UseMachineKeyStore); }
389 		}
390 
391 		private string ContainerName {
392 			get {
393 				if (_container == null) {
394 					if (UseDefaultKeyContainer) {
395 						// easy to spot
396 						_container = "default";
397 					}
398 					else if ((_params.KeyContainerName == null) || (_params.KeyContainerName.Length == 0)) {
399 						_container = Guid.NewGuid ().ToString ();
400 					}
401 					else {
402 						// we don't want to trust the key container name as we don't control it
403 						// anyway some characters may not be compatible with the file system
404 						byte[] data = Encoding.UTF8.GetBytes (_params.KeyContainerName);
405 						// Note: We use MD5 as it is faster than SHA1 and has the same length
406 						// as a GUID. Recent problems found in MD5 (like collisions) aren't a
407 						// problem in this case.
408 						MD5 hash = MD5.Create ();
409 						byte[] result = hash.ComputeHash (data);
410 						_container = new Guid (result).ToString ();
411 					}
412 				}
413 				return _container;
414 			}
415 		}
416 
417 		// we do not want any changes after receiving the csp informations
Copy(CspParameters p)418 		private CspParameters Copy (CspParameters p)
419 		{
420 			CspParameters copy = new CspParameters (p.ProviderType, p.ProviderName, p.KeyContainerName);
421 			copy.KeyNumber = p.KeyNumber;
422 			copy.Flags = p.Flags;
423 			return copy;
424 		}
425 
FromXml(string xml)426 		private void FromXml (string xml)
427 		{
428 			SecurityParser sp = new SecurityParser ();
429 			sp.LoadXml (xml);
430 
431 			SecurityElement root = sp.ToXml ();
432 			if (root.Tag == "KeyPair") {
433 				//SecurityElement prop = root.SearchForChildByTag ("Properties");
434 				SecurityElement keyv = root.SearchForChildByTag ("KeyValue");
435 				if (keyv.Children.Count > 0)
436 					_keyvalue = keyv.Children [0].ToString ();
437 				// Note: we do not read other stuff because
438 				// it can't be changed after key creation
439 			}
440 		}
441 
ToXml()442 		private string ToXml ()
443 		{
444 			// note: we do not use SecurityElement here because the
445 			// keypair is a XML string (requiring parsing)
446 			StringBuilder xml = new StringBuilder ();
447 			xml.AppendFormat ("<KeyPair>{0}\t<Properties>{0}\t\t<Provider ", Environment.NewLine);
448 			if ((_params.ProviderName != null) && (_params.ProviderName.Length != 0)) {
449 				xml.AppendFormat ("Name=\"{0}\" ", _params.ProviderName);
450 			}
451 			xml.AppendFormat ("Type=\"{0}\" />{1}\t\t<Container ", _params.ProviderType, Environment.NewLine);
452 			xml.AppendFormat ("Name=\"{0}\" />{1}\t</Properties>{1}\t<KeyValue", this.ContainerName, Environment.NewLine);
453 			if (_params.KeyNumber != -1) {
454 				xml.AppendFormat (" Id=\"{0}\" ", _params.KeyNumber);
455 			}
456 			xml.AppendFormat (">{1}\t\t{0}{1}\t</KeyValue>{1}</KeyPair>{1}", this.KeyValue, Environment.NewLine);
457 			return xml.ToString ();
458 		}
459 	}
460 }
461