diff --git a/src/KeePassLib2Android/IDatabaseFormat.cs b/src/KeePassLib2Android/IDatabaseFormat.cs index a58b69a3..9cbd53b5 100644 --- a/src/KeePassLib2Android/IDatabaseFormat.cs +++ b/src/KeePassLib2Android/IDatabaseFormat.cs @@ -15,5 +15,13 @@ namespace KeePassLib void Save(PwDatabase kpDatabase, Stream stream); bool CanHaveEntriesInRootGroup { get; } + bool CanHaveMultipleAttachments { get; } + bool CanHaveCustomFields { get; } + bool HasDefaultUsername { get; } + bool HasDatabaseName { get; } + bool SupportsAttachmentKeys { get; } + bool SupportsTags { get; } + bool SupportsOverrideUrl { get; } + bool CanRecycle { get; } } } \ No newline at end of file diff --git a/src/KeePassLib2Android/PwDatabase.cs b/src/KeePassLib2Android/PwDatabase.cs index 3b194722..7c96cbbf 100644 --- a/src/KeePassLib2Android/PwDatabase.cs +++ b/src/KeePassLib2Android/PwDatabase.cs @@ -426,11 +426,13 @@ namespace KeePassLib public byte[] HashOfFileOnDisk { get { return m_pbHashOfFileOnDisk; } + set { m_pbHashOfFileOnDisk = value; } } public byte[] HashOfLastIO { get { return m_pbHashOfLastIO; } + set { m_pbHashOfLastIO = value; } } public bool UseFileTransactions diff --git a/src/Kp2aBusinessLogic/database/KdbDatabaseFormat.cs b/src/Kp2aBusinessLogic/database/KdbDatabaseFormat.cs index 4746024e..61a2143e 100644 --- a/src/Kp2aBusinessLogic/database/KdbDatabaseFormat.cs +++ b/src/Kp2aBusinessLogic/database/KdbDatabaseFormat.cs @@ -26,6 +26,7 @@ namespace keepass2android { private Dictionary _groupData = new Dictionary(); private static readonly DateTime _expireNever = new DateTime(2999,12,28,23,59,59); + private List _metaStreams; public void PopulateDatabaseFromStream(PwDatabase db, Stream s, IStatusLogger slLogger) { @@ -34,6 +35,8 @@ namespace keepass2android var hashingStream = new HashingStreamEx(s, false, new SHA256Managed()); + _metaStreams = new List(); + string password = "";//no need to distinguish between null and "" because empty passwords are invalid (and null is not allowed) KcpPassword passwordKey = (KcpPassword)db.MasterKey.GetUserKey(typeof(KcpPassword)); if (passwordKey != null) @@ -54,6 +57,7 @@ namespace keepass2android var dbv3 = importer.OpenDatabase(hashingStream, password, keyfileStream); db.Name = dbv3.Name; + db.KeyEncryptionRounds = (ulong) dbv3.NumKeyEncRounds; db.RootGroup = ConvertGroup(dbv3.RootGroup); if (dbv3.Algorithm == PwEncryptionAlgorithm.Rjindal) { @@ -126,7 +130,11 @@ namespace keepass2android { var entry = groupV3.GetEntryAt(i); if (entry.IsMetaStream) + { + _metaStreams.Add(entry); continue; + } + pwGroup.AddEntry(ConvertEntry(entry), true); } @@ -251,8 +259,6 @@ namespace keepass2android db.RootGroup.Level = -1; AssignParent(kpDatabase.RootGroup, db, groupV3s); - - foreach (PwEntry e in kpDatabase.RootGroup.GetEntries(true)) { @@ -263,12 +269,25 @@ namespace keepass2android db.Entries.Add(entryV3); } + //add meta stream entries: + if (db.Groups.Any()) + { + foreach (var metaEntry in _metaStreams) + { + metaEntry.GroupId = db.Groups.First().Id.Id; + db.Entries.Add(metaEntry); + } + + } + HashingStreamEx hashedStream = new HashingStreamEx(stream, true, null); PwDbV3Output output = new PwDbV3Output(db, hashedStream); output.Output(); hashedStream.Close(); HashOfLastStream = hashedStream.Hash; + + kpDatabase.HashOfLastIO = kpDatabase.HashOfFileOnDisk = HashOfLastStream; stream.Close(); } @@ -277,6 +296,46 @@ namespace keepass2android get { return false; } } + public bool CanHaveMultipleAttachments + { + get { return false; } + } + + public bool CanHaveCustomFields + { + get { return false; } + } + + public bool HasDefaultUsername + { + get { return false; } + } + + public bool HasDatabaseName + { + get { return false; } + } + + public bool SupportsAttachmentKeys + { + get { return false; } + } + + public bool SupportsTags + { + get { return false; } + } + + public bool SupportsOverrideUrl + { + get { return false; } + } + + public bool CanRecycle + { + get { return false; } + } + private void AssignParent(PwGroup kpParent, PwDatabaseV3 dbV3, Dictionary groupV3s) { PwGroupV3 parentV3; @@ -304,6 +363,7 @@ namespace keepass2android { PwGroupV3 toGroup = new PwGroupV3(); toGroup.Name = fromGroup.Name; + //todo remove Android.Util.Log.Debug("KP2A", "save kdb: group " + fromGroup.Name); toGroup.TCreation = new PwDate(ConvertTime(fromGroup.CreationTime)); diff --git a/src/Kp2aBusinessLogic/database/KdbxDatabaseFormat.cs b/src/Kp2aBusinessLogic/database/KdbxDatabaseFormat.cs index 2fe4b0b2..21535d54 100644 --- a/src/Kp2aBusinessLogic/database/KdbxDatabaseFormat.cs +++ b/src/Kp2aBusinessLogic/database/KdbxDatabaseFormat.cs @@ -38,5 +38,45 @@ namespace keepass2android { get { return true; } } + + public bool CanHaveMultipleAttachments + { + get { return true; } + } + + public bool CanHaveCustomFields + { + get { return true; } + } + + public bool HasDefaultUsername + { + get { return true; } + } + + public bool HasDatabaseName + { + get { return true; } + } + + public bool SupportsAttachmentKeys + { + get { return true; } + } + + public bool SupportsTags + { + get { return true; } + } + + public bool SupportsOverrideUrl + { + get { return true; } + } + + public bool CanRecycle + { + get { return true; } + } } } \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/database/edit/DeleteEntry.cs b/src/Kp2aBusinessLogic/database/edit/DeleteEntry.cs index 4ab36ca7..4fcd4139 100644 --- a/src/Kp2aBusinessLogic/database/edit/DeleteEntry.cs +++ b/src/Kp2aBusinessLogic/database/edit/DeleteEntry.cs @@ -36,7 +36,7 @@ namespace keepass2android { get { - return CanRecycleGroup(_entry.ParentGroup); + return App.GetDb().DatabaseFormat.CanRecycle && CanRecycleGroup(_entry.ParentGroup); } } @@ -62,7 +62,8 @@ namespace keepass2android if(pgParent != null) { pgParent.Entries.Remove(pe); - + //TODO check if RecycleBin is deleted + //TODO no recycle bin in KDB if ((DeletePermanently) || (!CanRecycle)) { @@ -85,7 +86,7 @@ namespace keepass2android } else // Recycle { - EnsureRecycleBin(ref pgRecycleBin, ref bUpdateGroupList); + EnsureRecycleBinExists(ref pgRecycleBin, ref bUpdateGroupList); pgRecycleBin.AddEntry(pe, true, true); pe.Touch(false); @@ -97,6 +98,9 @@ namespace keepass2android Db.Dirty.Add(pgParent); // Mark new parent dirty Db.Dirty.Add(pgRecycleBin); + // mark root dirty if recycle bin was created + if (bUpdateGroupList) + Db.Dirty.Add(Db.Root); } else { // Let's not bother recovering from a failure to save a deleted entry. It is too much work. App.LockDatabase(false); diff --git a/src/Kp2aBusinessLogic/database/edit/DeleteGroup.cs b/src/Kp2aBusinessLogic/database/edit/DeleteGroup.cs index 7b1e01bd..6341eb91 100644 --- a/src/Kp2aBusinessLogic/database/edit/DeleteGroup.cs +++ b/src/Kp2aBusinessLogic/database/edit/DeleteGroup.cs @@ -56,7 +56,7 @@ namespace keepass2android { get { - return CanRecycleGroup(_group); + return App.GetDb().DatabaseFormat.CanRecycle && CanRecycleGroup(_group); } } @@ -91,8 +91,8 @@ namespace keepass2android } else // Recycle { - bool bDummy = false; - EnsureRecycleBin(ref pgRecycleBin, ref bDummy); + bool groupListUpdateRequired = false; + EnsureRecycleBinExists(ref pgRecycleBin, ref groupListUpdateRequired); pgRecycleBin.AddGroup(pg, true, true); pg.Touch(false); @@ -106,6 +106,10 @@ namespace keepass2android } //Mark old parent dirty: Db.Dirty.Add(pgParent); + + // mark root dirty if recycle bin was created + if (groupListUpdateRequired) + Db.Dirty.Add(Db.Root); } else { // Let's not bother recovering from a failure to save a deleted group. It is too much work. App.LockDatabase(false); diff --git a/src/Kp2aBusinessLogic/database/edit/DeleteRunnable.cs b/src/Kp2aBusinessLogic/database/edit/DeleteRunnable.cs index 5bde910d..df2c3f7e 100644 --- a/src/Kp2aBusinessLogic/database/edit/DeleteRunnable.cs +++ b/src/Kp2aBusinessLogic/database/edit/DeleteRunnable.cs @@ -66,7 +66,7 @@ namespace keepass2android } - protected void EnsureRecycleBin(ref PwGroup pgRecycleBin, + protected void EnsureRecycleBinExists(ref PwGroup pgRecycleBin, ref bool bGroupListUpdateRequired) { if ((Db == null) || (Db.KpDatabase == null)) { return; } @@ -87,7 +87,7 @@ namespace keepass2android }; Db.KpDatabase.RootGroup.AddGroup(pgRecycleBin, true); - + Db.Groups[pgRecycleBin.Uuid] = pgRecycleBin; Db.KpDatabase.RecycleBinUuid = pgRecycleBin.Uuid; bGroupListUpdateRequired = true; diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs index 09b574d3..a5ba6de9 100644 --- a/src/Kp2aUnitTests/MainActivity.cs +++ b/src/Kp2aUnitTests/MainActivity.cs @@ -31,9 +31,13 @@ namespace Kp2aUnitTests //runner.AddTests(new List { typeof(TestSaveDb) }); //runner.AddTests(new List { typeof(TestCachingFileStorage) }); //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdb1WithKeyfileOnly")); - runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadEditSaveWithSyncKdb")); - runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadAndSave_TestIdenticalFiles_kdb")); - runner.AddTests(typeof(TestSaveDb).GetMethod("TestCreateSaveAndLoad_TestIdenticalFiles_kdb")); + + //runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadEditSaveWithSyncKdb")); + //runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadAndSave_TestIdenticalFiles_kdb")); + //runner.AddTests(typeof(TestSaveDb).GetMethod("TestCreateSaveAndLoad_TestIdenticalFiles_kdb")); + + runner.AddTests(typeof(TestSaveDb).GetMethod("TestSaveTwice_kdb")); + //runner.AddTests(typeof(TestLoadDb).GetMethod("LoadAndSaveFromRemote1And1Ftp")); //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly")); //runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles")); diff --git a/src/Kp2aUnitTests/TestSaveDb.cs b/src/Kp2aUnitTests/TestSaveDb.cs index f07af98f..7428ea0e 100644 --- a/src/Kp2aUnitTests/TestSaveDb.cs +++ b/src/Kp2aUnitTests/TestSaveDb.cs @@ -373,6 +373,32 @@ namespace Kp2aUnitTests + } + + [TestMethod] + public void TestSaveTwice_kdb() + { + var filename = DefaultDirectory + "savetwice.kdb"; + //create the default database: + IKp2aApp app = SetupAppWithDatabase(filename); + DisplayGroups(app, "After create 1"); + //save it and reload it so we have a base version + Android.Util.Log.Debug("KP2A", "-- Save first version -- "); + SaveDatabase(app); + Android.Util.Log.Debug("KP2A", "-- Load DB -- "); + app = LoadDatabase(filename, DefaultPassword, DefaultKeyfile); + DisplayGroups(app, "After reload"); + + //save the database (first time): + Android.Util.Log.Debug("KP2A", "-- Save db first time "); + SaveDatabase(app); + + Android.Util.Log.Debug("KP2A", "-- Save db second time "); + SaveDatabase(app); + + //make sure the right question was asked + Assert.AreEqual(null, ((TestKp2aApp)app).LastYesNoCancelQuestionTitle); + } diff --git a/src/java/KP2AKdbLibrary/bin/kp2akdblibrary.jar b/src/java/KP2AKdbLibrary/bin/kp2akdblibrary.jar index db50f3f9..21fe6725 100644 Binary files a/src/java/KP2AKdbLibrary/bin/kp2akdblibrary.jar and b/src/java/KP2AKdbLibrary/bin/kp2akdblibrary.jar differ diff --git a/src/keepass2android/EntryEditActivity.cs b/src/keepass2android/EntryEditActivity.cs index a0a57357..3b0a5b74 100644 --- a/src/keepass2android/EntryEditActivity.cs +++ b/src/keepass2android/EntryEditActivity.cs @@ -261,9 +261,8 @@ namespace keepass2android TextView keyView = (TextView) ees.FindViewById(Resource.Id.title); keyView.RequestFocus(); - - }; + SetAddExtraStringEnabled(); ((CheckBox)FindViewById(Resource.Id.entry_expires_checkbox)).CheckedChange += (sender, e) => { @@ -280,6 +279,12 @@ namespace keepass2android } + private void SetAddExtraStringEnabled() + { + if (!App.Kp2a.GetDb().DatabaseFormat.CanHaveCustomFields) + ((Button)FindViewById(Resource.Id.add_advanced)).Visibility = ViewStates.Gone; + } + private void MakePasswordVisibleOrHidden() { TextView password = (TextView) FindViewById(Resource.Id.entry_password); @@ -663,7 +668,12 @@ namespace keepass2android foreach (KeyValuePair pair in State.Entry.Binaries.OrderBy(p => p.Key) ) { String key = pair.Key; - Button binaryButton = new Button(this) {Text = key}; + String label = key; + if ((String.IsNullOrEmpty(label) || (!App.Kp2a.GetDb().DatabaseFormat.SupportsAttachmentKeys))) + { + label = ""; + } + Button binaryButton = new Button(this) {Text = label}; binaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuDelete),null, null, null); binaryButton.Click += (sender, e) => @@ -680,6 +690,9 @@ namespace keepass2android Button addBinaryButton = new Button(this) {Text = GetString(Resource.String.add_binary)}; addBinaryButton.SetCompoundDrawablesWithIntrinsicBounds( Resources.GetDrawable(Android.Resource.Drawable.IcMenuAdd) , null, null, null); + addBinaryButton.Enabled = true; + if (!App.Kp2a.GetDb().DatabaseFormat.CanHaveMultipleAttachments) + addBinaryButton.Enabled = !State.Entry.Binaries.Any(); addBinaryButton.Click += (sender, e) => { Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false); @@ -837,8 +850,26 @@ namespace keepass2android PopulateBinaries(); - PopulateText(Resource.Id.entry_override_url, State.Entry.OverrideUrl); - PopulateText(Resource.Id.entry_tags, StrUtil.TagsToString(State.Entry.Tags, true)); + if (App.Kp2a.GetDb().DatabaseFormat.SupportsOverrideUrl) + { + PopulateText(Resource.Id.entry_override_url, State.Entry.OverrideUrl); + } + else + { + FindViewById(Resource.Id.entry_override_url_label).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.entry_override_url).Visibility = ViewStates.Gone; + } + + if (App.Kp2a.GetDb().DatabaseFormat.SupportsTags) + { + PopulateText(Resource.Id.entry_tags, StrUtil.TagsToString(State.Entry.Tags, true)); + } + else + { + FindViewById(Resource.Id.entry_tags_label).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.entry_tags).Visibility = ViewStates.Gone; + } + UpdateExpires(); } diff --git a/src/keepass2android/settings/DatabaseSettingsActivity.cs b/src/keepass2android/settings/DatabaseSettingsActivity.cs index 6000b0f3..81d120ce 100644 --- a/src/keepass2android/settings/DatabaseSettingsActivity.cs +++ b/src/keepass2android/settings/DatabaseSettingsActivity.cs @@ -89,56 +89,71 @@ namespace keepass2android rounds.Enabled = db.CanWrite; Preference defaultUser = FindPreference(GetString(Resource.String.default_username_key)); - defaultUser.Enabled = db.CanWrite; - ((EditTextPreference)defaultUser).EditText.Text = db.KpDatabase.DefaultUserName; - ((EditTextPreference)defaultUser).Text = db.KpDatabase.DefaultUserName; - defaultUser.PreferenceChange += (sender, e) => + if (!db.DatabaseFormat.HasDefaultUsername) { - DateTime previousUsernameChanged = db.KpDatabase.DefaultUserNameChanged; - String previousUsername = db.KpDatabase.DefaultUserName; - db.KpDatabase.DefaultUserName = e.NewValue.ToString(); - - SaveDb save = new SaveDb(this, App.Kp2a, new ActionOnFinish( (success, message) => + ((PreferenceScreen) FindPreference(GetString(Resource.String.db_key))).RemovePreference(defaultUser); + } + else + { + defaultUser.Enabled = db.CanWrite; + ((EditTextPreference)defaultUser).EditText.Text = db.KpDatabase.DefaultUserName; + ((EditTextPreference)defaultUser).Text = db.KpDatabase.DefaultUserName; + defaultUser.PreferenceChange += (sender, e) => { - if (!success) - { - db.KpDatabase.DefaultUserName = previousUsername; - db.KpDatabase.DefaultUserNameChanged = previousUsernameChanged; - Toast.MakeText(this, message, ToastLength.Long).Show(); - } - })); - ProgressTask pt = new ProgressTask(App.Kp2a, this, save); - pt.Run(); - }; + DateTime previousUsernameChanged = db.KpDatabase.DefaultUserNameChanged; + String previousUsername = db.KpDatabase.DefaultUserName; + db.KpDatabase.DefaultUserName = e.NewValue.ToString(); + SaveDb save = new SaveDb(this, App.Kp2a, new ActionOnFinish((success, message) => + { + if (!success) + { + db.KpDatabase.DefaultUserName = previousUsername; + db.KpDatabase.DefaultUserNameChanged = previousUsernameChanged; + Toast.MakeText(this, message, ToastLength.Long).Show(); + } + })); + ProgressTask pt = new ProgressTask(App.Kp2a, this, save); + pt.Run(); + }; + + } + + Preference databaseName = FindPreference(GetString(Resource.String.database_name_key)); - databaseName.Enabled = db.CanWrite; - ((EditTextPreference)databaseName).EditText.Text = db.KpDatabase.Name; - ((EditTextPreference)databaseName).Text = db.KpDatabase.Name; - databaseName.PreferenceChange += (sender, e) => + if (!db.DatabaseFormat.HasDatabaseName) { - DateTime previousNameChanged = db.KpDatabase.NameChanged; - String previousName = db.KpDatabase.Name; - db.KpDatabase.Name = e.NewValue.ToString(); - - SaveDb save = new SaveDb(this, App.Kp2a, new ActionOnFinish( (success, message) => - { - if (!success) + ((PreferenceScreen) FindPreference(GetString(Resource.String.db_key))).RemovePreference(databaseName); + } + else + { + databaseName.Enabled = db.CanWrite; + ((EditTextPreference) databaseName).EditText.Text = db.KpDatabase.Name; + ((EditTextPreference) databaseName).Text = db.KpDatabase.Name; + databaseName.PreferenceChange += (sender, e) => { - db.KpDatabase.Name = previousName; - db.KpDatabase.NameChanged = previousNameChanged; - Toast.MakeText(this, message, ToastLength.Long).Show(); - } - else - { - // Name is reflected in notification, so update it - App.Kp2a.UpdateOngoingNotification(); - } - })); - ProgressTask pt = new ProgressTask(App.Kp2a, this, save); - pt.Run(); - }; + DateTime previousNameChanged = db.KpDatabase.NameChanged; + String previousName = db.KpDatabase.Name; + db.KpDatabase.Name = e.NewValue.ToString(); + SaveDb save = new SaveDb(this, App.Kp2a, new ActionOnFinish((success, message) => + { + if (!success) + { + db.KpDatabase.Name = previousName; + db.KpDatabase.NameChanged = previousNameChanged; + Toast.MakeText(this, message, ToastLength.Long).Show(); + } + else + { + // Name is reflected in notification, so update it + App.Kp2a.UpdateOngoingNotification(); + } + })); + ProgressTask pt = new ProgressTask(App.Kp2a, this, save); + pt.Run(); + }; + } Preference changeMaster = FindPreference(GetString(Resource.String.master_pwd_key)); if (App.Kp2a.GetDb().CanWrite) {