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 f03549926..5d528959f 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 1f16be450..79edb2dc5 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 f51675daa..6d8c2befe 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 82325d809..87c3c05ab 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 2e18bc28f..6a618079f 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 b6938abbf..ea5e41d74 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 2744f3032..70da4efb2 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 69eec2a6b..129267c05 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 f233da185..7aa515322 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 e00e407fe..2b8af4dd2 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 e2ce4fcc3..373b8beb9 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 40f0876d2..8f9d71b98 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 a43c1175a..33fd180a7 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 08a78c559..d3d223901 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 433ff5511..883ec18be 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 ef8bffe5f..e3f215ff7 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 69d923804..71270091c 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 38bee7ba0..6b0606ea3 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")]