Introduced IDatabaseLoader

(kdb not yet working)
This commit is contained in:
Philipp Crocoll 2014-01-25 19:38:12 -08:00
parent ee4d40eb32
commit 6e0645559d
19 changed files with 255 additions and 65 deletions

View File

@ -0,0 +1,13 @@
using System.IO;
using KeePassLib.Interfaces;
using KeePassLib.Keys;
namespace KeePassLib
{
public interface IDatabaseLoader
{
void PopulateDatabaseFromStream(PwDatabase db, CompositeKey key, Stream s, IStatusLogger slLogger);
byte[] HashOfLastStream { get; }
}
}

View File

@ -20,7 +20,7 @@
<DebugType>full</DebugType>
<Optimize>False</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_KEYTRANSFORM;INCLUDE_FILECHOOSER;INCLUDE_JAVAFILESTORAGE</DefineConstants>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_KEYTRANSFORM;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>False</ConsolePause>
@ -58,6 +58,7 @@
<Reference Include="Mono.Security" />
</ItemGroup>
<ItemGroup>
<Compile Include="IDatabaseLoader.cs" />
<Compile Include="Kp2aLog.cs" />
<Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Resources\KLRes.Generated.cs" />
@ -153,11 +154,11 @@
<ItemGroup>
<AndroidResource Include="Resources\values\Strings.xml" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" />
<ItemGroup>
<ProjectReference Include="..\kp2akeytransform\kp2akeytransform.csproj" Condition="!$(DefineConstants.Contains('EXCLUDE_KEYTRANSFORM'))">
<Project>{A57B3ACE-5634-469A-88C4-858BB409F356}</Project>
<Name>kp2akeytransform</Name>
<ProjectReference Include="..\KP2AKdbLibraryBinding\KP2AKdbLibraryBinding.csproj">
<Project>{70d3844a-d9fa-4a64-b205-a84c6a822196}</Project>
<Name>KP2AKdbLibraryBinding</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" />
</Project>

View File

@ -3,6 +3,7 @@ using Android.App;
using System.IO;
using Android.Content;
using Android.OS;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using keepass2android.Io;
@ -23,7 +24,7 @@ namespace keepass2android
/// <summary>
/// Loads the specified data as the currently open database, as unlocked.
/// </summary>
void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, ProgressDialogStatusLogger statusLogger);
void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, ProgressDialogStatusLogger statusLogger, IDatabaseLoader databaseLoader);
/// <summary>
/// Returns the current database

View File

