1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-25 12:43:39 +00:00

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
This commit is contained in:
Matt Portune
2020-02-14 16:10:58 -05:00
committed by GitHub
parent 7a6fe5ed5f
commit 33df456cfd
31 changed files with 1149 additions and 8 deletions

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace Bit.Core.Abstractions
{
public interface IExportService
{
Task<string> GetExport(string format = "csv");
Task<string> GetOrganizationExport(string organizationId, string format = "csv");
string GetFileName(string prefix = null, string extension = "csv");
}
}

View File

@@ -33,5 +33,6 @@
public static string PreviousPageKey = "previousPage";
public const int SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43;
public const int SaveFileRequestCode = 44;
}
}

View File

@@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="15.0.0" />
<PackageReference Include="LiteDB" Version="4.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PCLCrypto" Version="2.0.147" />

View File

@@ -0,0 +1,42 @@
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Card
{
public Card() { }
public Card(CardView obj)
{
CardholderName = obj.CardholderName;
Brand = obj.Brand;
Number = obj.Number;
ExpMonth = obj.ExpMonth;
ExpYear = obj.ExpYear;
Code = obj.Code;
}
public string CardholderName { get; set; }
public string Brand { get; set; }
public string Number { get; set; }
public string ExpMonth { get; set; }
public string ExpYear { get; set; }
public string Code { get; set; }
public static CardView ToView(Card req, CardView view = null)
{
if(view == null)
{
view = new CardView();
}
view.CardholderName = req.CardholderName;
view.Brand = req.Brand;
view.Number = req.Number;
view.ExpMonth = req.ExpMonth;
view.ExpYear = req.ExpYear;
view.Code = req.Code;
return view;
}
}
}

View File

