mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 01:48:25 +03:00
attachments page with upload/delete
This commit is contained in:
parent
b32603b472
commit
f9d336a3a6
24 changed files with 786 additions and 80 deletions
|
@ -17,6 +17,7 @@ using Bit.App.Models.Page;
|
|||
using Bit.App;
|
||||
using Android.Nfc;
|
||||
using Android.Views.InputMethods;
|
||||
using System.IO;
|
||||
|
||||
namespace Bit.Android
|
||||
{
|
||||
|
@ -215,6 +216,25 @@ namespace Bit.Android
|
|||
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()
|
||||
{
|
||||
try
|
||||
|
|
|
@ -6,6 +6,7 @@ using Android.Webkit;
|
|||
using Plugin.CurrentActivity;
|
||||
using System.IO;
|
||||
using Android.Support.V4.Content;
|
||||
using Bit.App;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ using Android.Content;
|
|||
using Java.Security;
|
||||
using System.IO;
|
||||
using Android.Nfc;
|
||||
using Android.Provider;
|
||||
|
||||
namespace Bit.Android
|
||||
{
|
||||
|
@ -101,5 +102,28 @@ namespace Bit.Android
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,5 +8,7 @@ namespace Bit.App.Abstractions
|
|||
{
|
||||
Task<ApiResult<CipherResponse>> GetByIdAsync(string id);
|
||||
Task<ApiResult<ListResponse<CipherResponse>>> GetAsync();
|
||||
Task<ApiResult<CipherResponse>> PostAttachmentAsync(string cipherId, byte[] data, string fileName);
|
||||
Task<ApiResult> DeleteAttachmentAsync(string cipherId, string attachmentId);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ namespace Bit.App.Abstractions
|
|||
byte[] DecryptToBytes(byte[] encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] RsaDecryptToBytes(CipherString encyptedValue, byte[] privateKey);
|
||||
CipherString Encrypt(string plaintextValue, SymmetricCryptoKey key = null);
|
||||
byte[] EncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key = null);
|
||||
SymmetricCryptoKey MakeKeyFromPassword(string password, string salt);
|
||||
string MakeKeyFromPasswordBase64(string password, string salt);
|
||||
byte[] HashPassword(SymmetricCryptoKey key, string password);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
namespace Bit.App.Abstractions
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDeviceActionService
|
||||
{
|
||||
void CopyToClipboard(string text);
|
||||
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||
bool CanOpenFile(string fileName);
|
||||
byte[] SelectFile();
|
||||
void SelectFile();
|
||||
void ClearCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,5 +15,7 @@ namespace Bit.App.Abstractions
|
|||
Task<ApiResult<LoginResponse>> SaveAsync(Login login);
|
||||
Task<ApiResult> DeleteAsync(string id);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
<Compile Include="Controls\FormPickerCell.cs" />
|
||||
<Compile Include="Controls\FormEntryCell.cs" />
|
||||
<Compile Include="Controls\PinControl.cs" />
|
||||
<Compile Include="Controls\VaultAttachmentsViewCell.cs" />
|
||||
<Compile Include="Controls\VaultListViewCell.cs" />
|
||||
<Compile Include="Enums\TwoFactorProviderType.cs" />
|
||||
<Compile Include="Enums\EncryptionType.cs" />
|
||||
|
@ -121,6 +122,7 @@
|
|||
<Compile Include="Models\CipherString.cs" />
|
||||
<Compile Include="Models\Data\AttachmentData.cs" />
|
||||
<Compile Include="Models\Attachment.cs" />
|
||||
<Compile Include="Models\Page\VaultAttachmentsPageModel.cs" />
|
||||
<Compile Include="Models\SymmetricCryptoKey.cs" />
|
||||
<Compile Include="Models\Data\SettingsData.cs" />
|
||||
<Compile Include="Models\Data\FolderData.cs" />
|
||||
|
@ -162,6 +164,7 @@
|
|||
<Compile Include="Pages\Settings\SettingsPage.cs" />
|
||||
<Compile Include="Pages\Settings\SettingsListFoldersPage.cs" />
|
||||
<Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" />
|
||||
<Compile Include="Pages\Vault\VaultAttachmentsPage.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ILoginRepository.cs" />
|
||||
<Compile Include="Repositories\AttachmentRepository.cs" />
|
||||
|
|
|
@ -33,5 +33,7 @@
|
|||
public const string Locked = "other:locked";
|
||||
public const string LastLoginEmail = "other:lastLoginEmail";
|
||||
public const string LastSync = "other:lastSync";
|
||||
|
||||
public const int SelectFileRequestCode = 42;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ namespace Bit.App.Controls
|
|||
{
|
||||
public class LabeledRightDetailCell : ExtendedViewCell
|
||||
{
|
||||
public LabeledRightDetailCell()
|
||||
public LabeledRightDetailCell(bool showIcon = true)
|
||||
{
|
||||
Label = new Label
|
||||
{
|
||||
|
@ -22,32 +22,38 @@ namespace Bit.App.Controls
|
|||
VerticalOptions = LayoutOptions.Center
|
||||
};
|
||||
|
||||
Icon = new CachedImage
|
||||
{
|
||||
WidthRequest = 16,
|
||||
HeightRequest = 16,
|
||||
HorizontalOptions = LayoutOptions.End,
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
|
||||
var stackLayout = new StackLayout
|
||||
StackLayout = new StackLayout
|
||||
{
|
||||
Orientation = StackOrientation.Horizontal,
|
||||
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)
|
||||
{
|
||||
Label.TextColor = Color.Black;
|
||||
}
|
||||
|
||||
View = stackLayout;
|
||||
View = StackLayout;
|
||||
}
|
||||
|
||||
public Label Label { get; private set; }
|
||||
public Label Detail { get; private set; }
|
||||
public CachedImage Icon { get; private set; }
|
||||
public StackLayout StackLayout { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
21
src/App/Controls/VaultAttachmentsViewCell.cs
Normal file
21
src/App/Controls/VaultAttachmentsViewCell.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/App/Models/Page/VaultAttachmentsPageModel.cs
Normal file
25
src/App/Models/Page/VaultAttachmentsPageModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -217,6 +217,23 @@ namespace Bit.App.Pages
|
|||
private void Layout_LayoutChanged(object sender, EventArgs e)
|
||||
{
|
||||
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)
|
||||
|
|
288
src/App/Pages/Vault/VaultAttachmentsPage.cs
Normal file
288
src/App/Pages/Vault/VaultAttachmentsPage.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ namespace Bit.App.Pages
|
|||
public FormEditorCell NotesCell { get; private set; }
|
||||
public FormPickerCell FolderCell { get; private set; }
|
||||
public ExtendedTextCell GenerateCell { get; private set; }
|
||||
public ExtendedTextCell AttachmentsCell { get; private set; }
|
||||
public ExtendedTextCell DeleteCell { get; private set; }
|
||||
|
||||
private void Init()
|
||||
|
@ -112,6 +113,12 @@ namespace Bit.App.Pages
|
|||
On = login.Favorite
|
||||
};
|
||||
|
||||
AttachmentsCell = new ExtendedTextCell
|
||||
{
|
||||
Text = AppResources.Attachments,
|
||||
ShowDisclousure = true
|
||||
};
|
||||
|
||||
DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red };
|
||||
|
||||
var table = new ExtendedTableView
|
||||
|
@ -133,7 +140,8 @@ namespace Bit.App.Pages
|
|||
{
|
||||
TotpCell,
|
||||
FolderCell,
|
||||
favoriteCell
|
||||
favoriteCell,
|
||||
AttachmentsCell
|
||||
},
|
||||
new TableSection(AppResources.Notes)
|
||||
{
|
||||
|
@ -257,6 +265,10 @@ namespace Bit.App.Pages
|
|||
{
|
||||
GenerateCell.Tapped += GenerateCell_Tapped;
|
||||
}
|
||||
if(AttachmentsCell != null)
|
||||
{
|
||||
AttachmentsCell.Tapped += AttachmentsCell_Tapped;
|
||||
}
|
||||
if(DeleteCell != null)
|
||||
{
|
||||
DeleteCell.Tapped += DeleteCell_Tapped;
|
||||
|
@ -286,6 +298,10 @@ namespace Bit.App.Pages
|
|||
{
|
||||
GenerateCell.Tapped -= GenerateCell_Tapped;
|
||||
}
|
||||
if(AttachmentsCell != null)
|
||||
{
|
||||
AttachmentsCell.Tapped -= AttachmentsCell_Tapped;
|
||||
}
|
||||
if(DeleteCell != null)
|
||||
{
|
||||
DeleteCell.Tapped -= DeleteCell_Tapped;
|
||||
|
@ -336,6 +352,12 @@ namespace Bit.App.Pages
|
|||
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)
|
||||
{
|
||||
if(!_connectivity.IsConnected)
|
||||
|
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||
using Bit.App.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using Bit.App.Models;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
|
@ -164,52 +165,77 @@ namespace Bit.App.Pages
|
|||
|
||||
Model.Update(login);
|
||||
|
||||
if(!Model.ShowUri)
|
||||
if(LoginInformationSection.Contains(UriCell))
|
||||
{
|
||||
LoginInformationSection.Remove(UriCell);
|
||||
}
|
||||
else if(!LoginInformationSection.Contains(UriCell))
|
||||
if(Model.ShowUri)
|
||||
{
|
||||
LoginInformationSection.Add(UriCell);
|
||||
}
|
||||
|
||||
if(!Model.ShowUsername)
|
||||
if(LoginInformationSection.Contains(UsernameCell))
|
||||
{
|
||||
LoginInformationSection.Remove(UsernameCell);
|
||||
}
|
||||
else if(!LoginInformationSection.Contains(UsernameCell))
|
||||
if(Model.ShowUsername)
|
||||
{
|
||||
LoginInformationSection.Add(UsernameCell);
|
||||
}
|
||||
|
||||
if(!Model.ShowPassword)
|
||||
if(LoginInformationSection.Contains(PasswordCell))
|
||||
{
|
||||
LoginInformationSection.Remove(PasswordCell);
|
||||
}
|
||||
else if(!LoginInformationSection.Contains(PasswordCell))
|
||||
if(Model.ShowPassword)
|
||||
{
|
||||
LoginInformationSection.Add(PasswordCell);
|
||||
}
|
||||
|
||||
if(!Model.ShowNotes)
|
||||
if(Table.Root.Contains(NotesSection))
|
||||
{
|
||||
Table.Root.Remove(NotesSection);
|
||||
}
|
||||
else if(!Table.Root.Contains(NotesSection))
|
||||
if(Model.ShowNotes)
|
||||
{
|
||||
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();
|
||||
if(!Model.ShowAttachments && Table.Root.Contains(AttachmentsSection))
|
||||
if(Table.Root.Contains(AttachmentsSection))
|
||||
{
|
||||
Table.Root.Remove(AttachmentsSection);
|
||||
}
|
||||
else if(Model.ShowAttachments && !Table.Root.Contains(AttachmentsSection))
|
||||
if(Model.ShowAttachments)
|
||||
{
|
||||
AttachmentsSection = new TableSection(AppResources.Attachments);
|
||||
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 () =>
|
||||
{
|
||||
|
@ -222,38 +248,6 @@ namespace Bit.App.Pages
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ using Bit.App.Abstractions;
|
|||
using Bit.App.Models.Api;
|
||||
using Newtonsoft.Json;
|
||||
using Plugin.Connectivity.Abstractions;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
54
src/App/Resources/AppResources.Designer.cs
generated
54
src/App/Resources/AppResources.Designer.cs
generated
|
@ -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>
|
||||
/// 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>
|
||||
|
@ -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>
|
||||
/// Looks up a localized string similar to Close.
|
||||
/// </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>
|
||||
/// Looks up a localized string similar to File a Bug Report.
|
||||
/// </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>
|
||||
/// Looks up a localized string similar to There are no favorites in your vault..
|
||||
/// </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>
|
||||
/// Looks up a localized string similar to There are no logins in your vault..
|
||||
/// </summary>
|
||||
|
|
|
@ -965,4 +965,22 @@
|
|||
<data name="PremiumRequired" xml:space="preserve">
|
||||
<value>A premium membership is required to use this feature.</value>
|
||||
</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>
|
|
@ -260,6 +260,26 @@ namespace Bit.App.Services
|
|||
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)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace Bit.App.Services
|
|||
private readonly IAttachmentRepository _attachmentRepository;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILoginApiRepository _loginApiRepository;
|
||||
private readonly ICipherApiRepository _cipherApiRepository;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
|
@ -25,6 +26,7 @@ namespace Bit.App.Services
|
|||
IAttachmentRepository attachmentRepository,
|
||||
IAuthService authService,
|
||||
ILoginApiRepository loginApiRepository,
|
||||
ICipherApiRepository cipherApiRepository,
|
||||
ISettingsService settingsService,
|
||||
ICryptoService cryptoService)
|
||||
{
|
||||
|
@ -32,6 +34,7 @@ namespace Bit.App.Services
|
|||
_attachmentRepository = attachmentRepository;
|
||||
_authService = authService;
|
||||
_loginApiRepository = loginApiRepository;
|
||||
_cipherApiRepository = cipherApiRepository;
|
||||
_settingsService = settingsService;
|
||||
_cryptoService = cryptoService;
|
||||
}
|
||||
|
@ -238,7 +241,7 @@ namespace Bit.App.Services
|
|||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(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)
|
||||
{
|
||||
if(!UriIsAndroidApp(androidAppUriString))
|
||||
|
|
|
@ -10,6 +10,26 @@ namespace Bit.App.Utilities
|
|||
public static class Crypto
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -24,11 +44,10 @@ namespace Bit.App.Utilities
|
|||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var iv = RandomBytes(provider.BlockLength);
|
||||
var encryptedBytes = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMacBase64(encryptedBytes, iv, key.MacKey) : null;
|
||||
var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null;
|
||||
|
||||
return new CipherString(key.EncryptionType, Convert.ToBase64String(iv),
|
||||
Convert.ToBase64String(encryptedBytes), mac);
|
||||
return new Tuple<EncryptionType, byte[], byte[], byte[]>(key.EncryptionType, iv, mac, ct);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key)
|
||||
|
@ -84,12 +103,6 @@ namespace Bit.App.Utilities
|
|||
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)
|
||||
{
|
||||
if(ctBytes == null)
|
||||
|
|
|
@ -5,6 +5,9 @@ using Foundation;
|
|||
using System.IO;
|
||||
using MobileCoreServices;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
using Photos;
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.iOS.Services
|
||||
{
|
||||
|
@ -86,7 +89,7 @@ namespace Bit.iOS.Services
|
|||
return tmp;
|
||||
}
|
||||
|
||||
public byte[] SelectFile()
|
||||
public void SelectFile()
|
||||
{
|
||||
var controller = GetVisibleViewController();
|
||||
var picker = new UIDocumentMenuViewController(new string[] { UTType.Data }, UIDocumentPickerMode.Import);
|
||||
|
@ -114,18 +117,25 @@ namespace Bit.iOS.Services
|
|||
};
|
||||
|
||||
controller.PresentViewController(picker, true, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
|
||||
{
|
||||
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;
|
||||
if(false)
|
||||
if(lowerFilename != null && (lowerFilename.EndsWith(".jpg") || lowerFilename.EndsWith(".jpeg")))
|
||||
{
|
||||
using(var imageData = e.OriginalImage.AsJPEG())
|
||||
{
|
||||
|
@ -144,6 +154,7 @@ namespace Bit.iOS.Services
|
|||
}
|
||||
}
|
||||
|
||||
SelectFileResult(data, fileName);
|
||||
picker.DismissViewController(true, null);
|
||||
}
|
||||
}
|
||||
|
@ -159,17 +170,35 @@ namespace Bit.iOS.Services
|
|||
private void DocumentPicker_DidPickDocument(object sender, UIDocumentPickedEventArgs e)
|
||||
{
|
||||
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();
|
||||
|
||||
// TODO: get filename?
|
||||
|
||||
NSError error;
|
||||
fileCoordinator.CoordinateRead(e.Url, NSFileCoordinatorReadingOptions.WithoutChanges, out error, (url) =>
|
||||
{
|
||||
var data = NSData.FromUrl(url).ToArray();
|
||||
SelectFileResult(data, fileName ?? "unknown_file_name");
|
||||
});
|
||||
|
||||
e.Url.StopAccessingSecurityScopedResource();
|
||||
}
|
||||
|
||||
private void SelectFileResult(byte[] data, string fileName)
|
||||
{
|
||||
MessagingCenter.Send(Xamarin.Forms.Application.Current, "SelectFileResult",
|
||||
new Tuple<byte[], string>(data, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
test/Android.Test/Resources/Resource.Designer.cs
generated
9
test/Android.Test/Resources/Resource.Designer.cs
generated
|
@ -2170,6 +2170,9 @@ namespace Bit.Android.Test
|
|||
// aapt resource value: 0x7f080038
|
||||
public const int collapseActionView = 2131230776;
|
||||
|
||||
// aapt resource value: 0x7f0800b7
|
||||
public const int contentFrame = 2131230903;
|
||||
|
||||
// aapt resource value: 0x7f08004c
|
||||
public const int contentPanel = 2131230796;
|
||||
|
||||
|
@ -2790,6 +2793,12 @@ namespace Bit.Android.Test
|
|||
// aapt resource value: 0x7f03003e
|
||||
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()
|
||||
{
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
|
||||
|
|
Loading…
Reference in a new issue