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