@@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Newtonsoft.Json;
namespace Bit.Core.Models.Export
{
public class Cipher
{
public Cipher() { }
public Cipher(CipherView obj)
{
OrganizationId = obj.OrganizationId;
FolderId = obj.FolderId;
Type = obj.Type;
Name = obj.Name;
Notes = obj.Notes;
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; }
public string Name { get; set; }
public string Notes { get; set; }
public bool Favorite { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<Field> Fields { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Login Login { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public SecureNote SecureNote { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Card Card { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Identity Identity { get; set; }
public CipherView ToView(Cipher req, CipherView view = null)
{
if(view == null)
{
view = new CipherView();
}
view.Type = req.Type;
view.FolderId = req.FolderId;
if(view.OrganizationId == null)
{
view.OrganizationId = req.OrganizationId;
}
view.Name = req.Name;
view.Notes = req.Notes;
view.Favorite = req.Favorite;
view.Fields = req.Fields?.Select(f => Field.ToView(f)).ToList();
switch(req.Type)
{
case CipherType.Login:
view.Login = Login.ToView(req.Login);
break;
case CipherType.SecureNote:
view.SecureNote = SecureNote.ToView(req.SecureNote);
break;
case CipherType.Card:
view.Card = Card.ToView(req.Card);
break;
case CipherType.Identity:
view.Identity = Identity.ToView(req.Identity);
break;
}
return view;
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using Bit.Core.Models.View;
using Newtonsoft.Json;
namespace Bit.Core.Models.Export
{
public class CipherWithId : Cipher
{
public CipherWithId(CipherView obj) : base(obj)
{
Id = obj.Id;
CollectionIds = obj.CollectionIds;
}
[JsonProperty(Order = int.MinValue)]
public string Id { get; set; }
[JsonProperty(Order = int.MaxValue)]
public HashSet<string> CollectionIds { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Collection
{
public Collection() { }
public Collection(CollectionView obj)
{
OrganizationId = obj.OrganizationId;
Name = obj.Name;
ExternalId = obj.ExternalId;
}
public string OrganizationId { get; set; }
public string Name { get; set; }
public string ExternalId { get; set; }
public CollectionView ToView(Collection req, CollectionView view = null)
{
if(view == null)
{
view = new CollectionView();
}
view.Name = req.Name;
view.ExternalId = req.ExternalId;
if(view.OrganizationId == null)
{
view.OrganizationId = req.OrganizationId;
}
return view;
}
}
}

View File

@@ -0,0 +1,16 @@
using Bit.Core.Models.View;
using Newtonsoft.Json;
namespace Bit.Core.Models.Export
{
public class CollectionWithId : Collection
{
public CollectionWithId(CollectionView obj) : base(obj)
{
Id = obj.Id;
}
[JsonProperty(Order = int.MinValue)]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,34 @@
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Field
{
public Field() { }
public Field(FieldView obj)
{
Name = obj.Name;
Value = obj.Value;
Type = obj.Type;
}
public string Name { get; set; }
public string Value { get; set; }
public FieldType Type { get; set; }
public static FieldView ToView(Field req, FieldView view = null)
{
if(view == null)
{
view = new FieldView();
}
view.Type = req.Type;
view.Value = req.Value;
view.Name = req.Name;
return view;
}
}
}

View File

@@ -0,0 +1,27 @@
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Folder
{
public Folder() { }
public Folder(FolderView obj)
{
Name = obj.Name;
}
public string Name { get; set; }
public FolderView ToView(Folder req, FolderView view = null)
{
if(view == null)
{
view = new FolderView();
}
view.Name = req.Name;
return view;
}
}
}

View File

@@ -0,0 +1,16 @@
using Bit.Core.Models.View;
using Newtonsoft.Json;
namespace Bit.Core.Models.Export
{
public class FolderWithId : Folder
{
public FolderWithId(FolderView obj) : base(obj)
{
Id = obj.Id;
}
[JsonProperty(Order = int.MinValue)]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,78 @@
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Identity
{
public Identity() { }
public Identity(IdentityView obj)
{
Title = obj.Title;
FirstName = obj.FirstName;
MiddleName = obj.MiddleName;
LastName = obj.LastName;
Address1 = obj.Address1;
Address2 = obj.Address2;
Address3 = obj.Address3;
City = obj.City;
State = obj.State;
PostalCode = obj.PostalCode;
Country = obj.Country;
Company = obj.Company;
Email = obj.Email;
Phone = obj.Phone;
SSN = obj.SSN;
Username = obj.Username;
PassportNumber = obj.PassportNumber;
LicenseNumber = obj.LicenseNumber;
}
public string Title { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Company { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string SSN { get; set; }
public string Username { get; set; }
public string PassportNumber { get; set; }
public string LicenseNumber { get; set; }
public static IdentityView ToView(Identity req, IdentityView view = null)
{
if(view == null)
{
view = new IdentityView();
}
view.Title = req.Title;
view.FirstName = req.FirstName;
view.MiddleName = req.MiddleName;
view.LastName = req.LastName;
view.Address1 = req.Address1;
view.Address2 = req.Address2;
view.Address3 = req.Address3;
view.City = req.City;
view.State = req.State;
view.PostalCode = req.PostalCode;
view.Country = req.Country;
view.Company = req.Company;
view.Email = req.Email;
view.Phone = req.Phone;
view.SSN = req.SSN;
view.Username = req.Username;
view.PassportNumber = req.PassportNumber;
view.LicenseNumber = req.LicenseNumber;
return view;
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class Login
{
public Login() { }
public Login(LoginView obj)
{
Uris = obj.Uris?.Select(u => new LoginUri(u)).ToList();
Username = obj.Username;
Password = obj.Password;
Totp = obj.Totp;
}
public List<LoginUri> Uris { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Totp { get; set; }
public static LoginView ToView(Login req, LoginView view = null)
{
if(view == null)
{
view = new LoginView();
}
view.Uris = req.Uris?.Select(u => LoginUri.ToView(u)).ToList();
view.Username = req.Username;
view.Password = req.Password;
view.Totp = req.Totp;
return view;
}
}
}

View File

@@ -0,0 +1,31 @@
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class LoginUri
{
public LoginUri() { }
public LoginUri(LoginUriView obj)
{
Match = obj.Match;
Uri = obj.Uri;
}
public UriMatchType? Match { get; set; }
public string Uri { get; set; }
public static LoginUriView ToView(LoginUri req, LoginUriView view = null)
{
if(view == null)
{
view = new LoginUriView();
}
view.Match = req.Match;
view.Uri = req.Uri;
return view;
}
}
}

View File

@@ -0,0 +1,28 @@
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Export
{
public class SecureNote
{
public SecureNote() { }
public SecureNote(SecureNoteView obj)
{
Type = obj.Type;
}
public SecureNoteType Type { get; set; }
public SecureNoteView ToView(SecureNote req, SecureNoteView view = null)
{
if(view == null)
{
view = new SecureNoteView();
}
view.Type = req.Type;
return view;
}
}
}

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
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;
namespace Bit.Core.Services
{
public class ExportService : IExportService
{
private readonly IFolderService _folderService;
private readonly ICipherService _cipherService;
private List<FolderView> _decryptedFolders;
private List<CipherView> _decryptedCiphers;
public ExportService(
IFolderService folderService,
ICipherService cipherService)
{
_folderService = folderService;
_cipherService = cipherService;
}
public async Task<string> GetExport(string format = "csv")
{
_decryptedFolders = await _folderService.GetAllDecryptedAsync();
_decryptedCiphers = await _cipherService.GetAllDecryptedAsync();
if(format == "csv")
{
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();
}
}
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})
};
return CoreHelpers.SerializeJson(jsonDoc,
new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
}
public Task<string> GetOrganizationExport(string organizationId, string format = "csv")
{
throw new NotImplementedException();
}
public string GetFileName(string prefix = null, string extension = "csv")
{
var dateString = DateTime.Now.ToString("yyyyMMddHHmmss");
return string.Format("bitwarden{0}_export_{1}.{2}",
!string.IsNullOrEmpty(prefix) ? ("_" + prefix) : string.Empty, dateString, extension);
}
private void BuildCommonCipher(ExportCipher cipher, CipherView c)
{
cipher.Type = null;
cipher.Name = c.Name;
cipher.Notes = c.Notes;
cipher.Fields = null;
// Login props
cipher.LoginUris = null;
cipher.LoginUsername = null;
cipher.LoginPassword = null;
cipher.LoginTotp = null;
if(c.Fields != null)
{
foreach(var f in c.Fields)
{
if(cipher.Fields == null)
{
cipher.Fields = "";
}
else
{
cipher.Fields += "\n";
}
cipher.Fields += (f.Name ?? "") + ": " + f.Value;
}
}
switch(c.Type)
{
case CipherType.Login:
cipher.Type = "login";
cipher.LoginUsername = c.Login.Username;
cipher.LoginPassword = c.Login.Password;
cipher.LoginTotp = c.Login.Totp;
if(c.Login.Uris != null)
{
foreach(var u in c.Login.Uris)
{
if(cipher.LoginUris == null)
{
cipher.LoginUris = "";
}
else
{
cipher.LoginUris += ",";
}
cipher.LoginUris += u.Uri;
}
}
break;
case CipherType.SecureNote:
cipher.Type = "note";
break;
default:
return;
}
}
private class ExportCipher
{
[Name("folder")]
public string Folder { get; set; }
[Name("favorite")]
public string Favorite { get; set; }
[Name("type")]
public string Type { get; set; }
[Name("name")]
public string Name { get; set; }
[Name("notes")]
public string Notes { get; set; }
[Name("fields")]
public string Fields { get; set; }
[Name("login_uri")]
public string LoginUris { get; set; }
[Name("login_username")]
public string LoginUsername { get; set; }
[Name("login_password")]
public string LoginPassword { get; set; }
[Name("login_totp")]
public string LoginTotp { get; set; }
}
}
}

View File

@@ -190,6 +190,11 @@ namespace Bit.Core.Utilities
}
return JsonConvert.SerializeObject(obj, jsonSerializationSettings);
}
public static string SerializeJson(object obj, JsonSerializerSettings jsonSerializationSettings)
{
return JsonConvert.SerializeObject(obj, jsonSerializationSettings);
}
public static T DeserializeJson<T>(string json, bool ignoreNulls = false)
{

View File

@@ -58,7 +58,7 @@ namespace Bit.Core.Utilities
var totpService = new TotpService(storageService, cryptoFunctionService);
var authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService,
i18nService, platformUtilsService, messagingService, lockService);
// TODO: export service
var exportService = new ExportService(folderService, cipherService);
var auditService = new AuditService(cryptoFunctionService, apiService);
var environmentService = new EnvironmentService(apiService, storageService);
var eventService = new EventService(storageService, apiService, userService, cipherService);
@@ -80,6 +80,7 @@ namespace Bit.Core.Utilities
Register<IPasswordGenerationService>("passwordGenerationService", passwordGenerationService);
Register<ITotpService>("totpService", totpService);
Register<IAuthService>("authService", authService);
Register<IExportService>("exportService", exportService);
Register<IAuditService>("auditService", auditService);
Register<IEnvironmentService>("environmentService", environmentService);
Register<IEventService>("eventService", eventService);