1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-28 22:23:35 +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,101 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.ExportVaultPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:ExportVaultPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:ExportVaultPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:UpperCaseConverter x:Key="toUpper" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n FileFormat}"
StyleClass="box-label" />
<Picker
x:Name="_fileFormatPicker"
ItemsSource="{Binding FileFormatOptions, Mode=OneTime}"
SelectedIndex="{Binding FileFormatSelectedIndex}"
StyleClass="box-value" />
</StackLayout>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_masterPassword"
Text="{Binding MasterPassword}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0"
ReturnType="Go"
ReturnCommand="{Binding ExportVaultCommand}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
</Grid>
<Label
Text="{u:I18n ExportVaultMasterPasswordDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
<Label
StyleClass="box-footer-label, box-footer-label-switch"
Margin="0, 20">
<Label.FormattedText>
<FormattedString>
<Span
Text="{Binding Converter={StaticResource toUpper}, ConverterParameter={u:I18n Warning}}"
FontAttributes="Bold" />
<Span Text=": " FontAttributes="Bold" />
<Span Text="{u:I18n ExportVaultWarning}" />
</FormattedString>
</Label.FormattedText>
</Label>
<StackLayout Spacing="20">
<Button Text="{u:I18n ExportVault}"
Clicked="ExportVault_Clicked"
HorizontalOptions="Fill"
VerticalOptions="End" />
</StackLayout>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,68 @@
using System;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class ExportVaultPage : BaseContentPage
{
private readonly ExportVaultPageViewModel _vm;
private readonly IBroadcasterService _broadcasterService;
public ExportVaultPage()
{
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_vm = BindingContext as ExportVaultPageViewModel;
_vm.Page = this;
_fileFormatPicker.ItemDisplayBinding = new Binding("Value");
MasterPasswordEntry = _masterPassword;
}
protected async override void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
_broadcasterService.Subscribe(nameof(AttachmentsPage), (message) =>
{
if(message.Command == "selectSaveFileResult")
{
Device.BeginInvokeOnMainThread(() =>
{
var data = message.Data as Tuple<string, string>;
if(data == null)
{
return;
}
_vm.SaveFileSelected(data.Item1, data.Item2);
});
}
});
RequestFocus(_masterPassword);
}
protected async override void OnDisappearing()
{
base.OnDisappearing();
}
public Entry MasterPasswordEntry { get; set; }
private async void Close_Clicked(object sender, System.EventArgs e)
{
if(DoOnce())
{
await Navigation.PopModalAsync();
}
}
private async void ExportVault_Clicked(object sender, EventArgs e)
{
if(DoOnce())
{
await _vm.ExportVaultAsync();
}
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class ExportVaultPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly II18nService _i18nService;
private readonly ICryptoService _cryptoService;
private readonly IExportService _exportService;
private int _fileFormatSelectedIndex;
private bool _showPassword;
private string _masterPassword;
private byte[] _exportResult;
private string _defaultFilename;
public ExportVaultPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
PageTitle = AppResources.ExportVault;
TogglePasswordCommand = new Command(TogglePassword);
ExportVaultCommand = new Command(async () => await ExportVaultAsync());
FileFormatOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("json", ".json"),
new KeyValuePair<string, string>("csv", ".csv")
};
}
public async Task InitAsync()
{
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
}
public List<KeyValuePair<string, string>> FileFormatOptions { get; set; }
public int FileFormatSelectedIndex
{
get => _fileFormatSelectedIndex;
set { SetProperty(ref _fileFormatSelectedIndex, value); }
}
public bool ShowPassword
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[] {nameof(ShowPasswordIcon)});
}
public string MasterPassword
{
get => _masterPassword;
set => SetProperty(ref _masterPassword, value);
}
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? "" : "";
public void TogglePassword()
{
ShowPassword = !ShowPassword;
(Page as ExportVaultPage).MasterPasswordEntry.Focus();
}
public Command ExportVaultCommand { get; }
public async Task ExportVaultAsync()
{
if(string.IsNullOrEmpty(_masterPassword))
{
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
return;
}
var keyHash = await _cryptoService.HashPasswordAsync(_masterPassword, null);
MasterPassword = string.Empty;
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
{
try
{
var data = _exportService.GetExport(FileFormatOptions[FileFormatSelectedIndex].Key);
var fileFormat = FileFormatOptions[FileFormatSelectedIndex].Key;
_defaultFilename = _exportService.GetFileName(null, fileFormat);
_exportResult = Encoding.ASCII.GetBytes(data.Result);
if(!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
{
ClearResult();
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
}
}
catch(Exception ex)
{
ClearResult();
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
}
}
else
{
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
}
}
public async void SaveFileSelected(string contentUri, string filename)
{
if(_deviceActionService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
{
ClearResult();
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
return;
}
ClearResult();
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
}
private void ClearResult()
{
_defaultFilename = null;
_exportResult = null;
}
}
}

View File

@@ -103,7 +103,7 @@ namespace Bit.App.Pages
}
else if(item.Name == AppResources.ExportVault)
{
_vm.Export();
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
}
else if(item.Name == AppResources.ShareVault)
{

View File

@@ -127,11 +127,6 @@ namespace Bit.App.Pages
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/import-data/");
}
public void Export()
{
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/export-your-data/");
}
public void WebVault()
{
var url = _environmentService.GetWebVaultUrl();