From 8a1890bc1041d9ddae2e58437b2804324f556818 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 27 May 2025 15:26:00 +0200 Subject: [PATCH] refactor and improve EntryEditActivity: Ensure that configuration changes are handled properly --- .../CreateDatabaseActivity.cs | 2 +- src/keepass2android-app/EntryEditActivity.cs | 815 ++++++++++-------- .../EntryEditActivityState.cs | 2 +- .../GeneratePasswordActivity.cs | 6 +- .../layout/entry_edit_section_file.xml | 5 + 5 files changed, 458 insertions(+), 372 deletions(-) diff --git a/src/keepass2android-app/CreateDatabaseActivity.cs b/src/keepass2android-app/CreateDatabaseActivity.cs index d1d85df4..17a51da1 100644 --- a/src/keepass2android-app/CreateDatabaseActivity.cs +++ b/src/keepass2android-app/CreateDatabaseActivity.cs @@ -316,7 +316,7 @@ namespace keepass2android if (resultCode == KeePass.ResultOkPasswordGenerator) { - String generatedPassword = data.GetStringExtra("keepass2android.password.generated_password"); + String generatedPassword = data.GetStringExtra(GeneratePasswordActivity.GeneratedPasswordKey); FindViewById(Resource.Id.entry_password).Text = generatedPassword; FindViewById(Resource.Id.entry_confpassword).Text = generatedPassword; } diff --git a/src/keepass2android-app/EntryEditActivity.cs b/src/keepass2android-app/EntryEditActivity.cs index e82e33cc..d7758322 100644 --- a/src/keepass2android-app/EntryEditActivity.cs +++ b/src/keepass2android-app/EntryEditActivity.cs @@ -62,6 +62,105 @@ using Task = Android.Gms.Tasks.Task; namespace keepass2android { + class TotpEditUtil + { + public enum TotpEncoding + { + Default, Steam, Custom + } + public static 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 static string AlgorithmIndexToString(in int algorithm) + { + switch (algorithm) + { + case 0: + return "SHA1"; + case 1: + return "SHA256"; + case 2: + return "SHA512"; + default: + return ""; + } + } + + static 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); + + } + + + static 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; + } + + } + + public abstract class ExtraEditView : RelativeLayout { protected ExtraEditView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) @@ -74,6 +173,9 @@ namespace keepass2android } public Button EditButton => (Button)FindViewById(Resource.Id.edit_extra); + public abstract TextView KeyTextView { get; } + public abstract TextView ValueTextView { get; } + public abstract CheckBox ProtectionCheckbox { get; } //event which is triggered when the user modifies the extra string @@ -87,39 +189,268 @@ namespace keepass2android } } + + } public sealed class ExtraEditViewString: ExtraEditView { + private EntryEditActivity _activity; + private readonly PasswordFont _passwordFont; + private ExtraEditViewString(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } - public ExtraEditViewString(Context? context, KeyValuePair pair, PasswordFont passwordFont) : base(context) + public ExtraEditViewString(EntryEditActivity context, KeyValuePair pair, PasswordFont passwordFont) : base(context) { + _activity = context; + _passwordFont = passwordFont; Inflate(context, Resource.Layout.entry_edit_section, this); Tag = pair.Key; - var keyView = ((TextView)FindViewById(Resource.Id.extrakey))!; + KeyTextView = ((TextView)FindViewById(Resource.Id.extrakey))!; + ValueContainer = ((TextInputLayout)FindViewById(Resource.Id.value_container))!; + ValueEdit = ((EditText)FindViewById(Resource.Id.value))!; + ProtectionCheckbox = ((CheckBox)FindViewById(Resource.Id.protection))!; - keyView.Text = pair.Key; + // Set new IDs for views to ensure they are unique. Otherwise restoring the state may fail. + // Note that we shouldn't use Resource.Id.XXX with FindViewById afterwards + ValueEdit.Id = GenerateViewId(); + ValueContainer.Id = GenerateViewId(); + KeyTextView.Id = GenerateViewId(); + ProtectionCheckbox.Id = GenerateViewId(); + KeyTextView.Text = pair.Key; string stringValue = pair.Value.ReadString(); - ValueTextView.Text = stringValue; - ((TextInputLayout)FindViewById(Resource.Id.value_container)).Hint = pair.Key; - ValueTextView.TextChanged += + ValueEdit.Text = stringValue; + ValueContainer.Hint = pair.Key; + + ValueEdit.TextChanged += (sender, e) => { //invoke ExtraModified InvokeExtraModifiedHandler(); }; - passwordFont.ApplyTo(ValueTextView); - ((CheckBox)FindViewById(Resource.Id.protection)).Checked = pair.Value.IsProtected; + passwordFont.ApplyTo(ValueEdit); + + ProtectionCheckbox.Checked = pair.Value.IsProtected; + EditButton.Click += (sender, e) => Edit(); } - public TextView ValueTextView => ((TextView)FindViewById(Resource.Id.value)); + public EditText ValueEdit { get; } + + public override TextView KeyTextView { get; } + public override TextView ValueTextView => ValueEdit; + + public TextInputLayout ValueContainer { get; } + + public override CheckBox ProtectionCheckbox { get; } + + public void Edit() + { + var sender = EditButton; + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(_activity); + View dlgView = _activity.LayoutInflater.Inflate(Resource.Layout. + edit_extra_string_dialog, null); + + + + builder.SetView(dlgView); + builder.SetNegativeButton(Android.Resource.String.Cancel, (o, args) => { }); + builder.SetPositiveButton(Android.Resource.String.Ok, (o, args) => + { + CopyFieldFromExtraDialog(o, Resource.Id.title, KeyTextView); + + CopyCheckboxFromExtraDialog(o, Resource.Id.protection, ProtectionCheckbox); + var sourceFieldTitle = (EditText)((Dialog)o).FindViewById(Resource.Id.title); + var sourceFieldValue = (EditText)((Dialog)o).FindViewById(Resource.Id.value); + ValueEdit.Text = sourceFieldValue.Text; + ValueContainer.Hint = sourceFieldTitle.Text; + }); + Dialog dialog = builder.Create(); + + //setup delete button: + var deleteButton = dlgView.FindViewById