1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-25 12:05:59 +01:00

attachments page with upload/delete

This commit is contained in:
Kyle Spearrin 2017-07-22 15:38:08 -04:00
parent b32603b472
commit f9d336a3a6
24 changed files with 786 additions and 80 deletions

View File

@ -17,6 +17,7 @@ using Bit.App.Models.Page;
using Bit.App; using Bit.App;
using Android.Nfc; using Android.Nfc;
using Android.Views.InputMethods; using Android.Views.InputMethods;
using System.IO;
namespace Bit.Android namespace Bit.Android
{ {
@ -215,6 +216,25 @@ namespace Bit.Android
ZXing.Net.Mobile.Forms.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults); ZXing.Net.Mobile.Forms.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
} }
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if(requestCode == Constants.SelectFileRequestCode && resultCode == Result.Ok)
{
global::Android.Net.Uri uri = null;
if(data != null)
{
uri = data.Data;
using(var stream = ContentResolver.OpenInputStream(uri))
using(var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
MessagingCenter.Send(Xamarin.Forms.Application.Current, "SelectFileResult",
new Tuple<byte[], string>(memoryStream.ToArray(), Utilities.GetFileName(ApplicationContext, uri)));
}
}
}
}
public void RateApp() public void RateApp()
{ {
try try

View File

@ -6,6 +6,7 @@ using Android.Webkit;
using Plugin.CurrentActivity; using Plugin.CurrentActivity;
using System.IO; using System.IO;
using Android.Support.V4.Content; using Android.Support.V4.Content;
using Bit.App;
namespace Bit.Android.Services namespace Bit.Android.Services
{ {
@ -123,9 +124,12 @@ namespace Bit.Android.Services
} }
} }
public byte[] SelectFile() public void SelectFile()
{ {
return null; var intent = new Intent(Intent.ActionOpenDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("*/*");
CrossCurrentActivity.Current.Activity.StartActivityForResult(intent, Constants.SelectFileRequestCode);
} }
} }
} }

View File

@ -4,6 +4,7 @@ using Android.Content;
using Java.Security; using Java.Security;
using System.IO; using System.IO;
using Android.Nfc; using Android.Nfc;
using Android.Provider;
namespace Bit.Android namespace Bit.Android
{ {
@ -101,5 +102,28 @@ namespace Bit.Android
return message; return message;
} }
public static string GetFileName(Context context, global::Android.Net.Uri uri)
{
string name = null;
string[] projection = { MediaStore.MediaColumns.DisplayName };
var metaCursor = context.ContentResolver.Query(uri, projection, null, null, null);
if(metaCursor != null)
{
try
{
if(metaCursor.MoveToFirst())
{
name = metaCursor.GetString(0);
}
}
finally
{
metaCursor.Close();
}
}
return name;
}
} }
} }

View File

@ -8,5 +8,7 @@ namespace Bit.App.Abstractions
{ {
Task<ApiResult<CipherResponse>> GetByIdAsync(string id); Task<ApiResult<CipherResponse>> GetByIdAsync(string id);
Task<ApiResult<ListResponse<CipherResponse>>> GetAsync(); Task<ApiResult<ListResponse<CipherResponse>>> GetAsync();
Task<ApiResult<CipherResponse>> PostAttachmentAsync(string cipherId, byte[] data, string fileName);
Task<ApiResult> DeleteAttachmentAsync(string cipherId, string attachmentId);
} }
} }

View File

@ -22,6 +22,7 @@ namespace Bit.App.Abstractions
byte[] DecryptToBytes(byte[] encyptedValue, SymmetricCryptoKey key = null); byte[] DecryptToBytes(byte[] encyptedValue, SymmetricCryptoKey key = null);
byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey); byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey);
CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null); CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null);
byte[] EncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key = null);
SymmetricCryptoKey MakeKeyFromPassword(string password, string salt); SymmetricCryptoKey MakeKeyFromPassword(string password, string salt);
string MakeKeyFromPasswordBase64(string password, string salt); string MakeKeyFromPasswordBase64(string password, string salt);
byte[] HashPassword(SymmetricCryptoKey key, string password); byte[] HashPassword(SymmetricCryptoKey key, string password);

View File

@ -1,11 +1,13 @@
namespace Bit.App.Abstractions using System;
namespace Bit.App.Abstractions
{ {
public interface IDeviceActionService public interface IDeviceActionService
{ {
void CopyToClipboard(string text); void CopyToClipboard(string text);
bool OpenFile(byte[] fileData, string id, string fileName); bool OpenFile(byte[] fileData, string id, string fileName);
bool CanOpenFile(string fileName); bool CanOpenFile(string fileName);
byte[] SelectFile(); void SelectFile();
void ClearCache(); void ClearCache();
} }
} }

View File

@ -15,5 +15,7 @@ namespace Bit.App.Abstractions
Task<ApiResult<LoginResponse>> SaveAsync(Login login); Task<ApiResult<LoginResponse>> SaveAsync(Login login);
Task<ApiResult> DeleteAsync(string id); Task<ApiResult> DeleteAsync(string id);
Task<byte[]> DownloadAndDecryptAttachmentAsync(string url, string orgId = null); Task<byte[]> DownloadAndDecryptAttachmentAsync(string url, string orgId = null);
Task<ApiResult<CipherResponse>> EncryptAndSaveAttachmentAsync(Login login, byte[] data, string fileName);
Task<ApiResult> DeleteAttachmentAsync(Login login, string attachmentId);
} }
} }

