mirror of
https://github.com/bitwarden/android.git
synced 2024-12-25 02:18:27 +03:00
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:
parent
6e40b7f25b
commit
3227daddaf
18 changed files with 242 additions and 54 deletions
|
@ -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>
|
||||
|
|
|
@ -65,5 +65,10 @@ namespace Bit.App.Pages
|
|||
await _vm.ExportVaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void FileFormat_Changed(object sender, EventArgs e)
|
||||
{
|
||||
_vm?.UpdateWarning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
10
src/App/Resources/AppResources.Designer.cs
generated
10
src/App/Resources/AppResources.Designer.cs
generated
|
@ -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")]
|
||||
|
@ -2817,6 +2819,14 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
public static string EncExportVaultWarning
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("EncExportVaultWarning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Warning {
|
||||
get {
|
||||
return ResourceManager.GetString("Warning", resourceCulture);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")]
|
||||
|
|
Loading…
Reference in a new issue