@ -93,34 +93,15 @@ namespace keepass2android
/// <summary>
/// Do not call this method directly. Call App.Kp2a.LoadDatabase instead.
/// </summary>
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, CompositeKey compositeKey, ProgressDialogStatusLogger status)
public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, CompositeKey compositeKey, ProgressDialogStatusLogger status, IDatabaseLoader databaseLoader)
{
PwDatabase pwDatabase = new PwDatabase();
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
var filename = fileStorage.GetFilenameWithoutPathAndExt(iocInfo);
try
{
var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
pwDatabase.Open(databaseData ?? fileStorage.OpenFileForRead(iocInfo), filename, iocInfo, compositeKey, status);
LastFileVersion = fileVersion;
}
catch (InvalidCompositeKeyException)
{
KcpPassword passwordKey = (KcpPassword)compositeKey.GetUserKey(typeof(KcpPassword));
if ((passwordKey != null) && (passwordKey.Password.ReadString() == "") && (compositeKey.UserKeyCount > 1))
{
//if we don't get a password, we don't know whether this means "empty password" or "no password"
//retry without password:
compositeKey.RemoveUserKey(compositeKey.GetUserKey(typeof (KcpPassword)));
var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
//don't reuse the memory stream databaseData: it's already closed.
//We could try to avoid reading the file again here, but probably the case is rare enough so this is ok.
pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), filename, iocInfo, compositeKey, status);
LastFileVersion = fileVersion; }
else throw;
}
Stream s = databaseData ?? fileStorage.OpenFileForRead(iocInfo);
var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
PopulateDatabaseFromStream(pwDatabase, s, iocInfo, compositeKey, status, databaseLoader);
LastFileVersion = fileVersion;
status.UpdateSubMessage("");
@ -133,7 +114,14 @@ namespace keepass2android
SearchHelper = new SearchDbHelper(app);
}
protected virtual void PopulateDatabaseFromStream(PwDatabase pwDatabase, Stream s, IOConnectionInfo iocInfo, CompositeKey compositeKey, ProgressDialogStatusLogger status, IDatabaseLoader databaseLoader)
{
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
var filename = fileStorage.GetFilenameWithoutPathAndExt(iocInfo);
pwDatabase.Open(s, filename, iocInfo, compositeKey, status, databaseLoader);
}
public PwGroup SearchForText(String str) {
PwGroup group = SearchHelper.SearchForText(this, str);
@ -162,7 +150,7 @@ namespace keepass2android
}
public void SaveData(Context ctx) {
public virtual void SaveData(Context ctx) {
KpDatabase.UseFileTransactions = _app.GetBooleanPreference(PreferenceKey.UseFileTransactions);
using (IWriteTransaction trans = _app.GetFileStorage(Ioc).OpenWriteTransaction(Ioc, KpDatabase.UseFileTransactions))

View File

@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Security.Cryptography;
using Android.Content;
using Com.Keepassdroid.Database;
using Com.Keepassdroid.Database.Exception;
using Java.Lang;
using KeePassLib;
using KeePassLib.Cryptography;
using KeePassLib.Cryptography.Cipher;
using KeePassLib.Interfaces;
using KeePassLib.Keys;
using Exception = System.Exception;
namespace keepass2android
{
class KdbDatabaseLoader: IDatabaseLoader
{
private Context _ctx;
public KdbDatabaseLoader(Context ctx)
{
_ctx = ctx;
}
public void PopulateDatabaseFromStream(PwDatabase db, CompositeKey key, Stream s, IStatusLogger slLogger)
{
var importer = new Com.Keepassdroid.Database.Load.ImporterV3();
var hashingStream = new HashingStreamEx(s, false, new SHA256Managed());
string password = "";//no need to distinguish between null and "" because empty passwords are invalid (and null is not allowed)
KcpPassword passwordKey = (KcpPassword)key.GetUserKey(typeof(KcpPassword));
if (passwordKey != null)
{
password = passwordKey.Password.ReadString();
}
KcpKeyFile passwordKeyfile = (KcpKeyFile)key.GetUserKey(typeof(KcpKeyFile));
string keyfile = "";
if (passwordKeyfile != null)
{
keyfile = passwordKeyfile.Path;
}
try
{
var dbv3 = importer.OpenDatabase(hashingStream, password, keyfile);
db.Name = dbv3.Name;
}
catch (InvalidPasswordException e) {
return;
}
catch (Java.IO.FileNotFoundException e)
{
throw new FileNotFoundException(
e.Message, e);
}
catch (Java.Lang.Exception e)
{
throw new Exception(e.LocalizedMessage ??
e.Message ??
e.GetType().Name, e);
}
HashOfLastStream = hashingStream.Hash;
if (HashOfLastStream == null)
throw new Exception("hashing didn't work"); //todo remove
}
public byte[] HashOfLastStream { get; private set; }
}
}

View File

@ -0,0 +1,31 @@
using System.IO;
using KeePassLib;
using KeePassLib.Interfaces;
using KeePassLib.Keys;
using KeePassLib.Serialization;
namespace keepass2android
{
public class KdbxDatabaseLoader : IDatabaseLoader
{
private readonly KdbxFormat _format;
public KdbxDatabaseLoader(KdbxFormat format)
{
_format = format;
}
public void PopulateDatabaseFromStream(PwDatabase db, CompositeKey key, Stream s, IStatusLogger slLogger)
{
KdbxFile kdbx = new KdbxFile(db);
kdbx.DetachBinaries = db.DetachBinaries;
kdbx.Load(s, _format, slLogger);
HashOfLastStream = kdbx.HashOfFileOnDisk;
s.Close();
}
public byte[] HashOfLastStream { get; private set; }
}
}

View File

@ -19,6 +19,8 @@ using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
@ -50,10 +52,24 @@ namespace keepass2android
try
{
StatusLogger.UpdateMessage(UiStringKey.loading_database);
MemoryStream memoryStream = _databaseData == null ? null : _databaseData.Result;
_app.LoadDatabase(_ioc, memoryStream, _compositeKey, StatusLogger);
SaveFileData(_ioc, _keyfileOrProvider);
//get the stream data into a single stream variable (databaseStream) regardless whether its preloaded or not:
MemoryStream preloadedMemoryStream = _databaseData == null ? null : _databaseData.Result;
MemoryStream databaseStream;
if (preloadedMemoryStream != null)
databaseStream = preloadedMemoryStream;
else
{
using (Stream s = _app.GetFileStorage(_ioc).OpenFileForRead(_ioc))
{
databaseStream = new MemoryStream();
s.CopyTo(databaseStream);
databaseStream.Seek(0, SeekOrigin.Begin);
}
}
//ok, try to load the database. Let's start with Kdbx format and retry later if that is the wrong guess:
IDatabaseLoader loader = new KdbxDatabaseLoader(KdbpFile.GetFormatToUse(_ioc));
TryLoad(databaseStream, loader);
}
catch (KeyFileException)
{
@ -73,11 +89,6 @@ namespace keepass2android
Finish(false, _app.GetResourceString(UiStringKey.ErrorOcurred) + " " + message);
return;
}
catch (OldFormatException )
{
Finish(false, "Cannot open Keepass 1.x database. As explained in the app description, Keepass2Android is for Keepass 2 only! Please use the desktop application to convert your database to the new file format!");
return;
}
catch (Exception e)
{
Kp2aLog.Log("Exception: " + e);
@ -85,10 +96,48 @@ namespace keepass2android
return;
}
Kp2aLog.Log("LoadDB OK");
Finish(true);
}
private void TryLoad(MemoryStream databaseStream, IDatabaseLoader loader)
{
//create a copy of the stream so we can try again if we get an exception which indicates we should change parameters
//This is not optimal in terms of (short-time) memory usage but is hard to avoid because the Keepass library closes streams also in case of errors.
//Alternatives would involve increased traffic (if file is on remote) and slower loading times, so this seems to be the best choice.
MemoryStream workingCopy = new MemoryStream();
databaseStream.CopyTo(workingCopy);
workingCopy.Seek(0, SeekOrigin.Begin);
//reset stream if we need to reuse it later:
databaseStream.Seek(0, SeekOrigin.Begin);
//now let's go:
try
{
_app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, loader);
SaveFileData(_ioc, _keyfileOrProvider);
Kp2aLog.Log("LoadDB OK");
Finish(true);
}
catch (OldFormatException)
{
TryLoad(databaseStream, new KdbDatabaseLoader(Application.Context));
}
catch (InvalidCompositeKeyException)
{
KcpPassword passwordKey = (KcpPassword)_compositeKey.GetUserKey(typeof(KcpPassword));
if ((passwordKey != null) && (passwordKey.Password.ReadString() == "") && (_compositeKey.UserKeyCount > 1))
{
//if we don't get a password, we don't know whether this means "empty password" or "no password"
//retry without password:
_compositeKey.RemoveUserKey(passwordKey);
//retry:
TryLoad(databaseStream, loader);
}
else throw;
}
}
private void SaveFileData(IOConnectionInfo ioc, String keyfileOrProvider) {
if (!_rememberKeyfile)

View File

@ -16,6 +16,13 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidUseLatestPlatformSdk />
<TargetFrameworkVersion>v4.2</TargetFrameworkVersion>
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
<AndroidStoreUncompressedFileExtensions />
<MandroidI18n />
<JavaMaximumHeapSize />
<JavaOptions />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -96,8 +103,12 @@
<Project>{53A9CB7F-6553-4BC0-B56B-9410BB2E59AA}</Project>
<Name>Kp2aBusinessLogic</Name>
</ProjectReference>
<ProjectReference Include="..\KP2AKdbLibraryBinding\KP2AKdbLibraryBinding.csproj">
<Project>{70d3844a-d9fa-4a64-b205-a84c6a822196}</Project>
<Name>KP2AKdbLibraryBinding</Name>
</ProjectReference>
<ProjectReference Include="..\monodroid-unittesting\MonoDroidUnitTesting\MonoDroidUnitTesting.csproj">
<Project>{a5f8fb02-00e0-4335-91ef-aeaa2c2f3c48}</Project>
<Project>{A5F8FB02-00E0-4335-91EF-AEAA2C2F3C48}</Project>
<Name>MonoDroidUnitTesting</Name>
</ProjectReference>
</ItemGroup>

View File

@ -20,13 +20,14 @@ namespace Kp2aUnitTests
// Run all tests from this assembly
//runner.AddTests(Assembly.GetExecutingAssembly());
//runner.AddTests(new List<Type> { typeof(TestSynchronizeCachedDatabase)});
runner.AddTests(typeof(TestLoadDb).GetMethod("LoadErrorWithCertificateTrustFailure"));
//runner.AddTests(new List<Type> { typeof(TestSaveDb) });
//runner.AddTests(typeof(TestLoadDb).GetMethod("LoadErrorWithCertificateTrustFailure"));
//runner.AddTests(new List<Type> { typeof(TestLoadDb) });
//runner.AddTests(new List<Type> { typeof(TestCachingFileStorage) });
//runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote"));
//runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));
//runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));
runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdb1"));
runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadWithKeyfileOnly"));
runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));
runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));
return runner;
}
}