View File

@ -84,6 +84,7 @@
<Compile Include="Controls\FormPickerCell.cs" /> <Compile Include="Controls\FormPickerCell.cs" />
<Compile Include="Controls\FormEntryCell.cs" /> <Compile Include="Controls\FormEntryCell.cs" />
<Compile Include="Controls\PinControl.cs" /> <Compile Include="Controls\PinControl.cs" />
<Compile Include="Controls\VaultAttachmentsViewCell.cs" />
<Compile Include="Controls\VaultListViewCell.cs" /> <Compile Include="Controls\VaultListViewCell.cs" />
<Compile Include="Enums\TwoFactorProviderType.cs" /> <Compile Include="Enums\TwoFactorProviderType.cs" />
<Compile Include="Enums\EncryptionType.cs" /> <Compile Include="Enums\EncryptionType.cs" />
@ -121,6 +122,7 @@
<Compile Include="Models\CipherString.cs" /> <Compile Include="Models\CipherString.cs" />
<Compile Include="Models\Data\AttachmentData.cs" /> <Compile Include="Models\Data\AttachmentData.cs" />
<Compile Include="Models\Attachment.cs" /> <Compile Include="Models\Attachment.cs" />
<Compile Include="Models\Page\VaultAttachmentsPageModel.cs" />
<Compile Include="Models\SymmetricCryptoKey.cs" /> <Compile Include="Models\SymmetricCryptoKey.cs" />
<Compile Include="Models\Data\SettingsData.cs" /> <Compile Include="Models\Data\SettingsData.cs" />
<Compile Include="Models\Data\FolderData.cs" /> <Compile Include="Models\Data\FolderData.cs" />
@ -162,6 +164,7 @@
<Compile Include="Pages\Settings\SettingsPage.cs" /> <Compile Include="Pages\Settings\SettingsPage.cs" />
<Compile Include="Pages\Settings\SettingsListFoldersPage.cs" /> <Compile Include="Pages\Settings\SettingsListFoldersPage.cs" />
<Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" /> <Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" />
<Compile Include="Pages\Vault\VaultAttachmentsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ILoginRepository.cs" /> <Compile Include="Abstractions\Repositories\ILoginRepository.cs" />
<Compile Include="Repositories\AttachmentRepository.cs" /> <Compile Include="Repositories\AttachmentRepository.cs" />

View File

@ -33,5 +33,7 @@
public const string Locked = "other:locked"; public const string Locked = "other:locked";
public const string LastLoginEmail = "other:lastLoginEmail"; public const string LastLoginEmail = "other:lastLoginEmail";
public const string LastSync = "other:lastSync"; public const string LastSync = "other:lastSync";
public const int SelectFileRequestCode = 42;
} }
} }

View File

@ -5,7 +5,7 @@ namespace Bit.App.Controls
{ {
public class LabeledRightDetailCell : ExtendedViewCell public class LabeledRightDetailCell : ExtendedViewCell
{ {
public LabeledRightDetailCell() public LabeledRightDetailCell(bool showIcon = true)
{ {
Label = new Label Label = new Label
{ {
@ -22,32 +22,38 @@ namespace Bit.App.Controls
VerticalOptions = LayoutOptions.Center VerticalOptions = LayoutOptions.Center
}; };
Icon = new CachedImage StackLayout = new StackLayout
{
WidthRequest = 16,
HeightRequest = 16,
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(5, 0, 0, 0)
};
var stackLayout = new StackLayout
{ {
Orientation = StackOrientation.Horizontal, Orientation = StackOrientation.Horizontal,
Padding = new Thickness(15, 10), Padding = new Thickness(15, 10),
Children = { Label, Detail, Icon } Children = { Label, Detail }
}; };
if(showIcon)
{
Icon = new CachedImage
{
WidthRequest = 16,
HeightRequest = 16,
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(5, 0, 0, 0)
};
StackLayout.Children.Add(Icon);
}
if(Device.RuntimePlatform == Device.Android) if(Device.RuntimePlatform == Device.Android)
{ {
Label.TextColor = Color.Black; Label.TextColor = Color.Black;
} }
View = stackLayout; View = StackLayout;
} }
public Label Label { get; private set; } public Label Label { get; private set; }
public Label Detail { get; private set; } public Label Detail { get; private set; }
public CachedImage Icon { get; private set; } public CachedImage Icon { get; private set; }
public StackLayout StackLayout { get; private set; }
} }
} }

View File

