Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7490ba3179 | ||
|
|
45da12ad55 | ||
|
|
75f99bf899 | ||
|
|
bd2b1cc166 | ||
|
|
5fa8d64994 | ||
|
|
034957b556 | ||
|
|
a9d7c73b04 | ||
|
|
b09fe05fe6 | ||
|
|
ed146549ef | ||
|
|
86c11db1a1 | ||
|
|
580cd57433 | ||
|
|
9854ce82bd | ||
|
|
af11df97f5 | ||
|
|
9b4e664908 | ||
|
|
567e1ee116 | ||
|
|
ef7fa5363a | ||
|
|
f8bd7c2e64 | ||
|
|
de46c9ee36 | ||
|
|
5969e2d7ed | ||
|
|
83047558d5 | ||
|
|
f239868299 | ||
|
|
e4b962a3a6 | ||
|
|
b68a94c0e5 | ||
|
|
ec53ca8423 | ||
|
|
1ba0729e34 | ||
|
|
73425c0052 | ||
|
|
679859fb37 | ||
|
|
dbdc660464 | ||
|
|
aa22e7e952 | ||
|
|
b920e7e95c | ||
|
|
d14b23ca82 | ||
|
|
4e8f69f692 | ||
|
|
c96cf2b0e5 | ||
|
|
4921cfb593 | ||
|
|
395545f7b1 | ||
|
|
f9d336a3a6 | ||
|
|
b32603b472 | ||
|
|
1124c48c8d | ||
|
|
98e429505c | ||
|
|
d0b616ba24 | ||
|
|
dac4ffcb98 | ||
|
|
680310cf70 | ||
|
|
67ff82810f | ||
|
|
87e71ea860 | ||
|
|
26c110291e | ||
|
|
9879f074b4 | ||
|
|
65168c71c0 | ||
|
|
4c4996ee2a | ||
|
|
e0c67f87b0 | ||
|
|
352c8ee867 | ||
|
|
fe5cc1f8f3 | ||
|
|
eec4be1845 | ||
|
|
2f86b5c7b0 | ||
|
|
0d672c4f99 | ||
|
|
ac3fdbc2cd | ||
|
|
0a7ad44d23 | ||
|
|
18a86d3f12 | ||
|
|
665e66a9a6 | ||
|
|
06dc4117c7 | ||
|
|
2651afcef0 | ||
|
|
ce4d828380 | ||
|
|
74fba486bd | ||
|
|
56075cb7d9 | ||
|
|
d71bc775d5 | ||
|
|
45c5801538 | ||
|
|
cf41b524b0 | ||
|
|
e2a3e55a17 | ||
|
|
ae35bd2047 | ||
|
|
2f0ca6f7c0 | ||
|
|
37428c01dd | ||
|
|
4116d95a3e | ||
|
|
35ae2b783f | ||
|
|
8a24a6d192 | ||
|
|
19374a5df4 | ||
|
|
12da6fbd18 | ||
|
|
573ff15925 | ||
|
|
1b2abbe321 | ||
|
|
4a03da6b96 | ||
|
|
cf3998942f | ||
|
|
0c71f783fc | ||
|
|
d30b30b24f | ||
|
|
7823ec3fc8 | ||
|
|
1e5883f028 | ||
|
|
33c3cf4c4f | ||
|
|
f41ace4d7c | ||
|
|
65d2d45a82 | ||
|
|
47ca483459 | ||
|
|
ee759af078 | ||
|
|
872037cf4d | ||
|
|
6aaa083157 | ||
|
|
6a88524f8e | ||
|
|
82d93d2602 | ||
|
|
d62037ef6a | ||
|
|
7314b5a339 | ||
|
|
62bc230521 |
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.25420.1
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26430.13
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{04B18ED2-B76D-4947-8474-191F8FD2B5E0}"
|
||||
EndProject
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Android" />
|
||||
<Reference Include="Mono.Android.Export" />
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.9.0.1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll</HintPath>
|
||||
@@ -296,12 +297,28 @@
|
||||
<Reference Include="XLabs.Ioc.SimpleInjector, Version=2.0.5782.12229, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\XLabs.IoC.SimpleInjector.2.0.5782\lib\portable-net45+netcore45+wp8+MonoAndroid1+MonoTouch1\XLabs.Ioc.SimpleInjector.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXing.Net.Mobile.Core, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXing.Net.Mobile.Forms, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Forms.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXing.Net.Mobile.Forms.Android, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Forms.Android.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="zxing.portable, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\zxing.portable.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXingNetMobile, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\ZXingNetMobile.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AutofillActivity.cs" />
|
||||
<Compile Include="AutofillCredentials.cs" />
|
||||
<Compile Include="Controls\CustomSearchBarRenderer.cs" />
|
||||
<Compile Include="Controls\CustomButtonRenderer.cs" />
|
||||
<Compile Include="Controls\HybridWebViewRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedButtonRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedTabbedPageRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedTableViewRenderer.cs" />
|
||||
@@ -314,14 +331,14 @@
|
||||
<Compile Include="Controls\ExtendedPickerRenderer.cs" />
|
||||
<Compile Include="Controls\ExtendedEntryRenderer.cs" />
|
||||
<Compile Include="Services\HttpService.cs" />
|
||||
<Compile Include="Services\KeyStoreBackedStorageService.cs" />
|
||||
<Compile Include="Services\AndroidKeyStoreStorageService.cs" />
|
||||
<Compile Include="Services\LocalizeService.cs" />
|
||||
<Compile Include="MainApplication.cs" />
|
||||
<Compile Include="Resources\Resource.Designer.cs" />
|
||||
<Compile Include="Services\DeviceInfoService.cs" />
|
||||
<Compile Include="Services\GoogleAnalyticsService.cs" />
|
||||
<Compile Include="Services\AppInfoService.cs" />
|
||||
<Compile Include="Services\ClipboardService.cs" />
|
||||
<Compile Include="Services\DeviceActionService.cs" />
|
||||
<Compile Include="Services\BouncyCastleKeyDerivationService.cs" />
|
||||
<Compile Include="Services\KeyStoreStorageService.cs" />
|
||||
<Compile Include="MainActivity.cs" />
|
||||
@@ -858,6 +875,81 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\share_tools.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\yubikey.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\xml\filepaths.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\download.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\download.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\download.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\download.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\download.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\camera.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\camera.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\camera.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\camera.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\camera.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\paperclip.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\paperclip.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\paperclip.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\paperclip.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\paperclip.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\trash.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-hdpi\trash.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xhdpi\trash.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxhdpi\trash.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\trash.png" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
<Import Project="..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Bit.Android
|
||||
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
||||
new Browser("org.codeaurora.swe.browser", "url_bar"),
|
||||
new Browser("org.iron.srware", "url_bar"),
|
||||
new Browser("com.sec.android.app.sbrowser", "sbrowser_url_bar"),
|
||||
new Browser("com.sec.android.app.sbrowser", "location_bar_edit_text"),
|
||||
new Browser("com.sec.android.app.sbrowser.beta", "location_bar_edit_text"),
|
||||
new Browser("com.yandex.browser", "bro_omnibar_address_title_text",
|
||||
(s) => s.Split(' ').FirstOrDefault()),
|
||||
@@ -75,11 +75,9 @@ namespace Bit.Android
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
|
||||
var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
|
||||
testNodes.Dispose();
|
||||
*/
|
||||
//var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
|
||||
//var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
|
||||
//testNodes.Dispose();
|
||||
|
||||
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
|
||||
var cancelNotification = true;
|
||||
|
||||
72
src/Android/Controls/HybridWebViewRenderer.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Bit.Android.Controls;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
using Android.Webkit;
|
||||
using AWebkit = Android.Webkit;
|
||||
using Java.Interop;
|
||||
|
||||
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
|
||||
namespace Bit.Android.Controls
|
||||
{
|
||||
public class HybridWebViewRenderer : ViewRenderer<HybridWebView, AWebkit.WebView>
|
||||
{
|
||||
private const string JSFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<HybridWebView> e)
|
||||
{
|
||||
base.OnElementChanged(e);
|
||||
|
||||
if(Control == null)
|
||||
{
|
||||
var webView = new AWebkit.WebView(Forms.Context);
|
||||
webView.Settings.JavaScriptEnabled = true;
|
||||
SetNativeControl(webView);
|
||||
}
|
||||
|
||||
if(e.OldElement != null)
|
||||
{
|
||||
Control.RemoveJavascriptInterface("jsBridge");
|
||||
var hybridWebView = e.OldElement as HybridWebView;
|
||||
hybridWebView.Cleanup();
|
||||
}
|
||||
|
||||
if(e.NewElement != null)
|
||||
{
|
||||
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
|
||||
Control.LoadUrl(Element.Uri);
|
||||
InjectJS(JSFunction);
|
||||
}
|
||||
}
|
||||
|
||||
private void InjectJS(string script)
|
||||
{
|
||||
if(Control != null)
|
||||
{
|
||||
Control.LoadUrl(string.Format("javascript: {0}", script));
|
||||
}
|
||||
}
|
||||
|
||||
public class JSBridge : Java.Lang.Object
|
||||
{
|
||||
private readonly WeakReference<HybridWebViewRenderer> _hybridWebViewRenderer;
|
||||
|
||||
public JSBridge(HybridWebViewRenderer hybridRenderer)
|
||||
{
|
||||
_hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
|
||||
}
|
||||
|
||||
[JavascriptInterface]
|
||||
[Export("invokeAction")]
|
||||
public void InvokeAction(string data)
|
||||
{
|
||||
HybridWebViewRenderer hybridRenderer;
|
||||
if(_hybridWebViewRenderer != null && _hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
|
||||
{
|
||||
hybridRenderer.Element.InvokeAction(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ using Xamarin.Forms;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Page;
|
||||
using Bit.App;
|
||||
using Android.Nfc;
|
||||
using Android.Views.InputMethods;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Android
|
||||
{
|
||||
@@ -26,6 +30,9 @@ namespace Bit.Android
|
||||
{
|
||||
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
|
||||
private DateTime? _lastAction;
|
||||
private Java.Util.Regex.Pattern _otpPattern = Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private ISettings _settings;
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
@@ -49,7 +56,10 @@ namespace Bit.Android
|
||||
|
||||
Console.WriteLine("A OnCreate");
|
||||
Window.SetSoftInputMode(SoftInput.StateHidden);
|
||||
Window.AddFlags(WindowManagerFlags.Secure);
|
||||
if(!App.Utilities.Helpers.InDebugMode())
|
||||
{
|
||||
Window.AddFlags(WindowManagerFlags.Secure);
|
||||
}
|
||||
|
||||
var appIdService = Resolver.Resolve<IAppIdService>();
|
||||
var authService = Resolver.Resolve<IAuthService>();
|
||||
@@ -62,6 +72,8 @@ namespace Bit.Android
|
||||
typeof(Color).GetProperty("Accent", BindingFlags.Public | BindingFlags.Static)
|
||||
.SetValue(null, Color.FromHex("d2d6de"));
|
||||
|
||||
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
LoadApplication(new App.App(
|
||||
uri,
|
||||
Resolver.Resolve<IAuthService>(),
|
||||
@@ -69,12 +81,19 @@ namespace Bit.Android
|
||||
Resolver.Resolve<IUserDialogs>(),
|
||||
Resolver.Resolve<IDatabaseService>(),
|
||||
Resolver.Resolve<ISyncService>(),
|
||||
Resolver.Resolve<ISettings>(),
|
||||
_settings,
|
||||
Resolver.Resolve<ILockService>(),
|
||||
Resolver.Resolve<IGoogleAnalyticsService>(),
|
||||
Resolver.Resolve<ILocalizeService>(),
|
||||
Resolver.Resolve<IAppInfoService>(),
|
||||
Resolver.Resolve<IAppSettingsService>()));
|
||||
Resolver.Resolve<IAppSettingsService>(),
|
||||
_deviceActionService));
|
||||
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application>(
|
||||
Xamarin.Forms.Application.Current, "DismissKeyboard", (sender) =>
|
||||
{
|
||||
DismissKeyboard();
|
||||
});
|
||||
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "RateApp", (sender) =>
|
||||
{
|
||||
@@ -102,6 +121,12 @@ namespace Bit.Android
|
||||
{
|
||||
LaunchApp(args);
|
||||
});
|
||||
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application, bool>(
|
||||
Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender, listen) =>
|
||||
{
|
||||
ListenYubiKey(listen);
|
||||
});
|
||||
}
|
||||
|
||||
private void ReturnCredentials(VaultListPageModel.Login login)
|
||||
@@ -113,6 +138,13 @@ namespace Bit.Android
|
||||
}
|
||||
else
|
||||
{
|
||||
var isPremium = Resolver.Resolve<ITokenService>()?.TokenPremium ?? false;
|
||||
var autoCopyEnabled = !_settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false);
|
||||
if(isPremium && autoCopyEnabled && _deviceActionService != null && login.Totp.Value != null)
|
||||
{
|
||||
_deviceActionService.CopyToClipboard(App.Utilities.Crypto.Totp(login.Totp.Value));
|
||||
}
|
||||
|
||||
data.PutExtra("uri", login.Uri.Value);
|
||||
data.PutExtra("username", login.Username);
|
||||
data.PutExtra("password", login.Password.Value);
|
||||
@@ -134,6 +166,7 @@ namespace Bit.Android
|
||||
{
|
||||
Console.WriteLine("A OnPause");
|
||||
base.OnPause();
|
||||
ListenYubiKey(false);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
@@ -168,6 +201,68 @@ namespace Bit.Android
|
||||
// workaround for app compat bug
|
||||
// ref https://bugzilla.xamarin.com/show_bug.cgi?id=36907
|
||||
Task.Delay(10).Wait();
|
||||
|
||||
if(Utilities.NfcEnabled())
|
||||
{
|
||||
MessagingCenter.Send(Xamarin.Forms.Application.Current, "ResumeYubiKey");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
Console.WriteLine("A OnNewIntent");
|
||||
ParseYubiKey(intent.DataString);
|
||||
}
|
||||
|
||||
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
|
||||
{
|
||||
if(requestCode == Constants.SelectFilePermissionRequestCode)
|
||||
{
|
||||
if(grantResults.Any(r => r != Permission.Granted))
|
||||
{
|
||||
MessagingCenter.Send(Xamarin.Forms.Application.Current, "SelectFileCameraPermissionDenied");
|
||||
}
|
||||
await _deviceActionService.SelectFileAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
string fileName = null;
|
||||
if(data != null && data.Data != null)
|
||||
{
|
||||
uri = data.Data;
|
||||
fileName = Utilities.GetFileName(ApplicationContext, uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// camera
|
||||
var root = new Java.IO.File(global::Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
uri = global::Android.Net.Uri.FromFile(file);
|
||||
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
||||
}
|
||||
|
||||
if(uri == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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(), fileName ?? "unknown_file_name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RateApp()
|
||||
@@ -228,5 +323,59 @@ namespace Bit.Android
|
||||
StartActivity(launchIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void ListenYubiKey(bool listen)
|
||||
{
|
||||
if(!Utilities.NfcEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var adapter = NfcAdapter.GetDefaultAdapter(this);
|
||||
if(listen)
|
||||
{
|
||||
var intent = new Intent(this, Class);
|
||||
intent.AddFlags(ActivityFlags.SingleTop);
|
||||
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
|
||||
|
||||
// register for all NDEF tags starting with http och https
|
||||
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
||||
ndef.AddDataScheme("http");
|
||||
ndef.AddDataScheme("https");
|
||||
var filters = new IntentFilter[] { ndef };
|
||||
|
||||
// register for foreground dispatch so we'll receive tags according to our intent filters
|
||||
adapter.EnableForegroundDispatch(this, pendingIntent, filters, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
adapter.DisableForegroundDispatch(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseYubiKey(string data)
|
||||
{
|
||||
if(data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var otpMatch = _otpPattern.Matcher(data);
|
||||
if(otpMatch.Matches())
|
||||
{
|
||||
var otp = otpMatch.Group(1);
|
||||
MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", otp);
|
||||
}
|
||||
}
|
||||
|
||||
private void DismissKeyboard()
|
||||
{
|
||||
try
|
||||
{
|
||||
var imm = (InputMethodManager)GetSystemService(InputMethodService);
|
||||
imm.HideSoftInputFromWindow(CurrentFocus.WindowToken, 0);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Bit.Android
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership transer)
|
||||
: base(handle, transer)
|
||||
{
|
||||
// AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser;
|
||||
//AndroidEnvironment.UnhandledExceptionRaiser += AndroidEnvironment_UnhandledExceptionRaiser;
|
||||
|
||||
if(!Resolver.IsSet)
|
||||
{
|
||||
@@ -48,21 +48,11 @@ namespace Bit.Android
|
||||
|
||||
private void AndroidEnvironment_UnhandledExceptionRaiser(object sender, RaiseThrowableEventArgs e)
|
||||
{
|
||||
var message = AppendExceptionToMessage("", e.Exception);
|
||||
var message = Utilities.AppendExceptionToMessage("", e.Exception);
|
||||
//Utilities.SaveCrashFile(message, true);
|
||||
Utilities.SendCrashEmail(message, false);
|
||||
}
|
||||
|
||||
private string AppendExceptionToMessage(string message, Exception ex)
|
||||
{
|
||||
message += ("\n\n" + ex.Message + "\n\n" + ex.StackTrace);
|
||||
if(ex.InnerException != null)
|
||||
{
|
||||
return AppendExceptionToMessage(message, ex.InnerException);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public override void OnCreate()
|
||||
{
|
||||
base.OnCreate();
|
||||
@@ -103,7 +93,7 @@ namespace Bit.Android
|
||||
}
|
||||
|
||||
// 3. In debug mode
|
||||
if(InDebugMode())
|
||||
if(App.Utilities.Helpers.InDebugMode())
|
||||
{
|
||||
reregister = true;
|
||||
}
|
||||
@@ -124,15 +114,6 @@ namespace Bit.Android
|
||||
}
|
||||
}
|
||||
|
||||
private bool InDebugMode()
|
||||
{
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void OnTerminate()
|
||||
{
|
||||
base.OnTerminate();
|
||||
@@ -198,6 +179,7 @@ namespace Bit.Android
|
||||
{
|
||||
UserDialogs.Init(application);
|
||||
CachedImageRenderer.Init();
|
||||
ZXing.Net.Mobile.Forms.Android.Platform.Init();
|
||||
CrossFingerprint.SetCurrentActivityResolver(() => CrossCurrentActivity.Current.Activity);
|
||||
|
||||
//var container = new UnityContainer();
|
||||
@@ -207,30 +189,17 @@ namespace Bit.Android
|
||||
container.RegisterSingleton(application.ApplicationContext);
|
||||
container.RegisterSingleton<Application>(application);
|
||||
|
||||
// Secure Storage
|
||||
ISecureStorageService secureStorage;
|
||||
try
|
||||
{
|
||||
secureStorage = new KeyStoreBackedStorageService(CrossSettings.Current);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Some isolated devices are having a hard time generating RSA keys for the key store.
|
||||
// Continue using the "old" keystore implementation for now.
|
||||
secureStorage = new KeyStoreStorageService(new char[] { });
|
||||
}
|
||||
|
||||
// Services
|
||||
container.RegisterSingleton<IDatabaseService, DatabaseService>();
|
||||
container.RegisterSingleton<ISqlService, SqlService>();
|
||||
container.RegisterSingleton(secureStorage);
|
||||
container.RegisterSingleton<ISecureStorageService, AndroidKeyStoreStorageService>();
|
||||
container.RegisterSingleton<ICryptoService, CryptoService>();
|
||||
container.RegisterSingleton<IKeyDerivationService, BouncyCastleKeyDerivationService>();
|
||||
container.RegisterSingleton<IAuthService, AuthService>();
|
||||
container.RegisterSingleton<IFolderService, FolderService>();
|
||||
container.RegisterSingleton<ILoginService, LoginService>();
|
||||
container.RegisterSingleton<ISyncService, SyncService>();
|
||||
container.RegisterSingleton<IClipboardService, ClipboardService>();
|
||||
container.RegisterSingleton<IDeviceActionService, DeviceActionService>();
|
||||
container.RegisterSingleton<IAppIdService, AppIdService>();
|
||||
container.RegisterSingleton<IPasswordGenerationService, PasswordGenerationService>();
|
||||
container.RegisterSingleton<IReflectionService, ReflectionService>();
|
||||
@@ -250,6 +219,7 @@ namespace Bit.Android
|
||||
container.RegisterSingleton<IFolderRepository, FolderRepository>();
|
||||
container.RegisterSingleton<IFolderApiRepository, FolderApiRepository>();
|
||||
container.RegisterSingleton<ILoginRepository, LoginRepository>();
|
||||
container.RegisterSingleton<IAttachmentRepository, AttachmentRepository>();
|
||||
container.RegisterSingleton<ILoginApiRepository, LoginApiRepository>();
|
||||
container.RegisterSingleton<IConnectApiRepository, ConnectApiRepository>();
|
||||
container.RegisterSingleton<IDeviceApiRepository, DeviceApiRepository>();
|
||||
@@ -257,6 +227,7 @@ namespace Bit.Android
|
||||
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
|
||||
container.RegisterSingleton<ISettingsRepository, SettingsRepository>();
|
||||
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
|
||||
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
|
||||
|
||||
// Other
|
||||
container.RegisterSingleton(CrossSettings.Current);
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.6.5" android:installLocation="auto" android:versionCode="502">
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="23" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
<uses-permission android:name="com.x8bit.bitwarden.permission.C2D_MESSAGE" />
|
||||
<application android:label="bitwarden" android:theme="@style/BitwardenTheme" android:allowBackup="false"></application>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.8.0" android:installLocation="auto" android:versionCode="502" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="23" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
<uses-permission android:name="com.x8bit.bitwarden.permission.C2D_MESSAGE" />
|
||||
|
||||
<application android:label="bitwarden" android:theme="@style/BitwardenTheme" android:allowBackup="false">
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="com.x8bit.bitwarden.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -28,7 +28,3 @@ using Android.App;
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
// Add some common permissions, these can be removed if not needed
|
||||
[assembly: UsesPermission(Android.Manifest.Permission.Internet)]
|
||||
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
|
||||
367
src/Android/Resources/Resource.Designer.cs
generated
@@ -193,6 +193,12 @@ namespace Bit.Android
|
||||
global::Plugin.Fingerprint.Resource.Layout.FingerprintDialog = global::Bit.Android.Resource.Layout.FingerprintDialog;
|
||||
global::Splat.Resource.String.library_name = global::Bit.Android.Resource.String.library_name;
|
||||
global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarSize = global::Bit.Android.Resource.Attribute.actionBarSize;
|
||||
global::ZXing.Net.Mobile.Forms.Android.Resource.Layout.zxingscanneractivitylayout = global::Bit.Android.Resource.Layout.zxingscanneractivitylayout;
|
||||
global::ZXing.Net.Mobile.Forms.Android.Resource.Layout.zxingscannerfragmentlayout = global::Bit.Android.Resource.Layout.zxingscannerfragmentlayout;
|
||||
global::ZXing.Net.Mobile.Forms.Android.Resource.String.library_name = global::Bit.Android.Resource.String.library_name;
|
||||
global::ZXing.Mobile.Resource.Id.contentFrame = global::Bit.Android.Resource.Id.contentFrame;
|
||||
global::ZXing.Mobile.Resource.Layout.zxingscanneractivitylayout = global::Bit.Android.Resource.Layout.zxingscanneractivitylayout;
|
||||
global::ZXing.Mobile.Resource.Layout.zxingscannerfragmentlayout = global::Bit.Android.Resource.Layout.zxingscannerfragmentlayout;
|
||||
}
|
||||
|
||||
public partial class Animation
|
||||
@@ -2295,499 +2301,514 @@ namespace Bit.Android
|
||||
public const int accessibility_step2 = 2130837582;
|
||||
|
||||
// aapt resource value: 0x7f02004f
|
||||
public const int cloudup = 2130837583;
|
||||
public const int camera = 2130837583;
|
||||
|
||||
// aapt resource value: 0x7f020050
|
||||
public const int cogs = 2130837584;
|
||||
public const int cloudup = 2130837584;
|
||||
|
||||
// aapt resource value: 0x7f020051
|
||||
public const int cogs_selected = 2130837585;
|
||||
public const int cogs = 2130837585;
|
||||
|
||||
// aapt resource value: 0x7f020052
|
||||
public const int common_full_open_on_phone = 2130837586;
|
||||
public const int cogs_selected = 2130837586;
|
||||
|
||||
// aapt resource value: 0x7f020053
|
||||
public const int common_google_signin_btn_icon_dark = 2130837587;
|
||||
public const int common_full_open_on_phone = 2130837587;
|
||||
|
||||
// aapt resource value: 0x7f020054
|
||||
public const int common_google_signin_btn_icon_dark_disabled = 2130837588;
|
||||
public const int common_google_signin_btn_icon_dark = 2130837588;
|
||||
|
||||
// aapt resource value: 0x7f020055
|
||||
public const int common_google_signin_btn_icon_dark_focused = 2130837589;
|
||||
public const int common_google_signin_btn_icon_dark_disabled = 2130837589;
|
||||
|
||||
// aapt resource value: 0x7f020056
|
||||
public const int common_google_signin_btn_icon_dark_normal = 2130837590;
|
||||
public const int common_google_signin_btn_icon_dark_focused = 2130837590;
|
||||
|
||||
// aapt resource value: 0x7f020057
|
||||
public const int common_google_signin_btn_icon_dark_pressed = 2130837591;
|
||||
public const int common_google_signin_btn_icon_dark_normal = 2130837591;
|
||||
|
||||
// aapt resource value: 0x7f020058
|
||||
public const int common_google_signin_btn_icon_light = 2130837592;
|
||||
public const int common_google_signin_btn_icon_dark_pressed = 2130837592;
|
||||
|
||||
// aapt resource value: 0x7f020059
|
||||
public const int common_google_signin_btn_icon_light_disabled = 2130837593;
|
||||
public const int common_google_signin_btn_icon_light = 2130837593;
|
||||
|
||||
// aapt resource value: 0x7f02005a
|
||||
public const int common_google_signin_btn_icon_light_focused = 2130837594;
|
||||
public const int common_google_signin_btn_icon_light_disabled = 2130837594;
|
||||
|
||||
// aapt resource value: 0x7f02005b
|
||||
public const int common_google_signin_btn_icon_light_normal = 2130837595;
|
||||
public const int common_google_signin_btn_icon_light_focused = 2130837595;
|
||||
|
||||
// aapt resource value: 0x7f02005c
|
||||
public const int common_google_signin_btn_icon_light_pressed = 2130837596;
|
||||
public const int common_google_signin_btn_icon_light_normal = 2130837596;
|
||||
|
||||
// aapt resource value: 0x7f02005d
|
||||
public const int common_google_signin_btn_text_dark = 2130837597;
|
||||
public const int common_google_signin_btn_icon_light_pressed = 2130837597;
|
||||
|
||||
// aapt resource value: 0x7f02005e
|
||||
public const int common_google_signin_btn_text_dark_disabled = 2130837598;
|
||||
public const int common_google_signin_btn_text_dark = 2130837598;
|
||||
|
||||
// aapt resource value: 0x7f02005f
|
||||
public const int common_google_signin_btn_text_dark_focused = 2130837599;
|
||||
public const int common_google_signin_btn_text_dark_disabled = 2130837599;
|
||||
|
||||
// aapt resource value: 0x7f020060
|
||||
public const int common_google_signin_btn_text_dark_normal = 2130837600;
|
||||
public const int common_google_signin_btn_text_dark_focused = 2130837600;
|
||||
|
||||
// aapt resource value: 0x7f020061
|
||||
public const int common_google_signin_btn_text_dark_pressed = 2130837601;
|
||||
public const int common_google_signin_btn_text_dark_normal = 2130837601;
|
||||
|
||||
// aapt resource value: 0x7f020062
|
||||
public const int common_google_signin_btn_text_light = 2130837602;
|
||||
public const int common_google_signin_btn_text_dark_pressed = 2130837602;
|
||||
|
||||
// aapt resource value: 0x7f020063
|
||||
public const int common_google_signin_btn_text_light_disabled = 2130837603;
|
||||
public const int common_google_signin_btn_text_light = 2130837603;
|
||||
|
||||
// aapt resource value: 0x7f020064
|
||||
public const int common_google_signin_btn_text_light_focused = 2130837604;
|
||||
public const int common_google_signin_btn_text_light_disabled = 2130837604;
|
||||
|
||||
// aapt resource value: 0x7f020065
|
||||
public const int common_google_signin_btn_text_light_normal = 2130837605;
|
||||
public const int common_google_signin_btn_text_light_focused = 2130837605;
|
||||
|
||||
// aapt resource value: 0x7f020066
|
||||
public const int common_google_signin_btn_text_light_pressed = 2130837606;
|
||||
public const int common_google_signin_btn_text_light_normal = 2130837606;
|
||||
|
||||
// aapt resource value: 0x7f020067
|
||||
public const int common_ic_googleplayservices = 2130837607;
|
||||
public const int common_google_signin_btn_text_light_pressed = 2130837607;
|
||||
|
||||
// aapt resource value: 0x7f020068
|
||||
public const int common_plus_signin_btn_icon_dark = 2130837608;
|
||||
public const int common_ic_googleplayservices = 2130837608;
|
||||
|
||||
// aapt resource value: 0x7f020069
|
||||
public const int common_plus_signin_btn_icon_dark_disabled = 2130837609;
|
||||
public const int common_plus_signin_btn_icon_dark = 2130837609;
|
||||
|
||||
// aapt resource value: 0x7f02006a
|
||||
public const int common_plus_signin_btn_icon_dark_focused = 2130837610;
|
||||
public const int common_plus_signin_btn_icon_dark_disabled = 2130837610;
|
||||
|
||||
// aapt resource value: 0x7f02006b
|
||||
public const int common_plus_signin_btn_icon_dark_normal = 2130837611;
|
||||
public const int common_plus_signin_btn_icon_dark_focused = 2130837611;
|
||||
|
||||
// aapt resource value: 0x7f02006c
|
||||
public const int common_plus_signin_btn_icon_dark_pressed = 2130837612;
|
||||
public const int common_plus_signin_btn_icon_dark_normal = 2130837612;
|
||||
|
||||
// aapt resource value: 0x7f02006d
|
||||
public const int common_plus_signin_btn_icon_light = 2130837613;
|
||||
public const int common_plus_signin_btn_icon_dark_pressed = 2130837613;
|
||||
|
||||
// aapt resource value: 0x7f02006e
|
||||
public const int common_plus_signin_btn_icon_light_disabled = 2130837614;
|
||||
public const int common_plus_signin_btn_icon_light = 2130837614;
|
||||
|
||||
// aapt resource value: 0x7f02006f
|
||||
public const int common_plus_signin_btn_icon_light_focused = 2130837615;
|
||||
public const int common_plus_signin_btn_icon_light_disabled = 2130837615;
|
||||
|
||||
// aapt resource value: 0x7f020070
|
||||
public const int common_plus_signin_btn_icon_light_normal = 2130837616;
|
||||
public const int common_plus_signin_btn_icon_light_focused = 2130837616;
|
||||
|
||||
// aapt resource value: 0x7f020071
|
||||
public const int common_plus_signin_btn_icon_light_pressed = 2130837617;
|
||||
public const int common_plus_signin_btn_icon_light_normal = 2130837617;
|
||||
|
||||
// aapt resource value: 0x7f020072
|
||||
public const int common_plus_signin_btn_text_dark = 2130837618;
|
||||
public const int common_plus_signin_btn_icon_light_pressed = 2130837618;
|
||||
|
||||
// aapt resource value: 0x7f020073
|
||||
public const int common_plus_signin_btn_text_dark_disabled = 2130837619;
|
||||
public const int common_plus_signin_btn_text_dark = 2130837619;
|
||||
|
||||
// aapt resource value: 0x7f020074
|
||||
public const int common_plus_signin_btn_text_dark_focused = 2130837620;
|
||||
public const int common_plus_signin_btn_text_dark_disabled = 2130837620;
|
||||
|
||||
// aapt resource value: 0x7f020075
|
||||
public const int common_plus_signin_btn_text_dark_normal = 2130837621;
|
||||
public const int common_plus_signin_btn_text_dark_focused = 2130837621;
|
||||
|
||||
// aapt resource value: 0x7f020076
|
||||
public const int common_plus_signin_btn_text_dark_pressed = 2130837622;
|
||||
public const int common_plus_signin_btn_text_dark_normal = 2130837622;
|
||||
|
||||
// aapt resource value: 0x7f020077
|
||||
public const int common_plus_signin_btn_text_light = 2130837623;
|
||||
public const int common_plus_signin_btn_text_dark_pressed = 2130837623;
|
||||
|
||||
// aapt resource value: 0x7f020078
|
||||
public const int common_plus_signin_btn_text_light_disabled = 2130837624;
|
||||
public const int common_plus_signin_btn_text_light = 2130837624;
|
||||
|
||||
// aapt resource value: 0x7f020079
|
||||
public const int common_plus_signin_btn_text_light_focused = 2130837625;
|
||||
public const int common_plus_signin_btn_text_light_disabled = 2130837625;
|
||||
|
||||
// aapt resource value: 0x7f02007a
|
||||
public const int common_plus_signin_btn_text_light_normal = 2130837626;
|
||||
public const int common_plus_signin_btn_text_light_focused = 2130837626;
|
||||
|
||||
// aapt resource value: 0x7f02007b
|
||||
public const int common_plus_signin_btn_text_light_pressed = 2130837627;
|
||||
public const int common_plus_signin_btn_text_light_normal = 2130837627;
|
||||
|
||||
// aapt resource value: 0x7f02007c
|
||||
public const int design_fab_background = 2130837628;
|
||||
public const int common_plus_signin_btn_text_light_pressed = 2130837628;
|
||||
|
||||
// aapt resource value: 0x7f02007d
|
||||
public const int design_snackbar_background = 2130837629;
|
||||
public const int design_fab_background = 2130837629;
|
||||
|
||||
// aapt resource value: 0x7f02007e
|
||||
public const int envelope = 2130837630;
|
||||
public const int design_snackbar_background = 2130837630;
|
||||
|
||||
// aapt resource value: 0x7f02007f
|
||||
public const int eye = 2130837631;
|
||||
public const int download = 2130837631;
|
||||
|
||||
// aapt resource value: 0x7f020080
|
||||
public const int eye_slash = 2130837632;
|
||||
public const int envelope = 2130837632;
|
||||
|
||||
// aapt resource value: 0x7f020081
|
||||
public const int fa_lock = 2130837633;
|
||||
public const int eye = 2130837633;
|
||||
|
||||
// aapt resource value: 0x7f020082
|
||||
public const int fa_lock_selected = 2130837634;
|
||||
public const int eye_slash = 2130837634;
|
||||
|
||||
// aapt resource value: 0x7f020083
|
||||
public const int fingerprint = 2130837635;
|
||||
public const int fa_lock = 2130837635;
|
||||
|
||||
// aapt resource value: 0x7f020084
|
||||
public const int fingerprint_white = 2130837636;
|
||||
public const int fa_lock_selected = 2130837636;
|
||||
|
||||
// aapt resource value: 0x7f020085
|
||||
public const int folder = 2130837637;
|
||||
public const int fingerprint = 2130837637;
|
||||
|
||||
// aapt resource value: 0x7f020086
|
||||
public const int globe = 2130837638;
|
||||
public const int fingerprint_white = 2130837638;
|
||||
|
||||
// aapt resource value: 0x7f020087
|
||||
public const int hockeyapp_btn_background = 2130837639;
|
||||
public const int folder = 2130837639;
|
||||
|
||||
// aapt resource value: 0x7f020088
|
||||
public const int ic_audiotrack = 2130837640;
|
||||
public const int globe = 2130837640;
|
||||
|
||||
// aapt resource value: 0x7f020089
|
||||
public const int ic_audiotrack_light = 2130837641;
|
||||
public const int hockeyapp_btn_background = 2130837641;
|
||||
|
||||
// aapt resource value: 0x7f02008a
|
||||
public const int ic_bluetooth_grey = 2130837642;
|
||||
public const int ic_audiotrack = 2130837642;
|
||||
|
||||
// aapt resource value: 0x7f02008b
|
||||
public const int ic_bluetooth_white = 2130837643;
|
||||
public const int ic_audiotrack_light = 2130837643;
|
||||
|
||||
// aapt resource value: 0x7f02008c
|
||||
public const int ic_cast_dark = 2130837644;
|
||||
public const int ic_bluetooth_grey = 2130837644;
|
||||
|
||||
// aapt resource value: 0x7f02008d
|
||||
public const int ic_cast_disabled_light = 2130837645;
|
||||
public const int ic_bluetooth_white = 2130837645;
|
||||
|
||||
// aapt resource value: 0x7f02008e
|
||||
public const int ic_cast_grey = 2130837646;
|
||||
public const int ic_cast_dark = 2130837646;
|
||||
|
||||
// aapt resource value: 0x7f02008f
|
||||
public const int ic_cast_light = 2130837647;
|
||||
public const int ic_cast_disabled_light = 2130837647;
|
||||
|
||||
// aapt resource value: 0x7f020090
|
||||
public const int ic_cast_off_light = 2130837648;
|
||||
public const int ic_cast_grey = 2130837648;
|
||||
|
||||
// aapt resource value: 0x7f020091
|
||||
public const int ic_cast_on_0_light = 2130837649;
|
||||
public const int ic_cast_light = 2130837649;
|
||||
|
||||
// aapt resource value: 0x7f020092
|
||||
public const int ic_cast_on_1_light = 2130837650;
|
||||
public const int ic_cast_off_light = 2130837650;
|
||||
|
||||
// aapt resource value: 0x7f020093
|
||||
public const int ic_cast_on_2_light = 2130837651;
|
||||
public const int ic_cast_on_0_light = 2130837651;
|
||||
|
||||
// aapt resource value: 0x7f020094
|
||||
public const int ic_cast_on_light = 2130837652;
|
||||
public const int ic_cast_on_1_light = 2130837652;
|
||||
|
||||
// aapt resource value: 0x7f020095
|
||||
public const int ic_cast_white = 2130837653;
|
||||
public const int ic_cast_on_2_light = 2130837653;
|
||||
|
||||
// aapt resource value: 0x7f020096
|
||||
public const int ic_close_dark = 2130837654;
|
||||
public const int ic_cast_on_light = 2130837654;
|
||||
|
||||
// aapt resource value: 0x7f020097
|
||||
public const int ic_close_light = 2130837655;
|
||||
public const int ic_cast_white = 2130837655;
|
||||
|
||||
// aapt resource value: 0x7f020098
|
||||
public const int ic_collapse = 2130837656;
|
||||
public const int ic_close_dark = 2130837656;
|
||||
|
||||
// aapt resource value: 0x7f020099
|
||||
public const int ic_collapse_00000 = 2130837657;
|
||||
public const int ic_close_light = 2130837657;
|
||||
|
||||
// aapt resource value: 0x7f02009a
|
||||
public const int ic_collapse_00001 = 2130837658;
|
||||
public const int ic_collapse = 2130837658;
|
||||
|
||||
// aapt resource value: 0x7f02009b
|
||||
public const int ic_collapse_00002 = 2130837659;
|
||||
public const int ic_collapse_00000 = 2130837659;
|
||||
|
||||
// aapt resource value: 0x7f02009c
|
||||
public const int ic_collapse_00003 = 2130837660;
|
||||
public const int ic_collapse_00001 = 2130837660;
|
||||
|
||||
// aapt resource value: 0x7f02009d
|
||||
public const int ic_collapse_00004 = 2130837661;
|
||||
public const int ic_collapse_00002 = 2130837661;
|
||||
|
||||
// aapt resource value: 0x7f02009e
|
||||
public const int ic_collapse_00005 = 2130837662;
|
||||
public const int ic_collapse_00003 = 2130837662;
|
||||
|
||||
// aapt resource value: 0x7f02009f
|
||||
public const int ic_collapse_00006 = 2130837663;
|
||||
public const int ic_collapse_00004 = 2130837663;
|
||||
|
||||
// aapt resource value: 0x7f0200a0
|
||||
public const int ic_collapse_00007 = 2130837664;
|
||||
public const int ic_collapse_00005 = 2130837664;
|
||||
|
||||
// aapt resource value: 0x7f0200a1
|
||||
public const int ic_collapse_00008 = 2130837665;
|
||||
public const int ic_collapse_00006 = 2130837665;
|
||||
|
||||
// aapt resource value: 0x7f0200a2
|
||||
public const int ic_collapse_00009 = 2130837666;
|
||||
public const int ic_collapse_00007 = 2130837666;
|
||||
|
||||
// aapt resource value: 0x7f0200a3
|
||||
public const int ic_collapse_00010 = 2130837667;
|
||||
public const int ic_collapse_00008 = 2130837667;
|
||||
|
||||
// aapt resource value: 0x7f0200a4
|
||||
public const int ic_collapse_00011 = 2130837668;
|
||||
public const int ic_collapse_00009 = 2130837668;
|
||||
|
||||
// aapt resource value: 0x7f0200a5
|
||||
public const int ic_collapse_00012 = 2130837669;
|
||||
public const int ic_collapse_00010 = 2130837669;
|
||||
|
||||
// aapt resource value: 0x7f0200a6
|
||||
public const int ic_collapse_00013 = 2130837670;
|
||||
public const int ic_collapse_00011 = 2130837670;
|
||||
|
||||
// aapt resource value: 0x7f0200a7
|
||||
public const int ic_collapse_00014 = 2130837671;
|
||||
public const int ic_collapse_00012 = 2130837671;
|
||||
|
||||
// aapt resource value: 0x7f0200a8
|
||||
public const int ic_collapse_00015 = 2130837672;
|
||||
public const int ic_collapse_00013 = 2130837672;
|
||||
|
||||
// aapt resource value: 0x7f0200a9
|
||||
public const int ic_errorstatus = 2130837673;
|
||||
public const int ic_collapse_00014 = 2130837673;
|
||||
|
||||
// aapt resource value: 0x7f0200aa
|
||||
public const int ic_expand = 2130837674;
|
||||
public const int ic_collapse_00015 = 2130837674;
|
||||
|
||||
// aapt resource value: 0x7f0200ab
|
||||
public const int ic_expand_00000 = 2130837675;
|
||||
public const int ic_errorstatus = 2130837675;
|
||||
|
||||
// aapt resource value: 0x7f0200ac
|
||||
public const int ic_expand_00001 = 2130837676;
|
||||
public const int ic_expand = 2130837676;
|
||||
|
||||
// aapt resource value: 0x7f0200ad
|
||||
public const int ic_expand_00002 = 2130837677;
|
||||
public const int ic_expand_00000 = 2130837677;
|
||||
|
||||
// aapt resource value: 0x7f0200ae
|
||||
public const int ic_expand_00003 = 2130837678;
|
||||
public const int ic_expand_00001 = 2130837678;
|
||||
|
||||
// aapt resource value: 0x7f0200af
|
||||
public const int ic_expand_00004 = 2130837679;
|
||||
public const int ic_expand_00002 = 2130837679;
|
||||
|
||||
// aapt resource value: 0x7f0200b0
|
||||
public const int ic_expand_00005 = 2130837680;
|
||||
public const int ic_expand_00003 = 2130837680;
|
||||
|
||||
// aapt resource value: 0x7f0200b1
|
||||
public const int ic_expand_00006 = 2130837681;
|
||||
public const int ic_expand_00004 = 2130837681;
|
||||
|
||||
// aapt resource value: 0x7f0200b2
|
||||
public const int ic_expand_00007 = 2130837682;
|
||||
public const int ic_expand_00005 = 2130837682;
|
||||
|
||||
// aapt resource value: 0x7f0200b3
|
||||
public const int ic_expand_00008 = 2130837683;
|
||||
public const int ic_expand_00006 = 2130837683;
|
||||
|
||||
// aapt resource value: 0x7f0200b4
|
||||
public const int ic_expand_00009 = 2130837684;
|
||||
public const int ic_expand_00007 = 2130837684;
|
||||
|
||||
// aapt resource value: 0x7f0200b5
|
||||
public const int ic_expand_00010 = 2130837685;
|
||||
public const int ic_expand_00008 = 2130837685;
|
||||
|
||||
// aapt resource value: 0x7f0200b6
|
||||
public const int ic_expand_00011 = 2130837686;
|
||||
public const int ic_expand_00009 = 2130837686;
|
||||
|
||||
// aapt resource value: 0x7f0200b7
|
||||
public const int ic_expand_00012 = 2130837687;
|
||||
public const int ic_expand_00010 = 2130837687;
|
||||
|
||||
// aapt resource value: 0x7f0200b8
|
||||
public const int ic_expand_00013 = 2130837688;
|
||||
public const int ic_expand_00011 = 2130837688;
|
||||
|
||||
// aapt resource value: 0x7f0200b9
|
||||
public const int ic_expand_00014 = 2130837689;
|
||||
public const int ic_expand_00012 = 2130837689;
|
||||
|
||||
// aapt resource value: 0x7f0200ba
|
||||
public const int ic_expand_00015 = 2130837690;
|
||||
public const int ic_expand_00013 = 2130837690;
|
||||
|
||||
// aapt resource value: 0x7f0200bb
|
||||
public const int ic_media_pause = 2130837691;
|
||||
public const int ic_expand_00014 = 2130837691;
|
||||
|
||||
// aapt resource value: 0x7f0200bc
|
||||
public const int ic_media_play = 2130837692;
|
||||
public const int ic_expand_00015 = 2130837692;
|
||||
|
||||
// aapt resource value: 0x7f0200bd
|
||||
public const int ic_media_route_disabled_mono_dark = 2130837693;
|
||||
public const int ic_media_pause = 2130837693;
|
||||
|
||||
// aapt resource value: 0x7f0200be
|
||||
public const int ic_media_route_off_mono_dark = 2130837694;
|
||||
public const int ic_media_play = 2130837694;
|
||||
|
||||
// aapt resource value: 0x7f0200bf
|
||||
public const int ic_media_route_on_0_mono_dark = 2130837695;
|
||||
public const int ic_media_route_disabled_mono_dark = 2130837695;
|
||||
|
||||
// aapt resource value: 0x7f0200c0
|
||||
public const int ic_media_route_on_1_mono_dark = 2130837696;
|
||||
public const int ic_media_route_off_mono_dark = 2130837696;
|
||||
|
||||
// aapt resource value: 0x7f0200c1
|
||||
public const int ic_media_route_on_2_mono_dark = 2130837697;
|
||||
public const int ic_media_route_on_0_mono_dark = 2130837697;
|
||||
|
||||
// aapt resource value: 0x7f0200c2
|
||||
public const int ic_media_route_on_mono_dark = 2130837698;
|
||||
public const int ic_media_route_on_1_mono_dark = 2130837698;
|
||||
|
||||
// aapt resource value: 0x7f0200c3
|
||||
public const int ic_pause_dark = 2130837699;
|
||||
public const int ic_media_route_on_2_mono_dark = 2130837699;
|
||||
|
||||
// aapt resource value: 0x7f0200c4
|
||||
public const int ic_pause_light = 2130837700;
|
||||
public const int ic_media_route_on_mono_dark = 2130837700;
|
||||
|
||||
// aapt resource value: 0x7f0200c5
|
||||
public const int ic_play_dark = 2130837701;
|
||||
public const int ic_pause_dark = 2130837701;
|
||||
|
||||
// aapt resource value: 0x7f0200c6
|
||||
public const int ic_play_light = 2130837702;
|
||||
public const int ic_pause_light = 2130837702;
|
||||
|
||||
// aapt resource value: 0x7f0200c7
|
||||
public const int ic_speaker_dark = 2130837703;
|
||||
public const int ic_play_dark = 2130837703;
|
||||
|
||||
// aapt resource value: 0x7f0200c8
|
||||
public const int ic_speaker_group_dark = 2130837704;
|
||||
public const int ic_play_light = 2130837704;
|
||||
|
||||
// aapt resource value: 0x7f0200c9
|
||||
public const int ic_speaker_group_light = 2130837705;
|
||||
public const int ic_speaker_dark = 2130837705;
|
||||
|
||||
// aapt resource value: 0x7f0200ca
|
||||
public const int ic_speaker_light = 2130837706;
|
||||
public const int ic_speaker_group_dark = 2130837706;
|
||||
|
||||
// aapt resource value: 0x7f0200cb
|
||||
public const int ic_successstatus = 2130837707;
|
||||
public const int ic_speaker_group_light = 2130837707;
|
||||
|
||||
// aapt resource value: 0x7f0200cc
|
||||
public const int ic_tv_dark = 2130837708;
|
||||
public const int ic_speaker_light = 2130837708;
|
||||
|
||||
// aapt resource value: 0x7f0200cd
|
||||
public const int ic_tv_light = 2130837709;
|
||||
public const int ic_successstatus = 2130837709;
|
||||
|
||||
// aapt resource value: 0x7f0200ce
|
||||
public const int icon = 2130837710;
|
||||
public const int ic_tv_dark = 2130837710;
|
||||
|
||||
// aapt resource value: 0x7f0200cf
|
||||
public const int ion_chevron_right = 2130837711;
|
||||
public const int ic_tv_light = 2130837711;
|
||||
|
||||
// aapt resource value: 0x7f0200d0
|
||||
public const int lightbulb = 2130837712;
|
||||
public const int icon = 2130837712;
|
||||
|
||||
// aapt resource value: 0x7f0200d1
|
||||
public const int list_selector = 2130837713;
|
||||
public const int ion_chevron_right = 2130837713;
|
||||
|
||||
// aapt resource value: 0x7f0200d2
|
||||
public const int @lock = 2130837714;
|
||||
public const int lightbulb = 2130837714;
|
||||
|
||||
// aapt resource value: 0x7f0200d3
|
||||
public const int logo = 2130837715;
|
||||
public const int list_selector = 2130837715;
|
||||
|
||||
// aapt resource value: 0x7f0200d4
|
||||
public const int more = 2130837716;
|
||||
public const int @lock = 2130837716;
|
||||
|
||||
// aapt resource value: 0x7f0200d5
|
||||
public const int mr_dialog_material_background_dark = 2130837717;
|
||||
public const int logo = 2130837717;
|
||||
|
||||
// aapt resource value: 0x7f0200d6
|
||||
public const int mr_dialog_material_background_light = 2130837718;
|
||||
public const int more = 2130837718;
|
||||
|
||||
// aapt resource value: 0x7f0200d7
|
||||
public const int mr_ic_audiotrack_light = 2130837719;
|
||||
public const int mr_dialog_material_background_dark = 2130837719;
|
||||
|
||||
// aapt resource value: 0x7f0200d8
|
||||
public const int mr_ic_cast_dark = 2130837720;
|
||||
public const int mr_dialog_material_background_light = 2130837720;
|
||||
|
||||
// aapt resource value: 0x7f0200d9
|
||||
public const int mr_ic_cast_light = 2130837721;
|
||||
public const int mr_ic_audiotrack_light = 2130837721;
|
||||
|
||||
// aapt resource value: 0x7f0200da
|
||||
public const int mr_ic_close_dark = 2130837722;
|
||||
public const int mr_ic_cast_dark = 2130837722;
|
||||
|
||||
// aapt resource value: 0x7f0200db
|
||||
public const int mr_ic_close_light = 2130837723;
|
||||
public const int mr_ic_cast_light = 2130837723;
|
||||
|
||||
// aapt resource value: 0x7f0200dc
|
||||
public const int mr_ic_media_route_connecting_mono_dark = 2130837724;
|
||||
public const int mr_ic_close_dark = 2130837724;
|
||||
|
||||
// aapt resource value: 0x7f0200dd
|
||||
public const int mr_ic_media_route_connecting_mono_light = 2130837725;
|
||||
public const int mr_ic_close_light = 2130837725;
|
||||
|
||||
// aapt resource value: 0x7f0200de
|
||||
public const int mr_ic_media_route_mono_dark = 2130837726;
|
||||
public const int mr_ic_media_route_connecting_mono_dark = 2130837726;
|
||||
|
||||
// aapt resource value: 0x7f0200df
|
||||
public const int mr_ic_media_route_mono_light = 2130837727;
|
||||
public const int mr_ic_media_route_connecting_mono_light = 2130837727;
|
||||
|
||||
// aapt resource value: 0x7f0200e0
|
||||
public const int mr_ic_pause_dark = 2130837728;
|
||||
public const int mr_ic_media_route_mono_dark = 2130837728;
|
||||
|
||||
// aapt resource value: 0x7f0200e1
|
||||
public const int mr_ic_pause_light = 2130837729;
|
||||
public const int mr_ic_media_route_mono_light = 2130837729;
|
||||
|
||||
// aapt resource value: 0x7f0200e2
|
||||
public const int mr_ic_play_dark = 2130837730;
|
||||
public const int mr_ic_pause_dark = 2130837730;
|
||||
|
||||
// aapt resource value: 0x7f0200e3
|
||||
public const int mr_ic_play_light = 2130837731;
|
||||
public const int mr_ic_pause_light = 2130837731;
|
||||
|
||||
// aapt resource value: 0x7f0200e4
|
||||
public const int notification_sm = 2130837732;
|
||||
|
||||
// aapt resource value: 0x7f0200f3
|
||||
public const int notification_template_icon_bg = 2130837747;
|
||||
public const int mr_ic_play_dark = 2130837732;
|
||||
|
||||
// aapt resource value: 0x7f0200e5
|
||||
public const int plus = 2130837733;
|
||||
public const int mr_ic_play_light = 2130837733;
|
||||
|
||||
// aapt resource value: 0x7f0200e6
|
||||
public const int refresh = 2130837734;
|
||||
public const int notification_sm = 2130837734;
|
||||
|
||||
// aapt resource value: 0x7f0200f8
|
||||
public const int notification_template_icon_bg = 2130837752;
|
||||
|
||||
// aapt resource value: 0x7f0200e7
|
||||
public const int roundedbg = 2130837735;
|
||||
public const int paperclip = 2130837735;
|
||||
|
||||
// aapt resource value: 0x7f0200e8
|
||||
public const int roundedbgdark = 2130837736;
|
||||
public const int plus = 2130837736;
|
||||
|
||||
// aapt resource value: 0x7f0200e9
|
||||
public const int search = 2130837737;
|
||||
public const int refresh = 2130837737;
|
||||
|
||||
// aapt resource value: 0x7f0200ea
|
||||
public const int share = 2130837738;
|
||||
public const int roundedbg = 2130837738;
|
||||
|
||||
// aapt resource value: 0x7f0200eb
|
||||
public const int share_tools = 2130837739;
|
||||
public const int roundedbgdark = 2130837739;
|
||||
|
||||
// aapt resource value: 0x7f0200ec
|
||||
public const int splash_screen = 2130837740;
|
||||
public const int search = 2130837740;
|
||||
|
||||
// aapt resource value: 0x7f0200ed
|
||||
public const int star = 2130837741;
|
||||
public const int share = 2130837741;
|
||||
|
||||
// aapt resource value: 0x7f0200ee
|
||||
public const int star_selected = 2130837742;
|
||||
public const int share_tools = 2130837742;
|
||||
|
||||
// aapt resource value: 0x7f0200ef
|
||||
public const int tools = 2130837743;
|
||||
public const int splash_screen = 2130837743;
|
||||
|
||||
// aapt resource value: 0x7f0200f0
|
||||
public const int tools_selected = 2130837744;
|
||||
public const int star = 2130837744;
|
||||
|
||||
// aapt resource value: 0x7f0200f1
|
||||
public const int upload = 2130837745;
|
||||
public const int star_selected = 2130837745;
|
||||
|
||||
// aapt resource value: 0x7f0200f2
|
||||
public const int user = 2130837746;
|
||||
public const int tools = 2130837746;
|
||||
|
||||
// aapt resource value: 0x7f0200f3
|
||||
public const int tools_selected = 2130837747;
|
||||
|
||||
// aapt resource value: 0x7f0200f4
|
||||
public const int trash = 2130837748;
|
||||
|
||||
// aapt resource value: 0x7f0200f5
|
||||
public const int upload = 2130837749;
|
||||
|
||||
// aapt resource value: 0x7f0200f6
|
||||
public const int user = 2130837750;
|
||||
|
||||
// aapt resource value: 0x7f0200f7
|
||||
public const int yubikey = 2130837751;
|
||||
|
||||
static Drawable()
|
||||
{
|
||||
@@ -2919,6 +2940,9 @@ namespace Bit.Android
|
||||
// aapt resource value: 0x7f0c0027
|
||||
public const int collapseActionView = 2131492903;
|
||||
|
||||
// aapt resource value: 0x7f0c00c6
|
||||
public const int contentFrame = 2131493062;
|
||||
|
||||
// aapt resource value: 0x7f0c0052
|
||||
public const int contentPanel = 2131492946;
|
||||
|
||||
@@ -3675,6 +3699,12 @@ namespace Bit.Android
|
||||
// aapt resource value: 0x7f030042
|
||||
public const int toolbar = 2130903106;
|
||||
|
||||
// aapt resource value: 0x7f030043
|
||||
public const int zxingscanneractivitylayout = 2130903107;
|
||||
|
||||
// aapt resource value: 0x7f030044
|
||||
public const int zxingscannerfragmentlayout = 2130903108;
|
||||
|
||||
static Layout()
|
||||
{
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
|
||||
@@ -5293,6 +5323,9 @@ namespace Bit.Android
|
||||
// aapt resource value: 0x7f060000
|
||||
public const int accessibilityservice = 2131099648;
|
||||
|
||||
// aapt resource value: 0x7f060001
|
||||
public const int filepaths = 2131099649;
|
||||
|
||||
static Xml()
|
||||
{
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
|
||||
|
||||
BIN
src/Android/Resources/drawable-hdpi/camera.png
Normal file
|
After Width: | Height: | Size: 664 B |
BIN
src/Android/Resources/drawable-hdpi/download.png
Normal file
|
After Width: | Height: | Size: 467 B |
BIN
src/Android/Resources/drawable-hdpi/paperclip.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
src/Android/Resources/drawable-hdpi/trash.png
Normal file
|
After Width: | Height: | Size: 452 B |
BIN
src/Android/Resources/drawable-hdpi/yubikey.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
src/Android/Resources/drawable-xhdpi/camera.png
Normal file
|
After Width: | Height: | Size: 802 B |
BIN
src/Android/Resources/drawable-xhdpi/download.png
Normal file
|
After Width: | Height: | Size: 556 B |
BIN
src/Android/Resources/drawable-xhdpi/paperclip.png
Normal file
|
After Width: | Height: | Size: 802 B |
BIN
src/Android/Resources/drawable-xhdpi/trash.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
src/Android/Resources/drawable-xhdpi/yubikey.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/camera.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/download.png
Normal file
|
After Width: | Height: | Size: 707 B |
BIN
src/Android/Resources/drawable-xxhdpi/paperclip.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/trash.png
Normal file
|
After Width: | Height: | Size: 534 B |
BIN
src/Android/Resources/drawable-xxhdpi/yubikey.png
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/camera.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/download.png
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
src/Android/Resources/drawable-xxxhdpi/paperclip.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/trash.png
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
src/Android/Resources/drawable/camera.png
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
src/Android/Resources/drawable/download.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
src/Android/Resources/drawable/paperclip.png
Normal file
|
After Width: | Height: | Size: 249 B |
BIN
src/Android/Resources/drawable/trash.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
src/Android/Resources/drawable/yubikey.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
4
src/Android/Resources/xml/filepaths.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
||||
370
src/Android/Services/AndroidKeyStoreStorageService.cs
Normal file
@@ -0,0 +1,370 @@
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Android.OS;
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Android.Security;
|
||||
using Javax.Security.Auth.X500;
|
||||
using Java.Math;
|
||||
using Android.Security.Keystore;
|
||||
using Android.App;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Java.Util;
|
||||
using Javax.Crypto.Spec;
|
||||
using Android.Preferences;
|
||||
|
||||
namespace Bit.Android.Services
|
||||
{
|
||||
public class AndroidKeyStoreStorageService : ISecureStorageService
|
||||
{
|
||||
private const string AndroidKeyStore = "AndroidKeyStore";
|
||||
private const string AesMode = "AES/GCM/NoPadding";
|
||||
|
||||
private const string KeyAlias = "bitwardenKey2";
|
||||
private const string KeyAliasV1 = "bitwardenKey";
|
||||
|
||||
private const string SettingsFormat = "ksSecured2:{0}";
|
||||
private const string SettingsFormatV1 = "ksSecured:{0}";
|
||||
|
||||
private const string AesKey = "ksSecured2:aesKeyForService";
|
||||
private const string AesKeyV1 = "ksSecured:aesKeyForService";
|
||||
|
||||
private readonly string _rsaMode;
|
||||
private readonly bool _oldAndroid;
|
||||
private readonly ISettings _settings;
|
||||
private readonly KeyStore _keyStore;
|
||||
private readonly ISecureStorageService _oldKeyStorageService;
|
||||
|
||||
public AndroidKeyStoreStorageService(ISettings settings)
|
||||
{
|
||||
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
|
||||
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
_oldKeyStorageService = new KeyStoreStorageService(new char[] { });
|
||||
_settings = settings;
|
||||
|
||||
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
|
||||
_keyStore.Load(null);
|
||||
|
||||
GenerateStoreKey();
|
||||
GenerateAesKey();
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _settings.Contains(string.Format(SettingsFormat, key)) ||
|
||||
_settings.Contains(string.Format(SettingsFormatV1, key)) ||
|
||||
_oldKeyStorageService.Contains(key);
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
CleanupOld(key);
|
||||
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(_settings.Contains(formattedKey))
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Retrieve(string key)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(!_settings.Contains(formattedKey))
|
||||
{
|
||||
return TryGetAndMigrate(key);
|
||||
}
|
||||
|
||||
var cs = _settings.GetValueOrDefault<string>(formattedKey);
|
||||
if(string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return App.Utilities.Crypto.AesCbcDecrypt(new App.Models.CipherString(cs), aesKey);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt from secure storage.");
|
||||
_settings.Remove(formattedKey);
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string key, byte[] dataBytes)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
CleanupOld(key);
|
||||
if(dataBytes == null)
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cipherString = App.Utilities.Crypto.AesCbcEncrypt(dataBytes, aesKey);
|
||||
_settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Failed to encrypt to secure storage.");
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStoreKey()
|
||||
{
|
||||
if(_keyStore.ContainsAlias(KeyAlias))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearSettings();
|
||||
|
||||
var end = Calendar.Instance;
|
||||
end.Add(CalendarField.Year, 99);
|
||||
|
||||
if(_oldAndroid)
|
||||
{
|
||||
var subject = new X500Principal($"CN={KeyAlias}");
|
||||
|
||||
var spec = new KeyPairGeneratorSpec.Builder(Application.Context)
|
||||
.SetAlias(KeyAlias)
|
||||
.SetSubject(subject)
|
||||
.SetSerialNumber(BigInteger.Ten)
|
||||
.SetStartDate(new Date(0))
|
||||
.SetEndDate(end.Time)
|
||||
.Build();
|
||||
|
||||
var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore);
|
||||
gen.Initialize(spec);
|
||||
gen.GenerateKeyPair();
|
||||
}
|
||||
else
|
||||
{
|
||||
var spec = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)
|
||||
.SetKeyValidityStart(new Date(0))
|
||||
.SetKeyValidityEnd(end.Time)
|
||||
.Build();
|
||||
|
||||
var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore);
|
||||
gen.Init(spec);
|
||||
gen.GenerateKey();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore.PrivateKeyEntry GetRsaKeyEntry(string alias)
|
||||
{
|
||||
return _keyStore.GetEntry(alias, null) as KeyStore.PrivateKeyEntry;
|
||||
}
|
||||
|
||||
private void GenerateAesKey()
|
||||
{
|
||||
if(_settings.Contains(AesKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = App.Utilities.Crypto.RandomBytes(512 / 8);
|
||||
var encKey = _oldAndroid ? RsaEncrypt(key) : AesEncrypt(key);
|
||||
_settings.AddOrUpdateValue(AesKey, encKey);
|
||||
}
|
||||
|
||||
private App.Models.SymmetricCryptoKey GetAesKey(bool v1 = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var aesKey = v1 ? AesKeyV1 : AesKey;
|
||||
if(!_settings.Contains(aesKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var encKey = _settings.GetValueOrDefault<string>(aesKey);
|
||||
if(string.IsNullOrWhiteSpace(encKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_oldAndroid || v1)
|
||||
{
|
||||
var encKeyBytes = Convert.FromBase64String(encKey);
|
||||
var key = RsaDecrypt(encKeyBytes, v1);
|
||||
return new App.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = encKey.Split('|');
|
||||
if(parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ivBytes = Convert.FromBase64String(parts[0]);
|
||||
var encKeyBytes = Convert.FromBase64String(parts[1]);
|
||||
var key = AesDecrypt(ivBytes, encKeyBytes);
|
||||
return new App.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Cannot get AesKey.");
|
||||
_keyStore.DeleteEntry(KeyAlias);
|
||||
_settings.Remove(AesKey);
|
||||
if(!v1)
|
||||
{
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string AesEncrypt(byte[] input)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry);
|
||||
var encBytes = cipher.DoFinal(input);
|
||||
var ivBytes = cipher.GetIV();
|
||||
return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}";
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] AesDecrypt(byte[] iv, byte[] encData)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
var spec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.DecryptMode, entry, spec);
|
||||
var decBytes = cipher.DoFinal(encData);
|
||||
return decBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private string RsaEncrypt(byte[] data)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey);
|
||||
var cipherText = cipher.DoFinal(data);
|
||||
return Convert.ToBase64String(cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaDecrypt(byte[] encData, bool v1)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(v1 ? KeyAliasV1 : KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
if(_oldAndroid)
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default);
|
||||
}
|
||||
|
||||
var plainText = cipher.DoFinal(encData);
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetAndMigrate(string key)
|
||||
{
|
||||
if(_oldKeyStorageService.Contains(key))
|
||||
{
|
||||
var value = _oldKeyStorageService.Retrieve(key);
|
||||
Store(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
var aesKeyV1 = GetAesKey(true);
|
||||
if(aesKeyV1 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = _settings.GetValueOrDefault<string>(formattedKeyV1);
|
||||
var value = App.Utilities.Crypto.AesCbcDecrypt(new App.Models.CipherString(cs), aesKeyV1);
|
||||
Store(key, value);
|
||||
return value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt v1 from secure storage.");
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOld(string key)
|
||||
{
|
||||
if(_oldKeyStorageService.Contains(key))
|
||||
{
|
||||
_oldKeyStorageService.Delete(key);
|
||||
}
|
||||
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSettings(string format = SettingsFormat)
|
||||
{
|
||||
var prefix = string.Format(format, string.Empty);
|
||||
|
||||
using(var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(Application.Context))
|
||||
using(var sharedPreferencesEditor = sharedPreferences.Edit())
|
||||
{
|
||||
var removed = false;
|
||||
foreach(var pref in sharedPreferences.All)
|
||||
{
|
||||
if(pref.Key.StartsWith(prefix))
|
||||
{
|
||||
removed = true;
|
||||
sharedPreferencesEditor.Remove(pref.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if(removed)
|
||||
{
|
||||
sharedPreferencesEditor.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Android.Content;
|
||||
using Bit.App.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.Android.Services
|
||||
{
|
||||
public class ClipboardService : IClipboardService
|
||||
{
|
||||
public void CopyToClipboard(string text)
|
||||
{
|
||||
var clipboardManager = (ClipboardManager)Forms.Context.GetSystemService(Context.ClipboardService);
|
||||
clipboardManager.Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
229
src/Android/Services/DeviceActionService.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Bit.App.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
using Android.Webkit;
|
||||
using Plugin.CurrentActivity;
|
||||
using System.IO;
|
||||
using Android.Support.V4.Content;
|
||||
using Bit.App;
|
||||
using Bit.App.Resources;
|
||||
using Android.Provider;
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
using System.Collections.Generic;
|
||||
using Android;
|
||||
using Android.Content.PM;
|
||||
using Android.Support.V4.App;
|
||||
|
||||
namespace Bit.Android.Services
|
||||
{
|
||||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IAppSettingsService _appSettingsService;
|
||||
private bool _cameraPermissionsDenied;
|
||||
|
||||
public DeviceActionService(
|
||||
IAppSettingsService appSettingsService)
|
||||
{
|
||||
_appSettingsService = appSettingsService;
|
||||
}
|
||||
|
||||
public void CopyToClipboard(string text)
|
||||
{
|
||||
var clipboardManager = (ClipboardManager)Forms.Context.GetSystemService(Context.ClipboardService);
|
||||
clipboardManager.Text = text;
|
||||
}
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
if(!CanOpenFile(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cachePath = CrossCurrentActivity.Current.Activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(CrossCurrentActivity.Current.Activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
CrossCurrentActivity.Current.Activity.StartActivity(intent);
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
intent.SetType(mimeType);
|
||||
var activities = pm.QueryIntentActivities(intent, global::Android.Content.PM.PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
||||
_appSettingsService.LastCacheClear = DateTime.UtcNow;
|
||||
}
|
||||
catch(Exception) { }
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if(dir != null && dir.IsDirectory)
|
||||
{
|
||||
var children = dir.List();
|
||||
for(int i = 0; i < children.Length; i++)
|
||||
{
|
||||
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
||||
if(!success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return dir.Delete();
|
||||
}
|
||||
else if(dir != null && dir.IsFile)
|
||||
{
|
||||
return dir.Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task SelectFileAsync()
|
||||
{
|
||||
MessagingCenter.Unsubscribe<Application>(Application.Current, "SelectFileCameraPermissionDenied");
|
||||
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
|
||||
|
||||
if(!_cameraPermissionsDenied && !hasStorageWritePermission)
|
||||
{
|
||||
AskCameraPermission(Manifest.Permission.WriteExternalStorage);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
if(!_cameraPermissionsDenied && !hasCameraPermission)
|
||||
{
|
||||
AskCameraPermission(Manifest.Permission.Camera);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
var additionalIntents = new List<IParcelable>();
|
||||
|
||||
var docIntent = new Intent(Intent.ActionOpenDocument);
|
||||
docIntent.AddCategory(Intent.CategoryOpenable);
|
||||
docIntent.SetType("*/*");
|
||||
|
||||
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
|
||||
|
||||
if(!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = new Java.IO.File(global::Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
if(!file.Exists())
|
||||
{
|
||||
file.ParentFile.Mkdirs();
|
||||
file.CreateNewFile();
|
||||
}
|
||||
var outputFileUri = global::Android.Net.Uri.FromFile(file);
|
||||
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
||||
}
|
||||
catch(Java.IO.IOException) { }
|
||||
}
|
||||
|
||||
if(additionalIntents.Count > 0)
|
||||
{
|
||||
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
|
||||
}
|
||||
|
||||
CrossCurrentActivity.Current.Activity.StartActivityForResult(chooserIntent, Constants.SelectFileRequestCode);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private List<IParcelable> GetCameraIntents(global::Android.Net.Uri outputUri)
|
||||
{
|
||||
var intents = new List<IParcelable>();
|
||||
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
||||
var captureIntent = new Intent(MediaStore.ActionImageCapture);
|
||||
var listCam = pm.QueryIntentActivities(captureIntent, 0);
|
||||
foreach(var res in listCam)
|
||||
{
|
||||
var packageName = res.ActivityInfo.PackageName;
|
||||
var intent = new Intent(captureIntent);
|
||||
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
|
||||
intent.SetPackage(packageName);
|
||||
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
||||
intents.Add(intent);
|
||||
}
|
||||
return intents;
|
||||
}
|
||||
|
||||
private bool HasPermission(string permission)
|
||||
{
|
||||
return ContextCompat.CheckSelfPermission(CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
|
||||
}
|
||||
|
||||
private void AskCameraPermission(string permission)
|
||||
{
|
||||
MessagingCenter.Subscribe<Application>(Application.Current, "SelectFileCameraPermissionDenied", (sender) =>
|
||||
{
|
||||
_cameraPermissionsDenied = true;
|
||||
});
|
||||
|
||||
AskPermission(permission);
|
||||
}
|
||||
|
||||
private void AskPermission(string permission)
|
||||
{
|
||||
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
|
||||
Constants.SelectFilePermissionRequestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Android.App;
|
||||
using Android.OS;
|
||||
using Android.Util;
|
||||
using Bit.App.Abstractions;
|
||||
|
||||
namespace Bit.Android.Services
|
||||
@@ -42,5 +41,6 @@ namespace Bit.Android.Services
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
public bool NfcEnabled => Utilities.NfcEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
using System.IO;
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Android.OS;
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Android.Security;
|
||||
using Javax.Security.Auth.X500;
|
||||
using Java.Math;
|
||||
using Android.Security.Keystore;
|
||||
using Android.App;
|
||||
using Plugin.Settings.Abstractions;
|
||||
using Java.Util;
|
||||
using Javax.Crypto.Spec;
|
||||
|
||||
namespace Bit.Android.Services
|
||||
{
|
||||
public class KeyStoreBackedStorageService : ISecureStorageService
|
||||
{
|
||||
private const string AndroidKeyStore = "AndroidKeyStore";
|
||||
private const string KeyAlias = "bitwardenKey";
|
||||
private const string SettingsFormat = "ksSecured:{0}";
|
||||
private const string AesKey = "ksSecured:aesKeyForService";
|
||||
|
||||
private readonly string _rsaMode;
|
||||
private readonly bool _oldAndroid;
|
||||
private readonly ISettings _settings;
|
||||
private readonly KeyStore _keyStore;
|
||||
private readonly ISecureStorageService _oldKeyStorageService;
|
||||
|
||||
public KeyStoreBackedStorageService(ISettings settings)
|
||||
{
|
||||
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
|
||||
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
_oldKeyStorageService = new KeyStoreStorageService(new char[] { });
|
||||
_settings = settings;
|
||||
|
||||
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
|
||||
_keyStore.Load(null);
|
||||
|
||||
GenerateRsaKey();
|
||||
GenerateAesKey();
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _settings.Contains(string.Format(SettingsFormat, key)) || _oldKeyStorageService.Contains(key);
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
CleanupOldKeyStore(key);
|
||||
_settings.Remove(string.Format(SettingsFormat, key));
|
||||
}
|
||||
|
||||
public byte[] Retrieve(string key)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(!_settings.Contains(formattedKey))
|
||||
{
|
||||
return TryGetAndMigrateFromOldKeyStore(key);
|
||||
}
|
||||
|
||||
var cs = _settings.GetValueOrDefault<string>(formattedKey);
|
||||
if(string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return App.Utilities.Crypto.AesCbcDecrypt(new App.Models.CipherString(cs), aesKey);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt from secure storage.");
|
||||
_settings.Remove(formattedKey);
|
||||
Utilities.SendCrashEmail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string key, byte[] dataBytes)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
CleanupOldKeyStore(key);
|
||||
if(dataBytes == null)
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cipherString = App.Utilities.Crypto.AesCbcEncrypt(dataBytes, aesKey);
|
||||
_settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Failed to encrypt to secure storage.");
|
||||
Utilities.SendCrashEmail(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateRsaKey()
|
||||
{
|
||||
if(_keyStore.ContainsAlias(KeyAlias))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore);
|
||||
var start = Calendar.Instance;
|
||||
var end = Calendar.Instance;
|
||||
end.Add(CalendarField.Year, 30);
|
||||
var subject = new X500Principal($"CN={KeyAlias}");
|
||||
|
||||
if(_oldAndroid)
|
||||
{
|
||||
var spec = new KeyPairGeneratorSpec.Builder(Application.Context)
|
||||
.SetAlias(KeyAlias)
|
||||
.SetSubject(subject)
|
||||
.SetSerialNumber(BigInteger.Ten)
|
||||
.SetStartDate(start.Time)
|
||||
.SetEndDate(end.Time)
|
||||
.Build();
|
||||
|
||||
gen.Initialize(spec);
|
||||
}
|
||||
else
|
||||
{
|
||||
var spec = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt)
|
||||
.SetCertificateSubject(subject)
|
||||
.SetDigests(KeyProperties.DigestSha1)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingRsaOaep)
|
||||
.Build();
|
||||
|
||||
gen.Initialize(spec);
|
||||
}
|
||||
|
||||
gen.GenerateKeyPair();
|
||||
}
|
||||
|
||||
private KeyStore.PrivateKeyEntry GetRsaKeyEntry()
|
||||
{
|
||||
return _keyStore.GetEntry(KeyAlias, null) as KeyStore.PrivateKeyEntry;
|
||||
}
|
||||
|
||||
private void GenerateAesKey()
|
||||
{
|
||||
if(_settings.Contains(AesKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = App.Utilities.Crypto.RandomBytes(512 / 8);
|
||||
var encKey = RsaEncrypt(key);
|
||||
_settings.AddOrUpdateValue(AesKey, Convert.ToBase64String(encKey));
|
||||
}
|
||||
|
||||
private App.Models.SymmetricCryptoKey GetAesKey()
|
||||
{
|
||||
try
|
||||
{
|
||||
var encKey = _settings.GetValueOrDefault<string>(AesKey);
|
||||
if(encKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var encKeyBytes = Convert.FromBase64String(encKey);
|
||||
var key = RsaDecrypt(encKeyBytes);
|
||||
return new App.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Cannot get AesKey.");
|
||||
_keyStore.DeleteEntry(KeyAlias);
|
||||
_settings.Remove(AesKey);
|
||||
Utilities.SendCrashEmail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaEncrypt(byte[] data)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry())
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey);
|
||||
var cipherText = cipher.DoFinal(data);
|
||||
return cipherText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaDecrypt(byte[] encData)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry())
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
if(_oldAndroid)
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default);
|
||||
}
|
||||
|
||||
var plainText = cipher.DoFinal(encData);
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetAndMigrateFromOldKeyStore(string key)
|
||||
{
|
||||
if(_oldKeyStorageService.Contains(key))
|
||||
{
|
||||
var value = _oldKeyStorageService.Retrieve(key);
|
||||
Store(key, value);
|
||||
_oldKeyStorageService.Delete(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOldKeyStore(string key)
|
||||
{
|
||||
if(_oldKeyStorageService.Contains(key))
|
||||
{
|
||||
_oldKeyStorageService.Delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,72 @@
|
||||
using System;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Java.Security;
|
||||
using System.IO;
|
||||
using Android.Nfc;
|
||||
using Android.Provider;
|
||||
|
||||
namespace Bit.Android
|
||||
{
|
||||
public static class Utilities
|
||||
{
|
||||
public static bool NfcEnabled()
|
||||
{
|
||||
var manager = (NfcManager)Application.Context.GetSystemService("nfc");
|
||||
var adapter = manager.DefaultAdapter;
|
||||
return adapter != null && adapter.IsEnabled;
|
||||
}
|
||||
|
||||
public static void SendCrashEmail(Exception e, bool includeSecurityProviders = true)
|
||||
{
|
||||
SendCrashEmail(e.Message + "\n\n" + e.StackTrace, includeSecurityProviders);
|
||||
}
|
||||
|
||||
public static void SendCrashEmail(Activity act, Exception e, bool includeSecurityProviders = true)
|
||||
{
|
||||
SendCrashEmail(act, e.Message + "\n\n" + e.StackTrace, includeSecurityProviders);
|
||||
}
|
||||
|
||||
public static void SaveCrashFile(Exception e, bool includeSecurityProviders = true)
|
||||
{
|
||||
SaveCrashFile(e.Message + "\n\n" + e.StackTrace, includeSecurityProviders);
|
||||
}
|
||||
|
||||
public static void SendCrashEmail(string text, bool includeSecurityProviders = true)
|
||||
{
|
||||
var emailIntent = new Intent(Intent.ActionSend);
|
||||
|
||||
emailIntent.SetType("plain/text");
|
||||
emailIntent.PutExtra(Intent.ExtraEmail, new String[] { "hello@bitwarden.com" });
|
||||
emailIntent.PutExtra(Intent.ExtraSubject, "bitwarden Crash Report");
|
||||
emailIntent.PutExtra(Intent.ExtraText, FormatText(text, includeSecurityProviders));
|
||||
|
||||
Application.Context.StartActivity(Intent.CreateChooser(emailIntent, "Send mail..."));
|
||||
}
|
||||
|
||||
public static void SendCrashEmail(Activity act, string text, bool includeSecurityProviders = true)
|
||||
{
|
||||
var emailIntent = new Intent(Intent.ActionSend);
|
||||
|
||||
emailIntent.SetType("plain/text");
|
||||
emailIntent.PutExtra(Intent.ExtraEmail, new String[] { "hello@bitwarden.com" });
|
||||
emailIntent.PutExtra(Intent.ExtraSubject, "bitwarden Crash Report");
|
||||
emailIntent.PutExtra(Intent.ExtraText, FormatText(text, includeSecurityProviders));
|
||||
|
||||
act.StartActivity(Intent.CreateChooser(emailIntent, "Send mail..."));
|
||||
}
|
||||
|
||||
public static void SaveCrashFile(string text, bool includeSecurityProviders = true)
|
||||
{
|
||||
var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
|
||||
var filename = Path.Combine(path, $"crash-{Java.Lang.JavaSystem.CurrentTimeMillis()}.txt");
|
||||
using(var streamWriter = new StreamWriter(filename, true))
|
||||
{
|
||||
streamWriter.WriteLine(FormatText(text, includeSecurityProviders));
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatText(string text, bool includeSecurityProviders = true)
|
||||
{
|
||||
var crashMessage = "bitwarden has crashed. Please send this email to our support team so that we can help " +
|
||||
"resolve the problem for you. Thank you.";
|
||||
@@ -36,15 +89,41 @@ namespace Bit.Android
|
||||
}
|
||||
|
||||
text += "\n\n ==================================================== \n\n" + crashMessage;
|
||||
return text;
|
||||
}
|
||||
|
||||
var emailIntent = new Intent(Intent.ActionSend);
|
||||
public static string AppendExceptionToMessage(string message, Exception ex)
|
||||
{
|
||||
message += ("\n\n" + ex.Message + "\n\n" + ex.StackTrace);
|
||||
if(ex.InnerException != null)
|
||||
{
|
||||
return AppendExceptionToMessage(message, ex.InnerException);
|
||||
}
|
||||
|
||||
emailIntent.SetType("plain/text");
|
||||
emailIntent.PutExtra(Intent.ExtraEmail, new String[] { "hello@bitwarden.com" });
|
||||
emailIntent.PutExtra(Intent.ExtraSubject, "bitwarden Crash Report");
|
||||
emailIntent.PutExtra(Intent.ExtraText, text);
|
||||
return message;
|
||||
}
|
||||
|
||||
Application.Context.StartActivity(Intent.CreateChooser(emailIntent, "Send mail..."));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,4 +92,6 @@
|
||||
<package id="Xamarin.GooglePlayServices.Measurement" version="29.0.0.2" targetFramework="monoandroid60" />
|
||||
<package id="XLabs.IoC" version="2.0.5782" targetFramework="monoandroid60" />
|
||||
<package id="XLabs.IoC.SimpleInjector" version="2.0.5782" targetFramework="monoandroid71" />
|
||||
<package id="ZXing.Net.Mobile" version="2.1.47" targetFramework="monoandroid71" />
|
||||
<package id="ZXing.Net.Mobile.Forms" version="2.1.47" targetFramework="monoandroid71" />
|
||||
</packages>
|
||||
13
src/App/Abstractions/Repositories/IAttachmentRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IAttachmentRepository : IRepository<AttachmentData, string>
|
||||
{
|
||||
Task<IEnumerable<AttachmentData>> GetAllByLoginIdAsync(string loginId);
|
||||
Task<IEnumerable<AttachmentData>> GetAllByUserIdAsync(string userId);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
10
src/App/Abstractions/Repositories/ITwoFactorApiRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface ITwoFactorApiRepository
|
||||
{
|
||||
Task<ApiResult> PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
bool Locked { get; set; }
|
||||
DateTime LastActivity { get; set; }
|
||||
DateTime LastCacheClear { get; set; }
|
||||
bool AutofillPersistNotification { get; set; }
|
||||
bool AutofillPasswordField { get; set; }
|
||||
string SecurityStamp { get; set; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Enums;
|
||||
using Bit.App.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
@@ -15,6 +16,7 @@ namespace Bit.App.Abstractions
|
||||
bool BelongsToOrganization(string orgId);
|
||||
void LogOut();
|
||||
Task<FullLoginResult> TokenPostAsync(string email, string masterPassword);
|
||||
Task<LoginResult> TokenPostTwoFactorAsync(string token, string email, string masterPasswordHash, SymmetricCryptoKey key);
|
||||
Task<LoginResult> TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, string email,
|
||||
string masterPasswordHash, SymmetricCryptoKey key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IClipboardService
|
||||
{
|
||||
void CopyToClipboard(string text);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,10 @@ namespace Bit.App.Abstractions
|
||||
void ClearKeys();
|
||||
string Decrypt(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
byte[] DecryptToBytes(CipherString encyptedValue, SymmetricCryptoKey key = null);
|
||||
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);
|
||||
|
||||
14
src/App/Abstractions/Services/IDeviceActionService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDeviceActionService
|
||||
{
|
||||
void CopyToClipboard(string text);
|
||||
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||
bool CanOpenFile(string fileName);
|
||||
Task SelectFileAsync();
|
||||
void ClearCache();
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,6 @@
|
||||
string Model { get; }
|
||||
int Version { get; }
|
||||
float Scale { get; }
|
||||
bool NfcEnabled { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,8 @@ namespace Bit.App.Abstractions
|
||||
Task<Tuple<IEnumerable<Login>, IEnumerable<Login>>> GetAllAsync(string uriString);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Bit.App.Abstractions
|
||||
string RefreshToken { get; set; }
|
||||
[Obsolete("Old auth scheme")]
|
||||
string AuthBearer { get; set; }
|
||||
string GetTwoFactorToken(string email);
|
||||
void SetTwoFactorToken(string email, string token);
|
||||
DateTime TokenExpiration { get; }
|
||||
string TokenIssuer { get; }
|
||||
bool TokenExpired { get; }
|
||||
@@ -16,5 +18,6 @@ namespace Bit.App.Abstractions
|
||||
string TokenUserId { get; }
|
||||
string TokenEmail { get; }
|
||||
string TokenName { get; }
|
||||
bool TokenPremium { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Bit.App
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IAppInfoService _appInfoService;
|
||||
private readonly IAppSettingsService _appSettingsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
public App(
|
||||
string uri,
|
||||
@@ -45,7 +46,8 @@ namespace Bit.App
|
||||
IGoogleAnalyticsService googleAnalyticsService,
|
||||
ILocalizeService localizeService,
|
||||
IAppInfoService appInfoService,
|
||||
IAppSettingsService appSettingsService)
|
||||
IAppSettingsService appSettingsService,
|
||||
IDeviceActionService deviceActionService)
|
||||
{
|
||||
_uri = uri;
|
||||
_databaseService = databaseService;
|
||||
@@ -59,6 +61,7 @@ namespace Bit.App
|
||||
_localizeService = localizeService;
|
||||
_appInfoService = appInfoService;
|
||||
_appSettingsService = appSettingsService;
|
||||
_deviceActionService = deviceActionService;
|
||||
|
||||
SetCulture();
|
||||
SetStyles();
|
||||
@@ -101,7 +104,7 @@ namespace Bit.App
|
||||
if(string.IsNullOrWhiteSpace(_uri))
|
||||
{
|
||||
var lastBuild = _settings.GetValueOrDefault<string>(LastBuildKey);
|
||||
if(InDebugMode() || lastBuild == null || lastBuild != _appInfoService.Build)
|
||||
if(Utilities.Helpers.InDebugMode() || lastBuild == null || lastBuild != _appInfoService.Build)
|
||||
{
|
||||
_settings.AddOrUpdateValue(LastBuildKey, _appInfoService.Build);
|
||||
_databaseService.CreateTables();
|
||||
@@ -110,6 +113,11 @@ namespace Bit.App
|
||||
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if((DateTime.UtcNow - _appSettingsService.LastCacheClear).TotalDays >= 1)
|
||||
{
|
||||
await Task.Run(() => _deviceActionService.ClearCache()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Debug.WriteLine("OnStart");
|
||||
}
|
||||
|
||||
@@ -152,6 +160,13 @@ namespace Bit.App
|
||||
{
|
||||
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if((now - _appSettingsService.LastCacheClear).TotalDays >= 1
|
||||
&& (now - _appSettingsService.LastActivity).TotalHours >= 1)
|
||||
{
|
||||
await Task.Run(() => _deviceActionService.ClearCache()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetMainPageFromAutofill()
|
||||
@@ -169,15 +184,6 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
|
||||
private bool InDebugMode()
|
||||
{
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task FullSyncAsync()
|
||||
{
|
||||
if(_connectivity.IsConnected)
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Abstractions\Repositories\IAttachmentRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\IAccountsApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" />
|
||||
@@ -49,7 +51,7 @@
|
||||
<Compile Include="Abstractions\Services\IAppInfoService.cs" />
|
||||
<Compile Include="Abstractions\Services\IAppIdService.cs" />
|
||||
<Compile Include="Abstractions\Services\IAuthService.cs" />
|
||||
<Compile Include="Abstractions\Services\IClipboardService.cs" />
|
||||
<Compile Include="Abstractions\Services\IDeviceActionService.cs" />
|
||||
<Compile Include="Abstractions\Services\IKeyDerivationService.cs" />
|
||||
<Compile Include="Abstractions\Services\ILogService.cs" />
|
||||
<Compile Include="Abstractions\Services\IReflectionService.cs" />
|
||||
@@ -59,12 +61,14 @@
|
||||
<Compile Include="Abstractions\Services\ISecureStorageService.cs" />
|
||||
<Compile Include="Abstractions\Services\ISqlService.cs" />
|
||||
<Compile Include="Constants.cs" />
|
||||
<Compile Include="Controls\HybridWebView.cs" />
|
||||
<Compile Include="Controls\ExtendedToolbarItem.cs" />
|
||||
<Compile Include="Controls\DismissModalToolBarItem.cs" />
|
||||
<Compile Include="Controls\ExtendedEditor.cs" />
|
||||
<Compile Include="Controls\ExtendedButton.cs" />
|
||||
<Compile Include="Controls\ExtendedNavigationPage.cs" />
|
||||
<Compile Include="Controls\ExtendedContentPage.cs" />
|
||||
<Compile Include="Controls\LabeledRightDetailCell.cs" />
|
||||
<Compile Include="Controls\MemoryContentView.cs" />
|
||||
<Compile Include="Controls\StepperCell.cs" />
|
||||
<Compile Include="Controls\ExtendedTableView.cs" />
|
||||
@@ -80,7 +84,9 @@
|
||||
<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" />
|
||||
<Compile Include="Enums\OrganizationUserType.cs" />
|
||||
<Compile Include="Enums\LockType.cs" />
|
||||
@@ -95,11 +101,12 @@
|
||||
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\FolderRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\DeviceRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\TwoFactorEmailRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\RegisterRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\LoginRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\PasswordHintRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\TokenRequest.cs" />
|
||||
<Compile Include="Models\Api\Response\CipherHistoryResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\AttachmentResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\CipherResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\DomainsResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ErrorResponse.cs" />
|
||||
@@ -112,8 +119,10 @@
|
||||
<Compile Include="Models\Api\Response\TokenResponse.cs" />
|
||||
<Compile Include="Models\Api\Response\ProfileResponse.cs" />
|
||||
<Compile Include="Models\Api\LoginDataModel.cs" />
|
||||
<Compile Include="Models\Cipher.cs" />
|
||||
<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" />
|
||||
@@ -136,6 +145,7 @@
|
||||
<Compile Include="Pages\LoginTwoFactorPage.cs" />
|
||||
<Compile Include="Pages\PasswordHintPage.cs" />
|
||||
<Compile Include="Pages\RegisterPage.cs" />
|
||||
<Compile Include="Pages\ScanPage.cs" />
|
||||
<Compile Include="Pages\Settings\SettingsCreditsPage.cs" />
|
||||
<Compile Include="Pages\Settings\SettingsHelpPage.cs" />
|
||||
<Compile Include="Pages\Settings\SettingsFeaturesPage.cs" />
|
||||
@@ -154,8 +164,11 @@
|
||||
<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" />
|
||||
<Compile Include="Repositories\TwoFactorApiRepository.cs" />
|
||||
<Compile Include="Repositories\SettingsApiRepository.cs" />
|
||||
<Compile Include="Repositories\ApiRepository.cs" />
|
||||
<Compile Include="Repositories\AccountsApiRepository.cs" />
|
||||
@@ -205,6 +218,11 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>AppResources.hi.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Resources\AppResources.hr.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>AppResources.hr.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Resources\AppResources.id.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
@@ -230,6 +248,11 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>AppResources.pt-PT.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Resources\AppResources.ro.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>AppResources.ro.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Resources\AppResources.ru.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
@@ -285,6 +308,7 @@
|
||||
<Compile Include="Pages\Vault\VaultEditLoginPage.cs" />
|
||||
<Compile Include="Pages\Vault\VaultListLoginsPage.cs" />
|
||||
<Compile Include="Services\PasswordGenerationService.cs" />
|
||||
<Compile Include="Utilities\Base32.cs" />
|
||||
<Compile Include="Utilities\Crypto.cs" />
|
||||
<Compile Include="Utilities\Helpers.cs" />
|
||||
<Compile Include="Utilities\IdentityHttpClient.cs" />
|
||||
@@ -318,6 +342,10 @@
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.hi.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\AppResources.hr.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.hr.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\AppResources.id.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.id.Designer.cs</LastGenOutput>
|
||||
@@ -338,6 +366,10 @@
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.pt-PT.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\AppResources.ro.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.ro.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\AppResources.ru.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>AppResources.ru.Designer.cs</LastGenOutput>
|
||||
@@ -485,6 +517,18 @@
|
||||
<HintPath>..\..\packages\XLabs.IoC.2.0.5782\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1+Xamarin.iOS10\XLabs.Ioc.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="ZXing.Net.Mobile.Core, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXing.Net.Mobile.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXing.Net.Mobile.Forms, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXing.Net.Mobile.Forms.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="zxing.portable, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\zxing.portable.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ZXingNetMobile, Version=2.1.47.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXingNetMobile.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\AppResources.resx">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
public const string SettingPinUnlockOn = "setting:pinUnlockOn";
|
||||
public const string SettingLockSeconds = "setting:lockSeconds";
|
||||
public const string SettingGaOptOut = "setting:googleAnalyticsOptOut";
|
||||
public const string SettingDisableTotpCopy = "setting:disableAutoCopyTotp";
|
||||
public const string AutofillPersistNotification = "setting:persistNotification";
|
||||
public const string AutofillPasswordField = "setting:autofillPasswordField";
|
||||
|
||||
@@ -28,8 +29,12 @@
|
||||
|
||||
public const string SecurityStamp = "other:securityStamp";
|
||||
public const string LastActivityDate = "other:lastActivityDate";
|
||||
public const string LastCacheClearDate = "other:cacheClearDate";
|
||||
public const string Locked = "other:locked";
|
||||
public const string LastLoginEmail = "other:lastLoginEmail";
|
||||
public const string LastSync = "other:lastSync";
|
||||
|
||||
public const int SelectFileRequestCode = 42;
|
||||
public const int SelectFilePermissionRequestCode = 43;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/App/Controls/HybridWebView.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class HybridWebView : View
|
||||
{
|
||||
private Action<string> _func;
|
||||
|
||||
public static readonly BindableProperty UriProperty = BindableProperty.Create(propertyName: nameof(Uri),
|
||||
returnType: typeof(string), declaringType: typeof(HybridWebView), defaultValue: default(string));
|
||||
|
||||
public string Uri
|
||||
{
|
||||
get { return (string)GetValue(UriProperty); }
|
||||
set { SetValue(UriProperty, value); }
|
||||
}
|
||||
|
||||
public void RegisterAction(Action<string> callback)
|
||||
{
|
||||
_func = callback;
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_func = null;
|
||||
}
|
||||
|
||||
public void InvokeAction(string data)
|
||||
{
|
||||
_func?.Invoke(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ namespace Bit.App.Controls
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
|
||||
LabelIcon2 = new CachedImage
|
||||
{
|
||||
WidthRequest = 16,
|
||||
HeightRequest = 16,
|
||||
HorizontalOptions = LayoutOptions.Start,
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
|
||||
Button = new ExtendedButton
|
||||
{
|
||||
WidthRequest = 60
|
||||
@@ -42,13 +50,15 @@ namespace Bit.App.Controls
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(60, GridUnitType.Absolute) });
|
||||
grid.Children.Add(Label, 0, 0);
|
||||
grid.Children.Add(Detail, 0, 1);
|
||||
grid.Children.Add(LabelIcon, 1, 0);
|
||||
grid.Children.Add(Button, 2, 0);
|
||||
Grid.SetColumnSpan(Detail, 2);
|
||||
grid.Children.Add(LabelIcon2, 2, 0);
|
||||
grid.Children.Add(Button, 3, 0);
|
||||
Grid.SetColumnSpan(Detail, 3);
|
||||
Grid.SetRowSpan(Button, 2);
|
||||
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
@@ -62,6 +72,7 @@ namespace Bit.App.Controls
|
||||
public Label Label { get; private set; }
|
||||
public Label Detail { get; private set; }
|
||||
public CachedImage LabelIcon { get; private set; }
|
||||
public CachedImage LabelIcon2 { get; private set; }
|
||||
public Button Button { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
59
src/App/Controls/LabeledRightDetailCell.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using FFImageLoading.Forms;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class LabeledRightDetailCell : ExtendedViewCell
|
||||
{
|
||||
public LabeledRightDetailCell(bool showIcon = true)
|
||||
{
|
||||
Label = new Label
|
||||
{
|
||||
LineBreakMode = LineBreakMode.TailTruncation,
|
||||
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
|
||||
HorizontalOptions = LayoutOptions.StartAndExpand,
|
||||
};
|
||||
|
||||
Detail = new Label
|
||||
{
|
||||
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
|
||||
Style = (Style)Application.Current.Resources["text-muted"],
|
||||
HorizontalOptions = LayoutOptions.End,
|
||||
VerticalOptions = LayoutOptions.Center
|
||||
};
|
||||
|
||||
StackLayout = new StackLayout
|
||||
{
|
||||
Orientation = StackOrientation.Horizontal,
|
||||
Padding = new Thickness(15, 10),
|
||||
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;
|
||||
}
|
||||
|
||||
public Label Label { get; private set; }
|
||||
public Label Detail { get; private set; }
|
||||
public CachedImage Icon { get; private set; }
|
||||
public StackLayout StackLayout { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ namespace Bit.App.Controls
|
||||
string labelText = null,
|
||||
string valueText = null,
|
||||
string button1Text = null,
|
||||
string button2Text = null)
|
||||
string button2Text = null,
|
||||
string subText = null)
|
||||
{
|
||||
var containerStackLayout = new StackLayout
|
||||
{
|
||||
@@ -56,6 +57,18 @@ namespace Bit.App.Controls
|
||||
VerticalOptions = LayoutOptions.CenterAndExpand
|
||||
};
|
||||
|
||||
if(subText != null)
|
||||
{
|
||||
Sub = new Label
|
||||
{
|
||||
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
|
||||
HorizontalOptions = LayoutOptions.End,
|
||||
VerticalOptions = LayoutOptions.Center
|
||||
};
|
||||
|
||||
buttonStackLayout.Children.Add(Sub);
|
||||
}
|
||||
|
||||
if(button1Text != null)
|
||||
{
|
||||
Button1 = new ExtendedButton
|
||||
@@ -100,12 +113,18 @@ namespace Bit.App.Controls
|
||||
containerStackLayout.AdjustPaddingForDevice();
|
||||
}
|
||||
|
||||
if(Sub != null && Button1 != null)
|
||||
{
|
||||
Sub.Margin = new Thickness(0, 0, 10, 0);
|
||||
}
|
||||
|
||||
containerStackLayout.Children.Add(buttonStackLayout);
|
||||
View = containerStackLayout;
|
||||
}
|
||||
|
||||
public Label Label { get; private set; }
|
||||
public Label Value { get; private set; }
|
||||
public Label Sub { get; private set; }
|
||||
public ExtendedButton Button1 { get; private set; }
|
||||
public ExtendedButton Button2 { get; private set; }
|
||||
}
|
||||
|
||||
22
src/App/Controls/VaultAttachmentsViewCell.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Bit.App.Models.Page;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class VaultAttachmentsViewCell : LabeledRightDetailCell
|
||||
{
|
||||
public VaultAttachmentsViewCell()
|
||||
{
|
||||
Label.SetBinding(Label.TextProperty, nameof(VaultAttachmentsPageModel.Attachment.Name));
|
||||
Detail.SetBinding(Label.TextProperty, nameof(VaultAttachmentsPageModel.Attachment.SizeName));
|
||||
Icon.Source = "trash";
|
||||
Detail.MinimumWidthRequest = 100;
|
||||
BackgroundColor = Color.White;
|
||||
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
StackLayout.BackgroundColor = Color.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,14 @@ namespace Bit.App.Controls
|
||||
Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Login.Name));
|
||||
Detail.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Login.Username));
|
||||
LabelIcon.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Login.Shared));
|
||||
LabelIcon2.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Login.HasAttachments));
|
||||
|
||||
Button.Image = "more";
|
||||
Button.Command = new Command(() => moreClickedAction?.Invoke(LoginParameter));
|
||||
Button.BackgroundColor = Color.Transparent;
|
||||
|
||||
LabelIcon.Source = "share";
|
||||
LabelIcon2.Source = "paperclip";
|
||||
|
||||
BackgroundColor = Color.White;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
AesCbc128_HmacSha256_B64 = 1,
|
||||
AesCbc256_HmacSha256_B64 = 2,
|
||||
Rsa2048_OaepSha256_B64 = 3,
|
||||
Rsa2048_OaepSha1_B64 = 4
|
||||
Rsa2048_OaepSha1_B64 = 4,
|
||||
Rsa2048_OaepSha256_HmacSha256_B64 = 5,
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6
|
||||
}
|
||||
}
|
||||
|
||||
12
src/App/Enums/TwoFactorProviderType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.App.Enums
|
||||
{
|
||||
public enum TwoFactorProviderType : byte
|
||||
{
|
||||
Authenticator = 0,
|
||||
Email = 1,
|
||||
Duo = 2,
|
||||
YubiKey = 3,
|
||||
U2f = 4,
|
||||
Remember = 5
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Totp { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
Username = login.Username?.EncryptedString;
|
||||
Password = login.Password?.EncryptedString;
|
||||
Notes = login.Notes?.EncryptedString;
|
||||
Totp = login.Totp?.EncryptedString;
|
||||
Favorite = login.Favorite;
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Totp { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Bit.App.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
@@ -8,10 +9,11 @@ namespace Bit.App.Models.Api
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string Token { get; set; }
|
||||
public int? Provider { get; set; }
|
||||
public TwoFactorProviderType? Provider { get; set; }
|
||||
[Obsolete]
|
||||
public string OldAuthBearer { get; set; }
|
||||
public DeviceRequest Device { get; set; }
|
||||
public bool Remember { get; set; }
|
||||
|
||||
public IDictionary<string, string> ToIdentityTokenRequest()
|
||||
{
|
||||
@@ -40,7 +42,8 @@ namespace Bit.App.Models.Api
|
||||
if(Token != null && Provider.HasValue)
|
||||
{
|
||||
dict.Add("TwoFactorToken", Token);
|
||||
dict.Add("TwoFactorProvider", Provider.Value.ToString());
|
||||
dict.Add("TwoFactorProvider", ((byte)(Provider.Value)).ToString());
|
||||
dict.Add("TwoFactorRemember", Remember ? "1" : "0");
|
||||
}
|
||||
|
||||
return dict;
|
||||
|
||||
8
src/App/Models/Api/Request/TwoFactorEmailRequest.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class TwoFactorEmailRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
}
|
||||
11
src/App/Models/Api/Response/AttachmentResponse.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class AttachmentResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
public class CipherHistoryResponse
|
||||
{
|
||||
public IEnumerable<CipherResponse> Revised { get; set; }
|
||||
public IEnumerable<string> Deleted { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.App.Enums;
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
@@ -12,7 +13,10 @@ namespace Bit.App.Models.Api
|
||||
public string OrganizationId { get; set; }
|
||||
public CipherType Type { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Edit { get; set; }
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
public JObject Data { get; set; }
|
||||
public IEnumerable<AttachmentResponse> Attachments { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
{
|
||||
@@ -13,7 +14,11 @@ namespace Bit.App.Models.Api
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Totp { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Edit { get; set; }
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
public IEnumerable<AttachmentResponse> Attachments { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace Bit.App.Models.Api
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool UseGroups { get; set; }
|
||||
public bool UseDirectory { get; set; }
|
||||
public bool UseTotp { get; set; }
|
||||
public int Seats { get; set; }
|
||||
public int MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Bit.App.Models.Api
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool EmailVerified { get; set; }
|
||||
public bool Premium { get; set; }
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Bit.App.Enums;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Api
|
||||
@@ -13,8 +14,9 @@ namespace Bit.App.Models.Api
|
||||
public string RefreshToken { get; set; }
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
public List<int> TwoFactorProviders { get; set; }
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string TwoFactorToken { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
51
src/App/Models/Attachment.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Bit.App.Models.Api;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class Attachment
|
||||
{
|
||||
public Attachment()
|
||||
{ }
|
||||
|
||||
public Attachment(AttachmentData data)
|
||||
{
|
||||
Id = data.Id;
|
||||
Url = data.Url;
|
||||
FileName = data.FileName != null ? new CipherString(data.FileName) : null;
|
||||
SetSize(data.Size);
|
||||
SizeName = data.SizeName;
|
||||
}
|
||||
|
||||
public Attachment(AttachmentResponse response)
|
||||
{
|
||||
Id = response.Id;
|
||||
Url = response.Url;
|
||||
FileName = response.FileName != null ? new CipherString(response.FileName) : null;
|
||||
SetSize(response.Size);
|
||||
SizeName = response.SizeName;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public CipherString FileName { get; set; }
|
||||
public long Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
|
||||
public AttachmentData ToAttachmentData(string loginId)
|
||||
{
|
||||
return new AttachmentData(this, loginId);
|
||||
}
|
||||
|
||||
private void SetSize(string sizeString)
|
||||
{
|
||||
long size;
|
||||
if(!long.TryParse(sizeString, out size))
|
||||
{
|
||||
size = 0;
|
||||
}
|
||||
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public abstract class Cipher
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,15 @@ namespace Bit.App.Models
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
Mac = encPieces[1];
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown encType.");
|
||||
}
|
||||
|
||||
47
src/App/Models/Data/AttachmentData.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using SQLite;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Models.Data
|
||||
{
|
||||
[Table("Attachment")]
|
||||
public class AttachmentData : IDataObject<string>
|
||||
{
|
||||
public AttachmentData()
|
||||
{ }
|
||||
|
||||
public AttachmentData(Attachment attachment, string loginId)
|
||||
{
|
||||
Id = attachment.Id;
|
||||
LoginId = loginId;
|
||||
Url = attachment.Url;
|
||||
FileName = attachment.FileName?.EncryptedString;
|
||||
Size = attachment.Size.ToString();
|
||||
SizeName = attachment.SizeName;
|
||||
}
|
||||
|
||||
public AttachmentData(AttachmentResponse response, string loginId)
|
||||
{
|
||||
Id = response.Id;
|
||||
LoginId = loginId;
|
||||
Url = response.Url;
|
||||
FileName = response.FileName;
|
||||
Size = response.Size;
|
||||
SizeName = response.SizeName;
|
||||
}
|
||||
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; }
|
||||
[Indexed]
|
||||
public string LoginId { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public string Size { get; set; }
|
||||
public string SizeName { get; set; }
|
||||
|
||||
public Attachment ToAttachment()
|
||||
{
|
||||
return new Attachment(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,10 @@ namespace Bit.App.Models.Data
|
||||
Username = login.Username?.EncryptedString;
|
||||
Password = login.Password?.EncryptedString;
|
||||
Notes = login.Notes?.EncryptedString;
|
||||
Totp = login?.Notes?.EncryptedString;
|
||||
Favorite = login.Favorite;
|
||||
Edit = login.Edit;
|
||||
OrganizationUseTotp = login.OrganizationUseTotp;
|
||||
}
|
||||
|
||||
public LoginData(LoginResponse login, string userId)
|
||||
@@ -36,8 +39,11 @@ namespace Bit.App.Models.Data
|
||||
Username = login.Username;
|
||||
Password = login.Password;
|
||||
Notes = login.Notes;
|
||||
Totp = login.Totp;
|
||||
Favorite = login.Favorite;
|
||||
RevisionDateTime = login.RevisionDate;
|
||||
Edit = login.Edit;
|
||||
OrganizationUseTotp = login.OrganizationUseTotp;
|
||||
}
|
||||
|
||||
public LoginData(CipherResponse cipher, string userId)
|
||||
@@ -58,7 +64,10 @@ namespace Bit.App.Models.Data
|
||||
Username = data.Username;
|
||||
Password = data.Password;
|
||||
Notes = data.Notes;
|
||||
Totp = data.Totp;
|
||||
Favorite = cipher.Favorite;
|
||||
Edit = cipher.Edit;
|
||||
OrganizationUseTotp = cipher.OrganizationUseTotp;
|
||||
RevisionDateTime = cipher.RevisionDate;
|
||||
}
|
||||
|
||||
@@ -73,7 +82,10 @@ namespace Bit.App.Models.Data
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Totp { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Edit { get; set; }
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Login ToLogin()
|
||||
|
||||
@@ -3,7 +3,7 @@ using Bit.App.Models.Api;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class Folder : Cipher
|
||||
public class Folder
|
||||
{
|
||||
public Folder()
|
||||
{ }
|
||||
@@ -20,6 +20,9 @@ namespace Bit.App.Models
|
||||
Name = response.Name != null ? new CipherString(response.Name) : null;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
|
||||
public FolderRequest ToFolderRequest()
|
||||
{
|
||||
return new FolderRequest(this);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using Bit.App.Models.Api;
|
||||
using Bit.App.Models.Data;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class Login : Cipher
|
||||
public class Login
|
||||
{
|
||||
public Login()
|
||||
{ }
|
||||
|
||||
public Login(LoginData data)
|
||||
public Login(LoginData data, IEnumerable<AttachmentData> attachments = null)
|
||||
{
|
||||
Id = data.Id;
|
||||
UserId = data.UserId;
|
||||
@@ -19,7 +21,11 @@ namespace Bit.App.Models
|
||||
Username = data.Username != null ? new CipherString(data.Username) : null;
|
||||
Password = data.Password != null ? new CipherString(data.Password) : null;
|
||||
Notes = data.Notes != null ? new CipherString(data.Notes) : null;
|
||||
Totp = data.Totp != null ? new CipherString(data.Totp) : null;
|
||||
Favorite = data.Favorite;
|
||||
Edit = data.Edit;
|
||||
OrganizationUseTotp = data.OrganizationUseTotp;
|
||||
Attachments = attachments?.Select(a => new Attachment(a));
|
||||
}
|
||||
|
||||
public Login(LoginResponse response)
|
||||
@@ -33,17 +39,27 @@ namespace Bit.App.Models
|
||||
Username = response.Username != null ? new CipherString(response.Username) : null;
|
||||
Password = response.Password != null ? new CipherString(response.Password) : null;
|
||||
Notes = response.Notes != null ? new CipherString(response.Notes) : null;
|
||||
Totp = response.Totp != null ? new CipherString(response.Totp) : null;
|
||||
Favorite = response.Favorite;
|
||||
Edit = response.Edit;
|
||||
OrganizationUseTotp = response.OrganizationUseTotp;
|
||||
Attachments = response.Attachments?.Select(a => new Attachment(a));
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public CipherString Name { get; set; }
|
||||
public CipherString Uri { get; set; }
|
||||
public CipherString Username { get; set; }
|
||||
public CipherString Password { get; set; }
|
||||
public CipherString Notes { get; set; }
|
||||
public CipherString Totp { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public bool Edit { get; set; }
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
public IEnumerable<Attachment> Attachments { get; set; }
|
||||
|
||||
public LoginRequest ToLoginRequest()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace Bit.App.Models
|
||||
using Bit.App.Enums;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class LoginResult
|
||||
{
|
||||
@@ -8,7 +11,8 @@
|
||||
|
||||
public class FullLoginResult : LoginResult
|
||||
{
|
||||
public bool TwoFactorRequired { get; set; }
|
||||
public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0;
|
||||
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
|
||||
public SymmetricCryptoKey Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.App.Resources;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Models.Page
|
||||
{
|
||||
@@ -12,20 +13,24 @@ namespace Bit.App.Models.Page
|
||||
{
|
||||
Id = login.Id;
|
||||
Shared = !string.IsNullOrWhiteSpace(login.OrganizationId);
|
||||
HasAttachments = login.Attachments?.Any() ?? false;
|
||||
FolderId = login.FolderId;
|
||||
Name = login.Name?.Decrypt(login.OrganizationId);
|
||||
Username = login.Username?.Decrypt(login.OrganizationId) ?? " ";
|
||||
Password = new Lazy<string>(() => login.Password?.Decrypt(login.OrganizationId));
|
||||
Uri = new Lazy<string>(() => login.Uri?.Decrypt(login.OrganizationId));
|
||||
Totp = new Lazy<string>(() => login.Totp?.Decrypt(login.OrganizationId));
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public bool Shared { get; set; }
|
||||
public bool HasAttachments { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Username { get; set; }
|
||||
public Lazy<string> Password { get; set; }
|
||||
public Lazy<string> Uri { get; set; }
|
||||
public Lazy<string> Totp { get; set; }
|
||||
}
|
||||
|
||||
public class AutofillLogin : Login
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.ComponentModel;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Models.Page
|
||||
{
|
||||
@@ -12,7 +13,10 @@ namespace Bit.App.Models.Page
|
||||
private string _password;
|
||||
private string _uri;
|
||||
private string _notes;
|
||||
private string _totpCode;
|
||||
private int _totpSec = 30;
|
||||
private bool _revealPassword;
|
||||
private List<Attachment> _attachments;
|
||||
|
||||
public VaultViewLoginPageModel() { }
|
||||
|
||||
@@ -192,6 +196,43 @@ namespace Bit.App.Models.Page
|
||||
public string ShowHideText => RevealPassword ? AppResources.Hide : AppResources.Show;
|
||||
public ImageSource ShowHideImage => RevealPassword ? ImageSource.FromFile("eye_slash") : ImageSource.FromFile("eye");
|
||||
|
||||
public string TotpCode
|
||||
{
|
||||
get { return _totpCode; }
|
||||
set
|
||||
{
|
||||
_totpCode = value;
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpCode)));
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpCodeFormatted)));
|
||||
}
|
||||
}
|
||||
public int TotpSecond
|
||||
{
|
||||
get { return _totpSec; }
|
||||
set
|
||||
{
|
||||
_totpSec = value;
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpSecond)));
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpColor)));
|
||||
}
|
||||
}
|
||||
public bool TotpLow => TotpSecond <= 7;
|
||||
public Color TotpColor => !string.IsNullOrWhiteSpace(TotpCode) && TotpLow ? Color.Red : Color.Black;
|
||||
public string TotpCodeFormatted => !string.IsNullOrWhiteSpace(TotpCode) ?
|
||||
string.Format("{0} {1}", TotpCode.Substring(0, 3), TotpCode.Substring(3)) : null;
|
||||
|
||||
public List<Attachment> Attachments
|
||||
{
|
||||
get { return _attachments; }
|
||||
set
|
||||
{
|
||||
_attachments = value;
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Attachments)));
|
||||
PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowAttachments)));
|
||||
}
|
||||
}
|
||||
public bool ShowAttachments => (Attachments?.Count ?? 0) > 0;
|
||||
|
||||
public void Update(Login login)
|
||||
{
|
||||
Name = login.Name?.Decrypt(login.OrganizationId);
|
||||
@@ -199,6 +240,36 @@ namespace Bit.App.Models.Page
|
||||
Password = login.Password?.Decrypt(login.OrganizationId);
|
||||
Uri = login.Uri?.Decrypt(login.OrganizationId);
|
||||
Notes = login.Notes?.Decrypt(login.OrganizationId);
|
||||
|
||||
if(login.Attachments != null)
|
||||
{
|
||||
var attachments = new List<Attachment>();
|
||||
foreach(var attachment in login.Attachments)
|
||||
{
|
||||
attachments.Add(new Attachment
|
||||
{
|
||||
Id = attachment.Id,
|
||||
Name = attachment.FileName?.Decrypt(login.OrganizationId),
|
||||
SizeName = attachment.SizeName,
|
||||
Size = attachment.Size,
|
||||
Url = attachment.Url
|
||||
});
|
||||
}
|
||||
Attachments = attachments;
|
||||
}
|
||||
else
|
||||
{
|
||||
login.Attachments = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +189,12 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordCell.Entry.Text = string.Empty;
|
||||
|
||||
if(result.TwoFactorRequired)
|
||||
{
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn To Two-step");
|
||||
await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result.MasterPasswordHash, result.Key));
|
||||
await Navigation.PushAsync(new LoginTwoFactorPage(EmailCell.Entry.Text, result));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,155 +9,398 @@ using System.Threading.Tasks;
|
||||
using PushNotification.Plugin.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Enums;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using FFImageLoading.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginTwoFactorPage : ExtendedContentPage
|
||||
{
|
||||
private DateTime? _lastAction;
|
||||
private IAuthService _authService;
|
||||
private IUserDialogs _userDialogs;
|
||||
private ISyncService _syncService;
|
||||
private IDeviceInfoService _deviceInfoService;
|
||||
private IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private ITwoFactorApiRepository _twoFactorApiRepository;
|
||||
private IPushNotification _pushNotification;
|
||||
private readonly string _email;
|
||||
private readonly string _masterPasswordHash;
|
||||
private readonly SymmetricCryptoKey _key;
|
||||
private readonly Dictionary<TwoFactorProviderType, Dictionary<string, object>> _providers;
|
||||
private TwoFactorProviderType? _providerType;
|
||||
private readonly FullLoginResult _result;
|
||||
|
||||
public LoginTwoFactorPage(string email, string masterPasswordHash, SymmetricCryptoKey key)
|
||||
public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null)
|
||||
: base(updateActivity: false)
|
||||
{
|
||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
||||
|
||||
_email = email;
|
||||
_masterPasswordHash = masterPasswordHash;
|
||||
_key = key;
|
||||
_result = result;
|
||||
_masterPasswordHash = result.MasterPasswordHash;
|
||||
_key = result.Key;
|
||||
_providers = result.TwoFactorProviders;
|
||||
_providerType = type ?? GetDefaultProvider();
|
||||
|
||||
_authService = Resolver.Resolve<IAuthService>();
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_syncService = Resolver.Resolve<ISyncService>();
|
||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
_twoFactorApiRepository = Resolver.Resolve<ITwoFactorApiRepository>();
|
||||
_pushNotification = Resolver.Resolve<IPushNotification>();
|
||||
|
||||
Init();
|
||||
}
|
||||
|
||||
public FormEntryCell CodeCell { get; set; }
|
||||
public FormEntryCell TokenCell { get; set; }
|
||||
public ExtendedSwitchCell RememberCell { get; set; }
|
||||
|
||||
private void Init()
|
||||
{
|
||||
var padding = Helpers.OnPlatform(
|
||||
iOS: new Thickness(15, 20),
|
||||
Android: new Thickness(15, 8),
|
||||
WinPhone: new Thickness(15, 20));
|
||||
|
||||
CodeCell = new FormEntryCell(AppResources.VerificationCode, useLabelAsPlaceholder: true,
|
||||
imageSource: "lock", containerPadding: padding);
|
||||
|
||||
CodeCell.Entry.Keyboard = Keyboard.Numeric;
|
||||
CodeCell.Entry.ReturnType = Enums.ReturnType.Go;
|
||||
|
||||
var table = new ExtendedTableView
|
||||
SubscribeYubiKey(true);
|
||||
if(_providers.Count > 1)
|
||||
{
|
||||
Intent = TableIntent.Settings,
|
||||
EnableScrolling = false,
|
||||
HasUnevenRows = true,
|
||||
EnableSelection = true,
|
||||
NoFooter = true,
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
{
|
||||
CodeCell
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var codeLabel = new Label
|
||||
{
|
||||
Text = AppResources.EnterVerificationCode,
|
||||
LineBreakMode = LineBreakMode.WordWrap,
|
||||
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
|
||||
Style = (Style)Application.Current.Resources["text-muted"],
|
||||
Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25)
|
||||
};
|
||||
|
||||
var lostAppButton = new ExtendedButton
|
||||
{
|
||||
Text = AppResources.Lost2FAApp,
|
||||
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
|
||||
Margin = new Thickness(15, 0, 15, 25),
|
||||
Command = new Command(() => Lost2FAApp()),
|
||||
Uppercase = false,
|
||||
BackgroundColor = Color.Transparent
|
||||
};
|
||||
|
||||
var layout = new StackLayout
|
||||
{
|
||||
Children = { table, codeLabel, lostAppButton },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
var scrollView = new ScrollView { Content = layout };
|
||||
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
table.RowHeight = -1;
|
||||
table.EstimatedRowHeight = 70;
|
||||
var sendEmailTask = SendEmailAsync(false);
|
||||
}
|
||||
|
||||
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
|
||||
{
|
||||
await LogInAsync();
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
ToolbarItems.Clear();
|
||||
var scrollView = new ScrollView();
|
||||
|
||||
ToolbarItems.Add(continueToolbarItem);
|
||||
Title = AppResources.VerificationCode;
|
||||
Content = scrollView;
|
||||
var anotherMethodButton = new ExtendedButton
|
||||
{
|
||||
Text = AppResources.UseAnotherTwoStepMethod,
|
||||
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
|
||||
Margin = new Thickness(15, 0, 15, 25),
|
||||
Command = new Command(() => AnotherMethodAsync()),
|
||||
Uppercase = false,
|
||||
BackgroundColor = Color.Transparent,
|
||||
VerticalOptions = LayoutOptions.Start
|
||||
};
|
||||
|
||||
var instruction = new Label
|
||||
{
|
||||
LineBreakMode = LineBreakMode.WordWrap,
|
||||
Margin = new Thickness(15),
|
||||
HorizontalTextAlignment = TextAlignment.Center
|
||||
};
|
||||
|
||||
RememberCell = new ExtendedSwitchCell
|
||||
{
|
||||
Text = AppResources.RememberMe,
|
||||
On = false
|
||||
};
|
||||
|
||||
if(!_providerType.HasValue)
|
||||
{
|
||||
instruction.Text = AppResources.NoTwoStepAvailable;
|
||||
|
||||
var layout = new StackLayout
|
||||
{
|
||||
Children = { instruction, anotherMethodButton },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
scrollView.Content = layout;
|
||||
|
||||
Title = AppResources.LoginUnavailable;
|
||||
Content = scrollView;
|
||||
}
|
||||
else if(_providerType.Value == TwoFactorProviderType.Authenticator ||
|
||||
_providerType.Value == TwoFactorProviderType.Email)
|
||||
{
|
||||
var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () =>
|
||||
{
|
||||
var token = TokenCell?.Entry.Text.Trim().Replace(" ", "");
|
||||
await LogInAsync(token);
|
||||
}, ToolbarItemOrder.Default, 0);
|
||||
|
||||
var padding = Helpers.OnPlatform(
|
||||
iOS: new Thickness(15, 20),
|
||||
Android: new Thickness(15, 8),
|
||||
WinPhone: new Thickness(15, 20));
|
||||
|
||||
TokenCell = new FormEntryCell(AppResources.VerificationCode, useLabelAsPlaceholder: true,
|
||||
imageSource: "lock", containerPadding: padding);
|
||||
|
||||
TokenCell.Entry.Keyboard = Keyboard.Numeric;
|
||||
TokenCell.Entry.ReturnType = ReturnType.Go;
|
||||
|
||||
var table = new TwoFactorTable(
|
||||
new TableSection(" ")
|
||||
{
|
||||
TokenCell,
|
||||
RememberCell
|
||||
});
|
||||
|
||||
var layout = new StackLayout
|
||||
{
|
||||
Children = { instruction, table },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
scrollView.Content = layout;
|
||||
|
||||
switch(_providerType.Value)
|
||||
{
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
instruction.Text = AppResources.EnterVerificationCodeApp;
|
||||
layout.Children.Add(anotherMethodButton);
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
var emailParams = _providers[TwoFactorProviderType.Email];
|
||||
var redactedEmail = emailParams["Email"].ToString();
|
||||
|
||||
instruction.Text = string.Format(AppResources.EnterVerificationCodeEmail, redactedEmail);
|
||||
var resendEmailButton = new ExtendedButton
|
||||
{
|
||||
Text = AppResources.SendVerificationCodeAgain,
|
||||
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
|
||||
Margin = new Thickness(15, 0, 15, 0),
|
||||
Command = new Command(async () => await SendEmailAsync(true)),
|
||||
Uppercase = false,
|
||||
BackgroundColor = Color.Transparent,
|
||||
VerticalOptions = LayoutOptions.Start
|
||||
};
|
||||
|
||||
layout.Children.Add(resendEmailButton);
|
||||
layout.Children.Add(anotherMethodButton);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ToolbarItems.Add(continueToolbarItem);
|
||||
Title = AppResources.VerificationCode;
|
||||
|
||||
Content = scrollView;
|
||||
TokenCell.Entry.FocusWithDelay();
|
||||
}
|
||||
else if(_providerType == TwoFactorProviderType.Duo)
|
||||
{
|
||||
var duoParams = _providers[TwoFactorProviderType.Duo];
|
||||
|
||||
var host = WebUtility.UrlEncode(duoParams["Host"].ToString());
|
||||
var req = WebUtility.UrlEncode(duoParams["Signature"].ToString());
|
||||
|
||||
var webView = new HybridWebView
|
||||
{
|
||||
Uri = $"https://vault.bitwarden.com/duo-connector.html?host={host}&request={req}",
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
VerticalOptions = LayoutOptions.FillAndExpand,
|
||||
MinimumHeightRequest = 400
|
||||
};
|
||||
webView.RegisterAction(async (sig) =>
|
||||
{
|
||||
await LogInAsync(sig);
|
||||
});
|
||||
|
||||
var table = new TwoFactorTable(
|
||||
new TableSection(" ")
|
||||
{
|
||||
RememberCell
|
||||
});
|
||||
|
||||
var layout = new StackLayout
|
||||
{
|
||||
Children = { webView, table, anotherMethodButton },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
scrollView.Content = layout;
|
||||
|
||||
Title = "Duo";
|
||||
Content = scrollView;
|
||||
}
|
||||
else if(_providerType == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
instruction.Text = AppResources.YubiKeyInstruction;
|
||||
|
||||
var image = new CachedImage
|
||||
{
|
||||
Source = "yubikey",
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
WidthRequest = 266,
|
||||
HeightRequest = 160,
|
||||
Margin = new Thickness(0, 0, 0, 25)
|
||||
};
|
||||
|
||||
var table = new TwoFactorTable(
|
||||
new TableSection(" ")
|
||||
{
|
||||
RememberCell
|
||||
});
|
||||
|
||||
var layout = new StackLayout
|
||||
{
|
||||
Children = { instruction, image, table, anotherMethodButton },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
scrollView.Content = layout;
|
||||
|
||||
Title = AppResources.YubiKeyTitle;
|
||||
Content = scrollView;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
CodeCell.InitEvents();
|
||||
CodeCell.Entry.FocusWithDelay();
|
||||
CodeCell.Entry.Completed += Entry_Completed;
|
||||
ListenYubiKey(true);
|
||||
|
||||
InitEvents();
|
||||
if(TokenCell == null && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "DismissKeyboard");
|
||||
}
|
||||
}
|
||||
|
||||
private void InitEvents()
|
||||
{
|
||||
if(TokenCell != null)
|
||||
{
|
||||
TokenCell.InitEvents();
|
||||
TokenCell.Entry.Completed += Entry_Completed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
CodeCell.Dispose();
|
||||
CodeCell.Entry.Completed -= Entry_Completed;
|
||||
ListenYubiKey(false);
|
||||
|
||||
if(TokenCell != null)
|
||||
{
|
||||
TokenCell.Dispose();
|
||||
TokenCell.Entry.Completed -= Entry_Completed;
|
||||
}
|
||||
}
|
||||
|
||||
private void Lost2FAApp()
|
||||
private async void AnotherMethodAsync()
|
||||
{
|
||||
Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/"));
|
||||
var beforeProviderType = _providerType;
|
||||
|
||||
var options = new List<string>();
|
||||
if(_providers.ContainsKey(TwoFactorProviderType.Authenticator))
|
||||
{
|
||||
options.Add(AppResources.AuthenticatorAppTitle);
|
||||
}
|
||||
|
||||
if(_providers.ContainsKey(TwoFactorProviderType.Duo))
|
||||
{
|
||||
options.Add("Duo");
|
||||
}
|
||||
|
||||
if(_providers.ContainsKey(TwoFactorProviderType.YubiKey))
|
||||
{
|
||||
var nfcKey = _providers[TwoFactorProviderType.YubiKey].ContainsKey("Nfc") &&
|
||||
(bool)_providers[TwoFactorProviderType.YubiKey]["Nfc"];
|
||||
if(_deviceInfoService.NfcEnabled && nfcKey)
|
||||
{
|
||||
options.Add(AppResources.YubiKeyTitle);
|
||||
}
|
||||
}
|
||||
|
||||
if(_providers.ContainsKey(TwoFactorProviderType.Email))
|
||||
{
|
||||
options.Add(AppResources.Email);
|
||||
}
|
||||
|
||||
options.Add(AppResources.RecoveryCodeTitle);
|
||||
|
||||
var selection = await DisplayActionSheet(AppResources.TwoStepLoginOptions, AppResources.Cancel, null,
|
||||
options.ToArray());
|
||||
if(selection == AppResources.AuthenticatorAppTitle)
|
||||
{
|
||||
_providerType = TwoFactorProviderType.Authenticator;
|
||||
}
|
||||
else if(selection == "Duo")
|
||||
{
|
||||
_providerType = TwoFactorProviderType.Duo;
|
||||
}
|
||||
else if(selection == AppResources.YubiKeyTitle)
|
||||
{
|
||||
_providerType = TwoFactorProviderType.YubiKey;
|
||||
}
|
||||
else if(selection == AppResources.Email)
|
||||
{
|
||||
_providerType = TwoFactorProviderType.Email;
|
||||
}
|
||||
else if(selection == AppResources.RecoveryCodeTitle)
|
||||
{
|
||||
Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(beforeProviderType != _providerType)
|
||||
{
|
||||
Init();
|
||||
ListenYubiKey(false, beforeProviderType == TwoFactorProviderType.YubiKey);
|
||||
ListenYubiKey(true);
|
||||
InitEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendEmailAsync(bool doToast)
|
||||
{
|
||||
if(_providerType != TwoFactorProviderType.Email)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await _twoFactorApiRepository.PostSendEmailLoginAsync(new Models.Api.TwoFactorEmailRequest
|
||||
{
|
||||
Email = _email,
|
||||
MasterPasswordHash = _masterPasswordHash
|
||||
});
|
||||
|
||||
if(response.Succeeded && doToast)
|
||||
{
|
||||
_userDialogs.Toast(AppResources.VerificationEmailSent);
|
||||
}
|
||||
else if(!response.Succeeded)
|
||||
{
|
||||
_userDialogs.Alert(AppResources.VerificationEmailNotSent);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Entry_Completed(object sender, EventArgs e)
|
||||
{
|
||||
await LogInAsync();
|
||||
var token = TokenCell.Entry.Text.Trim().Replace(" ", "");
|
||||
await LogInAsync(token);
|
||||
}
|
||||
|
||||
private async Task LogInAsync()
|
||||
private async Task LogInAsync(string token)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(CodeCell.Entry.Text))
|
||||
if(!_providerType.HasValue || _lastAction.LastActionWasRecent())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastAction = DateTime.UtcNow;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired,
|
||||
AppResources.VerificationCode), AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
_userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black);
|
||||
var response = await _authService.TokenPostTwoFactorAsync(CodeCell.Entry.Text, _email, _masterPasswordHash, _key);
|
||||
_userDialogs.ShowLoading(string.Concat(AppResources.Validating, "..."), MaskType.Black);
|
||||
var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On,
|
||||
_email, _masterPasswordHash, _key);
|
||||
_userDialogs.HideLoading();
|
||||
if(!response.Success)
|
||||
{
|
||||
ListenYubiKey(true);
|
||||
await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step");
|
||||
_googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step", _providerType.Value.ToString());
|
||||
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
@@ -165,7 +408,128 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
Application.Current.MainPage = new MainPage();
|
||||
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Application.Current.MainPage = new MainPage();
|
||||
});
|
||||
}
|
||||
|
||||
private TwoFactorProviderType? GetDefaultProvider()
|
||||
{
|
||||
TwoFactorProviderType? provider = null;
|
||||
|
||||
if(_providers != null)
|
||||
{
|
||||
foreach(var p in _providers)
|
||||
{
|
||||
switch(p.Key)
|
||||
{
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
if(provider == TwoFactorProviderType.Duo || provider == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
if(provider.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.Duo:
|
||||
if(provider == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
var nfcKey = p.Value.ContainsKey("Nfc") && (bool)p.Value["Nfc"];
|
||||
if(!_deviceInfoService.NfcEnabled || !nfcKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
provider = p.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
private void ListenYubiKey(bool listen, bool overrideCheck = false)
|
||||
{
|
||||
if(_providerType == TwoFactorProviderType.YubiKey || overrideCheck)
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen);
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeYubiKey(bool subscribe)
|
||||
{
|
||||
if(_providerType != TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP");
|
||||
MessagingCenter.Unsubscribe<Application>(Application.Current, "ResumeYubiKey");
|
||||
if(!subscribe)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MessagingCenter.Subscribe<Application, string>(Application.Current, "GotYubiKeyOTP", async (sender, otp) =>
|
||||
{
|
||||
MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP");
|
||||
if(_providerType == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
await LogInAsync(otp);
|
||||
}
|
||||
});
|
||||
|
||||
SubscribeYubiKeyResume();
|
||||
}
|
||||
|
||||
private void SubscribeYubiKeyResume()
|
||||
{
|
||||
MessagingCenter.Subscribe<Application>(Application.Current, "ResumeYubiKey", (sender) =>
|
||||
{
|
||||
MessagingCenter.Unsubscribe<Application>(Application.Current, "ResumeYubiKey");
|
||||
if(_providerType == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", true);
|
||||
SubscribeYubiKeyResume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public class TwoFactorTable : ExtendedTableView
|
||||
{
|
||||
public TwoFactorTable(TableSection section)
|
||||
{
|
||||
Intent = TableIntent.Settings;
|
||||
EnableScrolling = false;
|
||||
HasUnevenRows = true;
|
||||
EnableSelection = true;
|
||||
NoFooter = true;
|
||||
NoHeader = true;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
Root = Root = new TableRoot
|
||||
{
|
||||
section
|
||||
};
|
||||
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
RowHeight = -1;
|
||||
EstimatedRowHeight = 70;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
src/App/Pages/ScanPage.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xamarin.Forms;
|
||||
using ZXing.Net.Mobile.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ScanPage : ExtendedContentPage
|
||||
{
|
||||
private readonly ZXingScannerView _zxing;
|
||||
private readonly OverlayGrid _overlay;
|
||||
private bool _pageDisappeared = true;
|
||||
|
||||
public ScanPage(Action<string> callback)
|
||||
: base(updateActivity: false)
|
||||
{
|
||||
_zxing = new ZXingScannerView
|
||||
{
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
VerticalOptions = LayoutOptions.FillAndExpand,
|
||||
AutomationId = "zxingScannerView",
|
||||
Options = new ZXing.Mobile.MobileBarcodeScanningOptions
|
||||
{
|
||||
UseNativeScanning = true,
|
||||
PossibleFormats = new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE },
|
||||
AutoRotate = false
|
||||
}
|
||||
};
|
||||
|
||||
_zxing.OnScanResult += (result) =>
|
||||
{
|
||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||
_zxing.IsAnalyzing = false;
|
||||
_zxing.IsScanning = false;
|
||||
|
||||
Uri uri;
|
||||
if(!string.IsNullOrWhiteSpace(result.Text) && Uri.TryCreate(result.Text, UriKind.Absolute, out uri) &&
|
||||
!string.IsNullOrWhiteSpace(uri.Query))
|
||||
{
|
||||
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
||||
foreach(var part in queryParts)
|
||||
{
|
||||
if(part.StartsWith("secret="))
|
||||
{
|
||||
callback(part.Substring(7)?.ToUpperInvariant());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(null);
|
||||
};
|
||||
|
||||
_overlay = new OverlayGrid
|
||||
{
|
||||
AutomationId = "zxingDefaultOverlay"
|
||||
};
|
||||
|
||||
_overlay.TopLabel.Text = AppResources.CameraInstructionTop;
|
||||
_overlay.BottomLabel.Text = AppResources.CameraInstructionBottom;
|
||||
|
||||
var grid = new Grid
|
||||
{
|
||||
VerticalOptions = LayoutOptions.FillAndExpand,
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
Children = { _zxing, _overlay }
|
||||
};
|
||||
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close));
|
||||
}
|
||||
|
||||
Title = AppResources.ScanQrTitle;
|
||||
Content = grid;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_pageDisappeared = false;
|
||||
base.OnAppearing();
|
||||
_zxing.IsScanning = true;
|
||||
Device.StartTimer(new TimeSpan(0, 0, 2), () =>
|
||||
{
|
||||
if(_pageDisappeared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_zxing.AutoFocus();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_pageDisappeared = true;
|
||||
_zxing.IsScanning = false;
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
public class OverlayGrid : Grid
|
||||
{
|
||||
public OverlayGrid()
|
||||
{
|
||||
VerticalOptions = LayoutOptions.FillAndExpand;
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand;
|
||||
|
||||
RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
RowDefinitions.Add(new RowDefinition { Height = new GridLength(2, GridUnitType.Star) });
|
||||
RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
Children.Add(new BoxView
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Fill,
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
BackgroundColor = Color.Black,
|
||||
Opacity = 0.7,
|
||||
}, 0, 0);
|
||||
|
||||
Children.Add(new BoxView
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
BackgroundColor = Color.Transparent
|
||||
}, 0, 1);
|
||||
|
||||
Children.Add(new BoxView
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Fill,
|
||||
HorizontalOptions = LayoutOptions.FillAndExpand,
|
||||
BackgroundColor = Color.Black,
|
||||
Opacity = 0.7,
|
||||
}, 0, 2);
|
||||
|
||||
TopLabel = new Label
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
TextColor = Color.White,
|
||||
AutomationId = "zxingDefaultOverlay_TopTextLabel",
|
||||
};
|
||||
Children.Add(TopLabel, 0, 0);
|
||||
|
||||
BottomLabel = new Label
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
TextColor = Color.White,
|
||||
AutomationId = "zxingDefaultOverlay_BottomTextLabel",
|
||||
};
|
||||
Children.Add(BottomLabel, 0, 2);
|
||||
}
|
||||
|
||||
public Label TopLabel { get; set; }
|
||||
public Label BottomLabel { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
private StackLayout StackLayout { get; set; }
|
||||
private ExtendedSwitchCell CopyTotpCell { get; set; }
|
||||
private Label CopyTotpLabel { get; set; }
|
||||
private ExtendedSwitchCell AnalyticsCell { get; set; }
|
||||
private Label AnalyticsLabel { get; set; }
|
||||
private ExtendedSwitchCell AutofillPersistNotificationCell { get; set; }
|
||||
@@ -38,6 +40,23 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Init()
|
||||
{
|
||||
CopyTotpCell = new ExtendedSwitchCell
|
||||
{
|
||||
Text = AppResources.DisableAutoTotpCopy,
|
||||
On = _settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false)
|
||||
};
|
||||
|
||||
var totpTable = new FormTableView(true)
|
||||
{
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
{
|
||||
CopyTotpCell
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AnalyticsCell = new ExtendedSwitchCell
|
||||
{
|
||||
Text = AppResources.DisableGA,
|
||||
@@ -55,18 +74,19 @@ namespace Bit.App.Pages
|
||||
}
|
||||
};
|
||||
|
||||
AnalyticsLabel = new Label
|
||||
CopyTotpLabel = new FormTableLabel(this)
|
||||
{
|
||||
Text = AppResources.DisbaleGADescription,
|
||||
LineBreakMode = LineBreakMode.WordWrap,
|
||||
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
|
||||
Style = (Style)Application.Current.Resources["text-muted"],
|
||||
Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25)
|
||||
Text = AppResources.DisableAutoTotpCopyDescription
|
||||
};
|
||||
|
||||
AnalyticsLabel = new FormTableLabel(this)
|
||||
{
|
||||
Text = AppResources.DisableGADescription
|
||||
};
|
||||
|
||||
StackLayout = new StackLayout
|
||||
{
|
||||
Children = { analyticsTable, AnalyticsLabel },
|
||||
Children = { totpTable, CopyTotpLabel, analyticsTable, AnalyticsLabel },
|
||||
Spacing = 0
|
||||
};
|
||||
|
||||
@@ -78,7 +98,7 @@ namespace Bit.App.Pages
|
||||
On = !_appSettings.AutofillPersistNotification && !_appSettings.AutofillPasswordField
|
||||
};
|
||||
|
||||
var autofillAlwaysTable = new FormTableView
|
||||
var autofillAlwaysTable = new FormTableView(true)
|
||||
{
|
||||
Root = new TableRoot
|
||||
{
|
||||
@@ -102,7 +122,6 @@ namespace Bit.App.Pages
|
||||
|
||||
var autofillPersistNotificationTable = new FormTableView
|
||||
{
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
@@ -125,7 +144,6 @@ namespace Bit.App.Pages
|
||||
|
||||
var autofillPasswordFieldTable = new FormTableView
|
||||
{
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
@@ -169,6 +187,7 @@ namespace Bit.App.Pages
|
||||
base.OnAppearing();
|
||||
|
||||
AnalyticsCell.OnChanged += AnalyticsCell_Changed;
|
||||
CopyTotpCell.OnChanged += CopyTotpCell_OnChanged;
|
||||
StackLayout.LayoutChanged += Layout_LayoutChanged;
|
||||
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
@@ -184,6 +203,7 @@ namespace Bit.App.Pages
|
||||
base.OnDisappearing();
|
||||
|
||||
AnalyticsCell.OnChanged -= AnalyticsCell_Changed;
|
||||
CopyTotpCell.OnChanged -= CopyTotpCell_OnChanged;
|
||||
StackLayout.LayoutChanged -= Layout_LayoutChanged;
|
||||
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
@@ -197,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)
|
||||
@@ -211,6 +248,17 @@ namespace Bit.App.Pages
|
||||
_googleAnalyticsService.SetAppOptOut(cell.On);
|
||||
}
|
||||
|
||||
private void CopyTotpCell_OnChanged(object sender, ToggledEventArgs e)
|
||||
{
|
||||
var cell = sender as ExtendedSwitchCell;
|
||||
if(cell == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings.AddOrUpdateValue(Constants.SettingDisableTotpCopy, cell.On);
|
||||
}
|
||||
|
||||
private void AutofillAlwaysCell_OnChanged(object sender, ToggledEventArgs e)
|
||||
{
|
||||
var cell = sender as ExtendedSwitchCell;
|
||||
@@ -262,7 +310,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private class FormTableView : ExtendedTableView
|
||||
{
|
||||
public FormTableView()
|
||||
public FormTableView(bool header = false)
|
||||
{
|
||||
Intent = TableIntent.Settings;
|
||||
EnableScrolling = false;
|
||||
@@ -270,6 +318,7 @@ namespace Bit.App.Pages
|
||||
EnableSelection = true;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
NoFooter = true;
|
||||
NoHeader = !header;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,16 @@ namespace Bit.App.Pages
|
||||
ShowDisclousure = true
|
||||
};
|
||||
|
||||
LockCell = new ExtendedTextCell
|
||||
{
|
||||
Text = AppResources.Lock
|
||||
};
|
||||
|
||||
var securitySecion = new TableSection(AppResources.Security)
|
||||
{
|
||||
LockOptionsCell,
|
||||
PinCell,
|
||||
LockCell,
|
||||
TwoStepCell
|
||||
};
|
||||
|
||||
@@ -117,11 +123,6 @@ namespace Bit.App.Pages
|
||||
ShowDisclousure = true
|
||||
};
|
||||
|
||||
LockCell = new ExtendedTextCell
|
||||
{
|
||||
Text = AppResources.Lock
|
||||
};
|
||||
|
||||
LogOutCell = new ExtendedTextCell
|
||||
{
|
||||
Text = AppResources.LogOut
|
||||
@@ -177,18 +178,14 @@ namespace Bit.App.Pages
|
||||
new TableSection(AppResources.Account)
|
||||
{
|
||||
ChangeMasterPasswordCell,
|
||||
ChangeEmailCell
|
||||
ChangeEmailCell,
|
||||
LogOutCell
|
||||
},
|
||||
new TableSection(AppResources.Manage)
|
||||
{
|
||||
FoldersCell,
|
||||
SyncCell
|
||||
},
|
||||
new TableSection(AppResources.CurrentSession)
|
||||
{
|
||||
LockCell,
|
||||
LogOutCell
|
||||
},
|
||||
otherSection
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private readonly IAppInfoService _appInfoService;
|
||||
private bool _pageDisappeared = false;
|
||||
|
||||
public ToolsAutofillServicePage()
|
||||
{
|
||||
@@ -170,6 +171,11 @@ namespace Bit.App.Pages
|
||||
UpdateEnabled();
|
||||
Device.StartTimer(new TimeSpan(0, 0, 3), () =>
|
||||
{
|
||||
if(_pageDisappeared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateEnabled();
|
||||
return true;
|
||||
});
|
||||
@@ -178,6 +184,18 @@ namespace Bit.App.Pages
|
||||
Content = ScrollView;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_pageDisappeared = false;
|
||||
base.OnAppearing();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_pageDisappeared = true;
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private void UpdateEnabled()
|
||||
{
|
||||
ScrollView.Content = _appInfoService.AutofillServiceEnabled ? EnabledStackLayout : DisabledStackLayout;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Bit.App.Pages
|
||||
private readonly IUserDialogs _userDialogs;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly ISettings _settings;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDeviceActionService _clipboardService;
|
||||
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
||||
private readonly Action<string> _passwordValueAction;
|
||||
private readonly bool _fromAutofill;
|
||||
@@ -26,7 +26,7 @@ namespace Bit.App.Pages
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_passwordGenerationService = Resolver.Resolve<IPasswordGenerationService>();
|
||||
_settings = Resolver.Resolve<ISettings>();
|
||||
_clipboardService = Resolver.Resolve<IClipboardService>();
|
||||
_clipboardService = Resolver.Resolve<IDeviceActionService>();
|
||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
_passwordValueAction = passwordValueAction;
|
||||
_fromAutofill = fromAutofill;
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace Bit.App.Pages
|
||||
public FormEntryCell UsernameCell { get; private set; }
|
||||
public FormEntryCell UriCell { get; private set; }
|
||||
public FormEntryCell NameCell { get; private set; }
|
||||
public FormEntryCell TotpCell { get; private set; }
|
||||
public FormEditorCell NotesCell { get; private set; }
|
||||
public FormPickerCell FolderCell { get; private set; }
|
||||
public ExtendedTextCell GenerateCell { get; private set; }
|
||||
@@ -58,7 +59,15 @@ namespace Bit.App.Pages
|
||||
private void Init()
|
||||
{
|
||||
NotesCell = new FormEditorCell(height: 180);
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: NotesCell.Editor,
|
||||
|
||||
TotpCell = new FormEntryCell(AppResources.AuthenticatorKey, nextElement: NotesCell.Editor,
|
||||
useButton: true);
|
||||
TotpCell.Button.Image = "camera";
|
||||
TotpCell.Entry.DisableAutocapitalize = true;
|
||||
TotpCell.Entry.Autocorrect = false;
|
||||
TotpCell.Entry.FontFamily = Helpers.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier");
|
||||
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: TotpCell.Entry,
|
||||
useButton: true);
|
||||
PasswordCell.Button.Image = "eye";
|
||||
PasswordCell.Entry.DisableAutocapitalize = true;
|
||||
@@ -115,6 +124,7 @@ namespace Bit.App.Pages
|
||||
},
|
||||
new TableSection(" ")
|
||||
{
|
||||
TotpCell,
|
||||
FolderCell,
|
||||
favoriteCell
|
||||
},
|
||||
@@ -132,7 +142,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
PasswordCell.Button.WidthRequest = 40;
|
||||
PasswordCell.Button.WidthRequest = TotpCell.Button.WidthRequest = 40;
|
||||
}
|
||||
|
||||
var saveToolBarItem = new ToolbarItem(AppResources.Save, null, async () =>
|
||||
@@ -158,11 +168,12 @@ namespace Bit.App.Pages
|
||||
|
||||
var login = new Login
|
||||
{
|
||||
Uri = UriCell.Entry.Text?.Encrypt(),
|
||||
Name = NameCell.Entry.Text?.Encrypt(),
|
||||
Username = UsernameCell.Entry.Text?.Encrypt(),
|
||||
Password = PasswordCell.Entry.Text?.Encrypt(),
|
||||
Notes = NotesCell.Editor.Text?.Encrypt(),
|
||||
Name = NameCell.Entry.Text.Encrypt(),
|
||||
Uri = string.IsNullOrWhiteSpace(UriCell.Entry.Text) ? null : UriCell.Entry.Text.Encrypt(),
|
||||
Username = string.IsNullOrWhiteSpace(UsernameCell.Entry.Text) ? null : UsernameCell.Entry.Text.Encrypt(),
|
||||
Password = string.IsNullOrWhiteSpace(PasswordCell.Entry.Text) ? null : PasswordCell.Entry.Text.Encrypt(),
|
||||
Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null : NotesCell.Editor.Text.Encrypt(),
|
||||
Totp = string.IsNullOrWhiteSpace(TotpCell.Entry.Text) ? null : TotpCell.Entry.Text.Encrypt(),
|
||||
Favorite = favoriteCell.On
|
||||
};
|
||||
|
||||
@@ -220,8 +231,10 @@ namespace Bit.App.Pages
|
||||
UriCell.InitEvents();
|
||||
NameCell.InitEvents();
|
||||
NotesCell.InitEvents();
|
||||
TotpCell.InitEvents();
|
||||
FolderCell.InitEvents();
|
||||
PasswordCell.Button.Clicked += PasswordButton_Clicked;
|
||||
TotpCell.Button.Clicked += TotpButton_Clicked;
|
||||
GenerateCell.Tapped += GenerateCell_Tapped;
|
||||
|
||||
if(!_fromAutofill && !_settings.GetValueOrDefault(AddedLoginAlertKey, false))
|
||||
@@ -250,8 +263,10 @@ namespace Bit.App.Pages
|
||||
UriCell.Dispose();
|
||||
NameCell.Dispose();
|
||||
NotesCell.Dispose();
|
||||
TotpCell.Dispose();
|
||||
FolderCell.Dispose();
|
||||
PasswordCell.Button.Clicked -= PasswordButton_Clicked;
|
||||
TotpCell.Button.Clicked -= TotpButton_Clicked;
|
||||
GenerateCell.Tapped -= GenerateCell_Tapped;
|
||||
}
|
||||
|
||||
@@ -261,6 +276,28 @@ namespace Bit.App.Pages
|
||||
PasswordCell.Button.Image = "eye" + (!PasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty);
|
||||
}
|
||||
|
||||
private async void TotpButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var scanPage = new ScanPage((key) =>
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
if(!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
TotpCell.Entry.Text = key;
|
||||
_userDialogs.Toast(AppResources.AuthenticatorKeyAdded);
|
||||
}
|
||||
else
|
||||
{
|
||||
_userDialogs.Alert(AppResources.AuthenticatorKeyReadError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage));
|
||||
}
|
||||
|
||||
private async void GenerateCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
var page = new ToolsPasswordGeneratorPage((password) =>
|
||||
|
||||
323
src/App/Pages/Vault/VaultAttachmentsPage.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
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 ITokenService _tokenService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly string _loginId;
|
||||
private Login _login;
|
||||
private byte[] _fileBytes;
|
||||
private DateTime? _lastAction;
|
||||
private bool _canUseAttachments = true;
|
||||
|
||||
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>();
|
||||
_tokenService = Resolver.Resolve<ITokenService>();
|
||||
_cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
|
||||
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()
|
||||
{
|
||||
_canUseAttachments = _cryptoService.EncKey != null;
|
||||
|
||||
SubscribeFileResult(true);
|
||||
var selectButton = new ExtendedButton
|
||||
{
|
||||
Text = AppResources.ChooseFile,
|
||||
Command = new Command(async () => await _deviceActiveService.SelectFileAsync()),
|
||||
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()),
|
||||
VerticalOptions = LayoutOptions.FillAndExpand
|
||||
};
|
||||
|
||||
if(_tokenService.TokenPremium)
|
||||
{
|
||||
ListView.Footer = NewTable;
|
||||
}
|
||||
|
||||
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(!_canUseAttachments)
|
||||
{
|
||||
await ShowUpdateKeyAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if(_tokenService.TokenPremium)
|
||||
{
|
||||
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();
|
||||
|
||||
if(_tokenService.TokenPremium && !_canUseAttachments)
|
||||
{
|
||||
await ShowUpdateKeyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if(!await _userDialogs.ConfirmAsync(AppResources.DoYouReallyWantToDelete, null, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ShowUpdateKeyAsync()
|
||||
{
|
||||
var confirmed = await _userDialogs.ConfirmAsync(AppResources.UpdateKey, AppResources.FeatureUnavailable,
|
||||
AppResources.LearnMore, AppResources.Cancel);
|
||||
if(confirmed)
|
||||
{
|
||||
Device.OpenUri(new Uri("https://help.bitwarden.com/article/update-encryption-key/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly ILoginService _loginService;
|
||||
private readonly IDeviceInfoService _deviceInfoService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDeviceActionService _clipboardService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private CancellationTokenSource _filterResultsCancellationTokenSource;
|
||||
private readonly string _name;
|
||||
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
|
||||
|
||||
_loginService = Resolver.Resolve<ILoginService>();
|
||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
||||
_clipboardService = Resolver.Resolve<IClipboardService>();
|
||||
_clipboardService = Resolver.Resolve<IDeviceActionService>();
|
||||
_settingsService = Resolver.Resolve<ISettingsService>();
|
||||
UserDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
GoogleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||
|
||||
@@ -38,9 +38,11 @@ namespace Bit.App.Pages
|
||||
public FormEntryCell UsernameCell { get; private set; }
|
||||
public FormEntryCell UriCell { get; private set; }
|
||||
public FormEntryCell NameCell { get; private set; }
|
||||
public FormEntryCell TotpCell { get; private set; }
|
||||
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()
|
||||
@@ -55,7 +57,15 @@ namespace Bit.App.Pages
|
||||
NotesCell = new FormEditorCell(height: 180);
|
||||
NotesCell.Editor.Text = login.Notes?.Decrypt(login.OrganizationId);
|
||||
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: NotesCell.Editor,
|
||||
TotpCell = new FormEntryCell(AppResources.AuthenticatorKey, nextElement: NotesCell.Editor,
|
||||
useButton: true);
|
||||
TotpCell.Entry.Text = login.Totp?.Decrypt(login.OrganizationId);
|
||||
TotpCell.Button.Image = "camera";
|
||||
TotpCell.Entry.DisableAutocapitalize = true;
|
||||
TotpCell.Entry.Autocorrect = false;
|
||||
TotpCell.Entry.FontFamily = Helpers.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier");
|
||||
|
||||
PasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: TotpCell.Entry,
|
||||
useButton: true);
|
||||
PasswordCell.Entry.Text = login.Password?.Decrypt(login.OrganizationId);
|
||||
PasswordCell.Button.Image = "eye";
|
||||
@@ -103,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
|
||||
@@ -122,8 +138,10 @@ namespace Bit.App.Pages
|
||||
},
|
||||
new TableSection(" ")
|
||||
{
|
||||
TotpCell,
|
||||
FolderCell,
|
||||
favoriteCell
|
||||
favoriteCell,
|
||||
AttachmentsCell
|
||||
},
|
||||
new TableSection(AppResources.Notes)
|
||||
{
|
||||
@@ -143,7 +161,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
PasswordCell.Button.WidthRequest = 40;
|
||||
PasswordCell.Button.WidthRequest = TotpCell.Button.WidthRequest = 40;
|
||||
}
|
||||
|
||||
var saveToolBarItem = new ToolbarItem(AppResources.Save, null, async () =>
|
||||
@@ -167,11 +185,17 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
login.Uri = UriCell.Entry.Text?.Encrypt(login.OrganizationId);
|
||||
login.Name = NameCell.Entry.Text?.Encrypt(login.OrganizationId);
|
||||
login.Username = UsernameCell.Entry.Text?.Encrypt(login.OrganizationId);
|
||||
login.Password = PasswordCell.Entry.Text?.Encrypt(login.OrganizationId);
|
||||
login.Notes = NotesCell.Editor.Text?.Encrypt(login.OrganizationId);
|
||||
login.Name = NameCell.Entry.Text.Encrypt(login.OrganizationId);
|
||||
login.Uri = string.IsNullOrWhiteSpace(UriCell.Entry.Text) ? null :
|
||||
UriCell.Entry.Text.Encrypt(login.OrganizationId);
|
||||
login.Username = string.IsNullOrWhiteSpace(UsernameCell.Entry.Text) ? null :
|
||||
UsernameCell.Entry.Text.Encrypt(login.OrganizationId);
|
||||
login.Password = string.IsNullOrWhiteSpace(PasswordCell.Entry.Text) ? null :
|
||||
PasswordCell.Entry.Text.Encrypt(login.OrganizationId);
|
||||
login.Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null :
|
||||
NotesCell.Editor.Text.Encrypt(login.OrganizationId);
|
||||
login.Totp = string.IsNullOrWhiteSpace(TotpCell.Entry.Text) ? null :
|
||||
TotpCell.Entry.Text.Encrypt(login.OrganizationId);
|
||||
login.Favorite = favoriteCell.On;
|
||||
|
||||
if(FolderCell.Picker.SelectedIndex > 0)
|
||||
@@ -226,16 +250,25 @@ namespace Bit.App.Pages
|
||||
UriCell?.InitEvents();
|
||||
NameCell?.InitEvents();
|
||||
NotesCell?.InitEvents();
|
||||
TotpCell?.InitEvents();
|
||||
FolderCell?.InitEvents();
|
||||
|
||||
if(PasswordCell?.Button != null)
|
||||
{
|
||||
PasswordCell.Button.Clicked += PasswordButton_Clicked;
|
||||
}
|
||||
if(TotpCell?.Button != null)
|
||||
{
|
||||
TotpCell.Button.Clicked += TotpButton_Clicked;
|
||||
}
|
||||
if(GenerateCell != null)
|
||||
{
|
||||
GenerateCell.Tapped += GenerateCell_Tapped;
|
||||
}
|
||||
if(AttachmentsCell != null)
|
||||
{
|
||||
AttachmentsCell.Tapped += AttachmentsCell_Tapped;
|
||||
}
|
||||
if(DeleteCell != null)
|
||||
{
|
||||
DeleteCell.Tapped += DeleteCell_Tapped;
|
||||
@@ -246,6 +279,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnDisappearing();
|
||||
PasswordCell?.Dispose();
|
||||
TotpCell?.Dispose();
|
||||
UsernameCell?.Dispose();
|
||||
UriCell?.Dispose();
|
||||
NameCell?.Dispose();
|
||||
@@ -256,10 +290,18 @@ namespace Bit.App.Pages
|
||||
{
|
||||
PasswordCell.Button.Clicked -= PasswordButton_Clicked;
|
||||
}
|
||||
if(TotpCell?.Button != null)
|
||||
{
|
||||
TotpCell.Button.Clicked -= TotpButton_Clicked;
|
||||
}
|
||||
if(GenerateCell != null)
|
||||
{
|
||||
GenerateCell.Tapped -= GenerateCell_Tapped;
|
||||
}
|
||||
if(AttachmentsCell != null)
|
||||
{
|
||||
AttachmentsCell.Tapped -= AttachmentsCell_Tapped;
|
||||
}
|
||||
if(DeleteCell != null)
|
||||
{
|
||||
DeleteCell.Tapped -= DeleteCell_Tapped;
|
||||
@@ -272,6 +314,28 @@ namespace Bit.App.Pages
|
||||
PasswordCell.Button.Image = "eye" + (!PasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty);
|
||||
}
|
||||
|
||||
private async void TotpButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var scanPage = new ScanPage((key) =>
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
if(!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
TotpCell.Entry.Text = key;
|
||||
_userDialogs.Toast(AppResources.AuthenticatorKeyAdded);
|
||||
}
|
||||
else
|
||||
{
|
||||
_userDialogs.Alert(AppResources.AuthenticatorKeyReadError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage));
|
||||
}
|
||||
|
||||
private async void GenerateCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(PasswordCell.Entry.Text)
|
||||
@@ -288,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)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly ILoginService _loginService;
|
||||
private readonly IUserDialogs _userDialogs;
|
||||
private readonly IConnectivity _connectivity;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDeviceActionService _clipboardService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IPushNotification _pushNotification;
|
||||
private readonly IDeviceInfoService _deviceInfoService;
|
||||
@@ -41,7 +41,7 @@ namespace Bit.App.Pages
|
||||
_loginService = Resolver.Resolve<ILoginService>();
|
||||
_connectivity = Resolver.Resolve<IConnectivity>();
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_clipboardService = Resolver.Resolve<IClipboardService>();
|
||||
_clipboardService = Resolver.Resolve<IDeviceActionService>();
|
||||
_syncService = Resolver.Resolve<ISyncService>();
|
||||
_pushNotification = Resolver.Resolve<IPushNotification>();
|
||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
||||
|
||||
@@ -8,6 +8,9 @@ using Xamarin.Forms;
|
||||
using XLabs.Ioc;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using Bit.App.Models;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -16,14 +19,17 @@ namespace Bit.App.Pages
|
||||
private readonly string _loginId;
|
||||
private readonly ILoginService _loginService;
|
||||
private readonly IUserDialogs _userDialogs;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private bool _pageDisappeared = true;
|
||||
|
||||
public VaultViewLoginPage(string loginId)
|
||||
{
|
||||
_loginId = loginId;
|
||||
_loginService = Resolver.Resolve<ILoginService>();
|
||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||
_clipboardService = Resolver.Resolve<IClipboardService>();
|
||||
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
|
||||
_tokenService = Resolver.Resolve<ITokenService>();
|
||||
|
||||
Init();
|
||||
}
|
||||
@@ -32,11 +38,14 @@ namespace Bit.App.Pages
|
||||
private ExtendedTableView Table { get; set; }
|
||||
private TableSection LoginInformationSection { get; set; }
|
||||
private TableSection NotesSection { get; set; }
|
||||
private TableSection AttachmentsSection { get; set; }
|
||||
public LabeledValueCell UsernameCell { get; set; }
|
||||
public LabeledValueCell PasswordCell { get; set; }
|
||||
public LabeledValueCell UriCell { get; set; }
|
||||
public LabeledValueCell NotesCell { get; set; }
|
||||
public LabeledValueCell TotpCodeCell { get; set; }
|
||||
private EditLoginToolBarItem EditItem { get; set; }
|
||||
public List<AttachmentViewCell> AttachmentCells { get; set; }
|
||||
|
||||
private void Init()
|
||||
{
|
||||
@@ -87,6 +96,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
});
|
||||
|
||||
// Totp
|
||||
TotpCodeCell = new LabeledValueCell(AppResources.VerificationCodeTotp, button1Text: AppResources.Copy, subText: "--");
|
||||
TotpCodeCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.TotpCodeFormatted));
|
||||
TotpCodeCell.Value.SetBinding(Label.TextColorProperty, nameof(VaultViewLoginPageModel.TotpColor));
|
||||
TotpCodeCell.Button1.Command = new Command(() => Copy(Model.TotpCode, AppResources.VerificationCodeTotp));
|
||||
TotpCodeCell.Sub.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.TotpSecond));
|
||||
TotpCodeCell.Sub.SetBinding(Label.TextColorProperty, nameof(VaultViewLoginPageModel.TotpColor));
|
||||
TotpCodeCell.Value.FontFamily = Helpers.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier");
|
||||
|
||||
// Notes
|
||||
NotesCell = new LabeledValueCell();
|
||||
NotesCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.Notes));
|
||||
@@ -107,11 +125,10 @@ namespace Bit.App.Pages
|
||||
Intent = TableIntent.Settings,
|
||||
EnableScrolling = true,
|
||||
HasUnevenRows = true,
|
||||
EnableSelection = false,
|
||||
EnableSelection = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
LoginInformationSection,
|
||||
NotesSection
|
||||
LoginInformationSection
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,6 +143,7 @@ namespace Bit.App.Pages
|
||||
PasswordCell.Button1.WidthRequest = 40;
|
||||
PasswordCell.Button2.WidthRequest = 59;
|
||||
UsernameCell.Button1.WidthRequest = 59;
|
||||
TotpCodeCell.Button1.WidthRequest = 59;
|
||||
UriCell.Button1.WidthRequest = 75;
|
||||
}
|
||||
|
||||
@@ -136,6 +154,7 @@ namespace Bit.App.Pages
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
_pageDisappeared = false;
|
||||
NotesCell.Tapped += NotesCell_Tapped;
|
||||
EditItem.InitEvents();
|
||||
|
||||
@@ -148,49 +167,152 @@ 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), () =>
|
||||
{
|
||||
if(_pageDisappeared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TotpTick(totpKey);
|
||||
return true;
|
||||
});
|
||||
|
||||
LoginInformationSection.Add(TotpCodeCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CleanupAttachmentCells();
|
||||
if(Table.Root.Contains(AttachmentsSection))
|
||||
{
|
||||
Table.Root.Remove(AttachmentsSection);
|
||||
}
|
||||
if(Model.ShowAttachments && _tokenService.TokenPremium)
|
||||
{
|
||||
AttachmentsSection = new TableSection(AppResources.Attachments);
|
||||
AttachmentCells = new List<AttachmentViewCell>();
|
||||
foreach(var attachment in Model.Attachments.OrderBy(s => s.Name))
|
||||
{
|
||||
var attachmentCell = new AttachmentViewCell(attachment, async () =>
|
||||
{
|
||||
await OpenAttachmentAsync(login, attachment);
|
||||
});
|
||||
AttachmentCells.Add(attachmentCell);
|
||||
AttachmentsSection.Add(attachmentCell);
|
||||
attachmentCell.InitEvents();
|
||||
}
|
||||
Table.Root.Add(AttachmentsSection);
|
||||
}
|
||||
|
||||
base.OnAppearing();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_pageDisappeared = true;
|
||||
NotesCell.Tapped -= NotesCell_Tapped;
|
||||
EditItem.Dispose();
|
||||
CleanupAttachmentCells();
|
||||
}
|
||||
|
||||
private void CleanupAttachmentCells()
|
||||
{
|
||||
if(AttachmentCells != null)
|
||||
{
|
||||
foreach(var cell in AttachmentCells)
|
||||
{
|
||||
cell.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAttachmentAsync(Login login, VaultViewLoginPageModel.Attachment attachment)
|
||||
{
|
||||
if(!_tokenService.TokenPremium && !login.OrganizationUseTotp)
|
||||
{
|
||||
_userDialogs.Alert(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
// 10 MB warning
|
||||
if(attachment.Size >= 10485760 && !(await _userDialogs.ConfirmAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!_deviceActionService.CanOpenFile(attachment.Name))
|
||||
{
|
||||
await _userDialogs.AlertAsync(AppResources.UnableToOpenFile, null, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
_userDialogs.ShowLoading(AppResources.Downloading, MaskType.Black);
|
||||
var data = await _loginService.DownloadAndDecryptAttachmentAsync(attachment.Url, login.OrganizationId);
|
||||
_userDialogs.HideLoading();
|
||||
if(data == null)
|
||||
{
|
||||
await _userDialogs.AlertAsync(AppResources.UnableToDownloadFile, null, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.Name))
|
||||
{
|
||||
await _userDialogs.AlertAsync(AppResources.UnableToOpenFile, null, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotesCell_Tapped(object sender, EventArgs e)
|
||||
@@ -200,8 +322,20 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Copy(string copyText, string alertLabel)
|
||||
{
|
||||
_clipboardService.CopyToClipboard(copyText);
|
||||
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
|
||||
_deviceActionService.CopyToClipboard(copyText);
|
||||
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
|
||||
}
|
||||
|
||||
private void TotpTick(string totpKey)
|
||||
{
|
||||
var now = Helpers.EpocUtcNow() / 1000;
|
||||
var mod = now % 30;
|
||||
Model.TotpSecond = (int)(30 - mod);
|
||||
|
||||
if(mod == 0)
|
||||
{
|
||||
Model.TotpCode = Crypto.Totp(totpKey);
|
||||
}
|
||||
}
|
||||
|
||||
private class EditLoginToolBarItem : ExtendedToolbarItem
|
||||
@@ -223,5 +357,35 @@ namespace Bit.App.Pages
|
||||
await _page.Navigation.PushForDeviceAsync(page);
|
||||
}
|
||||
}
|
||||
|
||||
public class AttachmentViewCell : LabeledRightDetailCell, IDisposable
|
||||
{
|
||||
private readonly Action _tapped;
|
||||
|
||||
public AttachmentViewCell(VaultViewLoginPageModel.Attachment attachment, Action tappedAction)
|
||||
{
|
||||
_tapped = tappedAction;
|
||||
Label.Text = attachment.Name;
|
||||
Detail.Text = attachment.SizeName;
|
||||
Icon.Source = "download";
|
||||
BackgroundColor = Color.White;
|
||||
Detail.MinimumWidthRequest = 100;
|
||||
}
|
||||
|
||||
public void InitEvents()
|
||||
{
|
||||
Tapped += AttachmentViewCell_Tapped;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Tapped -= AttachmentViewCell_Tapped;
|
||||
}
|
||||
|
||||
private void AttachmentViewCell_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
_tapped?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Api;
|
||||
using Plugin.Connectivity.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Repositories
|
||||
{
|
||||
public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository
|
||||
{
|
||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public AccountsApiRepository(
|
||||
IConnectivity connectivity,
|
||||
IHttpService httpService,
|
||||
@@ -125,7 +124,7 @@ namespace Bit.App.Repositories
|
||||
{
|
||||
return await HandleErrorAsync<DateTime?>(response).ConfigureAwait(false);
|
||||
}
|
||||
return ApiResult<DateTime?>.Success(_epoc.AddMilliseconds(ms), response.StatusCode);
|
||||
return ApiResult<DateTime?>.Success(Helpers.Epoc.AddMilliseconds(ms), response.StatusCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
36
src/App/Repositories/AttachmentRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models.Data;
|
||||
|
||||
namespace Bit.App.Repositories
|
||||
{
|
||||
public class AttachmentRepository : Repository<AttachmentData, string>, IAttachmentRepository
|
||||
{
|
||||
public AttachmentRepository(ISqlService sqlService)
|
||||
: base(sqlService)
|
||||
{ }
|
||||
|
||||
public Task<IEnumerable<AttachmentData>> GetAllByLoginIdAsync(string loginId)
|
||||
{
|
||||
var attachments = Connection.Table<AttachmentData>().Where(a => a.LoginId == loginId).Cast<AttachmentData>();
|
||||
return Task.FromResult(attachments);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<AttachmentData>> GetAllByUserIdAsync(string userId)
|
||||
{
|
||||
var attachments = Connection.Query<AttachmentData>(@"
|
||||
SELECT
|
||||
A.*
|
||||
FROM
|
||||
Attachment AS A
|
||||
INNER JOIN
|
||||
Site AS S ON S.Id = A.LoginId
|
||||
WHERE
|
||||
S.UserId = ?", userId);
|
||||
return Task.FromResult<IEnumerable<AttachmentData>>(attachments);
|
||||
}
|
||||
}
|
||||
}
|
||||