From 33df456cfddb7a866751b8bfbb014034769eb7fe Mon Sep 17 00:00:00 2001 From: Matt Portune <59324545+mportune-bw@users.noreply.github.com> Date: Fri, 14 Feb 2020 16:10:58 -0500 Subject: [PATCH] In-app vault export support (#729) * First pass at vault export UI * Password validation via cryptoService * Export service framework * support for constructing json export data * Support for constructing csv export data * Cleanup and simplification * Completion of vault export feature * Formatting and simplification * Use dialog instead of toast for invalid master password entry --- src/Android/MainActivity.cs | 11 +- src/Android/Services/DeviceActionService.cs | 55 +++++ src/App/Abstractions/IDeviceActionService.cs | 1 + src/App/App.csproj | 3 + src/App/Pages/Settings/ExportVaultPage.xaml | 101 +++++++++ .../Pages/Settings/ExportVaultPage.xaml.cs | 68 ++++++ .../Settings/ExportVaultPageViewModel.cs | 142 +++++++++++++ .../SettingsPage/SettingsPage.xaml.cs | 2 +- .../SettingsPage/SettingsPageViewModel.cs | 5 - src/App/Resources/AppResources.Designer.cs | 36 ++++ src/App/Resources/AppResources.resx | 18 ++ src/App/Utilities/UpperCaseConverter.cs | 29 +++ src/Core/Abstractions/IExportService.cs | 11 + src/Core/Constants.cs | 1 + src/Core/Core.csproj | 1 + src/Core/Models/Export/Card.cs | 42 ++++ src/Core/Models/Export/Cipher.cs | 97 +++++++++ src/Core/Models/Export/CipherWithId.cs | 20 ++ src/Core/Models/Export/Collection.cs | 37 ++++ src/Core/Models/Export/CollectionWithId.cs | 16 ++ src/Core/Models/Export/Field.cs | 34 +++ src/Core/Models/Export/Folder.cs | 27 +++ src/Core/Models/Export/FolderWithId.cs | 16 ++ src/Core/Models/Export/Identity.cs | 78 +++++++ src/Core/Models/Export/Login.cs | 40 ++++ src/Core/Models/Export/LoginUri.cs | 31 +++ src/Core/Models/Export/SecureNote.cs | 28 +++ src/Core/Services/ExportService.cs | 193 ++++++++++++++++++ src/Core/Utilities/CoreHelpers.cs | 5 + src/Core/Utilities/ServiceContainer.cs | 3 +- src/iOS.Core/Services/DeviceActionService.cs | 6 + 31 files changed, 1149 insertions(+), 8 deletions(-) create mode 100644 src/App/Pages/Settings/ExportVaultPage.xaml create mode 100644 src/App/Pages/Settings/ExportVaultPage.xaml.cs create mode 100644 src/App/Pages/Settings/ExportVaultPageViewModel.cs create mode 100644 src/App/Utilities/UpperCaseConverter.cs create mode 100644 src/Core/Abstractions/IExportService.cs create mode 100644 src/Core/Models/Export/Card.cs create mode 100644 src/Core/Models/Export/Cipher.cs create mode 100644 src/Core/Models/Export/CipherWithId.cs create mode 100644 src/Core/Models/Export/Collection.cs create mode 100644 src/Core/Models/Export/CollectionWithId.cs create mode 100644 src/Core/Models/Export/Field.cs create mode 100644 src/Core/Models/Export/Folder.cs create mode 100644 src/Core/Models/Export/FolderWithId.cs create mode 100644 src/Core/Models/Export/Identity.cs create mode 100644 src/Core/Models/Export/Login.cs create mode 100644 src/Core/Models/Export/LoginUri.cs create mode 100644 src/Core/Models/Export/SecureNote.cs create mode 100644 src/Core/Services/ExportService.cs diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 846991252..e913c6b50 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -201,7 +201,8 @@ namespace Bit.Droid protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { - if(requestCode == Constants.SelectFileRequestCode && resultCode == Result.Ok) + if(resultCode == Result.Ok && + (requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode)) { Android.Net.Uri uri = null; string fileName = null; @@ -222,6 +223,14 @@ namespace Bit.Droid { return; } + + if(requestCode == Constants.SaveFileRequestCode) + { + _messagingService.Send("selectSaveFileResult", + new Tuple(uri.ToString(), fileName)); + return; + } + try { using(var stream = ContentResolver.OpenInputStream(uri)) diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index e3b5faaf1..ffdb7370d 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -200,6 +200,61 @@ namespace Bit.Droid.Services catch { } return null; } + + public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri) + { + try + { + var activity = (MainActivity)CrossCurrentActivity.Current.Activity; + + if(contentUri != null) + { + var uri = Android.Net.Uri.Parse(contentUri); + var stream = activity.ContentResolver.OpenOutputStream(uri); + // Using java bufferedOutputStream due to this issue: + // https://github.com/xamarin/xamarin-android/issues/3498 + var javaStream = new Java.IO.BufferedOutputStream(stream); + javaStream.Write(fileData); + javaStream.Flush(); + javaStream.Close(); + return true; + } + + // Prompt for location to save file + var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower()); + if(extension == null) + { + return false; + } + + string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension); + if(mimeType == null) + { + if(extension == "json") + { + // Explicit support for json since older versions of Android don't recognize the extension + mimeType = "text/json"; + } + else + { + return false; + } + } + + var intent = new Intent(Intent.ActionCreateDocument); + intent.SetType(mimeType); + intent.AddCategory(Intent.CategoryOpenable); + intent.PutExtra(Intent.ExtraTitle, fileName); + + activity.StartActivityForResult(intent, Constants.SaveFileRequestCode); + return true; + } + catch(Exception ex) + { + System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace); + } + return false; + } public async Task ClearCacheAsync() { diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index cd706519d..785402fd6 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -13,6 +13,7 @@ namespace Bit.App.Abstractions Task ShowLoadingAsync(string text); Task HideLoadingAsync(); bool OpenFile(byte[] fileData, string id, string fileName); + bool SaveFile(byte[] fileData, string id, string fileName, string contentUri); bool CanOpenFile(string fileName); Task ClearCacheAsync(); Task SelectFileAsync(); diff --git a/src/App/App.csproj b/src/App/App.csproj index d9c007660..dda5432f6 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -66,6 +66,9 @@ FoldersPage.xaml + + ExportVaultPage.xaml + OptionsPage.xaml diff --git a/src/App/Pages/Settings/ExportVaultPage.xaml b/src/App/Pages/Settings/ExportVaultPage.xaml new file mode 100644 index 000000000..f03549926 --- /dev/null +++ b/src/App/Pages/Settings/ExportVaultPage.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +