Merge branch 'filecorruptionhandling'

This commit is contained in:
Philipp Crocoll
2018-04-11 06:28:33 +02:00
13 changed files with 276 additions and 103 deletions

View File

@@ -52,7 +52,7 @@ namespace keepass2android
/// <summary>
/// Tell the app that the file from ioc was opened with keyfile.
/// </summary>
void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile);
void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile, string displayName = "");
/// <summary>
/// Creates a new database and returns it

View File

@@ -11,9 +11,11 @@ using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Preferences;
using Android.Support.V13.App;
using Android.Support.V4.App;
using Java.IO;
using Java.Util;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using ActivityCompat = Android.Support.V13.App.ActivityCompat;
@@ -379,7 +381,14 @@ namespace keepass2android.Io
{
if (ioc.IsLocalFile())
{
if (IsLocalFileFlaggedReadOnly(ioc))
if (IsLocalBackup(ioc))
{
if (reason != null)
reason.Result = UiStringKey.ReadOnlyReason_LocalBackup;
return true;
}
if (IsLocalFileFlaggedReadOnly(ioc))
{
if (reason != null)
reason.Result = UiStringKey.ReadOnlyReason_ReadOnlyFlag;
@@ -400,7 +409,20 @@ namespace keepass2android.Io
return false;
}
private bool IsLocalFileFlaggedReadOnly(IOConnectionInfo ioc)
private readonly Dictionary<string, bool> _isLocalBackupCache = new Dictionary<string, bool>();
private bool IsLocalBackup(IOConnectionInfo ioc)
{
bool result;
if (_isLocalBackupCache.TryGetValue(ioc.Path, out result))
return result;
result = (PreferenceManager.GetDefaultSharedPreferences(Application.Context)
.GetBoolean(IoUtil.GetIocPrefKey(ioc, "is_local_backup"), false));
_isLocalBackupCache[ioc.Path] = result;
return result;
}
private bool IsLocalFileFlaggedReadOnly(IOConnectionInfo ioc)
{
//see http://stackoverflow.com/a/33292700/292233
try

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Android.Content;
using Android.OS;
using Java.IO;
using KeePassLib.Serialization;
using KeePassLib.Utility;
namespace keepass2android.Io
{
@@ -125,5 +127,53 @@ namespace keepass2android.Io
return ctx.FilesDir;
}
}
//creates a local ioc where the sourceIoc can be stored to
public static IOConnectionInfo GetInternalIoc(IOConnectionInfo sourceIoc, Context ctx)
{
Java.IO.File internalDirectory = IoUtil.GetInternalDirectory(ctx);
string targetPath = UrlUtil.GetFileName(sourceIoc.Path);
targetPath = targetPath.Trim("|\\?*<\":>+[]/'".ToCharArray());
if (targetPath == "")
targetPath = "internal";
if (new File(internalDirectory, targetPath).Exists())
{
int c = 1;
var ext = UrlUtil.GetExtension(targetPath);
var filenameWithoutExt = UrlUtil.StripExtension(targetPath);
do
{
c++;
targetPath = filenameWithoutExt + c;
if (!String.IsNullOrEmpty(ext))
targetPath += "." + ext;
} while (new File(internalDirectory, targetPath).Exists());
}
return IOConnectionInfo.FromPath(new File(internalDirectory, targetPath).CanonicalPath);
}
public static IOConnectionInfo ImportFileToInternalDirectory(IOConnectionInfo sourceIoc, Context ctx, IKp2aApp app)
{
var targetIoc = GetInternalIoc(sourceIoc, ctx);
IoUtil.Copy(targetIoc, sourceIoc, app);
return targetIoc;
}
public static string GetIocPrefKey(IOConnectionInfo ioc, string suffix)
{
var iocAsHexString = IocAsHexString(ioc);
return "kp2a_ioc_key_" + iocAsHexString + suffix;
}
public static string IocAsHexString(IOConnectionInfo ioc)
{
SHA256Managed sha256 = new SHA256Managed();
string iocAsHexString =
MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray())));
return iocAsHexString;
}
}
}

View File

@@ -85,6 +85,7 @@ namespace keepass2android
AskAddTemplatesMessage,
ReadOnlyReason_PreKitKat,
ReadOnlyReason_ReadOnlyFlag,
ReadOnlyReason_ReadOnlyKitKat
ReadOnlyReason_ReadOnlyKitKat,
ReadOnlyReason_LocalBackup
}
}

View File

