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:
parent
b32603b472
commit
f9d336a3a6
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" />
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,6 +22,15 @@ namespace Bit.App.Controls
|
|||||||
VerticalOptions = LayoutOptions.Center
|
VerticalOptions = LayoutOptions.Center
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StackLayout = new StackLayout
|
||||||
|
{
|
||||||
|
Orientation = StackOrientation.Horizontal,
|
||||||
|
Padding = new Thickness(15, 10),
|
||||||
|
Children = { Label, Detail }
|
||||||
|
};
|
||||||
|
|
||||||
|
if(showIcon)
|
||||||
|
{
|
||||||
Icon = new CachedImage
|
Icon = new CachedImage
|
||||||
{
|
{
|
||||||
WidthRequest = 16,
|
WidthRequest = 16,
|
||||||
@ -31,23 +40,20 @@ namespace Bit.App.Controls
|
|||||||
Margin = new Thickness(5, 0, 0, 0)
|
Margin = new Thickness(5, 0, 0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
var stackLayout = new StackLayout
|
StackLayout.Children.Add(Icon);
|
||||||
{
|
}
|
||||||
Orientation = StackOrientation.Horizontal,
|
|
||||||
Padding = new Thickness(15, 10),
|
|
||||||
Children = { Label, Detail, 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
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)
|
||||||
|
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 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)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
/// <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>
|
||||||
|
@ -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>
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
@ -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))
|
||||||
|
@ -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)
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
// 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();
|
||||||
|
Loading…
Reference in New Issue
Block a user