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:
101
src/App/Pages/Settings/ExportVaultPage.xaml
Normal file
101
src/App/Pages/Settings/ExportVaultPage.xaml
Normal 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>
|
||||
68
src/App/Pages/Settings/ExportVaultPage.xaml.cs
Normal file
68
src/App/Pages/Settings/ExportVaultPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
src/App/Pages/Settings/ExportVaultPageViewModel.cs
Normal file
142
src/App/Pages/Settings/ExportVaultPageViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user