@@ -152,27 +152,20 @@ namespace keepass2android
set { _databaseFormat = value; }
}
public static string GetFingerprintPrefKey(IOConnectionInfo ioc)
{
var iocAsHexString = IocAsHexString(ioc);
public string IocAsHexString()
{
return IoUtil.IocAsHexString(Ioc);
}
return "kp2a_ioc_" + iocAsHexString;
}
public static string GetFingerprintPrefKey(IOConnectionInfo ioc)
{
var iocAsHexString = IoUtil.IocAsHexString(ioc);
public string IocAsHexString()
{
return IocAsHexString(Ioc);
}
return "kp2a_ioc_" + iocAsHexString;
}
private static string IocAsHexString(IOConnectionInfo ioc)
{
SHA256Managed sha256 = new SHA256Managed();
string iocAsHexString =
MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray())));
return iocAsHexString;
}
public static string GetFingerprintModePrefKey(IOConnectionInfo ioc)
public static string GetFingerprintModePrefKey(IOConnectionInfo ioc)
{
return GetFingerprintPrefKey(ioc) + "_mode";
}

View File

@@ -308,7 +308,7 @@ namespace keepass2android
// to retry with typing the full password, but that's intended to avoid showing the password to a
// a potentially unauthorized user (feature request https://keepass2android.codeplex.com/workitem/274)
Handler handler = new Handler();
OnFinish onFinish = new AfterLoad(handler, this);
OnFinish onFinish = new AfterLoad(handler, this, _ioConnection);
_performingLoad = true;
LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish);
_loadDbFileTask = null; // prevent accidental re-use
@@ -1446,7 +1446,7 @@ namespace keepass2android
MakePasswordMaskedOrVisible();
Handler handler = new Handler();
OnFinish onFinish = new AfterLoad(handler, this);
OnFinish onFinish = new AfterLoad(handler, this, _ioConnection);
LoadDb task = (KeyProviderType == KeyProviders.Otp)
? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider,
onFinish, this)
@@ -2023,11 +2023,13 @@ namespace keepass2android
private class AfterLoad : OnFinish {
readonly PasswordActivity _act;
private readonly IOConnectionInfo _ioConnection;
public AfterLoad(Handler handler, PasswordActivity act):base(handler)
{
_act = act;
}
public AfterLoad(Handler handler, PasswordActivity act, IOConnectionInfo ioConnection):base(handler)
{
_act = act;
_ioConnection = ioConnection;
}
public override void Run()
@@ -2038,41 +2040,63 @@ namespace keepass2android
_act.ClearEnteredPassword();
_act.BroadcastOpenDatabase();
_act.InvalidCompositeKeyCount = 0;
_act.LoadingErrorCount = 0;
GC.Collect(); // Ensure temporary memory used while loading is collected
GC.Collect(); // Ensure temporary memory used while loading is collected
}
if (Exception != null)
{
_act.LoadingErrorCount++;
}
if (Exception is InvalidCompositeKeyException)
{
_act.InvalidCompositeKeyCount++;
if (_act.UsedFingerprintUnlock)
{
//disable fingerprint unlock if master password changed
_act.ClearFingerprintUnlockData();
_act.InitFingerprintUnlock();
if ((Exception != null) && (Exception.Message == KeePassLib.Resources.KLRes.FileCorrupted))
{
Message = _act.GetString(Resource.String.CorruptDatabaseHelp);
}
Message = _act.GetString(Resource.String.fingerprint_disabled_wrong_masterkey) + " " + _act.GetString(Resource.String.fingerprint_reenable2);
}
else
{
if (_act.InvalidCompositeKeyCount > 1)
{
Message = _act.GetString(Resource.String.RepeatedInvalidCompositeKeyHelp);
}
else
{
Message = _act.GetString(Resource.String.FirstInvalidCompositeKeyError);
}
}
if (Exception is InvalidCompositeKeyException)
{
_act.InvalidCompositeKeyCount++;
if (_act.UsedFingerprintUnlock)
{
//disable fingerprint unlock if master password changed
_act.ClearFingerprintUnlockData();
_act.InitFingerprintUnlock();
}
if ((Exception != null) && (Exception.Message == KeePassLib.Resources.KLRes.FileCorrupted))
{
Message = _act.GetString(Resource.String.CorruptDatabaseHelp);
}
Message = _act.GetString(Resource.String.fingerprint_disabled_wrong_masterkey) + " " +
_act.GetString(Resource.String.fingerprint_reenable2);
}
else
{
if (_act.InvalidCompositeKeyCount > 1)
{
Message = _act.GetString(Resource.String.RepeatedInvalidCompositeKeyHelp);
if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false))
{
Java.Lang.Object changeDb = _act.GetString(Resource.String.menu_change_db);
Message += _act.GetString(Resource.String.HintLocalBackupInvalidCompositeKey, new Java.Lang.Object[] {changeDb});
}
}
else
{
Message = _act.GetString(Resource.String.FirstInvalidCompositeKeyError);
}
}
}
else if (_act.LoadingErrorCount > 1)
{
if (_act._prefs.GetBoolean(IoUtil.GetIocPrefKey(_ioConnection, "has_local_backup"), false))
{
Java.Lang.Object changeDb = _act.GetString(Resource.String.menu_change_db);
Message += _act.GetString(Resource.String.HintLocalBackupOtherError, new Java.Lang.Object[] { changeDb });
}
}
if ((Message != null) && (Message.Length > 150)) //show long messages as dialog
@@ -2113,8 +2137,12 @@ namespace keepass2android
{
get; set;
}
public int LoadingErrorCount
{
get; set;
}
private void BroadcastOpenDatabase()
private void BroadcastOpenDatabase()
{
App.Kp2a.BroadcastDatabaseAction(this, Strings.ActionOpenDatabase);
}