@ -0,0 +1,21 @@
using Bit.App.Models.Page;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class VaultAttachmentsViewCell : LabeledRightDetailCell
{
public VaultAttachmentsViewCell()
: base(false)
{
Label.SetBinding(Label.TextProperty, nameof(VaultAttachmentsPageModel.Attachment.Name));
Detail.SetBinding(Label.TextProperty, nameof(VaultAttachmentsPageModel.Attachment.SizeName));
BackgroundColor = Color.White;
if(Device.RuntimePlatform == Device.iOS)
{
StackLayout.BackgroundColor = Color.White;
}
}
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Bit.App.Models.Page
{
public class VaultAttachmentsPageModel
{
public class Attachment : List<Attachment>
{
public string Id { get; set; }
public string Name { get; set; }
public string SizeName { get; set; }
public long Size { get; set; }
public string Url { get; set; }
public Attachment(Models.Attachment attachment)
{
Id = attachment.Id;
Name = attachment.FileName?.Decrypt();
SizeName = attachment.SizeName;
Size = attachment.Size;
Url = attachment.Url;
}
}
}
}

View File

@ -217,6 +217,23 @@ namespace Bit.App.Pages
private void Layout_LayoutChanged(object sender, EventArgs e) private void Layout_LayoutChanged(object sender, EventArgs e)
{ {
AnalyticsLabel.WidthRequest = StackLayout.Bounds.Width - AnalyticsLabel.Bounds.Left * 2; AnalyticsLabel.WidthRequest = StackLayout.Bounds.Width - AnalyticsLabel.Bounds.Left * 2;
CopyTotpLabel.WidthRequest = StackLayout.Bounds.Width - CopyTotpLabel.Bounds.Left * 2;
if(AutofillAlwaysLabel != null)
{
AutofillAlwaysLabel.WidthRequest = StackLayout.Bounds.Width - AutofillAlwaysLabel.Bounds.Left * 2;
}
if(AutofillPasswordFieldLabel != null)
{
AutofillPasswordFieldLabel.WidthRequest = StackLayout.Bounds.Width - AutofillPasswordFieldLabel.Bounds.Left * 2;
}
if(AutofillPersistNotificationLabel != null)
{
AutofillPersistNotificationLabel.WidthRequest =
StackLayout.Bounds.Width - AutofillPersistNotificationLabel.Bounds.Left * 2;
}
} }
private void AnalyticsCell_Changed(object sender, ToggledEventArgs e) private void AnalyticsCell_Changed(object sender, ToggledEventArgs e)

View File

@ -0,0 +1,288 @@
using System;
using System.Linq;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models.Page;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
using Plugin.Connectivity.Abstractions;
using System.Collections.Generic;
using Bit.App.Models;
using System.Threading.Tasks;
namespace Bit.App.Pages
{
public class VaultAttachmentsPage : ExtendedContentPage
{
private readonly ILoginService _loginService;
private readonly IUserDialogs _userDialogs;
private readonly IConnectivity _connectivity;
private readonly IDeviceActionService _deviceActiveService;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly string _loginId;
private Login _login;
private byte[] _fileBytes;
private DateTime? _lastAction;
public VaultAttachmentsPage(string loginId)
: base(true)
{
_loginId = loginId;
_loginService = Resolver.Resolve<ILoginService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_deviceActiveService = Resolver.Resolve<IDeviceActionService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
Init();
}
public ExtendedObservableCollection<VaultAttachmentsPageModel.Attachment> PresentationAttchments { get; private set; }
= new ExtendedObservableCollection<VaultAttachmentsPageModel.Attachment>();
public ListView ListView { get; set; }
public StackLayout NoDataStackLayout { get; set; }
public StackLayout AddNewStackLayout { get; set; }
public Label FileLabel { get; set; }
public ExtendedTableView NewTable { get; set; }
public Label NoDataLabel { get; set; }
private void Init()
{
SubscribeFileResult(true);
var selectButton = new ExtendedButton
{
Text = AppResources.ChooseFile,
Command = new Command(() => _deviceActiveService.SelectFile()),
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Button))
};
FileLabel = new Label
{
Text = AppResources.NoFileChosen,
Style = (Style)Application.Current.Resources["text-muted"],
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
HorizontalTextAlignment = TextAlignment.Center
};
AddNewStackLayout = new StackLayout
{
Children = { selectButton, FileLabel },
Orientation = StackOrientation.Vertical,
Padding = new Thickness(20, Helpers.OnPlatform(iOS: 10, Android: 20), 20, 20),
VerticalOptions = LayoutOptions.Start
};
NewTable = new ExtendedTableView
{
Intent = TableIntent.Settings,
HasUnevenRows = true,
NoFooter = true,
EnableScrolling = false,
EnableSelection = false,
VerticalOptions = LayoutOptions.Start,
Margin = new Thickness(0, Helpers.OnPlatform(iOS: 10, Android: 30), 0, 0),
Root = new TableRoot
{
new TableSection(AppResources.AddNewAttachment)
{
new ExtendedViewCell
{
View = AddNewStackLayout,
BackgroundColor = Color.White
}
}
}
};
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
ItemsSource = PresentationAttchments,
HasUnevenRows = true,
ItemTemplate = new DataTemplate(() => new VaultAttachmentsViewCell()),
Footer = NewTable,
VerticalOptions = LayoutOptions.FillAndExpand
};
NoDataLabel = new Label
{
Text = AppResources.NoAttachments,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"]
};
NoDataStackLayout = new StackLayout
{
VerticalOptions = LayoutOptions.Start,
Spacing = 0,
Margin = new Thickness(0, 40, 0, 0)
};
var saveToolBarItem = new ToolbarItem(AppResources.Save, null, async () =>
{
if(_lastAction.LastActionWasRecent() || _login == null)
{
return;
}
_lastAction = DateTime.UtcNow;
if(!_connectivity.IsConnected)
{
AlertNoConnection();
return;
}
if(_fileBytes == null)
{
await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired,
AppResources.File), AppResources.Ok);
return;
}
_userDialogs.ShowLoading(AppResources.Saving, MaskType.Black);
var saveTask = await _loginService.EncryptAndSaveAttachmentAsync(_login, _fileBytes, FileLabel.Text);
_userDialogs.HideLoading();
if(saveTask.Succeeded)
{
_fileBytes = null;
FileLabel.Text = AppResources.NoFileChosen;
_userDialogs.Toast(AppResources.AttachementAdded);
_googleAnalyticsService.TrackAppEvent("AddedAttachment");
await LoadAttachmentsAsync();
}
else if(saveTask.Errors.Count() > 0)
{
await _userDialogs.AlertAsync(saveTask.Errors.First().Message, AppResources.AnErrorHasOccurred);
}
else
{
await _userDialogs.AlertAsync(AppResources.AnErrorHasOccurred);
}
}, ToolbarItemOrder.Default, 0);
Title = AppResources.Attachments;
Content = ListView;
ToolbarItems.Add(saveToolBarItem);
if(Device.RuntimePlatform == Device.iOS)
{
ListView.RowHeight = -1;
NewTable.RowHeight = -1;
NewTable.EstimatedRowHeight = 44;
NewTable.HeightRequest = 180;
ListView.BackgroundColor = Color.Transparent;
ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close));
}
}
protected async override void OnAppearing()
{
base.OnAppearing();
ListView.ItemSelected += AttachmentSelected;
await LoadAttachmentsAsync();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
ListView.ItemSelected -= AttachmentSelected;
}
private async Task LoadAttachmentsAsync()
{
_login = await _loginService.GetByIdAsync(_loginId);
if(_login == null)
{
await Navigation.PopForDeviceAsync();
return;
}
var attachmentsToAdd = _login.Attachments
.Select(a => new VaultAttachmentsPageModel.Attachment(a))
.OrderBy(s => s.Name);
PresentationAttchments.ResetWithRange(attachmentsToAdd);
AdjustContent();
}
private void AdjustContent()
{
if(PresentationAttchments.Count == 0)
{
NoDataStackLayout.Children.Clear();
NoDataStackLayout.Children.Add(NoDataLabel);
NoDataStackLayout.Children.Add(NewTable);
Content = NoDataStackLayout;
}
else
{
Content = ListView;
}
}
private async void AttachmentSelected(object sender, SelectedItemChangedEventArgs e)
{
var attachment = e.SelectedItem as VaultAttachmentsPageModel.Attachment;
if(attachment == null)
{
return;
}
((ListView)sender).SelectedItem = null;
var buttons = new List<string> { };
var selection = await DisplayActionSheet(attachment.Name, AppResources.Cancel, AppResources.Delete,
buttons.ToArray());
if(selection == AppResources.Delete)
{
_userDialogs.ShowLoading(AppResources.Deleting, MaskType.Black);
var saveTask = await _loginService.DeleteAttachmentAsync(_login, attachment.Id);
_userDialogs.HideLoading();
if(saveTask.Succeeded)
{
_userDialogs.Toast(AppResources.AttachmentDeleted);
_googleAnalyticsService.TrackAppEvent("DeletedAttachment");
await LoadAttachmentsAsync();
}
else if(saveTask.Errors.Count() > 0)
{
await _userDialogs.AlertAsync(saveTask.Errors.First().Message, AppResources.AnErrorHasOccurred);
}
else
{
await _userDialogs.AlertAsync(AppResources.AnErrorHasOccurred);
}
}
}
private void AlertNoConnection()
{
DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage,
AppResources.Ok);
}
private void SubscribeFileResult(bool subscribe)
{
MessagingCenter.Unsubscribe<Application, Tuple<byte[], string>>(Application.Current, "SelectFileResult");
if(!subscribe)
{
return;
}
MessagingCenter.Subscribe<Application, Tuple<byte[], string>>(
Application.Current, "SelectFileResult", (sender, result) =>
{
FileLabel.Text = result.Item2;
_fileBytes = result.Item1;
SubscribeFileResult(true);
});
}
}
}

