/* This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin. Keepass2Android is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Keepass2Android is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Keepass2Android. If not, see . */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Android.App; using Android.Content; using Android.OS; using Android.Runtime; using Android.Views; using Android.Widget; using Android.Preferences; using KeePassLib.Utility; using KeePassLib; using Android.Text; using KeePassLib.Security; using Android.Content.PM; using keepass2android.view; using System.IO; using System.Globalization; namespace keepass2android { [Activity (Label = "@string/app_name", ConfigurationChanges=ConfigChanges.Orientation|ConfigChanges.KeyboardHidden, Theme="@style/NoTitleBar")] public class EntryEditActivity : LockCloseActivity { public const String KEY_ENTRY = "entry"; public const String KEY_PARENT = "parent"; public const int RESULT_OK_ICON_PICKER = (int)Result.FirstUser + 1000; public const int RESULT_OK_PASSWORD_GENERATOR = RESULT_OK_ICON_PICKER + 1; private PwEntry mEntry, mEntryInDatabase; private bool mShowPassword = false; private bool mIsNew; private PwIcon mSelectedIconID; private PwUuid mSelectedCustomIconID = PwUuid.Zero; private bool mSelectedIcon = false; public static void Launch(Activity act, PwEntry pw) { Intent i = new Intent(act, typeof(EntryEditActivity)); i.PutExtra(KEY_ENTRY, pw.Uuid.ToHexString()); act.StartActivityForResult(i, 0); } public static void Launch(Activity act, PwGroup pw) { Intent i = new Intent(act, typeof(EntryEditActivity)); PwGroup parent = pw; i.PutExtra(KEY_PARENT, parent.Uuid.ToHexString()); act.StartActivityForResult(i, 0); } private ScrollView scroll; protected override void OnCreate(Bundle savedInstanceState) { ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this); mShowPassword = ! prefs.GetBoolean(GetString(Resource.String.maskpass_key), Resources.GetBoolean(Resource.Boolean.maskpass_default)); base.OnCreate(savedInstanceState); SetContentView(Resource.Layout.entry_edit); SetResult(KeePass.EXIT_NORMAL); // Likely the app has been killed exit the activity Database db = App.getDB(); if ( ! db.Open ) { Finish(); return; } Intent i = Intent; String uuidBytes = i.GetStringExtra(KEY_ENTRY); PwUuid entryId = PwUuid.Zero; if (uuidBytes != null) entryId = new KeePassLib.PwUuid(MemUtil.HexStringToByteArray(uuidBytes)); PwGroup parentGroup = null; if ( entryId == PwUuid.Zero ) { String groupId = i.GetStringExtra(KEY_PARENT); parentGroup = db.groups[new PwUuid(MemUtil.HexStringToByteArray(groupId))]; mEntryInDatabase = new PwEntry(true, true); mEntryInDatabase.Strings.Set(PwDefs.UserNameField, new ProtectedString( db.pm.MemoryProtection.ProtectUserName, db.pm.DefaultUserName)); /*KPDesktop * ProtectedString psAutoGen; PwGenerator.Generate(out psAutoGen, Program.Config.PasswordGenerator.AutoGeneratedPasswordsProfile, null, Program.PwGeneratorPool); psAutoGen = psAutoGen.WithProtection(pwDb.MemoryProtection.ProtectPassword); pwe.Strings.Set(PwDefs.PasswordField, psAutoGen); int nExpireDays = Program.Config.Defaults.NewEntryExpiresInDays; if(nExpireDays >= 0) { pwe.Expires = true; pwe.ExpiryTime = DateTime.Now.AddDays(nExpireDays); }*/ if((parentGroup.IconId != PwIcon.Folder) && (parentGroup.IconId != PwIcon.FolderOpen) && (parentGroup.IconId != PwIcon.FolderPackage)) { mEntryInDatabase.IconId = parentGroup.IconId; // Inherit icon from group } mEntryInDatabase.CustomIconUuid = parentGroup.CustomIconUuid; /* * KPDesktop if(strDefaultSeq.Length == 0) { PwGroup pg = m_pwEntry.ParentGroup; if(pg != null) { strDefaultSeq = pg.GetAutoTypeSequenceInherited(); if(strDefaultSeq.Length == 0) { if(PwDefs.IsTanEntry(m_pwEntry)) strDefaultSeq = PwDefs.DefaultAutoTypeSequenceTan; else strDefaultSeq = PwDefs.DefaultAutoTypeSequence; } } }*/ mIsNew = true; } else { System.Diagnostics.Debug.Assert(entryId != null); mEntryInDatabase = db.entries[entryId]; mIsNew = false; } mEntry = mEntryInDatabase.CloneDeep(); fillData(); View scrollView = FindViewById(Resource.Id.entry_scroll); scrollView.ScrollBarStyle = ScrollbarStyles.InsideInset; ImageButton iconButton = (ImageButton) FindViewById(Resource.Id.icon_button); iconButton.Click += (sender, evt) => { IconPickerActivity.Launch(this); }; // Generate password button Button generatePassword = (Button) FindViewById(Resource.Id.generate_button); generatePassword.Click += (object sender, EventArgs e) => { GeneratePasswordActivity.Launch(this); }; // Save button Button save = (Button) FindViewById(Resource.Id.entry_save); save.Click += (object sender, EventArgs e) => { EntryEditActivity act = this; if (!validateBeforeSaving()) return; PwEntry initialEntry = mEntryInDatabase.CloneDeep(); PwEntry newEntry = mEntryInDatabase; //Clone history and re-assign: newEntry.History = newEntry.History.CloneDeep(); //Based on KeePass Desktop bool bCreateBackup = (!mIsNew); if(bCreateBackup) newEntry.CreateBackup(null); if (mSelectedIcon == false) { if (mIsNew) { newEntry.IconId = PwIcon.Key; } else { // Keep previous icon, if no new one was selected } } else { newEntry.IconId = mSelectedIconID; newEntry.CustomIconUuid = mSelectedCustomIconID; } /* KPDesktop if(m_cbCustomForegroundColor.Checked) newEntry.ForegroundColor = m_clrForeground; else newEntry.ForegroundColor = Color.Empty; if(m_cbCustomBackgroundColor.Checked) newEntry.BackgroundColor = m_clrBackground; else newEntry.BackgroundColor = Color.Empty; */ newEntry.Strings.Set(PwDefs.TitleField, new ProtectedString(db.pm.MemoryProtection.ProtectTitle, Util.getEditText(act, Resource.Id.entry_title))); newEntry.Strings.Set(PwDefs.UserNameField, new ProtectedString(db.pm.MemoryProtection.ProtectUserName, Util.getEditText(act, Resource.Id.entry_user_name))); String pass = Util.getEditText(act, Resource.Id.entry_password); byte[] password = StrUtil.Utf8.GetBytes(pass); newEntry.Strings.Set(PwDefs.PasswordField, new ProtectedString(db.pm.MemoryProtection.ProtectPassword, password)); MemUtil.ZeroByteArray(password); newEntry.Strings.Set(PwDefs.UrlField, new ProtectedString(db.pm.MemoryProtection.ProtectUrl, Util.getEditText(act, Resource.Id.entry_url))); newEntry.Strings.Set(PwDefs.NotesField, new ProtectedString(db.pm.MemoryProtection.ProtectNotes, Util.getEditText(act, Resource.Id.entry_comment))); // Delete all non standard strings var keys = newEntry.Strings.GetKeys(); foreach (String key in keys) if (PwDefs.IsStandardField(key) == false) newEntry.Strings.Remove(key); LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container); for (int index = 0; index < container.ChildCount; index++) { View view = container.GetChildAt(index); TextView keyView = (TextView)view.FindViewById(Resource.Id.title); String key = keyView.Text; TextView valueView = (TextView)view.FindViewById(Resource.Id.value); String value = valueView.Text; CheckBox cb = (CheckBox)view.FindViewById(Resource.Id.protection); bool protect = cb.Checked; newEntry.Strings.Set(key, new ProtectedString(protect, value)); } newEntry.Binaries = mEntry.Binaries; newEntry.Expires = mEntry.Expires; if (newEntry.Expires) { newEntry.ExpiryTime = mEntry.ExpiryTime; } newEntry.OverrideUrl = Util.getEditText(this,Resource.Id.entry_override_url); List vNewTags = StrUtil.StringToTags(Util.getEditText(this,Resource.Id.entry_tags)); newEntry.Tags.Clear(); foreach(string strTag in vNewTags) newEntry.AddTag(strTag); /*KPDesktop m_atConfig.Enabled = m_cbAutoTypeEnabled.Checked; m_atConfig.ObfuscationOptions = (m_cbAutoTypeObfuscation.Checked ? AutoTypeObfuscationOptions.UseClipboard : AutoTypeObfuscationOptions.None); SaveDefaultSeq(); newEntry.AutoType = m_atConfig; */ newEntry.Touch(true, false); // Touch *after* backup StrUtil.NormalizeNewLines(newEntry.Strings, true); bool bUndoBackup = false; PwCompareOptions cmpOpt = (PwCompareOptions.NullEmptyEquivStd | PwCompareOptions.IgnoreTimes); if(bCreateBackup) cmpOpt |= PwCompareOptions.IgnoreLastBackup; if(newEntry.EqualsEntry(initialEntry, cmpOpt, MemProtCmpMode.CustomOnly)) { // No modifications at all => restore last mod time and undo backup newEntry.LastModificationTime = initialEntry.LastModificationTime; bUndoBackup = bCreateBackup; } else if(bCreateBackup) { // If only history items have been modified (deleted) => undo // backup, but without restoring the last mod time PwCompareOptions cmpOptNH = (cmpOpt | PwCompareOptions.IgnoreHistory); if(newEntry.EqualsEntry(initialEntry, cmpOptNH, MemProtCmpMode.CustomOnly)) bUndoBackup = true; } if(bUndoBackup) newEntry.History.RemoveAt(newEntry.History.UCount - 1); newEntry.MaintainBackups(db.pm); //if ( newEntry.Strings.ReadSafe (PwDefs.TitleField).Equals(mEntry.Strings.ReadSafe (PwDefs.TitleField)) ) { // SetResult(KeePass.EXIT_REFRESH); //} else { //it's safer to always update the title as we might add further information in the title like expiry etc. SetResult(KeePass.EXIT_REFRESH_TITLE); //} RunnableOnFinish task; OnFinish onFinish = new AfterSave(new Handler(), act); if ( mIsNew ) { task = AddEntry.getInstance(this, App.getDB(), newEntry, parentGroup, onFinish); } else { task = new UpdateEntry(this, App.getDB(), initialEntry, newEntry, onFinish); } ProgressTask pt = new ProgressTask(act, task, Resource.String.saving_database); pt.run(); }; // Respect mask password setting if (mShowPassword) { EditText pass = (EditText) FindViewById(Resource.Id.entry_password); EditText conf = (EditText) FindViewById(Resource.Id.entry_confpassword); pass.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword; conf.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword; } scroll = (ScrollView) FindViewById(Resource.Id.entry_scroll); Button addButton = (Button) FindViewById(Resource.Id.add_advanced); addButton.Visibility = ViewStates.Visible; addButton.Click += (object sender, EventArgs e) => { LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container); EntryEditSection ees = (EntryEditSection) LayoutInflater.Inflate(Resource.Layout.entry_edit_section, null); ees.setData("", new ProtectedString(false, "")); ees.getDeleteButton().Click += (senderEes, eEes) => deleteAdvancedString((View)senderEes); container.AddView(ees); // Scroll bottom scroll.Post(() => { scroll.FullScroll(FocusSearchDirection.Down); }); }; ((CheckBox)FindViewById(Resource.Id.entry_expires_checkbox)).CheckedChange += (object sender, CompoundButton.CheckedChangeEventArgs e) => { mEntry.Expires = e.IsChecked; if (e.IsChecked) { if (mEntry.ExpiryTime < DateTime.Now) mEntry.ExpiryTime = DateTime.Now; } updateExpires(); }; } void addBinaryOrAsk(string filename) { string strItem = UrlUtil.GetFileName(filename); if(mEntry.Binaries.Get(strItem) != null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.SetTitle(GetString(Resource.String.AskOverwriteBinary_title)); builder.SetMessage(GetString(Resource.String.AskOverwriteBinary)); builder.SetPositiveButton(GetString(Resource.String.AskOverwriteBinary_yes), new EventHandler((dlgSender, dlgEvt) => { addBinary(filename, true); })); builder.SetNegativeButton(GetString(Resource.String.AskOverwriteBinary_no), new EventHandler((dlgSender, dlgEvt) => { addBinary(filename, false); })); builder.SetNeutralButton(GetString(Android.Resource.String.Cancel), new EventHandler((dlgSender, dlgEvt) => {})); Dialog dialog = builder.Create(); dialog.Show(); } else addBinary(filename, true); } void addBinary(string filename, bool overwrite) { string strItem = UrlUtil.GetFileName(filename); if (!overwrite) { string strFileName = UrlUtil.StripExtension(strItem); string strExtension = "." + UrlUtil.GetExtension(strItem); int nTry = 0; while(true) { string strNewName = strFileName + nTry.ToString() + strExtension; if(mEntry.Binaries.Get(strNewName) == null) { strItem = strNewName; break; } ++nTry; } } try { byte[] vBytes = File.ReadAllBytes(filename); if(vBytes != null) { ProtectedBinary pb = new ProtectedBinary(false, vBytes); mEntry.Binaries.Set(strItem, pb); } } catch(Exception exAttach) { Toast.MakeText(this, GetString(Resource.String.AttachFailed)+" "+exAttach.Message, ToastLength.Long).Show(); } populateBinaries(); } protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { switch ((int)resultCode) { case RESULT_OK_ICON_PICKER: mSelectedIconID = (PwIcon) data.Extras.GetInt(IconPickerActivity.KEY_ICON_ID); mSelectedCustomIconID = PwUuid.Zero; String customIconIdString = data.Extras.GetString(IconPickerActivity.KEY_CUSTOM_ICON_ID); if (!String.IsNullOrEmpty(customIconIdString)) mSelectedCustomIconID = new PwUuid(MemUtil.HexStringToByteArray(customIconIdString)); mSelectedIcon = true; ImageButton currIconButton = (ImageButton) FindViewById(Resource.Id.icon_button); //TODO: custom image currIconButton.SetImageResource(Icons.iconToResId(mSelectedIconID)); break; case RESULT_OK_PASSWORD_GENERATOR: String generatedPassword = data.GetStringExtra("keepass2android.password.generated_password"); EditText password = (EditText) FindViewById(Resource.Id.entry_password); EditText confPassword = (EditText) FindViewById(Resource.Id.entry_confpassword); password.Text = generatedPassword; confPassword.Text = generatedPassword; break; case (int)Result.Ok: if (requestCode == Intents.REQUEST_CODE_FILE_BROWSE) { String filename = data.DataString; if (filename != null) { if (filename.StartsWith("file://")) { filename = filename.Substring(7); } filename = Java.Net.URLDecoder.Decode(filename); addBinaryOrAsk(filename); } } break; case (int)Result.Canceled: break; default: break; } } void populateBinaries() { ViewGroup binariesGroup = (ViewGroup)FindViewById(Resource.Id.binaries); binariesGroup.RemoveAllViews(); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FillParent, RelativeLayout.LayoutParams.WrapContent); foreach (KeyValuePair pair in mEntry.Binaries) { String key = pair.Key; Button binaryButton = new Button(this); binaryButton.Text = key; binaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuDelete),null, null, null); binaryButton.Click += (object sender, EventArgs e) => { Button btnSender = (Button)(sender); mEntry.Binaries.Remove(key); populateBinaries(); }; binariesGroup.AddView(binaryButton,layoutParams); } Button addBinaryButton = new Button(this); addBinaryButton.Text = GetString(Resource.String.add_binary); addBinaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuAdd) , null, null, null); addBinaryButton.Click += (object sender, EventArgs e) => { Util.showBrowseDialog("/mnt/sdcard", this); }; binariesGroup.AddView(addBinaryButton,layoutParams); FindViewById(Resource.Id.entry_binaries_label).Visibility = mEntry.Binaries.UCount > 0 ? ViewStates.Visible : ViewStates.Gone; } public override bool OnCreateOptionsMenu(IMenu menu) { base.OnCreateOptionsMenu(menu); MenuInflater inflater = MenuInflater; inflater.Inflate(Resource.Menu.entry_edit, menu); IMenuItem togglePassword = menu.FindItem(Resource.Id.menu_toggle_pass); if ( mShowPassword ) { togglePassword.SetTitle(Resource.String.menu_hide_password); } else { togglePassword.SetTitle(Resource.String.show_password); } return true; } public override bool OnOptionsItemSelected(IMenuItem item) { switch ( item.ItemId ) { /*case Resource.Id.menu_donate: try { Util.gotoUrl(this, Resource.String.donate_url); } catch (ActivityNotFoundException) { Toast.MakeText(this, Resource.String.error_failed_to_launch_link, ToastLength.Long).Show(); return false; } return true;*/ case Resource.Id.menu_toggle_pass: if ( mShowPassword ) { item.SetTitle(Resource.String.show_password); mShowPassword = false; } else { item.SetTitle(Resource.String.menu_hide_password); mShowPassword = true; } setPasswordStyle(); return true; case Resource.Id.menu_cancel_edit: Finish(); break; } return base.OnOptionsItemSelected(item); } private void setPasswordStyle() { TextView password = (TextView) FindViewById(Resource.Id.entry_password); TextView confpassword = (TextView) FindViewById(Resource.Id.entry_confpassword); if ( mShowPassword ) { password.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword; confpassword.InputType = InputTypes.ClassText | InputTypes.TextVariationVisiblePassword; } else { password.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; confpassword.InputType = InputTypes.ClassText | InputTypes.TextVariationPassword; } } void updateExpires() { if (mEntry.Expires) { populateText(Resource.Id.entry_expires, getDateTime(mEntry.ExpiryTime)); } else { populateText(Resource.Id.entry_expires, GetString(Resource.String.never)); } ((CheckBox)FindViewById(Resource.Id.entry_expires_checkbox)).Checked = mEntry.Expires; ((EditText)FindViewById(Resource.Id.entry_expires)).Enabled = mEntry.Expires; } private void fillData() { ImageButton currIconButton = (ImageButton) FindViewById(Resource.Id.icon_button); App.getDB().drawFactory.assignDrawableTo(currIconButton, Resources, App.getDB().pm, mEntry.IconId, mEntry.CustomIconUuid); populateText(Resource.Id.entry_title, mEntry.Strings.ReadSafe (PwDefs.TitleField)); populateText(Resource.Id.entry_user_name, mEntry.Strings.ReadSafe (PwDefs.UserNameField)); populateText(Resource.Id.entry_url, mEntry.Strings.ReadSafe (PwDefs.UrlField)); String password = mEntry.Strings.ReadSafe(PwDefs.PasswordField); populateText(Resource.Id.entry_password, password); populateText(Resource.Id.entry_confpassword, password); setPasswordStyle(); populateText(Resource.Id.entry_comment, mEntry.Strings.ReadSafe (PwDefs.NotesField)); LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container); foreach (var pair in mEntry.Strings) { String key = pair.Key; if (!PwDefs.IsStandardField(key)) { EntryEditSection ees = (EntryEditSection) LayoutInflater.Inflate(Resource.Layout.entry_edit_section, null); ees.setData(key, pair.Value); ees.getDeleteButton().Click += (sender, e) => deleteAdvancedString((View)sender); container.AddView(ees); } } populateBinaries(); populateText(Resource.Id.entry_override_url, mEntry.OverrideUrl); populateText(Resource.Id.entry_tags, StrUtil.TagsToString(mEntry.Tags, true)); updateExpires(); } private String getDateTime(System.DateTime dt) { return dt.ToString ("g", CultureInfo.CurrentUICulture); } public void deleteAdvancedString(View view) { EntryEditSection section = (EntryEditSection) view.Parent; LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container); for (int i = 0; i < container.ChildCount; i++) { EntryEditSection ees = (EntryEditSection) container.GetChildAt(i); if (ees == section) { container.RemoveViewAt(i); container.Invalidate(); break; } } } protected bool validateBeforeSaving() { // Require title String title = Util.getEditText(this, Resource.Id.entry_title); if ( title.Length == 0 ) { Toast.MakeText(this, Resource.String.error_title_required, ToastLength.Long).Show(); return false; } // Validate password String pass = Util.getEditText(this, Resource.Id.entry_password); String conf = Util.getEditText(this, Resource.Id.entry_confpassword); if ( ! pass.Equals(conf) ) { Toast.MakeText(this, Resource.String.error_pass_match, ToastLength.Long).Show(); return false; } // Validate expiry date DateTime newExpiry = new DateTime(); if ((mEntry.Expires) && (!DateTime.TryParse( Util.getEditText(this,Resource.Id.entry_expires), out newExpiry))) { Toast.MakeText(this, Resource.String.error_invalid_expiry_date, ToastLength.Long).Show(); return false; } else { mEntry.ExpiryTime = newExpiry; } LinearLayout container = (LinearLayout) FindViewById(Resource.Id.advanced_container); for (int i = 0; i < container.ChildCount; i++) { EntryEditSection ees = (EntryEditSection) container.GetChildAt(i); TextView keyView = (TextView) ees.FindViewById(Resource.Id.title); string key = keyView.Text; if (String.IsNullOrEmpty(key)) { Toast.MakeText(this, Resource.String.error_string_key, ToastLength.Long).Show(); return false; } } return true; } private void populateText(int viewId, String text) { TextView tv = (TextView) FindViewById(viewId); tv.Text = text; } private class AfterSave : OnFinish { Activity act; public AfterSave(Handler handler, Activity act): base(handler) { this.act = act; } public override void run() { if ( mSuccess ) { act.Finish(); } else { displayMessage(act); } } } } }