View File

@@ -119,6 +119,7 @@
<string name="NoDonationReminder_key">NoDonationReminder</string>
<string name="UseOfflineCache_key">UseOfflineCache</string>
<string name="CreateBackups_key">CreateBackups_key</string>
<string name="AcceptAllServerCertificates_key">AcceptAllServerCertificates</string>
<string name="CheckForFileChangesOnSave_key">CheckForFileChangesOnSave</string>
<string name="CheckForDuplicateUuids_key">CheckForDuplicateUuids_key</string>

View File

@@ -360,7 +360,14 @@
<string name="UseOfflineCache_title">Database caching</string>
<string name="UseOfflineCache_summary">Keep a copy of remote database files in the application cache directory. This allows to use remote databases even when offline.</string>
<string name="AcceptAllServerCertificates_title">SSL certificates</string>
<string name="CreateBackups_title">Local backups</string>
<string name="CreateBackups_summary">Create a local backup copy after successfully loading a database.</string>
<string name="UpdatingBackup">Updating local backup...</string>
<string name="LocalBackupOf">Local backup of %1$s</string>
<string name="AcceptAllServerCertificates_title">SSL certificates</string>
<string name="AcceptAllServerCertificates_summary">Define the behavior when certificate validation fails. Note: you can install certificates on your device if validation fails!</string>
@@ -636,6 +643,7 @@
<string name="ReadOnlyReason_PreKitKat">It seems like you opened the file from an external app. This way does not support writing. If you want to make changes to the database, please close the database and select Change database. Then open the file from one of the available options if possible.</string>
<string name="ReadOnlyReason_ReadOnlyFlag">The read-only flag is set. Remove this flag if you want to make changes to the database.</string>
<string name="ReadOnlyReason_ReadOnlyKitKat">Writing is not possible because of restrictions introduced in Android KitKat. If you want to make changes to the database, close the database and select Change database. Then open the file using System file picker.</string>
<string name="ReadOnlyReason_LocalBackup">Local backups cannot be modified. You can use Database settings - Export database to export this backup to another location from which you can re-open it. It will then be writable again.</string>
<string name="AddCustomIcon">Add icon from file...</string>
@@ -685,6 +693,15 @@
&#8226; Make sure you have selected the correct database file.
</string>
<string name="HintLocalBackupInvalidCompositeKey">
\n
&#8226; Hint: If you think your database file might be corrupt or you do not remember the master key after modifying it, you can try with the last successfully opened file version by clicking "%1$s" and selecting the local backup.</string>
<string name="HintLocalBackupOtherError">
\n
&#8226; Hint: Keepass2Android has stored the last successfully opened file version in internal memory. You can open it by clicking "%1$s" and selecting the local backup.
</string>
<string name="CorruptDatabaseHelp">
File is corrupted. \n

View File

@@ -462,6 +462,15 @@
android:defaultValue="true"
android:title="@string/UseOfflineCache_title"
android:key="@string/UseOfflineCache_key" />
<CheckBoxPreference
android:enabled="true"
android:persistent="true"
android:summary="@string/CreateBackups_summary"
android:defaultValue="true"
android:title="@string/CreateBackups_title"
android:key="@string/CreateBackups_key" />
<ListPreference
android:key="@string/AcceptAllServerCertificates_key"
android:title="@string/AcceptAllServerCertificates_title"

View File

