implement editor for TOTP settings in EntryEditActivity, closes https://github.com/PhilippC/keepass2android/issues/770

This commit is contained in:
Philipp Crocoll
2021-05-13 16:20:17 +02:00
parent 84ac5fef4f
commit 1d6dadea58
14 changed files with 389 additions and 15 deletions

View File

@@ -33,12 +33,17 @@ using KeePassLib.Security;
using Android.Content.PM;
using System.IO;
using System.Globalization;
using System.Net;
using System.Text;
using Android.Content.Res;
using Android.Database;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Util;
using keepass2android.Io;
using KeePassLib.Serialization;
using KeeTrayTOTP.Libraries;
using PluginTOTP;
using Debug = System.Diagnostics.Debug;
using File = System.IO.File;
using Object = Java.Lang.Object;
@@ -287,7 +292,31 @@ namespace keepass2android
EditAdvancedString(ees.FindViewById(Resource.Id.edit_extra));
};
SetAddExtraStringEnabled();
FindViewById(Resource.Id.entry_extras_container).Visibility =
Button configureTotpButton = (Button)FindViewById(Resource.Id.configure_totp);
configureTotpButton.Visibility = CanConfigureOtpSettings() ? ViewStates.Gone : ViewStates.Visible;
configureTotpButton.Click += (sender, e) =>
{
bool added = false;
View ees = FindExtraEditSection("otp");
if (ees == null)
{
LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container);
KeyValuePair<string, ProtectedString> pair =
new KeyValuePair<string, ProtectedString>("otp", new ProtectedString(true, ""));
ees = CreateExtraStringView(pair);
container.AddView(ees);
added = true;
}
EditTotpString(ees.FindViewById(Resource.Id.edit_extra));
};
FindViewById(Resource.Id.entry_extras_container).Visibility =
State.EditMode.ShowAddExtras || State.Entry.Strings.Any(s => !PwDefs.IsStandardField(s.Key)) ? ViewStates.Visible : ViewStates.Gone;
FindViewById(Resource.Id.entry_binaries_container).Visibility =
State.EditMode.ShowAddAttachments || State.Entry.Binaries.Any() ? ViewStates.Visible : ViewStates.Gone;
@@ -402,9 +431,17 @@ namespace keepass2android
private void SetAddExtraStringEnabled()
{
((Button)FindViewById(Resource.Id.add_advanced)).Visibility = (!App.Kp2a.CurrentDb.DatabaseFormat.CanHaveCustomFields || !State.EditMode.ShowAddExtras) ? ViewStates.Gone : ViewStates.Visible;
((Button)FindViewById(Resource.Id.configure_totp)).Visibility = CanConfigureOtpSettings() ? ViewStates.Gone : ViewStates.Visible;
}
private void MakePasswordVisibleOrHidden()
private bool CanConfigureOtpSettings()
{
return (!App.Kp2a.CurrentDb.DatabaseFormat.CanHaveCustomFields || !State.EditMode.ShowAddExtras)
&& (new Kp2aTotp().TryGetAdapter(new PwEntryOutput(State.Entry, App.Kp2a.CurrentDb)) == null || (State.Entry.Strings.GetKeys().Contains("otp"))) //only allow to edit KeeWeb/KeepassXC style otps
;
}
private void MakePasswordVisibleOrHidden()
{
EditText password = (EditText) FindViewById(Resource.Id.entry_password);
TextView confpassword = (TextView) FindViewById(Resource.Id.entry_confpassword);
@@ -942,7 +979,8 @@ namespace keepass2android
binariesGroup.Visibility = ViewStates.Visible;
FindViewById(Resource.Id.entry_binaries_container).Visibility = ViewStates.Visible;
((Button)FindViewById(Resource.Id.add_advanced)).Visibility = ViewStates.Visible;
FindViewById(Resource.Id.entry_extras_container).Visibility = ViewStates.Visible;
((Button)FindViewById(Resource.Id.configure_totp)).Visibility = ViewStates.Visible;
FindViewById(Resource.Id.entry_extras_container).Visibility = ViewStates.Visible;
return true;
case Android.Resource.Id.Home:
@@ -1020,6 +1058,7 @@ namespace keepass2android
if (type == "bool")
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section_bool, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
var checkbox = ((CheckBox)ees.FindViewById(Resource.Id.checkbox));
var valueView = ((TextView)ees.FindViewById(Resource.Id.value));
@@ -1037,6 +1076,7 @@ namespace keepass2android
else if (type == "file")
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section_file, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
var titleView = ((TextView)ees.FindViewById(Resource.Id.title));
keyView.Text = pair.Key;
@@ -1054,6 +1094,7 @@ namespace keepass2android
else
{
RelativeLayout ees = (RelativeLayout)LayoutInflater.Inflate(Resource.Layout.entry_edit_section, null);
ees.Tag = pair.Key;
var keyView = ((TextView)ees.FindViewById(Resource.Id.extrakey));
var titleView = ((TextView)ees.FindViewById(Resource.Id.title));
keyView.Text = pair.Key;
@@ -1090,8 +1131,184 @@ namespace keepass2android
}
}
private void EditTotpString(View sender)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View dlgView = LayoutInflater.Inflate(Resource.Layout.
configure_totp_dialog, null);
private void EditAdvancedString(View sender)
builder.SetView(dlgView);
builder.SetNegativeButton(Android.Resource.String.Cancel, (o, args) => { });
builder.SetPositiveButton(Android.Resource.String.Ok, (o, args) =>
{
var targetField = ((TextView)((View)sender.Parent).FindViewById(Resource.Id.value));
if (targetField != null)
{
string entryTitle = Util.GetEditText(this, Resource.Id.entry_title);
string username = Util.GetEditText(this, Resource.Id.entry_user_name);
string secret = dlgView.FindViewById<TextView>(Resource.Id.totp_secret_key).Text;
string totpLength = dlgView.FindViewById<EditText>(Resource.Id.totp_length).Text;
string timeStep = dlgView.FindViewById<EditText>(Resource.Id.totp_time_step).Text;
var checkedTotpId = (int)dlgView.FindViewById<RadioGroup>(Resource.Id.totp_encoding).CheckedRadioButtonId;
TotpEncoding encoding = (checkedTotpId == Resource.Id.totp_encoding_steam)
? TotpEncoding.Steam : (checkedTotpId == Resource.Id.totp_encoding_rfc6238 ? TotpEncoding.Default : TotpEncoding.Custom);
var algorithm = (int)dlgView.FindViewById<Spinner>(Resource.Id.totp_algorithm).SelectedItemPosition;
targetField.Text = BuildOtpString(entryTitle, username, secret, totpLength, timeStep, encoding, algorithm);
}
else
{
Toast.MakeText(this, "did not find target field", ToastLength.Long).Show();
}
//not calling State.Entry.Strings.Set(...). We only do this when the user saves the changes.
State.EntryModified = true;
});
Dialog dialog = builder.Create();
dlgView.FindViewById<RadioButton>(Resource.Id.totp_encoding_custom).CheckedChange += (o, args) =>
{
dlgView.FindViewById(Resource.Id.totp_custom_settings_group).Visibility = args.IsChecked ? ViewStates.Visible : ViewStates.Gone;
};
//copy values from entry into dialog
View ees = (View)sender.Parent;
TotpData totpData = new Kp2aTotp().TryGetTotpData(new PwEntryOutput(State.Entry, App.Kp2a.CurrentDb));
if (totpData != null)
{
dlgView.FindViewById<TextView>(Resource.Id.totp_secret_key).Text = totpData.TotpSeed;
if (totpData.Encoder == TotpData.EncoderSteam)
{
dlgView.FindViewById<RadioButton>(Resource.Id.totp_encoding_steam).Checked = true;
}
else if ((totpData.Encoder == TotpData.EncoderRfc6238) && (totpData.IsDefaultRfc6238))
{
dlgView.FindViewById<RadioButton>(Resource.Id.totp_encoding_rfc6238).Checked = true;
}
else
{
dlgView.FindViewById<RadioButton>(Resource.Id.totp_encoding_custom).Checked = true;
}
dlgView.FindViewById<EditText>(Resource.Id.totp_length).Text = totpData.Length;
dlgView.FindViewById<EditText>(Resource.Id.totp_time_step).Text = totpData.Duration;
dlgView.FindViewById <Spinner>(Resource.Id.totp_algorithm).SetSelection(totpData.HashAlgorithm == TotpData.HashSha1 ? 0 : (
totpData.HashAlgorithm == TotpData.HashSha256 ? 1:
(totpData.HashAlgorithm == TotpData.HashSha256 ? 2 : 0)));
dlgView.FindViewById(Resource.Id.totp_custom_settings_group).Visibility = dlgView.FindViewById<RadioButton>(Resource.Id.totp_encoding_custom).Checked ? ViewStates.Visible : ViewStates.Gone;
}
_passwordFont.ApplyTo(dlgView.FindViewById<EditText>(Resource.Id.totp_secret_key));
Util.SetNoPersonalizedLearning(dlgView);
dialog.Show();
}
string SanitizeInput(string encodedData)
{
if (encodedData.Length <= 0)
{
return encodedData;
}
StringBuilder newEncodedDataBuilder = new StringBuilder(encodedData);
int i = 0;
foreach (var ch in encodedData)
{
switch (ch)
{
case '0':
newEncodedDataBuilder[i++] = 'O';
break;
case '1':
newEncodedDataBuilder[i++] = 'L';
break;
case '8':
newEncodedDataBuilder[i++] = 'B';
break;
default:
if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('2' <= ch && ch <= '7'))
{
newEncodedDataBuilder[i++] = ch;
}
break;
}
}
string newEncodedData = newEncodedDataBuilder.ToString().Substring(0, i);
return AddPadding(newEncodedData);
}
string AddPadding(string encodedData)
{
if (encodedData.Length <= 0 || encodedData.Length % 8 == 0) {
return encodedData;
}
int rBytes = encodedData.Length % 8;
// rBytes must be a member of {2, 4, 5, 7}
if (1 == rBytes || 3 == rBytes || 6 == rBytes) {
return encodedData;
}
string newEncodedData = encodedData;
for (int nPads = 8 - rBytes; nPads > 0; --nPads)
{
newEncodedData += "=";
}
return newEncodedData;
}
enum TotpEncoding
{
Default, Steam, Custom
}
private string BuildOtpString(string entryTitle, string userName, string secret, string totpLength, string timeStep, TotpEncoding encoding, int algorithm)
{
string entryEncoded = string.IsNullOrWhiteSpace(entryTitle)
? "Keepass2Android"
: System.Uri.EscapeUriString(entryTitle);
return $"otpauth://totp/{entryEncoded}:{System.Uri.EscapeUriString(userName)}?" +
$"secret={SanitizeInput(secret)}" +
$"&issuer={ entryEncoded}"
+ (encoding != TotpEncoding.Custom? "" : $"&period={timeStep}&digits={totpLength}&algorithm={AlgorithmIndexToString(algorithm)}") +
(encoding == TotpEncoding.Steam ? "&encoder=steam" : "");
}
private string AlgorithmIndexToString(in int algorithm)
{
switch (algorithm)
{
case 0:
return "SHA1";
case 1:
return "SHA256";
case 2:
return "SHA512";
default:
return "";
}
}
private void EditAdvancedString(View sender)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View dlgView = LayoutInflater.Inflate(Resource.Layout.

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/totp_secret_key"
android:singleLine="true"
android:inputType="text"
android:hint="@string/totp_secret_key"
android:dropDownWidth="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<RadioGroup
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/totp_encoding">
<RadioButton
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/totp_encoding_rfc6238"
android:id="@+id/totp_encoding_rfc6238" />
<RadioButton
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/totp_encoding_steam"
android:id="@+id/totp_encoding_steam" />
<RadioButton
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/totp_encoding_custom"
android:id="@+id/totp_encoding_custom" />
</RadioGroup>
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/totp_custom_settings_group">
<Spinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/totp_algorithms"
android:prompt="@string/algorithm"
android:id="@+id/totp_algorithm" />
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/totp_time_step"
android:id="@+id/totp_time_step" />
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/totp_length"
android:id="@+id/totp_length" />
</LinearLayout>
</LinearLayout>

View File

@@ -177,6 +177,19 @@
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="@string/add_extra_string"/>
<Button
android:id="@+id/configure_totp"
android:background="?android:selectableItemBackground"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAllCaps="true"
android:paddingLeft="8dp"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
android:drawableLeft="@drawable/baseline_schedule_white_24"
android:text="@string/configure_totp"/>
</LinearLayout>
</LinearLayout>

View File

@@ -177,6 +177,12 @@
<item>28</item>
</string-array>
<string-array name="totp_algorithms">
<item>SHA-1</item>
<item>SHA-256</item>
<item>SHA-512</item>
</string-array>
<string name="design_default">System</string>
<string-array name="design_values">
<item>Light</item>

View File

@@ -346,7 +346,15 @@
<string name="protection">Protected field</string>
<string name="add_binary">Add file attachment</string>
<string name="add_extra_string">Add additional string</string>
<string name="delete_extra_string">Delete additional string</string>
<string name="configure_totp">Configure TOTP</string>
<string name="totp_secret_key">Secret key</string>
<string name="totp_encoding_rfc6238">Default RFC6238 token settings</string>
<string name="totp_encoding_steam">Steam token settings</string>
<string name="totp_encoding_custom">Custom token settings</string>
<string name="totp_time_step">Time step</string>
<string name="totp_length">Code length</string>
<string name="delete_extra_string">Delete additional string</string>
<string name="database_loaded_quickunlock_enabled">%1$s: Locked. QuickUnlock enabled.</string>
<string name="database_loaded_unlocked">%1$s: Unlocked.</string>
<string name="credentials_dialog_title">Enter server credentials</string>

View File

@@ -33,9 +33,9 @@ namespace keepass2android
res.Encoder = parsedQuery.Get("encoder");
string algo = parsedQuery.Get("algorithm");
if (algo == "SHA512")
res.HashAlgorithm = "HMAC-SHA-512";
res.HashAlgorithm = TotpData.HashSha512;
if (algo == "SHA256")
res.HashAlgorithm = "HMAC-SHA-256";
res.HashAlgorithm = TotpData.HashSha256;
//set defaults according to https://github.com/google/google-authenticator/wiki/Key-Uri-Format

View File

@@ -42,7 +42,7 @@ namespace PluginTOTP
string strAlg;
entryFields.TryGetValue("TimeOtp-Algorithm", out strAlg);
res.HashAlgorithm = strAlg;
res.TotpSecret = pbSecret;
res.Length = uLength.ToString();
res.Duration = uPeriod.ToString();

View File

@@ -19,6 +19,23 @@ namespace keepass2android
new Keepass2TotpPluginAdapter(),
};
public TotpData TryGetTotpData(PwEntryOutput entry)
{
if (entry == null)
return null;
foreach (ITotpPluginAdapter adapter in _pluginAdapters)
{
TotpData totpData = adapter.GetTotpData(entry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString()), Application.Context, false);
if (totpData.IsTotpEntry)
{
return totpData;
}
}
return null;
}
public ITotpPluginAdapter TryGetAdapter(PwEntryOutput entry)
{
if (entry == null)

View File

@@ -127,7 +127,7 @@ namespace KeeTrayTOTP.Libraries
this.encoder = TOTPEncoder.rfc6238;
}
if(data.Url != null)
if(data.TimeCorrectionUrl != null)
{
{
this.TimeCorrection = TimeSpan.Zero;

View File

@@ -7,11 +7,14 @@ namespace PluginTOTP
{
public TotpData()
{
HashAlgorithm = "HMAC-SHA-1";
HashAlgorithm = HashSha1;
}
public const string EncoderSteam = "steam";
public const string EncoderRfc6238 = "rfc6238";
public const string HashSha1 = "HMAC-SHA-1";
public const string HashSha256 = "HMAC-SHA-256";
public const string HashSha512 = "HMAC-SHA-512";
public bool IsTotpEntry { get; set; }
@@ -20,12 +23,28 @@ namespace PluginTOTP
public string TotpSeed
{
set { TotpSecret = Base32.Decode(value.Trim()); }
get { return Base32.Encode(TotpSecret); }
}
public string Duration { get; set; }
public string Encoder { get; set; }
public string Length { get; set; }
public string Url { get; set; }
public string TimeCorrectionUrl { get; set; }
public string HashAlgorithm { get; set; }
public bool IsDefaultRfc6238
{
get { return Length == "6" && Duration == "30" && (HashAlgorithm == null || HashAlgorithm == HashSha1); }
}
public static TotpData MakeDefaultRfc6238()
{
return new TotpData()
{
Duration = "30",
Length = "6",
HashAlgorithm = HashSha1
};
}
}
}

View File

@@ -122,7 +122,7 @@ namespace PluginTOTP
if (ValidUrl)
{
NoTimeCorrection = true;
res.Url = Settings[2];
res.TimeCorrectionUrl = Settings[2];
/*var CurrentTimeCorrection = TimeCorrections[Settings[2]];
if (CurrentTimeCorrection != null)
{

View File

@@ -314,9 +314,6 @@
<None Include="Resources\drawable-xhdpi\2_action_about.png">
<Visible>False</Visible>
</None>
<AndroidResource Include="Resources\layout\edit_extra_string_dialog.xml">
<SubType>AndroidResource</SubType>
</AndroidResource>
<AndroidResource Include="Resources\layout\file_storage_setup.xml">
<SubType>AndroidResource</SubType>
</AndroidResource>
@@ -2070,6 +2067,21 @@
<ItemGroup>
<AndroidResource Include="Resources\menu\menu_selectdb.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\configure_totp_dialog.xml">
<SubType>AndroidResource</SubType>
<Generator>MSBuild:UpdateGeneratedFiles</Generator>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\edit_extra_string_dialog.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\baseline_schedule_24.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\baseline_schedule_white_24.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>