From 3227daddaf2d14a818651602a296e524d6976fd4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 14 Dec 2020 11:56:13 -0600 Subject: [PATCH] Enable Encrypted json export of vaults (#1174) * Enable Encrypted json export of vaults * Match jslib export of non-org ciphers * Clean up export * Update src/App/Pages/Settings/ExportVaultPage.xaml.cs Co-authored-by: Kyle Spearrin Co-authored-by: Matt Gibson Co-authored-by: Kyle Spearrin --- src/App/Pages/Settings/ExportVaultPage.xaml | 3 +- .../Pages/Settings/ExportVaultPage.xaml.cs | 5 + .../Settings/ExportVaultPageViewModel.cs | 31 ++++- src/App/Resources/AppResources.Designer.cs | 12 +- src/App/Resources/AppResources.resx | 3 + src/Core/Models/Export/Card.cs | 10 ++ src/Core/Models/Export/Cipher.cs | 28 ++++ src/Core/Models/Export/CipherWithId.cs | 8 +- src/Core/Models/Export/Collection.cs | 7 + src/Core/Models/Export/CollectionWithId.cs | 5 + src/Core/Models/Export/Field.cs | 7 + src/Core/Models/Export/Folder.cs | 5 + src/Core/Models/Export/FolderWithId.cs | 5 + src/Core/Models/Export/Identity.cs | 22 +++ src/Core/Models/Export/Login.cs | 9 ++ src/Core/Models/Export/LoginUri.cs | 6 + src/Core/Models/Export/SecureNote.cs | 5 + src/Core/Services/ExportService.cs | 125 +++++++++++------- 18 files changed, 242 insertions(+), 54 deletions(-) diff --git a/src/App/Pages/Settings/ExportVaultPage.xaml b/src/App/Pages/Settings/ExportVaultPage.xaml index f03549926f..5d528959f8 100644 --- a/src/App/Pages/Settings/ExportVaultPage.xaml +++ b/src/App/Pages/Settings/ExportVaultPage.xaml @@ -35,6 +35,7 @@ x:Name="_fileFormatPicker" ItemsSource="{Binding FileFormatOptions, Mode=OneTime}" SelectedIndex="{Binding FileFormatSelectedIndex}" + SelectedIndexChanged="FileFormat_Changed" StyleClass="box-value" /> @@ -84,7 +85,7 @@ Text="{Binding Converter={StaticResource toUpper}, ConverterParameter={u:I18n Warning}}" FontAttributes="Bold" /> - + diff --git a/src/App/Pages/Settings/ExportVaultPage.xaml.cs b/src/App/Pages/Settings/ExportVaultPage.xaml.cs index 1f16be450c..79edb2dc5c 100644 --- a/src/App/Pages/Settings/ExportVaultPage.xaml.cs +++ b/src/App/Pages/Settings/ExportVaultPage.xaml.cs @@ -65,5 +65,10 @@ namespace Bit.App.Pages await _vm.ExportVaultAsync(); } } + + void FileFormat_Changed(object sender, EventArgs e) + { + _vm?.UpdateWarning(); + } } } diff --git a/src/App/Pages/Settings/ExportVaultPageViewModel.cs b/src/App/Pages/Settings/ExportVaultPageViewModel.cs index f51675daa0..6d8c2befe4 100644 --- a/src/App/Pages/Settings/ExportVaultPageViewModel.cs +++ b/src/App/Pages/Settings/ExportVaultPageViewModel.cs @@ -22,10 +22,12 @@ namespace Bit.App.Pages private readonly IExportService _exportService; private int _fileFormatSelectedIndex; + private string _exportWarningMessage; private bool _showPassword; private string _masterPassword; private byte[] _exportResult; private string _defaultFilename; + private bool _initialized = false; public ExportVaultPageViewModel() { @@ -42,13 +44,16 @@ namespace Bit.App.Pages FileFormatOptions = new List> { new KeyValuePair("json", ".json"), - new KeyValuePair("csv", ".csv") + new KeyValuePair("csv", ".csv"), + new KeyValuePair("encrypted_json", ".json (Encrypted)") }; } public async Task InitAsync() { + _initialized = true; FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json"); + UpdateWarning(); } public List> FileFormatOptions { get; set; } @@ -59,6 +64,12 @@ namespace Bit.App.Pages set { SetProperty(ref _fileFormatSelectedIndex, value); } } + public string ExportWarningMessage + { + get => _exportWarningMessage; + set { SetProperty(ref _exportWarningMessage, value); } + } + public bool ShowPassword { get => _showPassword; @@ -140,6 +151,24 @@ namespace Bit.App.Pages await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure")); } + public void UpdateWarning() + { + if (!_initialized) + { + return; + } + + switch (FileFormatOptions[FileFormatSelectedIndex].Key) + { + case "encrypted_json": + ExportWarningMessage = _i18nService.T("EncExportVaultWarning"); + break; + default: + ExportWarningMessage = _i18nService.T("ExportVaultWarning"); + break; + } + } + private void ClearResult() { _defaultFilename = null; diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 82325d8091..87c3c05ab5 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,6 +10,7 @@ namespace Bit.App.Resources { using System; + using System.Reflection; [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] @@ -2816,7 +2818,15 @@ namespace Bit.App.Resources { return ResourceManager.GetString("ExportVaultWarning", resourceCulture); } } - + + public static string EncExportVaultWarning + { + get + { + return ResourceManager.GetString("EncExportVaultWarning", resourceCulture); + } + } + public static string Warning { get { return ResourceManager.GetString("Warning", resourceCulture); diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 2e18bc28f6..6a618079fc 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1602,6 +1602,9 @@ This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it. + + This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file. + Warning diff --git a/src/Core/Models/Export/Card.cs b/src/Core/Models/Export/Card.cs index b6938abbf1..ea5e41d742 100644 --- a/src/Core/Models/Export/Card.cs +++ b/src/Core/Models/Export/Card.cs @@ -16,6 +16,16 @@ namespace Bit.Core.Models.Export Code = obj.Code; } + public Card(Domain.Card obj) + { + CardholderName = obj.CardholderName?.EncryptedString; + Brand = obj.Brand?.EncryptedString; + Number = obj.Number?.EncryptedString; + ExpMonth = obj.ExpMonth?.EncryptedString; + ExpYear = obj.ExpYear?.EncryptedString; + Code = obj.Code?.EncryptedString; + } + public string CardholderName { get; set; } public string Brand { get; set; } public string Number { get; set; } diff --git a/src/Core/Models/Export/Cipher.cs b/src/Core/Models/Export/Cipher.cs index 2744f3032b..70da4efb22 100644 --- a/src/Core/Models/Export/Cipher.cs +++ b/src/Core/Models/Export/Cipher.cs @@ -38,6 +38,34 @@ namespace Bit.Core.Models.Export } } + public Cipher(Domain.Cipher obj) + { + OrganizationId = obj.OrganizationId; + FolderId = obj.FolderId; + Type = obj.Type; + Name = obj.Name?.EncryptedString; + Notes = obj.Notes?.EncryptedString; + Favorite = obj.Favorite; + + Fields = obj.Fields?.Select(f => new Field(f)).ToList(); + + switch (obj.Type) + { + case CipherType.Login: + Login = new Login(obj.Login); + break; + case CipherType.SecureNote: + SecureNote = new SecureNote(obj.SecureNote); + break; + case CipherType.Card: + Card = new Card(obj.Card); + break; + case CipherType.Identity: + Identity = new Identity(obj.Identity); + break; + } + } + public string OrganizationId { get; set; } public string FolderId { get; set; } public CipherType Type { get; set; } diff --git a/src/Core/Models/Export/CipherWithId.cs b/src/Core/Models/Export/CipherWithId.cs index 69eec2a6bf..129267c058 100644 --- a/src/Core/Models/Export/CipherWithId.cs +++ b/src/Core/Models/Export/CipherWithId.cs @@ -9,7 +9,13 @@ namespace Bit.Core.Models.Export public CipherWithId(CipherView obj) : base(obj) { Id = obj.Id; - CollectionIds = obj.CollectionIds; + CollectionIds = null; + } + + public CipherWithId(Domain.Cipher obj) : base(obj) + { + Id = obj.Id; + CollectionIds = null; } [JsonProperty(Order = int.MinValue)] diff --git a/src/Core/Models/Export/Collection.cs b/src/Core/Models/Export/Collection.cs index f233da185b..7aa5153224 100644 --- a/src/Core/Models/Export/Collection.cs +++ b/src/Core/Models/Export/Collection.cs @@ -13,6 +13,13 @@ namespace Bit.Core.Models.Export ExternalId = obj.ExternalId; } + public Collection(Domain.Collection obj) + { + OrganizationId = obj.OrganizationId; + Name = obj.Name?.EncryptedString; + ExternalId = obj.ExternalId; + } + public string OrganizationId { get; set; } public string Name { get; set; } public string ExternalId { get; set; } diff --git a/src/Core/Models/Export/CollectionWithId.cs b/src/Core/Models/Export/CollectionWithId.cs index e00e407fe6..2b8af4dd27 100644 --- a/src/Core/Models/Export/CollectionWithId.cs +++ b/src/Core/Models/Export/CollectionWithId.cs @@ -10,6 +10,11 @@ namespace Bit.Core.Models.Export Id = obj.Id; } + public CollectionWithId(Domain.Collection obj): base(obj) + { + Id = obj.Id; + } + [JsonProperty(Order = int.MinValue)] public string Id { get; set; } } diff --git a/src/Core/Models/Export/Field.cs b/src/Core/Models/Export/Field.cs index e2ce4fcc37..373b8beb94 100644 --- a/src/Core/Models/Export/Field.cs +++ b/src/Core/Models/Export/Field.cs @@ -14,6 +14,13 @@ namespace Bit.Core.Models.Export Type = obj.Type; } + public Field(Domain.Field obj) + { + Name = obj.Name?.EncryptedString; + Value = obj.Value?.EncryptedString; + Type = obj.Type; + } + public string Name { get; set; } public string Value { get; set; } public FieldType Type { get; set; } diff --git a/src/Core/Models/Export/Folder.cs b/src/Core/Models/Export/Folder.cs index 40f0876d2b..8f9d71b984 100644 --- a/src/Core/Models/Export/Folder.cs +++ b/src/Core/Models/Export/Folder.cs @@ -11,6 +11,11 @@ namespace Bit.Core.Models.Export Name = obj.Name; } + public Folder(Domain.Folder obj) + { + Name = obj.Name?.EncryptedString; + } + public string Name { get; set; } public FolderView ToView(Folder req, FolderView view = null) diff --git a/src/Core/Models/Export/FolderWithId.cs b/src/Core/Models/Export/FolderWithId.cs index a43c1175ae..33fd180a7d 100644 --- a/src/Core/Models/Export/FolderWithId.cs +++ b/src/Core/Models/Export/FolderWithId.cs @@ -10,6 +10,11 @@ namespace Bit.Core.Models.Export Id = obj.Id; } + public FolderWithId(Domain.Folder obj) : base(obj) + { + Id = obj.Id; + } + [JsonProperty(Order = int.MinValue)] public string Id { get; set; } } diff --git a/src/Core/Models/Export/Identity.cs b/src/Core/Models/Export/Identity.cs index 08a78c5597..d3d2239011 100644 --- a/src/Core/Models/Export/Identity.cs +++ b/src/Core/Models/Export/Identity.cs @@ -28,6 +28,28 @@ namespace Bit.Core.Models.Export LicenseNumber = obj.LicenseNumber; } + public Identity(Domain.Identity obj) + { + Title = obj.Title?.EncryptedString; + FirstName = obj.FirstName?.EncryptedString; + MiddleName = obj.FirstName?.EncryptedString; + LastName = obj.LastName?.EncryptedString; + Address1 = obj.Address1?.EncryptedString; + Address2 = obj.Address2?.EncryptedString; + Address3 = obj.Address3?.EncryptedString; + City = obj.City?.EncryptedString; + State = obj.State?.EncryptedString; + PostalCode = obj.PostalCode?.EncryptedString; + Country = obj.Country?.EncryptedString; + Company = obj.Company?.EncryptedString; + Email = obj.Email?.EncryptedString; + Phone = obj.Phone?.EncryptedString; + SSN = obj.SSN?.EncryptedString; + Username = obj.Username?.EncryptedString; + PassportNumber = obj.PassportNumber?.EncryptedString; + LicenseNumber = obj.LicenseNumber?.EncryptedString; + } + public string Title { get; set; } public string FirstName { get; set; } public string MiddleName { get; set; } diff --git a/src/Core/Models/Export/Login.cs b/src/Core/Models/Export/Login.cs index 433ff55113..883ec18bef 100644 --- a/src/Core/Models/Export/Login.cs +++ b/src/Core/Models/Export/Login.cs @@ -17,6 +17,15 @@ namespace Bit.Core.Models.Export Totp = obj.Totp; } + public Login(Domain.Login obj) + { + Uris = obj.Uris?.Select(u => new LoginUri(u)).ToList(); + + Username = obj.Username?.EncryptedString; + Password = obj.Password?.EncryptedString; + Totp = obj.Totp?.EncryptedString; + } + public List Uris { get; set; } public string Username { get; set; } public string Password { get; set; } diff --git a/src/Core/Models/Export/LoginUri.cs b/src/Core/Models/Export/LoginUri.cs index ef8bffe5f9..e3f215ff7f 100644 --- a/src/Core/Models/Export/LoginUri.cs +++ b/src/Core/Models/Export/LoginUri.cs @@ -13,6 +13,12 @@ namespace Bit.Core.Models.Export Uri = obj.Uri; } + public LoginUri(Domain.LoginUri obj) + { + Match = obj.Match; + Uri = obj.Uri?.EncryptedString; + } + public UriMatchType? Match { get; set; } public string Uri { get; set; } diff --git a/src/Core/Models/Export/SecureNote.cs b/src/Core/Models/Export/SecureNote.cs index 69d923804a..71270091c4 100644 --- a/src/Core/Models/Export/SecureNote.cs +++ b/src/Core/Models/Export/SecureNote.cs @@ -12,6 +12,11 @@ namespace Bit.Core.Models.Export Type = obj.Type; } + public SecureNote(Domain.SecureNote obj) + { + Type = obj.Type; + } + public SecureNoteType Type { get; set; } public SecureNoteView ToView(SecureNote req, SecureNoteView view = null) diff --git a/src/Core/Services/ExportService.cs b/src/Core/Services/ExportService.cs index 38bee7ba00..6b0606ea38 100644 --- a/src/Core/Services/ExportService.cs +++ b/src/Core/Services/ExportService.cs @@ -10,7 +10,6 @@ using Bit.Core.Models.Export; using Bit.Core.Models.View; using Bit.Core.Utilities; using CsvHelper; -using CsvHelper.Configuration; using CsvHelper.Configuration.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -22,9 +21,6 @@ namespace Bit.Core.Services private readonly IFolderService _folderService; private readonly ICipherService _cipherService; - private List _decryptedFolders; - private List _decryptedCiphers; - public ExportService( IFolderService folderService, ICipherService cipherService) @@ -35,58 +31,19 @@ namespace Bit.Core.Services public async Task GetExport(string format = "csv") { - _decryptedFolders = await _folderService.GetAllDecryptedAsync(); - _decryptedCiphers = await _cipherService.GetAllDecryptedAsync(); - - if (format == "csv") + if (format == "encrypted_json") { - var foldersMap = _decryptedFolders.Where(f => f.Id != null).ToDictionary(f => f.Id); + var folders = (await _folderService.GetAllAsync()).Where(f => f.Id != null).Select(f => new FolderWithId(f)); + var items = (await _cipherService.GetAllAsync()).Where(c => c.OrganizationId == null).Select(c => new CipherWithId(c)); - var exportCiphers = new List(); - foreach (var c in _decryptedCiphers) - { - // only export logins and secure notes - if (c.Type != CipherType.Login && c.Type != CipherType.SecureNote) - { - continue; - } - - if (c.OrganizationId != null) - { - continue; - } - - var cipher = new ExportCipher(); - cipher.Folder = c.FolderId != null && foldersMap.ContainsKey(c.FolderId) - ? foldersMap[c.FolderId].Name : null; - cipher.Favorite = c.Favorite ? "1" : null; - BuildCommonCipher(cipher, c); - exportCiphers.Add(cipher); - } - - using (var writer = new StringWriter()) - using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) - { - csv.WriteRecords(exportCiphers); - csv.Flush(); - return writer.ToString(); - } + return ExportEncryptedJson(folders, items); } else { - var jsonDoc = new - { - Folders = _decryptedFolders.Where(f => f.Id != null).Select(f => new FolderWithId(f)), - Items = _decryptedCiphers.Where(c => c.OrganizationId == null) - .Select(c => new CipherWithId(c) {CollectionIds = null}) - }; + var decryptedFolders = await _folderService.GetAllDecryptedAsync(); + var decryptedCiphers = await _cipherService.GetAllDecryptedAsync(); - return CoreHelpers.SerializeJson(jsonDoc, - new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); + return format == "csv" ? ExportCsv(decryptedFolders, decryptedCiphers) : ExportJson(decryptedFolders, decryptedCiphers); } } @@ -166,6 +123,74 @@ namespace Bit.Core.Services } } + private string ExportCsv(IEnumerable decryptedFolders, IEnumerable decryptedCiphers) + { + var foldersMap = decryptedFolders.Where(f => f.Id != null).ToDictionary(f => f.Id); + + var exportCiphers = new List(); + foreach (var c in decryptedCiphers) + { + // only export logins and secure notes + if (c.Type != CipherType.Login && c.Type != CipherType.SecureNote) + { + continue; + } + + if (c.OrganizationId != null) + { + continue; + } + + var cipher = new ExportCipher(); + cipher.Folder = c.FolderId != null && foldersMap.ContainsKey(c.FolderId) + ? foldersMap[c.FolderId].Name : null; + cipher.Favorite = c.Favorite ? "1" : null; + BuildCommonCipher(cipher, c); + exportCiphers.Add(cipher); + } + + using (var writer = new StringWriter()) + using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) + { + csv.WriteRecords(exportCiphers); + csv.Flush(); + return writer.ToString(); + } + } + + private string ExportJson(IEnumerable decryptedFolders, IEnumerable decryptedCiphers) + { + var jsonDoc = new + { + Folders = decryptedFolders.Where(f => f.Id != null).Select(f => new FolderWithId(f)), + Items = decryptedCiphers.Where(c => c.OrganizationId == null) + .Select(c => new CipherWithId(c) { CollectionIds = null }) + }; + + return CoreHelpers.SerializeJson(jsonDoc, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + } + + private string ExportEncryptedJson(IEnumerable folders, IEnumerable ciphers) + { + var jsonDoc = new + { + Folders = folders, + Items = ciphers, + }; + + return CoreHelpers.SerializeJson(jsonDoc, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + } + private class ExportCipher { [Name("folder")]