@@ -39,6 +39,8 @@ using TwofishCipher;
using Keepass2android.Pluginsdk;
using keepass2android.Io;
using keepass2android.addons.OtpKeyProv;
using KeePassLib.Interfaces;
using KeePassLib.Utility;
#if !NoNet
using Keepass2android.Javafilestorage;
using GoogleDriveFileStorage = keepass2android.Io.GoogleDriveFileStorage;
@@ -160,11 +162,68 @@ namespace keepass2android
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger, IDatabaseFormat databaseFormat)
{
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat);
public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey,
ProgressDialogStatusLogger statusLogger, IDatabaseFormat databaseFormat)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
var createBackup = prefs.GetBoolean(Application.Context.GetString(Resource.String.CreateBackups_key), true);
UpdateOngoingNotification();
MemoryStream backupCopy = new MemoryStream();
if (createBackup)
{
memoryStream.CopyTo(backupCopy);
backupCopy.Seek(0, SeekOrigin.Begin);
//reset stream if we need to reuse it later:
memoryStream.Seek(0, SeekOrigin.Begin);
}
_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat);
if (createBackup)
{
statusLogger.UpdateMessage(Application.Context.GetString(Resource.String.UpdatingBackup));
Java.IO.File internalDirectory = IoUtil.GetInternalDirectory(Application.Context);
string baseDisplayName = App.Kp2a.GetFileStorage(ioConnectionInfo).GetDisplayName(ioConnectionInfo);
string targetPath = baseDisplayName;
var charsToRemove = "|\\?*<\":>+[]/'";
foreach (char c in charsToRemove)
{
targetPath = targetPath.Replace(c.ToString(), string.Empty);
}
if (targetPath == "")
targetPath = "local_backup";
var targetIoc = IOConnectionInfo.FromPath(new Java.IO.File(internalDirectory, targetPath).CanonicalPath);
using (var transaction = new LocalFileStorage(App.Kp2a).OpenWriteTransaction(targetIoc, false))
{
var file = transaction.OpenFile();
backupCopy.CopyTo(file);
transaction.CommitWrite();
}
Java.Lang.Object baseIocDisplayName = baseDisplayName;
string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(ioConnectionInfo.Path);
App.Kp2a.StoreOpenedFileAsRecent(targetIoc, keyfile, Application.Context.
GetString(Resource.String.LocalBackupOf, new Java.Lang.Object[]{baseIocDisplayName}));
prefs.Edit()
.PutBoolean(IoUtil.GetIocPrefKey(ioConnectionInfo, "has_local_backup"), true)
.PutBoolean(IoUtil.GetIocPrefKey(targetIoc, "is_local_backup"), true)
.Commit();
}
else
{
prefs.Edit()
.PutBoolean(IoUtil.GetIocPrefKey(ioConnectionInfo, "has_local_backup"), false) //there might be an older local backup, but we won't "advertise" this anymore
.Commit();
}
UpdateOngoingNotification();
}
internal void UnlockDatabase()
@@ -301,9 +360,9 @@ namespace keepass2android
dialog.Show();
}
public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile)
public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile, string displayName = "")
{
FileDbHelper.CreateFile(ioc, keyfile);
FileDbHelper.CreateFile(ioc, keyfile, displayName);
}
public string GetResourceString(UiStringKey key)

View File

