From 7c36e29a12140d8baf789e477cc1d5f58791d02d Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Sat, 31 May 2014 13:13:36 +0200 Subject: [PATCH] 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 --- src/keepass2android/EntryActivity.cs | 6 +- .../Resources/values/config.xml | 3 + .../Resources/values/strings.xml | 8 + .../Resources/xml/preferences.xml | 21 + .../Totp/ITotpPluginAdapter.cs | 12 + .../Totp/KeeOtpPluginAdapter.cs | 110 +++++ src/keepass2android/Totp/Kp2aTotp.cs | 28 ++ src/keepass2android/Totp/TotpData.cs | 11 + src/keepass2android/Totp/Totp_Client.cs | 387 ++++++++++++++++++ .../Totp/TrayTotpPluginAdapter.cs | 322 +++++++++++++++ .../Totp/UpdateTotpTimerTask.cs | 82 ++++ src/keepass2android/app/App.cs | 1 + src/keepass2android/keepass2android.csproj | 9 +- .../pluginhost/PluginDatabase.cs | 44 +- 14 files changed, 1040 insertions(+), 4 deletions(-) create mode 100644 src/keepass2android/Totp/ITotpPluginAdapter.cs create mode 100644 src/keepass2android/Totp/KeeOtpPluginAdapter.cs create mode 100644 src/keepass2android/Totp/Kp2aTotp.cs create mode 100644 src/keepass2android/Totp/TotpData.cs create mode 100644 src/keepass2android/Totp/Totp_Client.cs create mode 100644 src/keepass2android/Totp/TrayTotpPluginAdapter.cs create mode 100644 src/keepass2android/Totp/UpdateTotpTimerTask.cs diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index 0c3d6822..96bdb2de 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -230,7 +230,7 @@ namespace keepass2android //update the Entry output in the App database and notify the CopyToClipboard service App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value)); Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService)); - Intent.SetAction(Intents.UpdateKeyboard); + updateKeyboardIntent.SetAction(Intents.UpdateKeyboard); updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString()); StartService(updateKeyboardIntent); @@ -394,12 +394,14 @@ namespace keepass2android i.PutExtra(Strings.ExtraSender, PackageName); AddEntryToIntent(i); - foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry)) { i.SetPackage(plugin); SendBroadcast(i); } + + new Kp2aTotp().OnOpenEntry(); + } private void NotifyPluginsOnModification(string fieldId) { diff --git a/src/keepass2android/Resources/values/config.xml b/src/keepass2android/Resources/values/config.xml index dfb7db1f..08aea388 100644 --- a/src/keepass2android/Resources/values/config.xml +++ b/src/keepass2android/Resources/values/config.xml @@ -74,6 +74,9 @@ true true + TrayTotp_SettingsField_key + TrayTotp_SeedField_key + TrayTotp_prefs_key password_access_prefs_key diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index ec8fd540..faf0021e 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -427,6 +427,14 @@ Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response! Error updating OTP auxiliary file! + TOTP Seed field name + 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. + + TOTP Settings field name + Enter the field name of the settings field for TrayTotp here. + + TrayTotp + Loading… Plug-ins diff --git a/src/keepass2android/Resources/xml/preferences.xml b/src/keepass2android/Resources/xml/preferences.xml index cf81c851..f763ed98 100644 --- a/src/keepass2android/Resources/xml/preferences.xml +++ b/src/keepass2android/Resources/xml/preferences.xml @@ -324,5 +324,26 @@ > + + + + + + diff --git a/src/keepass2android/Totp/ITotpPluginAdapter.cs b/src/keepass2android/Totp/ITotpPluginAdapter.cs new file mode 100644 index 00000000..29f1bf3d --- /dev/null +++ b/src/keepass2android/Totp/ITotpPluginAdapter.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Android.Content; +using KeePassLib.Collections; + +namespace PluginTOTP +{ + + interface ITotpPluginAdapter + { + TotpData GetTotpData(IDictionary entryFields, Context ctx); + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/KeeOtpPluginAdapter.cs b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs new file mode 100644 index 00000000..859fde11 --- /dev/null +++ b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Android.Content; +using KeePassLib.Collections; + +namespace PluginTOTP +{ + /// + /// Adapter to read the TOTP data from a KeeOTP entry. + /// + /// /// 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 entryFields, Context ctx) + { + return new KeeOtpHandler(entryFields, ctx).GetData(); + } + + internal class KeeOtpHandler + { + private readonly Context _ctx; + private readonly IDictionary _entryFields; + + public KeeOtpHandler(IDictionary 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; + } + + + + /// + /// 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. + /// + 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; + } + + } + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/Kp2aTotp.cs b/src/keepass2android/Totp/Kp2aTotp.cs new file mode 100644 index 00000000..d27bfeec --- /dev/null +++ b/src/keepass2android/Totp/Kp2aTotp.cs @@ -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(); + } + } + } + } +} diff --git a/src/keepass2android/Totp/TotpData.cs b/src/keepass2android/Totp/TotpData.cs new file mode 100644 index 00000000..43896512 --- /dev/null +++ b/src/keepass2android/Totp/TotpData.cs @@ -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; } + + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/Totp_Client.cs b/src/keepass2android/Totp/Totp_Client.cs new file mode 100644 index 00000000..27cdd07f --- /dev/null +++ b/src/keepass2android/Totp/Totp_Client.cs @@ -0,0 +1,387 @@ +using System; + +namespace OtpProviderClient +{ + /// + /// Provides Time-based One Time Passwords RFC 6238. + /// + public class Totp_Provider + { + /// + /// Time reference for TOTP generation. + /// + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Duration of generation of each totp, in seconds. + /// + private int _Duration; + + /// + /// Length of the generated totp. + /// + private int _Length; + + private TimeSpan _TimeCorrection; + /// + /// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords. + /// + public TimeSpan TimeCorrection { set { _TimeCorrection = value; } } + + /// + /// Instanciates a new Totp_Generator. + /// + /// Duration of generation of each totp, in seconds. + /// Length of the generated totp. + 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. + } + + /// + /// Returns current time with correction int UTC format. + /// + public DateTime Now + { + get + { + return DateTime.UtcNow - _TimeCorrection; //Computes current time minus time correction giving the corrected time. + } + } + + /// + /// Returns the time remaining before counter incrementation. + /// + 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. + } + } + + /// + /// Returns number of intervals that have elapsed. + /// + 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. + } + } + + /// + /// Converts an unsigned integer to binary data. + /// + /// Unsigned Integer. + /// Binary data. + 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; + } + + /// + /// Generate a Totp using provided binary data. + /// + /// Binary data. + /// Time-based One Time Password. + 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. + } + } + + /// + /// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server. + /// + public class TimeCorrection_Provider + { + /// + /// Timer providing the delay between each time correction check. + /// + private System.Timers.Timer _Timer; + + /// + /// Thread which handles the time correction check. + /// + private System.Threading.Thread Task; + + private bool _Enable; + /// + /// Defines weither or not the class will attempt to get time correction from the server. + /// + public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } } + + private static int _Interval = 60; + /// + /// Gets or sets the interval in minutes between each online checks for time correction. + /// + /// Time + public static int Interval { get { return _Interval; } set { _Interval = value; } } + private long _IntervalStretcher; + + private volatile string _Url; + /// + /// Returns the URL this instance is using to checks for time correction. + /// + public string Url { get { return _Url; } } + + private TimeSpan _TimeCorrection; + /// + /// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction. + /// + public TimeSpan TimeCorrection { get { return _TimeCorrection; } } + + private DateTime _LastUpdateDateTime; + /// + /// Returns the date and time in universal format of the last online check for time correction. + /// + public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } } + + private bool _LastUpdateSucceded = false; + /// + /// Returns true if the last check for time correction was successful. + /// + public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } } + + /// + /// Instanciates a new Totp_TimeCorrection using the specified URL to contact the server. + /// + /// URL of the server to get check. + /// Enable or disable the time correction check. + 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. + } + + /// + /// Task that occurs every time the timer's interval has elapsed. + /// + 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 + } + } + + /// + /// Instanciates a new task and starts it. + /// + /// Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check. + 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 + } + + /// + /// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server. + /// + 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). + } + + /// + /// Perform a time correction check, may a few seconds. + /// + /// Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails. + /// Attempts to get time correction even if disabled. + /// Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check. + 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 + } + } + + /// + /// Utility to deal with Base32 encoding and decoding. + /// + /// + /// http://tools.ietf.org/html/rfc4648 + /// + public static class Base32 + { + /// + /// The number of bits in a base32 encoded character. + /// + private const int encodedBitCount = 5; + /// + /// The number of bits in a byte. + /// + private const int byteBitCount = 8; + /// + /// 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. + /// + private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + /// + /// Takes a block of data and converts it to a base 32 encoded string. + /// + /// Input data. + /// base 32 string. + 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); + } + + /// + /// Takes a base 32 encoded value and converts it back to binary data. + /// + /// Base 32 encoded string. + /// Binary data. + 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; + } + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/TrayTotpPluginAdapter.cs b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs new file mode 100644 index 00000000..9575c793 --- /dev/null +++ b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs @@ -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 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; + } + + /// + /// Check if specified Entry contains Settings that are not null. + /// + internal bool SettingsCheck(IDictionary entryFields) + { + string settings; + entryFields.TryGetValue(SettingsFieldName, out settings); + return !String.IsNullOrEmpty(settings); + } + + internal bool SeedCheck(IDictionary entryFields) + { + string seed; + entryFields.TryGetValue(SeedFieldName, out seed); + return !String.IsNullOrEmpty(seed); + } + + /// + /// Check if specified Entry's Interval and Length are valid. All settings statuses are available as out booleans. + /// + /// Password Entry. + /// Interval Validity. + /// Length Validity. + /// Url Validity. + /// Error(s) while validating Interval or Length. + internal bool SettingsValidate(IDictionary 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 entryFields) + { + return entryFields[SettingsFieldName].Split(';'); + } + + public TotpData GetTotpData(IDictionary 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 entryFields, out string invalidCharacters) + { + return SeedGet(entryFields).ExtWithoutSpaces().ExtIsBase32(out invalidCharacters); + } + internal string SeedGet(IDictionary entryFields) + { + return entryFields[SeedFieldName]; + } + } + + + } + + /// + /// Class to support custom extensions. + /// + internal static class Extensions + { + /// + /// Concatenates a space in front of the current string. + /// + /// Current string. + /// + internal static string ExtWithSpaceBefore(this string Extension) + { + return " " + Extension; + } + + /// + /// Concatenates the current string with space to the end. + /// + /// Current string. + /// + internal static string ExtWithSpaceAfter(this string Extension) + { + return Extension + " "; + } + + /// + /// Concatenates the current string with a bracket in front and to the end. + /// + /// Current string. + /// + internal static string ExtWithBrackets(this string Extension) + { + return ExtWith(Extension, '{', '}'); + } + + /// + /// Concatenates the current string with a parenthesis in front and to the end. + /// + /// Current string. + /// + internal static string ExtWithParenthesis(this string Extension) + { + return ExtWith(Extension, '(', ')'); + } + + /// + /// Concatenates the current string with a charater in front and another character to the end. + /// + /// Current string. + /// Front character. + /// End charater. + /// + internal static string ExtWith(this string Extension, char Left, char Right) + { + return Left + Extension + Right; + } + + /// + /// Remove all spaces from the current string. + /// + /// Current string. + /// + internal static string ExtWithoutSpaces(this string Extension) + { + return Extension.ExtWithout(" "); + } + + /// + /// Remove all specified characters from the current string. + /// + /// Current string. + /// Characters to remove. + /// + internal static string ExtWithout(this string Extension, string Chars) + { + foreach (var Char in Chars) + { + Extension = Extension.Replace(Char.ToString(), ""); + } + return Extension; + } + + + + /// + /// Splits the string and returns specified substring. + /// + /// Current string. + /// Split index. + /// Split seperators. + /// + 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; + } + + /// + /// Makes sure the string provided as a Seed is Base32. Invalid characters are available as out string. + /// + /// Current string. + /// Invalid characters. + /// Validity of the string's characters for Base32 format. + 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; + } + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/UpdateTotpTimerTask.cs b/src/keepass2android/Totp/UpdateTotpTimerTask.cs new file mode 100644 index 00000000..79566c72 --- /dev/null +++ b/src/keepass2android/Totp/UpdateTotpTimerTask.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/src/keepass2android/app/App.cs b/src/keepass2android/app/App.cs index f5faf3a1..163102dc 100644 --- a/src/keepass2android/app/App.cs +++ b/src/keepass2android/app/App.cs @@ -100,6 +100,7 @@ namespace keepass2android BroadcastDatabaseAction(Application.Context, Strings.ActionLockDatabase); QuickLocked = true; + _db.LastOpenedEntry = null; } else { diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 7fa95ccb..9cc28e10 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -30,7 +30,7 @@ full false bin\Debug - DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM + DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;EXCLUDE_KEYTRANSFORM prompt 4 False @@ -146,6 +146,13 @@ + + + + + + + diff --git a/src/keepass2android/pluginhost/PluginDatabase.cs b/src/keepass2android/pluginhost/PluginDatabase.cs index 93098622..e329da9b 100644 --- a/src/keepass2android/pluginhost/PluginDatabase.cs +++ b/src/keepass2android/pluginhost/PluginDatabase.cs @@ -2,8 +2,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Security.Cryptography; +using System.Text; using Android.Content; using Android.Content.PM; +using Android.OS; using Android.Util; using Keepass2android.Pluginsdk; @@ -11,6 +14,26 @@ namespace keepass2android { 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 readonly Context _ctx; private const string _accessToken = "accessToken"; @@ -90,7 +113,7 @@ namespace keepass2android { if (enabled) { - string accessToken = Guid.NewGuid().ToString(); + string accessToken = KeyGenerator.GetUniqueKey(32); Intent i = new Intent(Strings.ActionReceiveAccess); i.SetPackage(pluginPackage); @@ -127,6 +150,10 @@ namespace keepass2android return false; } + //internal access token is valid for all scopes + if ((pluginPackage == _ctx.PackageName) && (accessToken == GetInternalToken())) + return true; + var prefs = GetPreferencesForPlugin(pluginPackage); if (prefs.GetString(_accessToken, null) != accessToken) { @@ -196,5 +223,20 @@ namespace keepass2android } 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; + } + } } } \ No newline at end of file