/* 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 3 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.IO; using System.Net.Security; using Android.App; using Android.Content; using Android.Graphics.Drawables; using Android.OS; using Android.Runtime; using Android.Views; using Android.Widget; using KeePassLib; using KeePassLib.Cryptography.Cipher; using KeePassLib.Keys; using KeePassLib.Serialization; using Android.Preferences; using Android.Support.V4.App; #if !EXCLUDE_TWOFISH using TwofishCipher; #endif using Keepass2android.Pluginsdk; using keepass2android.Io; using keepass2android.addons.OtpKeyProv; using Keepass2android.Javafilestorage; using GoogleDriveFileStorage = keepass2android.Io.GoogleDriveFileStorage; namespace keepass2android { #if NoNet /// /// Static strings containing App names for the Offline ("nonet") release /// public static class AppNames { public const string AppName = "@string/app_name_nonet"; public const int AppNameResource = Resource.String.app_name_nonet; public const string AppNameShort = "@string/short_app_name_nonet"; public const string AppLauncherTitle = "@string/short_app_name_nonet"; public const string PackagePart = "keepass2android_nonet"; public const int LauncherIcon = Resource.Drawable.ic_launcher_offline; public const int NotificationLockedIcon = Resource.Drawable.ic_notify_offline; public const int NotificationUnlockedIcon = Resource.Drawable.ic_notify_locked; public const string Searchable = "@xml/searchable_offline"; } #else /// /// Static strings containing App names for the Online release /// public static class AppNames { #if DEBUG public const string AppName = "@string/app_name_debug"; #else public const string AppName = "@string/app_name"; #endif public const int AppNameResource = Resource.String.app_name; public const string AppNameShort = "@string/short_app_name" + "DBG"; public const string AppLauncherTitle = "@string/app_name" + " Debug"; #if DEBUG public const string PackagePart = "keepass2android_debug"; public const string Searchable = "@xml/searchable_debug"; #else public const string PackagePart = "keepass2android"; public const string Searchable = "@xml/searchable"; #endif public const int LauncherIcon = Resource.Drawable.ic_launcher_online; public const int NotificationLockedIcon = Resource.Drawable.ic_notify_loaded; public const int NotificationUnlockedIcon = Resource.Drawable.ic_notify_locked; } #endif /// /// Main implementation of the IKp2aApp interface for usage in the real app. /// public class Kp2aApp: IKp2aApp, ICacheSupervisor { public void LockDatabase(bool allowQuickUnlock = true) { if (GetDb().Loaded) { if (QuickUnlockEnabled && allowQuickUnlock && _db.KpDatabase.MasterKey.ContainsType(typeof(KcpPassword)) && !((KcpPassword)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpPassword))).Password.IsEmpty) { if (!QuickLocked) { Kp2aLog.Log("QuickLocking database"); BroadcastDatabaseAction(Application.Context, Strings.ActionLockDatabase); QuickLocked = true; _db.LastOpenedEntry = null; } else { Kp2aLog.Log("Database already QuickLocked"); } } else { Kp2aLog.Log("Locking database"); BroadcastDatabaseAction(Application.Context, Strings.ActionCloseDatabase); // Couldn't quick-lock, so unload database instead _db.Clear(); QuickLocked = false; } } else { Kp2aLog.Log("Database not loaded, couldn't lock"); } UpdateOngoingNotification(); Application.Context.SendBroadcast(new Intent(Intents.DatabaseLocked)); } public void BroadcastDatabaseAction(Context ctx, string action) { Intent i = new Intent(action); //seems like this can happen. This code is for debugging. if (App.Kp2a.GetDb().Ioc == null) { Kp2aLog.LogUnexpectedError(new Exception("App.Kp2a.GetDb().Ioc is null")); return; } i.PutExtra(Strings.ExtraDatabaseFileDisplayname, App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc).GetDisplayName(App.Kp2a.GetDb().Ioc)); i.PutExtra(Strings.ExtraDatabaseFilepath, App.Kp2a.GetDb().Ioc.Path); foreach (var plugin in new PluginDatabase(ctx).GetPluginsWithAcceptedScope(Strings.ScopeDatabaseActions)) { i.SetPackage(plugin); ctx.SendBroadcast(i); } } public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger, IDatabaseFormat databaseFormat) { _db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat); UpdateOngoingNotification(); } internal void UnlockDatabase() { QuickLocked = false; UpdateOngoingNotification(); BroadcastDatabaseAction(Application.Context, Strings.ActionUnlockDatabase); } public void UpdateOngoingNotification() { // Start or update the notification icon service to reflect the current state var ctx = Application.Context; StartOnGoingService(ctx); } public static void StartOnGoingService(Context ctx) { ctx.StartService(new Intent(ctx, typeof (OngoingNotificationsService))); } public bool DatabaseIsUnlocked { get { return _db.Loaded && !QuickLocked; } } #region QuickUnlock public void SetQuickUnlockEnabled(bool enabled) { if (enabled) { //Set KeyLength of QuickUnlock at time of enabling. //This is important to not allow an attacker to set the length to 1 when QuickUnlock is started already. var ctx = Application.Context; var prefs = PreferenceManager.GetDefaultSharedPreferences(ctx); QuickUnlockKeyLength = Math.Max(1, int.Parse(prefs.GetString(ctx.GetString(Resource.String.QuickUnlockLength_key), ctx.GetString(Resource.String.QuickUnlockLength_default)))); } QuickUnlockEnabled = enabled; } public bool QuickUnlockEnabled { get; private set; } public int QuickUnlockKeyLength { get; private set; } /// /// If true, the database must be regarded as locked and not exposed to the user. /// public bool QuickLocked { get; private set; } #endregion private Database _db; /// /// See comments to EntryEditActivityState. /// internal EntryEditActivityState EntryEditActivityState = null; public FileDbHelper FileDbHelper; private List _fileStorages; public Database GetDb() { if (_db == null) { _db = CreateNewDatabase(); } return _db; } public bool GetBooleanPreference(PreferenceKey key) { Context ctx = Application.Context; ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(ctx); switch (key) { case PreferenceKey.remember_keyfile: return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.keyfile_key), ctx.Resources.GetBoolean(Resource.Boolean.keyfile_default)); case PreferenceKey.UseFileTransactions: return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.UseFileTransactions_key), true); case PreferenceKey.CheckForFileChangesOnSave: return prefs.GetBoolean(ctx.Resources.GetString(Resource.String.CheckForFileChangesOnSave_key), true); default: throw new Exception("unexpected key!"); } } public void CheckForOpenFileChanged(Activity activity) { if (_db.DidOpenFileChange()) { if (_db.ReloadRequested) { LockDatabase(false); activity.SetResult(KeePass.ExitReloadDb); activity.Finish(); //todo: return? } AskForReload(activity); } } private void AskForReload(Activity activity) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.SetTitle(activity.GetString(Resource.String.AskReloadFile_title)); builder.SetMessage(activity.GetString(Resource.String.AskReloadFile)); builder.SetPositiveButton(activity.GetString(Android.Resource.String.Yes), (dlgSender, dlgEvt) => { _db.ReloadRequested = true; activity.SetResult(KeePass.ExitReloadDb); activity.Finish(); }); builder.SetNegativeButton(activity.GetString(Android.Resource.String.No), (dlgSender, dlgEvt) => { }); Dialog dialog = builder.Create(); dialog.Show(); } public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile) { FileDbHelper.CreateFile(ioc, keyfile); } public string GetResourceString(UiStringKey key) { return GetResourceString(key.ToString()); } public string GetResourceString(string key) { var field = typeof(Resource.String).GetField(key); if (field == null) throw new Exception("Invalid key " + key); return Application.Context.GetString((int)field.GetValue(null)); } public Drawable GetResourceDrawable(string key) { var field = typeof(Resource.Drawable).GetField(key); if (field == null) throw new Exception("Invalid key " + key); return Application.Context.Resources.GetDrawable((int)field.GetValue(null)); } public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey, EventHandler yesHandler, EventHandler noHandler, EventHandler cancelHandler, Context ctx) { AskYesNoCancel(titleKey, messageKey, UiStringKey.yes, UiStringKey.no, yesHandler, noHandler, cancelHandler, ctx); } public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey, UiStringKey yesString, UiStringKey noString, EventHandler yesHandler, EventHandler noHandler, EventHandler cancelHandler, Context ctx) { AskYesNoCancel(titleKey, messageKey, yesString, noString, yesHandler, noHandler, cancelHandler, null, ctx); } public void AskYesNoCancel(UiStringKey titleKey, UiStringKey messageKey, UiStringKey yesString, UiStringKey noString, EventHandler yesHandler, EventHandler noHandler, EventHandler cancelHandler, EventHandler dismissHandler, Context ctx) { Handler handler = new Handler(Looper.MainLooper); handler.Post(() => { AlertDialog.Builder builder = new AlertDialog.Builder(ctx); builder.SetTitle(GetResourceString(titleKey)); builder.SetMessage(GetResourceString(messageKey)); string yesText = GetResourceString(yesString); builder.SetPositiveButton(yesText, yesHandler); string noText = ""; if (noHandler != null) { noText = GetResourceString(noString); builder.SetNegativeButton(noText, noHandler); } string cancelText = ""; if (cancelHandler != null) { cancelText = ctx.GetString(Android.Resource.String.Cancel); builder.SetNeutralButton(cancelText, cancelHandler); } AlertDialog dialog = builder.Create(); if (dismissHandler != null) dialog.SetOnDismissListener(new Util.DismissListener(() => dismissHandler(dialog, EventArgs.Empty))); dialog.Show(); if (yesText.Length + noText.Length + cancelText.Length >= 20) { try { Button button = dialog.GetButton((int)DialogButtonType.Positive); LinearLayout linearLayout = (LinearLayout)button.Parent; linearLayout.Orientation = Orientation.Vertical; } catch (Exception e) { Kp2aLog.LogUnexpectedError(e); } } } ); } public Handler UiThreadHandler { get { return new Handler(); } } /// /// Simple wrapper around ProgressDialog implementing IProgressDialog /// private class RealProgressDialog : IProgressDialog { private readonly ProgressDialog _pd; public RealProgressDialog(Context ctx) { _pd = new ProgressDialog(ctx); _pd.SetCancelable(false); } public void SetTitle(string title) { _pd.SetTitle(title); } public void SetMessage(string message) { _pd.SetMessage(message); } public void Dismiss() { try { _pd.Dismiss(); } catch (Exception e) { Kp2aLog.LogUnexpectedError(e); } } public void Show() { _pd.Show(); } } public IProgressDialog CreateProgressDialog(Context ctx) { return new RealProgressDialog(ctx); } public IFileStorage GetFileStorage(IOConnectionInfo iocInfo) { return GetFileStorage(iocInfo, true); } public IFileStorage GetFileStorage(IOConnectionInfo iocInfo, bool allowCache) { IFileStorage fileStorage; if (iocInfo.IsLocalFile()) fileStorage = new LocalFileStorage(this); else { IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); if (DatabaseCacheEnabled && allowCache) { fileStorage = new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this); } else { fileStorage = innerFileStorage; } } if (fileStorage is IOfflineSwitchable) { ((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode; } return fileStorage; } private IFileStorage GetCloudFileStorage(IOConnectionInfo iocInfo) { foreach (IFileStorage fs in FileStorages) { foreach (string protocolId in fs.SupportedProtocols) { if (iocInfo.Path.StartsWith(protocolId + "://")) return fs; } } //TODO: catch! throw new NoFileStorageFoundException("Unknown protocol " + iocInfo.Path); } public IEnumerable FileStorages { get { if (_fileStorages == null) { _fileStorages = new List { new AndroidContentStorage(Application.Context), #if !EXCLUDE_JAVAFILESTORAGE #if !NoNet new DropboxFileStorage(Application.Context, this), new DropboxAppFolderFileStorage(Application.Context, this), new GoogleDriveFileStorage(Application.Context, this), new OneDriveFileStorage(Application.Context, this), new SftpFileStorage(this), new NetFtpFileStorage(Application.Context, this), new WebDavFileStorage(this), #endif #endif new LocalFileStorage(this) }; } return _fileStorages; } } public void TriggerReload(Context ctx) { Handler handler = new Handler(Looper.MainLooper); handler.Post(() => { AskForReload((Activity) ctx); }); } public bool AlwaysFailOnValidationError() { return true; } public bool OnValidationError() { return false; } public RemoteCertificateValidationCallback CertificateValidationCallback { get { switch (GetValidationMode()) { case ValidationMode.Ignore: return (sender, certificate, chain, errors) => true; case ValidationMode.Warn: return (sender, certificate, chain, errors) => { if (errors != SslPolicyErrors.None) ShowValidationWarning(errors.ToString()); return true; }; case ValidationMode.Error: return (sender, certificate, chain, errors) => { if (errors == SslPolicyErrors.None) return true; return false; };; default: throw new ArgumentOutOfRangeException(); } } } private ValidationMode GetValidationMode() { var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); ValidationMode validationMode = ValidationMode.Warn; string strValMode = prefs.GetString(Application.Context.Resources.GetString(Resource.String.AcceptAllServerCertificates_key), Application.Context.Resources.GetString(Resource.String.AcceptAllServerCertificates_default)); if (strValMode == "IGNORE") validationMode = ValidationMode.Ignore; else if (strValMode == "ERROR") validationMode = ValidationMode.Error; return validationMode; } public bool CheckForDuplicateUuids { get { var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); return prefs.GetBoolean(Application.Context.GetString(Resource.String.CheckForDuplicateUuids_key), true); } } public ICertificateErrorHandler CertificateErrorHandler { get { return new CertificateErrorHandlerImpl(this); } } public class CertificateErrorHandlerImpl : Java.Lang.Object, Keepass2android.Javafilestorage.ICertificateErrorHandler { private readonly Kp2aApp _app; public CertificateErrorHandlerImpl(Kp2aApp app) { _app = app; } public bool AlwaysFailOnValidationError() { return _app.GetValidationMode() == ValidationMode.Error; } public bool OnValidationError(string errorMessage) { switch (_app.GetValidationMode()) { case ValidationMode.Ignore: return true; case ValidationMode.Warn: _app.ShowValidationWarning(errorMessage); return true; case ValidationMode.Error: return false; default: throw new Exception("Unexpected Validation mode!"); } } } private void ShowValidationWarning(string error) { ShowToast(Application.Context.GetString(Resource.String.CertificateWarning, error)); } public enum ValidationMode { Ignore, Warn, Error } internal void OnTerminate() { if (_db != null) { _db.Clear(); } if (FileDbHelper != null && FileDbHelper.IsOpen()) { FileDbHelper.Close(); } } internal void OnCreate(Application app) { FileDbHelper = new FileDbHelper(app); FileDbHelper.Open(); #if DEBUG foreach (UiStringKey key in Enum.GetValues(typeof(UiStringKey))) { GetResourceString(key); } #endif #if !EXCLUDE_TWOFISH CipherPool.GlobalPool.AddCipher(new TwofishCipherEngine()); #endif } public Database CreateNewDatabase() { _db = new Database(new DrawableFactory(), this); return _db; } internal void ShowToast(string message) { var handler = new Handler(Looper.MainLooper); handler.Post(() => { var toast = Toast.MakeText(Application.Context, message, ToastLength.Long); toast.SetGravity(GravityFlags.Center, 0, 0); toast.Show(); }); } public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e) { var errorMessage = GetErrorMessageForFileStorageException(e); ShowToast(Application.Context.GetString(Resource.String.CouldNotSaveToRemote, errorMessage)); } private string GetErrorMessageForFileStorageException(Exception e) { string errorMessage = e.Message; if (e is OfflineModeException) errorMessage = GetResourceString(UiStringKey.InOfflineMode); return errorMessage; } public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex) { var errorMessage = GetErrorMessageForFileStorageException(ex); ShowToast(Application.Context.GetString(Resource.String.CouldNotLoadFromRemote, errorMessage)); } public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc) { ShowToast(Application.Context.GetString(Resource.String.UpdatedCachedFileOnLoad, new Java.Lang.Object[] { Application.Context.GetString(Resource.String.database_file) })); } public void UpdatedRemoteFileOnLoad(IOConnectionInfo ioc) { ShowToast(Application.Context.GetString(Resource.String.UpdatedRemoteFileOnLoad)); } public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc) { ShowToast(Application.Context.GetString(Resource.String.NotifyOpenFromLocalDueToConflict)); } public void LoadedFromRemoteInSync(IOConnectionInfo ioc) { ShowToast(Application.Context.GetString(Resource.String.LoadedFromRemoteInSync)); } public void ClearOfflineCache() { new CachingFileStorage(new LocalFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache(); } public IFileStorage GetFileStorage(string protocolId) { return GetFileStorage(new IOConnectionInfo() {Path = protocolId + "://"}); } /// /// returns a file storage object to be used when accessing the auxiliary OTP file /// /// The reason why this requires a different file storage is the different caching behavior. public IFileStorage GetOtpAuxFileStorage(IOConnectionInfo iocInfo) { if (iocInfo.IsLocalFile()) return new LocalFileStorage(this); else { IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); if (DatabaseCacheEnabled) { return new OtpAuxCachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, new OtpAuxCacheSupervisor(this)); } else { return innerFileStorage; } } } private static bool DatabaseCacheEnabled { get { var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); bool cacheEnabled = prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key), #if NoNet false #else true #endif ); return cacheEnabled; } } public bool OfflineModePreference { get { var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); return prefs.GetBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), false); } set { ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); ISharedPreferencesEditor edit = prefs.Edit(); edit.PutBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), value); edit.Commit(); } } /// /// true if the app is used in offline mode /// public bool OfflineMode { get; set; } public void OnScreenOff() { if (PreferenceManager.GetDefaultSharedPreferences(Application.Context) .GetBoolean( Application.Context.GetString(Resource.String.LockWhenScreenOff_key), false)) { App.Kp2a.LockDatabase(); } } } ///Application class for Keepass2Android: Contains static Database variable to be used by all components. #if NoNet [Application(Debuggable=false, Label=AppNames.AppName)] #else #if RELEASE [Application(Debuggable=false, Label=AppNames.AppName)] #else [Application(Debuggable = true, Label = AppNames.AppName)] #endif #endif public class App : Application { public const string PrefErrorreportmode = "pref_ErrorReportMode"; public const string PrefHaspendingerrorreport = "pref_hasPendingErrorReport"; public App (IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } public static readonly Kp2aApp Kp2a = new Kp2aApp(); public enum ErrorReportMode { AskAgain=0, Disabled=1, Enabled=2 } public override void OnCreate() { base.OnCreate(); Kp2aLog.Log("Creating application "+PackageName+". Version=" + PackageManager.GetPackageInfo(PackageName, 0).VersionCode); Kp2a.OnCreate(this); AndroidEnvironment.UnhandledExceptionRaiser += MyApp_UnhandledExceptionHandler; #if !NoNet Kp2aLog.OnUnexpectedError += (sender, exception) => { var currentErrorReportMode = GetErrorReportMode(ApplicationContext); if (currentErrorReportMode != ErrorReportMode.Disabled) { Xamarin.Insights.Report(exception); if (Xamarin.Insights.DisableDataTransmission) { PreferenceManager.GetDefaultSharedPreferences(ApplicationContext) .Edit().PutBoolean(PrefHaspendingerrorreport, true).Commit(); } } }; Xamarin.Insights.Initialize("fed2b273ed2a964d0ba6acc3743e68f7a04da957", ApplicationContext); Xamarin.Insights.DisableExceptionCatching = true; var errorReportMode = GetErrorReportMode(ApplicationContext); SetErrorReportMode(ApplicationContext, errorReportMode); #endif } public static ErrorReportMode GetErrorReportMode(Context ctx) { ErrorReportMode errorReportMode; Enum.TryParse(PreferenceManager.GetDefaultSharedPreferences(ctx) .GetString(PrefErrorreportmode, ErrorReportMode.AskAgain.ToString()), out errorReportMode); return errorReportMode; } public override void OnTerminate() { base.OnTerminate(); Kp2aLog.Log("Terminating application"); Kp2a.OnTerminate(); } #if !NoNet void MyApp_UnhandledExceptionHandler(object sender, RaiseThrowableEventArgs e) { Kp2aLog.LogUnexpectedError(e.Exception); Xamarin.Insights.Save(); // Do your error handling here. //throw e.Exception; } protected override void Dispose(bool disposing) { AndroidEnvironment.UnhandledExceptionRaiser -= MyApp_UnhandledExceptionHandler; base.Dispose(disposing); } public static void SetErrorReportMode(Context ctx, ErrorReportMode mode) { Xamarin.Insights.DisableCollection = (mode == ErrorReportMode.Disabled); Xamarin.Insights.DisableDataTransmission = mode != ErrorReportMode.Enabled; var pref = PreferenceManager.GetDefaultSharedPreferences(ctx); var edit = pref.Edit(); if (mode != ErrorReportMode.AskAgain) { edit.PutBoolean(PrefHaspendingerrorreport, false); } edit.PutString(PrefErrorreportmode, mode.ToString()); edit.Commit(); } #else private void MyApp_UnhandledExceptionHandler(object sender, RaiseThrowableEventArgs e) { Kp2aLog.LogUnexpectedError(e.Exception); } #endif } }