View File

@ -42,6 +42,7 @@ namespace Bit.App.Pages
public FormEditorCell NotesCell { get; private set; } public FormEditorCell NotesCell { get; private set; }
public FormPickerCell FolderCell { get; private set; } public FormPickerCell FolderCell { get; private set; }
public ExtendedTextCell GenerateCell { get; private set; } public ExtendedTextCell GenerateCell { get; private set; }
public ExtendedTextCell AttachmentsCell { get; private set; }
public ExtendedTextCell DeleteCell { get; private set; } public ExtendedTextCell DeleteCell { get; private set; }
private void Init() private void Init()
@ -112,6 +113,12 @@ namespace Bit.App.Pages
On = login.Favorite On = login.Favorite
}; };
AttachmentsCell = new ExtendedTextCell
{
Text = AppResources.Attachments,
ShowDisclousure = true
};
DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red }; DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red };
var table = new ExtendedTableView var table = new ExtendedTableView
@ -133,7 +140,8 @@ namespace Bit.App.Pages
{ {
TotpCell, TotpCell,
FolderCell, FolderCell,
favoriteCell favoriteCell,
AttachmentsCell
}, },
new TableSection(AppResources.Notes) new TableSection(AppResources.Notes)
{ {
@ -257,6 +265,10 @@ namespace Bit.App.Pages
{ {
GenerateCell.Tapped += GenerateCell_Tapped; GenerateCell.Tapped += GenerateCell_Tapped;
} }
if(AttachmentsCell != null)
{
AttachmentsCell.Tapped += AttachmentsCell_Tapped;
}
if(DeleteCell != null) if(DeleteCell != null)
{ {
DeleteCell.Tapped += DeleteCell_Tapped; DeleteCell.Tapped += DeleteCell_Tapped;
@ -286,6 +298,10 @@ namespace Bit.App.Pages
{ {
GenerateCell.Tapped -= GenerateCell_Tapped; GenerateCell.Tapped -= GenerateCell_Tapped;
} }
if(AttachmentsCell != null)
{
AttachmentsCell.Tapped -= AttachmentsCell_Tapped;
}
if(DeleteCell != null) if(DeleteCell != null)
{ {
DeleteCell.Tapped -= DeleteCell_Tapped; DeleteCell.Tapped -= DeleteCell_Tapped;
@ -336,6 +352,12 @@ namespace Bit.App.Pages
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
private async void AttachmentsCell_Tapped(object sender, EventArgs e)
{
var page = new ExtendedNavigationPage(new VaultAttachmentsPage(_loginId));
await Navigation.PushModalAsync(page);
}
private async void DeleteCell_Tapped(object sender, EventArgs e) private async void DeleteCell_Tapped(object sender, EventArgs e)
{ {
if(!_connectivity.IsConnected) if(!_connectivity.IsConnected)

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Bit.App.Utilities; using Bit.App.Utilities;
using System.Collections.Generic; using System.Collections.Generic;
using Bit.App.Models; using Bit.App.Models;
using System.Linq;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -164,52 +165,77 @@ namespace Bit.App.Pages
Model.Update(login); Model.Update(login);
if(!Model.ShowUri) if(LoginInformationSection.Contains(UriCell))
{ {
LoginInformationSection.Remove(UriCell); LoginInformationSection.Remove(UriCell);
} }
else if(!LoginInformationSection.Contains(UriCell)) if(Model.ShowUri)
{ {
LoginInformationSection.Add(UriCell); LoginInformationSection.Add(UriCell);
} }
if(!Model.ShowUsername) if(LoginInformationSection.Contains(UsernameCell))
{ {
LoginInformationSection.Remove(UsernameCell); LoginInformationSection.Remove(UsernameCell);
} }
else if(!LoginInformationSection.Contains(UsernameCell)) if(Model.ShowUsername)
{ {
LoginInformationSection.Add(UsernameCell); LoginInformationSection.Add(UsernameCell);
} }
if(!Model.ShowPassword) if(LoginInformationSection.Contains(PasswordCell))
{ {
LoginInformationSection.Remove(PasswordCell); LoginInformationSection.Remove(PasswordCell);
} }
else if(!LoginInformationSection.Contains(PasswordCell)) if(Model.ShowPassword)
{ {
LoginInformationSection.Add(PasswordCell); LoginInformationSection.Add(PasswordCell);
} }
if(!Model.ShowNotes) if(Table.Root.Contains(NotesSection))
{ {
Table.Root.Remove(NotesSection); Table.Root.Remove(NotesSection);
} }
else if(!Table.Root.Contains(NotesSection)) if(Model.ShowNotes)
{ {
Table.Root.Add(NotesSection); Table.Root.Add(NotesSection);
} }
// Totp
if(LoginInformationSection.Contains(TotpCodeCell))
{
LoginInformationSection.Remove(TotpCodeCell);
}
if(login.Totp != null && (_tokenService.TokenPremium || login.OrganizationUseTotp))
{
var totpKey = login.Totp.Decrypt(login.OrganizationId);
if(!string.IsNullOrWhiteSpace(totpKey))
{
Model.TotpCode = Crypto.Totp(totpKey);
if(!string.IsNullOrWhiteSpace(Model.TotpCode))
{
TotpTick(totpKey);
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
TotpTick(totpKey);
return true;
});
LoginInformationSection.Add(TotpCodeCell);
}
}
}
CleanupAttachmentCells(); CleanupAttachmentCells();
if(!Model.ShowAttachments && Table.Root.Contains(AttachmentsSection)) if(Table.Root.Contains(AttachmentsSection))
{ {
Table.Root.Remove(AttachmentsSection); Table.Root.Remove(AttachmentsSection);
} }
else if(Model.ShowAttachments && !Table.Root.Contains(AttachmentsSection)) if(Model.ShowAttachments)
{ {
AttachmentsSection = new TableSection(AppResources.Attachments); AttachmentsSection = new TableSection(AppResources.Attachments);
AttachmentCells = new List<AttachmentViewCell>(); AttachmentCells = new List<AttachmentViewCell>();
foreach(var attachment in Model.Attachments) foreach(var attachment in Model.Attachments.OrderBy(s => s.Name))
{ {
var attachmentCell = new AttachmentViewCell(attachment, async () => var attachmentCell = new AttachmentViewCell(attachment, async () =>
{ {
@ -222,38 +248,6 @@ namespace Bit.App.Pages
Table.Root.Add(AttachmentsSection); Table.Root.Add(AttachmentsSection);
} }
// Totp
var removeTotp = login.Totp == null || (!_tokenService.TokenPremium && !login.OrganizationUseTotp);
if(!removeTotp)
{
var totpKey = login.Totp.Decrypt(login.OrganizationId);
removeTotp = string.IsNullOrWhiteSpace(totpKey);
if(!removeTotp)
{
Model.TotpCode = Crypto.Totp(totpKey);
removeTotp = string.IsNullOrWhiteSpace(Model.TotpCode);
if(!removeTotp)
{
TotpTick(totpKey);
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
TotpTick(totpKey);
return true;
});
if(!LoginInformationSection.Contains(TotpCodeCell))
{
LoginInformationSection.Add(TotpCodeCell);
}
}
}
}
if(removeTotp && LoginInformationSection.Contains(TotpCodeCell))
{
LoginInformationSection.Remove(TotpCodeCell);
}
base.OnAppearing(); base.OnAppearing();
} }

View File

@ -5,6 +5,8 @@ using Bit.App.Abstractions;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using Newtonsoft.Json; using Newtonsoft.Json;
using Plugin.Connectivity.Abstractions; using Plugin.Connectivity.Abstractions;
using System.Globalization;
using System.IO;
namespace Bit.App.Repositories namespace Bit.App.Repositories
{ {
@ -99,5 +101,89 @@ namespace Bit.App.Repositories
} }
} }
} }
public virtual async Task<ApiResult<CipherResponse>> PostAttachmentAsync(string cipherId, byte[] data,
string fileName)
{
if(!Connectivity.IsConnected)
{
return HandledNotConnected<CipherResponse>();
}
var tokenStateResponse = await HandleTokenStateAsync<CipherResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.ApiClient)
using(var content = new MultipartFormDataContent("--BWMobileFormBoundary" + DateTime.UtcNow.Ticks))
{
content.Add(new StreamContent(new MemoryStream(data)), "data", fileName);
var requestMessage = new TokenHttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/", cipherId, "/attachment")),
Content = content
};
try
{
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<CipherResponse>(response).ConfigureAwait(false);
}
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var responseObj = JsonConvert.DeserializeObject<CipherResponse>(responseContent);
return ApiResult<CipherResponse>.Success(responseObj, response.StatusCode);
}
catch
{
return HandledWebException<CipherResponse>();
}
}
}
public virtual async Task<ApiResult> DeleteAttachmentAsync(string cipherId, string attachmentId)
{
if(!Connectivity.IsConnected)
{
return HandledNotConnected();
}
var tokenStateResponse = await HandleTokenStateAsync();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.ApiClient)
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Delete,
RequestUri = new Uri(client.BaseAddress,
string.Concat(ApiRoute, "/", cipherId, "/attachment/", attachmentId)),
};
try
{
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync(response).ConfigureAwait(false);
}
return ApiResult.Success(response.StatusCode);
}
catch
{
return HandledWebException();
}
}
}
} }
} }

