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>
|
||||||
|
|||||||
@@ -324,5 +324,26 @@
|
|||||||
>
|
>
|
||||||
<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