View File

@ -43,7 +43,7 @@ namespace Kp2aUnitTests
//ensure the the database can be loaded from file:
PwDatabase loadedDb = new PwDatabase();
loadedDb.Open(ioc, new CompositeKey(), null);
loadedDb.Open(ioc, new CompositeKey(), null, new KdbxDatabaseLoader(KdbxFormat.Default));
//Check whether the databases are equal
AssertDatabasesAreEqual(loadedDb, app.GetDb().KpDatabase);

View File

@ -5,6 +5,7 @@ using System.Net.Security;
using Android.App;
using Android.Content;
using Android.OS;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using keepass2android;
@ -48,10 +49,9 @@ namespace Kp2aUnitTests
throw new NotImplementedException();
}
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey,
ProgressDialogStatusLogger statusLogger)
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, ProgressDialogStatusLogger statusLogger, IDatabaseLoader databaseLoader)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, compKey, statusLogger);
_db.LoadData(this, ioConnectionInfo, memoryStream, compKey, statusLogger, databaseLoader);
}
public Database GetDb()
{

View File

@ -20,10 +20,12 @@ namespace Kp2aUnitTests
app.CreateNewDatabase();
bool loadSuccesful = false;
var key = CreateKey(password, keyfile);
string loadErrorMessage = "";
LoadDb task = new LoadDb(app, new IOConnectionInfo { Path = TestDbDirectory+filenameWithoutDir }, null,
key, keyfile, new ActionOnFinish((success, message) =>
{
loadErrorMessage = message;
if (!success)
Android.Util.Log.Debug("KP2ATest", "error loading db: " + message);
loadSuccesful = success;
@ -34,7 +36,7 @@ namespace Kp2aUnitTests
pt.Run();
pt.JoinWorkerThread();
Android.Util.Log.Debug("KP2ATest", "PT.run finished");
Assert.IsTrue(loadSuccesful, "didn't succesfully load database :-(");
Assert.IsTrue(loadSuccesful, "didn't succesfully load database :-( "+loadErrorMessage);
Assert.AreEqual(6,app.GetDb().KpDatabase.RootGroup.Groups.Count());
Assert.AreEqual(2,app.GetDb().KpDatabase.RootGroup.Entries.Count());
@ -48,6 +50,14 @@ namespace Kp2aUnitTests
{
RunLoadTest("passwordonly.kdbx", DefaultPassword, "");
}
[TestMethod]
public void TestLoadKdb1()
{
RunLoadTest("test1.kdb", "12345", "");
}
[TestMethod]
public void TestLoadWithKeyfileOnly()
{

View File

@ -2,8 +2,8 @@
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="keepass2android.kp2akeytransform"
package="keepass2android.KdbLibrary"
android:versionCode="1"
android:versionName="1.0" >
@ -7,8 +7,7 @@
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<application
android:allowBackup="true">
<application>
</application>
</manifest>

View File

@ -835,7 +835,7 @@ namespace keepass2android
}
private MemoryStream LoadDbFile()
private MemoryStream PreloadDbFile()
{
if (KdbpFile.GetFormatToUse(_ioConnection) == KdbxFormat.ProtocolBuffers)
{
@ -998,7 +998,7 @@ namespace keepass2android
if (_loadDbTask == null && _prefs.GetBoolean(GetString(Resource.String.PreloadDatabaseEnabled_key), true))
{
// Create task to kick off file loading while the user enters the password
_loadDbTask = Task.Factory.StartNew<MemoryStream>(LoadDbFile);
_loadDbTask = Task.Factory.StartNew<MemoryStream>(PreloadDbFile);
}
}
}

View File

@ -24,6 +24,7 @@ using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Widget;
using KeePassLib;
using KeePassLib.Cryptography.Cipher;
using KeePassLib.Keys;
using KeePassLib.Serialization;
@ -108,9 +109,9 @@ namespace keepass2android
Application.Context.SendBroadcast(new Intent(Intents.DatabaseLocked));
}
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger)
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger, IDatabaseLoader databaseLoader)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger);
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseLoader);
UpdateOngoingNotification();
}

View File

@ -30,7 +30,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;INCLUDE_KEYBOARD;INCLUDE_KEYTRANSFORM;INCLUDE_FILECHOOSER;INCLUDE_JAVAFILESTORAGE</DefineConstants>
<DefineConstants>DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_KEYTRANSFORM;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>False</ConsolePause>
@ -911,4 +911,10 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable\ic_storage_sftp.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\4-collections-new-label.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\device_access_new_account.png" />
</ItemGroup>
</Project>

View File

@ -65,7 +65,7 @@ namespace MonoDroidUnitTesting {
protected override void OnStart() {
base.OnStart();
new Handler().Post(this.RunTests);
//new Handler().Post(this.RunTests);
}
protected virtual void OnTestRunStarted() { }
@ -171,6 +171,9 @@ namespace MonoDroidUnitTesting {
protected override void OnResume() {
base.OnResume();
TestRunner = null;
new Handler().Post(this.RunTests);
if (TestRunner != null) {
// Only remember this view if the test run finished and therefore the previous activity has been restored.
ISharedPreferencesEditor e = GetPreferences().Edit();