View File

@ -151,6 +151,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Attachment added.
/// </summary>
public static string AttachementAdded {
get {
return ResourceManager.GetString("AttachementAdded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Attachment deleted.
/// </summary>
public static string AttachmentDeleted {
get {
return ResourceManager.GetString("AttachmentDeleted", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to This attachment is {0} in size. Are you sure you want to download it onto your device?. /// Looks up a localized string similar to This attachment is {0} in size. Are you sure you want to download it onto your device?.
/// </summary> /// </summary>
@ -529,6 +547,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Choose File.
/// </summary>
public static string ChooseFile {
get {
return ResourceManager.GetString("ChooseFile", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Close. /// Looks up a localized string similar to Close.
/// </summary> /// </summary>
@ -997,6 +1024,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to File.
/// </summary>
public static string File {
get {
return ResourceManager.GetString("File", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to File a Bug Report. /// Looks up a localized string similar to File a Bug Report.
/// </summary> /// </summary>
@ -1582,6 +1618,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to There are no attachments..
/// </summary>
public static string NoAttachments {
get {
return ResourceManager.GetString("NoAttachments", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to There are no favorites in your vault.. /// Looks up a localized string similar to There are no favorites in your vault..
/// </summary> /// </summary>
@ -1591,6 +1636,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to No file chosen.
/// </summary>
public static string NoFileChosen {
get {
return ResourceManager.GetString("NoFileChosen", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to There are no logins in your vault.. /// Looks up a localized string similar to There are no logins in your vault..
/// </summary> /// </summary>

View File

@ -965,4 +965,22 @@
<data name="PremiumRequired" xml:space="preserve"> <data name="PremiumRequired" xml:space="preserve">
<value>A premium membership is required to use this feature.</value> <value>A premium membership is required to use this feature.</value>
</data> </data>
<data name="AttachementAdded" xml:space="preserve">
<value>Attachment added</value>
</data>
<data name="AttachmentDeleted" xml:space="preserve">
<value>Attachment deleted</value>
</data>
<data name="ChooseFile" xml:space="preserve">
<value>Choose File</value>
</data>
<data name="File" xml:space="preserve">
<value>File</value>
</data>
<data name="NoFileChosen" xml:space="preserve">
<value>No file chosen</value>
</data>
<data name="NoAttachments" xml:space="preserve">
<value>There are no attachments.</value>
</data>
</root> </root>

View File

@ -260,6 +260,26 @@ namespace Bit.App.Services
return Crypto.AesCbcEncrypt(plainBytes, key); return Crypto.AesCbcEncrypt(plainBytes, key);
} }
public byte[] EncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key = null)
{
if(key == null)
{
key = EncKey ?? Key;
}
if(key == null)
{
throw new ArgumentNullException(nameof(key));
}
if(plainBytes == null)
{
throw new ArgumentNullException(nameof(plainBytes));
}
return Crypto.AesCbcEncryptToBytes(plainBytes, key);
}
public string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null) public string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null)
{ {
try try

View File

@ -17,6 +17,7 @@ namespace Bit.App.Services
private readonly IAttachmentRepository _attachmentRepository; private readonly IAttachmentRepository _attachmentRepository;
private readonly IAuthService _authService; private readonly IAuthService _authService;
private readonly ILoginApiRepository _loginApiRepository; private readonly ILoginApiRepository _loginApiRepository;
private readonly ICipherApiRepository _cipherApiRepository;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
@ -25,6 +26,7 @@ namespace Bit.App.Services
IAttachmentRepository attachmentRepository, IAttachmentRepository attachmentRepository,
IAuthService authService, IAuthService authService,
ILoginApiRepository loginApiRepository, ILoginApiRepository loginApiRepository,
ICipherApiRepository cipherApiRepository,
ISettingsService settingsService, ISettingsService settingsService,
ICryptoService cryptoService) ICryptoService cryptoService)
{ {
@ -32,6 +34,7 @@ namespace Bit.App.Services
_attachmentRepository = attachmentRepository; _attachmentRepository = attachmentRepository;
_authService = authService; _authService = authService;
_loginApiRepository = loginApiRepository; _loginApiRepository = loginApiRepository;
_cipherApiRepository = cipherApiRepository;
_settingsService = settingsService; _settingsService = settingsService;
_cryptoService = cryptoService; _cryptoService = cryptoService;
} }
@ -238,7 +241,7 @@ namespace Bit.App.Services
{ {
return null; return null;
} }
if(!string.IsNullOrWhiteSpace(orgId)) if(!string.IsNullOrWhiteSpace(orgId))
{ {
return _cryptoService.DecryptToBytes(data, _cryptoService.GetOrgKey(orgId)); return _cryptoService.DecryptToBytes(data, _cryptoService.GetOrgKey(orgId));
@ -255,6 +258,47 @@ namespace Bit.App.Services
} }
} }
public async Task<ApiResult<CipherResponse>> EncryptAndSaveAttachmentAsync(Login login, byte[] data, string fileName)
{
var encFileName = fileName.Encrypt(login.OrganizationId);
var encBytes = _cryptoService.EncryptToBytes(data,
login.OrganizationId != null ? _cryptoService.GetOrgKey(login.OrganizationId) : null);
var response = await _cipherApiRepository.PostAttachmentAsync(login.Id, encBytes, encFileName.EncryptedString);
if(response.Succeeded)
{
var attachmentData = response.Result.Attachments.Select(a => new AttachmentData(a, login.Id));
foreach(var attachment in attachmentData)
{
await _attachmentRepository.UpsertAsync(attachment);
}
login.Attachments = response.Result.Attachments.Select(a => new Attachment(a));
}
else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden
|| response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
MessagingCenter.Send(Application.Current, "Logout", (string)null);
}
return response;
}
public async Task<ApiResult> DeleteAttachmentAsync(Login login, string attachmentId)
{
var response = await _cipherApiRepository.DeleteAttachmentAsync(login.Id, attachmentId);
if(response.Succeeded)
{
await _attachmentRepository.DeleteAsync(attachmentId);
}
else if(response.StatusCode == System.Net.HttpStatusCode.Forbidden
|| response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
MessagingCenter.Send(Application.Current, "Logout", (string)null);
}
return response;
}
private string WebUriFromAndroidAppUri(string androidAppUriString) private string WebUriFromAndroidAppUri(string androidAppUriString)
{ {
if(!UriIsAndroidApp(androidAppUriString)) if(!UriIsAndroidApp(androidAppUriString))

View File

@ -10,6 +10,26 @@ namespace Bit.App.Utilities
public static class Crypto public static class Crypto
{ {
public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key) public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key)
{
var parts = AesCbcEncryptToParts(plainBytes, key);
return new CipherString(parts.Item1, Convert.ToBase64String(parts.Item2),
Convert.ToBase64String(parts.Item4), Convert.ToBase64String(parts.Item3));
}
public static byte[] AesCbcEncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key)
{
var parts = AesCbcEncryptToParts(plainBytes, key);
var encBytes = new byte[1 + parts.Item2.Length + parts.Item3.Length + parts.Item4.Length];
encBytes[0] = (byte)parts.Item1;
parts.Item2.CopyTo(encBytes, 1);
parts.Item3.CopyTo(encBytes, 1 + parts.Item2.Length);
parts.Item4.CopyTo(encBytes, 1 + parts.Item2.Length + parts.Item3.Length);
return encBytes;
}
private static Tuple<EncryptionType, byte[], byte[], byte[]> AesCbcEncryptToParts(byte[] plainBytes,
SymmetricCryptoKey key)
{ {
if(key == null) if(key == null)
{ {
@ -24,11 +44,10 @@ namespace Bit.App.Utilities
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
var cryptoKey = provider.CreateSymmetricKey(key.EncKey); var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
var iv = RandomBytes(provider.BlockLength); var iv = RandomBytes(provider.BlockLength);
var encryptedBytes = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv); var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
var mac = key.MacKey != null ? ComputeMacBase64(encryptedBytes, iv, key.MacKey) : null; var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null;
return new CipherString(key.EncryptionType, Convert.ToBase64String(iv), return new Tuple<EncryptionType, byte[], byte[], byte[]>(key.EncryptionType, iv, mac, ct);
Convert.ToBase64String(encryptedBytes), mac);
} }
public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key) public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key)
@ -84,12 +103,6 @@ namespace Bit.App.Utilities
return WinRTCrypto.CryptographicBuffer.GenerateRandom(length); return WinRTCrypto.CryptographicBuffer.GenerateRandom(length);
} }
public static string ComputeMacBase64(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
{
var mac = ComputeMac(ctBytes, ivBytes, macKey);
return Convert.ToBase64String(mac);
}
public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey) public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
{ {
if(ctBytes == null) if(ctBytes == null)

View File

@ -5,6 +5,9 @@ using Foundation;
using System.IO; using System.IO;
using MobileCoreServices; using MobileCoreServices;
using Bit.App.Resources; using Bit.App.Resources;
using Xamarin.Forms;
using Photos;
using System.Net;
namespace Bit.iOS.Services namespace Bit.iOS.Services
{ {
@ -86,7 +89,7 @@ namespace Bit.iOS.Services
return tmp; return tmp;
} }
public byte[] SelectFile() public void SelectFile()
{ {
var controller = GetVisibleViewController(); var controller = GetVisibleViewController();
var picker = new UIDocumentMenuViewController(new string[] { UTType.Data }, UIDocumentPickerMode.Import); var picker = new UIDocumentMenuViewController(new string[] { UTType.Data }, UIDocumentPickerMode.Import);
@ -114,18 +117,25 @@ namespace Bit.iOS.Services
}; };
controller.PresentViewController(picker, true, null); controller.PresentViewController(picker, true, null);
return null;
} }
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e) private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{ {
if(sender is UIImagePickerController picker) if(sender is UIImagePickerController picker)
{ {
//var image = (UIImage)e.Info.ObjectForKey(new NSString("UIImagePickerControllerOriginalImage")); string fileName = null;
NSObject urlObj;
if(e.Info.TryGetValue(UIImagePickerController.ReferenceUrl, out urlObj))
{
var result = PHAsset.FetchAssets(new NSUrl[] { (urlObj as NSUrl) }, null);
fileName = result?.firstObject?.ValueForKey(new NSString("filename"))?.ToString();
}
// TODO: determine if JPG or PNG from extension. Get filename somehow? fileName = fileName ?? $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
var lowerFilename = fileName?.ToLowerInvariant();
byte[] data; byte[] data;
if(false) if(lowerFilename != null && (lowerFilename.EndsWith(".jpg") || lowerFilename.EndsWith(".jpeg")))
{ {
using(var imageData = e.OriginalImage.AsJPEG()) using(var imageData = e.OriginalImage.AsJPEG())
{ {
@ -144,6 +154,7 @@ namespace Bit.iOS.Services
} }
} }
SelectFileResult(data, fileName);
picker.DismissViewController(true, null); picker.DismissViewController(true, null);
} }
} }
@ -159,17 +170,35 @@ namespace Bit.iOS.Services
private void DocumentPicker_DidPickDocument(object sender, UIDocumentPickedEventArgs e) private void DocumentPicker_DidPickDocument(object sender, UIDocumentPickedEventArgs e)
{ {
e.Url.StartAccessingSecurityScopedResource(); e.Url.StartAccessingSecurityScopedResource();
var doc = new UIDocument(e.Url);
var fileName = doc.LocalizedName;
if(string.IsNullOrWhiteSpace(fileName))
{
var path = doc.FileUrl?.ToString();
if(path != null)
{
path = WebUtility.UrlDecode(path);
var split = path.LastIndexOf('/');
fileName = path.Substring(split + 1);
}
}
var fileCoordinator = new NSFileCoordinator(); var fileCoordinator = new NSFileCoordinator();
// TODO: get filename?
NSError error; NSError error;
fileCoordinator.CoordinateRead(e.Url, NSFileCoordinatorReadingOptions.WithoutChanges, out error, (url) => fileCoordinator.CoordinateRead(e.Url, NSFileCoordinatorReadingOptions.WithoutChanges, out error, (url) =>
{ {
var data = NSData.FromUrl(url).ToArray(); var data = NSData.FromUrl(url).ToArray();
SelectFileResult(data, fileName ?? "unknown_file_name");
}); });
e.Url.StopAccessingSecurityScopedResource(); e.Url.StopAccessingSecurityScopedResource();
} }
private void SelectFileResult(byte[] data, string fileName)
{
MessagingCenter.Send(Xamarin.Forms.Application.Current, "SelectFileResult",
new Tuple<byte[], string>(data, fileName));
}
} }
} }

View File

@ -2170,6 +2170,9 @@ namespace Bit.Android.Test
// aapt resource value: 0x7f080038 // aapt resource value: 0x7f080038
public const int collapseActionView = 2131230776; public const int collapseActionView = 2131230776;
// aapt resource value: 0x7f0800b7
public const int contentFrame = 2131230903;
// aapt resource value: 0x7f08004c // aapt resource value: 0x7f08004c
public const int contentPanel = 2131230796; public const int contentPanel = 2131230796;
@ -2790,6 +2793,12 @@ namespace Bit.Android.Test
// aapt resource value: 0x7f03003e // aapt resource value: 0x7f03003e
public const int test_suite = 2130903102; public const int test_suite = 2130903102;
// aapt resource value: 0x7f03003f
public const int zxingscanneractivitylayout = 2130903103;
// aapt resource value: 0x7f030040
public const int zxingscannerfragmentlayout = 2130903104;
static Layout() static Layout()
{ {
global::Android.Runtime.ResourceIdManager.UpdateIdValues(); global::Android.Runtime.ResourceIdManager.UpdateIdValues();