PluginHost: Introduced internal access token which can be used by internal modules if they want to send broadcasts "as a plugin" (this token always exists and does not have to be granted by the user)
+ TOTP support (TrayTotp and KeeOTP) (not released as a plugin because there is no additional permission; overload for users which don't use this is small (only some prefs); implemented with tighter coupling (instead of a broadcast receiver "plugin like") to avoid performance penalties for every opened entry) App: reset LastOpenedEntry so that TOTP module knows when entry is no longer open Using CryptRNG for token generation
This commit is contained in:
		| @@ -230,7 +230,7 @@ namespace keepass2android | |||||||
| 			//update the Entry output in the App database and notify the CopyToClipboard service | 			//update the Entry output in the App database and notify the CopyToClipboard service | ||||||
| 			App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value)); | 			App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value)); | ||||||
| 			Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService)); | 			Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService)); | ||||||
| 			Intent.SetAction(Intents.UpdateKeyboard); | 			updateKeyboardIntent.SetAction(Intents.UpdateKeyboard); | ||||||
| 			updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString()); | 			updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString()); | ||||||
| 			StartService(updateKeyboardIntent); | 			StartService(updateKeyboardIntent); | ||||||
|  |  | ||||||
| @@ -394,12 +394,14 @@ namespace keepass2android | |||||||
| 			i.PutExtra(Strings.ExtraSender, PackageName); | 			i.PutExtra(Strings.ExtraSender, PackageName); | ||||||
| 			AddEntryToIntent(i); | 			AddEntryToIntent(i); | ||||||
|  |  | ||||||
|  |  | ||||||
| 			foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry)) | 			foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry)) | ||||||
| 			{ | 			{ | ||||||
| 				i.SetPackage(plugin); | 				i.SetPackage(plugin); | ||||||
| 				SendBroadcast(i); | 				SendBroadcast(i); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			new Kp2aTotp().OnOpenEntry(); | ||||||
|  |  | ||||||
| 		} | 		} | ||||||
| 		private void NotifyPluginsOnModification(string fieldId) | 		private void NotifyPluginsOnModification(string fieldId) | ||||||
| 		{ | 		{ | ||||||
|   | |||||||
| @@ -74,6 +74,9 @@ | |||||||
|     <bool name="ShowGroupnameInSearchResult_default">true</bool> |     <bool name="ShowGroupnameInSearchResult_default">true</bool> | ||||||
|  |  | ||||||
| 	<bool name="RememberRecentFiles_default">true</bool> | 	<bool name="RememberRecentFiles_default">true</bool> | ||||||
|  | 	<string name="TrayTotp_SettingsField_key">TrayTotp_SettingsField_key</string> | ||||||
|  | 	<string name="TrayTotp_SeedField_key">TrayTotp_SeedField_key</string> | ||||||
|  | 	<string name="TrayTotp_prefs_key">TrayTotp_prefs_key</string> | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	<string name="password_access_prefs_key">password_access_prefs_key</string> | 	<string name="password_access_prefs_key">password_access_prefs_key</string> | ||||||
|   | |||||||
| @@ -427,6 +427,14 @@ | |||||||
|   <string name="CouldntLoadChalAuxFile_Hint">Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response!</string> |   <string name="CouldntLoadChalAuxFile_Hint">Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response!</string> | ||||||
|   <string name="ErrorUpdatingChalAuxFile">Error updating OTP auxiliary file!</string> |   <string name="ErrorUpdatingChalAuxFile">Error updating OTP auxiliary file!</string> | ||||||
|  |  | ||||||
|  | 	<string name="TrayTotp_SeedField_title">TOTP Seed field name</string> | ||||||
|  | 	<string name="TrayTotp_SeedField_summary">If you are using the Keepass 2 plugin "TrayTotp" with non-default settings, enter the field name for the seed field here according to the settings on the PC.</string> | ||||||
|  | 	 | ||||||
|  | 	<string name="TrayTotp_SettingsField_title">TOTP Settings field name</string> | ||||||
|  | 	<string name="TrayTotp_SettingsField_summary">Enter the field name of the settings field for TrayTotp here.</string> | ||||||
|  | 	 | ||||||
|  | 	<string name="TrayTotp_prefs">TrayTotp</string> | ||||||
|  |  | ||||||
| 	<string name="loading">Loading…</string> | 	<string name="loading">Loading…</string> | ||||||
| 		 | 		 | ||||||
| 	<string name="plugins">Plug-ins</string> | 	<string name="plugins">Plug-ins</string> | ||||||
|   | |||||||
| @@ -325,4 +325,25 @@ | |||||||
| 		<intent android:action="keepass2android.AboutActivity"/> | 		<intent android:action="keepass2android.AboutActivity"/> | ||||||
| 	</PreferenceScreen> | 	</PreferenceScreen> | ||||||
| 	 | 	 | ||||||
|  | <PreferenceScreen | ||||||
|  | 	  android:key="@string/TrayTotp_prefs_key" | ||||||
|  | 	  android:title="@string/TrayTotp_prefs" | ||||||
|  | 		> | ||||||
|  |  | ||||||
|  | 			<EditTextPreference | ||||||
|  | 				 android:enabled="true" | ||||||
|  | 				 android:persistent="true" | ||||||
|  | 				 android:summary="@string/TrayTotp_SeedField_summary" | ||||||
|  | 				 android:defaultValue="TOTP Seed" | ||||||
|  | 				 android:title="@string/BinaryDirectory_title" | ||||||
|  | 				 android:key="@string/TrayTotp_SeedField_key" /> | ||||||
|  | 			<EditTextPreference | ||||||
|  | 				 android:enabled="true" | ||||||
|  | 				 android:persistent="true" | ||||||
|  | 				 android:summary="@string/TrayTotp_SettingsField_summary" | ||||||
|  | 				 android:defaultValue="TOTP Settings" | ||||||
|  | 				 android:title="@string/TrayTotp_SettingsField_title" | ||||||
|  | 				 android:key="@string/TrayTotp_SettingsField_key" /> | ||||||
|  | 	</PreferenceScreen> | ||||||
|  | 		 | ||||||
| </PreferenceScreen> | </PreferenceScreen> | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/keepass2android/Totp/ITotpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/keepass2android/Totp/ITotpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using Android.Content; | ||||||
|  | using KeePassLib.Collections; | ||||||
|  |  | ||||||
|  | namespace PluginTOTP | ||||||
|  | { | ||||||
|  |  | ||||||
|  | 	interface ITotpPluginAdapter | ||||||
|  | 	{ | ||||||
|  | 		TotpData GetTotpData(IDictionary<string, string> entryFields, Context ctx); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								src/keepass2android/Totp/KeeOtpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/keepass2android/Totp/KeeOtpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Collections.Specialized; | ||||||
|  | using Android.Content; | ||||||
|  | using KeePassLib.Collections; | ||||||
|  |  | ||||||
|  | namespace PluginTOTP | ||||||
|  | { | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Adapter to read the TOTP data from a KeeOTP entry. | ||||||
|  | 	/// </summary> | ||||||
|  | 	/// /// This class uses some methods from the KeeOTP plugin (licensed under MIT license) | ||||||
|  | 	class KeeOtpPluginAdapter : ITotpPluginAdapter | ||||||
|  | 	{ | ||||||
|  | 		public const string StringDictionaryKey = "otp"; | ||||||
|  |  | ||||||
|  | 		const string KeyParameter = "key"; | ||||||
|  | 		const string StepParameter = "step"; | ||||||
|  | 		const string SizeParameter = "size"; | ||||||
|  |          | ||||||
|  |  | ||||||
|  | 		public TotpData GetTotpData(IDictionary<string, string> entryFields, Context ctx) | ||||||
|  | 		{ | ||||||
|  | 			return new KeeOtpHandler(entryFields, ctx).GetData(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		internal class KeeOtpHandler | ||||||
|  | 		{ | ||||||
|  | 			private readonly Context _ctx; | ||||||
|  | 			private readonly IDictionary<string, string> _entryFields; | ||||||
|  |  | ||||||
|  | 			public KeeOtpHandler(IDictionary<string, string> entryFields, Context ctx) | ||||||
|  | 			{ | ||||||
|  | 				_entryFields = entryFields; | ||||||
|  | 				_ctx = ctx; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			public TotpData GetData() | ||||||
|  | 			{ | ||||||
|  | 				TotpData res = new TotpData(); | ||||||
|  | 				string data; | ||||||
|  | 				if (!_entryFields.TryGetValue("otp", out data)) | ||||||
|  | 				{ | ||||||
|  | 					return res; | ||||||
|  | 				} | ||||||
|  | 				NameValueCollection parameters = ParseQueryString(data); | ||||||
|  |  | ||||||
|  | 				if (parameters[KeyParameter] == null) | ||||||
|  | 				{ | ||||||
|  | 					return res; | ||||||
|  | 				} | ||||||
|  | 				res.TotpSeed = parameters[KeyParameter]; | ||||||
|  |  | ||||||
|  | 				 | ||||||
|  | 				res.Duration = GetIntOrDefault(parameters, StepParameter, 30); | ||||||
|  | 				res.Length = GetIntOrDefault(parameters, SizeParameter, 6); | ||||||
|  |  | ||||||
|  | 				res.IsTotpEnry = true; | ||||||
|  | 				return res; | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			private static int GetIntOrDefault(NameValueCollection parameters, string parameterKey, int defaultValue) | ||||||
|  | 			{ | ||||||
|  | 				if (parameters[parameterKey] != null) | ||||||
|  | 				{ | ||||||
|  | 					int step; | ||||||
|  | 					if (int.TryParse(parameters[parameterKey], out step)) | ||||||
|  | 						return step; | ||||||
|  | 					else | ||||||
|  | 						return defaultValue; | ||||||
|  | 				} | ||||||
|  | 				else | ||||||
|  | 					return defaultValue; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  |          | ||||||
|  |  | ||||||
|  | 			/// <remarks> | ||||||
|  | 			/// Hacky query string parsing.  This was done due to reports | ||||||
|  | 			/// of people with just a 3.5 or 4.0 client profile getting errors | ||||||
|  | 			/// as the System.Web assembly where .net's implementation of | ||||||
|  | 			/// Url encoding and query string parsing is locate. | ||||||
|  | 			///  | ||||||
|  | 			/// This should be fine since the only thing stored in the string | ||||||
|  | 			/// that needs to be encoded or decoded is the '=' sign. | ||||||
|  | 			/// </remarks> | ||||||
|  | 			private static NameValueCollection ParseQueryString(string data) | ||||||
|  | 			{ | ||||||
|  | 				var collection = new NameValueCollection(); | ||||||
|  |  | ||||||
|  | 				var parameters = data.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries); | ||||||
|  | 				foreach (var parameter in parameters) | ||||||
|  | 				{ | ||||||
|  | 					if (parameter.Contains("=")) | ||||||
|  | 					{ | ||||||
|  | 						var pieces = parameter.Split('='); | ||||||
|  | 						if (pieces.Length != 2) | ||||||
|  | 							continue; | ||||||
|  |  | ||||||
|  | 						collection.Add(pieces[0], pieces[1].Replace("%3d", "=")); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				return collection; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								src/keepass2android/Totp/Kp2aTotp.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/keepass2android/Totp/Kp2aTotp.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Text; | ||||||
|  | using Android.App; | ||||||
|  | using KeePassLib.Utility; | ||||||
|  | using PluginTOTP; | ||||||
|  |  | ||||||
|  | namespace keepass2android | ||||||
|  | { | ||||||
|  | 	class Kp2aTotp | ||||||
|  | 	{ | ||||||
|  |  | ||||||
|  | 		readonly ITotpPluginAdapter[] _pluginAdapters = new ITotpPluginAdapter[] { new TrayTotpPluginAdapter(), new KeeOtpPluginAdapter() }; | ||||||
|  |  | ||||||
|  | 		public void OnOpenEntry() | ||||||
|  | 		{ | ||||||
|  | 			foreach (ITotpPluginAdapter adapter in _pluginAdapters) | ||||||
|  | 			{ | ||||||
|  | 				TotpData totpData = adapter.GetTotpData(App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString()), Application.Context); | ||||||
|  | 				if (totpData.IsTotpEnry) | ||||||
|  | 				{ | ||||||
|  | 					new UpdateTotpTimerTask(Application.Context, adapter).Run(); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/keepass2android/Totp/TotpData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/keepass2android/Totp/TotpData.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | namespace PluginTOTP | ||||||
|  | { | ||||||
|  | 	struct TotpData | ||||||
|  | 	{ | ||||||
|  | 		public bool IsTotpEnry { get; set; } | ||||||
|  | 		public string TotpSeed { get; set; } | ||||||
|  | 		public int Duration { get; set; } | ||||||
|  | 		public int Length { get; set; } | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										387
									
								
								src/keepass2android/Totp/Totp_Client.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								src/keepass2android/Totp/Totp_Client.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,387 @@ | |||||||
|  | using System; | ||||||
|  |  | ||||||
|  | namespace OtpProviderClient | ||||||
|  | { | ||||||
|  |     /// <summary> | ||||||
|  |     /// Provides Time-based One Time Passwords RFC 6238. | ||||||
|  |     /// </summary> | ||||||
|  |     public class Totp_Provider | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Time reference for TOTP generation. | ||||||
|  |         /// </summary> | ||||||
|  |         private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Duration of generation of each totp, in seconds. | ||||||
|  |         /// </summary> | ||||||
|  |         private int _Duration; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Length of the generated totp. | ||||||
|  |         /// </summary> | ||||||
|  |         private int _Length; | ||||||
|  |  | ||||||
|  |         private TimeSpan _TimeCorrection; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords. | ||||||
|  |         /// </summary> | ||||||
|  |         public TimeSpan TimeCorrection { set { _TimeCorrection = value; } } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Instanciates a new Totp_Generator. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="Duration">Duration of generation of each totp, in seconds.</param> | ||||||
|  |         /// <param name="Length">Length of the generated totp.</param> | ||||||
|  |         public Totp_Provider(int Duration, int Length) | ||||||
|  |         { | ||||||
|  |             if (!(Duration > 0)) throw new Exception("Invalid Duration."); //Throws an exception if the duration is invalid as the class cannot work without it. | ||||||
|  |             _Duration = Duration; //Defines variable from argument. | ||||||
|  |             if (!((Length > 5) && (Length < 9))) throw new Exception("Invalid Length."); //Throws an exception if the length is invalid as the class cannot work without it. | ||||||
|  |             _Length = Length; //Defines variable from argument. | ||||||
|  |             _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value. | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns current time with correction int UTC format. | ||||||
|  |         /// </summary> | ||||||
|  |         public DateTime Now | ||||||
|  |         { | ||||||
|  |             get | ||||||
|  |             { | ||||||
|  |                 return DateTime.UtcNow - _TimeCorrection; //Computes current time minus time correction giving the corrected time. | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns the time remaining before counter incrementation. | ||||||
|  |         /// </summary> | ||||||
|  |         public int Timer | ||||||
|  |         { | ||||||
|  |             get | ||||||
|  |             { | ||||||
|  |                 var n = (_Duration - (int)((Now - UnixEpoch).TotalSeconds % _Duration)); //Computes the seconds left before counter incrementation. | ||||||
|  |                 return n == 0 ? _Duration : n; //Returns timer value from 30 to 1. | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns number of intervals that have elapsed. | ||||||
|  |         /// </summary> | ||||||
|  |         public long Counter | ||||||
|  |         { | ||||||
|  |             get | ||||||
|  |             { | ||||||
|  |                 var ElapsedSeconds = (long)Math.Floor((Now - UnixEpoch).TotalSeconds); //Compute current counter for current time. | ||||||
|  |                 return ElapsedSeconds / _Duration; //Applies specified interval to computed counter. | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Converts an unsigned integer to binary data. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="n">Unsigned Integer.</param> | ||||||
|  |         /// <returns>Binary data.</returns> | ||||||
|  |         private byte[] GetBytes(ulong n) | ||||||
|  |         { | ||||||
|  |             byte[] b = new byte[8]; //Math. | ||||||
|  |             b[0] = (byte)(n >> 56); //Math. | ||||||
|  |             b[1] = (byte)(n >> 48); //Math. | ||||||
|  |             b[2] = (byte)(n >> 40); //Math. | ||||||
|  |             b[3] = (byte)(n >> 32); //Math. | ||||||
|  |             b[4] = (byte)(n >> 24); //Math. | ||||||
|  |             b[5] = (byte)(n >> 16); //Math. | ||||||
|  |             b[6] = (byte)(n >> 8);  //Math. | ||||||
|  |             b[7] = (byte)(n);       //Math. | ||||||
|  |             return b; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Generate a Totp using provided binary data. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="key">Binary data.</param> | ||||||
|  |         /// <returns>Time-based One Time Password.</returns> | ||||||
|  |         public string Generate(byte[] key) | ||||||
|  |         { | ||||||
|  |             System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(key, true); //Instanciates a new hash provider with a key. | ||||||
|  |             byte[] hash = hmac.ComputeHash(GetBytes((ulong)Counter)); //Generates hash from key using counter. | ||||||
|  |             hmac.Clear(); //Clear hash instance securing the key. | ||||||
|  |  | ||||||
|  |             int offset = hash[hash.Length - 1] & 0xf;           //Math. | ||||||
|  |             int binary =                                        //Math. | ||||||
|  |                 ((hash[offset] & 0x7f) << 24)                   //Math. | ||||||
|  |                 | ((hash[offset + 1] & 0xff) << 16)             //Math. | ||||||
|  |                 | ((hash[offset + 2] & 0xff) << 8)              //Math. | ||||||
|  |                 | (hash[offset + 3] & 0xff);                    //Math. | ||||||
|  |  | ||||||
|  |             int password = binary % (int)Math.Pow(10, _Length); //Math. | ||||||
|  |             return password.ToString(new string('0', _Length)); //Math. | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server. | ||||||
|  |     /// </summary> | ||||||
|  |     public class TimeCorrection_Provider | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Timer providing the delay between each time correction check. | ||||||
|  |         /// </summary> | ||||||
|  |         private System.Timers.Timer _Timer; | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Thread which handles the time correction check. | ||||||
|  |         /// </summary> | ||||||
|  |         private System.Threading.Thread Task; | ||||||
|  |  | ||||||
|  |         private bool _Enable; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Defines weither or not the class will attempt to get time correction from the server. | ||||||
|  |         /// </summary> | ||||||
|  |         public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } } | ||||||
|  |  | ||||||
|  |         private static int _Interval = 60; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Gets or sets the interval in minutes between each online checks for time correction. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <value>Time</value> | ||||||
|  |         public static int Interval { get { return _Interval; } set { _Interval = value; } } | ||||||
|  |         private long _IntervalStretcher; | ||||||
|  |  | ||||||
|  |         private volatile string _Url; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns the URL this instance is using to checks for time correction. | ||||||
|  |         /// </summary> | ||||||
|  |         public string Url { get { return _Url; } } | ||||||
|  |  | ||||||
|  |         private TimeSpan _TimeCorrection; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction. | ||||||
|  |         /// </summary> | ||||||
|  |         public TimeSpan TimeCorrection { get { return _TimeCorrection; } } | ||||||
|  |  | ||||||
|  |         private DateTime _LastUpdateDateTime; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns the date and time in universal format of the last online check for time correction. | ||||||
|  |         /// </summary> | ||||||
|  |         public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } } | ||||||
|  |  | ||||||
|  |         private bool _LastUpdateSucceded = false; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Returns true if the last check for time correction was successful. | ||||||
|  |         /// </summary> | ||||||
|  |         public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } } | ||||||
|  |          | ||||||
|  |         /// <summary> | ||||||
|  |         /// Instanciates a new Totp_TimeCorrection using the specified URL to contact the server. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="Url">URL of the server to get check.</param> | ||||||
|  |         /// <param name="Enable">Enable or disable the time correction check.</param> | ||||||
|  |         public TimeCorrection_Provider(string Url, bool Enable = true) | ||||||
|  |         { | ||||||
|  |             if (Url == string.Empty) throw new Exception("Invalid URL."); //Throws exception if the URL is invalid as the class cannot work without it. | ||||||
|  |             _Url = Url; //Defines variable from argument. | ||||||
|  |             _Enable = Enable; //Defines variable from argument. | ||||||
|  |             _LastUpdateDateTime = DateTime.MinValue; //Defines variable from non-constant default value. | ||||||
|  |             _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value. | ||||||
|  |             _Timer = new System.Timers.Timer(); //Instanciates timer. | ||||||
|  |             _Timer.Elapsed += Timer_Elapsed; //Handles the timer event | ||||||
|  |             _Timer.Interval = 1000; //Defines the timer interval to 1 seconds. | ||||||
|  |             _Timer.Enabled = _Enable; //Defines the timer to run if the class is initially enabled. | ||||||
|  |             Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. | ||||||
|  |             if (_Enable) Task.Start(); //Starts the new thread if the class is initially enabled. | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Task that occurs every time the timer's interval has elapsed. | ||||||
|  |         /// </summary> | ||||||
|  |         private void Timer_Elapsed(object sender, EventArgs e) | ||||||
|  |         { | ||||||
|  |             _IntervalStretcher++; //Increments timer. | ||||||
|  |             if (_IntervalStretcher >= (60 * _Interval)) //Checks if the specified delay has been reached. | ||||||
|  |             { | ||||||
|  |                 _IntervalStretcher = 0; //Resets the timer. | ||||||
|  |                 Task_Do(); //Attempts to run a new task | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Instanciates a new task and starts it. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check.</returns> | ||||||
|  |         private bool Task_Do() | ||||||
|  |         { | ||||||
|  |             if (!Task.IsAlive) //Checks if the task is still running. | ||||||
|  |             { | ||||||
|  |                 Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. | ||||||
|  |                 Task.Start(); //Starts the new thread. | ||||||
|  |                 return true; //Informs if successful | ||||||
|  |             } | ||||||
|  |             return false; //Informs if failed | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server. | ||||||
|  |         /// </summary> | ||||||
|  |         private void Task_Thread() | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var WebClient = new System.Net.WebClient(); //WebClient to connect to server. | ||||||
|  |                 WebClient.DownloadData(_Url); //Downloads the server's page using HTTP or HTTPS. | ||||||
|  |                 var DateHeader = WebClient.ResponseHeaders.Get("Date"); //Gets the date from the HTTP header of the downloaded page. | ||||||
|  |                 _TimeCorrection = DateTime.UtcNow - DateTime.Parse(DateHeader, System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat).ToUniversalTime(); //Compares the downloaded date to the systems date giving us a timespan. | ||||||
|  |                 _LastUpdateSucceded = true; //Informs that the date check has succeeded. | ||||||
|  |             } | ||||||
|  |             catch (Exception) | ||||||
|  |             { | ||||||
|  |                 _LastUpdateSucceded = false; //Informs that the date check has failed. | ||||||
|  |             } | ||||||
|  |             _LastUpdateDateTime = DateTime.Now; //Informs when the last update has been attempted (succeeded or not). | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Perform a time correction check, may a few seconds. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="ResetTimer">Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails.</param> | ||||||
|  |         /// <param name="ForceCheck">Attempts to get time correction even if disabled.</param> | ||||||
|  |         /// <returns>Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check.</returns> | ||||||
|  |         public bool CheckNow(bool ResetTimer = true, bool ForceCheck = false) | ||||||
|  |         { | ||||||
|  |             if (ResetTimer) //Checks if the timer should be reset. | ||||||
|  |             { | ||||||
|  |                 _IntervalStretcher = 0; //Resets the timer. | ||||||
|  |             } | ||||||
|  |             if (ForceCheck || _Enable) //Checks if this check is forced or if time correction is enabled. | ||||||
|  |             { | ||||||
|  |                 return Task_Do(); //Attempts to run a new task and informs if attempt to attemp is a success of fail | ||||||
|  |             } | ||||||
|  |             return false; //Informs if not attempted to attempt | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Utility to deal with Base32 encoding and decoding. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <remarks> | ||||||
|  |     /// http://tools.ietf.org/html/rfc4648 | ||||||
|  |     /// </remarks> | ||||||
|  |     public static class Base32 | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// The number of bits in a base32 encoded character. | ||||||
|  |         /// </summary> | ||||||
|  |         private const int encodedBitCount = 5; | ||||||
|  |         /// <summary> | ||||||
|  |         /// The number of bits in a byte. | ||||||
|  |         /// </summary> | ||||||
|  |         private const int byteBitCount = 8; | ||||||
|  |         /// <summary> | ||||||
|  |         /// A string containing all of the base32 characters in order. | ||||||
|  |         /// This allows a simple indexof or [index] to convert between | ||||||
|  |         /// a numeric value and an encoded character and vice versa. | ||||||
|  |         /// </summary> | ||||||
|  |         private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | ||||||
|  |         /// <summary> | ||||||
|  |         /// Takes a block of data and converts it to a base 32 encoded string. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="data">Input data.</param> | ||||||
|  |         /// <returns>base 32 string.</returns> | ||||||
|  |         public static string Encode(byte[] data) | ||||||
|  |         { | ||||||
|  |             if (data == null) | ||||||
|  |                 throw new ArgumentNullException(); | ||||||
|  |             if (data.Length == 0) | ||||||
|  |                 throw new ArgumentNullException(); | ||||||
|  |  | ||||||
|  |             // The output character count is calculated in 40 bit blocks.  That is because the least | ||||||
|  |             // common blocks size for both binary (8 bit) and base 32 (5 bit) is 40.  Padding must be used | ||||||
|  |             // to fill in the difference. | ||||||
|  |             int outputCharacterCount = (int)Math.Ceiling(data.Length / (decimal)encodedBitCount) * byteBitCount; | ||||||
|  |             char[] outputBuffer = new char[outputCharacterCount]; | ||||||
|  |  | ||||||
|  |             byte workingValue = 0; | ||||||
|  |             short remainingBits = encodedBitCount; | ||||||
|  |             int currentPosition = 0; | ||||||
|  |  | ||||||
|  |             foreach (byte workingByte in data) | ||||||
|  |             { | ||||||
|  |                 workingValue = (byte)(workingValue | (workingByte >> (byteBitCount - remainingBits))); | ||||||
|  |                 outputBuffer[currentPosition++] = encodingChars[workingValue]; | ||||||
|  |  | ||||||
|  |                 if (remainingBits <= byteBitCount - encodedBitCount) | ||||||
|  |                 { | ||||||
|  |                     workingValue = (byte)((workingByte >> (byteBitCount - encodedBitCount - remainingBits)) & 31); | ||||||
|  |                     outputBuffer[currentPosition++] = encodingChars[workingValue]; | ||||||
|  |                     remainingBits += encodedBitCount; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 remainingBits -= byteBitCount - encodedBitCount; | ||||||
|  |                 workingValue = (byte)((workingByte << remainingBits) & 31); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // If we didn't finish, write the last current working char. | ||||||
|  |             if (currentPosition != outputCharacterCount) | ||||||
|  |                 outputBuffer[currentPosition++] = encodingChars[workingValue]; | ||||||
|  |  | ||||||
|  |             // RFC 4648 specifies that padding up to the end of the next 40 bit block must be provided | ||||||
|  |             // Since the outputCharacterCount does account for the paddingCharacters, fill it out. | ||||||
|  |             while (currentPosition < outputCharacterCount) | ||||||
|  |             { | ||||||
|  |                 // The RFC defined paddinc char is '='. | ||||||
|  |                 outputBuffer[currentPosition++] = '='; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return new string(outputBuffer); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Takes a base 32 encoded value and converts it back to binary data. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="base32">Base 32 encoded string.</param> | ||||||
|  |         /// <returns>Binary data.</returns> | ||||||
|  |         public static byte[] Decode(string base32) | ||||||
|  |         { | ||||||
|  |             if (string.IsNullOrEmpty(base32)) | ||||||
|  |                 throw new ArgumentNullException(); | ||||||
|  |  | ||||||
|  |             var unpaddedBase32 = base32.ToUpperInvariant().TrimEnd('='); | ||||||
|  |             foreach (var c in unpaddedBase32) | ||||||
|  |             { | ||||||
|  |                 if (encodingChars.IndexOf(c) < 0) | ||||||
|  |                     throw new ArgumentException("Base32 contains illegal characters."); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // we have already removed the padding so this will tell us how many actual bytes there should be. | ||||||
|  |             int outputByteCount = unpaddedBase32.Length * encodedBitCount / byteBitCount; | ||||||
|  |             byte[] outputBuffer = new byte[outputByteCount]; | ||||||
|  |  | ||||||
|  |             byte workingByte = 0; | ||||||
|  |             short bitsRemaining = byteBitCount; | ||||||
|  |             int mask = 0; | ||||||
|  |             int arrayIndex = 0; | ||||||
|  |  | ||||||
|  |             foreach (char workingChar in unpaddedBase32) | ||||||
|  |             { | ||||||
|  |                 int encodedCharacterNumericValue = encodingChars.IndexOf(workingChar); | ||||||
|  |  | ||||||
|  |                 if (bitsRemaining > encodedBitCount) | ||||||
|  |                 { | ||||||
|  |                     mask = encodedCharacterNumericValue << (bitsRemaining - encodedBitCount); | ||||||
|  |                     workingByte = (byte)(workingByte | mask); | ||||||
|  |                     bitsRemaining -= encodedBitCount; | ||||||
|  |                 } | ||||||
|  |                 else | ||||||
|  |                 { | ||||||
|  |                     mask = encodedCharacterNumericValue >> (encodedBitCount - bitsRemaining); | ||||||
|  |                     workingByte = (byte)(workingByte | mask); | ||||||
|  |                     outputBuffer[arrayIndex++] = workingByte; | ||||||
|  |                     workingByte = (byte)(encodedCharacterNumericValue << (byteBitCount - encodedBitCount + bitsRemaining)); | ||||||
|  |                     bitsRemaining += byteBitCount - encodedBitCount; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return outputBuffer; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										322
									
								
								src/keepass2android/Totp/TrayTotpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								src/keepass2android/Totp/TrayTotpPluginAdapter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,322 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using Android.Content; | ||||||
|  | using Android.Preferences; | ||||||
|  | using Android.Widget; | ||||||
|  | using KeePassLib.Collections; | ||||||
|  | using keepass2android; | ||||||
|  |  | ||||||
|  | namespace PluginTOTP | ||||||
|  | { | ||||||
|  | 	class TrayTotpPluginAdapter : ITotpPluginAdapter | ||||||
|  | 	{ | ||||||
|  | 		public TotpData GetTotpData(IDictionary<string, string> entryFields, Context ctx) | ||||||
|  | 		{ | ||||||
|  | 			return new TrayTotpHandler(ctx).GetTotpData(entryFields); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private class TrayTotpHandler | ||||||
|  | 		{ | ||||||
|  | 			private readonly Context _ctx; | ||||||
|  |  | ||||||
|  | 			private string SeedFieldName { get { return PreferenceManager.GetDefaultSharedPreferences(_ctx).GetString(_ctx.GetString(Resource.String.TrayTotp_SeedField_key), "TOTP Seed"); } } | ||||||
|  | 			private string SettingsFieldName { get { return PreferenceManager.GetDefaultSharedPreferences(_ctx).GetString(_ctx.GetString(Resource.String.TrayTotp_SettingsField_key), "TOTP Settings"); } } | ||||||
|  |  | ||||||
|  | 			public TrayTotpHandler(Context ctx) | ||||||
|  | 			{ | ||||||
|  | 				_ctx = ctx; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			/// <summary> | ||||||
|  | 			/// Check if specified Entry contains Settings that are not null. | ||||||
|  | 			/// </summary> | ||||||
|  | 			internal bool SettingsCheck(IDictionary<string, string> entryFields) | ||||||
|  | 			{ | ||||||
|  | 				string settings; | ||||||
|  | 				entryFields.TryGetValue(SettingsFieldName, out settings); | ||||||
|  | 				return !String.IsNullOrEmpty(settings); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			internal bool SeedCheck(IDictionary<string, string> entryFields) | ||||||
|  | 			{ | ||||||
|  | 				string seed; | ||||||
|  | 				entryFields.TryGetValue(SeedFieldName, out seed); | ||||||
|  | 				return !String.IsNullOrEmpty(seed); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			/// <summary> | ||||||
|  | 			/// Check if specified Entry's Interval and Length are valid. All settings statuses are available as out booleans. | ||||||
|  | 			/// </summary> | ||||||
|  | 			/// <param name="pe">Password Entry.</param> | ||||||
|  | 			/// <param name="IsIntervalValid">Interval Validity.</param> | ||||||
|  | 			/// <param name="IsLengthValid">Length Validity.</param> | ||||||
|  | 			/// <param name="IsUrlValid">Url Validity.</param> | ||||||
|  | 			/// <returns>Error(s) while validating Interval or Length.</returns> | ||||||
|  | 			internal bool SettingsValidate(IDictionary<string, string> entryFields, out bool IsIntervalValid, out bool IsLengthValid, out bool IsUrlValid) | ||||||
|  | 			{ | ||||||
|  | 				bool SettingsValid = true; | ||||||
|  | 				try | ||||||
|  | 				{ | ||||||
|  | 					string[] Settings = SettingsGet(entryFields); | ||||||
|  | 					try | ||||||
|  | 					{ | ||||||
|  | 						IsIntervalValid = (Convert.ToInt16(Settings[0]) > 0) && (Convert.ToInt16(Settings[0]) < 61); //Interval | ||||||
|  | 					} | ||||||
|  | 					catch (Exception) | ||||||
|  | 					{ | ||||||
|  | 						IsIntervalValid = false; | ||||||
|  | 						SettingsValid = false; | ||||||
|  | 					} | ||||||
|  | 					try | ||||||
|  | 					{ | ||||||
|  | 						IsLengthValid = (Settings[1] == "6") || (Settings[1] == "8"); //Length | ||||||
|  | 					} | ||||||
|  | 					catch (Exception) | ||||||
|  | 					{ | ||||||
|  | 						IsLengthValid = false; | ||||||
|  | 						SettingsValid = false; | ||||||
|  | 					} | ||||||
|  | 					try | ||||||
|  | 					{ | ||||||
|  | 						IsUrlValid = (Settings[2].StartsWith("http://")) || (Settings[2].StartsWith("https://")); //Url | ||||||
|  | 					} | ||||||
|  | 					catch (Exception) | ||||||
|  | 					{ | ||||||
|  | 						IsUrlValid = false; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				catch (Exception) | ||||||
|  | 				{ | ||||||
|  | 					IsIntervalValid = false; | ||||||
|  | 					IsLengthValid = false; | ||||||
|  | 					IsUrlValid = false; | ||||||
|  | 					SettingsValid = false; | ||||||
|  | 				} | ||||||
|  | 				return SettingsValid; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			private string[] SettingsGet(IDictionary<string, string> entryFields) | ||||||
|  | 			{ | ||||||
|  | 				return entryFields[SettingsFieldName].Split(';'); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			public TotpData GetTotpData(IDictionary<string, string> entryFields) | ||||||
|  | 			{ | ||||||
|  | 				TotpData res = new TotpData(); | ||||||
|  |  | ||||||
|  | 				if (SettingsCheck(entryFields) && SeedCheck(entryFields)) | ||||||
|  | 				{ | ||||||
|  | 					bool ValidInterval; bool ValidLength; bool ValidUrl; | ||||||
|  | 					if (SettingsValidate(entryFields, out ValidInterval, out ValidLength, out ValidUrl)) | ||||||
|  | 					{ | ||||||
|  | 						bool NoTimeCorrection = false; | ||||||
|  | 						string[] Settings = SettingsGet(entryFields); | ||||||
|  | 						res.Duration = Convert.ToInt16(Settings[0]); | ||||||
|  | 						res.Length = Convert.ToInt16(Settings[1]); | ||||||
|  | 						if (ValidUrl) | ||||||
|  | 						{ | ||||||
|  | 							NoTimeCorrection = true; | ||||||
|  | 							/*var CurrentTimeCorrection = TimeCorrections[Settings[2]]; | ||||||
|  | 							if (CurrentTimeCorrection != null) | ||||||
|  | 							{ | ||||||
|  | 								TotpGenerator.TimeCorrection = CurrentTimeCorrection.TimeCorrection; | ||||||
|  | 							} | ||||||
|  | 							else | ||||||
|  | 							{ | ||||||
|  | 								TotpGenerator.TimeCorrection = TimeSpan.Zero; | ||||||
|  | 								NoTimeCorrection = true; | ||||||
|  | 							}*/ | ||||||
|  | 						} | ||||||
|  | 						string InvalidCharacters; | ||||||
|  | 						if (SeedValidate(entryFields, out InvalidCharacters)) | ||||||
|  | 						{ | ||||||
|  | 							res.IsTotpEnry = true; | ||||||
|  | 							res.TotpSeed = SeedGet(entryFields).ExtWithoutSpaces(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 						} | ||||||
|  | 						else | ||||||
|  | 						{ | ||||||
|  | 							ShowWarning("Bad seed!" + InvalidCharacters.ExtWithParenthesis().ExtWithSpaceBefore()); | ||||||
|  | 						} | ||||||
|  | 						if (NoTimeCorrection) | ||||||
|  | 							ShowWarning("Warning: TOTP Time correction not implemented!"); | ||||||
|  | 					} | ||||||
|  | 					else | ||||||
|  | 					{ | ||||||
|  | 						ShowWarning("Bad settings!"); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				else | ||||||
|  | 				{ | ||||||
|  | 					//no totp entry | ||||||
|  | 				} | ||||||
|  | 				return res; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			private void ShowWarning(string warning) | ||||||
|  | 			{ | ||||||
|  | 				Toast.MakeText(_ctx, warning, ToastLength.Short).Show(); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			private bool SeedValidate(IDictionary<string, string> entryFields, out string invalidCharacters) | ||||||
|  | 			{ | ||||||
|  | 				return SeedGet(entryFields).ExtWithoutSpaces().ExtIsBase32(out invalidCharacters); | ||||||
|  | 			} | ||||||
|  | 			internal string SeedGet(IDictionary<string, string> entryFields) | ||||||
|  | 			{ | ||||||
|  | 				return entryFields[SeedFieldName]; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Class to support custom extensions. | ||||||
|  | 	/// </summary> | ||||||
|  | 	internal static class Extensions | ||||||
|  | 	{ | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Concatenates a space in front of the current string. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithSpaceBefore(this string Extension) | ||||||
|  | 		{ | ||||||
|  | 			return " " + Extension; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Concatenates the current string with space to the end. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithSpaceAfter(this string Extension) | ||||||
|  | 		{ | ||||||
|  | 			return Extension + " "; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Concatenates the current string with a bracket in front and to the end. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithBrackets(this string Extension) | ||||||
|  | 		{ | ||||||
|  | 			return ExtWith(Extension, '{', '}'); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Concatenates the current string with a parenthesis in front and to the end. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithParenthesis(this string Extension) | ||||||
|  | 		{ | ||||||
|  | 			return ExtWith(Extension, '(', ')'); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Concatenates the current string with a charater in front and another character to the end. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <param name="Left">Front character.</param> | ||||||
|  | 		/// <param name="Right">End charater.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWith(this string Extension, char Left, char Right) | ||||||
|  | 		{ | ||||||
|  | 			return Left + Extension + Right; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Remove all spaces from the current string. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithoutSpaces(this string Extension) | ||||||
|  | 		{ | ||||||
|  | 			return Extension.ExtWithout(" "); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Remove all specified characters from the current string. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <param name="Chars">Characters to remove.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtWithout(this string Extension, string Chars) | ||||||
|  | 		{ | ||||||
|  | 			foreach (var Char in Chars) | ||||||
|  | 			{ | ||||||
|  | 				Extension = Extension.Replace(Char.ToString(), ""); | ||||||
|  | 			} | ||||||
|  | 			return Extension; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		 | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Splits the string and returns specified substring. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <param name="Index">Split index.</param> | ||||||
|  | 		/// <param name="Seperator">Split seperators.</param> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		internal static string ExtSplit(this string Extension, int Index, char Seperator = ';') | ||||||
|  | 		{ | ||||||
|  | 			if (Extension != string.Empty) | ||||||
|  | 			{ | ||||||
|  | 				try | ||||||
|  | 				{ | ||||||
|  | 					var Text = Extension; | ||||||
|  | 					if (Text.Contains(Seperator.ToString())) | ||||||
|  | 					{ | ||||||
|  | 						return Text.Split(Seperator)[Index]; | ||||||
|  | 					} | ||||||
|  | 					return Text; | ||||||
|  | 				} | ||||||
|  | 				catch (Exception) | ||||||
|  | 				{ | ||||||
|  | 					return string.Empty; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return string.Empty; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Makes sure the string provided as a Seed is Base32. Invalid characters are available as out string. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="Extension">Current string.</param> | ||||||
|  | 		/// <param name="InvalidChars">Invalid characters.</param> | ||||||
|  | 		/// <returns>Validity of the string's characters for Base32 format.</returns> | ||||||
|  | 		internal static bool ExtIsBase32(this string Extension, out string InvalidChars) | ||||||
|  | 		{ | ||||||
|  | 			InvalidChars = null; | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				foreach (var CurrentChar in Extension) | ||||||
|  | 				{ | ||||||
|  | 					var CurrentCharValue = Char.GetNumericValue(CurrentChar); | ||||||
|  | 					if (Char.IsLetter(CurrentChar)) | ||||||
|  | 					{ | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 					if (Char.IsDigit(CurrentChar)) | ||||||
|  | 					{ | ||||||
|  | 						if ((CurrentCharValue > 1) && (CurrentCharValue < 8)) | ||||||
|  | 						{ | ||||||
|  | 							continue; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					InvalidChars = (InvalidChars + CurrentCharValue.ToString().ExtWithSpaceBefore()).Trim(); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			catch (Exception) | ||||||
|  | 			{ | ||||||
|  | 				InvalidChars = "(error)"; | ||||||
|  | 			} | ||||||
|  | 			return InvalidChars == null; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										82
									
								
								src/keepass2android/Totp/UpdateTotpTimerTask.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/keepass2android/Totp/UpdateTotpTimerTask.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using Android.Content; | ||||||
|  | using Java.Util; | ||||||
|  | using KeePassLib.Security; | ||||||
|  | using KeePassLib.Utility; | ||||||
|  | using Keepass2android.Pluginsdk; | ||||||
|  | using OtpProviderClient; | ||||||
|  | using keepass2android; | ||||||
|  |  | ||||||
|  | namespace PluginTOTP | ||||||
|  | { | ||||||
|  | 	class UpdateTotpTimerTask: TimerTask | ||||||
|  | 	{ | ||||||
|  | 		private const string _totp = "TOTP"; | ||||||
|  | 		private readonly Context _context; | ||||||
|  | 		private readonly ITotpPluginAdapter _adapter; | ||||||
|  |  | ||||||
|  | 		public UpdateTotpTimerTask(Context context, ITotpPluginAdapter adapter) | ||||||
|  | 		{ | ||||||
|  | 			_context = context; | ||||||
|  | 			_adapter = adapter; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public override void Run() | ||||||
|  | 		{ | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				if (App.Kp2a.GetDb().LastOpenedEntry == null) | ||||||
|  | 					return; //DB was locked | ||||||
|  |  | ||||||
|  | 				Dictionary<string, string> entryFields = App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString()); | ||||||
|  | 				TotpData totpData = _adapter.GetTotpData(entryFields, _context); | ||||||
|  | 				if (totpData.IsTotpEnry) | ||||||
|  | 				{ | ||||||
|  | 					//generate a new totp | ||||||
|  | 					Totp_Provider prov = new Totp_Provider(totpData.Duration, totpData.Length); | ||||||
|  | 					string totp = prov.Generate(Base32.Decode(totpData.TotpSeed)); | ||||||
|  | 					//update entry and keyboard | ||||||
|  | 					UpdateEntryData(totp); | ||||||
|  | 					//broadcast new field value (update EntryActivity). this might result in another keyboard  | ||||||
|  | 					//update, but that's inexpensive and relatively rare | ||||||
|  | 					BroadcastNewTotp(totp); | ||||||
|  | 					//restart timer | ||||||
|  | 					new Timer().Schedule(new UpdateTotpTimerTask(_context, _adapter), 1000 * prov.Timer); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			catch (Exception e) | ||||||
|  | 			{ | ||||||
|  | 				Android.Util.Log.Debug(_totp, e.ToString()); | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private void UpdateEntryData(string totp) | ||||||
|  | 		{ | ||||||
|  | 			//update the Entry output in the App database and notify the CopyToClipboard service | ||||||
|  | 			App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(_totp, new ProtectedString(true, totp)); | ||||||
|  | 			Intent updateKeyboardIntent = new Intent(_context, typeof(CopyToClipboardService)); | ||||||
|  | 			updateKeyboardIntent.SetAction(Intents.UpdateKeyboard); | ||||||
|  | 			updateKeyboardIntent.PutExtra("entry", App.Kp2a.GetDb().LastOpenedEntry.Uuid.ToHexString()); | ||||||
|  | 			_context.StartService(updateKeyboardIntent); | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private void BroadcastNewTotp(string totp) | ||||||
|  | 		{ | ||||||
|  | 			Intent i = new Intent(Strings.ActionSetEntryField); | ||||||
|  | 			i.PutExtra(Strings.ExtraAccessToken,new PluginDatabase(_context).GetInternalToken()); | ||||||
|  | 			i.SetPackage(_context.PackageName); | ||||||
|  | 			i.PutExtra(Strings.ExtraSender, _context.PackageName); | ||||||
|  | 			i.PutExtra(Strings.ExtraFieldValue, totp); | ||||||
|  | 			i.PutExtra(Strings.ExtraEntryId, App.Kp2a.GetDb().LastOpenedEntry.Entry.Uuid.ToHexString()); | ||||||
|  | 			i.PutExtra(Strings.ExtraFieldId, _totp); | ||||||
|  | 			i.PutExtra(Strings.ExtraFieldProtected, true); | ||||||
|  | 			 | ||||||
|  | 			_context.SendBroadcast(i); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -100,6 +100,7 @@ namespace keepass2android | |||||||
| 						BroadcastDatabaseAction(Application.Context, Strings.ActionLockDatabase); | 						BroadcastDatabaseAction(Application.Context, Strings.ActionLockDatabase); | ||||||
|  |  | ||||||
| 						QuickLocked = true; | 						QuickLocked = true; | ||||||
|  | 						_db.LastOpenedEntry = null; | ||||||
| 					} | 					} | ||||||
| 					else | 					else | ||||||
| 					{ | 					{ | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ | |||||||
|     <DebugType>full</DebugType> |     <DebugType>full</DebugType> | ||||||
|     <Optimize>false</Optimize> |     <Optimize>false</Optimize> | ||||||
|     <OutputPath>bin\Debug</OutputPath> |     <OutputPath>bin\Debug</OutputPath> | ||||||
|     <DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM</DefineConstants> |     <DefineConstants>DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM</DefineConstants> | ||||||
|     <ErrorReport>prompt</ErrorReport> |     <ErrorReport>prompt</ErrorReport> | ||||||
|     <WarningLevel>4</WarningLevel> |     <WarningLevel>4</WarningLevel> | ||||||
|     <ConsolePause>False</ConsolePause> |     <ConsolePause>False</ConsolePause> | ||||||
| @@ -146,6 +146,13 @@ | |||||||
|     <Compile Include="GroupBaseActivity.cs" /> |     <Compile Include="GroupBaseActivity.cs" /> | ||||||
|     <Compile Include="LockCloseListActivity.cs" /> |     <Compile Include="LockCloseListActivity.cs" /> | ||||||
|     <Compile Include="LockingListActivity.cs" /> |     <Compile Include="LockingListActivity.cs" /> | ||||||
|  |     <Compile Include="Totp\ITotpPluginAdapter.cs" /> | ||||||
|  |     <Compile Include="Totp\KeeOtpPluginAdapter.cs" /> | ||||||
|  |     <Compile Include="Totp\Kp2aTotp.cs" /> | ||||||
|  |     <Compile Include="Totp\TotpData.cs" /> | ||||||
|  |     <Compile Include="Totp\Totp_Client.cs" /> | ||||||
|  |     <Compile Include="Totp\TrayTotpPluginAdapter.cs" /> | ||||||
|  |     <Compile Include="Totp\UpdateTotpTimerTask.cs" /> | ||||||
|     <Compile Include="Utils\ActivityDesign.cs" /> |     <Compile Include="Utils\ActivityDesign.cs" /> | ||||||
|     <Compile Include="Utils\LoadingDialog.cs" /> |     <Compile Include="Utils\LoadingDialog.cs" /> | ||||||
|     <Compile Include="Utils\Util.cs" /> |     <Compile Include="Utils\Util.cs" /> | ||||||
|   | |||||||
| @@ -2,8 +2,11 @@ using System; | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Collections.ObjectModel; | using System.Collections.ObjectModel; | ||||||
| using System.Linq; | using System.Linq; | ||||||
|  | using System.Security.Cryptography; | ||||||
|  | using System.Text; | ||||||
| using Android.Content; | using Android.Content; | ||||||
| using Android.Content.PM; | using Android.Content.PM; | ||||||
|  | using Android.OS; | ||||||
| using Android.Util; | using Android.Util; | ||||||
| using Keepass2android.Pluginsdk; | using Keepass2android.Pluginsdk; | ||||||
|  |  | ||||||
| @@ -11,6 +14,26 @@ namespace keepass2android | |||||||
| { | { | ||||||
| 	public class PluginDatabase | 	public class PluginDatabase | ||||||
| 	{ | 	{ | ||||||
|  | 		public class KeyGenerator | ||||||
|  | 		{ | ||||||
|  | 			public static string GetUniqueKey(int maxSize) | ||||||
|  | 			{ | ||||||
|  | 				char[] chars = | ||||||
|  | 				"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); | ||||||
|  | 				byte[] data = new byte[1]; | ||||||
|  | 				RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider(); | ||||||
|  | 				crypto.GetNonZeroBytes(data); | ||||||
|  | 				data = new byte[maxSize]; | ||||||
|  | 				crypto.GetNonZeroBytes(data); | ||||||
|  | 				StringBuilder result = new StringBuilder(maxSize); | ||||||
|  | 				foreach (byte b in data) | ||||||
|  | 				{ | ||||||
|  | 					result.Append(chars[b % (chars.Length)]); | ||||||
|  | 				} | ||||||
|  | 				return result.ToString(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		private const string _tag = "KP2A_PluginDatabase"; | 		private const string _tag = "KP2A_PluginDatabase"; | ||||||
| 		private readonly Context _ctx; | 		private readonly Context _ctx; | ||||||
| 		private const string _accessToken = "accessToken"; | 		private const string _accessToken = "accessToken"; | ||||||
| @@ -90,7 +113,7 @@ namespace keepass2android | |||||||
| 		{ | 		{ | ||||||
| 			if (enabled) | 			if (enabled) | ||||||
| 			{ | 			{ | ||||||
| 				string accessToken = Guid.NewGuid().ToString(); | 				string accessToken = KeyGenerator.GetUniqueKey(32); | ||||||
|  |  | ||||||
| 				Intent i = new Intent(Strings.ActionReceiveAccess); | 				Intent i = new Intent(Strings.ActionReceiveAccess); | ||||||
| 				i.SetPackage(pluginPackage); | 				i.SetPackage(pluginPackage); | ||||||
| @@ -127,6 +150,10 @@ namespace keepass2android | |||||||
| 				return false; | 				return false; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			//internal access token is valid for all scopes | ||||||
|  | 			if ((pluginPackage == _ctx.PackageName) && (accessToken == GetInternalToken())) | ||||||
|  | 				return true; | ||||||
|  |  | ||||||
| 			var prefs = GetPreferencesForPlugin(pluginPackage); | 			var prefs = GetPreferencesForPlugin(pluginPackage); | ||||||
| 			if (prefs.GetString(_accessToken, null) != accessToken) | 			if (prefs.GetString(_accessToken, null) != accessToken) | ||||||
| 			{ | 			{ | ||||||
| @@ -196,5 +223,20 @@ namespace keepass2android | |||||||
| 			} | 			} | ||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		public string GetInternalToken() | ||||||
|  | 		{ | ||||||
|  | 			var prefs = _ctx.GetSharedPreferences("KP2A.PluginHost" , FileCreationMode.Private); | ||||||
|  | 			if (prefs.Contains(_accessToken)) | ||||||
|  | 			{ | ||||||
|  | 				return prefs.GetString(_accessToken, null); | ||||||
|  | 			} | ||||||
|  | 			else | ||||||
|  | 			{ | ||||||
|  | 				var token = KeyGenerator.GetUniqueKey(32); | ||||||
|  | 				prefs.Edit().PutString(_accessToken, token).Commit(); | ||||||
|  | 				return token; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user
	 Philipp Crocoll
					Philipp Crocoll