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 <kspearrin@users.noreply.github.com>

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
This commit is contained in:
Matt Gibson 2020-12-14 11:56:13 -06:00 committed by GitHub
parent 6e40b7f25b
commit 3227daddaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 242 additions and 54 deletions

View file

@ -35,6 +35,7 @@
x:Name="_fileFormatPicker"
ItemsSource="{Binding FileFormatOptions, Mode=OneTime}"
SelectedIndex="{Binding FileFormatSelectedIndex}"
SelectedIndexChanged="FileFormat_Changed"
StyleClass="box-value" />
</StackLayout>
<Grid StyleClass="box-row">
@ -84,7 +85,7 @@
Text="{Binding Converter={StaticResource toUpper}, ConverterParameter={u:I18n Warning}}"
FontAttributes="Bold" />
<Span Text=": " FontAttributes="Bold" />
<Span Text="{u:I18n ExportVaultWarning}" />
<Span Text="{Binding ExportWarningMessage}" />
</FormattedString>
</Label.FormattedText>
</Label>

View file

@ -65,5 +65,10 @@ namespace Bit.App.Pages
await _vm.ExportVaultAsync();
}
}
void FileFormat_Changed(object sender, EventArgs e)
{
_vm?.UpdateWarning();
}
}
}

View file

@ -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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("json", ".json"),
new KeyValuePair<string, string>("csv", ".csv")
new KeyValuePair<string, string>("csv", ".csv"),
new KeyValuePair<string, string>("encrypted_json", ".json (Encrypted)")
};
}
public async Task InitAsync()
{
_initialized = true;
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
UpdateWarning();
}
public List<KeyValuePair<string, string>> 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;

View file

@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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);

View file

@ -1602,6 +1602,9 @@
<data name="ExportVaultWarning" xml:space="preserve">
<value>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.</value>
</data>
<data name="EncExportVaultWarning" xml:space="preserve">
<value>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.</value>
</data>
<data name="Warning" xml:space="preserve">
<value>Warning</value>
</data>

View file

@ -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; }

View file

@ -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; }

View file

@ -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)]

View file

@ -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; }

View file

@ -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; }
}

View file

@ -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; }

View file

@ -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)

View file

@ -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; }
}

View file

@ -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; }

View file

@ -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<LoginUri> Uris { get; set; }
public string Username { get; set; }
public string Password { get; set; }

View file

@ -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; }

View file

@ -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)

View file

@ -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<FolderView> _decryptedFolders;
private List<CipherView> _decryptedCiphers;
public ExportService(
IFolderService folderService,
ICipherService cipherService)
@ -35,58 +31,19 @@ namespace Bit.Core.Services
public async Task<string> 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<ExportCipher>();
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<FolderView> decryptedFolders, IEnumerable<CipherView> decryptedCiphers)
{
var foldersMap = decryptedFolders.Where(f => f.Id != null).ToDictionary(f => f.Id);
var exportCiphers = new List<ExportCipher>();
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<FolderView> decryptedFolders, IEnumerable<CipherView> 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<FolderWithId> folders, IEnumerable<CipherWithId> 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")]