@@ -34,13 +34,14 @@ namespace keepass2android
private const String DatabaseName = "keepass2android";
private const String FileTable = "files";
private const int DatabaseVersion = 1;
private const int DatabaseVersion = 2;
private const int MaxFiles = 15;
public const String KeyFileId = "_id";
public const String KeyFileFilename = "fileName";
public const String KeyFileUsername = "username";
public const String KeyFileDisplayname = "displayname";
public const String KeyFileUsername = "username";
public const String KeyFilePassword = "password";
public const String KeyFileCredsavemode = "credSaveMode";
public const String KeyFileKeyfile = "keyFile";
@@ -48,12 +49,14 @@ namespace keepass2android
private const String DatabaseCreate =
"create table " + FileTable + " ( " + KeyFileId + " integer primary key autoincrement, "
+ KeyFileFilename + " text not null, "
+ KeyFileKeyfile + " text, "
+ KeyFileFilename + " text not null, "
+ KeyFileKeyfile + " text, "
+ KeyFileUsername + " text, "
+ KeyFilePassword + " text, "
+ KeyFileCredsavemode + " integer not null,"
+ KeyFileUpdated + " integer not null);";
+ KeyFileUpdated + " integer not null,"
+ KeyFileDisplayname + " text "
+");";
private readonly Context mCtx;
private DatabaseHelper mDbHelper;
@@ -71,7 +74,11 @@ namespace keepass2android
public override void OnUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Only one database version so far
if (oldVersion == 1)
{
db.ExecSQL("alter table " + FileTable + " add column " + KeyFileDisplayname + " text ");
}
}
}
@@ -94,7 +101,7 @@ namespace keepass2android
mDb.Close();
}
public long CreateFile(IOConnectionInfo ioc, String keyFile) {
public long CreateFile(IOConnectionInfo ioc, string keyFile, string displayName = "") {
// Check to see if this filename is already used
ICursor cursor;
@@ -126,6 +133,7 @@ namespace keepass2android
vals.Put(KeyFileUsername, iocToStore.UserName);
vals.Put(KeyFilePassword, iocToStore.Password);
vals.Put(KeyFileCredsavemode, (int)iocToStore.CredSaveMode);
vals.Put(KeyFileDisplayname, displayName);
result = mDb.Update(FileTable, vals, KeyFileId + " = " + id, null);
@@ -138,8 +146,9 @@ namespace keepass2android
vals.Put(KeyFilePassword, iocToStore.Password);
vals.Put(KeyFileCredsavemode, (int)iocToStore.CredSaveMode);
vals.Put(KeyFileUpdated, Java.Lang.JavaSystem.CurrentTimeMillis());
result = mDb.Insert(FileTable, null, vals);
vals.Put(KeyFileDisplayname, displayName);
result = mDb.Insert(FileTable, null, vals);
}
// Delete all but the last X records
@@ -193,7 +202,8 @@ namespace keepass2android
KeyFileKeyfile,
KeyFileUsername,
KeyFilePassword,
KeyFileCredsavemode
KeyFileCredsavemode,
KeyFileDisplayname
};
}

View File

@@ -229,12 +229,21 @@ namespace keepass2android
public override void BindView(View view, Context context, ICursor cursor)
{
String path = cursor.GetString(1);
TextView textView = view.FindViewById<TextView>(Resource.Id.file_filename);
IOConnectionInfo ioc = new IOConnectionInfo { Path = path };
var fileStorage = _app.GetFileStorage(ioc);
textView.Text = fileStorage.GetDisplayName(ioc);
String displayName = cursor.GetString(6);
if (string.IsNullOrEmpty(displayName))
{
displayName = fileStorage.GetDisplayName(ioc);
}
textView.Text = displayName;
textView.Tag = ioc.Path;
}

View File

@@ -865,7 +865,7 @@ namespace keepass2android
{
CompositeKey masterKey = App.Kp2a.GetDb().KpDatabase.MasterKey;
var sourceIoc = ((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).Ioc;
var newIoc = ImportFileToInternalDirectory(sourceIoc);
var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a);
((KcpKeyFile)masterKey.GetUserKey(typeof(KcpKeyFile))).ResetIoc(newIoc);
var keyfileString = IOConnectionInfo.SerializeToString(newIoc);
App.Kp2a.StoreOpenedFileAsRecent(App.Kp2a.GetDb().Ioc, keyfileString);
@@ -934,7 +934,7 @@ namespace keepass2android
try
{
var sourceIoc = App.Kp2a.GetDb().Ioc;
var newIoc = ImportFileToInternalDirectory(sourceIoc);
var newIoc = IoUtil.ImportFileToInternalDirectory(sourceIoc, Activity, App.Kp2a);
return () =>
{
var builder = new AlertDialog.Builder(Activity);
@@ -968,32 +968,6 @@ namespace keepass2android
}
private IOConnectionInfo ImportFileToInternalDirectory(IOConnectionInfo sourceIoc)
{
Java.IO.File internalDirectory = IoUtil.GetInternalDirectory(Activity);
string targetPath = UrlUtil.GetFileName(sourceIoc.Path);
targetPath = targetPath.Trim("|\\?*<\":>+[]/'".ToCharArray());
if (targetPath == "")
targetPath = "imported";
if (new File(internalDirectory, targetPath).Exists())
{
int c = 1;
var ext = UrlUtil.GetExtension(targetPath);
var filenameWithoutExt = UrlUtil.StripExtension(targetPath);
do
{
c++;
targetPath = filenameWithoutExt + c;
if (!String.IsNullOrEmpty(ext))
targetPath += "." + ext;
} while (new File(internalDirectory, targetPath).Exists());
}
var targetIoc = IOConnectionInfo.FromPath(new File(internalDirectory, targetPath).CanonicalPath);
IoUtil.Copy(targetIoc, sourceIoc, App.Kp2a);
return targetIoc;
}
private void SetAlgorithm(Database db, Preference algorithm)